@researai/deepscientist 1.5.8 → 1.5.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +186 -21
- package/README.md +108 -95
- package/assets/branding/connector-qq.png +0 -0
- package/assets/branding/connector-rokid.png +0 -0
- package/assets/branding/connector-weixin.png +0 -0
- package/assets/branding/projects.png +0 -0
- package/bin/ds.js +172 -13
- package/docs/assets/branding/projects.png +0 -0
- package/docs/en/00_QUICK_START.md +308 -70
- package/docs/en/01_SETTINGS_REFERENCE.md +3 -0
- package/docs/en/02_START_RESEARCH_GUIDE.md +112 -0
- package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +62 -179
- package/docs/en/09_DOCTOR.md +41 -5
- package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +137 -0
- package/docs/en/11_LICENSE_AND_RISK.md +256 -0
- package/docs/en/12_GUIDED_WORKFLOW_TOUR.md +427 -0
- package/docs/en/13_CORE_ARCHITECTURE_GUIDE.md +297 -0
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
- package/docs/en/99_ACKNOWLEDGEMENTS.md +4 -1
- package/docs/en/README.md +79 -0
- package/docs/images/lingzhu/rokid-agent-platform-create.png +0 -0
- package/docs/images/weixin/weixin-plugin-entry.png +0 -0
- package/docs/images/weixin/weixin-plugin-entry.svg +33 -0
- package/docs/images/weixin/weixin-qr-confirm.svg +30 -0
- package/docs/images/weixin/weixin-quest-media-flow.svg +44 -0
- package/docs/images/weixin/weixin-settings-bind.svg +57 -0
- package/docs/zh/00_QUICK_START.md +315 -74
- package/docs/zh/01_SETTINGS_REFERENCE.md +3 -0
- package/docs/zh/02_START_RESEARCH_GUIDE.md +112 -0
- package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +62 -193
- package/docs/zh/09_DOCTOR.md +41 -5
- package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +144 -0
- package/docs/zh/11_LICENSE_AND_RISK.md +256 -0
- package/docs/zh/12_GUIDED_WORKFLOW_TOUR.md +423 -0
- package/docs/zh/13_CORE_ARCHITECTURE_GUIDE.md +296 -0
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +4 -1
- package/docs/zh/README.md +126 -0
- package/install.sh +0 -34
- package/package.json +3 -3
- package/pyproject.toml +2 -2
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/annotations.py +343 -0
- package/src/deepscientist/artifact/arxiv.py +484 -37
- package/src/deepscientist/artifact/metrics.py +1 -3
- package/src/deepscientist/artifact/service.py +1347 -111
- package/src/deepscientist/arxiv_library.py +275 -0
- package/src/deepscientist/bash_exec/service.py +9 -0
- package/src/deepscientist/bridges/builtins.py +2 -0
- package/src/deepscientist/bridges/connectors.py +447 -0
- package/src/deepscientist/channels/__init__.py +2 -0
- package/src/deepscientist/channels/builtins.py +3 -1
- package/src/deepscientist/channels/qq.py +1 -1
- package/src/deepscientist/channels/qq_gateway.py +1 -1
- package/src/deepscientist/channels/relay.py +7 -1
- package/src/deepscientist/channels/weixin.py +59 -0
- package/src/deepscientist/channels/weixin_ilink.py +317 -0
- package/src/deepscientist/config/models.py +22 -2
- package/src/deepscientist/config/service.py +431 -60
- package/src/deepscientist/connector/__init__.py +4 -0
- package/src/deepscientist/connector/connector_profiles.py +481 -0
- package/src/deepscientist/connector/lingzhu_support.py +668 -0
- package/src/deepscientist/connector/qq_profiles.py +206 -0
- package/src/deepscientist/connector/weixin_support.py +663 -0
- package/src/deepscientist/connector_profiles.py +1 -374
- package/src/deepscientist/connector_runtime.py +2 -0
- package/src/deepscientist/daemon/api/handlers.py +295 -5
- package/src/deepscientist/daemon/api/router.py +16 -1
- package/src/deepscientist/daemon/app.py +1130 -61
- package/src/deepscientist/doctor.py +5 -2
- package/src/deepscientist/gitops/diff.py +120 -29
- package/src/deepscientist/lingzhu_support.py +1 -182
- package/src/deepscientist/mcp/server.py +14 -5
- package/src/deepscientist/prompts/builder.py +29 -1
- package/src/deepscientist/qq_profiles.py +1 -196
- package/src/deepscientist/quest/node_traces.py +152 -2
- package/src/deepscientist/quest/service.py +169 -43
- package/src/deepscientist/quest/stage_views.py +172 -9
- package/src/deepscientist/registries/baseline.py +56 -4
- package/src/deepscientist/runners/codex.py +55 -3
- package/src/deepscientist/weixin_support.py +1 -0
- package/src/prompts/connectors/lingzhu.md +3 -1
- package/src/prompts/connectors/weixin.md +230 -0
- package/src/prompts/system.md +9 -0
- package/src/skills/idea/SKILL.md +16 -0
- package/src/skills/idea/references/literature-survey-template.md +24 -0
- package/src/skills/idea/references/related-work-playbook.md +4 -0
- package/src/skills/idea/references/selection-gate.md +9 -0
- package/src/skills/write/SKILL.md +1 -1
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-m2FNtwbn.js → AiManusChatView-D0mTXG4-.js} +156 -48
- package/src/ui/dist/assets/{AnalysisPlugin-BMTF8EGL.js → AnalysisPlugin-Db0cTXxm.js} +1 -1
- package/src/ui/dist/assets/{CliPlugin-BEOWgxCI.js → CliPlugin-DrV8je02.js} +164 -9
- package/src/ui/dist/assets/{CodeEditorPlugin-BCXvjqmb.js → CodeEditorPlugin-QXMSCH71.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-DaJcy3nD.js → CodeViewerPlugin-7hhtWj_E.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-ByfeIq4K.js → DocViewerPlugin-BWMSnRJe.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-Cksf3VZ-.js → GitDiffViewerPlugin-7J9h9Vy_.js} +20 -21
- package/src/ui/dist/assets/{ImageViewerPlugin-CFz-OsTS.js → ImageViewerPlugin-CHJl_0lr.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-CJ1cJzoX.js → LabCopilotPanel-1qSow1es.js} +11 -11
- package/src/ui/dist/assets/{LabPlugin-BF3dVJwa.js → LabPlugin-eQpPPCEp.js} +2 -1
- package/src/ui/dist/assets/{LatexPlugin-DDkwZ6Sj.js → LatexPlugin-BwRfi89Z.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-HAuvurcT.js → MarkdownViewerPlugin-836PVQWV.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-BtoTYy2C.js → MarketplacePlugin-C2y_556i.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-CSJYx7b-.js → NotebookEditor-BRzJbGsn.js} +12 -12
- package/src/ui/dist/assets/{NotebookEditor-DQgRezm_.js → NotebookEditor-DIX7Mlzu.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-DPa_-fv6.js → PdfLoader-DzRaTAlq.js} +14 -7
- package/src/ui/dist/assets/{PdfMarkdownPlugin-BZpXOEjm.js → PdfMarkdownPlugin-DZUfIUnp.js} +73 -6
- package/src/ui/dist/assets/{PdfViewerPlugin-BT8a6wGR.js → PdfViewerPlugin-BwtICzue.js} +103 -34
- package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +3627 -0
- package/src/ui/dist/assets/{SearchPlugin-D_blveZi.js → SearchPlugin-DHeIAMsx.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-Btx0M3hX.js → TextViewerPlugin-C3tCmFox.js} +5 -4
- package/src/ui/dist/assets/{VNCViewer-DImJO4rO.js → VNCViewer-CQsKVm3t.js} +10 -10
- package/src/ui/dist/assets/bot-BEA2vWuK.js +21 -0
- package/src/ui/dist/assets/branding/logo-rokid.png +0 -0
- package/src/ui/dist/assets/browser-BAcuE0Xj.js +2895 -0
- package/src/ui/dist/assets/{code-BUfXGJSl.js → code-XfbSR8K2.js} +1 -1
- package/src/ui/dist/assets/{file-content-VqamwI3X.js → file-content-BjxNaIfy.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-C_wOoS7a.js → file-diff-panel-D_lLVQk0.js} +1 -1
- package/src/ui/dist/assets/{file-socket-D2bTuMVP.js → file-socket-D9x_5vlY.js} +1 -1
- package/src/ui/dist/assets/{image-BZkGJ4mM.js → image-BhWT33W1.js} +1 -1
- package/src/ui/dist/assets/{index-DdRW6RMJ.js → index--c4iXtuy.js} +12 -12
- package/src/ui/dist/assets/{index-CxkvSeKw.js → index-BDxipwrC.js} +2 -2
- package/src/ui/dist/assets/{index-DjggJovS.js → index-DZTZ8mWP.js} +14934 -9613
- package/src/ui/dist/assets/{index-DXZ1daiJ.css → index-Dqj-Mjb4.css} +2 -13
- package/src/ui/dist/assets/index-PJbSbPTy.js +25 -0
- package/src/ui/dist/assets/{monaco-DHMc7kKM.js → monaco-K8izTGgo.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-DSw_D3RV.js → pdf-effect-queue-DfBors6y.js} +16 -1
- package/src/ui/dist/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
- package/src/ui/dist/assets/{popover-B85oCgCS.js → popover-yFK1J4fL.js} +1 -1
- package/src/ui/dist/assets/{project-sync-DOMCcPac.js → project-sync-PENr2zcz.js} +1 -74
- package/src/ui/dist/assets/select-CAbJDfYv.js +1690 -0
- package/src/ui/dist/assets/{sigma-BO2rQrl3.js → sigma-DEuYJqTl.js} +1 -1
- package/src/ui/dist/assets/{index-D9QIGcmc.js → square-check-big-omoSUmcd.js} +2 -13
- package/src/ui/dist/assets/{trash-BsVEH_dV.js → trash--F119N47.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-b8L6JuZm.js → useCliAccess-D31UR23I.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-BY7uA9hV.js → useFileDiffOverlay-BH6KcMzq.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-BwyVuUIK.js → wrap-text-CZ613PM5.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-RDpLugQP.js → zoom-out-BgDLAv3z.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/src/ui/dist/assets/AutoFigurePlugin-BGxN8Umr.css +0 -3056
- package/src/ui/dist/assets/AutoFigurePlugin-DxPdMUNb.js +0 -8149
- package/src/ui/dist/assets/PdfViewerPlugin-BJXtIwj_.css +0 -260
- package/src/ui/dist/assets/Stepper-DH2k75Vo.js +0 -158
- package/src/ui/dist/assets/bibtex-B-Hqu0Sg.js +0 -189
- package/src/ui/dist/assets/file-utils--zJCPN1i.js +0 -109
- package/src/ui/dist/assets/message-square-FUIPIhU2.js +0 -16
- package/src/ui/dist/assets/pdfjs-DU1YE8WO.js +0 -3
- package/src/ui/dist/assets/tooltip-B1OspAkx.js +0 -108
|
@@ -4,6 +4,7 @@ import base64
|
|
|
4
4
|
import json
|
|
5
5
|
import mimetypes
|
|
6
6
|
import os
|
|
7
|
+
import re
|
|
7
8
|
import shutil
|
|
8
9
|
import subprocess
|
|
9
10
|
import threading
|
|
@@ -16,6 +17,7 @@ from urllib.parse import parse_qs, urlencode, urlparse
|
|
|
16
17
|
from urllib.request import Request
|
|
17
18
|
|
|
18
19
|
from .. import __version__
|
|
20
|
+
from ..annotations import AnnotationService
|
|
19
21
|
from ..artifact import ArtifactService
|
|
20
22
|
from ..bash_exec import BashExecService
|
|
21
23
|
from ..bash_exec.runtime import TerminalClient
|
|
@@ -27,9 +29,10 @@ from ..channels.feishu_long_connection import FeishuLongConnectionService
|
|
|
27
29
|
from ..channels.qq_gateway import QQGatewayService
|
|
28
30
|
from ..channels.slack_socket import SlackSocketModeService
|
|
29
31
|
from ..channels.telegram_polling import TelegramPollingService
|
|
32
|
+
from ..channels.weixin_ilink import WeixinIlinkService
|
|
30
33
|
from ..channels.whatsapp_local_session import WhatsAppLocalSessionService
|
|
31
34
|
from ..cloud import CloudLinkService
|
|
32
|
-
from ..connector_profiles import (
|
|
35
|
+
from ..connector.connector_profiles import (
|
|
33
36
|
CONNECTOR_PROFILE_SPECS,
|
|
34
37
|
PROFILEABLE_CONNECTOR_NAMES,
|
|
35
38
|
connector_profile_label,
|
|
@@ -44,15 +47,36 @@ from ..home import repo_root
|
|
|
44
47
|
from ..memory import MemoryService
|
|
45
48
|
from ..network import urlopen_with_proxy as urlopen
|
|
46
49
|
from ..latex_runtime import QuestLatexService
|
|
50
|
+
from ..connector.lingzhu_support import (
|
|
51
|
+
lingzhu_detect_tool_call_from_text,
|
|
52
|
+
lingzhu_extract_task_text,
|
|
53
|
+
lingzhu_extract_user_text,
|
|
54
|
+
lingzhu_health_payload,
|
|
55
|
+
lingzhu_is_passive_conversation_id,
|
|
56
|
+
lingzhu_passive_conversation_id,
|
|
57
|
+
lingzhu_request_conversation_id,
|
|
58
|
+
lingzhu_request_sender_id,
|
|
59
|
+
lingzhu_sse_answer,
|
|
60
|
+
lingzhu_sse_tool_call,
|
|
61
|
+
lingzhu_surface_action_tool_call,
|
|
62
|
+
lingzhu_verify_auth_header,
|
|
63
|
+
)
|
|
47
64
|
from ..prompts import PromptBuilder
|
|
48
65
|
from ..prompts.builder import STANDARD_SKILLS
|
|
49
|
-
from ..qq_profiles import list_qq_profiles, merge_qq_profile_config, normalize_qq_connector_config
|
|
66
|
+
from ..connector.qq_profiles import list_qq_profiles, merge_qq_profile_config, normalize_qq_connector_config
|
|
50
67
|
from ..quest import QuestService
|
|
51
68
|
from ..runners import CodexRunner, RunRequest, get_runner_factory, register_builtin_runners
|
|
52
69
|
from ..runtime_logs import JsonlLogger
|
|
53
70
|
from ..shared import append_jsonl, ensure_dir, generate_id, read_json, read_jsonl, read_text, resolve_within, run_command, slugify, utc_now, which, write_json
|
|
54
71
|
from ..skills import SkillInstaller
|
|
55
72
|
from ..team import SingleTeamService
|
|
73
|
+
from ..connector.weixin_support import (
|
|
74
|
+
DEFAULT_WEIXIN_BOT_TYPE,
|
|
75
|
+
fetch_weixin_qrcode,
|
|
76
|
+
normalize_weixin_base_url,
|
|
77
|
+
normalize_weixin_cdn_base_url,
|
|
78
|
+
poll_weixin_qrcode_status,
|
|
79
|
+
)
|
|
56
80
|
from .api import ApiHandlers, match_route
|
|
57
81
|
from .sessions import SessionStore
|
|
58
82
|
from websockets.datastructures import Headers
|
|
@@ -70,6 +94,23 @@ CODEX_RETRY_DEFAULT_MAX_BACKOFF_SEC = 1800.0
|
|
|
70
94
|
LEGACY_CODEX_RETRY_INITIAL_BACKOFF_SEC = 1.0
|
|
71
95
|
LEGACY_CODEX_RETRY_BACKOFF_MULTIPLIER = 2.0
|
|
72
96
|
LEGACY_CODEX_RETRY_MAX_BACKOFF_SEC = 8.0
|
|
97
|
+
_LINGZHU_SHORT_COMMAND_DIRECT_MAP = {
|
|
98
|
+
"帮助": "help",
|
|
99
|
+
"列表": "list",
|
|
100
|
+
"状态": "status",
|
|
101
|
+
"总结": "summary",
|
|
102
|
+
"图谱": "graph",
|
|
103
|
+
"指标": "metrics",
|
|
104
|
+
}
|
|
105
|
+
_LINGZHU_SHORT_COMMAND_PREFIX_MAP = {
|
|
106
|
+
"绑定": "use",
|
|
107
|
+
"新建": "new",
|
|
108
|
+
"删除": "delete",
|
|
109
|
+
"暂停": "stop",
|
|
110
|
+
"恢复": "resume",
|
|
111
|
+
}
|
|
112
|
+
_LINGZHU_SHORT_LATEST_ALIASES = {"latest", "newest", "最新"}
|
|
113
|
+
_LINGZHU_DELETE_CONFIRM_ALIASES = {"确认", "强制", "--yes", "-y"}
|
|
73
114
|
|
|
74
115
|
|
|
75
116
|
class DaemonApp:
|
|
@@ -88,6 +129,7 @@ class DaemonApp:
|
|
|
88
129
|
self.quest_service = QuestService(home, skill_installer=self.skill_installer)
|
|
89
130
|
self.latex_service = QuestLatexService(self.quest_service)
|
|
90
131
|
self.memory_service = MemoryService(home)
|
|
132
|
+
self.annotation_service = AnnotationService(home)
|
|
91
133
|
self.artifact_service = ArtifactService(home)
|
|
92
134
|
self.bash_exec_service = BashExecService(home)
|
|
93
135
|
self.team_service = SingleTeamService(home)
|
|
@@ -127,6 +169,7 @@ class DaemonApp:
|
|
|
127
169
|
}
|
|
128
170
|
self.channels = {name: self._create_channel(name) for name in list_channel_names()}
|
|
129
171
|
self.sessions = SessionStore()
|
|
172
|
+
self._canonicalize_lingzhu_binding_state()
|
|
130
173
|
self._turn_lock = threading.Lock()
|
|
131
174
|
self._turn_state: dict[str, dict[str, object]] = {}
|
|
132
175
|
self._server: ThreadingHTTPServer | None = None
|
|
@@ -138,11 +181,13 @@ class DaemonApp:
|
|
|
138
181
|
self._serve_port: int | None = None
|
|
139
182
|
self._shutdown_requested = threading.Event()
|
|
140
183
|
self._qq_gateways: dict[str, QQGatewayService] = {}
|
|
184
|
+
self._weixin_ilink: WeixinIlinkService | None = None
|
|
141
185
|
self._telegram_polling: dict[str, TelegramPollingService] = {}
|
|
142
186
|
self._slack_socket: dict[str, SlackSocketModeService] = {}
|
|
143
187
|
self._discord_gateway: dict[str, DiscordGatewayService] = {}
|
|
144
188
|
self._feishu_long_connection: dict[str, FeishuLongConnectionService] = {}
|
|
145
189
|
self._whatsapp_local_session: dict[str, WhatsAppLocalSessionService] = {}
|
|
190
|
+
self._weixin_login_sessions: dict[str, dict[str, Any]] = {}
|
|
146
191
|
self.handlers = ApiHandlers(self)
|
|
147
192
|
|
|
148
193
|
def list_connector_statuses(self) -> list[dict[str, object]]:
|
|
@@ -150,10 +195,10 @@ class DaemonApp:
|
|
|
150
195
|
items = [
|
|
151
196
|
self._augment_connector_status(channel.status(), title_by_quest=title_by_quest)
|
|
152
197
|
for name, channel in self.channels.items()
|
|
153
|
-
if name == "local" or self._is_connector_system_enabled(name)
|
|
198
|
+
if name == "local" or (name != "lingzhu" and self._is_connector_system_enabled(name))
|
|
154
199
|
]
|
|
155
200
|
lingzhu_config = self.connectors_config.get("lingzhu")
|
|
156
|
-
if isinstance(lingzhu_config, dict)
|
|
201
|
+
if isinstance(lingzhu_config, dict):
|
|
157
202
|
items.append(self._augment_connector_status(self.config_manager.lingzhu_snapshot(lingzhu_config), title_by_quest=title_by_quest))
|
|
158
203
|
return items
|
|
159
204
|
|
|
@@ -858,13 +903,15 @@ class DaemonApp:
|
|
|
858
903
|
return {
|
|
859
904
|
name
|
|
860
905
|
for name in SYSTEM_CONNECTOR_NAMES
|
|
861
|
-
if bool(system_enabled.get(name, name
|
|
906
|
+
if bool(system_enabled.get(name, name in {"qq", "weixin"}))
|
|
862
907
|
}
|
|
863
908
|
|
|
864
909
|
def _is_connector_system_enabled(self, connector_name: str) -> bool:
|
|
865
910
|
normalized = str(connector_name or "").strip().lower()
|
|
866
911
|
if normalized == "local":
|
|
867
912
|
return True
|
|
913
|
+
if normalized == "lingzhu":
|
|
914
|
+
return True
|
|
868
915
|
enabled = self._system_enabled_connector_names()
|
|
869
916
|
if normalized in enabled:
|
|
870
917
|
return True
|
|
@@ -999,10 +1046,13 @@ class DaemonApp:
|
|
|
999
1046
|
preferred_connector_conversation_id: str | None = None,
|
|
1000
1047
|
requested_connector_bindings: list[dict[str, object]] | None = None,
|
|
1001
1048
|
force_connector_rebind: bool = True,
|
|
1049
|
+
auto_bind_latest_connectors: bool = True,
|
|
1002
1050
|
requested_baseline_ref: dict[str, object] | None = None,
|
|
1003
1051
|
startup_contract: dict[str, object] | None = None,
|
|
1004
1052
|
) -> dict:
|
|
1005
1053
|
normalized_requested_bindings = self._normalize_requested_connector_bindings(requested_connector_bindings)
|
|
1054
|
+
if len(normalized_requested_bindings) > 1:
|
|
1055
|
+
raise ValueError("A quest may bind at most one external connector target.")
|
|
1006
1056
|
snapshot = self.quest_service.create(
|
|
1007
1057
|
goal=goal,
|
|
1008
1058
|
title=title,
|
|
@@ -1126,7 +1176,7 @@ class DaemonApp:
|
|
|
1126
1176
|
),
|
|
1127
1177
|
}
|
|
1128
1178
|
)
|
|
1129
|
-
|
|
1179
|
+
elif auto_bind_latest_connectors:
|
|
1130
1180
|
self._auto_bind_connectors_to_latest_quest(
|
|
1131
1181
|
snapshot["quest_id"],
|
|
1132
1182
|
goal=goal,
|
|
@@ -2370,6 +2420,176 @@ class DaemonApp:
|
|
|
2370
2420
|
channel = self._channel_with_bindings(connector_name)
|
|
2371
2421
|
return channel.list_bindings()
|
|
2372
2422
|
|
|
2423
|
+
def start_weixin_login_qr(self, *, force: bool = False) -> dict[str, Any]:
|
|
2424
|
+
connectors = self.config_manager.load_named_normalized("connectors")
|
|
2425
|
+
weixin = connectors.get("weixin") if isinstance(connectors.get("weixin"), dict) else {}
|
|
2426
|
+
base_url = normalize_weixin_base_url(weixin.get("base_url"))
|
|
2427
|
+
bot_type = str(weixin.get("bot_type") or DEFAULT_WEIXIN_BOT_TYPE).strip() or DEFAULT_WEIXIN_BOT_TYPE
|
|
2428
|
+
route_tag = str(weixin.get("route_tag") or "").strip() or None
|
|
2429
|
+
qr_payload = fetch_weixin_qrcode(base_url=base_url, bot_type=bot_type, route_tag=route_tag)
|
|
2430
|
+
qrcode_token = str(qr_payload.get("qrcode") or "").strip()
|
|
2431
|
+
qrcode_content = str(qr_payload.get("qrcode_img_content") or qr_payload.get("url") or "").strip()
|
|
2432
|
+
if not qrcode_token or not qrcode_content:
|
|
2433
|
+
raise RuntimeError("Weixin QR login did not return a valid qrcode token or renderable content.")
|
|
2434
|
+
session_key = generate_id("wxqr")
|
|
2435
|
+
self._weixin_login_sessions[session_key] = {
|
|
2436
|
+
"session_key": session_key,
|
|
2437
|
+
"qrcode": qrcode_token,
|
|
2438
|
+
"qrcode_content": qrcode_content,
|
|
2439
|
+
"base_url": base_url,
|
|
2440
|
+
"bot_type": bot_type,
|
|
2441
|
+
"route_tag": route_tag,
|
|
2442
|
+
"started_at": time.time(),
|
|
2443
|
+
"refresh_count": 0,
|
|
2444
|
+
"force": bool(force),
|
|
2445
|
+
}
|
|
2446
|
+
return {
|
|
2447
|
+
"ok": True,
|
|
2448
|
+
"session_key": session_key,
|
|
2449
|
+
"qrcode_content": qrcode_content,
|
|
2450
|
+
"qrcode_url": qrcode_content,
|
|
2451
|
+
"message": "Weixin QR code is ready. Scan it with WeChat to connect DeepScientist.",
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
def wait_weixin_login_qr(self, *, session_key: str, timeout_ms: int = 1_500) -> dict[str, Any]:
|
|
2455
|
+
normalized_session_key = str(session_key or "").strip()
|
|
2456
|
+
if not normalized_session_key:
|
|
2457
|
+
return {
|
|
2458
|
+
"ok": False,
|
|
2459
|
+
"connected": False,
|
|
2460
|
+
"message": "Weixin QR session key is required.",
|
|
2461
|
+
}
|
|
2462
|
+
session = self._weixin_login_sessions.get(normalized_session_key)
|
|
2463
|
+
if not isinstance(session, dict):
|
|
2464
|
+
return {
|
|
2465
|
+
"ok": False,
|
|
2466
|
+
"connected": False,
|
|
2467
|
+
"message": "Weixin QR session was not found. Start a new login first.",
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
deadline = time.time() + max(int(timeout_ms or 1_500), 500) / 1000.0
|
|
2471
|
+
while time.time() < deadline:
|
|
2472
|
+
remaining = max(deadline - time.time(), 1.0)
|
|
2473
|
+
try:
|
|
2474
|
+
status = poll_weixin_qrcode_status(
|
|
2475
|
+
base_url=str(session.get("base_url") or ""),
|
|
2476
|
+
qrcode=str(session.get("qrcode") or ""),
|
|
2477
|
+
route_tag=str(session.get("route_tag") or "").strip() or None,
|
|
2478
|
+
timeout=min(remaining, 35.0),
|
|
2479
|
+
)
|
|
2480
|
+
except Exception as exc:
|
|
2481
|
+
message = str(exc or "").strip().lower()
|
|
2482
|
+
if isinstance(exc, TimeoutError) or "timed out" in message or "timeout" in message:
|
|
2483
|
+
break
|
|
2484
|
+
raise
|
|
2485
|
+
state = str(status.get("status") or "wait").strip().lower() or "wait"
|
|
2486
|
+
session["status"] = state
|
|
2487
|
+
if state == "confirmed":
|
|
2488
|
+
return self._persist_weixin_login_session(session, status)
|
|
2489
|
+
if state == "expired":
|
|
2490
|
+
refreshed = self._refresh_weixin_login_session(session)
|
|
2491
|
+
return {
|
|
2492
|
+
"ok": True,
|
|
2493
|
+
"connected": False,
|
|
2494
|
+
"status": "expired",
|
|
2495
|
+
"session_key": normalized_session_key,
|
|
2496
|
+
"qrcode_content": refreshed.get("qrcode_content"),
|
|
2497
|
+
"qrcode_url": refreshed.get("qrcode_content"),
|
|
2498
|
+
"message": "Weixin QR code expired and was refreshed automatically.",
|
|
2499
|
+
}
|
|
2500
|
+
if state in {"scaned", "scanned"}:
|
|
2501
|
+
return {
|
|
2502
|
+
"ok": True,
|
|
2503
|
+
"connected": False,
|
|
2504
|
+
"status": "scaned",
|
|
2505
|
+
"session_key": normalized_session_key,
|
|
2506
|
+
"qrcode_content": str(session.get("qrcode_content") or "").strip() or None,
|
|
2507
|
+
"qrcode_url": str(session.get("qrcode_content") or "").strip() or None,
|
|
2508
|
+
"message": "QR code scanned. Confirm the login inside WeChat.",
|
|
2509
|
+
}
|
|
2510
|
+
return {
|
|
2511
|
+
"ok": True,
|
|
2512
|
+
"connected": False,
|
|
2513
|
+
"status": str(session.get("status") or "wait").strip() or "wait",
|
|
2514
|
+
"session_key": normalized_session_key,
|
|
2515
|
+
"qrcode_content": str(session.get("qrcode_content") or "").strip() or None,
|
|
2516
|
+
"qrcode_url": str(session.get("qrcode_content") or "").strip() or None,
|
|
2517
|
+
"message": "Waiting for Weixin QR confirmation.",
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
def _refresh_weixin_login_session(self, session: dict[str, Any]) -> dict[str, Any]:
|
|
2521
|
+
qr_payload = fetch_weixin_qrcode(
|
|
2522
|
+
base_url=str(session.get("base_url") or ""),
|
|
2523
|
+
bot_type=str(session.get("bot_type") or DEFAULT_WEIXIN_BOT_TYPE),
|
|
2524
|
+
route_tag=str(session.get("route_tag") or "").strip() or None,
|
|
2525
|
+
)
|
|
2526
|
+
session["qrcode"] = str(qr_payload.get("qrcode") or "").strip()
|
|
2527
|
+
session["qrcode_content"] = str(qr_payload.get("qrcode_img_content") or qr_payload.get("url") or "").strip()
|
|
2528
|
+
session["started_at"] = time.time()
|
|
2529
|
+
session["refresh_count"] = int(session.get("refresh_count") or 0) + 1
|
|
2530
|
+
return session
|
|
2531
|
+
|
|
2532
|
+
def _persist_weixin_login_session(self, session: dict[str, Any], status: dict[str, Any]) -> dict[str, Any]:
|
|
2533
|
+
bot_token = str(status.get("bot_token") or "").strip()
|
|
2534
|
+
account_id = str(status.get("ilink_bot_id") or "").strip()
|
|
2535
|
+
login_user_id = str(status.get("ilink_user_id") or "").strip() or None
|
|
2536
|
+
if not bot_token or not account_id:
|
|
2537
|
+
return {
|
|
2538
|
+
"ok": False,
|
|
2539
|
+
"connected": False,
|
|
2540
|
+
"status": "confirmed",
|
|
2541
|
+
"message": "Weixin QR login confirmed, but the platform did not return `bot_token` or `ilink_bot_id`.",
|
|
2542
|
+
}
|
|
2543
|
+
connectors = self.config_manager.load_named_normalized("connectors")
|
|
2544
|
+
weixin = connectors.get("weixin") if isinstance(connectors.get("weixin"), dict) else {}
|
|
2545
|
+
weixin.update(
|
|
2546
|
+
{
|
|
2547
|
+
"enabled": True,
|
|
2548
|
+
"transport": "ilink_long_poll",
|
|
2549
|
+
"base_url": normalize_weixin_base_url(status.get("baseurl") or session.get("base_url")),
|
|
2550
|
+
"cdn_base_url": normalize_weixin_cdn_base_url(weixin.get("cdn_base_url")),
|
|
2551
|
+
"bot_type": str(session.get("bot_type") or DEFAULT_WEIXIN_BOT_TYPE),
|
|
2552
|
+
"bot_token": bot_token,
|
|
2553
|
+
"account_id": account_id,
|
|
2554
|
+
"login_user_id": login_user_id,
|
|
2555
|
+
}
|
|
2556
|
+
)
|
|
2557
|
+
connectors["weixin"] = weixin
|
|
2558
|
+
save_result = self.config_manager.save_named_payload("connectors", connectors)
|
|
2559
|
+
if not bool(save_result.get("ok")):
|
|
2560
|
+
self.logger.log(
|
|
2561
|
+
"warning",
|
|
2562
|
+
"connector.weixin_qr_persist_failed",
|
|
2563
|
+
session_key=str(session.get("session_key") or ""),
|
|
2564
|
+
account_id=account_id,
|
|
2565
|
+
errors=save_result.get("errors") or [],
|
|
2566
|
+
warnings=save_result.get("warnings") or [],
|
|
2567
|
+
)
|
|
2568
|
+
return {
|
|
2569
|
+
"ok": False,
|
|
2570
|
+
"connected": False,
|
|
2571
|
+
"status": "confirmed",
|
|
2572
|
+
"errors": save_result.get("errors") or [],
|
|
2573
|
+
"warnings": save_result.get("warnings") or [],
|
|
2574
|
+
"message": "Weixin login succeeded, but DeepScientist could not persist the connector config.",
|
|
2575
|
+
}
|
|
2576
|
+
self.reload_connectors_config()
|
|
2577
|
+
self._weixin_login_sessions.pop(str(session.get("session_key") or ""), None)
|
|
2578
|
+
snapshot = next(
|
|
2579
|
+
(item for item in self.list_connector_statuses() if str(item.get("name") or "").strip().lower() == "weixin"),
|
|
2580
|
+
None,
|
|
2581
|
+
)
|
|
2582
|
+
return {
|
|
2583
|
+
"ok": True,
|
|
2584
|
+
"connected": True,
|
|
2585
|
+
"status": "confirmed",
|
|
2586
|
+
"account_id": account_id,
|
|
2587
|
+
"login_user_id": login_user_id,
|
|
2588
|
+
"base_url": str(weixin.get("base_url") or "").strip() or None,
|
|
2589
|
+
"snapshot": snapshot,
|
|
2590
|
+
"message": "Weixin login succeeded and the connector config was saved.",
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2373
2593
|
def delete_connector_profile(self, connector_name: str, profile_id: str) -> dict | tuple[int, dict]:
|
|
2374
2594
|
normalized_connector = str(connector_name or "").strip().lower()
|
|
2375
2595
|
normalized_profile_id = str(profile_id or "").strip()
|
|
@@ -2701,8 +2921,9 @@ class DaemonApp:
|
|
|
2701
2921
|
connector_name = str(parsed.get("connector") or "").strip().lower()
|
|
2702
2922
|
if not connector_name or connector_name == "local" or connector_name not in self.channels:
|
|
2703
2923
|
return 400, {"ok": False, "message": f"Unknown connector `{connector_name}` for conversation `{normalized}`."}
|
|
2924
|
+
binding_conversation_id = self._logical_connector_binding_conversation(connector_name, normalized)
|
|
2704
2925
|
channel = self._channel_with_bindings(connector_name)
|
|
2705
|
-
conflicts = self._inspect_connector_binding_conflicts(quest_id,
|
|
2926
|
+
conflicts = self._inspect_connector_binding_conflicts(quest_id, binding_conversation_id)
|
|
2706
2927
|
if conflicts and not force:
|
|
2707
2928
|
return 409, {
|
|
2708
2929
|
"ok": False,
|
|
@@ -2710,23 +2931,23 @@ class DaemonApp:
|
|
|
2710
2931
|
"message": "Conversation is already bound to another quest.",
|
|
2711
2932
|
"quest_id": quest_id,
|
|
2712
2933
|
"connector": connector_name,
|
|
2713
|
-
"conversation_id":
|
|
2934
|
+
"conversation_id": binding_conversation_id,
|
|
2714
2935
|
"conflicts": conflicts,
|
|
2715
2936
|
}
|
|
2716
|
-
existing_bound = channel.resolve_bound_quest(
|
|
2937
|
+
existing_bound = channel.resolve_bound_quest(binding_conversation_id)
|
|
2717
2938
|
for item in conflicts:
|
|
2718
2939
|
other_id = str(item.get("quest_id") or "").strip()
|
|
2719
2940
|
if other_id and other_id != quest_id:
|
|
2720
|
-
self.quest_service.unbind_source(other_id,
|
|
2721
|
-
self.sessions.unbind(other_id,
|
|
2722
|
-
channel.bind_conversation(
|
|
2723
|
-
self.sessions.bind(quest_id,
|
|
2941
|
+
self.quest_service.unbind_source(other_id, binding_conversation_id)
|
|
2942
|
+
self.sessions.unbind(other_id, binding_conversation_id)
|
|
2943
|
+
channel.bind_conversation(binding_conversation_id, quest_id)
|
|
2944
|
+
self.sessions.bind(quest_id, binding_conversation_id)
|
|
2724
2945
|
self.quest_service.bind_source(quest_id, "local:default")
|
|
2725
|
-
self.quest_service.bind_source(quest_id,
|
|
2946
|
+
self.quest_service.bind_source(quest_id, binding_conversation_id)
|
|
2726
2947
|
if clear_scope == "all_external":
|
|
2727
|
-
removed = self._unbind_external_bindings(quest_id, preserve={
|
|
2948
|
+
removed = self._unbind_external_bindings(quest_id, preserve={binding_conversation_id})
|
|
2728
2949
|
elif clear_scope == "connector":
|
|
2729
|
-
removed = self._unbind_quest_connector_bindings(quest_id, connector_name, preserve={
|
|
2950
|
+
removed = self._unbind_quest_connector_bindings(quest_id, connector_name, preserve={binding_conversation_id})
|
|
2730
2951
|
else:
|
|
2731
2952
|
removed = []
|
|
2732
2953
|
snapshot = self.quest_service.snapshot(quest_id)
|
|
@@ -2743,7 +2964,7 @@ class DaemonApp:
|
|
|
2743
2964
|
"ok": True,
|
|
2744
2965
|
"quest_id": quest_id,
|
|
2745
2966
|
"connector": connector_name,
|
|
2746
|
-
"conversation_id":
|
|
2967
|
+
"conversation_id": binding_conversation_id,
|
|
2747
2968
|
"snapshot": snapshot,
|
|
2748
2969
|
"removed_conversations": removed,
|
|
2749
2970
|
"conflicts_resolved": [item.get("quest_id") for item in conflicts if item.get("quest_id")],
|
|
@@ -2790,7 +3011,7 @@ class DaemonApp:
|
|
|
2790
3011
|
"message": f"Conversation `{normalized}` does not belong to connector `{normalized_connector}`.",
|
|
2791
3012
|
}
|
|
2792
3013
|
|
|
2793
|
-
return self._apply_conversation_binding(quest_id, normalized, force=force, clear_scope="
|
|
3014
|
+
return self._apply_conversation_binding(quest_id, normalized, force=force, clear_scope="all_external")
|
|
2794
3015
|
|
|
2795
3016
|
def update_quest_bindings(
|
|
2796
3017
|
self,
|
|
@@ -2803,6 +3024,12 @@ class DaemonApp:
|
|
|
2803
3024
|
if not quest_root.joinpath("quest.yaml").exists():
|
|
2804
3025
|
return 404, {"ok": False, "message": f"Unknown quest `{quest_id}`."}
|
|
2805
3026
|
normalized_bindings = self._normalize_requested_connector_bindings(requested_bindings)
|
|
3027
|
+
if len(normalized_bindings) > 1:
|
|
3028
|
+
return 400, {
|
|
3029
|
+
"ok": False,
|
|
3030
|
+
"message": "A quest may bind at most one external connector target.",
|
|
3031
|
+
"quest_id": quest_id,
|
|
3032
|
+
}
|
|
2806
3033
|
conflicts = self.preview_connector_binding_conflicts(normalized_bindings, quest_id=quest_id)
|
|
2807
3034
|
if conflicts and not force:
|
|
2808
3035
|
return 409, {
|
|
@@ -2959,12 +3186,22 @@ class DaemonApp:
|
|
|
2959
3186
|
channel = self._channel_with_bindings(connector_name)
|
|
2960
3187
|
connector_label = self._connector_label(connector_name)
|
|
2961
3188
|
conversation_id = str(message.get("conversation_id") or "")
|
|
3189
|
+
binding_conversation_id = self._logical_connector_binding_conversation(connector_name, conversation_id)
|
|
2962
3190
|
text = str(message.get("text") or "").strip()
|
|
2963
3191
|
command_prefix = channel.command_prefix()
|
|
2964
3192
|
quest_id = channel.resolve_bound_quest(conversation_id)
|
|
2965
|
-
|
|
3193
|
+
if quest_id is None and str(connector_name or "").strip().lower() == "lingzhu":
|
|
3194
|
+
quest_id = self._resolve_lingzhu_bound_quest(conversation_id)
|
|
3195
|
+
command_name = ""
|
|
3196
|
+
args: list[str] = []
|
|
2966
3197
|
if text.startswith(command_prefix):
|
|
2967
3198
|
command_name, args = self._parse_prefixed_command(text, command_prefix)
|
|
3199
|
+
elif str(connector_name or "").strip().lower() == "lingzhu":
|
|
3200
|
+
parsed_lingzhu_command = self._parse_lingzhu_short_command(text)
|
|
3201
|
+
if parsed_lingzhu_command is not None:
|
|
3202
|
+
command_name, args = parsed_lingzhu_command
|
|
3203
|
+
|
|
3204
|
+
if command_name:
|
|
2968
3205
|
if command_name == "help":
|
|
2969
3206
|
return channel.send(
|
|
2970
3207
|
{
|
|
@@ -3001,7 +3238,7 @@ class DaemonApp:
|
|
|
3001
3238
|
announce_connector_binding=True,
|
|
3002
3239
|
exclude_conversation_id=conversation_id,
|
|
3003
3240
|
)
|
|
3004
|
-
self.update_quest_binding(created["quest_id"],
|
|
3241
|
+
self.update_quest_binding(created["quest_id"], binding_conversation_id, force=True)
|
|
3005
3242
|
self.submit_user_message(
|
|
3006
3243
|
created["quest_id"],
|
|
3007
3244
|
text=goal_text,
|
|
@@ -3051,7 +3288,23 @@ class DaemonApp:
|
|
|
3051
3288
|
),
|
|
3052
3289
|
}
|
|
3053
3290
|
)
|
|
3054
|
-
self.
|
|
3291
|
+
previous_external = self._quest_external_binding(target_quest)
|
|
3292
|
+
binding_result = self.update_quest_binding(target_quest, binding_conversation_id, force=True)
|
|
3293
|
+
if isinstance(binding_result, tuple):
|
|
3294
|
+
_status, payload = binding_result
|
|
3295
|
+
return channel.send(
|
|
3296
|
+
{
|
|
3297
|
+
"conversation_id": conversation_id,
|
|
3298
|
+
"kind": "ack",
|
|
3299
|
+
"message": str(payload.get("message") or "Unable to switch connector binding."),
|
|
3300
|
+
}
|
|
3301
|
+
)
|
|
3302
|
+
transition = self._binding_transition_summary(
|
|
3303
|
+
quest_id=target_quest,
|
|
3304
|
+
previous_conversation_id=previous_external,
|
|
3305
|
+
current_conversation_id=self._quest_external_binding(target_quest),
|
|
3306
|
+
)
|
|
3307
|
+
self._announce_binding_transition(transition, notify_new=False, notify_old=True)
|
|
3055
3308
|
return channel.send(
|
|
3056
3309
|
{
|
|
3057
3310
|
"conversation_id": conversation_id,
|
|
@@ -3139,9 +3392,22 @@ class DaemonApp:
|
|
|
3139
3392
|
}
|
|
3140
3393
|
)
|
|
3141
3394
|
|
|
3142
|
-
if quest_id is None and command_name
|
|
3395
|
+
if quest_id is None and command_name not in {"help", "projects", "quests", "list", "new", "use", "delete"}:
|
|
3143
3396
|
auto_bound = self._maybe_auto_bind_connector_conversation(connector_name, conversation_id)
|
|
3144
3397
|
if auto_bound is not None:
|
|
3398
|
+
if bool(auto_bound.get("blocked")):
|
|
3399
|
+
return channel.send(
|
|
3400
|
+
{
|
|
3401
|
+
"conversation_id": conversation_id,
|
|
3402
|
+
"kind": "ack",
|
|
3403
|
+
"message": self._connector_switch_required_message(
|
|
3404
|
+
connector_name=connector_name,
|
|
3405
|
+
quest_id=str(auto_bound.get("quest_id") or "").strip(),
|
|
3406
|
+
current_conversation_id=str(auto_bound.get("current_conversation_id") or "").strip(),
|
|
3407
|
+
requested_conversation_id=str(auto_bound.get("requested_conversation_id") or "").strip(),
|
|
3408
|
+
),
|
|
3409
|
+
}
|
|
3410
|
+
)
|
|
3145
3411
|
quest_id = str(auto_bound.get("quest_id") or "").strip() or None
|
|
3146
3412
|
|
|
3147
3413
|
if command_name in {"stop", "resume"}:
|
|
@@ -3172,7 +3438,7 @@ class DaemonApp:
|
|
|
3172
3438
|
target_quest_id = str(target_quest_id or "").strip()
|
|
3173
3439
|
bound_quest_id = str(channel.resolve_bound_quest(conversation_id) or "").strip() or None
|
|
3174
3440
|
self.sessions.bind(target_quest_id, conversation_id)
|
|
3175
|
-
self.quest_service.bind_source(target_quest_id,
|
|
3441
|
+
self.quest_service.bind_source(target_quest_id, binding_conversation_id)
|
|
3176
3442
|
result = self.control_quest(
|
|
3177
3443
|
target_quest_id,
|
|
3178
3444
|
action=command_name,
|
|
@@ -3197,7 +3463,7 @@ class DaemonApp:
|
|
|
3197
3463
|
)
|
|
3198
3464
|
|
|
3199
3465
|
self.sessions.bind(quest_id, conversation_id)
|
|
3200
|
-
self.quest_service.bind_source(quest_id,
|
|
3466
|
+
self.quest_service.bind_source(quest_id, binding_conversation_id)
|
|
3201
3467
|
if command_name == "status":
|
|
3202
3468
|
snapshot = self.quest_service.snapshot(quest_id)
|
|
3203
3469
|
return channel.send(
|
|
@@ -3370,6 +3636,19 @@ class DaemonApp:
|
|
|
3370
3636
|
if quest_id is None:
|
|
3371
3637
|
auto_bound = self._maybe_auto_bind_connector_conversation(connector_name, conversation_id)
|
|
3372
3638
|
if auto_bound is not None:
|
|
3639
|
+
if bool(auto_bound.get("blocked")):
|
|
3640
|
+
return channel.send(
|
|
3641
|
+
{
|
|
3642
|
+
"conversation_id": conversation_id,
|
|
3643
|
+
"kind": "ack",
|
|
3644
|
+
"message": self._connector_switch_required_message(
|
|
3645
|
+
connector_name=connector_name,
|
|
3646
|
+
quest_id=str(auto_bound.get("quest_id") or "").strip(),
|
|
3647
|
+
current_conversation_id=str(auto_bound.get("current_conversation_id") or "").strip(),
|
|
3648
|
+
requested_conversation_id=str(auto_bound.get("requested_conversation_id") or "").strip(),
|
|
3649
|
+
),
|
|
3650
|
+
}
|
|
3651
|
+
)
|
|
3373
3652
|
quest_id = str(auto_bound.get("quest_id") or "").strip() or None
|
|
3374
3653
|
|
|
3375
3654
|
if quest_id is None:
|
|
@@ -3382,6 +3661,7 @@ class DaemonApp:
|
|
|
3382
3661
|
)
|
|
3383
3662
|
|
|
3384
3663
|
self.sessions.bind(quest_id, conversation_id)
|
|
3664
|
+
self.quest_service.bind_source(quest_id, binding_conversation_id)
|
|
3385
3665
|
materialized_attachments = self._materialize_connector_attachments(
|
|
3386
3666
|
quest_id=quest_id,
|
|
3387
3667
|
connector_name=connector_name,
|
|
@@ -3500,13 +3780,35 @@ class DaemonApp:
|
|
|
3500
3780
|
name = str(resolved.get("name") or "").strip()
|
|
3501
3781
|
content_type = str(resolved.get("content_type") or "").strip()
|
|
3502
3782
|
url = str(resolved.get("url") or "").strip()
|
|
3783
|
+
path = str(resolved.get("path") or "").strip()
|
|
3503
3784
|
target_path = batch_root / self._connector_attachment_filename(index=index, name=name, content_type=content_type)
|
|
3504
3785
|
resolved["manifest_path"] = str(batch_root / "manifest.json")
|
|
3505
3786
|
resolved["batch_path"] = str(batch_root)
|
|
3506
|
-
if
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3787
|
+
if path:
|
|
3788
|
+
try:
|
|
3789
|
+
source_path = Path(path).expanduser()
|
|
3790
|
+
if not source_path.is_absolute():
|
|
3791
|
+
source_path = (quest_root / source_path).resolve()
|
|
3792
|
+
else:
|
|
3793
|
+
source_path = source_path.resolve()
|
|
3794
|
+
if not source_path.exists():
|
|
3795
|
+
raise FileNotFoundError(f"attachment local path does not exist: {source_path}")
|
|
3796
|
+
size_bytes = self._copy_connector_attachment(
|
|
3797
|
+
source_path=source_path,
|
|
3798
|
+
target_path=target_path,
|
|
3799
|
+
)
|
|
3800
|
+
resolved["path"] = str(target_path)
|
|
3801
|
+
resolved["source_path"] = str(source_path)
|
|
3802
|
+
resolved["quest_relative_path"] = str(target_path.relative_to(quest_root))
|
|
3803
|
+
resolved["size_bytes"] = int(size_bytes)
|
|
3804
|
+
resolved["materialized"] = True
|
|
3805
|
+
resolved["downloaded_at"] = utc_now()
|
|
3806
|
+
return resolved
|
|
3807
|
+
except Exception as exc:
|
|
3808
|
+
if not url:
|
|
3809
|
+
resolved["materialized"] = False
|
|
3810
|
+
resolved["download_error"] = str(exc)
|
|
3811
|
+
return resolved
|
|
3510
3812
|
try:
|
|
3511
3813
|
size_bytes = self._download_connector_attachment(
|
|
3512
3814
|
connector_name=connector_name,
|
|
@@ -3526,6 +3828,28 @@ class DaemonApp:
|
|
|
3526
3828
|
resolved["download_error"] = str(exc)
|
|
3527
3829
|
return resolved
|
|
3528
3830
|
|
|
3831
|
+
def _copy_connector_attachment(
|
|
3832
|
+
self,
|
|
3833
|
+
*,
|
|
3834
|
+
source_path: Path,
|
|
3835
|
+
target_path: Path,
|
|
3836
|
+
) -> int:
|
|
3837
|
+
ensure_dir(target_path.parent)
|
|
3838
|
+
total = 0
|
|
3839
|
+
with source_path.open("rb") as source_handle:
|
|
3840
|
+
with target_path.open("wb") as target_handle:
|
|
3841
|
+
while True:
|
|
3842
|
+
chunk = source_handle.read(65536)
|
|
3843
|
+
if not chunk:
|
|
3844
|
+
break
|
|
3845
|
+
total += len(chunk)
|
|
3846
|
+
if total > self._MAX_INBOUND_ATTACHMENT_BYTES:
|
|
3847
|
+
raise ValueError(
|
|
3848
|
+
f"attachment exceeds max inbound size limit ({self._MAX_INBOUND_ATTACHMENT_BYTES} bytes)"
|
|
3849
|
+
)
|
|
3850
|
+
target_handle.write(chunk)
|
|
3851
|
+
return total
|
|
3852
|
+
|
|
3529
3853
|
def _download_connector_attachment(
|
|
3530
3854
|
self,
|
|
3531
3855
|
*,
|
|
@@ -3611,37 +3935,37 @@ class DaemonApp:
|
|
|
3611
3935
|
if not candidates:
|
|
3612
3936
|
return []
|
|
3613
3937
|
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
return
|
|
3938
|
+
preferred_conversation_id = str(self.connector_availability_summary().get("preferred_conversation_id") or "").strip()
|
|
3939
|
+
selected: tuple[str, str] | None = None
|
|
3940
|
+
if preferred_conversation_id:
|
|
3941
|
+
for item in candidates:
|
|
3942
|
+
if conversation_identity_key(item[1]) == conversation_identity_key(preferred_conversation_id):
|
|
3943
|
+
selected = item
|
|
3944
|
+
break
|
|
3945
|
+
if selected is None:
|
|
3946
|
+
selected = candidates[0]
|
|
3947
|
+
|
|
3948
|
+
connector_name, conversation_id = selected
|
|
3949
|
+
result = self._apply_conversation_binding(quest_id, conversation_id, force=True, clear_scope="none")
|
|
3950
|
+
if isinstance(result, tuple):
|
|
3951
|
+
return []
|
|
3952
|
+
bound_conversation = str(result.get("conversation_id") or "").strip() or conversation_id
|
|
3953
|
+
if announce:
|
|
3954
|
+
channel = self._channel_with_bindings(connector_name)
|
|
3955
|
+
channel.send(
|
|
3956
|
+
{
|
|
3957
|
+
"conversation_id": bound_conversation,
|
|
3958
|
+
"quest_id": quest_id,
|
|
3959
|
+
"kind": "ack",
|
|
3960
|
+
"message": self._quest_created_connector_message(
|
|
3961
|
+
connector_name,
|
|
3962
|
+
quest_id=quest_id,
|
|
3963
|
+
goal=goal,
|
|
3964
|
+
previous_quest_id=str(result.get("previous_quest_id") or "").strip() or None,
|
|
3965
|
+
),
|
|
3966
|
+
}
|
|
3967
|
+
)
|
|
3968
|
+
return [bound_conversation]
|
|
3645
3969
|
|
|
3646
3970
|
def _latest_connector_conversation_id(self, connector_name: str) -> str:
|
|
3647
3971
|
candidates = self._latest_connector_conversation_ids(connector_name)
|
|
@@ -3801,12 +4125,24 @@ class DaemonApp:
|
|
|
3801
4125
|
latest_quest_id = self._latest_quest_id()
|
|
3802
4126
|
if latest_quest_id is None:
|
|
3803
4127
|
return None
|
|
3804
|
-
|
|
4128
|
+
normalized_conversation_id = self._logical_connector_binding_conversation(connector_name, conversation_id)
|
|
4129
|
+
current_external = self._quest_external_binding(latest_quest_id)
|
|
4130
|
+
if current_external and conversation_identity_key(current_external) != conversation_identity_key(normalized_conversation_id):
|
|
4131
|
+
return {
|
|
4132
|
+
"ok": False,
|
|
4133
|
+
"blocked": True,
|
|
4134
|
+
"quest_id": latest_quest_id,
|
|
4135
|
+
"current_conversation_id": current_external,
|
|
4136
|
+
"requested_conversation_id": normalized_conversation_id,
|
|
4137
|
+
}
|
|
4138
|
+
result = self.update_quest_binding(latest_quest_id, normalized_conversation_id, force=True)
|
|
3805
4139
|
if isinstance(result, tuple):
|
|
3806
4140
|
return None
|
|
3807
4141
|
return result
|
|
3808
4142
|
|
|
3809
4143
|
def _connector_home_help(self, connector_name: str, *, message: dict) -> str:
|
|
4144
|
+
if str(connector_name or "").strip().lower() == "lingzhu":
|
|
4145
|
+
return self._with_qq_main_chat_notice(message, self._lingzhu_unbound_help_text())
|
|
3810
4146
|
quests = self.quest_service.list_quests()
|
|
3811
4147
|
latest = str(quests[0]["quest_id"]) if quests else "none"
|
|
3812
4148
|
body = self._polite_copy(
|
|
@@ -3839,6 +4175,241 @@ class DaemonApp:
|
|
|
3839
4175
|
)
|
|
3840
4176
|
return self._with_qq_main_chat_notice(message, body)
|
|
3841
4177
|
|
|
4178
|
+
def _quest_external_binding(self, quest_id: str | None) -> str | None:
|
|
4179
|
+
normalized_quest_id = str(quest_id or "").strip()
|
|
4180
|
+
if not normalized_quest_id:
|
|
4181
|
+
return None
|
|
4182
|
+
for source in self.quest_service.binding_sources(normalized_quest_id):
|
|
4183
|
+
parsed = parse_conversation_id(source)
|
|
4184
|
+
if parsed is None:
|
|
4185
|
+
continue
|
|
4186
|
+
if str(parsed.get("connector") or "").strip().lower() == "local":
|
|
4187
|
+
continue
|
|
4188
|
+
return normalize_conversation_id(source)
|
|
4189
|
+
return None
|
|
4190
|
+
|
|
4191
|
+
def _lingzhu_passive_conversation_id(self) -> str | None:
|
|
4192
|
+
lingzhu_config = self.connectors_config.get("lingzhu")
|
|
4193
|
+
resolved = dict(lingzhu_config) if isinstance(lingzhu_config, dict) else {}
|
|
4194
|
+
auth_ak = self.config_manager._secret(resolved, "auth_ak", "auth_ak_env")
|
|
4195
|
+
if not auth_ak:
|
|
4196
|
+
return None
|
|
4197
|
+
return lingzhu_passive_conversation_id(resolved)
|
|
4198
|
+
|
|
4199
|
+
def _logical_connector_binding_conversation(self, connector_name: str, conversation_id: str | None) -> str:
|
|
4200
|
+
normalized_connector = str(connector_name or "").strip().lower()
|
|
4201
|
+
if normalized_connector == "lingzhu":
|
|
4202
|
+
passive_conversation_id = self._lingzhu_passive_conversation_id()
|
|
4203
|
+
if passive_conversation_id:
|
|
4204
|
+
return passive_conversation_id
|
|
4205
|
+
return normalize_conversation_id(conversation_id)
|
|
4206
|
+
|
|
4207
|
+
def _remove_connector_sources_from_quest(self, quest_id: str, connector_name: str) -> None:
|
|
4208
|
+
normalized_connector = str(connector_name or "").strip().lower()
|
|
4209
|
+
if not normalized_connector:
|
|
4210
|
+
return
|
|
4211
|
+
current_sources = self.quest_service.binding_sources(quest_id)
|
|
4212
|
+
filtered_sources = []
|
|
4213
|
+
for source in current_sources:
|
|
4214
|
+
parsed = parse_conversation_id(source)
|
|
4215
|
+
if parsed is not None and str(parsed.get("connector") or "").strip().lower() == normalized_connector:
|
|
4216
|
+
continue
|
|
4217
|
+
filtered_sources.append(source)
|
|
4218
|
+
self.quest_service.set_binding_sources(quest_id, filtered_sources or ["local:default"])
|
|
4219
|
+
|
|
4220
|
+
def _canonicalize_lingzhu_binding_state(self) -> None:
|
|
4221
|
+
passive_conversation_id = self._lingzhu_passive_conversation_id()
|
|
4222
|
+
if not passive_conversation_id:
|
|
4223
|
+
return
|
|
4224
|
+
try:
|
|
4225
|
+
channel = self._channel_with_bindings("lingzhu")
|
|
4226
|
+
except Exception:
|
|
4227
|
+
return
|
|
4228
|
+
bindings = [dict(item) for item in channel.list_bindings() if isinstance(item, dict)]
|
|
4229
|
+
selected_quest_id: str | None = None
|
|
4230
|
+
selected_updated_at = ""
|
|
4231
|
+
quests_with_lingzhu_sources: set[str] = set()
|
|
4232
|
+
|
|
4233
|
+
for item in bindings:
|
|
4234
|
+
quest_id = str(item.get("quest_id") or "").strip()
|
|
4235
|
+
updated_at = str(item.get("updated_at") or "").strip()
|
|
4236
|
+
if quest_id and (updated_at, quest_id) >= (selected_updated_at, str(selected_quest_id or "")):
|
|
4237
|
+
selected_quest_id = quest_id
|
|
4238
|
+
selected_updated_at = updated_at
|
|
4239
|
+
|
|
4240
|
+
for quest in self.quest_service.list_quests():
|
|
4241
|
+
quest_id = str(quest.get("quest_id") or "").strip()
|
|
4242
|
+
if not quest_id:
|
|
4243
|
+
continue
|
|
4244
|
+
sources = self.quest_service.binding_sources(quest_id)
|
|
4245
|
+
if any(
|
|
4246
|
+
(
|
|
4247
|
+
parsed := parse_conversation_id(source)
|
|
4248
|
+
) is not None and str(parsed.get("connector") or "").strip().lower() == "lingzhu"
|
|
4249
|
+
for source in sources
|
|
4250
|
+
):
|
|
4251
|
+
quests_with_lingzhu_sources.add(quest_id)
|
|
4252
|
+
if not selected_quest_id:
|
|
4253
|
+
selected_quest_id = quest_id
|
|
4254
|
+
|
|
4255
|
+
for item in bindings:
|
|
4256
|
+
conversation_id = str(item.get("conversation_id") or "").strip()
|
|
4257
|
+
quest_id = str(item.get("quest_id") or "").strip() or None
|
|
4258
|
+
if not conversation_id:
|
|
4259
|
+
continue
|
|
4260
|
+
channel.unbind_conversation(conversation_id, quest_id=quest_id)
|
|
4261
|
+
if quest_id:
|
|
4262
|
+
self.sessions.unbind(quest_id, conversation_id)
|
|
4263
|
+
|
|
4264
|
+
for quest_id in quests_with_lingzhu_sources:
|
|
4265
|
+
self._remove_connector_sources_from_quest(quest_id, "lingzhu")
|
|
4266
|
+
|
|
4267
|
+
if selected_quest_id:
|
|
4268
|
+
channel.bind_conversation(passive_conversation_id, selected_quest_id)
|
|
4269
|
+
self.quest_service.bind_source(selected_quest_id, "local:default")
|
|
4270
|
+
self.quest_service.bind_source(selected_quest_id, passive_conversation_id)
|
|
4271
|
+
|
|
4272
|
+
def _resolve_lingzhu_bound_quest(self, conversation_id: str) -> str | None:
|
|
4273
|
+
normalized_conversation_id = normalize_conversation_id(conversation_id)
|
|
4274
|
+
channel = self._channel_with_bindings("lingzhu")
|
|
4275
|
+
known_quest_id = str(channel.resolve_bound_quest(normalized_conversation_id) or "").strip() or None
|
|
4276
|
+
if known_quest_id:
|
|
4277
|
+
return known_quest_id
|
|
4278
|
+
passive_conversation_id = self._lingzhu_passive_conversation_id()
|
|
4279
|
+
passive_quest_id = str(channel.resolve_bound_quest(passive_conversation_id) or "").strip() or None
|
|
4280
|
+
if not passive_quest_id:
|
|
4281
|
+
return None
|
|
4282
|
+
return passive_quest_id
|
|
4283
|
+
|
|
4284
|
+
def _connector_target_label(self, conversation_id: str | None) -> str:
|
|
4285
|
+
normalized = normalize_conversation_id(conversation_id)
|
|
4286
|
+
parsed = parse_conversation_id(normalized)
|
|
4287
|
+
if parsed is None:
|
|
4288
|
+
return str(conversation_id or "unknown").strip() or "unknown"
|
|
4289
|
+
connector_label = self._connector_label(str(parsed.get("connector") or "").strip())
|
|
4290
|
+
profile_id = str(parsed.get("profile_id") or "").strip()
|
|
4291
|
+
if lingzhu_is_passive_conversation_id(normalized):
|
|
4292
|
+
agent_id = str(parsed.get("chat_id_raw") or parsed.get("chat_id") or "").strip() or "main"
|
|
4293
|
+
return f"{connector_label} · passive · {agent_id}"
|
|
4294
|
+
chat_id = str(parsed.get("chat_id_raw") or parsed.get("chat_id") or normalized).strip()
|
|
4295
|
+
if profile_id:
|
|
4296
|
+
return f"{connector_label} · {profile_id} · {chat_id}"
|
|
4297
|
+
return f"{connector_label} · {chat_id}"
|
|
4298
|
+
|
|
4299
|
+
def _binding_transition_summary(
|
|
4300
|
+
self,
|
|
4301
|
+
*,
|
|
4302
|
+
quest_id: str,
|
|
4303
|
+
previous_conversation_id: str | None,
|
|
4304
|
+
current_conversation_id: str | None,
|
|
4305
|
+
) -> dict[str, Any]:
|
|
4306
|
+
previous = normalize_conversation_id(previous_conversation_id)
|
|
4307
|
+
current = normalize_conversation_id(current_conversation_id)
|
|
4308
|
+
if conversation_identity_key(previous) == conversation_identity_key(current):
|
|
4309
|
+
mode = "unchanged"
|
|
4310
|
+
elif previous and current:
|
|
4311
|
+
mode = "switch"
|
|
4312
|
+
elif current:
|
|
4313
|
+
mode = "bind"
|
|
4314
|
+
elif previous:
|
|
4315
|
+
mode = "disconnect"
|
|
4316
|
+
else:
|
|
4317
|
+
mode = "unchanged"
|
|
4318
|
+
return {
|
|
4319
|
+
"quest_id": quest_id,
|
|
4320
|
+
"mode": mode,
|
|
4321
|
+
"previous_conversation_id": previous or None,
|
|
4322
|
+
"previous_label": self._connector_target_label(previous) if previous else None,
|
|
4323
|
+
"current_conversation_id": current or None,
|
|
4324
|
+
"current_label": self._connector_target_label(current) if current else None,
|
|
4325
|
+
"changed": mode != "unchanged",
|
|
4326
|
+
}
|
|
4327
|
+
|
|
4328
|
+
def _announce_binding_transition(
|
|
4329
|
+
self,
|
|
4330
|
+
summary: dict[str, Any] | None,
|
|
4331
|
+
*,
|
|
4332
|
+
notify_new: bool,
|
|
4333
|
+
notify_old: bool,
|
|
4334
|
+
) -> None:
|
|
4335
|
+
if not isinstance(summary, dict) or not bool(summary.get("changed")):
|
|
4336
|
+
return
|
|
4337
|
+
quest_id = str(summary.get("quest_id") or "").strip()
|
|
4338
|
+
previous_conversation_id = str(summary.get("previous_conversation_id") or "").strip() or None
|
|
4339
|
+
current_conversation_id = str(summary.get("current_conversation_id") or "").strip() or None
|
|
4340
|
+
previous_label = str(summary.get("previous_label") or "").strip() or None
|
|
4341
|
+
current_label = str(summary.get("current_label") or "").strip() or None
|
|
4342
|
+
mode = str(summary.get("mode") or "").strip()
|
|
4343
|
+
|
|
4344
|
+
if notify_old and previous_conversation_id and conversation_identity_key(previous_conversation_id) != conversation_identity_key(current_conversation_id):
|
|
4345
|
+
old_connector = str((parse_conversation_id(previous_conversation_id) or {}).get("connector") or "").strip().lower()
|
|
4346
|
+
if old_connector and old_connector in self.channels:
|
|
4347
|
+
channel = self._channel_with_bindings(old_connector)
|
|
4348
|
+
if mode == "disconnect":
|
|
4349
|
+
message = self._polite_copy(
|
|
4350
|
+
zh=f"当前已退出 Quest `{quest_id}`,项目已切换为仅本地。",
|
|
4351
|
+
en=f"This conversation is no longer bound to Quest `{quest_id}`. The project is now local only.",
|
|
4352
|
+
)
|
|
4353
|
+
else:
|
|
4354
|
+
message = self._polite_copy(
|
|
4355
|
+
zh=f"当前已退出 Quest `{quest_id}`,后续请在 {current_label} 查看进展。",
|
|
4356
|
+
en=f"This conversation is no longer bound to Quest `{quest_id}`. Continue from {current_label}.",
|
|
4357
|
+
)
|
|
4358
|
+
channel.send(
|
|
4359
|
+
{
|
|
4360
|
+
"conversation_id": previous_conversation_id,
|
|
4361
|
+
"quest_id": quest_id,
|
|
4362
|
+
"kind": "binding_notice",
|
|
4363
|
+
"message": message,
|
|
4364
|
+
}
|
|
4365
|
+
)
|
|
4366
|
+
|
|
4367
|
+
if notify_new and current_conversation_id:
|
|
4368
|
+
new_connector = str((parse_conversation_id(current_conversation_id) or {}).get("connector") or "").strip().lower()
|
|
4369
|
+
if new_connector and new_connector in self.channels:
|
|
4370
|
+
channel = self._channel_with_bindings(new_connector)
|
|
4371
|
+
if mode == "bind":
|
|
4372
|
+
message = self._polite_copy(
|
|
4373
|
+
zh=f"当前已绑定 Quest `{quest_id}`。",
|
|
4374
|
+
en=f"This conversation is now bound to Quest `{quest_id}`.",
|
|
4375
|
+
)
|
|
4376
|
+
elif mode == "switch":
|
|
4377
|
+
message = self._polite_copy(
|
|
4378
|
+
zh=f"当前已绑定 Quest `{quest_id}`,并已从 {previous_label} 切换到当前会话。",
|
|
4379
|
+
en=f"This conversation is now bound to Quest `{quest_id}`, replacing {previous_label}.",
|
|
4380
|
+
)
|
|
4381
|
+
else:
|
|
4382
|
+
message = ""
|
|
4383
|
+
if message:
|
|
4384
|
+
channel.send(
|
|
4385
|
+
{
|
|
4386
|
+
"conversation_id": current_conversation_id,
|
|
4387
|
+
"quest_id": quest_id,
|
|
4388
|
+
"kind": "binding_notice",
|
|
4389
|
+
"message": message,
|
|
4390
|
+
}
|
|
4391
|
+
)
|
|
4392
|
+
|
|
4393
|
+
def _connector_switch_required_message(
|
|
4394
|
+
self,
|
|
4395
|
+
*,
|
|
4396
|
+
connector_name: str,
|
|
4397
|
+
quest_id: str,
|
|
4398
|
+
current_conversation_id: str,
|
|
4399
|
+
requested_conversation_id: str,
|
|
4400
|
+
) -> str:
|
|
4401
|
+
switch_command = f"绑定{quest_id}" if str(connector_name or "").strip().lower() == "lingzhu" else f"/use {quest_id}"
|
|
4402
|
+
return self._polite_copy(
|
|
4403
|
+
zh=(
|
|
4404
|
+
f"当前 Quest `{quest_id}` 已绑定 {self._connector_target_label(current_conversation_id)}。\n"
|
|
4405
|
+
f"如需切换到 {self._connector_target_label(requested_conversation_id)},请发送 `{switch_command}`,或在项目设置里保存切换。"
|
|
4406
|
+
),
|
|
4407
|
+
en=(
|
|
4408
|
+
f"Quest `{quest_id}` is already bound to {self._connector_target_label(current_conversation_id)}.\n"
|
|
4409
|
+
f"To switch to {self._connector_target_label(requested_conversation_id)}, send `{switch_command}` or save the change from project settings."
|
|
4410
|
+
),
|
|
4411
|
+
)
|
|
4412
|
+
|
|
3842
4413
|
def _unbind_external_bindings(self, quest_id: str, *, preserve: set[str] | None = None) -> list[str]:
|
|
3843
4414
|
preserve_keys = {conversation_identity_key(item) for item in (preserve or set()) if item}
|
|
3844
4415
|
removed: list[str] = []
|
|
@@ -3924,6 +4495,46 @@ class DaemonApp:
|
|
|
3924
4495
|
parts = stripped.split()
|
|
3925
4496
|
return parts[0].lower(), parts[1:]
|
|
3926
4497
|
|
|
4498
|
+
@staticmethod
|
|
4499
|
+
def _parse_lingzhu_short_command(text: str) -> tuple[str, list[str]] | None:
|
|
4500
|
+
normalized = re.sub(r"\s+", " ", str(text or "").strip())
|
|
4501
|
+
if not normalized or normalized.startswith("/"):
|
|
4502
|
+
return None
|
|
4503
|
+
direct = _LINGZHU_SHORT_COMMAND_DIRECT_MAP.get(normalized)
|
|
4504
|
+
if direct:
|
|
4505
|
+
return direct, []
|
|
4506
|
+
for prefix, command_name in _LINGZHU_SHORT_COMMAND_PREFIX_MAP.items():
|
|
4507
|
+
if not normalized.startswith(prefix):
|
|
4508
|
+
continue
|
|
4509
|
+
remainder = normalized[len(prefix) :].strip().lstrip("::,,。.;;!!?? ")
|
|
4510
|
+
if command_name == "new":
|
|
4511
|
+
return command_name, [remainder] if remainder else []
|
|
4512
|
+
if command_name == "delete":
|
|
4513
|
+
matched = re.match(r"^(?P<target>\S+)?(?:\s+(?P<confirm>\S+))?$", remainder)
|
|
4514
|
+
target = str((matched.group("target") if matched else "") or "").strip()
|
|
4515
|
+
confirm = str((matched.group("confirm") if matched else "") or "").strip()
|
|
4516
|
+
args: list[str] = []
|
|
4517
|
+
if target:
|
|
4518
|
+
args.append("latest" if target in _LINGZHU_SHORT_LATEST_ALIASES else target)
|
|
4519
|
+
if confirm in _LINGZHU_DELETE_CONFIRM_ALIASES:
|
|
4520
|
+
args.append("--yes")
|
|
4521
|
+
return command_name, args
|
|
4522
|
+
if remainder:
|
|
4523
|
+
return command_name, ["latest" if remainder in _LINGZHU_SHORT_LATEST_ALIASES else remainder]
|
|
4524
|
+
return command_name, []
|
|
4525
|
+
return None
|
|
4526
|
+
|
|
4527
|
+
def _lingzhu_unbound_help_text(self) -> str:
|
|
4528
|
+
latest = str(self._latest_quest_id() or "none")
|
|
4529
|
+
return (
|
|
4530
|
+
"当前还没绑定 Quest。\n"
|
|
4531
|
+
"可直接说:帮助、列表、绑定025、绑定最新、新建 复现一个 baseline。\n"
|
|
4532
|
+
f"当前最新 Quest:`{latest}`。\n"
|
|
4533
|
+
"绑定后再说:我现在的任务是 ……\n"
|
|
4534
|
+
"查看进展可说:继续 或 汇报。\n"
|
|
4535
|
+
"快捷指令:状态、总结、暂停、恢复、删除025。"
|
|
4536
|
+
)
|
|
4537
|
+
|
|
3927
4538
|
def _maybe_bind_qq_main_chat(self, message: dict) -> dict | None:
|
|
3928
4539
|
chat_type = str(message.get("chat_type") or "").strip().lower()
|
|
3929
4540
|
if chat_type != "direct":
|
|
@@ -4053,6 +4664,20 @@ class DaemonApp:
|
|
|
4053
4664
|
)
|
|
4054
4665
|
if gateway.start():
|
|
4055
4666
|
self._qq_gateways[profile_id] = gateway
|
|
4667
|
+
weixin_config = self.connectors_config.get("weixin", {})
|
|
4668
|
+
if self._is_connector_system_enabled("weixin") and isinstance(weixin_config, dict) and self._weixin_ilink is None:
|
|
4669
|
+
weixin = WeixinIlinkService(
|
|
4670
|
+
home=self.home,
|
|
4671
|
+
config=weixin_config,
|
|
4672
|
+
on_event=lambda event: self.handle_connector_inbound("weixin", event),
|
|
4673
|
+
log=lambda level, message: self.logger.log(
|
|
4674
|
+
level,
|
|
4675
|
+
"connector.weixin_ilink",
|
|
4676
|
+
message=message,
|
|
4677
|
+
),
|
|
4678
|
+
)
|
|
4679
|
+
if weixin.start():
|
|
4680
|
+
self._weixin_ilink = weixin
|
|
4056
4681
|
if self._is_connector_system_enabled("telegram") and not self._telegram_polling:
|
|
4057
4682
|
for profile_id, profile_label, profile_config in self._profiled_connector_configs("telegram"):
|
|
4058
4683
|
polling = TelegramPollingService(
|
|
@@ -4149,6 +4774,10 @@ class DaemonApp:
|
|
|
4149
4774
|
self._qq_gateways = {}
|
|
4150
4775
|
for gateway in gateways:
|
|
4151
4776
|
gateway.stop()
|
|
4777
|
+
weixin = self._weixin_ilink
|
|
4778
|
+
self._weixin_ilink = None
|
|
4779
|
+
if weixin is not None:
|
|
4780
|
+
weixin.stop()
|
|
4152
4781
|
polling = list(self._telegram_polling.values())
|
|
4153
4782
|
self._telegram_polling = {}
|
|
4154
4783
|
for item in polling:
|
|
@@ -4263,6 +4892,435 @@ class DaemonApp:
|
|
|
4263
4892
|
accept = str(headers.get("Accept") or headers.get("accept") or "").lower()
|
|
4264
4893
|
return stream_value in {"1", "true", "yes", "stream"} or "text/event-stream" in accept
|
|
4265
4894
|
|
|
4895
|
+
def lingzhu_health_payload(self) -> dict[str, Any]:
|
|
4896
|
+
config = self.connectors_config.get("lingzhu")
|
|
4897
|
+
resolved = dict(config) if isinstance(config, dict) else {}
|
|
4898
|
+
return lingzhu_health_payload(resolved, chat_completions_enabled=True)
|
|
4899
|
+
|
|
4900
|
+
def _lingzhu_state_path(self) -> Path:
|
|
4901
|
+
return self.home / "logs" / "connectors" / "lingzhu" / "metis_state.json"
|
|
4902
|
+
|
|
4903
|
+
def _read_lingzhu_state(self) -> dict[str, Any]:
|
|
4904
|
+
payload = read_json(self._lingzhu_state_path(), {"delivered_counts": {}})
|
|
4905
|
+
if not isinstance(payload, dict):
|
|
4906
|
+
payload = {}
|
|
4907
|
+
delivered_counts = payload.get("delivered_counts")
|
|
4908
|
+
if not isinstance(delivered_counts, dict):
|
|
4909
|
+
delivered_counts = {}
|
|
4910
|
+
return {"delivered_counts": delivered_counts}
|
|
4911
|
+
|
|
4912
|
+
def _write_lingzhu_state(self, payload: dict[str, Any]) -> None:
|
|
4913
|
+
path = self._lingzhu_state_path()
|
|
4914
|
+
ensure_dir(path.parent)
|
|
4915
|
+
write_json(path, payload)
|
|
4916
|
+
|
|
4917
|
+
def _lingzhu_delivered_count(self, conversation_id: str) -> int:
|
|
4918
|
+
delivered_counts = self._read_lingzhu_state().get("delivered_counts") or {}
|
|
4919
|
+
raw_value = delivered_counts.get(conversation_identity_key(conversation_id))
|
|
4920
|
+
try:
|
|
4921
|
+
return max(0, int(raw_value))
|
|
4922
|
+
except (TypeError, ValueError):
|
|
4923
|
+
return 0
|
|
4924
|
+
|
|
4925
|
+
def _set_lingzhu_delivered_count(self, conversation_id: str, delivered_count: int) -> None:
|
|
4926
|
+
state = self._read_lingzhu_state()
|
|
4927
|
+
counts = dict(state.get("delivered_counts") or {})
|
|
4928
|
+
counts[conversation_identity_key(conversation_id)] = max(0, int(delivered_count))
|
|
4929
|
+
state["delivered_counts"] = counts
|
|
4930
|
+
self._write_lingzhu_state(state)
|
|
4931
|
+
|
|
4932
|
+
def _lingzhu_outbox_records(self, conversation_id: str) -> list[dict[str, Any]]:
|
|
4933
|
+
outbox_path = self.home / "logs" / "connectors" / "lingzhu" / "outbox.jsonl"
|
|
4934
|
+
target_key = conversation_identity_key(conversation_id)
|
|
4935
|
+
items: list[dict[str, Any]] = []
|
|
4936
|
+
for record in read_jsonl(outbox_path):
|
|
4937
|
+
if not isinstance(record, dict):
|
|
4938
|
+
continue
|
|
4939
|
+
current_conversation_id = str(record.get("conversation_id") or "").strip()
|
|
4940
|
+
if not current_conversation_id:
|
|
4941
|
+
continue
|
|
4942
|
+
if conversation_identity_key(current_conversation_id) != target_key:
|
|
4943
|
+
continue
|
|
4944
|
+
text = str(record.get("text") or "").strip()
|
|
4945
|
+
if not text:
|
|
4946
|
+
continue
|
|
4947
|
+
items.append(dict(record))
|
|
4948
|
+
return items
|
|
4949
|
+
|
|
4950
|
+
def _lingzhu_pending_outbox_records(
|
|
4951
|
+
self,
|
|
4952
|
+
conversation_id: str,
|
|
4953
|
+
*,
|
|
4954
|
+
delivered_count: int | None = None,
|
|
4955
|
+
) -> tuple[list[dict[str, Any]], int]:
|
|
4956
|
+
records = self._lingzhu_outbox_records(conversation_id)
|
|
4957
|
+
baseline = self._lingzhu_delivered_count(conversation_id) if delivered_count is None else delivered_count
|
|
4958
|
+
applied_baseline = max(0, min(int(baseline), len(records)))
|
|
4959
|
+
return records[applied_baseline:], len(records)
|
|
4960
|
+
|
|
4961
|
+
@staticmethod
|
|
4962
|
+
def _lingzhu_wait_timeout_seconds(config: dict[str, Any]) -> float:
|
|
4963
|
+
try:
|
|
4964
|
+
timeout_ms = int(config.get("request_timeout_ms") or 60000)
|
|
4965
|
+
except (TypeError, ValueError):
|
|
4966
|
+
timeout_ms = 60000
|
|
4967
|
+
timeout_ms = max(15000, min(timeout_ms, 120000))
|
|
4968
|
+
return timeout_ms / 1000.0
|
|
4969
|
+
|
|
4970
|
+
def _lingzhu_wait_for_outbox_records(
|
|
4971
|
+
self,
|
|
4972
|
+
conversation_id: str,
|
|
4973
|
+
*,
|
|
4974
|
+
delivered_count: int,
|
|
4975
|
+
timeout_seconds: float,
|
|
4976
|
+
) -> tuple[list[dict[str, Any]], int]:
|
|
4977
|
+
deadline = time.monotonic() + max(0.1, timeout_seconds)
|
|
4978
|
+
while time.monotonic() < deadline:
|
|
4979
|
+
pending_records, total_count = self._lingzhu_pending_outbox_records(
|
|
4980
|
+
conversation_id,
|
|
4981
|
+
delivered_count=delivered_count,
|
|
4982
|
+
)
|
|
4983
|
+
if pending_records:
|
|
4984
|
+
return pending_records, total_count
|
|
4985
|
+
time.sleep(0.25)
|
|
4986
|
+
return self._lingzhu_pending_outbox_records(conversation_id, delivered_count=delivered_count)
|
|
4987
|
+
|
|
4988
|
+
def _lingzhu_emit_outbox_records(
|
|
4989
|
+
self,
|
|
4990
|
+
handler: BaseHTTPRequestHandler,
|
|
4991
|
+
*,
|
|
4992
|
+
message_id: str,
|
|
4993
|
+
agent_id: str,
|
|
4994
|
+
records: list[dict[str, Any]],
|
|
4995
|
+
config: dict[str, Any] | None = None,
|
|
4996
|
+
) -> int:
|
|
4997
|
+
emitted = 0
|
|
4998
|
+
resolved = dict(config or {})
|
|
4999
|
+
default_navigation_mode = str(resolved.get("default_navigation_mode") or "0").strip() or "0"
|
|
5000
|
+
experimental_enabled = bool(resolved.get("enable_experimental_native_actions", False))
|
|
5001
|
+
for record in records:
|
|
5002
|
+
raw_text = str(record.get("text") or "").strip()
|
|
5003
|
+
detected_tool_call = None
|
|
5004
|
+
text = raw_text
|
|
5005
|
+
if raw_text:
|
|
5006
|
+
detected_tool_call, text = lingzhu_detect_tool_call_from_text(
|
|
5007
|
+
raw_text,
|
|
5008
|
+
default_navigation_mode=default_navigation_mode,
|
|
5009
|
+
experimental_enabled=experimental_enabled,
|
|
5010
|
+
)
|
|
5011
|
+
if text:
|
|
5012
|
+
self._write_sse_event(
|
|
5013
|
+
handler,
|
|
5014
|
+
event="message",
|
|
5015
|
+
data=lingzhu_sse_answer(
|
|
5016
|
+
message_id=message_id,
|
|
5017
|
+
agent_id=agent_id,
|
|
5018
|
+
answer_stream=text,
|
|
5019
|
+
is_finish=True,
|
|
5020
|
+
),
|
|
5021
|
+
)
|
|
5022
|
+
emitted += 1
|
|
5023
|
+
emitted_tool_call = False
|
|
5024
|
+
for action in record.get("surface_actions") or []:
|
|
5025
|
+
tool_call = lingzhu_surface_action_tool_call(
|
|
5026
|
+
action,
|
|
5027
|
+
default_navigation_mode=default_navigation_mode,
|
|
5028
|
+
experimental_enabled=experimental_enabled,
|
|
5029
|
+
)
|
|
5030
|
+
if not tool_call:
|
|
5031
|
+
continue
|
|
5032
|
+
self._write_sse_event(
|
|
5033
|
+
handler,
|
|
5034
|
+
event="message",
|
|
5035
|
+
data=lingzhu_sse_tool_call(
|
|
5036
|
+
message_id=message_id,
|
|
5037
|
+
agent_id=agent_id,
|
|
5038
|
+
tool_call=tool_call,
|
|
5039
|
+
is_finish=True,
|
|
5040
|
+
),
|
|
5041
|
+
)
|
|
5042
|
+
emitted += 1
|
|
5043
|
+
emitted_tool_call = True
|
|
5044
|
+
if not emitted_tool_call and detected_tool_call:
|
|
5045
|
+
self._write_sse_event(
|
|
5046
|
+
handler,
|
|
5047
|
+
event="message",
|
|
5048
|
+
data=lingzhu_sse_tool_call(
|
|
5049
|
+
message_id=message_id,
|
|
5050
|
+
agent_id=agent_id,
|
|
5051
|
+
tool_call=detected_tool_call,
|
|
5052
|
+
is_finish=True,
|
|
5053
|
+
),
|
|
5054
|
+
)
|
|
5055
|
+
emitted += 1
|
|
5056
|
+
return emitted
|
|
5057
|
+
|
|
5058
|
+
def _lingzhu_short_status_text(self, quest_id: str | None) -> str:
|
|
5059
|
+
if not quest_id:
|
|
5060
|
+
return self._lingzhu_unbound_help_text()
|
|
5061
|
+
snapshot = self.quest_service.snapshot(quest_id)
|
|
5062
|
+
runtime_status = str(snapshot.get("runtime_status") or snapshot.get("status") or "").strip().lower()
|
|
5063
|
+
if runtime_status in {"running", "active"}:
|
|
5064
|
+
return "进行中"
|
|
5065
|
+
if runtime_status == "waiting_for_user":
|
|
5066
|
+
return "等你确认"
|
|
5067
|
+
if runtime_status in {"paused", "stopped"}:
|
|
5068
|
+
return "已暂停"
|
|
5069
|
+
if runtime_status == "completed":
|
|
5070
|
+
return "已完成"
|
|
5071
|
+
if runtime_status == "error":
|
|
5072
|
+
return "出错了"
|
|
5073
|
+
return "暂无新进展"
|
|
5074
|
+
|
|
5075
|
+
@staticmethod
|
|
5076
|
+
def _lingzhu_reply_payload(result: dict[str, Any]) -> tuple[str, str | None, str]:
|
|
5077
|
+
if not isinstance(result, dict):
|
|
5078
|
+
return "", None, ""
|
|
5079
|
+
reply = result.get("reply")
|
|
5080
|
+
if not isinstance(reply, dict):
|
|
5081
|
+
return "", None, ""
|
|
5082
|
+
payload = reply.get("payload")
|
|
5083
|
+
if not isinstance(payload, dict):
|
|
5084
|
+
return "", None, ""
|
|
5085
|
+
text = str(payload.get("text") or payload.get("message") or "").strip()
|
|
5086
|
+
quest_id = str(payload.get("quest_id") or "").strip() or None
|
|
5087
|
+
kind = str(payload.get("kind") or "").strip()
|
|
5088
|
+
return text, quest_id, kind
|
|
5089
|
+
|
|
5090
|
+
def stream_lingzhu_sse(
|
|
5091
|
+
self,
|
|
5092
|
+
handler: BaseHTTPRequestHandler,
|
|
5093
|
+
*,
|
|
5094
|
+
raw_body: bytes,
|
|
5095
|
+
headers: dict[str, str],
|
|
5096
|
+
) -> None:
|
|
5097
|
+
config = self.connectors_config.get("lingzhu")
|
|
5098
|
+
resolved = dict(config) if isinstance(config, dict) else {}
|
|
5099
|
+
if resolved.get("enabled") is False:
|
|
5100
|
+
handler.send_response(503)
|
|
5101
|
+
handler.send_header("Content-Type", "application/json; charset=utf-8")
|
|
5102
|
+
handler.end_headers()
|
|
5103
|
+
handler.wfile.write(json.dumps({"error": "Lingzhu connector is disabled"}, ensure_ascii=False).encode("utf-8"))
|
|
5104
|
+
return
|
|
5105
|
+
|
|
5106
|
+
auth_ak = self.config_manager._secret(resolved, "auth_ak", "auth_ak_env")
|
|
5107
|
+
auth_header = headers.get("Authorization") or headers.get("authorization") or ""
|
|
5108
|
+
if not lingzhu_verify_auth_header(auth_header, auth_ak):
|
|
5109
|
+
handler.send_response(401)
|
|
5110
|
+
handler.send_header("Content-Type", "application/json; charset=utf-8")
|
|
5111
|
+
handler.end_headers()
|
|
5112
|
+
handler.wfile.write(json.dumps({"error": "Unauthorized"}, ensure_ascii=False).encode("utf-8"))
|
|
5113
|
+
return
|
|
5114
|
+
|
|
5115
|
+
try:
|
|
5116
|
+
body = self.handlers.parse_body(raw_body)
|
|
5117
|
+
except Exception:
|
|
5118
|
+
handler.send_response(400)
|
|
5119
|
+
handler.send_header("Content-Type", "application/json; charset=utf-8")
|
|
5120
|
+
handler.end_headers()
|
|
5121
|
+
handler.wfile.write(json.dumps({"error": "Invalid JSON body"}, ensure_ascii=False).encode("utf-8"))
|
|
5122
|
+
return
|
|
5123
|
+
|
|
5124
|
+
if not isinstance(body, dict):
|
|
5125
|
+
handler.send_response(400)
|
|
5126
|
+
handler.send_header("Content-Type", "application/json; charset=utf-8")
|
|
5127
|
+
handler.end_headers()
|
|
5128
|
+
handler.wfile.write(json.dumps({"error": "Request body must be a JSON object"}, ensure_ascii=False).encode("utf-8"))
|
|
5129
|
+
return
|
|
5130
|
+
|
|
5131
|
+
message_id = str(body.get("message_id") or body.get("request_id") or generate_id("lingzhu")).strip()
|
|
5132
|
+
agent_id = str(body.get("agent_id") or resolved.get("agent_id") or "main").strip() or "main"
|
|
5133
|
+
messages = body.get("message")
|
|
5134
|
+
if not isinstance(messages, list):
|
|
5135
|
+
messages = body.get("messages")
|
|
5136
|
+
if not isinstance(messages, list):
|
|
5137
|
+
text = str(body.get("text") or body.get("content") or "").strip()
|
|
5138
|
+
messages = [{"role": "user", "type": "text", "text": text}] if text else None
|
|
5139
|
+
if not isinstance(messages, list):
|
|
5140
|
+
handler.send_response(400)
|
|
5141
|
+
handler.send_header("Content-Type", "application/json; charset=utf-8")
|
|
5142
|
+
handler.end_headers()
|
|
5143
|
+
handler.wfile.write(
|
|
5144
|
+
json.dumps({"error": "Missing required fields: message or messages"}, ensure_ascii=False).encode("utf-8")
|
|
5145
|
+
)
|
|
5146
|
+
return
|
|
5147
|
+
|
|
5148
|
+
conversation_id = lingzhu_request_conversation_id(body)
|
|
5149
|
+
binding_conversation_id = self._logical_connector_binding_conversation("lingzhu", conversation_id)
|
|
5150
|
+
sender_id = lingzhu_request_sender_id(body)
|
|
5151
|
+
inbound_text = lingzhu_extract_user_text(messages) or self._polite_copy(
|
|
5152
|
+
zh="你好,请继续。",
|
|
5153
|
+
en="Hello, please continue.",
|
|
5154
|
+
)
|
|
5155
|
+
channel = self._channel_with_bindings("lingzhu")
|
|
5156
|
+
known_quest_id = self._resolve_lingzhu_bound_quest(conversation_id)
|
|
5157
|
+
delivered_count = self._lingzhu_delivered_count(conversation_id)
|
|
5158
|
+
task_text = lingzhu_extract_task_text(inbound_text)
|
|
5159
|
+
is_command = inbound_text.startswith(channel.command_prefix()) or self._parse_lingzhu_short_command(inbound_text) is not None
|
|
5160
|
+
|
|
5161
|
+
inbound_payload = {
|
|
5162
|
+
"conversation_id": conversation_id,
|
|
5163
|
+
"chat_type": "direct",
|
|
5164
|
+
"message_id": message_id,
|
|
5165
|
+
"sender_id": sender_id,
|
|
5166
|
+
"sender_name": sender_id,
|
|
5167
|
+
"user_id": sender_id,
|
|
5168
|
+
"direct_id": sender_id,
|
|
5169
|
+
"text": inbound_text,
|
|
5170
|
+
"message": inbound_text,
|
|
5171
|
+
"content": inbound_text,
|
|
5172
|
+
"raw_event": body,
|
|
5173
|
+
"metadata": body.get("metadata"),
|
|
5174
|
+
}
|
|
5175
|
+
|
|
5176
|
+
handler.send_response(200)
|
|
5177
|
+
handler.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
|
5178
|
+
handler.send_header("Cache-Control", "no-cache")
|
|
5179
|
+
handler.send_header("Connection", "close")
|
|
5180
|
+
handler.send_header("X-Accel-Buffering", "no")
|
|
5181
|
+
handler.end_headers()
|
|
5182
|
+
|
|
5183
|
+
try:
|
|
5184
|
+
handler.wfile.write(b": keepalive\n\n")
|
|
5185
|
+
handler.wfile.flush()
|
|
5186
|
+
|
|
5187
|
+
if is_command:
|
|
5188
|
+
result = self.handle_connector_inbound("lingzhu", inbound_payload)
|
|
5189
|
+
reply_text, _, _ = self._lingzhu_reply_payload(result)
|
|
5190
|
+
pending_records, total_count = self._lingzhu_pending_outbox_records(
|
|
5191
|
+
conversation_id,
|
|
5192
|
+
delivered_count=delivered_count,
|
|
5193
|
+
)
|
|
5194
|
+
emitted = self._lingzhu_emit_outbox_records(
|
|
5195
|
+
handler,
|
|
5196
|
+
message_id=message_id,
|
|
5197
|
+
agent_id=agent_id,
|
|
5198
|
+
records=pending_records,
|
|
5199
|
+
config=resolved,
|
|
5200
|
+
)
|
|
5201
|
+
if emitted:
|
|
5202
|
+
self._set_lingzhu_delivered_count(conversation_id, total_count)
|
|
5203
|
+
else:
|
|
5204
|
+
answer_text = reply_text
|
|
5205
|
+
if not answer_text:
|
|
5206
|
+
if not bool(result.get("accepted", False)):
|
|
5207
|
+
reason = str(result.get("reason") or (result.get("normalized") or {}).get("reason") or "").strip()
|
|
5208
|
+
answer_text = reason or "请求未接受"
|
|
5209
|
+
else:
|
|
5210
|
+
answer_text = "已收到"
|
|
5211
|
+
self._write_sse_event(
|
|
5212
|
+
handler,
|
|
5213
|
+
event="message",
|
|
5214
|
+
data=lingzhu_sse_answer(
|
|
5215
|
+
message_id=message_id,
|
|
5216
|
+
agent_id=agent_id,
|
|
5217
|
+
answer_stream=answer_text,
|
|
5218
|
+
is_finish=True,
|
|
5219
|
+
),
|
|
5220
|
+
)
|
|
5221
|
+
handler.close_connection = True
|
|
5222
|
+
return
|
|
5223
|
+
|
|
5224
|
+
if task_text is not None:
|
|
5225
|
+
target_quest_id = known_quest_id
|
|
5226
|
+
if target_quest_id:
|
|
5227
|
+
self.sessions.bind(target_quest_id, conversation_id)
|
|
5228
|
+
self.quest_service.bind_source(target_quest_id, binding_conversation_id)
|
|
5229
|
+
pending_before, total_before = self._lingzhu_pending_outbox_records(
|
|
5230
|
+
conversation_id,
|
|
5231
|
+
delivered_count=delivered_count,
|
|
5232
|
+
)
|
|
5233
|
+
emitted_before = self._lingzhu_emit_outbox_records(
|
|
5234
|
+
handler,
|
|
5235
|
+
message_id=message_id,
|
|
5236
|
+
agent_id=agent_id,
|
|
5237
|
+
records=pending_before,
|
|
5238
|
+
config=resolved,
|
|
5239
|
+
)
|
|
5240
|
+
if emitted_before:
|
|
5241
|
+
delivered_count = total_before
|
|
5242
|
+
self._set_lingzhu_delivered_count(conversation_id, total_before)
|
|
5243
|
+
|
|
5244
|
+
if not target_quest_id:
|
|
5245
|
+
self._write_sse_event(
|
|
5246
|
+
handler,
|
|
5247
|
+
event="message",
|
|
5248
|
+
data=lingzhu_sse_answer(
|
|
5249
|
+
message_id=message_id,
|
|
5250
|
+
agent_id=agent_id,
|
|
5251
|
+
answer_stream=self._lingzhu_unbound_help_text(),
|
|
5252
|
+
is_finish=True,
|
|
5253
|
+
),
|
|
5254
|
+
)
|
|
5255
|
+
handler.close_connection = True
|
|
5256
|
+
return
|
|
5257
|
+
|
|
5258
|
+
self.submit_user_message(
|
|
5259
|
+
target_quest_id,
|
|
5260
|
+
text=task_text,
|
|
5261
|
+
source=conversation_id,
|
|
5262
|
+
client_message_id=message_id,
|
|
5263
|
+
)
|
|
5264
|
+
pending_after, total_after = self._lingzhu_wait_for_outbox_records(
|
|
5265
|
+
conversation_id,
|
|
5266
|
+
delivered_count=delivered_count,
|
|
5267
|
+
timeout_seconds=self._lingzhu_wait_timeout_seconds(resolved),
|
|
5268
|
+
)
|
|
5269
|
+
emitted_after = self._lingzhu_emit_outbox_records(
|
|
5270
|
+
handler,
|
|
5271
|
+
message_id=message_id,
|
|
5272
|
+
agent_id=agent_id,
|
|
5273
|
+
records=pending_after,
|
|
5274
|
+
config=resolved,
|
|
5275
|
+
)
|
|
5276
|
+
if emitted_after:
|
|
5277
|
+
self._set_lingzhu_delivered_count(conversation_id, total_after)
|
|
5278
|
+
else:
|
|
5279
|
+
self._write_sse_event(
|
|
5280
|
+
handler,
|
|
5281
|
+
event="message",
|
|
5282
|
+
data=lingzhu_sse_answer(
|
|
5283
|
+
message_id=message_id,
|
|
5284
|
+
agent_id=agent_id,
|
|
5285
|
+
answer_stream="已开始" if emitted_before else self._lingzhu_short_status_text(target_quest_id),
|
|
5286
|
+
is_finish=True,
|
|
5287
|
+
),
|
|
5288
|
+
)
|
|
5289
|
+
handler.close_connection = True
|
|
5290
|
+
return
|
|
5291
|
+
|
|
5292
|
+
if known_quest_id:
|
|
5293
|
+
self.sessions.bind(known_quest_id, conversation_id)
|
|
5294
|
+
self.quest_service.bind_source(known_quest_id, binding_conversation_id)
|
|
5295
|
+
|
|
5296
|
+
pending_records, total_count = self._lingzhu_pending_outbox_records(
|
|
5297
|
+
conversation_id,
|
|
5298
|
+
delivered_count=delivered_count,
|
|
5299
|
+
)
|
|
5300
|
+
emitted = self._lingzhu_emit_outbox_records(
|
|
5301
|
+
handler,
|
|
5302
|
+
message_id=message_id,
|
|
5303
|
+
agent_id=agent_id,
|
|
5304
|
+
records=pending_records,
|
|
5305
|
+
config=resolved,
|
|
5306
|
+
)
|
|
5307
|
+
if emitted:
|
|
5308
|
+
self._set_lingzhu_delivered_count(conversation_id, total_count)
|
|
5309
|
+
else:
|
|
5310
|
+
self._write_sse_event(
|
|
5311
|
+
handler,
|
|
5312
|
+
event="message",
|
|
5313
|
+
data=lingzhu_sse_answer(
|
|
5314
|
+
message_id=message_id,
|
|
5315
|
+
agent_id=agent_id,
|
|
5316
|
+
answer_stream=self._lingzhu_short_status_text(known_quest_id),
|
|
5317
|
+
is_finish=True,
|
|
5318
|
+
),
|
|
5319
|
+
)
|
|
5320
|
+
handler.close_connection = True
|
|
5321
|
+
except (BrokenPipeError, ConnectionResetError, TimeoutError):
|
|
5322
|
+
return
|
|
5323
|
+
|
|
4266
5324
|
@staticmethod
|
|
4267
5325
|
def _write_sse_event(
|
|
4268
5326
|
handler: BaseHTTPRequestHandler,
|
|
@@ -4730,6 +5788,14 @@ class DaemonApp:
|
|
|
4730
5788
|
except Exception as exc:
|
|
4731
5789
|
self._write_json(500, {"ok": False, "message": str(exc)})
|
|
4732
5790
|
return
|
|
5791
|
+
if route_name == "lingzhu_sse":
|
|
5792
|
+
content_length = int(self.headers.get("Content-Length", "0"))
|
|
5793
|
+
raw_body = self.rfile.read(content_length) if content_length else b""
|
|
5794
|
+
try:
|
|
5795
|
+
app.stream_lingzhu_sse(self, raw_body=raw_body, headers=dict(self.headers.items()))
|
|
5796
|
+
except Exception as exc:
|
|
5797
|
+
self._write_json(500, {"ok": False, "message": str(exc)})
|
|
5798
|
+
return
|
|
4733
5799
|
|
|
4734
5800
|
content_length = int(self.headers.get("Content-Length", "0"))
|
|
4735
5801
|
raw_body = self.rfile.read(content_length) if content_length else b""
|
|
@@ -4765,11 +5831,14 @@ class DaemonApp:
|
|
|
4765
5831
|
"terminal_restore",
|
|
4766
5832
|
"terminal_history",
|
|
4767
5833
|
"latex_builds",
|
|
5834
|
+
"arxiv_list",
|
|
5835
|
+
"annotations_file",
|
|
5836
|
+
"annotations_project",
|
|
4768
5837
|
}:
|
|
4769
5838
|
payload = result(**params, path=self.path)
|
|
4770
5839
|
elif method == "GET":
|
|
4771
5840
|
payload = result(**params) if params else result()
|
|
4772
|
-
elif route_name in {"document_open", "document_asset_upload", "chat", "command", "quest_control", "config_save", "quest_create", "quest_baseline_binding", "run_create", "qq_inbound", "connector_inbound", "docs_open", "admin_shutdown", "bash_stop", "quest_settings", "quest_bindings", "quest_delete", "terminal_session_ensure", "terminal_attach", "terminal_input", "stage_view", "latex_init", "latex_compile", "system_update_action"}:
|
|
5841
|
+
elif route_name in {"document_open", "document_asset_upload", "chat", "command", "quest_control", "config_save", "quest_create", "quest_baseline_binding", "run_create", "qq_inbound", "connector_inbound", "docs_open", "admin_shutdown", "bash_stop", "quest_settings", "quest_bindings", "quest_delete", "quest_layout_update", "terminal_session_ensure", "terminal_attach", "terminal_input", "stage_view", "latex_init", "latex_compile", "system_update_action", "weixin_login_qr_start", "weixin_login_qr_wait", "arxiv_import", "annotation_create"}:
|
|
4773
5842
|
payload = result(**params, body=body)
|
|
4774
5843
|
elif route_name == "config_validate":
|
|
4775
5844
|
payload = result(body)
|