@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
|
@@ -8,6 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
from urllib.error import URLError
|
|
9
9
|
from urllib.request import Request, urlopen
|
|
10
10
|
|
|
11
|
+
from ..connector_profiles import PROFILEABLE_CONNECTOR_NAMES, normalize_connector_config
|
|
11
12
|
from ..connector_runtime import infer_connector_transport
|
|
12
13
|
from ..home import repo_root
|
|
13
14
|
from ..lingzhu_support import (
|
|
@@ -22,6 +23,12 @@ from ..lingzhu_support import (
|
|
|
22
23
|
lingzhu_sse_url,
|
|
23
24
|
lingzhu_supported_commands,
|
|
24
25
|
)
|
|
26
|
+
from ..qq_profiles import (
|
|
27
|
+
find_qq_profile,
|
|
28
|
+
list_qq_profiles,
|
|
29
|
+
normalize_qq_connector_config,
|
|
30
|
+
qq_profile_label,
|
|
31
|
+
)
|
|
25
32
|
from ..shared import read_json, read_text, read_yaml, resolve_runner_binary, run_command, sha256_text, utc_now, which, write_text, write_yaml
|
|
26
33
|
from .models import (
|
|
27
34
|
CONFIG_NAMES,
|
|
@@ -125,27 +132,42 @@ class ConfigManager:
|
|
|
125
132
|
def save_named_payload(self, name: str, payload: dict) -> dict:
|
|
126
133
|
return self.save_named_text(name, self.render_named_payload(name, payload))
|
|
127
134
|
|
|
128
|
-
def bind_qq_main_chat(self, *, chat_id: str) -> dict:
|
|
135
|
+
def bind_qq_main_chat(self, *, profile_id: str | None = None, chat_id: str) -> dict:
|
|
129
136
|
normalized_chat_id = str(chat_id or "").strip()
|
|
130
137
|
if not normalized_chat_id:
|
|
131
138
|
return {"ok": False, "saved": False, "message": "QQ main chat id is empty."}
|
|
132
139
|
connectors = self.load_named_normalized("connectors")
|
|
133
140
|
qq = connectors.get("qq") if isinstance(connectors.get("qq"), dict) else {}
|
|
134
|
-
|
|
141
|
+
profiles = list_qq_profiles(qq)
|
|
142
|
+
if not profiles:
|
|
143
|
+
return {"ok": False, "saved": False, "message": "QQ profile is not configured yet."}
|
|
144
|
+
resolved_profile = find_qq_profile(qq, profile_id=profile_id)
|
|
145
|
+
if resolved_profile is None and len(profiles) == 1:
|
|
146
|
+
resolved_profile = profiles[0]
|
|
147
|
+
if resolved_profile is None:
|
|
148
|
+
return {"ok": False, "saved": False, "message": "Unable to determine which QQ profile should save this OpenID."}
|
|
149
|
+
configured = str((resolved_profile or {}).get("main_chat_id") or "").strip()
|
|
135
150
|
if configured:
|
|
136
151
|
return {
|
|
137
152
|
"ok": True,
|
|
138
153
|
"saved": False,
|
|
139
154
|
"chat_id": configured,
|
|
140
155
|
"already_configured": True,
|
|
156
|
+
"profile_id": resolved_profile.get("profile_id"),
|
|
141
157
|
}
|
|
142
|
-
|
|
158
|
+
for item in profiles:
|
|
159
|
+
if str(item.get("profile_id") or "").strip() == str(resolved_profile.get("profile_id") or "").strip():
|
|
160
|
+
item["main_chat_id"] = normalized_chat_id
|
|
161
|
+
qq["profiles"] = profiles
|
|
162
|
+
qq = normalize_qq_connector_config(qq)
|
|
143
163
|
connectors["qq"] = qq
|
|
144
164
|
result = self.save_named_payload("connectors", connectors)
|
|
145
165
|
return {
|
|
146
166
|
"ok": bool(result.get("ok")),
|
|
147
167
|
"saved": bool(result.get("ok")),
|
|
148
168
|
"chat_id": normalized_chat_id,
|
|
169
|
+
"profile_id": resolved_profile.get("profile_id"),
|
|
170
|
+
"profile_label": qq_profile_label(resolved_profile),
|
|
149
171
|
"saved_at": result.get("saved_at"),
|
|
150
172
|
"errors": result.get("errors") or [],
|
|
151
173
|
"warnings": result.get("warnings") or [],
|
|
@@ -231,8 +253,7 @@ This page edits `~/DeepScientist/config/connectors.yaml` directly.
|
|
|
231
253
|
- connect Telegram, Discord, Slack, Feishu, WhatsApp, or QQ
|
|
232
254
|
- choose one preferred connector for proactive artifact updates
|
|
233
255
|
- decide whether artifact updates fan out or stay focused
|
|
234
|
-
-
|
|
235
|
-
- keep legacy webhook or relay settings only as a fallback path
|
|
256
|
+
- use the built-in direct runtime for each connector
|
|
236
257
|
- keep all secrets in one visible place
|
|
237
258
|
|
|
238
259
|
## Recommended order
|
|
@@ -252,22 +273,12 @@ This page edits `~/DeepScientist/config/connectors.yaml` directly.
|
|
|
252
273
|
- WhatsApp: `local_session`
|
|
253
274
|
- QQ: `gateway_direct`
|
|
254
275
|
|
|
255
|
-
## Legacy fallback routes
|
|
256
|
-
|
|
257
|
-
- `POST /api/bridges/telegram/webhook`
|
|
258
|
-
- `POST /api/bridges/slack/webhook`
|
|
259
|
-
- `POST /api/bridges/feishu/webhook`
|
|
260
|
-
- `GET /api/bridges/whatsapp/webhook`
|
|
261
|
-
- `POST /api/bridges/whatsapp/webhook`
|
|
262
|
-
- `POST /api/bridges/discord/webhook`
|
|
263
|
-
|
|
264
276
|
## Practical notes
|
|
265
277
|
|
|
266
278
|
### Telegram
|
|
267
279
|
|
|
268
280
|
- set `bot_token`
|
|
269
281
|
- prefer `transport: polling`
|
|
270
|
-
- use webhook fields only in the legacy section
|
|
271
282
|
- readiness test uses `getMe`
|
|
272
283
|
|
|
273
284
|
### Slack
|
|
@@ -281,32 +292,30 @@ This page edits `~/DeepScientist/config/connectors.yaml` directly.
|
|
|
281
292
|
|
|
282
293
|
- set `bot_token`
|
|
283
294
|
- prefer `transport: gateway`
|
|
284
|
-
- keep interaction callback fields only for legacy compatibility
|
|
285
295
|
|
|
286
296
|
### Feishu
|
|
287
297
|
|
|
288
298
|
- set `app_id`
|
|
289
299
|
- set `app_secret`
|
|
290
300
|
- prefer `transport: long_connection`
|
|
291
|
-
- keep verification or encrypt keys only for legacy webhook fallback
|
|
292
301
|
- test checks whether tenant token exchange succeeds
|
|
293
302
|
|
|
294
303
|
### WhatsApp
|
|
295
304
|
|
|
296
305
|
- prefer `transport: local_session`
|
|
297
306
|
- the local-session path is designed to avoid public callbacks
|
|
298
|
-
-
|
|
299
|
-
- current live test can still probe Meta credentials when configured
|
|
307
|
+
- keep one writable `session_dir` for the local auth state
|
|
300
308
|
|
|
301
309
|
### QQ
|
|
302
310
|
|
|
303
311
|
- QQ only uses the built-in gateway direct path with `app_id` + `app_secret`
|
|
304
|
-
-
|
|
305
|
-
-
|
|
312
|
+
- each QQ bot is stored as one item under `qq.profiles`
|
|
313
|
+
- save one QQ bot profile first, then ask the user to send one private QQ message to that specific bot
|
|
314
|
+
- the daemon auto-detects that user's `openid` and saves it into that profile's `main_chat_id`
|
|
306
315
|
- private QQ chats can then auto-follow the latest quest by default, unless disabled in settings
|
|
307
316
|
- readiness test exchanges `access_token` and probes `/gateway`
|
|
308
317
|
- active send targets use QQ user `openid` or group `group_openid`
|
|
309
|
-
- the settings page also surfaces recently discovered targets from runtime activity
|
|
318
|
+
- the settings page also surfaces recently discovered targets from runtime activity, grouped by QQ bot profile
|
|
310
319
|
- milestone delivery toggles default to enabled; adjust them only if you want less outbound push
|
|
311
320
|
- the recommended first-run path is: save credentials -> send one QQ private message -> confirm `Detected OpenID` -> run a probe
|
|
312
321
|
|
|
@@ -643,17 +652,39 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
643
652
|
enabled_connectors.append(str(name))
|
|
644
653
|
|
|
645
654
|
if name == "qq":
|
|
646
|
-
|
|
655
|
+
profiles = list_qq_profiles(config)
|
|
656
|
+
if not profiles:
|
|
657
|
+
errors.append("qq: requires at least one configured profile under `qq.profiles`.")
|
|
658
|
+
continue
|
|
659
|
+
legacy_missing_app_id = False
|
|
660
|
+
legacy_missing_secret = False
|
|
661
|
+
seen_profile_ids: set[str] = set()
|
|
662
|
+
seen_app_ids: set[str] = set()
|
|
663
|
+
for profile in profiles:
|
|
664
|
+
profile_id = str(profile.get("profile_id") or "").strip() or "unknown"
|
|
665
|
+
app_id = str(profile.get("app_id") or "").strip()
|
|
666
|
+
if not profile_id:
|
|
667
|
+
errors.append("qq: every profile requires a stable `profile_id`.")
|
|
668
|
+
elif profile_id in seen_profile_ids:
|
|
669
|
+
errors.append(f"qq: duplicate profile_id `{profile_id}`.")
|
|
670
|
+
else:
|
|
671
|
+
seen_profile_ids.add(profile_id)
|
|
672
|
+
if not app_id:
|
|
673
|
+
legacy_missing_app_id = True
|
|
674
|
+
errors.append(f"qq[{profile_id}]: requires `app_id`.")
|
|
675
|
+
elif app_id in seen_app_ids:
|
|
676
|
+
errors.append(f"qq: duplicate app_id `{app_id}` across profiles.")
|
|
677
|
+
else:
|
|
678
|
+
seen_app_ids.add(app_id)
|
|
679
|
+
if not self._has_secret(profile, "app_secret", "app_secret_env"):
|
|
680
|
+
legacy_missing_secret = True
|
|
681
|
+
errors.append(f"qq[{profile_id}]: requires `app_secret` or `app_secret_env`.")
|
|
682
|
+
if len(profiles) == 1 and legacy_missing_app_id:
|
|
647
683
|
errors.append("qq: requires `app_id` for the built-in gateway direct connector.")
|
|
648
|
-
if
|
|
684
|
+
if len(profiles) == 1 and legacy_missing_secret:
|
|
649
685
|
errors.append("qq: requires `app_secret` or `app_secret_env` for the built-in gateway direct connector.")
|
|
650
686
|
continue
|
|
651
687
|
transport = infer_connector_transport(name, config)
|
|
652
|
-
relay_url = str(config.get("relay_url") or "").strip()
|
|
653
|
-
public_callback_url = str(config.get("public_callback_url") or "").strip()
|
|
654
|
-
|
|
655
|
-
if transport == "relay" and not relay_url:
|
|
656
|
-
errors.append(f"{name}: `transport: relay` requires `relay_url`.")
|
|
657
688
|
|
|
658
689
|
policy_validation = self._validate_access_policies(name, config)
|
|
659
690
|
warnings.extend(policy_validation["warnings"])
|
|
@@ -661,75 +692,41 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
661
692
|
|
|
662
693
|
if name == "telegram":
|
|
663
694
|
has_token = self._has_secret(config, "bot_token", "bot_token_env")
|
|
664
|
-
if transport
|
|
665
|
-
errors.append("telegram: `transport
|
|
666
|
-
|
|
667
|
-
errors.append("telegram: `transport:
|
|
668
|
-
elif not has_token and not relay_url:
|
|
669
|
-
warnings.append("telegram: set `bot_token` or `bot_token_env` so the connector can authenticate with the Telegram Bot API.")
|
|
695
|
+
if transport != "polling":
|
|
696
|
+
errors.append("telegram: `transport` must stay `polling`.")
|
|
697
|
+
if not has_token:
|
|
698
|
+
errors.append("telegram: `transport: polling` requires `bot_token` or `bot_token_env`.")
|
|
670
699
|
elif name == "discord":
|
|
671
700
|
has_token = self._has_secret(config, "bot_token", "bot_token_env")
|
|
672
|
-
if transport
|
|
701
|
+
if transport != "gateway":
|
|
702
|
+
errors.append("discord: `transport` must stay `gateway`.")
|
|
703
|
+
if not has_token:
|
|
673
704
|
errors.append("discord: `transport: gateway` requires `bot_token` or `bot_token_env`.")
|
|
674
|
-
elif transport == "legacy_interactions":
|
|
675
|
-
if not has_token:
|
|
676
|
-
errors.append("discord: `transport: legacy_interactions` requires `bot_token` or `bot_token_env`.")
|
|
677
|
-
if public_callback_url or str(config.get("public_interactions_url") or "").strip():
|
|
678
|
-
if not self._has_secret(config, "public_key", "public_key_env"):
|
|
679
|
-
errors.append("discord: interaction callback mode requires `public_key` or `public_key_env`.")
|
|
680
|
-
elif transport == "relay" and not relay_url:
|
|
681
|
-
errors.append("discord: `transport: relay` requires `relay_url`.")
|
|
682
705
|
if not str(config.get("application_id") or "").strip():
|
|
683
706
|
warnings.append("discord: `application_id` is recommended for richer routing and future slash command support.")
|
|
684
707
|
elif name == "slack":
|
|
685
708
|
has_bot_token = self._has_secret(config, "bot_token", "bot_token_env")
|
|
686
709
|
has_app_token = self._has_secret(config, "app_token", "app_token_env")
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
elif transport == "legacy_events_api":
|
|
694
|
-
if not has_bot_token:
|
|
695
|
-
errors.append("slack: `transport: legacy_events_api` requires `bot_token` or `bot_token_env`.")
|
|
696
|
-
if public_callback_url and not has_signing_secret:
|
|
697
|
-
errors.append("slack: callback-based setup requires `signing_secret` or `signing_secret_env`.")
|
|
698
|
-
elif transport == "relay" and not relay_url:
|
|
699
|
-
errors.append("slack: `transport: relay` requires `relay_url`.")
|
|
710
|
+
if transport != "socket_mode":
|
|
711
|
+
errors.append("slack: `transport` must stay `socket_mode`.")
|
|
712
|
+
if not has_bot_token:
|
|
713
|
+
errors.append("slack: `transport: socket_mode` requires `bot_token` or `bot_token_env`.")
|
|
714
|
+
if not has_app_token:
|
|
715
|
+
errors.append("slack: `transport: socket_mode` requires `app_token` or `app_token_env`.")
|
|
700
716
|
elif name == "feishu":
|
|
701
717
|
has_app_id = bool(str(config.get("app_id") or "").strip())
|
|
702
718
|
has_app_secret = self._has_secret(config, "app_secret", "app_secret_env")
|
|
703
|
-
if transport
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
errors.append("feishu: `transport: relay` requires `relay_url`.")
|
|
710
|
-
if public_callback_url and not self._has_secret(
|
|
711
|
-
config,
|
|
712
|
-
"verification_token",
|
|
713
|
-
"verification_token_env",
|
|
714
|
-
):
|
|
715
|
-
errors.append("feishu: webhook-style bridge configuration requires `verification_token` or `verification_token_env`.")
|
|
719
|
+
if transport != "long_connection":
|
|
720
|
+
errors.append("feishu: `transport` must stay `long_connection`.")
|
|
721
|
+
if not has_app_id:
|
|
722
|
+
errors.append("feishu: `transport: long_connection` requires `app_id`.")
|
|
723
|
+
if not has_app_secret:
|
|
724
|
+
errors.append("feishu: `transport: long_connection` requires `app_secret` or `app_secret_env`.")
|
|
716
725
|
elif name == "whatsapp":
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
elif transport == "relay":
|
|
722
|
-
if not relay_url:
|
|
723
|
-
errors.append("whatsapp: `transport: relay` requires `relay_url`.")
|
|
724
|
-
elif transport == "legacy_meta_cloud":
|
|
725
|
-
if provider not in {"relay", "meta"}:
|
|
726
|
-
errors.append(f"whatsapp: unsupported provider `{provider}`. Supported providers: meta, relay.")
|
|
727
|
-
if not self._has_secret(config, "access_token", "access_token_env"):
|
|
728
|
-
errors.append("whatsapp: `provider: meta` requires `access_token` or `access_token_env`.")
|
|
729
|
-
if not str(config.get("phone_number_id") or "").strip():
|
|
730
|
-
errors.append("whatsapp: `provider: meta` requires `phone_number_id`.")
|
|
731
|
-
if not self._has_secret(config, "verify_token", "verify_token_env"):
|
|
732
|
-
errors.append("whatsapp: `provider: meta` requires `verify_token` or `verify_token_env`.")
|
|
726
|
+
if transport != "local_session":
|
|
727
|
+
errors.append("whatsapp: `transport` must stay `local_session`.")
|
|
728
|
+
if not str(config.get("session_dir") or "").strip():
|
|
729
|
+
warnings.append("whatsapp: `transport: local_session` should set `session_dir` for local auth state.")
|
|
733
730
|
elif name == "lingzhu":
|
|
734
731
|
if transport != "openclaw_sse":
|
|
735
732
|
errors.append("lingzhu: `transport` must stay `openclaw_sse`.")
|
|
@@ -794,57 +791,49 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
794
791
|
return {
|
|
795
792
|
"ok": all(item["ok"] for item in items) if items else True,
|
|
796
793
|
"name": "connectors",
|
|
797
|
-
"summary": "Connector
|
|
794
|
+
"summary": "Connector test completed." if items else "No enabled connectors to test.",
|
|
798
795
|
"warnings": [],
|
|
799
796
|
"errors": [],
|
|
800
797
|
"items": items,
|
|
801
798
|
}
|
|
802
799
|
|
|
803
800
|
def _test_single_connector(self, name: str, config: dict, *, live: bool, delivery_target: dict[str, object] | None = None) -> dict:
|
|
804
|
-
relay_url = str(config.get("relay_url") or "").strip()
|
|
805
801
|
transport = infer_connector_transport(name, config)
|
|
806
802
|
warnings: list[str] = []
|
|
807
803
|
errors: list[str] = []
|
|
808
804
|
details: dict[str, object] = {
|
|
809
|
-
"mode": "gateway-direct" if name == "qq" else config.get("mode"
|
|
805
|
+
"mode": "gateway-direct" if name == "qq" else str(config.get("mode") or transport),
|
|
810
806
|
"transport": transport,
|
|
811
807
|
}
|
|
812
|
-
if name != "qq" and relay_url:
|
|
813
|
-
details["relay_url"] = relay_url
|
|
814
|
-
if relay_url and name != "qq" and transport == "relay":
|
|
815
|
-
warnings.append("Configured with relay_url. The live test checks local prerequisites, not the external bridge health.")
|
|
816
808
|
|
|
817
809
|
try:
|
|
818
810
|
if name == "telegram":
|
|
819
811
|
token = self._secret(config, "bot_token", "bot_token_env")
|
|
820
|
-
if transport
|
|
812
|
+
if transport == "polling" and live and token:
|
|
821
813
|
payload = self._http_json(f"https://api.telegram.org/bot{token}/getMe")
|
|
822
814
|
if not payload.get("ok", False):
|
|
823
815
|
errors.append("Telegram getMe did not return ok=true.")
|
|
824
816
|
else:
|
|
825
817
|
details["identity"] = (payload.get("result") or {}).get("username")
|
|
826
|
-
elif transport
|
|
827
|
-
errors.append("Telegram requires `bot_token` for polling
|
|
828
|
-
|
|
829
|
-
errors.append("Telegram
|
|
818
|
+
elif transport == "polling" and not token:
|
|
819
|
+
errors.append("Telegram requires `bot_token` for polling.")
|
|
820
|
+
else:
|
|
821
|
+
errors.append("Telegram transport must stay `polling`.")
|
|
830
822
|
elif name == "slack":
|
|
831
823
|
token = self._secret(config, "bot_token", "bot_token_env")
|
|
832
824
|
app_token = self._secret(config, "app_token", "app_token_env")
|
|
833
|
-
signing_secret = self._secret(config, "signing_secret", "signing_secret_env")
|
|
834
825
|
if transport == "socket_mode" and not app_token:
|
|
835
826
|
errors.append("Slack Socket Mode requires `app_token` or `app_token_env`.")
|
|
836
|
-
if transport == "legacy_events_api" and not signing_secret:
|
|
837
|
-
warnings.append("Slack signing_secret is empty; inbound verification will be skipped.")
|
|
838
827
|
if live and token:
|
|
839
828
|
payload = self._http_json("https://slack.com/api/auth.test", method="POST", headers={"Authorization": f"Bearer {token}"})
|
|
840
829
|
if not payload.get("ok", False):
|
|
841
830
|
errors.append(str(payload.get("error") or "Slack auth.test failed."))
|
|
842
831
|
else:
|
|
843
832
|
details["identity"] = payload.get("user")
|
|
844
|
-
elif transport
|
|
833
|
+
elif transport == "socket_mode" and not token:
|
|
845
834
|
errors.append("Slack requires `bot_token` for native runtime access.")
|
|
846
|
-
elif transport
|
|
847
|
-
errors.append("Slack
|
|
835
|
+
elif transport != "socket_mode":
|
|
836
|
+
errors.append("Slack transport must stay `socket_mode`.")
|
|
848
837
|
elif name == "discord":
|
|
849
838
|
token = self._secret(config, "bot_token", "bot_token_env")
|
|
850
839
|
if live and token:
|
|
@@ -853,10 +842,10 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
853
842
|
errors.append(str(payload.get("message") or "Discord identity check failed."))
|
|
854
843
|
else:
|
|
855
844
|
details["identity"] = payload.get("username")
|
|
856
|
-
elif transport
|
|
857
|
-
errors.append("Discord requires `bot_token` for gateway
|
|
858
|
-
elif transport
|
|
859
|
-
errors.append("Discord
|
|
845
|
+
elif transport == "gateway" and not token:
|
|
846
|
+
errors.append("Discord requires `bot_token` for gateway access.")
|
|
847
|
+
elif transport != "gateway":
|
|
848
|
+
errors.append("Discord transport must stay `gateway`.")
|
|
860
849
|
elif name == "feishu":
|
|
861
850
|
app_id = str(config.get("app_id") or "").strip()
|
|
862
851
|
app_secret = self._secret(config, "app_secret", "app_secret_env")
|
|
@@ -869,15 +858,11 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
869
858
|
)
|
|
870
859
|
if not payload.get("tenant_access_token"):
|
|
871
860
|
errors.append(str(payload.get("msg") or "Feishu tenant token exchange failed."))
|
|
872
|
-
elif transport
|
|
873
|
-
errors.append("Feishu requires `app_id` + `app_secret` for long-connection
|
|
874
|
-
elif transport
|
|
875
|
-
errors.append("Feishu
|
|
861
|
+
elif transport == "long_connection" and not (app_id and app_secret):
|
|
862
|
+
errors.append("Feishu requires `app_id` + `app_secret` for long-connection access.")
|
|
863
|
+
elif transport != "long_connection":
|
|
864
|
+
errors.append("Feishu transport must stay `long_connection`.")
|
|
876
865
|
elif name == "whatsapp":
|
|
877
|
-
provider = str(config.get("provider") or "relay").strip().lower()
|
|
878
|
-
details["provider"] = provider
|
|
879
|
-
token = self._secret(config, "access_token", "access_token_env")
|
|
880
|
-
phone_number_id = str(config.get("phone_number_id") or "").strip()
|
|
881
866
|
if transport == "local_session":
|
|
882
867
|
session_dir = str(config.get("session_dir") or "").strip()
|
|
883
868
|
details["session_dir"] = session_dir or None
|
|
@@ -885,49 +870,66 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
885
870
|
details["session_dir_exists"] = Path(session_dir).expanduser().exists()
|
|
886
871
|
if not session_dir:
|
|
887
872
|
warnings.append("WhatsApp local-session mode still needs a local `session_dir` for auth state.")
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
api_version = str(config.get("api_version") or "v21.0").strip()
|
|
891
|
-
payload = self._http_json(
|
|
892
|
-
f"{api_base_url}/{api_version}/{phone_number_id}",
|
|
893
|
-
headers={"Authorization": f"Bearer {token}"},
|
|
894
|
-
)
|
|
895
|
-
if payload.get("error"):
|
|
896
|
-
errors.append(str(payload["error"].get("message") or "WhatsApp phone number probe failed."))
|
|
897
|
-
else:
|
|
898
|
-
details["identity"] = payload.get("display_phone_number")
|
|
899
|
-
elif transport == "legacy_meta_cloud" and not (token and phone_number_id):
|
|
900
|
-
errors.append("WhatsApp Meta Cloud fallback requires `access_token` + `phone_number_id`.")
|
|
901
|
-
elif transport == "relay" and not relay_url:
|
|
902
|
-
errors.append("WhatsApp requires `relay_url` when `transport: relay` is selected.")
|
|
873
|
+
else:
|
|
874
|
+
errors.append("WhatsApp transport must stay `local_session`.")
|
|
903
875
|
elif name == "qq":
|
|
904
|
-
app_id = str(config.get("app_id") or "").strip()
|
|
905
|
-
app_secret = self._secret(config, "app_secret", "app_secret_env")
|
|
906
876
|
details["transport"] = "gateway_direct"
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
877
|
+
profile_results: list[dict[str, object]] = []
|
|
878
|
+
profiles = list_qq_profiles(config)
|
|
879
|
+
if not profiles:
|
|
880
|
+
errors.append("QQ requires at least one configured profile.")
|
|
881
|
+
for profile in profiles:
|
|
882
|
+
profile_id = str(profile.get("profile_id") or "").strip() or "unknown"
|
|
883
|
+
app_id = str(profile.get("app_id") or "").strip()
|
|
884
|
+
app_secret = self._secret(profile, "app_secret", "app_secret_env")
|
|
885
|
+
profile_details: dict[str, object] = {
|
|
886
|
+
"profile_id": profile_id,
|
|
887
|
+
"label": qq_profile_label(profile),
|
|
888
|
+
"app_id": app_id or None,
|
|
889
|
+
"main_chat_id": str(profile.get("main_chat_id") or "").strip() or None,
|
|
890
|
+
}
|
|
891
|
+
if not app_id or not app_secret:
|
|
892
|
+
profile_details["ok"] = False
|
|
893
|
+
profile_details["error"] = "QQ requires `app_id` + `app_secret` for each configured profile."
|
|
894
|
+
errors.append(f"QQ profile `{profile_id}` is missing `app_id` or `app_secret`.")
|
|
895
|
+
profile_results.append(profile_details)
|
|
896
|
+
continue
|
|
897
|
+
if live:
|
|
898
|
+
token_payload = self._http_json(
|
|
899
|
+
"https://bots.qq.com/app/getAppAccessToken",
|
|
900
|
+
method="POST",
|
|
901
|
+
headers={"Content-Type": "application/json; charset=utf-8"},
|
|
902
|
+
body={"appId": app_id, "clientSecret": app_secret},
|
|
903
|
+
)
|
|
904
|
+
access_token = str(token_payload.get("access_token") or "").strip()
|
|
905
|
+
if not access_token:
|
|
906
|
+
message = str(token_payload.get("message") or "QQ access token exchange failed.")
|
|
907
|
+
errors.append(f"QQ profile `{profile_id}`: {message}")
|
|
908
|
+
profile_details["ok"] = False
|
|
909
|
+
profile_details["error"] = message
|
|
910
|
+
profile_results.append(profile_details)
|
|
911
|
+
continue
|
|
922
912
|
gateway_payload = self._http_json(
|
|
923
913
|
"https://api.sgroup.qq.com/gateway",
|
|
924
914
|
headers={"Authorization": f"QQBot {access_token}"},
|
|
925
915
|
)
|
|
926
916
|
gateway_url = str(gateway_payload.get("url") or "").strip()
|
|
927
917
|
if not gateway_url:
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
918
|
+
message = str(gateway_payload.get("message") or "QQ gateway probe failed.")
|
|
919
|
+
errors.append(f"QQ profile `{profile_id}`: {message}")
|
|
920
|
+
profile_details["ok"] = False
|
|
921
|
+
profile_details["error"] = message
|
|
922
|
+
profile_results.append(profile_details)
|
|
923
|
+
continue
|
|
924
|
+
profile_details["gateway_url"] = gateway_url
|
|
925
|
+
profile_details["token_expires_in"] = token_payload.get("expires_in")
|
|
926
|
+
profile_details["ok"] = True
|
|
927
|
+
profile_results.append(profile_details)
|
|
928
|
+
details["profiles"] = profile_results
|
|
929
|
+
if len(profile_results) == 1 and profile_results[0].get("ok"):
|
|
930
|
+
details["identity"] = profile_results[0].get("app_id")
|
|
931
|
+
details["gateway_url"] = profile_results[0].get("gateway_url")
|
|
932
|
+
details["token_expires_in"] = profile_results[0].get("token_expires_in")
|
|
931
933
|
elif name == "lingzhu":
|
|
932
934
|
details.update(self._lingzhu_snapshot_details(config))
|
|
933
935
|
auth_ak = self._secret(config, "auth_ak", "auth_ak_env")
|
|
@@ -952,7 +954,11 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
952
954
|
delivery_message = str(delivery_target.get("text") or "").strip()
|
|
953
955
|
chat_type = str(delivery_target.get("chat_type") or "direct").strip().lower()
|
|
954
956
|
chat_id = str(delivery_target.get("chat_id") or "").strip()
|
|
955
|
-
default_chat_id =
|
|
957
|
+
default_chat_id = ""
|
|
958
|
+
if name == "qq":
|
|
959
|
+
profiles = list_qq_profiles(config)
|
|
960
|
+
if len(profiles) == 1:
|
|
961
|
+
default_chat_id = str(profiles[0].get("main_chat_id") or "").strip()
|
|
956
962
|
if not default_chat_id:
|
|
957
963
|
default_chat_id = self._connector_recent_chat_id(name, chat_type)
|
|
958
964
|
if not chat_id and default_chat_id:
|
|
@@ -994,7 +1000,7 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
994
1000
|
delivery = bridge.deliver(outbound, config)
|
|
995
1001
|
if delivery is None:
|
|
996
1002
|
warnings.append(
|
|
997
|
-
"The current connector mode cannot actively send a test message.
|
|
1003
|
+
"The current connector mode cannot actively send a test message yet. Finish the native direct setup first."
|
|
998
1004
|
)
|
|
999
1005
|
else:
|
|
1000
1006
|
details["delivery"] = delivery
|
|
@@ -1052,7 +1058,7 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1052
1058
|
"warnings": [],
|
|
1053
1059
|
"errors": [
|
|
1054
1060
|
"Codex binary is not installed or could not be resolved.",
|
|
1055
|
-
"
|
|
1061
|
+
"DeepScientist could not resolve the bundled or configured `codex` CLI.",
|
|
1056
1062
|
],
|
|
1057
1063
|
"details": details,
|
|
1058
1064
|
"guidance": [
|
|
@@ -1197,7 +1203,6 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1197
1203
|
"mode": "openclaw_companion",
|
|
1198
1204
|
"transport": "openclaw_sse",
|
|
1199
1205
|
"enabled": bool(resolved.get("enabled", False)),
|
|
1200
|
-
"relay_url": None,
|
|
1201
1206
|
"main_chat_id": None,
|
|
1202
1207
|
"last_conversation_id": None,
|
|
1203
1208
|
"inbox_count": 0,
|
|
@@ -1337,7 +1342,11 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1337
1342
|
if connector_name == "qq":
|
|
1338
1343
|
for legacy_key in ("mode", "relay_url", "relay_auth_token", "public_callback_url", "webhook_verify_signature"):
|
|
1339
1344
|
sanitized_payload.pop(legacy_key, None)
|
|
1340
|
-
|
|
1345
|
+
normalized["qq"] = normalize_qq_connector_config({**base, **sanitized_payload})
|
|
1346
|
+
continue
|
|
1347
|
+
if connector_name in PROFILEABLE_CONNECTOR_NAMES:
|
|
1348
|
+
normalized[connector_name] = normalize_connector_config(connector_name, {**base, **sanitized_payload})
|
|
1349
|
+
continue
|
|
1341
1350
|
elif connector_name == "lingzhu":
|
|
1342
1351
|
sanitized_payload["transport"] = "openclaw_sse"
|
|
1343
1352
|
elif "transport" not in sanitized_payload:
|