@researai/deepscientist 1.5.9 → 1.5.12
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 +112 -99
- 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 +519 -63
- package/docs/assets/branding/projects.png +0 -0
- package/docs/en/00_QUICK_START.md +338 -68
- package/docs/en/01_SETTINGS_REFERENCE.md +14 -0
- package/docs/en/02_START_RESEARCH_GUIDE.md +180 -4
- package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +62 -179
- package/docs/en/09_DOCTOR.md +66 -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 +446 -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/15_CODEX_PROVIDER_SETUP.md +284 -0
- package/docs/en/99_ACKNOWLEDGEMENTS.md +4 -1
- package/docs/en/README.md +83 -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 +345 -72
- package/docs/zh/01_SETTINGS_REFERENCE.md +14 -0
- package/docs/zh/02_START_RESEARCH_GUIDE.md +181 -3
- package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +62 -193
- package/docs/zh/09_DOCTOR.md +68 -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 +442 -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/15_CODEX_PROVIDER_SETUP.md +285 -0
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +4 -1
- package/docs/zh/README.md +129 -0
- package/install.sh +0 -34
- package/package.json +2 -2
- package/pyproject.toml +1 -1
- 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/service.py +574 -108
- package/src/deepscientist/arxiv_library.py +275 -0
- package/src/deepscientist/bash_exec/monitor.py +7 -5
- package/src/deepscientist/bash_exec/service.py +93 -21
- 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/local.py +3 -3
- package/src/deepscientist/channels/qq.py +8 -8
- package/src/deepscientist/channels/qq_gateway.py +1 -1
- package/src/deepscientist/channels/relay.py +14 -8
- package/src/deepscientist/channels/weixin.py +59 -0
- package/src/deepscientist/channels/weixin_ilink.py +388 -0
- package/src/deepscientist/config/models.py +23 -2
- package/src/deepscientist/config/service.py +539 -67
- 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 +165 -5
- package/src/deepscientist/daemon/api/router.py +13 -1
- package/src/deepscientist/daemon/app.py +1444 -67
- package/src/deepscientist/doctor.py +4 -5
- package/src/deepscientist/gitops/diff.py +120 -29
- package/src/deepscientist/lingzhu_support.py +1 -182
- package/src/deepscientist/mcp/server.py +135 -7
- package/src/deepscientist/prompts/builder.py +128 -11
- package/src/deepscientist/qq_profiles.py +1 -196
- package/src/deepscientist/quest/node_traces.py +23 -0
- package/src/deepscientist/quest/service.py +359 -74
- package/src/deepscientist/quest/stage_views.py +71 -5
- package/src/deepscientist/runners/codex.py +170 -19
- package/src/deepscientist/runners/runtime_overrides.py +6 -0
- package/src/deepscientist/shared.py +33 -14
- package/src/deepscientist/weixin_support.py +1 -0
- package/src/prompts/connectors/lingzhu.md +3 -1
- package/src/prompts/connectors/qq.md +2 -1
- package/src/prompts/connectors/weixin.md +231 -0
- package/src/prompts/contracts/shared_interaction.md +4 -1
- package/src/prompts/system.md +61 -9
- package/src/skills/analysis-campaign/SKILL.md +46 -6
- package/src/skills/analysis-campaign/references/campaign-plan-template.md +21 -8
- package/src/skills/baseline/SKILL.md +1 -1
- package/src/skills/decision/SKILL.md +1 -1
- package/src/skills/experiment/SKILL.md +1 -1
- package/src/skills/finalize/SKILL.md +1 -1
- package/src/skills/idea/SKILL.md +1 -1
- package/src/skills/intake-audit/SKILL.md +1 -1
- package/src/skills/rebuttal/SKILL.md +74 -1
- package/src/skills/rebuttal/references/response-letter-template.md +55 -11
- package/src/skills/review/SKILL.md +118 -1
- package/src/skills/review/references/experiment-todo-template.md +23 -0
- package/src/skills/review/references/review-report-template.md +16 -0
- package/src/skills/review/references/revision-log-template.md +4 -0
- package/src/skills/scout/SKILL.md +1 -1
- package/src/skills/write/SKILL.md +168 -7
- package/src/skills/write/references/paper-experiment-matrix-template.md +131 -0
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-BKZ103sn.js → AiManusChatView-CnJcXynW.js} +156 -48
- package/src/ui/dist/assets/{AnalysisPlugin-mTTzGAlK.js → AnalysisPlugin-DeyzPEhV.js} +1 -1
- package/src/ui/dist/assets/{CliPlugin-BH58n3GY.js → CliPlugin-CB1YODQn.js} +164 -9
- package/src/ui/dist/assets/{CodeEditorPlugin-BKGRUH7e.js → CodeEditorPlugin-B-xicq1e.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-BMADwFWJ.js → CodeViewerPlugin-DT54ysXa.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-ZOnTIHLN.js → DocViewerPlugin-DQtKT-VD.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-CQ7h1Djm.js → GitDiffViewerPlugin-hqHbCfnv.js} +20 -21
- package/src/ui/dist/assets/{ImageViewerPlugin-GVS5MsnC.js → ImageViewerPlugin-OcVo33jV.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-BZNv1JML.js → LabCopilotPanel-DdGwhEUV.js} +11 -11
- package/src/ui/dist/assets/{LabPlugin-TWcJsdQA.js → LabPlugin-Ciz1gDaX.js} +2 -1
- package/src/ui/dist/assets/{LatexPlugin-DIjHiR2x.js → LatexPlugin-BhmjNQRC.js} +37 -11
- package/src/ui/dist/assets/{MarkdownViewerPlugin-D3ooGAH0.js → MarkdownViewerPlugin-BzdVH9Bx.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-DfVfE9hN.js → MarketplacePlugin-DmyHspXt.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-DDl0_Mc0.js → NotebookEditor-BMXKrDRk.js} +1 -1
- package/src/ui/dist/assets/{NotebookEditor-s8JhzuX1.js → NotebookEditor-BTVYRGkm.js} +12 -12
- package/src/ui/dist/assets/{PdfLoader-C2Sf6SJM.js → PdfLoader-CvcjJHXv.js} +14 -7
- package/src/ui/dist/assets/{PdfMarkdownPlugin-CXFLoIsa.js → PdfMarkdownPlugin-DW2ej8Vk.js} +73 -6
- package/src/ui/dist/assets/{PdfViewerPlugin-BYTmz2fK.js → PdfViewerPlugin-CmlDxbhU.js} +103 -34
- package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +3627 -0
- package/src/ui/dist/assets/{SearchPlugin-CjWBI1O9.js → SearchPlugin-DAjQZPSv.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-DdOBU3-S.js → TextViewerPlugin-C-nVAZb_.js} +5 -4
- package/src/ui/dist/assets/{VNCViewer-B8HGgLwQ.js → VNCViewer-D7-dIYon.js} +10 -10
- package/src/ui/dist/assets/bot-C_G4WtNI.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-BWAY76JP.js → code-Cd7WfiWq.js} +1 -1
- package/src/ui/dist/assets/{file-content-C1NwU5oQ.js → file-content-B57zsL9y.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-CywslwB9.js → file-diff-panel-DVoheLFq.js} +1 -1
- package/src/ui/dist/assets/{file-socket-B4kzuOBQ.js → file-socket-B5kXFxZP.js} +1 -1
- package/src/ui/dist/assets/{image-D-NZM-6P.js → image-LLOjkMHF.js} +1 -1
- package/src/ui/dist/assets/{index-DGIYDuTv.css → index-BQG-1s2o.css} +40 -13
- package/src/ui/dist/assets/{index-DHZJ_0TI.js → index-C3r2iGrp.js} +12 -12
- package/src/ui/dist/assets/{index-7Chr1g9c.js → index-CLQauncb.js} +15050 -9561
- package/src/ui/dist/assets/index-Dxa2eYMY.js +25 -0
- package/src/ui/dist/assets/{index-BdM1Gqfr.js → index-hOUOWbW2.js} +2 -2
- package/src/ui/dist/assets/{monaco-Cb2uKKe6.js → monaco-BGGAEii3.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-DSw_D3RV.js → pdf-effect-queue-DlEr1_y5.js} +16 -1
- package/src/ui/dist/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
- package/src/ui/dist/assets/{popover-Bg72DGgT.js → popover-CWJbJuYY.js} +1 -1
- package/src/ui/dist/assets/{project-sync-Ce_0BglY.js → project-sync-CRJiucYO.js} +18 -77
- package/src/ui/dist/assets/select-CoHB7pvH.js +1690 -0
- package/src/ui/dist/assets/{sigma-DPaACDrh.js → sigma-D5aJWR8J.js} +1 -1
- package/src/ui/dist/assets/{index-CDxNdQdz.js → square-check-big-DUK_mnkS.js} +2 -13
- package/src/ui/dist/assets/{trash-BvTgE5__.js → trash-ChU3SEE3.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-CgPeMOwP.js → useCliAccess-BrJBV3tY.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-xPhz7P5B.js → useFileDiffOverlay-C2OQaVWc.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-C3Un3YQr.js → wrap-text-C7Qqh-om.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-BgxLa0Ri.js → zoom-out-rtX0FKya.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-C_wWw4AP.js +0 -8149
- package/src/ui/dist/assets/PdfViewerPlugin-BJXtIwj_.css +0 -260
- package/src/ui/dist/assets/Stepper-B0Dd8CxK.js +0 -158
- package/src/ui/dist/assets/bibtex-CKaefIN2.js +0 -189
- package/src/ui/dist/assets/file-utils-H2fjA46S.js +0 -109
- package/src/ui/dist/assets/message-square-BzjLiXir.js +0 -16
- package/src/ui/dist/assets/pdfjs-DU1YE8WO.js +0 -3
- package/src/ui/dist/assets/tooltip-C_mA6R0w.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,58 @@ 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
|
+
previous = self.load_named_normalized(name) if name in CONFIG_NAMES and self.path_for(name).exists() else default_payload(name, self.home)
|
|
176
|
+
result = self.save_named_text(name, self.render_named_payload(name, prepared))
|
|
177
|
+
if result.get("ok") and name == "runners":
|
|
178
|
+
self._invalidate_codex_bootstrap_state_if_runner_changed(previous, self.load_named_normalized("runners"))
|
|
179
|
+
return result
|
|
180
|
+
|
|
181
|
+
def _prepare_payload_for_save(self, name: str, payload: dict) -> dict:
|
|
182
|
+
prepared = deepcopy(payload) if isinstance(payload, dict) else {}
|
|
183
|
+
if name != "connectors":
|
|
184
|
+
return prepared
|
|
185
|
+
lingzhu = prepared.get("lingzhu")
|
|
186
|
+
if not isinstance(lingzhu, dict):
|
|
187
|
+
return prepared
|
|
188
|
+
enabled = self._coerce_bool(lingzhu.get("enabled"), default=False)
|
|
189
|
+
raw_public_base_url = str(lingzhu.get("public_base_url") or "").strip()
|
|
190
|
+
direct_auth_ak = str(lingzhu.get("auth_ak") or "").strip()
|
|
191
|
+
if lingzhu_auth_ak_needs_rotation(direct_auth_ak):
|
|
192
|
+
lingzhu["auth_ak"] = generate_lingzhu_auth_ak()
|
|
193
|
+
elif (enabled or raw_public_base_url) and not self._has_secret(lingzhu, "auth_ak", "auth_ak_env"):
|
|
194
|
+
lingzhu["auth_ak"] = generate_lingzhu_auth_ak()
|
|
195
|
+
prepared["lingzhu"] = lingzhu
|
|
196
|
+
return prepared
|
|
197
|
+
|
|
198
|
+
def _invalidate_codex_bootstrap_state_if_runner_changed(self, previous: dict, current: dict) -> None:
|
|
199
|
+
previous_codex = previous.get("codex") if isinstance(previous.get("codex"), dict) else {}
|
|
200
|
+
current_codex = current.get("codex") if isinstance(current.get("codex"), dict) else {}
|
|
201
|
+
tracked_keys = (
|
|
202
|
+
"binary",
|
|
203
|
+
"config_dir",
|
|
204
|
+
"profile",
|
|
205
|
+
"model",
|
|
206
|
+
"model_reasoning_effort",
|
|
207
|
+
"approval_policy",
|
|
208
|
+
"sandbox_mode",
|
|
209
|
+
"env",
|
|
210
|
+
)
|
|
211
|
+
if all(previous_codex.get(key) == current_codex.get(key) for key in tracked_keys):
|
|
212
|
+
return
|
|
213
|
+
config = self.load_named_normalized("config")
|
|
214
|
+
bootstrap = config.get("bootstrap") if isinstance(config.get("bootstrap"), dict) else {}
|
|
215
|
+
bootstrap["codex_ready"] = False
|
|
216
|
+
bootstrap["codex_last_checked_at"] = utc_now()
|
|
217
|
+
bootstrap["codex_last_result"] = {
|
|
218
|
+
"ok": False,
|
|
219
|
+
"summary": "Codex runner configuration changed. A new startup probe is required.",
|
|
220
|
+
"warnings": [],
|
|
221
|
+
"errors": [],
|
|
222
|
+
"guidance": [],
|
|
223
|
+
}
|
|
224
|
+
config["bootstrap"] = bootstrap
|
|
225
|
+
self.save_named_text("config", self.render_named_payload("config", config))
|
|
167
226
|
|
|
168
227
|
def bind_qq_main_chat(self, *, profile_id: str | None = None, chat_id: str) -> dict:
|
|
169
228
|
normalized_chat_id = str(chat_id or "").strip()
|
|
@@ -283,7 +342,7 @@ This page edits `~/DeepScientist/config/connectors.yaml` directly.
|
|
|
283
342
|
|
|
284
343
|
## What this page is for
|
|
285
344
|
|
|
286
|
-
- connect Telegram, Discord, Slack, Feishu, WhatsApp, or QQ
|
|
345
|
+
- connect Weixin, Telegram, Discord, Slack, Feishu, WhatsApp, or QQ
|
|
287
346
|
- choose one preferred connector for proactive artifact updates
|
|
288
347
|
- decide whether artifact updates fan out or stay focused
|
|
289
348
|
- use the built-in direct runtime for each connector
|
|
@@ -291,7 +350,7 @@ This page edits `~/DeepScientist/config/connectors.yaml` directly.
|
|
|
291
350
|
|
|
292
351
|
## Recommended order
|
|
293
352
|
|
|
294
|
-
1.
|
|
353
|
+
1. configure one connector first
|
|
295
354
|
2. fill the required token or secret fields
|
|
296
355
|
3. click **Validate**
|
|
297
356
|
4. click **Test**
|
|
@@ -299,6 +358,7 @@ This page edits `~/DeepScientist/config/connectors.yaml` directly.
|
|
|
299
358
|
|
|
300
359
|
## Preferred transports
|
|
301
360
|
|
|
361
|
+
- Weixin: `ilink_long_poll`
|
|
302
362
|
- Telegram: `polling`
|
|
303
363
|
- Slack: `socket_mode`
|
|
304
364
|
- Discord: `gateway`
|
|
@@ -321,6 +381,13 @@ This page edits `~/DeepScientist/config/connectors.yaml` directly.
|
|
|
321
381
|
- prefer `transport: socket_mode`
|
|
322
382
|
- readiness test uses `auth.test`
|
|
323
383
|
|
|
384
|
+
### Weixin
|
|
385
|
+
|
|
386
|
+
- scan the QR code first so DeepScientist can persist `bot_token` and `account_id`
|
|
387
|
+
- keep `transport: ilink_long_poll`
|
|
388
|
+
- every reply depends on the latest inbound `context_token`
|
|
389
|
+
- media send uses the built-in AES + CDN upload path for image, video, and file attachments
|
|
390
|
+
|
|
324
391
|
### Discord
|
|
325
392
|
|
|
326
393
|
- set `bot_token`
|
|
@@ -354,12 +421,12 @@ This page edits `~/DeepScientist/config/connectors.yaml` directly.
|
|
|
354
421
|
|
|
355
422
|
### Lingzhu
|
|
356
423
|
|
|
357
|
-
- Lingzhu is
|
|
424
|
+
- Lingzhu is hosted directly by DeepScientist on `/metis/agent/api`
|
|
358
425
|
- keep `transport: openclaw_sse`
|
|
359
|
-
-
|
|
360
|
-
-
|
|
361
|
-
-
|
|
362
|
-
-
|
|
426
|
+
- use the same public DeepScientist origin and port that the browser is already serving on
|
|
427
|
+
- save once so DeepScientist can persist `auth_ak`; Rokid must use the same Bearer token
|
|
428
|
+
- `public_base_url` must be a public IP or public domain; loopback and private addresses are invalid for Rokid
|
|
429
|
+
- new Lingzhu tasks must start with `我现在的任务是`; other requests are treated as reconnect or progress polling
|
|
363
430
|
|
|
364
431
|
## Safety
|
|
365
432
|
|
|
@@ -417,6 +484,8 @@ This page edits `{home_text}/config/runners.yaml`.
|
|
|
417
484
|
- keep `codex.enabled: true`
|
|
418
485
|
- keep `claude.enabled: false`
|
|
419
486
|
- `claude` remains TODO / reserved in the current open-source release and is not runnable yet
|
|
487
|
+
- set `codex.profile` only when your Codex CLI uses a named provider profile such as `m27`
|
|
488
|
+
- when you launch DeepScientist ad hoc with a provider profile, you can also use `ds --codex-profile <name>`
|
|
420
489
|
- keep `codex.model_reasoning_effort: xhigh` unless you explicitly want a lighter default
|
|
421
490
|
- keep `codex.retry_on_failure: true` so transient Codex failures can resume automatically
|
|
422
491
|
- keep retry timing near `10s / 6x / 1800s max` so Codex backs off exponentially and the last retry waits about 30 minutes
|
|
@@ -428,7 +497,7 @@ The **Test** button checks:
|
|
|
428
497
|
|
|
429
498
|
- whether the configured runner binaries are on PATH
|
|
430
499
|
- whether disabled runners are intentionally skipped
|
|
431
|
-
- for Codex, it also runs a real hello probe so login and first-run setup
|
|
500
|
+
- for Codex, it also runs a real hello probe so login problems, profile misconfiguration, and first-run setup issues surface before quest execution
|
|
432
501
|
- it does not simulate the full failure/retry loop, so use quest runtime logs when debugging recovery behavior
|
|
433
502
|
"""
|
|
434
503
|
if name == "plugins":
|
|
@@ -680,9 +749,10 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
680
749
|
continue
|
|
681
750
|
config = raw_config
|
|
682
751
|
enabled = bool(config.get("enabled", False))
|
|
683
|
-
if not
|
|
752
|
+
if not self._should_validate_connector(str(name), config):
|
|
684
753
|
continue
|
|
685
|
-
|
|
754
|
+
if enabled:
|
|
755
|
+
enabled_connectors.append(str(name))
|
|
686
756
|
|
|
687
757
|
if name == "qq":
|
|
688
758
|
profiles = list_qq_profiles(config)
|
|
@@ -746,6 +816,15 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
746
816
|
errors.append("slack: `transport: socket_mode` requires `bot_token` or `bot_token_env`.")
|
|
747
817
|
if not has_app_token:
|
|
748
818
|
errors.append("slack: `transport: socket_mode` requires `app_token` or `app_token_env`.")
|
|
819
|
+
elif name == "weixin":
|
|
820
|
+
if transport != "ilink_long_poll":
|
|
821
|
+
errors.append("weixin: `transport` must stay `ilink_long_poll`.")
|
|
822
|
+
if not self._has_secret(config, "bot_token", "bot_token_env"):
|
|
823
|
+
errors.append("weixin: requires `bot_token` or `bot_token_env` after QR login.")
|
|
824
|
+
if not str(config.get("account_id") or "").strip():
|
|
825
|
+
errors.append("weixin: requires `account_id` after QR login.")
|
|
826
|
+
if not str(config.get("login_user_id") or "").strip():
|
|
827
|
+
warnings.append("weixin: `login_user_id` is empty. Save the scanner user id after QR login for easier diagnostics.")
|
|
749
828
|
elif name == "feishu":
|
|
750
829
|
has_app_id = bool(str(config.get("app_id") or "").strip())
|
|
751
830
|
has_app_secret = self._has_secret(config, "app_secret", "app_secret_env")
|
|
@@ -767,6 +846,8 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
767
846
|
warnings.append("lingzhu: `local_host` is empty; DeepScientist will fall back to `127.0.0.1`.")
|
|
768
847
|
if not self._has_secret(config, "auth_ak", "auth_ak_env"):
|
|
769
848
|
errors.append("lingzhu: requires `auth_ak` for Bearer authentication.")
|
|
849
|
+
elif lingzhu_auth_ak_needs_rotation(self._secret(config, "auth_ak", "auth_ak_env")):
|
|
850
|
+
errors.append("lingzhu: `auth_ak` is still using the bundled example token; generate a new random AK before binding Rokid.")
|
|
770
851
|
raw_gateway_port = str(config.get("gateway_port") or "").strip()
|
|
771
852
|
normalized_port = lingzhu_gateway_port(config)
|
|
772
853
|
if raw_gateway_port and str(normalized_port) != raw_gateway_port:
|
|
@@ -775,6 +856,10 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
775
856
|
public_base_url = lingzhu_public_base_url(config)
|
|
776
857
|
if raw_public_base_url and public_base_url is None:
|
|
777
858
|
errors.append("lingzhu: `public_base_url` must be a valid `http://` or `https://` URL when set.")
|
|
859
|
+
elif raw_public_base_url and not public_base_url_looks_public(raw_public_base_url):
|
|
860
|
+
errors.append(
|
|
861
|
+
"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."
|
|
862
|
+
)
|
|
778
863
|
raw_visible_progress_heartbeat_sec = str(
|
|
779
864
|
config.get("visible_progress_heartbeat_sec") or ""
|
|
780
865
|
).strip()
|
|
@@ -817,14 +902,14 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
817
902
|
if not isinstance(raw_config, dict):
|
|
818
903
|
continue
|
|
819
904
|
config = raw_config
|
|
820
|
-
if not
|
|
905
|
+
if not self._should_validate_connector(str(name), config):
|
|
821
906
|
continue
|
|
822
907
|
target = (delivery_targets or {}).get(name)
|
|
823
908
|
items.append(self._test_single_connector(name, config, live=live, delivery_target=target if isinstance(target, dict) else None))
|
|
824
909
|
return {
|
|
825
910
|
"ok": all(item["ok"] for item in items) if items else True,
|
|
826
911
|
"name": "connectors",
|
|
827
|
-
"summary": "Connector test completed." if items else "No
|
|
912
|
+
"summary": "Connector test completed." if items else "No configured connectors to test.",
|
|
828
913
|
"warnings": [],
|
|
829
914
|
"errors": [],
|
|
830
915
|
"items": items,
|
|
@@ -963,6 +1048,25 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
963
1048
|
details["identity"] = profile_results[0].get("app_id")
|
|
964
1049
|
details["gateway_url"] = profile_results[0].get("gateway_url")
|
|
965
1050
|
details["token_expires_in"] = profile_results[0].get("token_expires_in")
|
|
1051
|
+
elif name == "weixin":
|
|
1052
|
+
details.update(
|
|
1053
|
+
{
|
|
1054
|
+
"base_url": normalize_weixin_base_url(config.get("base_url")),
|
|
1055
|
+
"cdn_base_url": normalize_weixin_cdn_base_url(config.get("cdn_base_url")),
|
|
1056
|
+
"account_id": str(config.get("account_id") or "").strip() or None,
|
|
1057
|
+
"login_user_id": str(config.get("login_user_id") or "").strip() or None,
|
|
1058
|
+
}
|
|
1059
|
+
)
|
|
1060
|
+
if transport != "ilink_long_poll":
|
|
1061
|
+
errors.append("Weixin transport must stay `ilink_long_poll`.")
|
|
1062
|
+
if not self._has_secret(config, "bot_token", "bot_token_env"):
|
|
1063
|
+
errors.append("Weixin requires `bot_token` after QR login.")
|
|
1064
|
+
if not str(config.get("account_id") or "").strip():
|
|
1065
|
+
errors.append("Weixin requires `account_id` after QR login.")
|
|
1066
|
+
if live and not errors:
|
|
1067
|
+
warnings.append(
|
|
1068
|
+
"Weixin readiness is credential-based. Send one inbound Weixin message to populate `context_token` before testing outbound delivery."
|
|
1069
|
+
)
|
|
966
1070
|
elif name == "lingzhu":
|
|
967
1071
|
details.update(self._lingzhu_snapshot_details(config))
|
|
968
1072
|
auth_ak = self._secret(config, "auth_ak", "auth_ak_env")
|
|
@@ -1070,11 +1174,157 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1070
1174
|
"If you received it, the connector binding and outbound delivery path are working."
|
|
1071
1175
|
)
|
|
1072
1176
|
|
|
1177
|
+
@staticmethod
|
|
1178
|
+
def _codex_should_inherit_model(model: object) -> bool:
|
|
1179
|
+
normalized = str(model or "").strip().lower()
|
|
1180
|
+
return normalized in {"", "inherit", "default", "codex-default"}
|
|
1181
|
+
|
|
1182
|
+
@staticmethod
|
|
1183
|
+
def _codex_requested_model(config: dict) -> str:
|
|
1184
|
+
raw_model = config.get("model")
|
|
1185
|
+
if raw_model is None:
|
|
1186
|
+
return "gpt-5.4"
|
|
1187
|
+
return str(raw_model).strip()
|
|
1188
|
+
|
|
1189
|
+
@staticmethod
|
|
1190
|
+
def _codex_profile_name(config: dict) -> str:
|
|
1191
|
+
raw_profile = config.get("profile")
|
|
1192
|
+
if raw_profile is None:
|
|
1193
|
+
return ""
|
|
1194
|
+
return str(raw_profile).strip()
|
|
1195
|
+
|
|
1196
|
+
@staticmethod
|
|
1197
|
+
def _codex_runner_env(config: dict) -> dict[str, str]:
|
|
1198
|
+
raw_env = config.get("env")
|
|
1199
|
+
if not isinstance(raw_env, dict):
|
|
1200
|
+
return {}
|
|
1201
|
+
resolved: dict[str, str] = {}
|
|
1202
|
+
for key, value in raw_env.items():
|
|
1203
|
+
env_key = str(key or "").strip()
|
|
1204
|
+
if not env_key or value is None:
|
|
1205
|
+
continue
|
|
1206
|
+
resolved[env_key] = str(value)
|
|
1207
|
+
return resolved
|
|
1208
|
+
|
|
1209
|
+
def _codex_missing_binary_guidance(self, config: dict) -> list[str]:
|
|
1210
|
+
profile = self._codex_profile_name(config)
|
|
1211
|
+
guidance = [
|
|
1212
|
+
"Run `npm install -g @researai/deepscientist` again so the bundled Codex dependency is installed.",
|
|
1213
|
+
"If `codex` is still missing, install it explicitly with `npm install -g @openai/codex`.",
|
|
1214
|
+
]
|
|
1215
|
+
if profile:
|
|
1216
|
+
guidance.extend(
|
|
1217
|
+
[
|
|
1218
|
+
f"Then verify `codex --profile {profile}` works from a terminal before starting DeepScientist.",
|
|
1219
|
+
"If that profile uses a custom provider, make sure its API key and Base URL are configured in Codex first.",
|
|
1220
|
+
]
|
|
1221
|
+
)
|
|
1222
|
+
else:
|
|
1223
|
+
guidance.append("Run `codex --login` (or `codex`) once and finish authentication before starting DeepScientist.")
|
|
1224
|
+
guidance.append("If you use a custom Codex path, set `runners.codex.binary` to that absolute executable path.")
|
|
1225
|
+
return guidance
|
|
1226
|
+
|
|
1227
|
+
def _codex_probe_failure_guidance(self, config: dict) -> tuple[list[str], list[str]]:
|
|
1228
|
+
profile = self._codex_profile_name(config)
|
|
1229
|
+
if profile:
|
|
1230
|
+
return (
|
|
1231
|
+
[
|
|
1232
|
+
f"Codex profile `{profile}` did not complete the startup hello probe successfully.",
|
|
1233
|
+
],
|
|
1234
|
+
[
|
|
1235
|
+
f"Run `codex --profile {profile}` in a terminal and confirm that profile can start normally.",
|
|
1236
|
+
"If the profile uses a custom provider, make sure its API key, Base URL, and model configuration are available to Codex.",
|
|
1237
|
+
"If the provider expects the model from the Codex profile itself, set `model: inherit` in `~/DeepScientist/config/runners.yaml`.",
|
|
1238
|
+
"Then run `ds doctor` and start DeepScientist again.",
|
|
1239
|
+
],
|
|
1240
|
+
)
|
|
1241
|
+
return (
|
|
1242
|
+
[
|
|
1243
|
+
"Run `codex --login` (or `codex`) once and complete login before starting DeepScientist.",
|
|
1244
|
+
],
|
|
1245
|
+
[
|
|
1246
|
+
"Run `codex --login` (or `codex`) in a terminal and complete login or first-run setup.",
|
|
1247
|
+
"If `codex` is missing, install it explicitly with `npm install -g @openai/codex`.",
|
|
1248
|
+
"If the configured model is not available to your Codex account, update `~/DeepScientist/config/runners.yaml` and try again.",
|
|
1249
|
+
"Then run `ds doctor` and start DeepScientist again.",
|
|
1250
|
+
],
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
@staticmethod
|
|
1254
|
+
def _codex_model_unavailable(stdout_text: str, stderr_text: str) -> bool:
|
|
1255
|
+
haystack = f"{stdout_text}\n{stderr_text}".lower()
|
|
1256
|
+
markers = [
|
|
1257
|
+
"unknown model",
|
|
1258
|
+
"invalid model",
|
|
1259
|
+
"model not found",
|
|
1260
|
+
"unsupported model",
|
|
1261
|
+
"model is not available",
|
|
1262
|
+
"not authorized to use model",
|
|
1263
|
+
"you do not have access",
|
|
1264
|
+
"access to model",
|
|
1265
|
+
"model access",
|
|
1266
|
+
"unrecognized model",
|
|
1267
|
+
]
|
|
1268
|
+
return any(marker in haystack for marker in markers)
|
|
1269
|
+
|
|
1270
|
+
def _build_codex_probe_command(
|
|
1271
|
+
self,
|
|
1272
|
+
*,
|
|
1273
|
+
resolved_binary: str,
|
|
1274
|
+
profile: str,
|
|
1275
|
+
requested_model: str,
|
|
1276
|
+
approval_policy: str,
|
|
1277
|
+
reasoning_effort: str | None,
|
|
1278
|
+
sandbox_mode: str,
|
|
1279
|
+
) -> list[str]:
|
|
1280
|
+
command = [
|
|
1281
|
+
resolved_binary,
|
|
1282
|
+
"--search",
|
|
1283
|
+
]
|
|
1284
|
+
if profile:
|
|
1285
|
+
command.extend(["--profile", profile])
|
|
1286
|
+
command.extend(
|
|
1287
|
+
[
|
|
1288
|
+
"exec",
|
|
1289
|
+
"--json",
|
|
1290
|
+
"--cd",
|
|
1291
|
+
str(repo_root()),
|
|
1292
|
+
"--skip-git-repo-check",
|
|
1293
|
+
]
|
|
1294
|
+
)
|
|
1295
|
+
if not self._codex_should_inherit_model(requested_model):
|
|
1296
|
+
command.extend(["--model", requested_model])
|
|
1297
|
+
if approval_policy:
|
|
1298
|
+
command.extend(["-c", f'approval_policy="{approval_policy}"'])
|
|
1299
|
+
if reasoning_effort:
|
|
1300
|
+
command.extend(["-c", f'model_reasoning_effort="{reasoning_effort}"'])
|
|
1301
|
+
if sandbox_mode:
|
|
1302
|
+
command.extend(["--sandbox", sandbox_mode])
|
|
1303
|
+
command.append("-")
|
|
1304
|
+
return command
|
|
1305
|
+
|
|
1306
|
+
def _persist_codex_model_inherit(self, requested_model: object) -> None:
|
|
1307
|
+
normalized_requested_model = str(requested_model or "").strip()
|
|
1308
|
+
if not normalized_requested_model:
|
|
1309
|
+
return
|
|
1310
|
+
runners = self.load_named("runners")
|
|
1311
|
+
codex = runners.get("codex") if isinstance(runners.get("codex"), dict) else None
|
|
1312
|
+
if not isinstance(codex, dict):
|
|
1313
|
+
return
|
|
1314
|
+
current_model = str(codex.get("model") or "").strip()
|
|
1315
|
+
if current_model != normalized_requested_model:
|
|
1316
|
+
return
|
|
1317
|
+
codex["model"] = "inherit"
|
|
1318
|
+
runners["codex"] = codex
|
|
1319
|
+
self.save_named_payload("runners", runners)
|
|
1320
|
+
|
|
1073
1321
|
def _probe_codex_runner(self, config: dict) -> dict:
|
|
1074
1322
|
config = apply_codex_runtime_overrides(config)
|
|
1075
1323
|
checked_at = utc_now()
|
|
1076
1324
|
binary = str(config.get("binary") or "codex").strip() or "codex"
|
|
1077
1325
|
resolved_binary = resolve_runner_binary(binary, runner_name="codex")
|
|
1326
|
+
profile = self._codex_profile_name(config)
|
|
1327
|
+
requested_model = self._codex_requested_model(config)
|
|
1078
1328
|
raw_reasoning_effort = config.get("model_reasoning_effort")
|
|
1079
1329
|
reasoning_effort = (
|
|
1080
1330
|
str(raw_reasoning_effort).strip()
|
|
@@ -1085,10 +1335,15 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1085
1335
|
"binary": binary,
|
|
1086
1336
|
"resolved_binary": resolved_binary,
|
|
1087
1337
|
"config_dir": str(config.get("config_dir") or "~/.codex"),
|
|
1088
|
-
"
|
|
1338
|
+
"profile": profile,
|
|
1339
|
+
"model": requested_model or "inherit",
|
|
1340
|
+
"requested_model": requested_model or "inherit",
|
|
1341
|
+
"effective_model": requested_model or "inherit",
|
|
1089
1342
|
"approval_policy": str(config.get("approval_policy") or "on-request"),
|
|
1090
1343
|
"sandbox_mode": str(config.get("sandbox_mode") or "workspace-write"),
|
|
1091
1344
|
"reasoning_effort": reasoning_effort,
|
|
1345
|
+
"model_fallback_attempted": False,
|
|
1346
|
+
"model_fallback_used": False,
|
|
1092
1347
|
"checked_at": checked_at,
|
|
1093
1348
|
}
|
|
1094
1349
|
if not resolved_binary:
|
|
@@ -1101,56 +1356,50 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1101
1356
|
"DeepScientist could not resolve the bundled or configured `codex` CLI.",
|
|
1102
1357
|
],
|
|
1103
1358
|
"details": details,
|
|
1104
|
-
"guidance":
|
|
1105
|
-
"Run `npm install -g @researai/deepscientist` again so the bundled Codex dependency is installed.",
|
|
1106
|
-
"If you use a custom Codex path, set `runners.codex.binary` to that absolute executable path.",
|
|
1107
|
-
],
|
|
1359
|
+
"guidance": self._codex_missing_binary_guidance(config),
|
|
1108
1360
|
}
|
|
1109
1361
|
|
|
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
1362
|
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
1363
|
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
1364
|
|
|
1131
1365
|
env = os.environ.copy()
|
|
1366
|
+
env.update(self._codex_runner_env(config))
|
|
1132
1367
|
config_dir = str(config.get("config_dir") or "~/.codex").strip()
|
|
1133
1368
|
if config_dir:
|
|
1134
1369
|
env["CODEX_HOME"] = str(Path(config_dir).expanduser())
|
|
1135
1370
|
prompt = "Reply with exactly HELLO."
|
|
1136
1371
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
timeout=90,
|
|
1146
|
-
check=False,
|
|
1372
|
+
def run_probe_once(model_for_command: str) -> tuple[list[str], subprocess.CompletedProcess[str] | None, subprocess.TimeoutExpired | None]:
|
|
1373
|
+
command = self._build_codex_probe_command(
|
|
1374
|
+
resolved_binary=resolved_binary,
|
|
1375
|
+
profile=profile,
|
|
1376
|
+
requested_model=model_for_command,
|
|
1377
|
+
approval_policy=approval_policy,
|
|
1378
|
+
reasoning_effort=reasoning_effort,
|
|
1379
|
+
sandbox_mode=sandbox_mode,
|
|
1147
1380
|
)
|
|
1148
|
-
|
|
1381
|
+
try:
|
|
1382
|
+
result = subprocess.run(
|
|
1383
|
+
command,
|
|
1384
|
+
input=prompt,
|
|
1385
|
+
cwd=str(repo_root()),
|
|
1386
|
+
env=env,
|
|
1387
|
+
text=True,
|
|
1388
|
+
capture_output=True,
|
|
1389
|
+
timeout=90,
|
|
1390
|
+
check=False,
|
|
1391
|
+
)
|
|
1392
|
+
except subprocess.TimeoutExpired as exc:
|
|
1393
|
+
return command, None, exc
|
|
1394
|
+
return command, result, None
|
|
1395
|
+
|
|
1396
|
+
command, result, timeout_error = run_probe_once(requested_model)
|
|
1397
|
+
if timeout_error is not None:
|
|
1149
1398
|
details.update(
|
|
1150
1399
|
{
|
|
1151
1400
|
"exit_code": None,
|
|
1152
|
-
"stdout_excerpt": self._compact_probe_text(
|
|
1153
|
-
"stderr_excerpt": self._compact_probe_text(
|
|
1401
|
+
"stdout_excerpt": self._compact_probe_text(timeout_error.stdout or ""),
|
|
1402
|
+
"stderr_excerpt": self._compact_probe_text(timeout_error.stderr or ""),
|
|
1154
1403
|
"probe_command": command,
|
|
1155
1404
|
}
|
|
1156
1405
|
)
|
|
@@ -1160,19 +1409,72 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1160
1409
|
"warnings": [],
|
|
1161
1410
|
"errors": [
|
|
1162
1411
|
"Codex did not answer the startup hello probe within 90 seconds.",
|
|
1163
|
-
|
|
1412
|
+
*self._codex_probe_failure_guidance(config)[0],
|
|
1164
1413
|
],
|
|
1165
1414
|
"details": details,
|
|
1166
1415
|
"guidance": [
|
|
1167
|
-
|
|
1168
|
-
"
|
|
1416
|
+
*self._codex_probe_failure_guidance(config)[1],
|
|
1417
|
+
"If `codex` is missing on PATH, install it explicitly with `npm install -g @openai/codex`.",
|
|
1418
|
+
"Confirm the configured model is available to your Codex setup. DeepScientist currently probes Codex with the configured runner model first.",
|
|
1169
1419
|
],
|
|
1170
1420
|
}
|
|
1171
1421
|
|
|
1422
|
+
assert result is not None
|
|
1172
1423
|
stdout_text = (result.stdout or "").strip()
|
|
1173
1424
|
stderr_text = (result.stderr or "").strip()
|
|
1174
1425
|
hello_seen = "HELLO" in stdout_text.upper()
|
|
1175
1426
|
ok = result.returncode == 0 and hello_seen
|
|
1427
|
+
fallback_warning: str | None = None
|
|
1428
|
+
if (
|
|
1429
|
+
not ok
|
|
1430
|
+
and not self._codex_should_inherit_model(requested_model)
|
|
1431
|
+
and self._codex_model_unavailable(stdout_text, stderr_text)
|
|
1432
|
+
):
|
|
1433
|
+
details["model_fallback_attempted"] = True
|
|
1434
|
+
fallback_command, fallback_result, fallback_timeout = run_probe_once("inherit")
|
|
1435
|
+
details["initial_probe_command"] = command
|
|
1436
|
+
details["initial_exit_code"] = result.returncode
|
|
1437
|
+
details["initial_stdout_excerpt"] = self._compact_probe_text(stdout_text)
|
|
1438
|
+
details["initial_stderr_excerpt"] = self._compact_probe_text(stderr_text)
|
|
1439
|
+
details["fallback_probe_command"] = fallback_command
|
|
1440
|
+
if fallback_timeout is None and fallback_result is not None:
|
|
1441
|
+
fallback_stdout_text = (fallback_result.stdout or "").strip()
|
|
1442
|
+
fallback_stderr_text = (fallback_result.stderr or "").strip()
|
|
1443
|
+
fallback_hello_seen = "HELLO" in fallback_stdout_text.upper()
|
|
1444
|
+
fallback_ok = fallback_result.returncode == 0 and fallback_hello_seen
|
|
1445
|
+
details["fallback_exit_code"] = fallback_result.returncode
|
|
1446
|
+
details["fallback_stdout_excerpt"] = self._compact_probe_text(fallback_stdout_text)
|
|
1447
|
+
details["fallback_stderr_excerpt"] = self._compact_probe_text(fallback_stderr_text)
|
|
1448
|
+
if fallback_ok:
|
|
1449
|
+
details.update(
|
|
1450
|
+
{
|
|
1451
|
+
"exit_code": fallback_result.returncode,
|
|
1452
|
+
"stdout_excerpt": self._compact_probe_text(fallback_stdout_text),
|
|
1453
|
+
"stderr_excerpt": self._compact_probe_text(fallback_stderr_text),
|
|
1454
|
+
"probe_command": fallback_command,
|
|
1455
|
+
"effective_model": "inherit",
|
|
1456
|
+
"model_fallback_used": True,
|
|
1457
|
+
}
|
|
1458
|
+
)
|
|
1459
|
+
fallback_warning = (
|
|
1460
|
+
f"Configured Codex model `{requested_model}` is not available. "
|
|
1461
|
+
"DeepScientist fell back to the current Codex default model."
|
|
1462
|
+
)
|
|
1463
|
+
return {
|
|
1464
|
+
"ok": True,
|
|
1465
|
+
"summary": "Codex startup probe completed with Codex default model fallback.",
|
|
1466
|
+
"warnings": [fallback_warning],
|
|
1467
|
+
"errors": [],
|
|
1468
|
+
"details": details,
|
|
1469
|
+
"guidance": [
|
|
1470
|
+
"DeepScientist switched the Codex runner model to `inherit` so future runs keep using the current Codex default model.",
|
|
1471
|
+
],
|
|
1472
|
+
}
|
|
1473
|
+
else:
|
|
1474
|
+
details["fallback_exit_code"] = None
|
|
1475
|
+
details["fallback_stdout_excerpt"] = self._compact_probe_text((fallback_timeout.stdout if fallback_timeout else "") or "")
|
|
1476
|
+
details["fallback_stderr_excerpt"] = self._compact_probe_text((fallback_timeout.stderr if fallback_timeout else "") or "")
|
|
1477
|
+
|
|
1176
1478
|
details.update(
|
|
1177
1479
|
{
|
|
1178
1480
|
"exit_code": result.returncode,
|
|
@@ -1189,17 +1491,17 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1189
1491
|
errors.append("Codex responded, but the reply did not contain the expected `HELLO` marker.")
|
|
1190
1492
|
if stderr_text:
|
|
1191
1493
|
warnings.append("Codex returned stderr during the startup probe.")
|
|
1192
|
-
|
|
1494
|
+
if details.get("model_fallback_attempted") and not details.get("model_fallback_used"):
|
|
1495
|
+
warnings.append("DeepScientist also tried the current Codex default model, but that fallback probe did not succeed.")
|
|
1496
|
+
errors.extend(self._codex_probe_failure_guidance(config)[0])
|
|
1497
|
+
failure_guidance = self._codex_probe_failure_guidance(config)[1]
|
|
1193
1498
|
return {
|
|
1194
1499
|
"ok": ok,
|
|
1195
1500
|
"summary": "Codex startup probe completed." if ok else "Codex startup probe failed.",
|
|
1196
1501
|
"warnings": warnings,
|
|
1197
1502
|
"errors": errors,
|
|
1198
1503
|
"details": details,
|
|
1199
|
-
"guidance": [] if ok else
|
|
1200
|
-
"Run `codex` in a terminal and complete login or first-run setup.",
|
|
1201
|
-
"Then start DeepScientist again.",
|
|
1202
|
-
],
|
|
1504
|
+
"guidance": [] if ok else failure_guidance,
|
|
1203
1505
|
}
|
|
1204
1506
|
|
|
1205
1507
|
def _persist_codex_bootstrap_result(self, result: dict) -> None:
|
|
@@ -1216,7 +1518,12 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1216
1518
|
"guidance": list(result.get("guidance") or []),
|
|
1217
1519
|
"binary": details.get("binary"),
|
|
1218
1520
|
"resolved_binary": details.get("resolved_binary"),
|
|
1521
|
+
"profile": details.get("profile"),
|
|
1219
1522
|
"model": details.get("model"),
|
|
1523
|
+
"requested_model": details.get("requested_model"),
|
|
1524
|
+
"effective_model": details.get("effective_model"),
|
|
1525
|
+
"model_fallback_attempted": bool(details.get("model_fallback_attempted")),
|
|
1526
|
+
"model_fallback_used": bool(details.get("model_fallback_used")),
|
|
1220
1527
|
"approval_policy": details.get("approval_policy"),
|
|
1221
1528
|
"sandbox_mode": details.get("sandbox_mode"),
|
|
1222
1529
|
"reasoning_effort": details.get("reasoning_effort"),
|
|
@@ -1226,6 +1533,8 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1226
1533
|
}
|
|
1227
1534
|
config["bootstrap"] = bootstrap
|
|
1228
1535
|
self.save_named_payload("config", config)
|
|
1536
|
+
if bool(result.get("ok")) and bool(details.get("model_fallback_used")):
|
|
1537
|
+
self._persist_codex_model_inherit(details.get("requested_model"))
|
|
1229
1538
|
|
|
1230
1539
|
@staticmethod
|
|
1231
1540
|
def _compact_probe_text(value: str, *, limit: int = 1200) -> str:
|
|
@@ -1236,6 +1545,41 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1236
1545
|
|
|
1237
1546
|
def lingzhu_snapshot(self, config: dict | None = None) -> dict:
|
|
1238
1547
|
resolved = dict(config or self.load_named_normalized("connectors").get("lingzhu") or {})
|
|
1548
|
+
state = self._lingzhu_runtime_state()
|
|
1549
|
+
raw_bindings = self._lingzhu_bindings()
|
|
1550
|
+
last_real_conversation_id = str(state.get("last_conversation_id") or "").strip() or None
|
|
1551
|
+
has_auth_ak = bool(self._secret(resolved, "auth_ak", "auth_ak_env"))
|
|
1552
|
+
passive_conversation_id = (
|
|
1553
|
+
lingzhu_passive_conversation_id(resolved)
|
|
1554
|
+
if bool(resolved.get("enabled", False)) and has_auth_ak
|
|
1555
|
+
else None
|
|
1556
|
+
)
|
|
1557
|
+
effective_binding = (
|
|
1558
|
+
self._lingzhu_effective_binding(raw_bindings, passive_conversation_id)
|
|
1559
|
+
if passive_conversation_id
|
|
1560
|
+
else None
|
|
1561
|
+
)
|
|
1562
|
+
bindings = [effective_binding] if isinstance(effective_binding, dict) and effective_binding.get("quest_id") else []
|
|
1563
|
+
default_target = (
|
|
1564
|
+
{
|
|
1565
|
+
**(
|
|
1566
|
+
build_discovered_target(
|
|
1567
|
+
passive_conversation_id,
|
|
1568
|
+
source="passive_binding",
|
|
1569
|
+
is_default=True,
|
|
1570
|
+
label="Passive binding",
|
|
1571
|
+
quest_id=str((effective_binding or {}).get("quest_id") or "").strip() or None,
|
|
1572
|
+
updated_at=str((effective_binding or {}).get("updated_at") or "").strip() or None,
|
|
1573
|
+
)
|
|
1574
|
+
or {}
|
|
1575
|
+
),
|
|
1576
|
+
"selectable": True,
|
|
1577
|
+
"is_passive": True,
|
|
1578
|
+
}
|
|
1579
|
+
if passive_conversation_id
|
|
1580
|
+
else None
|
|
1581
|
+
)
|
|
1582
|
+
discovered_targets = [default_target] if isinstance(default_target, dict) and default_target else []
|
|
1239
1583
|
snapshot: dict[str, object] = {
|
|
1240
1584
|
"name": "lingzhu",
|
|
1241
1585
|
"display_mode": "companion_config",
|
|
@@ -1243,22 +1587,27 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1243
1587
|
"transport": "openclaw_sse",
|
|
1244
1588
|
"enabled": bool(resolved.get("enabled", False)),
|
|
1245
1589
|
"main_chat_id": None,
|
|
1246
|
-
"last_conversation_id":
|
|
1590
|
+
"last_conversation_id": passive_conversation_id,
|
|
1247
1591
|
"inbox_count": 0,
|
|
1248
1592
|
"outbox_count": 0,
|
|
1249
1593
|
"ignored_count": 0,
|
|
1250
|
-
"binding_count":
|
|
1251
|
-
"
|
|
1594
|
+
"binding_count": len(bindings),
|
|
1595
|
+
"bindings": bindings,
|
|
1596
|
+
"target_count": len(discovered_targets),
|
|
1252
1597
|
"recent_conversations": [],
|
|
1253
1598
|
"recent_events": [],
|
|
1254
|
-
"
|
|
1599
|
+
"known_targets": [],
|
|
1600
|
+
"discovered_targets": discovered_targets,
|
|
1601
|
+
"default_target": default_target,
|
|
1255
1602
|
"details": self._lingzhu_snapshot_details(resolved),
|
|
1256
1603
|
}
|
|
1604
|
+
snapshot["details"]["last_real_conversation_id"] = last_real_conversation_id
|
|
1605
|
+
snapshot["details"]["historical_target_count"] = len(self._lingzhu_recent_conversations(state))
|
|
1257
1606
|
if not snapshot["enabled"]:
|
|
1258
1607
|
snapshot["connection_state"] = "disabled"
|
|
1259
1608
|
snapshot["auth_state"] = "disabled"
|
|
1260
1609
|
return snapshot
|
|
1261
|
-
snapshot["auth_state"] = "ready" if
|
|
1610
|
+
snapshot["auth_state"] = "ready" if has_auth_ak else "missing_auth_ak"
|
|
1262
1611
|
health_probe = self._probe_lingzhu_health(resolved, timeout=1.5)
|
|
1263
1612
|
snapshot["details"]["health_probe"] = health_probe
|
|
1264
1613
|
if health_probe.get("ok", False):
|
|
@@ -1269,6 +1618,80 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1269
1618
|
snapshot["last_error"] = health_probe.get("message")
|
|
1270
1619
|
return snapshot
|
|
1271
1620
|
|
|
1621
|
+
def _lingzhu_runtime_state(self) -> dict[str, object]:
|
|
1622
|
+
payload = read_json(self.home / "logs" / "connectors" / "lingzhu" / "state.json", {})
|
|
1623
|
+
return payload if isinstance(payload, dict) else {}
|
|
1624
|
+
|
|
1625
|
+
def _lingzhu_bindings(self) -> list[dict[str, object]]:
|
|
1626
|
+
payload = read_json(self.home / "logs" / "connectors" / "lingzhu" / "bindings.json", {"bindings": {}})
|
|
1627
|
+
raw_bindings = payload.get("bindings") if isinstance(payload, dict) else {}
|
|
1628
|
+
if not isinstance(raw_bindings, dict):
|
|
1629
|
+
return []
|
|
1630
|
+
items: list[dict[str, object]] = []
|
|
1631
|
+
for conversation_id, binding in sorted(raw_bindings.items()):
|
|
1632
|
+
if not isinstance(binding, dict):
|
|
1633
|
+
continue
|
|
1634
|
+
normalized_conversation_id = str(conversation_id or "").strip()
|
|
1635
|
+
if not normalized_conversation_id:
|
|
1636
|
+
continue
|
|
1637
|
+
items.append(
|
|
1638
|
+
{
|
|
1639
|
+
"conversation_id": normalized_conversation_id,
|
|
1640
|
+
"quest_id": str(binding.get("quest_id") or "").strip() or None,
|
|
1641
|
+
"updated_at": str(binding.get("updated_at") or "").strip() or None,
|
|
1642
|
+
"profile_id": None,
|
|
1643
|
+
"profile_label": None,
|
|
1644
|
+
"is_passive": lingzhu_is_passive_conversation_id(normalized_conversation_id),
|
|
1645
|
+
}
|
|
1646
|
+
)
|
|
1647
|
+
return items
|
|
1648
|
+
|
|
1649
|
+
@staticmethod
|
|
1650
|
+
def _lingzhu_effective_binding(
|
|
1651
|
+
bindings: list[dict[str, object]],
|
|
1652
|
+
passive_conversation_id: str | None,
|
|
1653
|
+
) -> dict[str, object] | None:
|
|
1654
|
+
normalized_passive_conversation_id = str(passive_conversation_id or "").strip()
|
|
1655
|
+
if not normalized_passive_conversation_id:
|
|
1656
|
+
return None
|
|
1657
|
+
candidate_bindings = [
|
|
1658
|
+
dict(item)
|
|
1659
|
+
for item in bindings
|
|
1660
|
+
if isinstance(item, dict) and str(item.get("quest_id") or "").strip()
|
|
1661
|
+
]
|
|
1662
|
+
if not candidate_bindings:
|
|
1663
|
+
return None
|
|
1664
|
+
selected = max(
|
|
1665
|
+
candidate_bindings,
|
|
1666
|
+
key=lambda item: (
|
|
1667
|
+
str(item.get("updated_at") or ""),
|
|
1668
|
+
str(item.get("quest_id") or ""),
|
|
1669
|
+
str(item.get("conversation_id") or ""),
|
|
1670
|
+
),
|
|
1671
|
+
)
|
|
1672
|
+
return {
|
|
1673
|
+
"conversation_id": normalized_passive_conversation_id,
|
|
1674
|
+
"quest_id": str(selected.get("quest_id") or "").strip() or None,
|
|
1675
|
+
"updated_at": str(selected.get("updated_at") or "").strip() or None,
|
|
1676
|
+
"profile_id": None,
|
|
1677
|
+
"profile_label": None,
|
|
1678
|
+
"is_passive": True,
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
@staticmethod
|
|
1682
|
+
def _lingzhu_recent_conversations(state: dict[str, object]) -> list[dict[str, object]]:
|
|
1683
|
+
items = state.get("recent_conversations")
|
|
1684
|
+
if not isinstance(items, list):
|
|
1685
|
+
return []
|
|
1686
|
+
return [dict(item) for item in items if isinstance(item, dict)]
|
|
1687
|
+
|
|
1688
|
+
@staticmethod
|
|
1689
|
+
def _lingzhu_known_targets(state: dict[str, object]) -> list[dict[str, object]]:
|
|
1690
|
+
items = state.get("known_targets")
|
|
1691
|
+
if not isinstance(items, list):
|
|
1692
|
+
return []
|
|
1693
|
+
return [dict(item) for item in items if isinstance(item, dict)]
|
|
1694
|
+
|
|
1272
1695
|
def _lingzhu_snapshot_details(self, config: dict) -> dict:
|
|
1273
1696
|
auth_ak = self._secret(config, "auth_ak", "auth_ak_env")
|
|
1274
1697
|
return {
|
|
@@ -1395,8 +1818,18 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1395
1818
|
if connector_name in PROFILEABLE_CONNECTOR_NAMES:
|
|
1396
1819
|
normalized[connector_name] = normalize_connector_config(connector_name, {**base, **sanitized_payload})
|
|
1397
1820
|
continue
|
|
1821
|
+
elif connector_name == "weixin":
|
|
1822
|
+
sanitized_payload["transport"] = "ilink_long_poll"
|
|
1823
|
+
merged = {**base, **sanitized_payload}
|
|
1824
|
+
merged["enabled"] = self._weixin_auto_enabled(merged)
|
|
1825
|
+
normalized[connector_name] = merged
|
|
1826
|
+
continue
|
|
1398
1827
|
elif connector_name == "lingzhu":
|
|
1399
1828
|
sanitized_payload["transport"] = "openclaw_sse"
|
|
1829
|
+
merged = {**base, **sanitized_payload}
|
|
1830
|
+
merged["enabled"] = self._lingzhu_auto_enabled(merged)
|
|
1831
|
+
normalized[connector_name] = merged
|
|
1832
|
+
continue
|
|
1400
1833
|
elif "transport" not in sanitized_payload:
|
|
1401
1834
|
inferred_transport = infer_connector_transport(connector_name, {**base, **sanitized_payload})
|
|
1402
1835
|
if inferred_transport:
|
|
@@ -1481,6 +1914,45 @@ Use **Test** when the file exposes runtime dependencies.
|
|
|
1481
1914
|
return False
|
|
1482
1915
|
return bool(value)
|
|
1483
1916
|
|
|
1917
|
+
@staticmethod
|
|
1918
|
+
def _connector_has_secret(payload: dict[str, object], direct_key: str, env_key: str) -> bool:
|
|
1919
|
+
return bool(str(payload.get(direct_key) or "").strip() or str(payload.get(env_key) or "").strip())
|
|
1920
|
+
|
|
1921
|
+
def _weixin_auto_enabled(self, payload: dict[str, object]) -> bool:
|
|
1922
|
+
return self._connector_has_secret(payload, "bot_token", "bot_token_env") and bool(
|
|
1923
|
+
str(payload.get("account_id") or "").strip()
|
|
1924
|
+
)
|
|
1925
|
+
|
|
1926
|
+
def _lingzhu_auto_enabled(self, payload: dict[str, object]) -> bool:
|
|
1927
|
+
auth_ready = self._connector_has_secret(payload, "auth_ak", "auth_ak_env")
|
|
1928
|
+
public_base_url = str(payload.get("public_base_url") or "").strip()
|
|
1929
|
+
if not auth_ready or not public_base_url:
|
|
1930
|
+
return False
|
|
1931
|
+
normalized_public_base_url = lingzhu_public_base_url(payload)
|
|
1932
|
+
if normalized_public_base_url is None:
|
|
1933
|
+
return False
|
|
1934
|
+
return public_base_url_looks_public(normalized_public_base_url)
|
|
1935
|
+
|
|
1936
|
+
def _connector_has_user_config(self, name: str, config: dict[str, object]) -> bool:
|
|
1937
|
+
if name == "qq":
|
|
1938
|
+
return bool(list_qq_profiles(config))
|
|
1939
|
+
if name in PROFILEABLE_CONNECTOR_NAMES:
|
|
1940
|
+
return bool(list_connector_profiles(name, config))
|
|
1941
|
+
if name == "weixin":
|
|
1942
|
+
return any(
|
|
1943
|
+
str(config.get(key) or "").strip()
|
|
1944
|
+
for key in ("bot_token", "bot_token_env", "account_id", "login_user_id", "route_tag")
|
|
1945
|
+
)
|
|
1946
|
+
if name == "lingzhu":
|
|
1947
|
+
return any(
|
|
1948
|
+
str(config.get(key) or "").strip()
|
|
1949
|
+
for key in ("auth_ak", "auth_ak_env", "public_base_url")
|
|
1950
|
+
)
|
|
1951
|
+
return False
|
|
1952
|
+
|
|
1953
|
+
def _should_validate_connector(self, name: str, config: dict[str, object]) -> bool:
|
|
1954
|
+
return bool(config.get("enabled", False)) or self._connector_has_user_config(name, config)
|
|
1955
|
+
|
|
1484
1956
|
def _normalize_plugins_payload(self, payload: dict) -> dict:
|
|
1485
1957
|
normalized = deepcopy(payload)
|
|
1486
1958
|
if "load_paths" not in normalized and isinstance(normalized.get("search_paths"), list):
|