mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-03 08:07:14 +00:00
Windows 兼容性修复 + Dify API 适配 + 企微 WS 加载优化 (#2470)
* fix: Windows fsync requires write access to file descriptor
- _fsync_file: os.O_RDONLY -> os.O_RDWR (Windows _commit() requires write access)
- _fsync_directory: tolerate OSError on fsync and os.open (chmod 0o700 blocks access on Windows)
* fix: Dify SSE empty data lines and upload response format mismatch
- Skip malformed/flushed SSE data lines (JSONDecodeError)
- Accept HTTP 200 besides 201 for file upload response
- Unwrap data wrapper in upload response (resp.get('data', resp))
* feat: WeCom WS mode sends empty initial stream frame for loading spinner
Send an empty reply_stream frame immediately after stream session
creation so the WeCom client shows its built-in loading indicator
while the pipeline processes the message (e.g. RAG retrieval).
* fix(compat): harden Windows, Dify, and WeCom changes
* fix(migration): clean interrupted SQLite temp files
---------
Co-authored-by: Hyu <chenhyu@proton.me>
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import httpx
|
||||
import typing
|
||||
import json
|
||||
import os
|
||||
import typing
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
from .errors import DifyAPIError
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
_MAX_DIFY_RESPONSE_BYTES = 1024 * 1024
|
||||
_MAX_DIFY_SSE_LINE_BYTES = 1024 * 1024
|
||||
@@ -15,6 +16,32 @@ _MAX_DIFY_STREAM_BYTES = 16 * 1024 * 1024
|
||||
_MAX_DIFY_UPLOAD_BYTES = 10 * 1024 * 1024
|
||||
|
||||
|
||||
def _decode_sse_data(line: bytes) -> dict[str, typing.Any] | None:
|
||||
data = line[5:].strip()
|
||||
if not data or data == b'[DONE]':
|
||||
return None
|
||||
try:
|
||||
payload = json.loads(data.decode('utf-8'))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
||||
raise DifyAPIError('Dify SSE data line is not valid JSON') from exc
|
||||
if not isinstance(payload, dict):
|
||||
raise DifyAPIError('Dify SSE event is not a JSON object')
|
||||
return payload
|
||||
|
||||
|
||||
def _decode_upload_response(body: bytes) -> dict[str, typing.Any]:
|
||||
try:
|
||||
response = json.loads(body)
|
||||
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
||||
raise DifyAPIError('Dify upload response is not valid JSON') from exc
|
||||
if not isinstance(response, dict):
|
||||
raise DifyAPIError('Dify upload response is not a JSON object')
|
||||
payload = response.get('data', response)
|
||||
if not isinstance(payload, dict) or not isinstance(payload.get('id'), str) or not payload['id']:
|
||||
raise DifyAPIError('Dify upload response does not contain a valid file id')
|
||||
return payload
|
||||
|
||||
|
||||
async def _read_limited_response(
|
||||
response: httpx.Response,
|
||||
*,
|
||||
@@ -56,16 +83,16 @@ async def _iter_sse_json(
|
||||
line = raw_line.rstrip(b'\r').strip()
|
||||
if not line or not line.startswith(b'data:'):
|
||||
continue
|
||||
payload = json.loads(line[5:].decode('utf-8', errors='replace'))
|
||||
if isinstance(payload, dict):
|
||||
payload = _decode_sse_data(line)
|
||||
if payload is not None:
|
||||
yield payload
|
||||
if len(buffer) > _MAX_DIFY_SSE_LINE_BYTES:
|
||||
raise DifyAPIError('Dify SSE event exceeds the runtime limit')
|
||||
|
||||
line = bytes(buffer).rstrip(b'\r').strip()
|
||||
if line.startswith(b'data:'):
|
||||
payload = json.loads(line[5:].decode('utf-8', errors='replace'))
|
||||
if isinstance(payload, dict):
|
||||
payload = _decode_sse_data(line)
|
||||
if payload is not None:
|
||||
yield payload
|
||||
|
||||
|
||||
@@ -242,7 +269,7 @@ class AsyncDifyServiceClient:
|
||||
file: httpx._types.FileTypes,
|
||||
user: str,
|
||||
timeout: float = 30.0,
|
||||
) -> str:
|
||||
) -> dict[str, typing.Any]:
|
||||
# 处理 Path 对象
|
||||
if isinstance(file, Path):
|
||||
if not file.exists():
|
||||
@@ -271,6 +298,6 @@ class AsyncDifyServiceClient:
|
||||
timeout=timeout,
|
||||
) as response:
|
||||
body = await _read_limited_response(response)
|
||||
if response.status_code != 201:
|
||||
if response.status_code not in (200, 201):
|
||||
raise DifyAPIError(f'{response.status_code} {body.decode(errors="replace")}')
|
||||
return json.loads(body)
|
||||
return _decode_upload_response(body)
|
||||
|
||||
@@ -936,6 +936,13 @@ class WecomBotWsClient:
|
||||
'chat_type': message_data.get('type', 'single'),
|
||||
}
|
||||
self._prune_stream_state()
|
||||
# Send an initial empty stream frame so the WeCom client
|
||||
# shows its built-in loading spinner while the pipeline
|
||||
# processes the message (e.g. RAG retrieval).
|
||||
try:
|
||||
await self.reply_stream(req_id, stream_id, '', finish=False)
|
||||
except Exception:
|
||||
await self.logger.warning(f'Failed to send initial stream frame: {traceback.format_exc()}')
|
||||
message_data['stream_id'] = stream_id
|
||||
message_data['req_id'] = req_id
|
||||
|
||||
|
||||
Reference in New Issue
Block a user