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:
Neos
2026-08-29 22:34:10 +08:00
committed by GitHub
parent bafdaf0033
commit e69a80f5e9
6 changed files with 320 additions and 25 deletions
@@ -44,6 +44,86 @@ def test_webhook_dispatch_tasks_are_bounded():
assert len(client._dispatch_tasks) == 100
@pytest.mark.asyncio
async def test_ws_initial_stream_frame_precedes_pipeline_dispatch(monkeypatch):
from langbot.libs.wecom_ai_bot_api import ws_client as ws_client_module
order = []
logger = types.SimpleNamespace(
debug=Mock(),
error=Mock(),
warning=Mock(),
)
client = WecomBotWsClient('bot-id', 'secret', logger)
async def parse_message(*args, **kwargs):
del args, kwargs
return {'msgid': 'msg-1', 'type': 'single', 'userid': 'user-1'}
async def reply_stream(*args, **kwargs):
del args, kwargs
order.append('initial-frame')
return {}
async def dispatch_event(event):
del event
order.append('pipeline-dispatch')
monkeypatch.setattr(ws_client_module, 'parse_wecom_bot_message', parse_message)
monkeypatch.setattr(ws_client_module.wecombotevent, 'WecomBotEvent', lambda data: data)
client.reply_stream = reply_stream
client._dispatch_event = dispatch_event
await client._handle_message_callback({'headers': {'req_id': 'req-1'}, 'body': {}})
assert order == ['initial-frame', 'pipeline-dispatch']
@pytest.mark.asyncio
async def test_ws_initial_stream_failure_still_dispatches_message(monkeypatch):
from langbot.libs.wecom_ai_bot_api import ws_client as ws_client_module
dispatched = []
class Logger:
def __init__(self):
self.warnings = []
async def debug(self, message):
del message
async def error(self, message):
raise AssertionError(message)
async def warning(self, message):
self.warnings.append(message)
logger = Logger()
client = WecomBotWsClient('bot-id', 'secret', logger)
async def parse_message(*args, **kwargs):
del args, kwargs
return {'msgid': 'msg-1', 'type': 'single', 'userid': 'user-1'}
async def reply_stream(*args, **kwargs):
del args, kwargs
raise ConnectionError('simulated reply failure')
async def dispatch_event(event):
dispatched.append(event)
monkeypatch.setattr(ws_client_module, 'parse_wecom_bot_message', parse_message)
monkeypatch.setattr(ws_client_module.wecombotevent, 'WecomBotEvent', lambda data: data)
client.reply_stream = reply_stream
client._dispatch_event = dispatch_event
await client._handle_message_callback({'headers': {'req_id': 'req-1'}, 'body': {}})
assert len(dispatched) == 1
assert len(logger.warnings) == 1
assert 'simulated reply failure' in logger.warnings[0]
def test_extract_template_card_action_supports_nested_button_key():
task_id, event_key, card_type = extract_template_card_action(
{