@researai/deepscientist 1.5.8 → 1.5.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +186 -21
- package/README.md +108 -95
- package/assets/branding/connector-qq.png +0 -0
- package/assets/branding/connector-rokid.png +0 -0
- package/assets/branding/connector-weixin.png +0 -0
- package/assets/branding/projects.png +0 -0
- package/bin/ds.js +172 -13
- package/docs/assets/branding/projects.png +0 -0
- package/docs/en/00_QUICK_START.md +308 -70
- package/docs/en/01_SETTINGS_REFERENCE.md +3 -0
- package/docs/en/02_START_RESEARCH_GUIDE.md +112 -0
- package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +62 -179
- package/docs/en/09_DOCTOR.md +41 -5
- package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +137 -0
- package/docs/en/11_LICENSE_AND_RISK.md +256 -0
- package/docs/en/12_GUIDED_WORKFLOW_TOUR.md +427 -0
- package/docs/en/13_CORE_ARCHITECTURE_GUIDE.md +297 -0
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
- package/docs/en/99_ACKNOWLEDGEMENTS.md +4 -1
- package/docs/en/README.md +79 -0
- package/docs/images/lingzhu/rokid-agent-platform-create.png +0 -0
- package/docs/images/weixin/weixin-plugin-entry.png +0 -0
- package/docs/images/weixin/weixin-plugin-entry.svg +33 -0
- package/docs/images/weixin/weixin-qr-confirm.svg +30 -0
- package/docs/images/weixin/weixin-quest-media-flow.svg +44 -0
- package/docs/images/weixin/weixin-settings-bind.svg +57 -0
- package/docs/zh/00_QUICK_START.md +315 -74
- package/docs/zh/01_SETTINGS_REFERENCE.md +3 -0
- package/docs/zh/02_START_RESEARCH_GUIDE.md +112 -0
- package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +62 -193
- package/docs/zh/09_DOCTOR.md +41 -5
- package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +144 -0
- package/docs/zh/11_LICENSE_AND_RISK.md +256 -0
- package/docs/zh/12_GUIDED_WORKFLOW_TOUR.md +423 -0
- package/docs/zh/13_CORE_ARCHITECTURE_GUIDE.md +296 -0
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +4 -1
- package/docs/zh/README.md +126 -0
- package/install.sh +0 -34
- package/package.json +3 -3
- package/pyproject.toml +2 -2
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/annotations.py +343 -0
- package/src/deepscientist/artifact/arxiv.py +484 -37
- package/src/deepscientist/artifact/metrics.py +1 -3
- package/src/deepscientist/artifact/service.py +1347 -111
- package/src/deepscientist/arxiv_library.py +275 -0
- package/src/deepscientist/bash_exec/service.py +9 -0
- package/src/deepscientist/bridges/builtins.py +2 -0
- package/src/deepscientist/bridges/connectors.py +447 -0
- package/src/deepscientist/channels/__init__.py +2 -0
- package/src/deepscientist/channels/builtins.py +3 -1
- package/src/deepscientist/channels/qq.py +1 -1
- package/src/deepscientist/channels/qq_gateway.py +1 -1
- package/src/deepscientist/channels/relay.py +7 -1
- package/src/deepscientist/channels/weixin.py +59 -0
- package/src/deepscientist/channels/weixin_ilink.py +317 -0
- package/src/deepscientist/config/models.py +22 -2
- package/src/deepscientist/config/service.py +431 -60
- package/src/deepscientist/connector/__init__.py +4 -0
- package/src/deepscientist/connector/connector_profiles.py +481 -0
- package/src/deepscientist/connector/lingzhu_support.py +668 -0
- package/src/deepscientist/connector/qq_profiles.py +206 -0
- package/src/deepscientist/connector/weixin_support.py +663 -0
- package/src/deepscientist/connector_profiles.py +1 -374
- package/src/deepscientist/connector_runtime.py +2 -0
- package/src/deepscientist/daemon/api/handlers.py +295 -5
- package/src/deepscientist/daemon/api/router.py +16 -1
- package/src/deepscientist/daemon/app.py +1130 -61
- package/src/deepscientist/doctor.py +5 -2
- package/src/deepscientist/gitops/diff.py +120 -29
- package/src/deepscientist/lingzhu_support.py +1 -182
- package/src/deepscientist/mcp/server.py +14 -5
- package/src/deepscientist/prompts/builder.py +29 -1
- package/src/deepscientist/qq_profiles.py +1 -196
- package/src/deepscientist/quest/node_traces.py +152 -2
- package/src/deepscientist/quest/service.py +169 -43
- package/src/deepscientist/quest/stage_views.py +172 -9
- package/src/deepscientist/registries/baseline.py +56 -4
- package/src/deepscientist/runners/codex.py +55 -3
- package/src/deepscientist/weixin_support.py +1 -0
- package/src/prompts/connectors/lingzhu.md +3 -1
- package/src/prompts/connectors/weixin.md +230 -0
- package/src/prompts/system.md +9 -0
- package/src/skills/idea/SKILL.md +16 -0
- package/src/skills/idea/references/literature-survey-template.md +24 -0
- package/src/skills/idea/references/related-work-playbook.md +4 -0
- package/src/skills/idea/references/selection-gate.md +9 -0
- package/src/skills/write/SKILL.md +1 -1
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-m2FNtwbn.js → AiManusChatView-D0mTXG4-.js} +156 -48
- package/src/ui/dist/assets/{AnalysisPlugin-BMTF8EGL.js → AnalysisPlugin-Db0cTXxm.js} +1 -1
- package/src/ui/dist/assets/{CliPlugin-BEOWgxCI.js → CliPlugin-DrV8je02.js} +164 -9
- package/src/ui/dist/assets/{CodeEditorPlugin-BCXvjqmb.js → CodeEditorPlugin-QXMSCH71.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-DaJcy3nD.js → CodeViewerPlugin-7hhtWj_E.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-ByfeIq4K.js → DocViewerPlugin-BWMSnRJe.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-Cksf3VZ-.js → GitDiffViewerPlugin-7J9h9Vy_.js} +20 -21
- package/src/ui/dist/assets/{ImageViewerPlugin-CFz-OsTS.js → ImageViewerPlugin-CHJl_0lr.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-CJ1cJzoX.js → LabCopilotPanel-1qSow1es.js} +11 -11
- package/src/ui/dist/assets/{LabPlugin-BF3dVJwa.js → LabPlugin-eQpPPCEp.js} +2 -1
- package/src/ui/dist/assets/{LatexPlugin-DDkwZ6Sj.js → LatexPlugin-BwRfi89Z.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-HAuvurcT.js → MarkdownViewerPlugin-836PVQWV.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-BtoTYy2C.js → MarketplacePlugin-C2y_556i.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-CSJYx7b-.js → NotebookEditor-BRzJbGsn.js} +12 -12
- package/src/ui/dist/assets/{NotebookEditor-DQgRezm_.js → NotebookEditor-DIX7Mlzu.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-DPa_-fv6.js → PdfLoader-DzRaTAlq.js} +14 -7
- package/src/ui/dist/assets/{PdfMarkdownPlugin-BZpXOEjm.js → PdfMarkdownPlugin-DZUfIUnp.js} +73 -6
- package/src/ui/dist/assets/{PdfViewerPlugin-BT8a6wGR.js → PdfViewerPlugin-BwtICzue.js} +103 -34
- package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +3627 -0
- package/src/ui/dist/assets/{SearchPlugin-D_blveZi.js → SearchPlugin-DHeIAMsx.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-Btx0M3hX.js → TextViewerPlugin-C3tCmFox.js} +5 -4
- package/src/ui/dist/assets/{VNCViewer-DImJO4rO.js → VNCViewer-CQsKVm3t.js} +10 -10
- package/src/ui/dist/assets/bot-BEA2vWuK.js +21 -0
- package/src/ui/dist/assets/branding/logo-rokid.png +0 -0
- package/src/ui/dist/assets/browser-BAcuE0Xj.js +2895 -0
- package/src/ui/dist/assets/{code-BUfXGJSl.js → code-XfbSR8K2.js} +1 -1
- package/src/ui/dist/assets/{file-content-VqamwI3X.js → file-content-BjxNaIfy.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-C_wOoS7a.js → file-diff-panel-D_lLVQk0.js} +1 -1
- package/src/ui/dist/assets/{file-socket-D2bTuMVP.js → file-socket-D9x_5vlY.js} +1 -1
- package/src/ui/dist/assets/{image-BZkGJ4mM.js → image-BhWT33W1.js} +1 -1
- package/src/ui/dist/assets/{index-DdRW6RMJ.js → index--c4iXtuy.js} +12 -12
- package/src/ui/dist/assets/{index-CxkvSeKw.js → index-BDxipwrC.js} +2 -2
- package/src/ui/dist/assets/{index-DjggJovS.js → index-DZTZ8mWP.js} +14934 -9613
- package/src/ui/dist/assets/{index-DXZ1daiJ.css → index-Dqj-Mjb4.css} +2 -13
- package/src/ui/dist/assets/index-PJbSbPTy.js +25 -0
- package/src/ui/dist/assets/{monaco-DHMc7kKM.js → monaco-K8izTGgo.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-DSw_D3RV.js → pdf-effect-queue-DfBors6y.js} +16 -1
- package/src/ui/dist/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
- package/src/ui/dist/assets/{popover-B85oCgCS.js → popover-yFK1J4fL.js} +1 -1
- package/src/ui/dist/assets/{project-sync-DOMCcPac.js → project-sync-PENr2zcz.js} +1 -74
- package/src/ui/dist/assets/select-CAbJDfYv.js +1690 -0
- package/src/ui/dist/assets/{sigma-BO2rQrl3.js → sigma-DEuYJqTl.js} +1 -1
- package/src/ui/dist/assets/{index-D9QIGcmc.js → square-check-big-omoSUmcd.js} +2 -13
- package/src/ui/dist/assets/{trash-BsVEH_dV.js → trash--F119N47.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-b8L6JuZm.js → useCliAccess-D31UR23I.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-BY7uA9hV.js → useFileDiffOverlay-BH6KcMzq.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-BwyVuUIK.js → wrap-text-CZ613PM5.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-RDpLugQP.js → zoom-out-BgDLAv3z.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/src/ui/dist/assets/AutoFigurePlugin-BGxN8Umr.css +0 -3056
- package/src/ui/dist/assets/AutoFigurePlugin-DxPdMUNb.js +0 -8149
- package/src/ui/dist/assets/PdfViewerPlugin-BJXtIwj_.css +0 -260
- package/src/ui/dist/assets/Stepper-DH2k75Vo.js +0 -158
- package/src/ui/dist/assets/bibtex-B-Hqu0Sg.js +0 -189
- package/src/ui/dist/assets/file-utils--zJCPN1i.js +0 -109
- package/src/ui/dist/assets/message-square-FUIPIhU2.js +0 -16
- package/src/ui/dist/assets/pdfjs-DU1YE8WO.js +0 -3
- package/src/ui/dist/assets/tooltip-B1OspAkx.js +0 -108
|
@@ -8,27 +8,33 @@ from pathlib import Path
|
|
|
8
8
|
from urllib.error import URLError
|
|
9
9
|
from urllib.request import Request
|
|
10
10
|
|
|
11
|
-
from ..connector_profiles import PROFILEABLE_CONNECTOR_NAMES, normalize_connector_config
|
|
12
|
-
from ..connector_runtime import infer_connector_transport
|
|
11
|
+
from ..connector.connector_profiles import PROFILEABLE_CONNECTOR_NAMES, list_connector_profiles, normalize_connector_config
|
|
12
|
+
from ..connector_runtime import build_discovered_target, infer_connector_transport
|
|
13
13
|
from ..home import repo_root
|
|
14
|
-
from ..lingzhu_support import (
|
|
14
|
+
from ..connector.lingzhu_support import (
|
|
15
|
+
generate_lingzhu_auth_ak,
|
|
16
|
+
lingzhu_auth_ak_needs_rotation,
|
|
15
17
|
lingzhu_agent_id,
|
|
16
18
|
lingzhu_generated_curl,
|
|
17
19
|
lingzhu_generated_openclaw_config_text,
|
|
18
20
|
lingzhu_gateway_port,
|
|
19
21
|
lingzhu_health_url,
|
|
22
|
+
lingzhu_is_passive_conversation_id,
|
|
20
23
|
lingzhu_local_base_url,
|
|
24
|
+
lingzhu_passive_conversation_id,
|
|
21
25
|
lingzhu_probe_payload,
|
|
22
26
|
lingzhu_public_base_url,
|
|
23
27
|
lingzhu_sse_url,
|
|
24
28
|
lingzhu_supported_commands,
|
|
29
|
+
public_base_url_looks_public,
|
|
25
30
|
)
|
|
26
|
-
from ..qq_profiles import (
|
|
31
|
+
from ..connector.qq_profiles import (
|
|
27
32
|
find_qq_profile,
|
|
28
33
|
list_qq_profiles,
|
|
29
34
|
normalize_qq_connector_config,
|
|
30
35
|
qq_profile_label,
|
|
31
36
|
)
|
|
37
|
+
from ..connector.weixin_support import normalize_weixin_base_url, normalize_weixin_cdn_base_url
|
|
32
38
|
from ..network import urlopen_with_proxy as urlopen
|
|
33
39
|
from ..runners.runtime_overrides import apply_codex_runtime_overrides, apply_runners_runtime_overrides
|
|
34
40
|
from ..shared import read_json, read_text, read_yaml, resolve_runner_binary, run_command, sha256_text, utc_now, which, write_text, write_yaml
|
|
@@ -104,7 +110,7 @@ class ConfigManager:
|
|
|
104
110
|
connectors = config.get("connectors") if isinstance(config.get("connectors"), dict) else {}
|
|
105
111
|
system_enabled = connectors.get("system_enabled") if isinstance(connectors.get("system_enabled"), dict) else {}
|
|
106
112
|
return {
|
|
107
|
-
name: self._coerce_bool(system_enabled.get(name), default=name
|
|
113
|
+
name: self._coerce_bool(system_enabled.get(name), default=name in {"qq", "weixin"})
|
|
108
114
|
for name in SYSTEM_CONNECTOR_NAMES
|
|
109
115
|
}
|
|
110
116
|
|
|
@@ -118,6 +124,8 @@ class ConfigManager:
|
|
|
118
124
|
return False
|
|
119
125
|
if normalized == "local":
|
|
120
126
|
return True
|
|
127
|
+
if normalized == "lingzhu":
|
|
128
|
+
return True
|
|
121
129
|
gates = self.system_connector_gates()
|
|
122
130
|
if normalized in gates:
|
|
123
131
|
return gates[normalized]
|
|
@@ -163,7 +171,25 @@ class ConfigManager:
|
|
|
163
171
|
return self.validate_named_text(name, self.render_named_payload(name, payload))
|
|
164
172
|
|
|
165
173
|
def save_named_payload(self, name: str, payload: dict) -> dict:
|
|
166
|
-
|
|
174
|
+
prepared = self._prepare_payload_for_save(name, payload)
|
|
175
|
+
return self.save_named_text(name, self.render_named_payload(name, prepared))
|
|
176
|
+
|
|
177
|
+
def _prepare_payload_for_save(self, name: str, payload: dict) -> dict:
|
|
178
|
+
prepared = deepcopy(payload) if isinstance(payload, dict) else {}
|
|
179
|
+
if name != "connectors":
|
|
180
|
+
return prepared
|
|
181
|
+
lingzhu = prepared.get("lingzhu")
|
|
182
|
+
if not isinstance(lingzhu, dict):
|
|
183
|
+
return prepared
|
|
184
|
+
enabled = self._coerce_bool(lingzhu.get("enabled"), default=False)
|
|
185
|
+
raw_public_base_url = str(lingzhu.get("public_base_url") or "").strip()
|
|
186
|
+
direct_auth_ak = str(lingzhu.get("auth_ak") or "").strip()
|
|
187
|
+
if lingzhu_auth_ak_needs_rotation(direct_auth_ak):
|
|
188
|
+
lingzhu["auth_ak"] = generate_lingzhu_auth_ak()
|
|
189
|
+
elif (enabled or raw_public_base_url) and not self._has_secret(lingzhu, "auth_ak", "auth_ak_env"):
|
|
190
|
+
lingzhu["auth_ak"] = generate_lingzhu_auth_ak()
|
|
191
|
+
prepared["lingzhu"] = lingzhu
|
|
192
|
+
return prepared
|
|
167
193
|
|
|
168
194
|
def bind_qq_main_chat(self, *, profile_id: str | None = None, chat_id: str) -> dict:
|
|
169
195
|
normalized_chat_id = str(chat_id or "").strip()
|
|
@@ -283,7 +309,7 @@ This page edits `~/DeepScientist/config/connectors.yaml` directly.
|
|
|
283
309
|
|
|
284
310
|
## What this page is for
|
|
285
311
|
|
|
286
|
-
- connect Telegram, Discord, Slack, Feishu, WhatsApp, or QQ
|
|
312
|
+
- connect Weixin, Telegram, Discord, Slack, Feishu, WhatsApp, or QQ
|
|
287
313
|
- choose one preferred connector for proactive artifact updates
|
|
288
314
|
- decide whether artifact updates fan out or stay focused
|
|
289
315
|
- use the built-in direct runtime for each connector
|
|
@@ -291,7 +317,7 @@ This page edits `~/DeepScientist/config/connectors.yaml` directly.
|
|
|
291
317
|
|
|
292
318
|
## Recommended order
|
|
293
319
|
|
|
294
|
-
1.
|
|
320
|
+
1. configure one connector first
|
|
295
321
|
2. fill the required token or secret fields
|
|
296
322
|
3. click **Validate**
|
|
297
323
|
4. click **Test**
|
|
@@ -299,6 +325,7 @@ This page edits `~/DeepScientist/config/connectors.yaml` directly.
|
|
|
299
325
|
|
|
300
326
|
## Preferred transports
|
|
301
327
|
|
|
328
|
+
- Weixin: `ilink_long_poll`
|
|
302
329
|
- Telegram: `polling`
|
|
303
330
|
- Slack: `socket_mode`
|
|
304
331
|
- Discord: `gateway`
|
|
@@ -321,6 +348,13 @@ This page edits `~/DeepScientist/config/connectors.yaml` directly.
|
|
|
321
348
|
- prefer `transport: socket_mode`
|
|
322
349
|
- readiness test uses `auth.test`
|
|
323
350
|
|
|
351
|
+
### Weixin
|
|
352
|
+
|
|
353
|
+
- scan the QR code first so DeepScientist can persist `bot_token` and `account_id`
|
|
354
|
+
- keep `transport: ilink_long_poll`
|
|
355
|
+
- every reply depends on the latest inbound `context_token`
|
|
356
|
+
- media send uses the built-in AES + CDN upload path for image, video, and file attachments
|
|
357
|
+
|
|
324
358
|
### Discord
|
|
325
359
|
|
|
326
360
|
- set `bot_token`
|
|
@@ -354,12 +388,12 @@ This page edits `~/DeepScientist/config/connectors.yaml` directly.
|
|
|
354
388
|
|
|
355
389
|
### Lingzhu
|
|
356
390
|
|
|
357
|
-
- Lingzhu is
|
|
391
|
+
- Lingzhu is hosted directly by DeepScientist on `/metis/agent/api`
|
|
358
392
|
- keep `transport: openclaw_sse`
|
|
359
|
-
-
|
|
360
|
-
-
|
|
361
|
-
-
|
|
362
|
-
-
|
|
393
|
+
- use the same public DeepScientist origin and port that the browser is already serving on
|
|
394
|
+
- save once so DeepScientist can persist `auth_ak`; Rokid must use the same Bearer token
|
|
395
|
+
- `public_base_url` must be a public IP or public domain; loopback and private addresses are invalid for Rokid
|
|
396
|
+
- new Lingzhu tasks must start with `我现在的任务是`; other requests are treated as reconnect or progress polling
|
|
363
397
|
|
|
364
398
|
## Safety
|
|
365
399
|
|
|
@@ -680,9 +714,10 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
680
714
|
continue
|
|
681
715
|
config = raw_config
|
|
682
716
|
enabled = bool(config.get("enabled", False))
|
|
683
|
-
if not
|
|
717
|
+
if not self._should_validate_connector(str(name), config):
|
|
684
718
|
continue
|
|
685
|
-
|
|
719
|
+
if enabled:
|
|
720
|
+
enabled_connectors.append(str(name))
|
|
686
721
|
|
|
687
722
|
if name == "qq":
|
|
688
723
|
profiles = list_qq_profiles(config)
|
|
@@ -746,6 +781,15 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
746
781
|
errors.append("slack: `transport: socket_mode` requires `bot_token` or `bot_token_env`.")
|
|
747
782
|
if not has_app_token:
|
|
748
783
|
errors.append("slack: `transport: socket_mode` requires `app_token` or `app_token_env`.")
|
|
784
|
+
elif name == "weixin":
|
|
785
|
+
if transport != "ilink_long_poll":
|
|
786
|
+
errors.append("weixin: `transport` must stay `ilink_long_poll`.")
|
|
787
|
+
if not self._has_secret(config, "bot_token", "bot_token_env"):
|
|
788
|
+
errors.append("weixin: requires `bot_token` or `bot_token_env` after QR login.")
|
|
789
|
+
if not str(config.get("account_id") or "").strip():
|
|
790
|
+
errors.append("weixin: requires `account_id` after QR login.")
|
|
791
|
+
if not str(config.get("login_user_id") or "").strip():
|
|
792
|
+
warnings.append("weixin: `login_user_id` is empty. Save the scanner user id after QR login for easier diagnostics.")
|
|
749
793
|
elif name == "feishu":
|
|
750
794
|
has_app_id = bool(str(config.get("app_id") or "").strip())
|
|
751
795
|
has_app_secret = self._has_secret(config, "app_secret", "app_secret_env")
|
|
@@ -767,6 +811,8 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
767
811
|
warnings.append("lingzhu: `local_host` is empty; DeepScientist will fall back to `127.0.0.1`.")
|
|
768
812
|
if not self._has_secret(config, "auth_ak", "auth_ak_env"):
|
|
769
813
|
errors.append("lingzhu: requires `auth_ak` for Bearer authentication.")
|
|
814
|
+
elif lingzhu_auth_ak_needs_rotation(self._secret(config, "auth_ak", "auth_ak_env")):
|
|
815
|
+
errors.append("lingzhu: `auth_ak` is still using the bundled example token; generate a new random AK before binding Rokid.")
|
|
770
816
|
raw_gateway_port = str(config.get("gateway_port") or "").strip()
|
|
771
817
|
normalized_port = lingzhu_gateway_port(config)
|
|
772
818
|
if raw_gateway_port and str(normalized_port) != raw_gateway_port:
|
|
@@ -775,6 +821,10 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
775
821
|
public_base_url = lingzhu_public_base_url(config)
|
|
776
822
|
if raw_public_base_url and public_base_url is None:
|
|
777
823
|
errors.append("lingzhu: `public_base_url` must be a valid `http://` or `https://` URL when set.")
|
|
824
|
+
elif raw_public_base_url and not public_base_url_looks_public(raw_public_base_url):
|
|
825
|
+
errors.append(
|
|
826
|
+
"lingzhu: `public_base_url` must be a public IP or public domain. `127.0.0.1`, `localhost`, and private network addresses cannot be registered on Rokid."
|
|
827
|
+
)
|
|
778
828
|
raw_visible_progress_heartbeat_sec = str(
|
|
779
829
|
config.get("visible_progress_heartbeat_sec") or ""
|
|
780
830
|
).strip()
|
|
@@ -817,14 +867,14 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
817
867
|
if not isinstance(raw_config, dict):
|
|
818
868
|
continue
|
|
819
869
|
config = raw_config
|
|
820
|
-
if not
|
|
870
|
+
if not self._should_validate_connector(str(name), config):
|
|
821
871
|
continue
|
|
822
872
|
target = (delivery_targets or {}).get(name)
|
|
823
873
|
items.append(self._test_single_connector(name, config, live=live, delivery_target=target if isinstance(target, dict) else None))
|
|
824
874
|
return {
|
|
825
875
|
"ok": all(item["ok"] for item in items) if items else True,
|
|
826
876
|
"name": "connectors",
|
|
827
|
-
"summary": "Connector test completed." if items else "No
|
|
877
|
+
"summary": "Connector test completed." if items else "No configured connectors to test.",
|
|
828
878
|
"warnings": [],
|
|
829
879
|
"errors": [],
|
|
830
880
|
"items": items,
|
|
@@ -963,6 +1013,25 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
963
1013
|
details["identity"] = profile_results[0].get("app_id")
|
|
964
1014
|
details["gateway_url"] = profile_results[0].get("gateway_url")
|
|
965
1015
|
details["token_expires_in"] = profile_results[0].get("token_expires_in")
|
|
1016
|
+
elif name == "weixin":
|
|
1017
|
+
details.update(
|
|
1018
|
+
{
|
|
1019
|
+
"base_url": normalize_weixin_base_url(config.get("base_url")),
|
|
1020
|
+
"cdn_base_url": normalize_weixin_cdn_base_url(config.get("cdn_base_url")),
|
|
1021
|
+
"account_id": str(config.get("account_id") or "").strip() or None,
|
|
1022
|
+
"login_user_id": str(config.get("login_user_id") or "").strip() or None,
|
|
1023
|
+
}
|
|
1024
|
+
)
|
|
1025
|
+
if transport != "ilink_long_poll":
|
|
1026
|
+
errors.append("Weixin transport must stay `ilink_long_poll`.")
|
|
1027
|
+
if not self._has_secret(config, "bot_token", "bot_token_env"):
|
|
1028
|
+
errors.append("Weixin requires `bot_token` after QR login.")
|
|
1029
|
+
if not str(config.get("account_id") or "").strip():
|
|
1030
|
+
errors.append("Weixin requires `account_id` after QR login.")
|
|
1031
|
+
if live and not errors:
|
|
1032
|
+
warnings.append(
|
|
1033
|
+
"Weixin readiness is credential-based. Send one inbound Weixin message to populate `context_token` before testing outbound delivery."
|
|
1034
|
+
)
|
|
966
1035
|
elif name == "lingzhu":
|
|
967
1036
|
details.update(self._lingzhu_snapshot_details(config))
|
|
968
1037
|
auth_ak = self._secret(config, "auth_ak", "auth_ak_env")
|
|
@@ -1070,11 +1139,85 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1070
1139
|
"If you received it, the connector binding and outbound delivery path are working."
|
|
1071
1140
|
)
|
|
1072
1141
|
|
|
1142
|
+
@staticmethod
|
|
1143
|
+
def _codex_should_inherit_model(model: object) -> bool:
|
|
1144
|
+
normalized = str(model or "").strip().lower()
|
|
1145
|
+
return normalized in {"", "inherit", "default", "codex-default"}
|
|
1146
|
+
|
|
1147
|
+
@staticmethod
|
|
1148
|
+
def _codex_requested_model(config: dict) -> str:
|
|
1149
|
+
raw_model = config.get("model")
|
|
1150
|
+
if raw_model is None:
|
|
1151
|
+
return "gpt-5.4"
|
|
1152
|
+
return str(raw_model).strip()
|
|
1153
|
+
|
|
1154
|
+
@staticmethod
|
|
1155
|
+
def _codex_model_unavailable(stdout_text: str, stderr_text: str) -> bool:
|
|
1156
|
+
haystack = f"{stdout_text}\n{stderr_text}".lower()
|
|
1157
|
+
markers = [
|
|
1158
|
+
"unknown model",
|
|
1159
|
+
"invalid model",
|
|
1160
|
+
"model not found",
|
|
1161
|
+
"unsupported model",
|
|
1162
|
+
"model is not available",
|
|
1163
|
+
"not authorized to use model",
|
|
1164
|
+
"you do not have access",
|
|
1165
|
+
"access to model",
|
|
1166
|
+
"model access",
|
|
1167
|
+
"unrecognized model",
|
|
1168
|
+
]
|
|
1169
|
+
return any(marker in haystack for marker in markers)
|
|
1170
|
+
|
|
1171
|
+
def _build_codex_probe_command(
|
|
1172
|
+
self,
|
|
1173
|
+
*,
|
|
1174
|
+
resolved_binary: str,
|
|
1175
|
+
requested_model: str,
|
|
1176
|
+
approval_policy: str,
|
|
1177
|
+
reasoning_effort: str | None,
|
|
1178
|
+
sandbox_mode: str,
|
|
1179
|
+
) -> list[str]:
|
|
1180
|
+
command = [
|
|
1181
|
+
resolved_binary,
|
|
1182
|
+
"--search",
|
|
1183
|
+
"exec",
|
|
1184
|
+
"--json",
|
|
1185
|
+
"--cd",
|
|
1186
|
+
str(repo_root()),
|
|
1187
|
+
"--skip-git-repo-check",
|
|
1188
|
+
]
|
|
1189
|
+
if not self._codex_should_inherit_model(requested_model):
|
|
1190
|
+
command.extend(["--model", requested_model])
|
|
1191
|
+
if approval_policy:
|
|
1192
|
+
command.extend(["-c", f'approval_policy="{approval_policy}"'])
|
|
1193
|
+
if reasoning_effort:
|
|
1194
|
+
command.extend(["-c", f'model_reasoning_effort="{reasoning_effort}"'])
|
|
1195
|
+
if sandbox_mode:
|
|
1196
|
+
command.extend(["--sandbox", sandbox_mode])
|
|
1197
|
+
command.append("-")
|
|
1198
|
+
return command
|
|
1199
|
+
|
|
1200
|
+
def _persist_codex_model_inherit(self, requested_model: object) -> None:
|
|
1201
|
+
normalized_requested_model = str(requested_model or "").strip()
|
|
1202
|
+
if not normalized_requested_model:
|
|
1203
|
+
return
|
|
1204
|
+
runners = self.load_named("runners")
|
|
1205
|
+
codex = runners.get("codex") if isinstance(runners.get("codex"), dict) else None
|
|
1206
|
+
if not isinstance(codex, dict):
|
|
1207
|
+
return
|
|
1208
|
+
current_model = str(codex.get("model") or "").strip()
|
|
1209
|
+
if current_model != normalized_requested_model:
|
|
1210
|
+
return
|
|
1211
|
+
codex["model"] = "inherit"
|
|
1212
|
+
runners["codex"] = codex
|
|
1213
|
+
self.save_named_payload("runners", runners)
|
|
1214
|
+
|
|
1073
1215
|
def _probe_codex_runner(self, config: dict) -> dict:
|
|
1074
1216
|
config = apply_codex_runtime_overrides(config)
|
|
1075
1217
|
checked_at = utc_now()
|
|
1076
1218
|
binary = str(config.get("binary") or "codex").strip() or "codex"
|
|
1077
1219
|
resolved_binary = resolve_runner_binary(binary, runner_name="codex")
|
|
1220
|
+
requested_model = self._codex_requested_model(config)
|
|
1078
1221
|
raw_reasoning_effort = config.get("model_reasoning_effort")
|
|
1079
1222
|
reasoning_effort = (
|
|
1080
1223
|
str(raw_reasoning_effort).strip()
|
|
@@ -1085,10 +1228,14 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1085
1228
|
"binary": binary,
|
|
1086
1229
|
"resolved_binary": resolved_binary,
|
|
1087
1230
|
"config_dir": str(config.get("config_dir") or "~/.codex"),
|
|
1088
|
-
"model":
|
|
1231
|
+
"model": requested_model or "inherit",
|
|
1232
|
+
"requested_model": requested_model or "inherit",
|
|
1233
|
+
"effective_model": requested_model or "inherit",
|
|
1089
1234
|
"approval_policy": str(config.get("approval_policy") or "on-request"),
|
|
1090
1235
|
"sandbox_mode": str(config.get("sandbox_mode") or "workspace-write"),
|
|
1091
1236
|
"reasoning_effort": reasoning_effort,
|
|
1237
|
+
"model_fallback_attempted": False,
|
|
1238
|
+
"model_fallback_used": False,
|
|
1092
1239
|
"checked_at": checked_at,
|
|
1093
1240
|
}
|
|
1094
1241
|
if not resolved_binary:
|
|
@@ -1103,30 +1250,14 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1103
1250
|
"details": details,
|
|
1104
1251
|
"guidance": [
|
|
1105
1252
|
"Run `npm install -g @researai/deepscientist` again so the bundled Codex dependency is installed.",
|
|
1253
|
+
"If `codex` is still missing, install it explicitly with `npm install -g @openai/codex`.",
|
|
1254
|
+
"Run `codex --login` (or `codex`) once and finish authentication before starting DeepScientist.",
|
|
1106
1255
|
"If you use a custom Codex path, set `runners.codex.binary` to that absolute executable path.",
|
|
1107
1256
|
],
|
|
1108
1257
|
}
|
|
1109
1258
|
|
|
1110
|
-
command = [
|
|
1111
|
-
resolved_binary,
|
|
1112
|
-
"--search",
|
|
1113
|
-
"exec",
|
|
1114
|
-
"--json",
|
|
1115
|
-
"--cd",
|
|
1116
|
-
str(repo_root()),
|
|
1117
|
-
"--skip-git-repo-check",
|
|
1118
|
-
"--model",
|
|
1119
|
-
str(config.get("model") or "gpt-5.4"),
|
|
1120
|
-
]
|
|
1121
1259
|
approval_policy = str(config.get("approval_policy") or "on-request").strip()
|
|
1122
|
-
if approval_policy:
|
|
1123
|
-
command.extend(["-c", f'approval_policy="{approval_policy}"'])
|
|
1124
|
-
if reasoning_effort:
|
|
1125
|
-
command.extend(["-c", f'model_reasoning_effort="{reasoning_effort}"'])
|
|
1126
1260
|
sandbox_mode = str(config.get("sandbox_mode") or "workspace-write").strip()
|
|
1127
|
-
if sandbox_mode:
|
|
1128
|
-
command.extend(["--sandbox", sandbox_mode])
|
|
1129
|
-
command.append("-")
|
|
1130
1261
|
|
|
1131
1262
|
env = os.environ.copy()
|
|
1132
1263
|
config_dir = str(config.get("config_dir") or "~/.codex").strip()
|
|
@@ -1134,23 +1265,36 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1134
1265
|
env["CODEX_HOME"] = str(Path(config_dir).expanduser())
|
|
1135
1266
|
prompt = "Reply with exactly HELLO."
|
|
1136
1267
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
capture_output=True,
|
|
1145
|
-
timeout=90,
|
|
1146
|
-
check=False,
|
|
1268
|
+
def run_probe_once(model_for_command: str) -> tuple[list[str], subprocess.CompletedProcess[str] | None, subprocess.TimeoutExpired | None]:
|
|
1269
|
+
command = self._build_codex_probe_command(
|
|
1270
|
+
resolved_binary=resolved_binary,
|
|
1271
|
+
requested_model=model_for_command,
|
|
1272
|
+
approval_policy=approval_policy,
|
|
1273
|
+
reasoning_effort=reasoning_effort,
|
|
1274
|
+
sandbox_mode=sandbox_mode,
|
|
1147
1275
|
)
|
|
1148
|
-
|
|
1276
|
+
try:
|
|
1277
|
+
result = subprocess.run(
|
|
1278
|
+
command,
|
|
1279
|
+
input=prompt,
|
|
1280
|
+
cwd=str(repo_root()),
|
|
1281
|
+
env=env,
|
|
1282
|
+
text=True,
|
|
1283
|
+
capture_output=True,
|
|
1284
|
+
timeout=90,
|
|
1285
|
+
check=False,
|
|
1286
|
+
)
|
|
1287
|
+
except subprocess.TimeoutExpired as exc:
|
|
1288
|
+
return command, None, exc
|
|
1289
|
+
return command, result, None
|
|
1290
|
+
|
|
1291
|
+
command, result, timeout_error = run_probe_once(requested_model)
|
|
1292
|
+
if timeout_error is not None:
|
|
1149
1293
|
details.update(
|
|
1150
1294
|
{
|
|
1151
1295
|
"exit_code": None,
|
|
1152
|
-
"stdout_excerpt": self._compact_probe_text(
|
|
1153
|
-
"stderr_excerpt": self._compact_probe_text(
|
|
1296
|
+
"stdout_excerpt": self._compact_probe_text(timeout_error.stdout or ""),
|
|
1297
|
+
"stderr_excerpt": self._compact_probe_text(timeout_error.stderr or ""),
|
|
1154
1298
|
"probe_command": command,
|
|
1155
1299
|
}
|
|
1156
1300
|
)
|
|
@@ -1160,19 +1304,73 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1160
1304
|
"warnings": [],
|
|
1161
1305
|
"errors": [
|
|
1162
1306
|
"Codex did not answer the startup hello probe within 90 seconds.",
|
|
1163
|
-
"Run `codex` manually, complete login, then retry DeepScientist.",
|
|
1307
|
+
"Run `codex --login` (or `codex`) manually, complete login, then retry DeepScientist.",
|
|
1164
1308
|
],
|
|
1165
1309
|
"details": details,
|
|
1166
1310
|
"guidance": [
|
|
1167
|
-
"Run `codex` manually to finish interactive login or first-run setup.",
|
|
1168
|
-
"
|
|
1311
|
+
"Run `codex --login` (or `codex`) manually to finish interactive login or first-run setup.",
|
|
1312
|
+
"If `codex` is missing on PATH, install it explicitly with `npm install -g @openai/codex`.",
|
|
1313
|
+
"Confirm the configured model is available to your account. DeepScientist currently probes Codex with the configured runner model.",
|
|
1314
|
+
"Run `ds doctor` after login, then start DeepScientist again.",
|
|
1169
1315
|
],
|
|
1170
1316
|
}
|
|
1171
1317
|
|
|
1318
|
+
assert result is not None
|
|
1172
1319
|
stdout_text = (result.stdout or "").strip()
|
|
1173
1320
|
stderr_text = (result.stderr or "").strip()
|
|
1174
1321
|
hello_seen = "HELLO" in stdout_text.upper()
|
|
1175
1322
|
ok = result.returncode == 0 and hello_seen
|
|
1323
|
+
fallback_warning: str | None = None
|
|
1324
|
+
if (
|
|
1325
|
+
not ok
|
|
1326
|
+
and not self._codex_should_inherit_model(requested_model)
|
|
1327
|
+
and self._codex_model_unavailable(stdout_text, stderr_text)
|
|
1328
|
+
):
|
|
1329
|
+
details["model_fallback_attempted"] = True
|
|
1330
|
+
fallback_command, fallback_result, fallback_timeout = run_probe_once("inherit")
|
|
1331
|
+
details["initial_probe_command"] = command
|
|
1332
|
+
details["initial_exit_code"] = result.returncode
|
|
1333
|
+
details["initial_stdout_excerpt"] = self._compact_probe_text(stdout_text)
|
|
1334
|
+
details["initial_stderr_excerpt"] = self._compact_probe_text(stderr_text)
|
|
1335
|
+
details["fallback_probe_command"] = fallback_command
|
|
1336
|
+
if fallback_timeout is None and fallback_result is not None:
|
|
1337
|
+
fallback_stdout_text = (fallback_result.stdout or "").strip()
|
|
1338
|
+
fallback_stderr_text = (fallback_result.stderr or "").strip()
|
|
1339
|
+
fallback_hello_seen = "HELLO" in fallback_stdout_text.upper()
|
|
1340
|
+
fallback_ok = fallback_result.returncode == 0 and fallback_hello_seen
|
|
1341
|
+
details["fallback_exit_code"] = fallback_result.returncode
|
|
1342
|
+
details["fallback_stdout_excerpt"] = self._compact_probe_text(fallback_stdout_text)
|
|
1343
|
+
details["fallback_stderr_excerpt"] = self._compact_probe_text(fallback_stderr_text)
|
|
1344
|
+
if fallback_ok:
|
|
1345
|
+
details.update(
|
|
1346
|
+
{
|
|
1347
|
+
"exit_code": fallback_result.returncode,
|
|
1348
|
+
"stdout_excerpt": self._compact_probe_text(fallback_stdout_text),
|
|
1349
|
+
"stderr_excerpt": self._compact_probe_text(fallback_stderr_text),
|
|
1350
|
+
"probe_command": fallback_command,
|
|
1351
|
+
"effective_model": "inherit",
|
|
1352
|
+
"model_fallback_used": True,
|
|
1353
|
+
}
|
|
1354
|
+
)
|
|
1355
|
+
fallback_warning = (
|
|
1356
|
+
f"Configured Codex model `{requested_model}` is not available. "
|
|
1357
|
+
"DeepScientist fell back to the current Codex default model."
|
|
1358
|
+
)
|
|
1359
|
+
return {
|
|
1360
|
+
"ok": True,
|
|
1361
|
+
"summary": "Codex startup probe completed with Codex default model fallback.",
|
|
1362
|
+
"warnings": [fallback_warning],
|
|
1363
|
+
"errors": [],
|
|
1364
|
+
"details": details,
|
|
1365
|
+
"guidance": [
|
|
1366
|
+
"DeepScientist switched the Codex runner model to `inherit` so future runs keep using the current Codex default model.",
|
|
1367
|
+
],
|
|
1368
|
+
}
|
|
1369
|
+
else:
|
|
1370
|
+
details["fallback_exit_code"] = None
|
|
1371
|
+
details["fallback_stdout_excerpt"] = self._compact_probe_text((fallback_timeout.stdout if fallback_timeout else "") or "")
|
|
1372
|
+
details["fallback_stderr_excerpt"] = self._compact_probe_text((fallback_timeout.stderr if fallback_timeout else "") or "")
|
|
1373
|
+
|
|
1176
1374
|
details.update(
|
|
1177
1375
|
{
|
|
1178
1376
|
"exit_code": result.returncode,
|
|
@@ -1189,7 +1387,9 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1189
1387
|
errors.append("Codex responded, but the reply did not contain the expected `HELLO` marker.")
|
|
1190
1388
|
if stderr_text:
|
|
1191
1389
|
warnings.append("Codex returned stderr during the startup probe.")
|
|
1192
|
-
|
|
1390
|
+
if details.get("model_fallback_attempted") and not details.get("model_fallback_used"):
|
|
1391
|
+
warnings.append("DeepScientist also tried the current Codex default model, but that fallback probe did not succeed.")
|
|
1392
|
+
errors.append("Run `codex --login` (or `codex`) once and complete login before starting DeepScientist.")
|
|
1193
1393
|
return {
|
|
1194
1394
|
"ok": ok,
|
|
1195
1395
|
"summary": "Codex startup probe completed." if ok else "Codex startup probe failed.",
|
|
@@ -1197,8 +1397,10 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1197
1397
|
"errors": errors,
|
|
1198
1398
|
"details": details,
|
|
1199
1399
|
"guidance": [] if ok else [
|
|
1200
|
-
"Run `codex` in a terminal and complete login or first-run setup.",
|
|
1201
|
-
"
|
|
1400
|
+
"Run `codex --login` (or `codex`) in a terminal and complete login or first-run setup.",
|
|
1401
|
+
"If `codex` is missing, install it explicitly with `npm install -g @openai/codex`.",
|
|
1402
|
+
"If the configured model is not available to your Codex account, update `~/DeepScientist/config/runners.yaml` and try again.",
|
|
1403
|
+
"Then run `ds doctor` and start DeepScientist again.",
|
|
1202
1404
|
],
|
|
1203
1405
|
}
|
|
1204
1406
|
|
|
@@ -1217,6 +1419,10 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1217
1419
|
"binary": details.get("binary"),
|
|
1218
1420
|
"resolved_binary": details.get("resolved_binary"),
|
|
1219
1421
|
"model": details.get("model"),
|
|
1422
|
+
"requested_model": details.get("requested_model"),
|
|
1423
|
+
"effective_model": details.get("effective_model"),
|
|
1424
|
+
"model_fallback_attempted": bool(details.get("model_fallback_attempted")),
|
|
1425
|
+
"model_fallback_used": bool(details.get("model_fallback_used")),
|
|
1220
1426
|
"approval_policy": details.get("approval_policy"),
|
|
1221
1427
|
"sandbox_mode": details.get("sandbox_mode"),
|
|
1222
1428
|
"reasoning_effort": details.get("reasoning_effort"),
|
|
@@ -1226,6 +1432,8 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1226
1432
|
}
|
|
1227
1433
|
config["bootstrap"] = bootstrap
|
|
1228
1434
|
self.save_named_payload("config", config)
|
|
1435
|
+
if bool(result.get("ok")) and bool(details.get("model_fallback_used")):
|
|
1436
|
+
self._persist_codex_model_inherit(details.get("requested_model"))
|
|
1229
1437
|
|
|
1230
1438
|
@staticmethod
|
|
1231
1439
|
def _compact_probe_text(value: str, *, limit: int = 1200) -> str:
|
|
@@ -1236,6 +1444,41 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1236
1444
|
|
|
1237
1445
|
def lingzhu_snapshot(self, config: dict | None = None) -> dict:
|
|
1238
1446
|
resolved = dict(config or self.load_named_normalized("connectors").get("lingzhu") or {})
|
|
1447
|
+
state = self._lingzhu_runtime_state()
|
|
1448
|
+
raw_bindings = self._lingzhu_bindings()
|
|
1449
|
+
last_real_conversation_id = str(state.get("last_conversation_id") or "").strip() or None
|
|
1450
|
+
has_auth_ak = bool(self._secret(resolved, "auth_ak", "auth_ak_env"))
|
|
1451
|
+
passive_conversation_id = (
|
|
1452
|
+
lingzhu_passive_conversation_id(resolved)
|
|
1453
|
+
if bool(resolved.get("enabled", False)) and has_auth_ak
|
|
1454
|
+
else None
|
|
1455
|
+
)
|
|
1456
|
+
effective_binding = (
|
|
1457
|
+
self._lingzhu_effective_binding(raw_bindings, passive_conversation_id)
|
|
1458
|
+
if passive_conversation_id
|
|
1459
|
+
else None
|
|
1460
|
+
)
|
|
1461
|
+
bindings = [effective_binding] if isinstance(effective_binding, dict) and effective_binding.get("quest_id") else []
|
|
1462
|
+
default_target = (
|
|
1463
|
+
{
|
|
1464
|
+
**(
|
|
1465
|
+
build_discovered_target(
|
|
1466
|
+
passive_conversation_id,
|
|
1467
|
+
source="passive_binding",
|
|
1468
|
+
is_default=True,
|
|
1469
|
+
label="Passive binding",
|
|
1470
|
+
quest_id=str((effective_binding or {}).get("quest_id") or "").strip() or None,
|
|
1471
|
+
updated_at=str((effective_binding or {}).get("updated_at") or "").strip() or None,
|
|
1472
|
+
)
|
|
1473
|
+
or {}
|
|
1474
|
+
),
|
|
1475
|
+
"selectable": True,
|
|
1476
|
+
"is_passive": True,
|
|
1477
|
+
}
|
|
1478
|
+
if passive_conversation_id
|
|
1479
|
+
else None
|
|
1480
|
+
)
|
|
1481
|
+
discovered_targets = [default_target] if isinstance(default_target, dict) and default_target else []
|
|
1239
1482
|
snapshot: dict[str, object] = {
|
|
1240
1483
|
"name": "lingzhu",
|
|
1241
1484
|
"display_mode": "companion_config",
|
|
@@ -1243,22 +1486,27 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1243
1486
|
"transport": "openclaw_sse",
|
|
1244
1487
|
"enabled": bool(resolved.get("enabled", False)),
|
|
1245
1488
|
"main_chat_id": None,
|
|
1246
|
-
"last_conversation_id":
|
|
1489
|
+
"last_conversation_id": passive_conversation_id,
|
|
1247
1490
|
"inbox_count": 0,
|
|
1248
1491
|
"outbox_count": 0,
|
|
1249
1492
|
"ignored_count": 0,
|
|
1250
|
-
"binding_count":
|
|
1251
|
-
"
|
|
1493
|
+
"binding_count": len(bindings),
|
|
1494
|
+
"bindings": bindings,
|
|
1495
|
+
"target_count": len(discovered_targets),
|
|
1252
1496
|
"recent_conversations": [],
|
|
1253
1497
|
"recent_events": [],
|
|
1254
|
-
"
|
|
1498
|
+
"known_targets": [],
|
|
1499
|
+
"discovered_targets": discovered_targets,
|
|
1500
|
+
"default_target": default_target,
|
|
1255
1501
|
"details": self._lingzhu_snapshot_details(resolved),
|
|
1256
1502
|
}
|
|
1503
|
+
snapshot["details"]["last_real_conversation_id"] = last_real_conversation_id
|
|
1504
|
+
snapshot["details"]["historical_target_count"] = len(self._lingzhu_recent_conversations(state))
|
|
1257
1505
|
if not snapshot["enabled"]:
|
|
1258
1506
|
snapshot["connection_state"] = "disabled"
|
|
1259
1507
|
snapshot["auth_state"] = "disabled"
|
|
1260
1508
|
return snapshot
|
|
1261
|
-
snapshot["auth_state"] = "ready" if
|
|
1509
|
+
snapshot["auth_state"] = "ready" if has_auth_ak else "missing_auth_ak"
|
|
1262
1510
|
health_probe = self._probe_lingzhu_health(resolved, timeout=1.5)
|
|
1263
1511
|
snapshot["details"]["health_probe"] = health_probe
|
|
1264
1512
|
if health_probe.get("ok", False):
|
|
@@ -1269,6 +1517,80 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1269
1517
|
snapshot["last_error"] = health_probe.get("message")
|
|
1270
1518
|
return snapshot
|
|
1271
1519
|
|
|
1520
|
+
def _lingzhu_runtime_state(self) -> dict[str, object]:
|
|
1521
|
+
payload = read_json(self.home / "logs" / "connectors" / "lingzhu" / "state.json", {})
|
|
1522
|
+
return payload if isinstance(payload, dict) else {}
|
|
1523
|
+
|
|
1524
|
+
def _lingzhu_bindings(self) -> list[dict[str, object]]:
|
|
1525
|
+
payload = read_json(self.home / "logs" / "connectors" / "lingzhu" / "bindings.json", {"bindings": {}})
|
|
1526
|
+
raw_bindings = payload.get("bindings") if isinstance(payload, dict) else {}
|
|
1527
|
+
if not isinstance(raw_bindings, dict):
|
|
1528
|
+
return []
|
|
1529
|
+
items: list[dict[str, object]] = []
|
|
1530
|
+
for conversation_id, binding in sorted(raw_bindings.items()):
|
|
1531
|
+
if not isinstance(binding, dict):
|
|
1532
|
+
continue
|
|
1533
|
+
normalized_conversation_id = str(conversation_id or "").strip()
|
|
1534
|
+
if not normalized_conversation_id:
|
|
1535
|
+
continue
|
|
1536
|
+
items.append(
|
|
1537
|
+
{
|
|
1538
|
+
"conversation_id": normalized_conversation_id,
|
|
1539
|
+
"quest_id": str(binding.get("quest_id") or "").strip() or None,
|
|
1540
|
+
"updated_at": str(binding.get("updated_at") or "").strip() or None,
|
|
1541
|
+
"profile_id": None,
|
|
1542
|
+
"profile_label": None,
|
|
1543
|
+
"is_passive": lingzhu_is_passive_conversation_id(normalized_conversation_id),
|
|
1544
|
+
}
|
|
1545
|
+
)
|
|
1546
|
+
return items
|
|
1547
|
+
|
|
1548
|
+
@staticmethod
|
|
1549
|
+
def _lingzhu_effective_binding(
|
|
1550
|
+
bindings: list[dict[str, object]],
|
|
1551
|
+
passive_conversation_id: str | None,
|
|
1552
|
+
) -> dict[str, object] | None:
|
|
1553
|
+
normalized_passive_conversation_id = str(passive_conversation_id or "").strip()
|
|
1554
|
+
if not normalized_passive_conversation_id:
|
|
1555
|
+
return None
|
|
1556
|
+
candidate_bindings = [
|
|
1557
|
+
dict(item)
|
|
1558
|
+
for item in bindings
|
|
1559
|
+
if isinstance(item, dict) and str(item.get("quest_id") or "").strip()
|
|
1560
|
+
]
|
|
1561
|
+
if not candidate_bindings:
|
|
1562
|
+
return None
|
|
1563
|
+
selected = max(
|
|
1564
|
+
candidate_bindings,
|
|
1565
|
+
key=lambda item: (
|
|
1566
|
+
str(item.get("updated_at") or ""),
|
|
1567
|
+
str(item.get("quest_id") or ""),
|
|
1568
|
+
str(item.get("conversation_id") or ""),
|
|
1569
|
+
),
|
|
1570
|
+
)
|
|
1571
|
+
return {
|
|
1572
|
+
"conversation_id": normalized_passive_conversation_id,
|
|
1573
|
+
"quest_id": str(selected.get("quest_id") or "").strip() or None,
|
|
1574
|
+
"updated_at": str(selected.get("updated_at") or "").strip() or None,
|
|
1575
|
+
"profile_id": None,
|
|
1576
|
+
"profile_label": None,
|
|
1577
|
+
"is_passive": True,
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
@staticmethod
|
|
1581
|
+
def _lingzhu_recent_conversations(state: dict[str, object]) -> list[dict[str, object]]:
|
|
1582
|
+
items = state.get("recent_conversations")
|
|
1583
|
+
if not isinstance(items, list):
|
|
1584
|
+
return []
|
|
1585
|
+
return [dict(item) for item in items if isinstance(item, dict)]
|
|
1586
|
+
|
|
1587
|
+
@staticmethod
|
|
1588
|
+
def _lingzhu_known_targets(state: dict[str, object]) -> list[dict[str, object]]:
|
|
1589
|
+
items = state.get("known_targets")
|
|
1590
|
+
if not isinstance(items, list):
|
|
1591
|
+
return []
|
|
1592
|
+
return [dict(item) for item in items if isinstance(item, dict)]
|
|
1593
|
+
|
|
1272
1594
|
def _lingzhu_snapshot_details(self, config: dict) -> dict:
|
|
1273
1595
|
auth_ak = self._secret(config, "auth_ak", "auth_ak_env")
|
|
1274
1596
|
return {
|
|
@@ -1395,8 +1717,18 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1395
1717
|
if connector_name in PROFILEABLE_CONNECTOR_NAMES:
|
|
1396
1718
|
normalized[connector_name] = normalize_connector_config(connector_name, {**base, **sanitized_payload})
|
|
1397
1719
|
continue
|
|
1720
|
+
elif connector_name == "weixin":
|
|
1721
|
+
sanitized_payload["transport"] = "ilink_long_poll"
|
|
1722
|
+
merged = {**base, **sanitized_payload}
|
|
1723
|
+
merged["enabled"] = self._weixin_auto_enabled(merged)
|
|
1724
|
+
normalized[connector_name] = merged
|
|
1725
|
+
continue
|
|
1398
1726
|
elif connector_name == "lingzhu":
|
|
1399
1727
|
sanitized_payload["transport"] = "openclaw_sse"
|
|
1728
|
+
merged = {**base, **sanitized_payload}
|
|
1729
|
+
merged["enabled"] = self._lingzhu_auto_enabled(merged)
|
|
1730
|
+
normalized[connector_name] = merged
|
|
1731
|
+
continue
|
|
1400
1732
|
elif "transport" not in sanitized_payload:
|
|
1401
1733
|
inferred_transport = infer_connector_transport(connector_name, {**base, **sanitized_payload})
|
|
1402
1734
|
if inferred_transport:
|
|
@@ -1481,6 +1813,45 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1481
1813
|
return False
|
|
1482
1814
|
return bool(value)
|
|
1483
1815
|
|
|
1816
|
+
@staticmethod
|
|
1817
|
+
def _connector_has_secret(payload: dict[str, object], direct_key: str, env_key: str) -> bool:
|
|
1818
|
+
return bool(str(payload.get(direct_key) or "").strip() or str(payload.get(env_key) or "").strip())
|
|
1819
|
+
|
|
1820
|
+
def _weixin_auto_enabled(self, payload: dict[str, object]) -> bool:
|
|
1821
|
+
return self._connector_has_secret(payload, "bot_token", "bot_token_env") and bool(
|
|
1822
|
+
str(payload.get("account_id") or "").strip()
|
|
1823
|
+
)
|
|
1824
|
+
|
|
1825
|
+
def _lingzhu_auto_enabled(self, payload: dict[str, object]) -> bool:
|
|
1826
|
+
auth_ready = self._connector_has_secret(payload, "auth_ak", "auth_ak_env")
|
|
1827
|
+
public_base_url = str(payload.get("public_base_url") or "").strip()
|
|
1828
|
+
if not auth_ready or not public_base_url:
|
|
1829
|
+
return False
|
|
1830
|
+
normalized_public_base_url = lingzhu_public_base_url(payload)
|
|
1831
|
+
if normalized_public_base_url is None:
|
|
1832
|
+
return False
|
|
1833
|
+
return public_base_url_looks_public(normalized_public_base_url)
|
|
1834
|
+
|
|
1835
|
+
def _connector_has_user_config(self, name: str, config: dict[str, object]) -> bool:
|
|
1836
|
+
if name == "qq":
|
|
1837
|
+
return bool(list_qq_profiles(config))
|
|
1838
|
+
if name in PROFILEABLE_CONNECTOR_NAMES:
|
|
1839
|
+
return bool(list_connector_profiles(name, config))
|
|
1840
|
+
if name == "weixin":
|
|
1841
|
+
return any(
|
|
1842
|
+
str(config.get(key) or "").strip()
|
|
1843
|
+
for key in ("bot_token", "bot_token_env", "account_id", "login_user_id", "route_tag")
|
|
1844
|
+
)
|
|
1845
|
+
if name == "lingzhu":
|
|
1846
|
+
return any(
|
|
1847
|
+
str(config.get(key) or "").strip()
|
|
1848
|
+
for key in ("auth_ak", "auth_ak_env", "public_base_url")
|
|
1849
|
+
)
|
|
1850
|
+
return False
|
|
1851
|
+
|
|
1852
|
+
def _should_validate_connector(self, name: str, config: dict[str, object]) -> bool:
|
|
1853
|
+
return bool(config.get("enabled", False)) or self._connector_has_user_config(name, config)
|
|
1854
|
+
|
|
1484
1855
|
def _normalize_plugins_payload(self, payload: dict) -> dict:
|
|
1485
1856
|
normalized = deepcopy(payload)
|
|
1486
1857
|
if "load_paths" not in normalized and isinstance(normalized.get("search_paths"), list):
|