@researai/deepscientist 1.5.1 → 1.5.3
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/README.md +69 -1
- package/bin/ds.js +2239 -153
- package/docs/en/00_QUICK_START.md +60 -20
- package/docs/en/01_SETTINGS_REFERENCE.md +20 -20
- package/docs/en/02_START_RESEARCH_GUIDE.md +11 -11
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +10 -10
- package/docs/en/05_TUI_GUIDE.md +1 -1
- package/docs/en/09_DOCTOR.md +48 -4
- package/docs/en/90_ARCHITECTURE.md +4 -2
- package/docs/zh/00_QUICK_START.md +60 -20
- package/docs/zh/01_SETTINGS_REFERENCE.md +21 -21
- package/docs/zh/02_START_RESEARCH_GUIDE.md +19 -19
- package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +10 -10
- package/docs/zh/05_TUI_GUIDE.md +1 -1
- package/docs/zh/09_DOCTOR.md +46 -4
- package/install.sh +125 -8
- package/package.json +2 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +6 -1
- package/src/deepscientist/artifact/service.py +553 -26
- package/src/deepscientist/bash_exec/monitor.py +23 -4
- package/src/deepscientist/bash_exec/runtime.py +3 -0
- package/src/deepscientist/bash_exec/service.py +132 -4
- package/src/deepscientist/bridges/base.py +10 -19
- package/src/deepscientist/channels/discord_gateway.py +25 -2
- package/src/deepscientist/channels/feishu_long_connection.py +41 -3
- package/src/deepscientist/channels/qq.py +524 -64
- package/src/deepscientist/channels/qq_gateway.py +22 -3
- package/src/deepscientist/channels/relay.py +429 -90
- package/src/deepscientist/channels/slack_socket.py +29 -5
- package/src/deepscientist/channels/telegram_polling.py +25 -2
- package/src/deepscientist/channels/whatsapp_local_session.py +32 -4
- package/src/deepscientist/cli.py +27 -0
- package/src/deepscientist/config/models.py +6 -40
- package/src/deepscientist/config/service.py +165 -156
- package/src/deepscientist/connector_profiles.py +346 -0
- package/src/deepscientist/connector_runtime.py +88 -43
- package/src/deepscientist/daemon/api/handlers.py +65 -11
- package/src/deepscientist/daemon/api/router.py +4 -2
- package/src/deepscientist/daemon/app.py +772 -219
- package/src/deepscientist/doctor.py +69 -2
- package/src/deepscientist/gitops/diff.py +3 -0
- package/src/deepscientist/home.py +25 -2
- package/src/deepscientist/mcp/context.py +3 -1
- package/src/deepscientist/mcp/server.py +66 -7
- package/src/deepscientist/migration.py +114 -0
- package/src/deepscientist/prompts/builder.py +71 -3
- package/src/deepscientist/qq_profiles.py +186 -0
- package/src/deepscientist/quest/layout.py +1 -0
- package/src/deepscientist/quest/service.py +70 -12
- package/src/deepscientist/quest/stage_views.py +46 -0
- package/src/deepscientist/runners/codex.py +2 -0
- package/src/deepscientist/shared.py +44 -17
- package/src/prompts/connectors/lingzhu.md +3 -0
- package/src/prompts/connectors/qq.md +42 -2
- package/src/prompts/system.md +123 -10
- package/src/skills/analysis-campaign/SKILL.md +35 -6
- package/src/skills/baseline/SKILL.md +73 -32
- package/src/skills/decision/SKILL.md +4 -3
- package/src/skills/experiment/SKILL.md +28 -6
- package/src/skills/finalize/SKILL.md +5 -2
- package/src/skills/idea/SKILL.md +2 -2
- package/src/skills/intake-audit/SKILL.md +2 -2
- package/src/skills/rebuttal/SKILL.md +4 -2
- package/src/skills/review/SKILL.md +4 -2
- package/src/skills/scout/SKILL.md +2 -2
- package/src/skills/write/SKILL.md +2 -2
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-w5lF2Ttt.js → AiManusChatView-qzChi9uh.js} +67 -94
- package/src/ui/dist/assets/{AnalysisPlugin-DJOED79I.js → AnalysisPlugin-CcC_-UqN.js} +1 -1
- package/src/ui/dist/assets/{AutoFigurePlugin-DaG61Y0M.js → AutoFigurePlugin-DD8LkJLe.js} +5 -5
- package/src/ui/dist/assets/{CliPlugin-CV4LqUB_.js → CliPlugin-DJJFfVmW.js} +17 -110
- package/src/ui/dist/assets/{CodeEditorPlugin-DylfAea4.js → CodeEditorPlugin-CrjkHNLh.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-F7saY0LM.js → CodeViewerPlugin-obnD6G5R.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-COP0c7jf.js → DocViewerPlugin-DB9SUQVd.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-CAS05pT9.js → GitDiffViewerPlugin-DZLlNlD2.js} +1 -1
- package/src/ui/dist/assets/{ImageViewerPlugin-Bco1CN_w.js → ImageViewerPlugin-BGwfDZ0Y.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-CvMlCD99.js → LabCopilotPanel-dfLptQcR.js} +10 -10
- package/src/ui/dist/assets/{LabPlugin-BYankkE4.js → LabPlugin-CeGjAl3A.js} +1 -1
- package/src/ui/dist/assets/{LatexPlugin-LDSMR-t-.js → LatexPlugin-BBJ7kd1V.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-B7o80jgm.js → MarkdownViewerPlugin-DKZi7BcB.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-CM6ZOcpC.js → MarketplacePlugin-C_k-9jD0.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-Dc61cXmK.js → NotebookEditor-4R88_BMO.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-DWowuQwx.js → PdfLoader-DwEFQLrw.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-BsJM1q_a.js → PdfMarkdownPlugin-D-jdsqF8.js} +3 -3
- package/src/ui/dist/assets/{PdfViewerPlugin-DB2eEEFQ.js → PdfViewerPlugin-CmeBGDY0.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-CraThSvt.js → SearchPlugin-Dlz2WKJ4.js} +1 -1
- package/src/ui/dist/assets/{Stepper-CgocRTPq.js → Stepper-ClOgzWM3.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-B1JGhKtd.js → TextViewerPlugin-DDQWxibk.js} +4 -4
- package/src/ui/dist/assets/{VNCViewer-CclFC7FM.js → VNCViewer-CJXT0Nm8.js} +9 -9
- package/src/ui/dist/assets/{bibtex-D3IKsMl7.js → bibtex-DLr4Rtk4.js} +1 -1
- package/src/ui/dist/assets/{code-BP37Xx0p.js → code-DgKK408Y.js} +1 -1
- package/src/ui/dist/assets/{file-content-BAJSu-9r.js → file-content-6HBqQnvQ.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DUGeCTuy.js → file-diff-panel-Dhu0TbBM.js} +1 -1
- package/src/ui/dist/assets/{file-socket-CXc1Ojf7.js → file-socket-CP3iwVZG.js} +1 -1
- package/src/ui/dist/assets/{file-utils-2J21jt7M.js → file-utils-BsS-Aw68.js} +1 -1
- package/src/ui/dist/assets/{image-CMMmgvcn.js → image-ByeK-Zcv.js} +1 -1
- package/src/ui/dist/assets/{index-DmwmJmbW.js → index-BLjo5--a.js} +33610 -31016
- package/src/ui/dist/assets/{index-CWgMgpow.js → index-BdsE0uRz.js} +11 -11
- package/src/ui/dist/assets/{index-s7aHnNQ4.js → index-C-eX-N6A.js} +1 -1
- package/src/ui/dist/assets/{index-KGt-z-dD.css → index-CuQhlrR-.css} +2747 -2
- package/src/ui/dist/assets/{index-BaVumsQT.js → index-DyremSIv.js} +2 -2
- package/src/ui/dist/assets/{message-square-CQRfX0Am.js → message-square-DnagiLnc.js} +1 -1
- package/src/ui/dist/assets/{monaco-B4TbdsrF.js → monaco-4kBFeprs.js} +1 -1
- package/src/ui/dist/assets/{popover-B8Rokodk.js → popover-hRCXZzs2.js} +1 -1
- package/src/ui/dist/assets/{project-sync-D_i96KH4.js → project-sync-O_85YuP6.js} +1 -1
- package/src/ui/dist/assets/{sigma-D12PnzCN.js → sigma-DvKopSnL.js} +1 -1
- package/src/ui/dist/assets/{tooltip-B6YrI4aJ.js → tooltip-BmlPc6kc.js} +1 -1
- package/src/ui/dist/assets/{trash-Bc8jGp0V.js → trash-n-UvdZFR.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-mXVCYSZ-.js → useCliAccess-WDd3_wIh.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-Bg6b9H9K.js → useFileDiffOverlay-rXLIL2NF.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-Drh5GEnL.js → wrap-text-qIYQ4a_W.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-CJj9DZLn.js → zoom-out-fZXCEFsy.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/uv.lock +1155 -0
- package/src/ui/dist/assets/LabPlugin-D9jVIo0A.css +0 -2698
|
@@ -5,6 +5,7 @@ import json
|
|
|
5
5
|
import mimetypes
|
|
6
6
|
import os
|
|
7
7
|
import shutil
|
|
8
|
+
import subprocess
|
|
8
9
|
import threading
|
|
9
10
|
import time
|
|
10
11
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
@@ -16,7 +17,7 @@ from urllib.request import Request, urlopen
|
|
|
16
17
|
from ..artifact import ArtifactService
|
|
17
18
|
from ..bash_exec import BashExecService
|
|
18
19
|
from ..bash_exec.runtime import TerminalClient
|
|
19
|
-
from ..bridges import
|
|
20
|
+
from ..bridges import register_builtin_connector_bridges
|
|
20
21
|
from ..bridges.connectors import QQConnectorBridge
|
|
21
22
|
from ..channels import QQRelayChannel, get_channel_factory, list_channel_names, register_builtin_channels
|
|
22
23
|
from ..channels.discord_gateway import DiscordGatewayService
|
|
@@ -26,13 +27,15 @@ from ..channels.slack_socket import SlackSocketModeService
|
|
|
26
27
|
from ..channels.telegram_polling import TelegramPollingService
|
|
27
28
|
from ..channels.whatsapp_local_session import WhatsAppLocalSessionService
|
|
28
29
|
from ..cloud import CloudLinkService
|
|
29
|
-
from ..
|
|
30
|
+
from ..connector_profiles import PROFILEABLE_CONNECTOR_NAMES, connector_profile_label, list_connector_profiles, merge_connector_profile_config
|
|
31
|
+
from ..connector_runtime import conversation_identity_key, format_conversation_id, normalize_conversation_id, parse_conversation_id
|
|
30
32
|
from ..config import ConfigManager
|
|
31
33
|
from ..home import repo_root
|
|
32
34
|
from ..memory import MemoryService
|
|
33
35
|
from ..latex_runtime import QuestLatexService
|
|
34
36
|
from ..prompts import PromptBuilder
|
|
35
37
|
from ..prompts.builder import STANDARD_SKILLS
|
|
38
|
+
from ..qq_profiles import list_qq_profiles, merge_qq_profile_config
|
|
36
39
|
from ..quest import QuestService
|
|
37
40
|
from ..runners import CodexRunner, RunRequest, get_runner_factory, register_builtin_runners
|
|
38
41
|
from ..runtime_logs import JsonlLogger
|
|
@@ -61,7 +64,7 @@ class DaemonApp:
|
|
|
61
64
|
self.repo_root = repo_root()
|
|
62
65
|
self.config_manager = ConfigManager(home)
|
|
63
66
|
self.runners_config = self.config_manager.load_named("runners")
|
|
64
|
-
self.connectors_config = self.config_manager.
|
|
67
|
+
self.connectors_config = self.config_manager.load_named_normalized("connectors")
|
|
65
68
|
self.skill_installer = SkillInstaller(self.repo_root, home)
|
|
66
69
|
self.quest_service = QuestService(home, skill_installer=self.skill_installer)
|
|
67
70
|
self.latex_service = QuestLatexService(self.quest_service)
|
|
@@ -106,22 +109,310 @@ class DaemonApp:
|
|
|
106
109
|
self._terminal_attach_thread: threading.Thread | None = None
|
|
107
110
|
self._terminal_attach_host: str | None = None
|
|
108
111
|
self._terminal_attach_port: int | None = None
|
|
112
|
+
self._serve_host: str | None = None
|
|
113
|
+
self._serve_port: int | None = None
|
|
109
114
|
self._shutdown_requested = threading.Event()
|
|
110
|
-
self.
|
|
111
|
-
self._telegram_polling: TelegramPollingService
|
|
112
|
-
self._slack_socket: SlackSocketModeService
|
|
113
|
-
self._discord_gateway: DiscordGatewayService
|
|
114
|
-
self._feishu_long_connection: FeishuLongConnectionService
|
|
115
|
-
self._whatsapp_local_session: WhatsAppLocalSessionService
|
|
115
|
+
self._qq_gateways: dict[str, QQGatewayService] = {}
|
|
116
|
+
self._telegram_polling: dict[str, TelegramPollingService] = {}
|
|
117
|
+
self._slack_socket: dict[str, SlackSocketModeService] = {}
|
|
118
|
+
self._discord_gateway: dict[str, DiscordGatewayService] = {}
|
|
119
|
+
self._feishu_long_connection: dict[str, FeishuLongConnectionService] = {}
|
|
120
|
+
self._whatsapp_local_session: dict[str, WhatsAppLocalSessionService] = {}
|
|
116
121
|
self.handlers = ApiHandlers(self)
|
|
117
122
|
|
|
118
123
|
def list_connector_statuses(self) -> list[dict[str, object]]:
|
|
119
|
-
|
|
124
|
+
title_by_quest = self._quest_titles_by_id()
|
|
125
|
+
items = [self._augment_connector_status(channel.status(), title_by_quest=title_by_quest) for channel in self.channels.values()]
|
|
120
126
|
lingzhu_config = self.connectors_config.get("lingzhu")
|
|
121
127
|
if isinstance(lingzhu_config, dict):
|
|
122
|
-
items.append(self.config_manager.lingzhu_snapshot(lingzhu_config))
|
|
128
|
+
items.append(self._augment_connector_status(self.config_manager.lingzhu_snapshot(lingzhu_config), title_by_quest=title_by_quest))
|
|
123
129
|
return items
|
|
124
130
|
|
|
131
|
+
def _quest_titles_by_id(self) -> dict[str, str | None]:
|
|
132
|
+
return {
|
|
133
|
+
str(item.get("quest_id") or "").strip(): str(item.get("title") or "").strip() or None
|
|
134
|
+
for item in self.quest_service.list_quests()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
def _augment_connector_status(
|
|
138
|
+
self,
|
|
139
|
+
snapshot: dict[str, object],
|
|
140
|
+
*,
|
|
141
|
+
title_by_quest: dict[str, str | None] | None = None,
|
|
142
|
+
) -> dict[str, object]:
|
|
143
|
+
if not isinstance(snapshot, dict):
|
|
144
|
+
return snapshot
|
|
145
|
+
connector_name = str(snapshot.get("name") or "").strip().lower()
|
|
146
|
+
titles = title_by_quest or self._quest_titles_by_id()
|
|
147
|
+
binding_map: dict[str, dict[str, str | None]] = {}
|
|
148
|
+
for raw in snapshot.get("bindings") or []:
|
|
149
|
+
if not isinstance(raw, dict):
|
|
150
|
+
continue
|
|
151
|
+
conversation_id = str(raw.get("conversation_id") or "").strip()
|
|
152
|
+
if not conversation_id:
|
|
153
|
+
continue
|
|
154
|
+
quest_id = str(raw.get("quest_id") or "").strip() or None
|
|
155
|
+
binding_map[conversation_identity_key(conversation_id)] = {
|
|
156
|
+
"quest_id": quest_id,
|
|
157
|
+
"quest_title": titles.get(quest_id or ""),
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
def augment_target(target: object) -> dict[str, object] | None:
|
|
161
|
+
if not isinstance(target, dict):
|
|
162
|
+
return None
|
|
163
|
+
payload = dict(target)
|
|
164
|
+
conversation_id = str(payload.get("conversation_id") or "").strip()
|
|
165
|
+
if not conversation_id:
|
|
166
|
+
return payload
|
|
167
|
+
binding = binding_map.get(conversation_identity_key(conversation_id)) or {}
|
|
168
|
+
bound_quest_id = str(binding.get("quest_id") or "").strip() or None
|
|
169
|
+
bound_quest_title = str(binding.get("quest_title") or "").strip() or None
|
|
170
|
+
if bound_quest_id:
|
|
171
|
+
payload["bound_quest_id"] = bound_quest_id
|
|
172
|
+
payload["bound_quest_title"] = bound_quest_title
|
|
173
|
+
payload["is_bound"] = True
|
|
174
|
+
payload["warning"] = f"Currently bound to {bound_quest_id}"
|
|
175
|
+
else:
|
|
176
|
+
payload["bound_quest_id"] = None
|
|
177
|
+
payload["bound_quest_title"] = None
|
|
178
|
+
payload["is_bound"] = False
|
|
179
|
+
payload["selectable"] = True
|
|
180
|
+
if not payload.get("connector") and connector_name:
|
|
181
|
+
payload["connector"] = connector_name
|
|
182
|
+
return payload
|
|
183
|
+
|
|
184
|
+
known_targets = [item for item in (augment_target(target) for target in snapshot.get("known_targets") or []) if item is not None]
|
|
185
|
+
discovered_targets = [item for item in (augment_target(target) for target in snapshot.get("discovered_targets") or []) if item is not None]
|
|
186
|
+
bindings = []
|
|
187
|
+
for raw in snapshot.get("bindings") or []:
|
|
188
|
+
if not isinstance(raw, dict):
|
|
189
|
+
continue
|
|
190
|
+
payload = dict(raw)
|
|
191
|
+
quest_id = str(payload.get("quest_id") or "").strip() or None
|
|
192
|
+
payload["quest_title"] = titles.get(quest_id or "")
|
|
193
|
+
bindings.append(payload)
|
|
194
|
+
payload = dict(snapshot)
|
|
195
|
+
if known_targets:
|
|
196
|
+
payload["known_targets"] = known_targets
|
|
197
|
+
if discovered_targets:
|
|
198
|
+
payload["discovered_targets"] = discovered_targets
|
|
199
|
+
payload["target_count"] = len(discovered_targets)
|
|
200
|
+
default_target = augment_target(snapshot.get("default_target"))
|
|
201
|
+
if default_target is not None:
|
|
202
|
+
payload["default_target"] = default_target
|
|
203
|
+
if bindings:
|
|
204
|
+
payload["bindings"] = bindings
|
|
205
|
+
profiles_payload = []
|
|
206
|
+
for raw_profile in snapshot.get("profiles") or []:
|
|
207
|
+
if not isinstance(raw_profile, dict):
|
|
208
|
+
continue
|
|
209
|
+
profile_payload = dict(raw_profile)
|
|
210
|
+
profile_payload["discovered_targets"] = [
|
|
211
|
+
item
|
|
212
|
+
for item in (
|
|
213
|
+
augment_target(target)
|
|
214
|
+
for target in raw_profile.get("discovered_targets") or []
|
|
215
|
+
)
|
|
216
|
+
if item is not None
|
|
217
|
+
]
|
|
218
|
+
profile_payload["recent_conversations"] = [
|
|
219
|
+
dict(item)
|
|
220
|
+
for item in raw_profile.get("recent_conversations") or []
|
|
221
|
+
if isinstance(item, dict)
|
|
222
|
+
]
|
|
223
|
+
profile_payload["bindings"] = [
|
|
224
|
+
{
|
|
225
|
+
**dict(item),
|
|
226
|
+
"quest_title": titles.get(str(item.get("quest_id") or "").strip() or ""),
|
|
227
|
+
}
|
|
228
|
+
for item in raw_profile.get("bindings") or []
|
|
229
|
+
if isinstance(item, dict)
|
|
230
|
+
]
|
|
231
|
+
profiles_payload.append(profile_payload)
|
|
232
|
+
if profiles_payload:
|
|
233
|
+
payload["profiles"] = profiles_payload
|
|
234
|
+
return payload
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _connector_has_delivery_target(snapshot: dict[str, object]) -> bool:
|
|
238
|
+
if str(snapshot.get("main_chat_id") or "").strip():
|
|
239
|
+
return True
|
|
240
|
+
if str(snapshot.get("last_conversation_id") or "").strip():
|
|
241
|
+
return True
|
|
242
|
+
if isinstance(snapshot.get("default_target"), dict) and snapshot.get("default_target"):
|
|
243
|
+
return True
|
|
244
|
+
if isinstance(snapshot.get("bindings"), list) and snapshot.get("bindings"):
|
|
245
|
+
return True
|
|
246
|
+
if isinstance(snapshot.get("recent_conversations"), list) and snapshot.get("recent_conversations"):
|
|
247
|
+
return True
|
|
248
|
+
if isinstance(snapshot.get("discovered_targets"), list) and snapshot.get("discovered_targets"):
|
|
249
|
+
return True
|
|
250
|
+
for key in ("binding_count", "target_count"):
|
|
251
|
+
try:
|
|
252
|
+
if int(snapshot.get(key) or 0) > 0:
|
|
253
|
+
return True
|
|
254
|
+
except (TypeError, ValueError):
|
|
255
|
+
continue
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
def connector_availability_summary(self) -> dict[str, object]:
|
|
259
|
+
available_connectors: list[dict[str, object]] = []
|
|
260
|
+
preferred_connector_name: str | None = None
|
|
261
|
+
preferred_conversation_id: str | None = None
|
|
262
|
+
has_enabled_external_connector = False
|
|
263
|
+
has_bound_external_connector = False
|
|
264
|
+
|
|
265
|
+
for item in self.list_connector_statuses():
|
|
266
|
+
if not isinstance(item, dict):
|
|
267
|
+
continue
|
|
268
|
+
name = str(item.get("name") or "").strip()
|
|
269
|
+
if not name or name == "local":
|
|
270
|
+
continue
|
|
271
|
+
enabled = bool(item.get("enabled"))
|
|
272
|
+
connection_state = str(item.get("connection_state") or "").strip() or None
|
|
273
|
+
has_target = self._connector_has_delivery_target(item)
|
|
274
|
+
if enabled:
|
|
275
|
+
has_enabled_external_connector = True
|
|
276
|
+
if enabled and has_target:
|
|
277
|
+
has_bound_external_connector = True
|
|
278
|
+
if preferred_connector_name is None:
|
|
279
|
+
preferred_connector_name = name
|
|
280
|
+
preferred_conversation_id = str(
|
|
281
|
+
((item.get("default_target") or {}) if isinstance(item.get("default_target"), dict) else {}).get(
|
|
282
|
+
"conversation_id"
|
|
283
|
+
)
|
|
284
|
+
or item.get("last_conversation_id")
|
|
285
|
+
or ""
|
|
286
|
+
).strip() or None
|
|
287
|
+
available_connectors.append(
|
|
288
|
+
{
|
|
289
|
+
"name": name,
|
|
290
|
+
"enabled": enabled,
|
|
291
|
+
"connection_state": connection_state,
|
|
292
|
+
"binding_count": int(item.get("binding_count") or 0),
|
|
293
|
+
"target_count": int(item.get("target_count") or 0),
|
|
294
|
+
"has_delivery_target": has_target,
|
|
295
|
+
}
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
"has_enabled_external_connector": has_enabled_external_connector,
|
|
300
|
+
"has_bound_external_connector": has_bound_external_connector,
|
|
301
|
+
"should_recommend_binding": not has_bound_external_connector,
|
|
302
|
+
"preferred_connector_name": preferred_connector_name,
|
|
303
|
+
"preferred_conversation_id": preferred_conversation_id,
|
|
304
|
+
"available_connectors": available_connectors,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
def _normalize_requested_connector_bindings(
|
|
308
|
+
self,
|
|
309
|
+
requested_connector_bindings: list[dict[str, object]] | None,
|
|
310
|
+
) -> list[dict[str, str | None]]:
|
|
311
|
+
items = requested_connector_bindings if isinstance(requested_connector_bindings, list) else []
|
|
312
|
+
normalized_by_connector: dict[str, dict[str, str | None]] = {}
|
|
313
|
+
for raw in items:
|
|
314
|
+
if not isinstance(raw, dict):
|
|
315
|
+
continue
|
|
316
|
+
raw_connector_name = str(raw.get("connector") or "").strip().lower()
|
|
317
|
+
conversation_id = normalize_conversation_id(raw.get("conversation_id"))
|
|
318
|
+
parsed = parse_conversation_id(conversation_id)
|
|
319
|
+
connector_name = raw_connector_name
|
|
320
|
+
if parsed is not None:
|
|
321
|
+
connector_name = str(parsed.get("connector") or connector_name).strip().lower()
|
|
322
|
+
if not connector_name or connector_name == "local":
|
|
323
|
+
continue
|
|
324
|
+
if connector_name not in self.channels:
|
|
325
|
+
continue
|
|
326
|
+
if parsed is not None and str(parsed.get("connector") or "").strip().lower() != connector_name:
|
|
327
|
+
continue
|
|
328
|
+
normalized_by_connector[connector_name] = {
|
|
329
|
+
"connector": connector_name,
|
|
330
|
+
"conversation_id": conversation_id or None,
|
|
331
|
+
}
|
|
332
|
+
return list(normalized_by_connector.values())
|
|
333
|
+
|
|
334
|
+
def _launcher_update_base_command(self) -> list[str]:
|
|
335
|
+
node_binary = str(os.environ.get("DEEPSCIENTIST_NODE_BINARY") or "").strip() or which("node") or which("nodejs")
|
|
336
|
+
launcher_path = str(os.environ.get("DEEPSCIENTIST_LAUNCHER_PATH") or "").strip()
|
|
337
|
+
if not launcher_path:
|
|
338
|
+
launcher_path = str(self.repo_root / "bin" / "ds.js")
|
|
339
|
+
if not node_binary:
|
|
340
|
+
raise RuntimeError("Node.js is not available on PATH, so DeepScientist cannot check npm updates.")
|
|
341
|
+
if not Path(launcher_path).exists():
|
|
342
|
+
raise RuntimeError(f"DeepScientist launcher path does not exist: {launcher_path}")
|
|
343
|
+
return [node_binary, launcher_path, "update", "--home", str(self.home)]
|
|
344
|
+
|
|
345
|
+
def system_update_status(self) -> dict[str, object]:
|
|
346
|
+
command = [*self._launcher_update_base_command(), "--check", "--json"]
|
|
347
|
+
try:
|
|
348
|
+
result = subprocess.run(
|
|
349
|
+
command,
|
|
350
|
+
cwd=str(self.repo_root),
|
|
351
|
+
capture_output=True,
|
|
352
|
+
text=True,
|
|
353
|
+
timeout=8,
|
|
354
|
+
check=False,
|
|
355
|
+
env=os.environ.copy(),
|
|
356
|
+
)
|
|
357
|
+
except subprocess.TimeoutExpired as exc:
|
|
358
|
+
raise RuntimeError("DeepScientist update check timed out.") from exc
|
|
359
|
+
if result.returncode != 0:
|
|
360
|
+
raise RuntimeError((result.stderr or result.stdout or "Update check failed.").strip())
|
|
361
|
+
try:
|
|
362
|
+
payload = json.loads(result.stdout or "{}")
|
|
363
|
+
except json.JSONDecodeError as exc:
|
|
364
|
+
raise RuntimeError("DeepScientist update check returned invalid JSON.") from exc
|
|
365
|
+
if not isinstance(payload, dict):
|
|
366
|
+
raise RuntimeError("DeepScientist update check returned an invalid payload.")
|
|
367
|
+
return payload
|
|
368
|
+
|
|
369
|
+
def request_system_update(self, *, action: str) -> dict[str, object]:
|
|
370
|
+
normalized = str(action or "").strip().lower()
|
|
371
|
+
if normalized not in {"install_latest", "remind_later", "skip_version"}:
|
|
372
|
+
raise ValueError(f"Unsupported update action `{action}`.")
|
|
373
|
+
command = self._launcher_update_base_command()
|
|
374
|
+
if normalized == "install_latest":
|
|
375
|
+
host = self._serve_host or "0.0.0.0"
|
|
376
|
+
port = self._serve_port or 20999
|
|
377
|
+
command.extend(
|
|
378
|
+
[
|
|
379
|
+
"--yes",
|
|
380
|
+
"--background",
|
|
381
|
+
"--restart-daemon",
|
|
382
|
+
"--host",
|
|
383
|
+
str(host),
|
|
384
|
+
"--port",
|
|
385
|
+
str(port),
|
|
386
|
+
"--json",
|
|
387
|
+
]
|
|
388
|
+
)
|
|
389
|
+
elif normalized == "remind_later":
|
|
390
|
+
command.extend(["--remind-later", "--json"])
|
|
391
|
+
else:
|
|
392
|
+
command.extend(["--skip-version", "--json"])
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
result = subprocess.run(
|
|
396
|
+
command,
|
|
397
|
+
cwd=str(self.repo_root),
|
|
398
|
+
capture_output=True,
|
|
399
|
+
text=True,
|
|
400
|
+
timeout=8,
|
|
401
|
+
check=False,
|
|
402
|
+
env=os.environ.copy(),
|
|
403
|
+
)
|
|
404
|
+
except subprocess.TimeoutExpired as exc:
|
|
405
|
+
raise RuntimeError("DeepScientist update request timed out.") from exc
|
|
406
|
+
if result.returncode != 0:
|
|
407
|
+
raise RuntimeError((result.stderr or result.stdout or "Update request failed.").strip())
|
|
408
|
+
try:
|
|
409
|
+
payload = json.loads(result.stdout or "{}")
|
|
410
|
+
except json.JSONDecodeError as exc:
|
|
411
|
+
raise RuntimeError("DeepScientist update request returned invalid JSON.") from exc
|
|
412
|
+
if not isinstance(payload, dict):
|
|
413
|
+
raise RuntimeError("DeepScientist update request returned an invalid payload.")
|
|
414
|
+
return payload
|
|
415
|
+
|
|
125
416
|
def _process_terminal_attach_request(
|
|
126
417
|
self,
|
|
127
418
|
connection: ServerConnection,
|
|
@@ -322,7 +613,7 @@ class DaemonApp:
|
|
|
322
613
|
return factory(home=self.home, app=self, config=self.connectors_config.get(name, {}))
|
|
323
614
|
|
|
324
615
|
def reload_connectors_config(self, *, restart_background: bool = True) -> dict[str, object]:
|
|
325
|
-
self.connectors_config = self.config_manager.
|
|
616
|
+
self.connectors_config = self.config_manager.load_named_normalized("connectors")
|
|
326
617
|
register_builtin_channels(home=self.home, connectors_config=self.connectors_config)
|
|
327
618
|
for name, channel in self.channels.items():
|
|
328
619
|
config = self.connectors_config.get(name, {})
|
|
@@ -410,9 +701,12 @@ class DaemonApp:
|
|
|
410
701
|
announce_connector_binding: bool = True,
|
|
411
702
|
exclude_conversation_id: str | None = None,
|
|
412
703
|
preferred_connector_conversation_id: str | None = None,
|
|
704
|
+
requested_connector_bindings: list[dict[str, object]] | None = None,
|
|
705
|
+
force_connector_rebind: bool = False,
|
|
413
706
|
requested_baseline_ref: dict[str, object] | None = None,
|
|
414
707
|
startup_contract: dict[str, object] | None = None,
|
|
415
708
|
) -> dict:
|
|
709
|
+
normalized_requested_bindings = self._normalize_requested_connector_bindings(requested_connector_bindings)
|
|
416
710
|
snapshot = self.quest_service.create(
|
|
417
711
|
goal=goal,
|
|
418
712
|
title=title,
|
|
@@ -460,7 +754,51 @@ class DaemonApp:
|
|
|
460
754
|
) from exc
|
|
461
755
|
preferred_binding = normalize_conversation_id(preferred_connector_conversation_id)
|
|
462
756
|
preferred_parsed = parse_conversation_id(preferred_binding)
|
|
463
|
-
if
|
|
757
|
+
if normalized_requested_bindings:
|
|
758
|
+
try:
|
|
759
|
+
binding_result = self.update_quest_bindings(
|
|
760
|
+
snapshot["quest_id"],
|
|
761
|
+
normalized_requested_bindings,
|
|
762
|
+
force=force_connector_rebind,
|
|
763
|
+
)
|
|
764
|
+
if isinstance(binding_result, tuple):
|
|
765
|
+
raise RuntimeError(str(binding_result[1].get("message") or "Unable to bind connector targets."))
|
|
766
|
+
if announce_connector_binding:
|
|
767
|
+
for result in binding_result.get("results") or []:
|
|
768
|
+
if not isinstance(result, dict):
|
|
769
|
+
continue
|
|
770
|
+
conversation_id = str(result.get("conversation_id") or "").strip()
|
|
771
|
+
connector_name = str(result.get("connector") or "").strip().lower()
|
|
772
|
+
if not conversation_id or not connector_name or connector_name == "local":
|
|
773
|
+
continue
|
|
774
|
+
channel = self._channel_with_bindings(connector_name)
|
|
775
|
+
channel.send(
|
|
776
|
+
{
|
|
777
|
+
"conversation_id": conversation_id,
|
|
778
|
+
"quest_id": snapshot["quest_id"],
|
|
779
|
+
"kind": "ack",
|
|
780
|
+
"message": self._quest_created_connector_message(
|
|
781
|
+
connector_name,
|
|
782
|
+
quest_id=snapshot["quest_id"],
|
|
783
|
+
goal=goal,
|
|
784
|
+
previous_quest_id=str(result.get("previous_quest_id") or "").strip() or None,
|
|
785
|
+
),
|
|
786
|
+
}
|
|
787
|
+
)
|
|
788
|
+
snapshot = self.quest_service.snapshot(snapshot["quest_id"])
|
|
789
|
+
except Exception as exc:
|
|
790
|
+
shutil.rmtree(Path(snapshot["quest_root"]), ignore_errors=True)
|
|
791
|
+
self.sessions.forget(snapshot["quest_id"])
|
|
792
|
+
self.logger.log(
|
|
793
|
+
"warning",
|
|
794
|
+
"quest.connector_binding_failed",
|
|
795
|
+
quest_id=snapshot.get("quest_id"),
|
|
796
|
+
message=str(exc),
|
|
797
|
+
)
|
|
798
|
+
raise RuntimeError(
|
|
799
|
+
f"Quest creation failed because one or more selected connector targets could not be bound: {exc}"
|
|
800
|
+
) from exc
|
|
801
|
+
elif (
|
|
464
802
|
preferred_binding
|
|
465
803
|
and preferred_parsed
|
|
466
804
|
and str(preferred_parsed.get("connector") or "").strip().lower() in self.channels
|
|
@@ -492,7 +830,7 @@ class DaemonApp:
|
|
|
492
830
|
),
|
|
493
831
|
}
|
|
494
832
|
)
|
|
495
|
-
|
|
833
|
+
else:
|
|
496
834
|
self._auto_bind_connectors_to_latest_quest(
|
|
497
835
|
snapshot["quest_id"],
|
|
498
836
|
goal=goal,
|
|
@@ -1698,50 +2036,54 @@ class DaemonApp:
|
|
|
1698
2036
|
channel = self._channel_with_bindings(connector_name)
|
|
1699
2037
|
return channel.list_bindings()
|
|
1700
2038
|
|
|
1701
|
-
def
|
|
2039
|
+
def preview_connector_binding_conflicts(
|
|
1702
2040
|
self,
|
|
1703
|
-
|
|
1704
|
-
conversation_id: str | None,
|
|
2041
|
+
requested_bindings: list[dict[str, object]] | None,
|
|
1705
2042
|
*,
|
|
1706
|
-
|
|
1707
|
-
) -> dict
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
2043
|
+
quest_id: str | None = None,
|
|
2044
|
+
) -> list[dict[str, object]]:
|
|
2045
|
+
normalized_bindings = self._normalize_requested_connector_bindings(requested_bindings)
|
|
2046
|
+
conflicts: list[dict[str, object]] = []
|
|
2047
|
+
seen: set[tuple[str, str]] = set()
|
|
2048
|
+
for item in normalized_bindings:
|
|
2049
|
+
connector_name = str(item.get("connector") or "").strip().lower()
|
|
2050
|
+
conversation_id = str(item.get("conversation_id") or "").strip()
|
|
2051
|
+
if not connector_name or not conversation_id:
|
|
2052
|
+
continue
|
|
2053
|
+
for conflict in self._inspect_connector_binding_conflicts(quest_id, conversation_id):
|
|
2054
|
+
conflict_quest_id = str(conflict.get("quest_id") or "").strip()
|
|
2055
|
+
identity = (connector_name, conflict_quest_id)
|
|
2056
|
+
if not conflict_quest_id or identity in seen:
|
|
2057
|
+
continue
|
|
2058
|
+
seen.add(identity)
|
|
2059
|
+
conflicts.append(
|
|
2060
|
+
{
|
|
2061
|
+
"connector": connector_name,
|
|
2062
|
+
"conversation_id": conversation_id,
|
|
2063
|
+
**conflict,
|
|
2064
|
+
}
|
|
2065
|
+
)
|
|
2066
|
+
return conflicts
|
|
1711
2067
|
|
|
2068
|
+
def _inspect_connector_binding_conflicts(
|
|
2069
|
+
self,
|
|
2070
|
+
quest_id: str | None,
|
|
2071
|
+
conversation_id: str,
|
|
2072
|
+
) -> list[dict[str, object]]:
|
|
1712
2073
|
normalized = normalize_conversation_id(conversation_id)
|
|
1713
2074
|
parsed = parse_conversation_id(normalized)
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
removed = self._unbind_external_bindings(quest_id)
|
|
1717
|
-
self.quest_service.set_binding_sources(quest_id, ["local:default"])
|
|
1718
|
-
snapshot = self.quest_service.snapshot(quest_id)
|
|
1719
|
-
return {
|
|
1720
|
-
"ok": True,
|
|
1721
|
-
"quest_id": quest_id,
|
|
1722
|
-
"conversation_id": None,
|
|
1723
|
-
"snapshot": snapshot,
|
|
1724
|
-
"removed_conversations": removed,
|
|
1725
|
-
}
|
|
1726
|
-
|
|
2075
|
+
if parsed is None or str(parsed.get("connector") or "").strip().lower() == "local":
|
|
2076
|
+
return []
|
|
1727
2077
|
connector_name = str(parsed.get("connector") or "").strip().lower()
|
|
1728
|
-
if connector_name not in self.channels
|
|
1729
|
-
return
|
|
1730
|
-
"ok": False,
|
|
1731
|
-
"message": f"Unknown connector `{connector_name}` for conversation `{normalized}`.",
|
|
1732
|
-
}
|
|
1733
|
-
|
|
2078
|
+
if connector_name not in self.channels:
|
|
2079
|
+
return []
|
|
1734
2080
|
channel = self._channel_with_bindings(connector_name)
|
|
1735
2081
|
conversation_key = conversation_identity_key(normalized)
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
str(item.get("quest_id") or "").strip(): str(item.get("title") or "").strip() or None
|
|
1739
|
-
for item in self.quest_service.list_quests()
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
conflicts: list[dict] = []
|
|
2082
|
+
titles = self._quest_titles_by_id()
|
|
2083
|
+
conflicts: list[dict[str, object]] = []
|
|
1743
2084
|
existing_bound = channel.resolve_bound_quest(normalized)
|
|
1744
|
-
|
|
2085
|
+
normalized_quest_id = str(quest_id or "").strip() or None
|
|
2086
|
+
if existing_bound and existing_bound != normalized_quest_id:
|
|
1745
2087
|
conflicts.append(
|
|
1746
2088
|
{
|
|
1747
2089
|
"quest_id": existing_bound,
|
|
@@ -1749,10 +2091,9 @@ class DaemonApp:
|
|
|
1749
2091
|
"reason": "connector_binding",
|
|
1750
2092
|
}
|
|
1751
2093
|
)
|
|
1752
|
-
|
|
1753
2094
|
for item in self.quest_service.list_quests():
|
|
1754
2095
|
other_id = str(item.get("quest_id") or "").strip()
|
|
1755
|
-
if not other_id or other_id ==
|
|
2096
|
+
if not other_id or other_id == normalized_quest_id:
|
|
1756
2097
|
continue
|
|
1757
2098
|
sources = self.quest_service.binding_sources(other_id)
|
|
1758
2099
|
if any(conversation_identity_key(source) == conversation_key for source in sources):
|
|
@@ -1763,8 +2104,7 @@ class DaemonApp:
|
|
|
1763
2104
|
"reason": "quest_binding",
|
|
1764
2105
|
}
|
|
1765
2106
|
)
|
|
1766
|
-
|
|
1767
|
-
deduped_conflicts: list[dict] = []
|
|
2107
|
+
deduped_conflicts: list[dict[str, object]] = []
|
|
1768
2108
|
seen_conflict_ids: set[str] = set()
|
|
1769
2109
|
for item in conflicts:
|
|
1770
2110
|
candidate = str(item.get("quest_id") or "").strip()
|
|
@@ -1772,34 +2112,91 @@ class DaemonApp:
|
|
|
1772
2112
|
continue
|
|
1773
2113
|
seen_conflict_ids.add(candidate)
|
|
1774
2114
|
deduped_conflicts.append(item)
|
|
2115
|
+
return deduped_conflicts
|
|
1775
2116
|
|
|
1776
|
-
|
|
2117
|
+
def _unbind_quest_connector_bindings(
|
|
2118
|
+
self,
|
|
2119
|
+
quest_id: str,
|
|
2120
|
+
connector_name: str,
|
|
2121
|
+
*,
|
|
2122
|
+
preserve: set[str] | None = None,
|
|
2123
|
+
) -> list[str]:
|
|
2124
|
+
normalized_connector = str(connector_name or "").strip().lower()
|
|
2125
|
+
preserve_keys = {conversation_identity_key(item) for item in (preserve or set()) if item}
|
|
2126
|
+
removed: list[str] = []
|
|
2127
|
+
if not normalized_connector or normalized_connector == "local":
|
|
2128
|
+
return removed
|
|
2129
|
+
try:
|
|
2130
|
+
channel = self._channel_with_bindings(normalized_connector)
|
|
2131
|
+
except Exception:
|
|
2132
|
+
return removed
|
|
2133
|
+
for item in channel.list_bindings():
|
|
2134
|
+
if str(item.get("quest_id") or "").strip() != quest_id:
|
|
2135
|
+
continue
|
|
2136
|
+
conversation_id = str(item.get("conversation_id") or "").strip()
|
|
2137
|
+
if not conversation_id:
|
|
2138
|
+
continue
|
|
2139
|
+
if preserve_keys and conversation_identity_key(conversation_id) in preserve_keys:
|
|
2140
|
+
continue
|
|
2141
|
+
if channel.unbind_conversation(conversation_id, quest_id=quest_id):
|
|
2142
|
+
removed.append(conversation_id)
|
|
2143
|
+
self.sessions.unbind(quest_id, conversation_id)
|
|
2144
|
+
for conversation_id in removed:
|
|
2145
|
+
self.quest_service.unbind_source(quest_id, conversation_id)
|
|
2146
|
+
return removed
|
|
2147
|
+
|
|
2148
|
+
def _apply_conversation_binding(
|
|
2149
|
+
self,
|
|
2150
|
+
quest_id: str,
|
|
2151
|
+
conversation_id: str,
|
|
2152
|
+
*,
|
|
2153
|
+
force: bool = False,
|
|
2154
|
+
clear_scope: str = "connector",
|
|
2155
|
+
) -> dict | tuple[int, dict]:
|
|
2156
|
+
quest_root = self.home / "quests" / quest_id
|
|
2157
|
+
if not quest_root.joinpath("quest.yaml").exists():
|
|
2158
|
+
return 404, {"ok": False, "message": f"Unknown quest `{quest_id}`."}
|
|
2159
|
+
normalized = normalize_conversation_id(conversation_id)
|
|
2160
|
+
parsed = parse_conversation_id(normalized)
|
|
2161
|
+
if parsed is None:
|
|
2162
|
+
return 400, {"ok": False, "message": f"Invalid connector conversation `{conversation_id}`."}
|
|
2163
|
+
connector_name = str(parsed.get("connector") or "").strip().lower()
|
|
2164
|
+
if not connector_name or connector_name == "local" or connector_name not in self.channels:
|
|
2165
|
+
return 400, {"ok": False, "message": f"Unknown connector `{connector_name}` for conversation `{normalized}`."}
|
|
2166
|
+
channel = self._channel_with_bindings(connector_name)
|
|
2167
|
+
conflicts = self._inspect_connector_binding_conflicts(quest_id, normalized)
|
|
2168
|
+
if conflicts and not force:
|
|
1777
2169
|
return 409, {
|
|
1778
2170
|
"ok": False,
|
|
1779
2171
|
"conflict": True,
|
|
1780
2172
|
"message": "Conversation is already bound to another quest.",
|
|
1781
2173
|
"quest_id": quest_id,
|
|
2174
|
+
"connector": connector_name,
|
|
1782
2175
|
"conversation_id": normalized,
|
|
1783
|
-
"conflicts":
|
|
2176
|
+
"conflicts": conflicts,
|
|
1784
2177
|
}
|
|
1785
|
-
|
|
1786
|
-
for item in
|
|
2178
|
+
existing_bound = channel.resolve_bound_quest(normalized)
|
|
2179
|
+
for item in conflicts:
|
|
1787
2180
|
other_id = str(item.get("quest_id") or "").strip()
|
|
1788
2181
|
if other_id and other_id != quest_id:
|
|
1789
2182
|
self.quest_service.unbind_source(other_id, normalized)
|
|
1790
2183
|
self.sessions.unbind(other_id, normalized)
|
|
1791
|
-
|
|
1792
2184
|
channel.bind_conversation(normalized, quest_id)
|
|
1793
2185
|
self.sessions.bind(quest_id, normalized)
|
|
1794
2186
|
self.quest_service.bind_source(quest_id, "local:default")
|
|
1795
2187
|
self.quest_service.bind_source(quest_id, normalized)
|
|
1796
|
-
|
|
2188
|
+
if clear_scope == "all_external":
|
|
2189
|
+
removed = self._unbind_external_bindings(quest_id, preserve={normalized})
|
|
2190
|
+
elif clear_scope == "connector":
|
|
2191
|
+
removed = self._unbind_quest_connector_bindings(quest_id, connector_name, preserve={normalized})
|
|
2192
|
+
else:
|
|
2193
|
+
removed = []
|
|
1797
2194
|
snapshot = self.quest_service.snapshot(quest_id)
|
|
1798
2195
|
previous_quest_id = str(existing_bound or "").strip() or None
|
|
1799
2196
|
if previous_quest_id == quest_id:
|
|
1800
2197
|
previous_quest_id = None
|
|
1801
2198
|
if previous_quest_id is None:
|
|
1802
|
-
for item in
|
|
2199
|
+
for item in conflicts:
|
|
1803
2200
|
candidate = str(item.get("quest_id") or "").strip()
|
|
1804
2201
|
if candidate and candidate != quest_id:
|
|
1805
2202
|
previous_quest_id = candidate
|
|
@@ -1807,13 +2204,123 @@ class DaemonApp:
|
|
|
1807
2204
|
return {
|
|
1808
2205
|
"ok": True,
|
|
1809
2206
|
"quest_id": quest_id,
|
|
2207
|
+
"connector": connector_name,
|
|
1810
2208
|
"conversation_id": normalized,
|
|
1811
2209
|
"snapshot": snapshot,
|
|
1812
2210
|
"removed_conversations": removed,
|
|
1813
|
-
"conflicts_resolved": [item.get("quest_id") for item in
|
|
2211
|
+
"conflicts_resolved": [item.get("quest_id") for item in conflicts if item.get("quest_id")],
|
|
1814
2212
|
"previous_quest_id": previous_quest_id,
|
|
1815
2213
|
}
|
|
1816
2214
|
|
|
2215
|
+
def update_quest_connector_binding(
|
|
2216
|
+
self,
|
|
2217
|
+
quest_id: str,
|
|
2218
|
+
connector_name: str,
|
|
2219
|
+
conversation_id: str | None,
|
|
2220
|
+
*,
|
|
2221
|
+
force: bool = False,
|
|
2222
|
+
) -> dict | tuple[int, dict]:
|
|
2223
|
+
quest_root = self.home / "quests" / quest_id
|
|
2224
|
+
if not quest_root.joinpath("quest.yaml").exists():
|
|
2225
|
+
return 404, {"ok": False, "message": f"Unknown quest `{quest_id}`."}
|
|
2226
|
+
normalized_connector = str(connector_name or "").strip().lower()
|
|
2227
|
+
if not normalized_connector or normalized_connector == "local":
|
|
2228
|
+
return 400, {"ok": False, "message": "A non-local connector name is required."}
|
|
2229
|
+
if normalized_connector not in self.channels:
|
|
2230
|
+
return 400, {"ok": False, "message": f"Unknown connector `{normalized_connector}`."}
|
|
2231
|
+
|
|
2232
|
+
normalized = normalize_conversation_id(conversation_id)
|
|
2233
|
+
if not normalized:
|
|
2234
|
+
removed = self._unbind_quest_connector_bindings(quest_id, normalized_connector)
|
|
2235
|
+
self.quest_service.bind_source(quest_id, "local:default")
|
|
2236
|
+
snapshot = self.quest_service.snapshot(quest_id)
|
|
2237
|
+
return {
|
|
2238
|
+
"ok": True,
|
|
2239
|
+
"quest_id": quest_id,
|
|
2240
|
+
"connector": normalized_connector,
|
|
2241
|
+
"conversation_id": None,
|
|
2242
|
+
"snapshot": snapshot,
|
|
2243
|
+
"removed_conversations": removed,
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
parsed = parse_conversation_id(normalized)
|
|
2247
|
+
if parsed is None:
|
|
2248
|
+
return 400, {"ok": False, "message": f"Invalid connector conversation `{normalized}`."}
|
|
2249
|
+
if str(parsed.get("connector") or "").strip().lower() != normalized_connector:
|
|
2250
|
+
return 400, {
|
|
2251
|
+
"ok": False,
|
|
2252
|
+
"message": f"Conversation `{normalized}` does not belong to connector `{normalized_connector}`.",
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
return self._apply_conversation_binding(quest_id, normalized, force=force, clear_scope="connector")
|
|
2256
|
+
|
|
2257
|
+
def update_quest_bindings(
|
|
2258
|
+
self,
|
|
2259
|
+
quest_id: str,
|
|
2260
|
+
requested_bindings: list[dict[str, object]] | None,
|
|
2261
|
+
*,
|
|
2262
|
+
force: bool = False,
|
|
2263
|
+
) -> dict | tuple[int, dict]:
|
|
2264
|
+
quest_root = self.home / "quests" / quest_id
|
|
2265
|
+
if not quest_root.joinpath("quest.yaml").exists():
|
|
2266
|
+
return 404, {"ok": False, "message": f"Unknown quest `{quest_id}`."}
|
|
2267
|
+
normalized_bindings = self._normalize_requested_connector_bindings(requested_bindings)
|
|
2268
|
+
conflicts = self.preview_connector_binding_conflicts(normalized_bindings, quest_id=quest_id)
|
|
2269
|
+
if conflicts and not force:
|
|
2270
|
+
return 409, {
|
|
2271
|
+
"ok": False,
|
|
2272
|
+
"conflict": True,
|
|
2273
|
+
"message": "One or more connector targets are already bound to another quest.",
|
|
2274
|
+
"quest_id": quest_id,
|
|
2275
|
+
"conflicts": conflicts,
|
|
2276
|
+
}
|
|
2277
|
+
results: list[dict[str, object]] = []
|
|
2278
|
+
for item in normalized_bindings:
|
|
2279
|
+
connector_name = str(item.get("connector") or "").strip().lower()
|
|
2280
|
+
result = self.update_quest_connector_binding(
|
|
2281
|
+
quest_id,
|
|
2282
|
+
connector_name,
|
|
2283
|
+
str(item.get("conversation_id") or "").strip() or None,
|
|
2284
|
+
force=True if force or conflicts else False,
|
|
2285
|
+
)
|
|
2286
|
+
if isinstance(result, tuple):
|
|
2287
|
+
return result
|
|
2288
|
+
results.append(result)
|
|
2289
|
+
snapshot = self.quest_service.snapshot(quest_id)
|
|
2290
|
+
return {
|
|
2291
|
+
"ok": True,
|
|
2292
|
+
"quest_id": quest_id,
|
|
2293
|
+
"snapshot": snapshot,
|
|
2294
|
+
"results": results,
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
def update_quest_binding(
|
|
2298
|
+
self,
|
|
2299
|
+
quest_id: str,
|
|
2300
|
+
conversation_id: str | None,
|
|
2301
|
+
*,
|
|
2302
|
+
force: bool = False,
|
|
2303
|
+
) -> dict | tuple[int, dict]:
|
|
2304
|
+
normalized = normalize_conversation_id(conversation_id)
|
|
2305
|
+
parsed = parse_conversation_id(normalized)
|
|
2306
|
+
|
|
2307
|
+
if parsed is None or parsed.get("connector", "").lower() == "local":
|
|
2308
|
+
removed = self._unbind_external_bindings(quest_id)
|
|
2309
|
+
self.quest_service.set_binding_sources(quest_id, ["local:default"])
|
|
2310
|
+
snapshot = self.quest_service.snapshot(quest_id)
|
|
2311
|
+
return {
|
|
2312
|
+
"ok": True,
|
|
2313
|
+
"quest_id": quest_id,
|
|
2314
|
+
"conversation_id": None,
|
|
2315
|
+
"snapshot": snapshot,
|
|
2316
|
+
"removed_conversations": removed,
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
connector_name = str(parsed.get("connector") or "").strip().lower()
|
|
2320
|
+
if connector_name not in self.channels or connector_name == "local":
|
|
2321
|
+
return 400, {"ok": False, "message": f"Unknown connector `{connector_name}` for conversation `{normalized}`."}
|
|
2322
|
+
return self._apply_conversation_binding(quest_id, normalized, force=force, clear_scope="all_external")
|
|
2323
|
+
|
|
1817
2324
|
def delete_quest(self, quest_id: str, *, source: str = "web") -> dict | tuple[int, dict]:
|
|
1818
2325
|
quests_root = self.home / "quests"
|
|
1819
2326
|
try:
|
|
@@ -1903,52 +2410,6 @@ class DaemonApp:
|
|
|
1903
2410
|
"reply": reply,
|
|
1904
2411
|
}
|
|
1905
2412
|
|
|
1906
|
-
def handle_bridge_webhook(
|
|
1907
|
-
self,
|
|
1908
|
-
connector_name: str,
|
|
1909
|
-
*,
|
|
1910
|
-
method: str,
|
|
1911
|
-
path: str,
|
|
1912
|
-
raw_body: bytes,
|
|
1913
|
-
headers: dict[str, str],
|
|
1914
|
-
body: dict,
|
|
1915
|
-
) -> tuple[int, dict, bytes | str] | dict:
|
|
1916
|
-
bridge = get_connector_bridge(connector_name)
|
|
1917
|
-
if bridge is None:
|
|
1918
|
-
return 404, {"Content-Type": "application/json; charset=utf-8"}, json.dumps({"ok": False, "message": f"Unknown bridge `{connector_name}`."}, ensure_ascii=False)
|
|
1919
|
-
query = self.handlers.parse_query(path)
|
|
1920
|
-
result = bridge.parse_webhook(
|
|
1921
|
-
method=method,
|
|
1922
|
-
headers=headers,
|
|
1923
|
-
query=query,
|
|
1924
|
-
raw_body=raw_body,
|
|
1925
|
-
body=body,
|
|
1926
|
-
config=self.connectors_config.get(connector_name, {}),
|
|
1927
|
-
)
|
|
1928
|
-
if result.response_body is not None and not result.events:
|
|
1929
|
-
headers_out = {"Content-Type": "application/json; charset=utf-8", **result.response_headers}
|
|
1930
|
-
body_out = result.response_body
|
|
1931
|
-
if isinstance(body_out, bytes):
|
|
1932
|
-
return result.status_code, headers_out, body_out
|
|
1933
|
-
if isinstance(body_out, str):
|
|
1934
|
-
return result.status_code, headers_out, body_out
|
|
1935
|
-
return result.status_code, headers_out, json.dumps(body_out, ensure_ascii=False)
|
|
1936
|
-
|
|
1937
|
-
responses: list[dict] = []
|
|
1938
|
-
accepted = False
|
|
1939
|
-
for event in result.events:
|
|
1940
|
-
routed = self.handle_connector_inbound(connector_name, event)
|
|
1941
|
-
responses.append(routed)
|
|
1942
|
-
accepted = accepted or bool(routed.get("accepted"))
|
|
1943
|
-
return {
|
|
1944
|
-
"ok": result.ok,
|
|
1945
|
-
"accepted": accepted,
|
|
1946
|
-
"connector": connector_name,
|
|
1947
|
-
"event_count": len(result.events),
|
|
1948
|
-
"message": result.message,
|
|
1949
|
-
"responses": responses,
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
2413
|
def _route_connector_message(self, connector_name: str, message: dict) -> dict:
|
|
1953
2414
|
channel = self._channel_with_bindings(connector_name)
|
|
1954
2415
|
connector_label = self._connector_label(connector_name)
|
|
@@ -2609,7 +3070,7 @@ class DaemonApp:
|
|
|
2609
3070
|
original_identity = conversation_identity_key(conversation_id)
|
|
2610
3071
|
if original_identity in seen_identity_keys:
|
|
2611
3072
|
continue
|
|
2612
|
-
result = self.
|
|
3073
|
+
result = self._apply_conversation_binding(quest_id, conversation_id, force=True, clear_scope="none")
|
|
2613
3074
|
if isinstance(result, tuple):
|
|
2614
3075
|
continue
|
|
2615
3076
|
bound_conversation = str(result.get("conversation_id") or "").strip() or conversation_id
|
|
@@ -2644,9 +3105,35 @@ class DaemonApp:
|
|
|
2644
3105
|
if connector_name == "qq":
|
|
2645
3106
|
qq_config = self.connectors_config.get("qq", {})
|
|
2646
3107
|
if isinstance(qq_config, dict):
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
3108
|
+
profiles = list_qq_profiles(qq_config)
|
|
3109
|
+
encode_profile_id = len(profiles) > 1
|
|
3110
|
+
for profile in profiles:
|
|
3111
|
+
main_chat_id = str(profile.get("main_chat_id") or "").strip()
|
|
3112
|
+
profile_id = str(profile.get("profile_id") or "").strip()
|
|
3113
|
+
if main_chat_id:
|
|
3114
|
+
conversation_ids.append(
|
|
3115
|
+
format_conversation_id(
|
|
3116
|
+
"qq",
|
|
3117
|
+
"direct",
|
|
3118
|
+
main_chat_id,
|
|
3119
|
+
profile_id=profile_id if encode_profile_id else None,
|
|
3120
|
+
)
|
|
3121
|
+
)
|
|
3122
|
+
elif connector_name in PROFILEABLE_CONNECTOR_NAMES:
|
|
3123
|
+
connector_config = self.connectors_config.get(connector_name, {})
|
|
3124
|
+
if isinstance(connector_config, dict):
|
|
3125
|
+
for profile in list_connector_profiles(connector_name, connector_config):
|
|
3126
|
+
profile_id = str(profile.get("profile_id") or "").strip()
|
|
3127
|
+
if not profile_id:
|
|
3128
|
+
continue
|
|
3129
|
+
runtime_state = read_json(
|
|
3130
|
+
self.home / "logs" / "connectors" / connector_name / "profiles" / profile_id / "runtime.json",
|
|
3131
|
+
{},
|
|
3132
|
+
)
|
|
3133
|
+
if isinstance(runtime_state, dict):
|
|
3134
|
+
runtime_last_conversation_id = str(runtime_state.get("last_conversation_id") or "").strip()
|
|
3135
|
+
if runtime_last_conversation_id:
|
|
3136
|
+
conversation_ids.append(runtime_last_conversation_id)
|
|
2650
3137
|
state_path = self.home / "logs" / "connectors" / connector_name / "state.json"
|
|
2651
3138
|
state = read_json(state_path, {})
|
|
2652
3139
|
if isinstance(state, dict):
|
|
@@ -2892,36 +3379,32 @@ class DaemonApp:
|
|
|
2892
3379
|
chat_type = str(message.get("chat_type") or "").strip().lower()
|
|
2893
3380
|
if chat_type != "direct":
|
|
2894
3381
|
return None
|
|
3382
|
+
profile_id = str(message.get("profile_id") or "").strip() or None
|
|
2895
3383
|
chat_id = str(message.get("chat_id") or message.get("direct_id") or "").strip()
|
|
2896
3384
|
if not chat_id:
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
3385
|
+
parsed = parse_conversation_id(message.get("conversation_id"))
|
|
3386
|
+
if parsed is not None and str(parsed.get("connector") or "").strip().lower() == "qq":
|
|
3387
|
+
chat_id = str(parsed.get("chat_id") or "").strip()
|
|
3388
|
+
profile_id = str(parsed.get("profile_id") or "").strip() or profile_id
|
|
2901
3389
|
if not chat_id:
|
|
2902
3390
|
return None
|
|
2903
|
-
result = self.config_manager.bind_qq_main_chat(chat_id=chat_id)
|
|
3391
|
+
result = self.config_manager.bind_qq_main_chat(profile_id=profile_id, chat_id=chat_id)
|
|
2904
3392
|
if not result.get("ok"):
|
|
2905
3393
|
self.logger.log(
|
|
2906
3394
|
"warning",
|
|
2907
3395
|
"connector.qq_main_chat_bind_failed",
|
|
2908
3396
|
chat_id=chat_id,
|
|
3397
|
+
profile_id=profile_id,
|
|
2909
3398
|
errors=result.get("errors") or [],
|
|
2910
3399
|
)
|
|
2911
3400
|
return None
|
|
2912
3401
|
if not result.get("saved"):
|
|
2913
3402
|
return None
|
|
2914
|
-
|
|
2915
|
-
if isinstance(qq_config, dict):
|
|
2916
|
-
qq_config["main_chat_id"] = chat_id
|
|
2917
|
-
channel = self.channels.get("qq")
|
|
2918
|
-
if channel is not None and hasattr(channel, "config") and isinstance(channel.config, dict):
|
|
2919
|
-
channel.config["main_chat_id"] = chat_id
|
|
2920
|
-
gateway = self._qq_gateway
|
|
2921
|
-
if gateway is not None and isinstance(gateway.config, dict):
|
|
2922
|
-
gateway.config["main_chat_id"] = chat_id
|
|
3403
|
+
self.reload_connectors_config(restart_background=False)
|
|
2923
3404
|
return {
|
|
2924
3405
|
"chat_id": chat_id,
|
|
3406
|
+
"profile_id": result.get("profile_id"),
|
|
3407
|
+
"profile_label": result.get("profile_label"),
|
|
2925
3408
|
"saved_at": result.get("saved_at"),
|
|
2926
3409
|
}
|
|
2927
3410
|
|
|
@@ -2932,9 +3415,17 @@ class DaemonApp:
|
|
|
2932
3415
|
chat_id = str(binding.get("chat_id") or "").strip()
|
|
2933
3416
|
if not chat_id:
|
|
2934
3417
|
return base
|
|
3418
|
+
profile_label = str(binding.get("profile_label") or "").strip()
|
|
2935
3419
|
notice = self._polite_copy(
|
|
2936
|
-
zh=
|
|
2937
|
-
|
|
3420
|
+
zh=(
|
|
3421
|
+
f"已自动检测并保存当前 QQ openid:`{chat_id}`。"
|
|
3422
|
+
f"{f'当前 bot:{profile_label}。' if profile_label else ''}您现在可以在 settings 页面看到这个绑定结果。"
|
|
3423
|
+
),
|
|
3424
|
+
en=(
|
|
3425
|
+
f"I automatically detected and saved this QQ openid: `{chat_id}`. "
|
|
3426
|
+
f"{f'Current bot: {profile_label}. ' if profile_label else ''}"
|
|
3427
|
+
"You can now see the binding in settings."
|
|
3428
|
+
),
|
|
2938
3429
|
)
|
|
2939
3430
|
return f"{notice}\n\n{base}"
|
|
2940
3431
|
|
|
@@ -2973,93 +3464,160 @@ class DaemonApp:
|
|
|
2973
3464
|
+ restore_en,
|
|
2974
3465
|
)
|
|
2975
3466
|
|
|
3467
|
+
def _profiled_connector_configs(self, connector_name: str) -> list[tuple[str, str | None, dict[str, Any]]]:
|
|
3468
|
+
connector_config = self.connectors_config.get(connector_name, {})
|
|
3469
|
+
if not isinstance(connector_config, dict):
|
|
3470
|
+
return []
|
|
3471
|
+
profiles = list_connector_profiles(connector_name, connector_config)
|
|
3472
|
+
encode_profile_id = len(profiles) > 1
|
|
3473
|
+
items: list[tuple[str, str | None, dict[str, Any]]] = []
|
|
3474
|
+
for profile in profiles:
|
|
3475
|
+
profile_id = str(profile.get("profile_id") or "").strip()
|
|
3476
|
+
if not profile_id:
|
|
3477
|
+
continue
|
|
3478
|
+
merged = merge_connector_profile_config(connector_name, connector_config, profile)
|
|
3479
|
+
merged["encode_profile_id"] = encode_profile_id
|
|
3480
|
+
items.append((profile_id, connector_profile_label(connector_name, profile), merged))
|
|
3481
|
+
return items
|
|
3482
|
+
|
|
2976
3483
|
def _start_background_connectors(self) -> None:
|
|
2977
3484
|
qq_config = self.connectors_config.get("qq", {})
|
|
2978
|
-
if isinstance(qq_config, dict) and self.
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
if
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3485
|
+
if isinstance(qq_config, dict) and not self._qq_gateways:
|
|
3486
|
+
profiles = list_qq_profiles(qq_config)
|
|
3487
|
+
encode_profile_id = len(profiles) > 1
|
|
3488
|
+
for profile in profiles:
|
|
3489
|
+
profile_id = str(profile.get("profile_id") or "").strip()
|
|
3490
|
+
profile_config = merge_qq_profile_config(qq_config, profile)
|
|
3491
|
+
profile_config["encode_profile_id"] = encode_profile_id
|
|
3492
|
+
gateway = QQGatewayService(
|
|
3493
|
+
home=self.home,
|
|
3494
|
+
config=profile_config,
|
|
3495
|
+
on_event=lambda event: self.handle_connector_inbound("qq", event),
|
|
3496
|
+
log=lambda level, message, _profile_id=profile_id: self.logger.log(
|
|
3497
|
+
level,
|
|
3498
|
+
"connector.qq_gateway",
|
|
3499
|
+
profile_id=_profile_id,
|
|
3500
|
+
message=message,
|
|
3501
|
+
),
|
|
3502
|
+
)
|
|
3503
|
+
if gateway.start():
|
|
3504
|
+
self._qq_gateways[profile_id] = gateway
|
|
3505
|
+
if not self._telegram_polling:
|
|
3506
|
+
for profile_id, profile_label, profile_config in self._profiled_connector_configs("telegram"):
|
|
3507
|
+
polling = TelegramPollingService(
|
|
3508
|
+
home=self.home,
|
|
3509
|
+
config=profile_config,
|
|
3510
|
+
on_event=lambda event: self.handle_connector_inbound("telegram", event),
|
|
3511
|
+
log=lambda level, message, _profile_id=profile_id: self.logger.log(
|
|
3512
|
+
level,
|
|
3513
|
+
"connector.telegram_polling",
|
|
3514
|
+
profile_id=_profile_id,
|
|
3515
|
+
message=message,
|
|
3516
|
+
),
|
|
3517
|
+
profile_id=profile_id,
|
|
3518
|
+
profile_label=profile_label,
|
|
3519
|
+
encode_profile_id=bool(profile_config.get("encode_profile_id")),
|
|
3520
|
+
)
|
|
3521
|
+
if polling.start():
|
|
3522
|
+
self._telegram_polling[profile_id] = polling
|
|
3523
|
+
if not self._slack_socket:
|
|
3524
|
+
for profile_id, profile_label, profile_config in self._profiled_connector_configs("slack"):
|
|
3525
|
+
slack = SlackSocketModeService(
|
|
3526
|
+
home=self.home,
|
|
3527
|
+
config=profile_config,
|
|
3528
|
+
on_event=lambda event: self.handle_connector_inbound("slack", event),
|
|
3529
|
+
log=lambda level, message, _profile_id=profile_id: self.logger.log(
|
|
3530
|
+
level,
|
|
3531
|
+
"connector.slack_socket",
|
|
3532
|
+
profile_id=_profile_id,
|
|
3533
|
+
message=message,
|
|
3534
|
+
),
|
|
3535
|
+
profile_id=profile_id,
|
|
3536
|
+
profile_label=profile_label,
|
|
3537
|
+
encode_profile_id=bool(profile_config.get("encode_profile_id")),
|
|
3538
|
+
)
|
|
3539
|
+
if slack.start():
|
|
3540
|
+
self._slack_socket[profile_id] = slack
|
|
3541
|
+
if not self._discord_gateway:
|
|
3542
|
+
for profile_id, profile_label, profile_config in self._profiled_connector_configs("discord"):
|
|
3543
|
+
discord = DiscordGatewayService(
|
|
3544
|
+
home=self.home,
|
|
3545
|
+
config=profile_config,
|
|
3546
|
+
on_event=lambda event: self.handle_connector_inbound("discord", event),
|
|
3547
|
+
log=lambda level, message, _profile_id=profile_id: self.logger.log(
|
|
3548
|
+
level,
|
|
3549
|
+
"connector.discord_gateway",
|
|
3550
|
+
profile_id=_profile_id,
|
|
3551
|
+
message=message,
|
|
3552
|
+
),
|
|
3553
|
+
profile_id=profile_id,
|
|
3554
|
+
profile_label=profile_label,
|
|
3555
|
+
encode_profile_id=bool(profile_config.get("encode_profile_id")),
|
|
3556
|
+
)
|
|
3557
|
+
if discord.start():
|
|
3558
|
+
self._discord_gateway[profile_id] = discord
|
|
3559
|
+
if not self._feishu_long_connection:
|
|
3560
|
+
for profile_id, profile_label, profile_config in self._profiled_connector_configs("feishu"):
|
|
3561
|
+
feishu = FeishuLongConnectionService(
|
|
3562
|
+
home=self.home,
|
|
3563
|
+
config=profile_config,
|
|
3564
|
+
on_event=lambda event: self.handle_connector_inbound("feishu", event),
|
|
3565
|
+
log=lambda level, message, _profile_id=profile_id: self.logger.log(
|
|
3566
|
+
level,
|
|
3567
|
+
"connector.feishu_long_connection",
|
|
3568
|
+
profile_id=_profile_id,
|
|
3569
|
+
message=message,
|
|
3570
|
+
),
|
|
3571
|
+
profile_id=profile_id,
|
|
3572
|
+
profile_label=profile_label,
|
|
3573
|
+
encode_profile_id=bool(profile_config.get("encode_profile_id")),
|
|
3574
|
+
)
|
|
3575
|
+
if feishu.start():
|
|
3576
|
+
self._feishu_long_connection[profile_id] = feishu
|
|
3577
|
+
if not self._whatsapp_local_session:
|
|
3578
|
+
for profile_id, profile_label, profile_config in self._profiled_connector_configs("whatsapp"):
|
|
3579
|
+
whatsapp = WhatsAppLocalSessionService(
|
|
3580
|
+
home=self.home,
|
|
3581
|
+
config=profile_config,
|
|
3582
|
+
on_event=lambda event: self.handle_connector_inbound("whatsapp", event),
|
|
3583
|
+
log=lambda level, message, _profile_id=profile_id: self.logger.log(
|
|
3584
|
+
level,
|
|
3585
|
+
"connector.whatsapp_local_session",
|
|
3586
|
+
profile_id=_profile_id,
|
|
3587
|
+
message=message,
|
|
3588
|
+
),
|
|
3589
|
+
profile_id=profile_id,
|
|
3590
|
+
profile_label=profile_label,
|
|
3591
|
+
encode_profile_id=bool(profile_config.get("encode_profile_id")),
|
|
3592
|
+
)
|
|
3593
|
+
if whatsapp.start():
|
|
3594
|
+
self._whatsapp_local_session[profile_id] = whatsapp
|
|
3037
3595
|
|
|
3038
3596
|
def _stop_background_connectors(self) -> None:
|
|
3039
|
-
|
|
3040
|
-
self.
|
|
3041
|
-
|
|
3597
|
+
gateways = list(self._qq_gateways.values())
|
|
3598
|
+
self._qq_gateways = {}
|
|
3599
|
+
for gateway in gateways:
|
|
3042
3600
|
gateway.stop()
|
|
3043
|
-
polling = self._telegram_polling
|
|
3044
|
-
self._telegram_polling =
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
slack = self._slack_socket
|
|
3048
|
-
self._slack_socket =
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
discord = self._discord_gateway
|
|
3052
|
-
self._discord_gateway =
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
feishu = self._feishu_long_connection
|
|
3056
|
-
self._feishu_long_connection =
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
whatsapp = self._whatsapp_local_session
|
|
3060
|
-
self._whatsapp_local_session =
|
|
3061
|
-
|
|
3062
|
-
|
|
3601
|
+
polling = list(self._telegram_polling.values())
|
|
3602
|
+
self._telegram_polling = {}
|
|
3603
|
+
for item in polling:
|
|
3604
|
+
item.stop()
|
|
3605
|
+
slack = list(self._slack_socket.values())
|
|
3606
|
+
self._slack_socket = {}
|
|
3607
|
+
for item in slack:
|
|
3608
|
+
item.stop()
|
|
3609
|
+
discord = list(self._discord_gateway.values())
|
|
3610
|
+
self._discord_gateway = {}
|
|
3611
|
+
for item in discord:
|
|
3612
|
+
item.stop()
|
|
3613
|
+
feishu = list(self._feishu_long_connection.values())
|
|
3614
|
+
self._feishu_long_connection = {}
|
|
3615
|
+
for item in feishu:
|
|
3616
|
+
item.stop()
|
|
3617
|
+
whatsapp = list(self._whatsapp_local_session.values())
|
|
3618
|
+
self._whatsapp_local_session = {}
|
|
3619
|
+
for item in whatsapp:
|
|
3620
|
+
item.stop()
|
|
3063
3621
|
|
|
3064
3622
|
@staticmethod
|
|
3065
3623
|
def _format_status(snapshot: dict) -> str:
|
|
@@ -3658,16 +4216,7 @@ class DaemonApp:
|
|
|
3658
4216
|
payload = result(**params, path=self.path)
|
|
3659
4217
|
elif method == "GET":
|
|
3660
4218
|
payload = result(**params) if params else result()
|
|
3661
|
-
elif route_name
|
|
3662
|
-
payload = result(
|
|
3663
|
-
**params,
|
|
3664
|
-
method=method,
|
|
3665
|
-
path=self.path,
|
|
3666
|
-
raw_body=raw_body,
|
|
3667
|
-
headers=dict(self.headers.items()),
|
|
3668
|
-
body=body,
|
|
3669
|
-
)
|
|
3670
|
-
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"}:
|
|
4219
|
+
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"}:
|
|
3671
4220
|
payload = result(**params, body=body)
|
|
3672
4221
|
elif route_name == "config_validate":
|
|
3673
4222
|
payload = result(body)
|
|
@@ -3711,6 +4260,8 @@ class DaemonApp:
|
|
|
3711
4260
|
server = ThreadingHTTPServer((host, port), RequestHandler)
|
|
3712
4261
|
server.daemon_threads = True
|
|
3713
4262
|
self._server = server
|
|
4263
|
+
self._serve_host = host
|
|
4264
|
+
self._serve_port = port
|
|
3714
4265
|
self._shutdown_requested.clear()
|
|
3715
4266
|
self._start_terminal_attach_server(host, port)
|
|
3716
4267
|
self._start_background_connectors()
|
|
@@ -3724,4 +4275,6 @@ class DaemonApp:
|
|
|
3724
4275
|
self._stop_terminal_attach_server()
|
|
3725
4276
|
self.bash_exec_service.shutdown()
|
|
3726
4277
|
self._server = None
|
|
4278
|
+
self._serve_host = None
|
|
4279
|
+
self._serve_port = None
|
|
3727
4280
|
server.server_close()
|