@researai/deepscientist 1.5.0 → 1.5.1

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.
Files changed (163) hide show
  1. package/AGENTS.md +26 -0
  2. package/README.md +19 -179
  3. package/assets/connectors/lingzhu/openclaw-bridge/README.md +124 -0
  4. package/assets/connectors/lingzhu/openclaw-bridge/index.ts +162 -0
  5. package/assets/connectors/lingzhu/openclaw-bridge/openclaw.plugin.json +145 -0
  6. package/assets/connectors/lingzhu/openclaw-bridge/package.json +35 -0
  7. package/assets/connectors/lingzhu/openclaw-bridge/src/cli.ts +180 -0
  8. package/assets/connectors/lingzhu/openclaw-bridge/src/config.ts +196 -0
  9. package/assets/connectors/lingzhu/openclaw-bridge/src/debug-log.ts +111 -0
  10. package/assets/connectors/lingzhu/openclaw-bridge/src/events.ts +4 -0
  11. package/assets/connectors/lingzhu/openclaw-bridge/src/http-handler.ts +1133 -0
  12. package/assets/connectors/lingzhu/openclaw-bridge/src/image-cache.ts +75 -0
  13. package/assets/connectors/lingzhu/openclaw-bridge/src/lingzhu-tools.ts +246 -0
  14. package/assets/connectors/lingzhu/openclaw-bridge/src/transform.ts +541 -0
  15. package/assets/connectors/lingzhu/openclaw-bridge/src/types.ts +131 -0
  16. package/assets/connectors/lingzhu/openclaw-bridge/tsconfig.json +14 -0
  17. package/assets/connectors/lingzhu/openclaw.lingzhu.config.template.json +39 -0
  18. package/bin/ds.js +233 -53
  19. package/docs/en/00_QUICK_START.md +134 -0
  20. package/docs/en/01_SETTINGS_REFERENCE.md +1104 -0
  21. package/docs/en/02_START_RESEARCH_GUIDE.md +404 -0
  22. package/docs/en/03_QQ_CONNECTOR_GUIDE.md +325 -0
  23. package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +216 -0
  24. package/docs/en/05_TUI_GUIDE.md +141 -0
  25. package/docs/en/06_RUNTIME_AND_CANVAS.md +679 -0
  26. package/docs/en/07_MEMORY_AND_MCP.md +253 -0
  27. package/docs/en/08_FIGURE_STYLE_GUIDE.md +97 -0
  28. package/docs/en/09_DOCTOR.md +108 -0
  29. package/docs/en/90_ARCHITECTURE.md +245 -0
  30. package/docs/en/91_DEVELOPMENT.md +195 -0
  31. package/docs/en/99_ACKNOWLEDGEMENTS.md +29 -0
  32. package/docs/zh/00_QUICK_START.md +134 -0
  33. package/docs/zh/01_SETTINGS_REFERENCE.md +1137 -0
  34. package/docs/zh/02_START_RESEARCH_GUIDE.md +414 -0
  35. package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +324 -0
  36. package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +230 -0
  37. package/docs/zh/05_TUI_GUIDE.md +128 -0
  38. package/docs/zh/06_RUNTIME_AND_CANVAS.md +271 -0
  39. package/docs/zh/07_MEMORY_AND_MCP.md +235 -0
  40. package/docs/zh/08_FIGURE_STYLE_GUIDE.md +97 -0
  41. package/docs/zh/09_DOCTOR.md +112 -0
  42. package/docs/zh/99_ACKNOWLEDGEMENTS.md +29 -0
  43. package/install.sh +32 -8
  44. package/package.json +4 -2
  45. package/pyproject.toml +1 -1
  46. package/src/deepscientist/artifact/guidance.py +9 -2
  47. package/src/deepscientist/artifact/service.py +482 -22
  48. package/src/deepscientist/bash_exec/monitor.py +27 -5
  49. package/src/deepscientist/bash_exec/runtime.py +639 -0
  50. package/src/deepscientist/bash_exec/service.py +99 -16
  51. package/src/deepscientist/bridges/base.py +3 -0
  52. package/src/deepscientist/bridges/connectors.py +292 -13
  53. package/src/deepscientist/channels/qq.py +19 -2
  54. package/src/deepscientist/channels/relay.py +1 -0
  55. package/src/deepscientist/cli.py +32 -25
  56. package/src/deepscientist/config/models.py +28 -2
  57. package/src/deepscientist/config/service.py +201 -6
  58. package/src/deepscientist/connector_runtime.py +2 -0
  59. package/src/deepscientist/daemon/api/handlers.py +50 -5
  60. package/src/deepscientist/daemon/api/router.py +1 -0
  61. package/src/deepscientist/daemon/app.py +442 -15
  62. package/src/deepscientist/doctor.py +444 -0
  63. package/src/deepscientist/home.py +1 -0
  64. package/src/deepscientist/latex_runtime.py +17 -4
  65. package/src/deepscientist/lingzhu_support.py +182 -0
  66. package/src/deepscientist/mcp/server.py +49 -2
  67. package/src/deepscientist/prompts/builder.py +181 -58
  68. package/src/deepscientist/quest/layout.py +1 -0
  69. package/src/deepscientist/quest/service.py +63 -2
  70. package/src/deepscientist/quest/stage_views.py +19 -1
  71. package/src/deepscientist/runtime_tools/__init__.py +16 -0
  72. package/src/deepscientist/runtime_tools/builtins.py +19 -0
  73. package/src/deepscientist/runtime_tools/models.py +29 -0
  74. package/src/deepscientist/runtime_tools/registry.py +40 -0
  75. package/src/deepscientist/runtime_tools/service.py +59 -0
  76. package/src/deepscientist/runtime_tools/tinytex.py +25 -0
  77. package/src/deepscientist/tinytex.py +276 -0
  78. package/src/prompts/connectors/lingzhu.md +12 -0
  79. package/src/prompts/connectors/qq.md +121 -0
  80. package/src/prompts/system.md +177 -33
  81. package/src/skills/analysis-campaign/SKILL.md +22 -6
  82. package/src/skills/baseline/SKILL.md +5 -4
  83. package/src/skills/decision/SKILL.md +4 -3
  84. package/src/skills/experiment/SKILL.md +5 -4
  85. package/src/skills/finalize/SKILL.md +5 -4
  86. package/src/skills/idea/SKILL.md +5 -4
  87. package/src/skills/intake-audit/SKILL.md +277 -0
  88. package/src/skills/intake-audit/references/state-audit-template.md +41 -0
  89. package/src/skills/rebuttal/SKILL.md +407 -0
  90. package/src/skills/rebuttal/references/action-plan-template.md +63 -0
  91. package/src/skills/rebuttal/references/evidence-update-template.md +30 -0
  92. package/src/skills/rebuttal/references/response-letter-template.md +113 -0
  93. package/src/skills/rebuttal/references/review-matrix-template.md +55 -0
  94. package/src/skills/review/SKILL.md +293 -0
  95. package/src/skills/review/references/experiment-todo-template.md +29 -0
  96. package/src/skills/review/references/review-report-template.md +83 -0
  97. package/src/skills/review/references/revision-log-template.md +40 -0
  98. package/src/skills/scout/SKILL.md +5 -4
  99. package/src/skills/write/SKILL.md +7 -3
  100. package/src/tui/dist/components/WelcomePanel.js +17 -43
  101. package/src/tui/dist/components/messages/BashExecOperationMessage.js +3 -2
  102. package/src/tui/package.json +1 -1
  103. package/src/ui/dist/assets/{AiManusChatView-7v-dHngU.js → AiManusChatView-w5lF2Ttt.js} +109 -575
  104. package/src/ui/dist/assets/{AnalysisPlugin-B_Xmz-KE.js → AnalysisPlugin-DJOED79I.js} +1 -1
  105. package/src/ui/dist/assets/{AutoFigurePlugin-Cko-0tm1.js → AutoFigurePlugin-DaG61Y0M.js} +63 -8
  106. package/src/ui/dist/assets/{CliPlugin-BsU0ht7q.js → CliPlugin-CV4LqUB_.js} +43 -609
  107. package/src/ui/dist/assets/{CodeEditorPlugin-DcMMP0Rt.js → CodeEditorPlugin-DylfAea4.js} +8 -8
  108. package/src/ui/dist/assets/{CodeViewerPlugin-BqoQ5QyY.js → CodeViewerPlugin-F7saY0LM.js} +5 -5
  109. package/src/ui/dist/assets/{DocViewerPlugin-D7eHNhU6.js → DocViewerPlugin-COP0c7jf.js} +3 -3
  110. package/src/ui/dist/assets/{GitDiffViewerPlugin-DLJN42T5.js → GitDiffViewerPlugin-CAS05pT9.js} +1 -1
  111. package/src/ui/dist/assets/{ImageViewerPlugin-gJMV7MOu.js → ImageViewerPlugin-Bco1CN_w.js} +5 -6
  112. package/src/ui/dist/assets/{LabCopilotPanel-B857sfxP.js → LabCopilotPanel-CvMlCD99.js} +12 -15
  113. package/src/ui/dist/assets/LabPlugin-BYankkE4.js +2676 -0
  114. package/src/ui/dist/assets/LabPlugin-D9jVIo0A.css +2698 -0
  115. package/src/ui/dist/assets/{LatexPlugin-DWKEo-Wj.js → LatexPlugin-LDSMR-t-.js} +16 -16
  116. package/src/ui/dist/assets/{MarkdownViewerPlugin-DBzoEmhv.js → MarkdownViewerPlugin-B7o80jgm.js} +4 -4
  117. package/src/ui/dist/assets/{MarketplacePlugin-DoHc-8vo.js → MarketplacePlugin-CM6ZOcpC.js} +3 -3
  118. package/src/ui/dist/assets/{NotebookEditor-CKjKH-yS.js → NotebookEditor-Dc61cXmK.js} +3 -3
  119. package/src/ui/dist/assets/{PdfLoader-zFoL0VPo.js → PdfLoader-DWowuQwx.js} +1 -1
  120. package/src/ui/dist/assets/{PdfMarkdownPlugin-DXPaL9Nt.js → PdfMarkdownPlugin-BsJM1q_a.js} +3 -3
  121. package/src/ui/dist/assets/{PdfViewerPlugin-DhK8qCFp.js → PdfViewerPlugin-DB2eEEFQ.js} +10 -10
  122. package/src/ui/dist/assets/{SearchPlugin-CdSi6krf.js → SearchPlugin-CraThSvt.js} +1 -1
  123. package/src/ui/dist/assets/{Stepper-V-WiDQJl.js → Stepper-CgocRTPq.js} +1 -1
  124. package/src/ui/dist/assets/{TextViewerPlugin-hIs1Efiu.js → TextViewerPlugin-B1JGhKtd.js} +4 -4
  125. package/src/ui/dist/assets/{VNCViewer-DG8b0q2X.js → VNCViewer-CclFC7FM.js} +9 -10
  126. package/src/ui/dist/assets/{bibtex-HDac6fVW.js → bibtex-D3IKsMl7.js} +1 -1
  127. package/src/ui/dist/assets/{code-BnBeNxBc.js → code-BP37Xx0p.js} +1 -1
  128. package/src/ui/dist/assets/{file-content-IRQ3jHb8.js → file-content-BAJSu-9r.js} +1 -1
  129. package/src/ui/dist/assets/{file-diff-panel-DZoQ9I6r.js → file-diff-panel-DUGeCTuy.js} +1 -1
  130. package/src/ui/dist/assets/{file-socket-BMCdLc-P.js → file-socket-CXc1Ojf7.js} +1 -1
  131. package/src/ui/dist/assets/{file-utils-CltILB3w.js → file-utils-2J21jt7M.js} +1 -1
  132. package/src/ui/dist/assets/{image-Boe6ffhu.js → image-CMMmgvcn.js} +1 -1
  133. package/src/ui/dist/assets/{index-BlplpvE1.js → index-BaVumsQT.js} +2 -2
  134. package/src/ui/dist/assets/{index-DZqJ-qAM.js → index-CWgMgpow.js} +60 -2154
  135. package/src/ui/dist/assets/{index-DO43pFZP.js → index-DmwmJmbW.js} +6372 -8434
  136. package/src/ui/dist/assets/{index-Bq2bvfkl.css → index-KGt-z-dD.css} +225 -2920
  137. package/src/ui/dist/assets/{index-2Zf65FZt.js → index-s7aHnNQ4.js} +1 -1
  138. package/src/ui/dist/assets/{message-square-mUHn_Ssb.js → message-square-CQRfX0Am.js} +1 -1
  139. package/src/ui/dist/assets/{monaco-fe0arNEU.js → monaco-B4TbdsrF.js} +1 -1
  140. package/src/ui/dist/assets/{popover-D_7i19qU.js → popover-B8Rokodk.js} +1 -1
  141. package/src/ui/dist/assets/{project-sync-DyVGrU7H.js → project-sync-D_i96KH4.js} +2 -8
  142. package/src/ui/dist/assets/{sigma-BzazRyxQ.js → sigma-D12PnzCN.js} +1 -1
  143. package/src/ui/dist/assets/{tooltip-DN_yjHFH.js → tooltip-B6YrI4aJ.js} +1 -1
  144. package/src/ui/dist/assets/trash-Bc8jGp0V.js +32 -0
  145. package/src/ui/dist/assets/{useCliAccess-DV2L2Qxy.js → useCliAccess-mXVCYSZ-.js} +12 -42
  146. package/src/ui/dist/assets/{useFileDiffOverlay-DyTj-p_V.js → useFileDiffOverlay-Bg6b9H9K.js} +1 -1
  147. package/src/ui/dist/assets/{wrap-text-ozYHtUwq.js → wrap-text-Drh5GEnL.js} +1 -1
  148. package/src/ui/dist/assets/{zoom-out-BN9MUyCQ.js → zoom-out-CJj9DZLn.js} +1 -1
  149. package/src/ui/dist/index.html +2 -2
  150. package/assets/fonts/Inter-Variable.ttf +0 -0
  151. package/assets/fonts/NotoSerifSC-Regular-C94HN_ZN.ttf +0 -0
  152. package/assets/fonts/NunitoSans-Variable.ttf +0 -0
  153. package/assets/fonts/Satoshi-Medium-ByP-Zb-9.woff2 +0 -0
  154. package/assets/fonts/SourceSans3-Variable.ttf +0 -0
  155. package/assets/fonts/ds-fonts.css +0 -83
  156. package/src/ui/dist/assets/Inter-Variable-VF2RPR_K.ttf +0 -0
  157. package/src/ui/dist/assets/LabPlugin-bL7rpic8.js +0 -43
  158. package/src/ui/dist/assets/NotoSerifSC-Regular-C94HN_ZN-C94HN_ZN.ttf +0 -0
  159. package/src/ui/dist/assets/NunitoSans-Variable-B_ZymHAd.ttf +0 -0
  160. package/src/ui/dist/assets/Satoshi-Medium-ByP-Zb-9-GkA34YXu.woff2 +0 -0
  161. package/src/ui/dist/assets/SourceSans3-Variable-CD-WOsSK.ttf +0 -0
  162. package/src/ui/dist/assets/info-CcsK_htA.js +0 -18
  163. package/src/ui/dist/assets/user-plus-BusDx-hF.js +0 -79
@@ -14,12 +14,14 @@ from urllib.request import Request, urlopen
14
14
  from .artifact import ArtifactService
15
15
  from .config import ConfigManager
16
16
  from .daemon import DaemonApp
17
+ from .doctor import render_doctor_report, run_doctor
17
18
  from .home import default_home, ensure_home_layout, repo_root
18
19
  from .memory import MemoryService
19
20
  from .prompts import PromptBuilder
20
21
  from .quest import QuestService
21
22
  from .registries import BaselineRegistry
22
23
  from .runners import CodexRunner, RunRequest, get_runner_factory, register_builtin_runners
24
+ from .runtime_tools import RuntimeToolService
23
25
  from .runtime_logs import JsonlLogger
24
26
  from .shared import ensure_dir, read_yaml
25
27
  from .skills import SkillInstaller
@@ -76,8 +78,7 @@ def build_parser() -> argparse.ArgumentParser:
76
78
  graph_parser = subparsers.add_parser("graph")
77
79
  graph_parser.add_argument("quest_id")
78
80
 
79
- metrics_parser = subparsers.add_parser("metrics")
80
- metrics_parser.add_argument("target")
81
+ subparsers.add_parser("doctor", aliases=["docker"])
81
82
 
82
83
  push_parser = subparsers.add_parser("push")
83
84
  push_parser.add_argument("quest_id")
@@ -95,6 +96,11 @@ def build_parser() -> argparse.ArgumentParser:
95
96
  baseline_attach.add_argument("--baseline-id", required=True)
96
97
  baseline_attach.add_argument("--variant-id", default=None)
97
98
 
99
+ latex_parser = subparsers.add_parser("latex")
100
+ latex_subparsers = latex_parser.add_subparsers(dest="latex_command", required=True)
101
+ latex_subparsers.add_parser("status")
102
+ latex_subparsers.add_parser("install-runtime")
103
+
98
104
  config_parser = subparsers.add_parser("config")
99
105
  config_subparsers = config_parser.add_subparsers(dest="config_command", required=True)
100
106
  config_show = config_subparsers.add_parser("show")
@@ -355,27 +361,10 @@ def graph_command(home: Path, quest_id: str) -> int:
355
361
  return 0
356
362
 
357
363
 
358
- def metrics_command(home: Path, target: str) -> int:
359
- if (home / "quests" / target / "quest.yaml").exists():
360
- quest_root = home / "quests" / target
361
- runs = sorted((quest_root / "artifacts" / "runs").glob("*.json"))
362
- payload = []
363
- from .shared import read_json
364
-
365
- for path in runs:
366
- item = read_json(path, {})
367
- payload.append(
368
- {
369
- "run_id": item.get("run_id"),
370
- "run_kind": item.get("run_kind"),
371
- "exit_code": item.get("exit_code"),
372
- "summary": item.get("summary"),
373
- }
374
- )
375
- print(json.dumps(payload, ensure_ascii=False, indent=2))
376
- return 0
377
- print(json.dumps({"message": "Run-level metrics lookup is not implemented yet."}, ensure_ascii=False, indent=2))
378
- return 0
364
+ def doctor_command(home: Path) -> int:
365
+ report = run_doctor(home, repo_root=repo_root())
366
+ sys.stdout.write(render_doctor_report(report))
367
+ return 0 if report.get("ok") else 1
379
368
 
380
369
 
381
370
  def push_command(home: Path, quest_id: str) -> int:
@@ -415,6 +404,20 @@ def baseline_attach_command(home: Path, quest_id: str, baseline_id: str, variant
415
404
  return 0 if result.get("ok") else 1
416
405
 
417
406
 
407
+ def latex_status_command(home: Path) -> int:
408
+ ensure_home_layout(home)
409
+ payload = RuntimeToolService(home).status("tinytex")
410
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
411
+ return 0 if payload.get("ok") else 1
412
+
413
+
414
+ def latex_install_runtime_command(home: Path) -> int:
415
+ ensure_home_layout(home)
416
+ payload = RuntimeToolService(home).install("tinytex")
417
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
418
+ return 0 if payload.get("ok") else 1
419
+
420
+
418
421
  def config_show_command(home: Path, name: str) -> int:
419
422
  manager = ConfigManager(home)
420
423
  text = manager.load_named_text(name, create_optional=True)
@@ -469,8 +472,8 @@ def main(argv: list[str] | None = None) -> int:
469
472
  return approve_command(home, args.quest_id, args.decision_id, args.reason)
470
473
  if args.command == "graph":
471
474
  return graph_command(home, args.quest_id)
472
- if args.command == "metrics":
473
- return metrics_command(home, args.target)
475
+ if args.command in {"doctor", "docker"}:
476
+ return doctor_command(home)
474
477
  if args.command == "push":
475
478
  return push_command(home, args.quest_id)
476
479
  if args.command == "memory" and args.memory_command == "search":
@@ -479,6 +482,10 @@ def main(argv: list[str] | None = None) -> int:
479
482
  return baseline_list_command(home)
480
483
  if args.command == "baseline" and args.baseline_command == "attach":
481
484
  return baseline_attach_command(home, args.quest_id, args.baseline_id, args.variant_id)
485
+ if args.command == "latex" and args.latex_command == "status":
486
+ return latex_status_command(home)
487
+ if args.command == "latex" and args.latex_command == "install-runtime":
488
+ return latex_install_runtime_command(home)
482
489
  if args.command == "config" and args.config_command == "show":
483
490
  return config_show_command(home, args.name)
484
491
  if args.command == "config" and args.config_command == "edit":
@@ -35,7 +35,7 @@ def default_config(home: Path) -> dict:
35
35
  "host": "0.0.0.0",
36
36
  "port": 20999,
37
37
  "auto_open_browser": True,
38
- "default_mode": "both",
38
+ "default_mode": "web",
39
39
  },
40
40
  "logging": {
41
41
  "level": "info",
@@ -133,8 +133,9 @@ def default_connectors() -> dict:
133
133
  "gateway_restart_on_config_change": True,
134
134
  "auto_send_main_experiment_png": True,
135
135
  "auto_send_analysis_summary_png": True,
136
- "auto_send_slice_png": False,
136
+ "auto_send_slice_png": True,
137
137
  "auto_send_paper_pdf": True,
138
+ "enable_markdown_send": False,
138
139
  "enable_file_upload_experimental": False,
139
140
  },
140
141
  "telegram": {
@@ -257,6 +258,31 @@ def default_connectors() -> dict:
257
258
  "groups": [],
258
259
  "auto_bind_dm_to_active_quest": True,
259
260
  },
261
+ "lingzhu": {
262
+ "enabled": False,
263
+ "transport": "openclaw_sse",
264
+ "local_host": "127.0.0.1",
265
+ "gateway_port": 18789,
266
+ "public_base_url": None,
267
+ "auth_ak": None,
268
+ "agent_id": "main",
269
+ "include_metadata": True,
270
+ "request_timeout_ms": 60000,
271
+ "system_prompt": "",
272
+ "default_navigation_mode": "0",
273
+ "enable_follow_up": True,
274
+ "follow_up_max_count": 3,
275
+ "max_image_bytes": 5242880,
276
+ "session_mode": "per_user",
277
+ "session_namespace": "lingzhu",
278
+ "auto_receipt_ack": True,
279
+ "visible_progress_heartbeat": True,
280
+ "visible_progress_heartbeat_sec": 10,
281
+ "debug_logging": False,
282
+ "debug_log_payloads": False,
283
+ "debug_log_dir": None,
284
+ "enable_experimental_native_actions": False,
285
+ },
260
286
  }
261
287
 
262
288
 
@@ -5,10 +5,23 @@ import os
5
5
  import subprocess
6
6
  from copy import deepcopy
7
7
  from pathlib import Path
8
+ from urllib.error import URLError
8
9
  from urllib.request import Request, urlopen
9
10
 
10
11
  from ..connector_runtime import infer_connector_transport
11
12
  from ..home import repo_root
13
+ from ..lingzhu_support import (
14
+ lingzhu_agent_id,
15
+ lingzhu_generated_curl,
16
+ lingzhu_generated_openclaw_config_text,
17
+ lingzhu_gateway_port,
18
+ lingzhu_health_url,
19
+ lingzhu_local_base_url,
20
+ lingzhu_probe_payload,
21
+ lingzhu_public_base_url,
22
+ lingzhu_sse_url,
23
+ lingzhu_supported_commands,
24
+ )
12
25
  from ..shared import read_json, read_text, read_yaml, resolve_runner_binary, run_command, sha256_text, utc_now, which, write_text, write_yaml
13
26
  from .models import (
14
27
  CONFIG_NAMES,
@@ -294,9 +307,17 @@ This page edits `~/DeepScientist/config/connectors.yaml` directly.
294
307
  - readiness test exchanges `access_token` and probes `/gateway`
295
308
  - active send targets use QQ user `openid` or group `group_openid`
296
309
  - the settings page also surfaces recently discovered targets from runtime activity
297
- - QQ should stay text-first by default; only milestone media should be auto-sent
298
- - recommended auto-send policy is: main experiment summary PNG, campaign summary PNG, final paper PDF
299
- - do not auto-send every slice image or draft paper figure unless the user asked for it
310
+ - milestone delivery toggles default to enabled; adjust them only if you want less outbound push
311
+ - the recommended first-run path is: save credentials -> send one QQ private message -> confirm `Detected OpenID` -> run a probe
312
+
313
+ ### Lingzhu
314
+
315
+ - Lingzhu is configured as an OpenClaw companion endpoint, not as a full DeepScientist chat bridge
316
+ - keep `transport: openclaw_sse`
317
+ - set `gateway_port` to the OpenClaw HTTP gateway port, usually `18789`
318
+ - generate and save `auth_ak`, then fill the same Bearer token into the Lingzhu platform
319
+ - set `public_base_url` to a public IP or public domain before binding a real Rokid device
320
+ - readiness test first probes `GET /metis/agent/api/health`, then runs a minimal SSE smoke request against `POST /metis/agent/api/sse`
300
321
 
301
322
  ## Safety
302
323
 
@@ -352,7 +373,8 @@ This page edits `{home_text}/config/runners.yaml`.
352
373
  ## Recommended v1 choice
353
374
 
354
375
  - keep `codex.enabled: true`
355
- - keep `claude.enabled: false` unless you are wiring the reserved TODO path
376
+ - keep `claude.enabled: false`
377
+ - `claude` remains TODO / reserved in the current open-source release and is not runnable yet
356
378
  - keep `codex.model_reasoning_effort: xhigh` unless you explicitly want a lighter default
357
379
  - keep `codex.retry_on_failure: true` so transient Codex failures can resume automatically
358
380
  - keep retry timing near `1s / 2x / 8s max` unless you have a strong reason to slow recovery down
@@ -708,6 +730,40 @@ Use **Test** when the file exposes runtime dependencies.
708
730
  errors.append("whatsapp: `provider: meta` requires `phone_number_id`.")
709
731
  if not self._has_secret(config, "verify_token", "verify_token_env"):
710
732
  errors.append("whatsapp: `provider: meta` requires `verify_token` or `verify_token_env`.")
733
+ elif name == "lingzhu":
734
+ if transport != "openclaw_sse":
735
+ errors.append("lingzhu: `transport` must stay `openclaw_sse`.")
736
+ if not str(config.get("local_host") or "").strip():
737
+ warnings.append("lingzhu: `local_host` is empty; DeepScientist will fall back to `127.0.0.1`.")
738
+ if not self._has_secret(config, "auth_ak", "auth_ak_env"):
739
+ errors.append("lingzhu: requires `auth_ak` for Bearer authentication.")
740
+ raw_gateway_port = str(config.get("gateway_port") or "").strip()
741
+ normalized_port = lingzhu_gateway_port(config)
742
+ if raw_gateway_port and str(normalized_port) != raw_gateway_port:
743
+ errors.append("lingzhu: `gateway_port` must be a valid TCP port between 1 and 65535.")
744
+ raw_public_base_url = str(config.get("public_base_url") or "").strip()
745
+ public_base_url = lingzhu_public_base_url(config)
746
+ if raw_public_base_url and public_base_url is None:
747
+ errors.append("lingzhu: `public_base_url` must be a valid `http://` or `https://` URL when set.")
748
+ raw_visible_progress_heartbeat_sec = str(
749
+ config.get("visible_progress_heartbeat_sec") or ""
750
+ ).strip()
751
+ if raw_visible_progress_heartbeat_sec:
752
+ try:
753
+ visible_progress_heartbeat_sec = int(raw_visible_progress_heartbeat_sec)
754
+ except ValueError:
755
+ errors.append(
756
+ "lingzhu: `visible_progress_heartbeat_sec` must be an integer between 5 and 120."
757
+ )
758
+ else:
759
+ if visible_progress_heartbeat_sec < 5 or visible_progress_heartbeat_sec > 120:
760
+ errors.append(
761
+ "lingzhu: `visible_progress_heartbeat_sec` must be an integer between 5 and 120."
762
+ )
763
+ if not raw_public_base_url:
764
+ warnings.append(
765
+ "lingzhu: set `public_base_url` to a public IP or public domain before filling values into the Lingzhu platform."
766
+ )
711
767
 
712
768
  if preferred_connector and preferred_connector not in enabled_connectors:
713
769
  warnings.append(
@@ -872,12 +928,27 @@ Use **Test** when the file exposes runtime dependencies.
872
928
  errors.append(str(gateway_payload.get("message") or "QQ gateway probe failed."))
873
929
  else:
874
930
  details["gateway_url"] = gateway_url
931
+ elif name == "lingzhu":
932
+ details.update(self._lingzhu_snapshot_details(config))
933
+ auth_ak = self._secret(config, "auth_ak", "auth_ak_env")
934
+ if not auth_ak:
935
+ errors.append("Lingzhu requires `auth_ak` before it can accept Bearer-authenticated requests.")
936
+ elif live:
937
+ health_probe = self._probe_lingzhu_health(config)
938
+ details["health_probe"] = health_probe
939
+ if not health_probe.get("ok", False):
940
+ errors.append(str(health_probe.get("message") or "Lingzhu health probe failed."))
941
+ else:
942
+ sse_probe = self._probe_lingzhu_sse(config)
943
+ details["sse_probe"] = sse_probe
944
+ if not sse_probe.get("ok", False):
945
+ errors.append(str(sse_probe.get("message") or "Lingzhu SSE probe failed."))
875
946
  else:
876
947
  warnings.append(f"No dedicated system test exists for connector `{name}`.")
877
948
  except Exception as exc: # pragma: no cover - network-dependent
878
949
  errors.append(str(exc))
879
950
 
880
- if delivery_target:
951
+ if delivery_target and name != "lingzhu":
881
952
  delivery_message = str(delivery_target.get("text") or "").strip()
882
953
  chat_type = str(delivery_target.get("chat_type") or "direct").strip().lower()
883
954
  chat_id = str(delivery_target.get("chat_id") or "").strip()
@@ -899,7 +970,9 @@ Use **Test** when the file exposes runtime dependencies.
899
970
  warnings.append("Delivery test chat_type must be `direct` or `group`.")
900
971
  elif not chat_id:
901
972
  if name == "qq":
902
- warnings.append("Delivery test target is empty. For QQ direct sends, use a user `openid` or group `group_openid`.")
973
+ warnings.append(
974
+ "QQ readiness is healthy, but no OpenID has been learned yet. Save credentials, then send one private QQ message so DeepScientist can auto-detect and save the `openid`."
975
+ )
903
976
  else:
904
977
  warnings.append("Delivery test is configured, but the target chat id is empty.")
905
978
  elif errors:
@@ -1116,6 +1189,126 @@ Use **Test** when the file exposes runtime dependencies.
1116
1189
  return text
1117
1190
  return text[: limit - 1].rstrip() + "…"
1118
1191
 
1192
+ def lingzhu_snapshot(self, config: dict | None = None) -> dict:
1193
+ resolved = dict(config or self.load_named_normalized("connectors").get("lingzhu") or {})
1194
+ snapshot: dict[str, object] = {
1195
+ "name": "lingzhu",
1196
+ "display_mode": "companion_config",
1197
+ "mode": "openclaw_companion",
1198
+ "transport": "openclaw_sse",
1199
+ "enabled": bool(resolved.get("enabled", False)),
1200
+ "relay_url": None,
1201
+ "main_chat_id": None,
1202
+ "last_conversation_id": None,
1203
+ "inbox_count": 0,
1204
+ "outbox_count": 0,
1205
+ "ignored_count": 0,
1206
+ "binding_count": 0,
1207
+ "target_count": 0,
1208
+ "recent_conversations": [],
1209
+ "recent_events": [],
1210
+ "discovered_targets": [],
1211
+ "details": self._lingzhu_snapshot_details(resolved),
1212
+ }
1213
+ if not snapshot["enabled"]:
1214
+ snapshot["connection_state"] = "disabled"
1215
+ snapshot["auth_state"] = "disabled"
1216
+ return snapshot
1217
+ snapshot["auth_state"] = "ready" if self._secret(resolved, "auth_ak", "auth_ak_env") else "missing_auth_ak"
1218
+ health_probe = self._probe_lingzhu_health(resolved, timeout=1.5)
1219
+ snapshot["details"]["health_probe"] = health_probe
1220
+ if health_probe.get("ok", False):
1221
+ snapshot["connection_state"] = "reachable"
1222
+ else:
1223
+ snapshot["connection_state"] = "offline"
1224
+ if health_probe.get("message"):
1225
+ snapshot["last_error"] = health_probe.get("message")
1226
+ return snapshot
1227
+
1228
+ def _lingzhu_snapshot_details(self, config: dict) -> dict:
1229
+ auth_ak = self._secret(config, "auth_ak", "auth_ak_env")
1230
+ return {
1231
+ "local_base_url": lingzhu_local_base_url(config),
1232
+ "health_url": lingzhu_health_url(config),
1233
+ "endpoint_url": lingzhu_sse_url(config),
1234
+ "public_base_url": lingzhu_public_base_url(config),
1235
+ "public_health_url": lingzhu_health_url(config, public=True),
1236
+ "public_endpoint_url": lingzhu_sse_url(config, public=True),
1237
+ "gateway_port": lingzhu_gateway_port(config),
1238
+ "agent_id": lingzhu_agent_id(config),
1239
+ "auth_ak_masked": self._mask_secret(auth_ak),
1240
+ "generated_curl": lingzhu_generated_curl({**config, "auth_ak": auth_ak}),
1241
+ "generated_openclaw_config": lingzhu_generated_openclaw_config_text({**config, "auth_ak": auth_ak}),
1242
+ "packaged_bridge_dir": "assets/connectors/lingzhu/openclaw-bridge",
1243
+ "packaged_template_path": "assets/connectors/lingzhu/openclaw.lingzhu.config.template.json",
1244
+ "supported_commands": lingzhu_supported_commands(
1245
+ experimental_enabled=bool(config.get("enable_experimental_native_actions", False))
1246
+ ),
1247
+ "public_ip_required": True,
1248
+ }
1249
+
1250
+ def _probe_lingzhu_health(self, config: dict, *, timeout: float = 5.0) -> dict:
1251
+ url = lingzhu_health_url(config)
1252
+ if not url:
1253
+ return {"ok": False, "message": "Lingzhu health URL is empty."}
1254
+ try:
1255
+ request = Request(url, method="GET", headers={"Accept": "application/json"})
1256
+ with urlopen(request, timeout=timeout) as response: # noqa: S310
1257
+ payload = json.loads(response.read().decode("utf-8", errors="replace") or "{}")
1258
+ status_code = response.status
1259
+ return {
1260
+ "ok": True,
1261
+ "status_code": status_code,
1262
+ "status": payload.get("status"),
1263
+ "payload": payload,
1264
+ }
1265
+ except URLError as exc:
1266
+ return {"ok": False, "message": str(exc.reason or exc)}
1267
+ except Exception as exc:
1268
+ return {"ok": False, "message": str(exc)}
1269
+
1270
+ def _probe_lingzhu_sse(self, config: dict, *, timeout: float = 8.0) -> dict:
1271
+ url = lingzhu_sse_url(config)
1272
+ auth_ak = self._secret(config, "auth_ak", "auth_ak_env")
1273
+ if not url:
1274
+ return {"ok": False, "message": "Lingzhu SSE URL is empty."}
1275
+ if not auth_ak:
1276
+ return {"ok": False, "message": "Lingzhu auth_ak is empty."}
1277
+ request = Request(
1278
+ url,
1279
+ method="POST",
1280
+ headers={
1281
+ "Accept": "text/event-stream",
1282
+ "Authorization": f"Bearer {auth_ak}",
1283
+ "Content-Type": "application/json; charset=utf-8",
1284
+ },
1285
+ data=json.dumps(lingzhu_probe_payload(config), ensure_ascii=False).encode("utf-8"),
1286
+ )
1287
+ try:
1288
+ with urlopen(request, timeout=timeout) as response: # noqa: S310
1289
+ preview = response.read(512).decode("utf-8", errors="replace")
1290
+ content_type = str(response.headers.get("Content-Type") or "")
1291
+ ok = "text/event-stream" in content_type or "event:" in preview or "data:" in preview
1292
+ return {
1293
+ "ok": ok,
1294
+ "content_type": content_type,
1295
+ "preview": self._compact_probe_text(preview, limit=512),
1296
+ "message": None if ok else "Lingzhu SSE probe did not return an event-stream payload.",
1297
+ }
1298
+ except URLError as exc:
1299
+ return {"ok": False, "message": str(exc.reason or exc)}
1300
+ except Exception as exc:
1301
+ return {"ok": False, "message": str(exc)}
1302
+
1303
+ @staticmethod
1304
+ def _mask_secret(value: str) -> str:
1305
+ text = str(value or "").strip()
1306
+ if not text:
1307
+ return ""
1308
+ if len(text) <= 8:
1309
+ return "*" * len(text)
1310
+ return f"{text[:4]}{'*' * (len(text) - 8)}{text[-4:]}"
1311
+
1119
1312
  def _normalize_named_payload(self, name: str, payload: dict) -> dict:
1120
1313
  if not isinstance(payload, dict):
1121
1314
  return default_payload(name, self.home)
@@ -1145,6 +1338,8 @@ Use **Test** when the file exposes runtime dependencies.
1145
1338
  for legacy_key in ("mode", "relay_url", "relay_auth_token", "public_callback_url", "webhook_verify_signature"):
1146
1339
  sanitized_payload.pop(legacy_key, None)
1147
1340
  sanitized_payload["transport"] = "gateway_direct"
1341
+ elif connector_name == "lingzhu":
1342
+ sanitized_payload["transport"] = "openclaw_sse"
1148
1343
  elif "transport" not in sanitized_payload:
1149
1344
  inferred_transport = infer_connector_transport(connector_name, {**base, **sanitized_payload})
1150
1345
  if inferred_transport:
@@ -59,6 +59,8 @@ def infer_connector_transport(name: str, config: dict[str, Any] | None) -> str:
59
59
  ):
60
60
  return "legacy_meta_cloud"
61
61
  return "local_session"
62
+ if normalized == "lingzhu":
63
+ return "openclaw_sse"
62
64
  if relay_url and mode == "relay":
63
65
  return "relay"
64
66
  return "direct"
@@ -194,7 +194,7 @@ npm --prefix src/ui run build</pre>
194
194
  return get_acp_bridge_status().as_dict()
195
195
 
196
196
  def connectors(self) -> list[dict]:
197
- return [channel.status() for channel in self.app.channels.values()]
197
+ return self.app.list_connector_statuses()
198
198
 
199
199
  def baselines(self) -> list[dict]:
200
200
  return self.app.artifact_service.baselines.list_entries()
@@ -234,6 +234,9 @@ npm --prefix src/ui run build</pre>
234
234
  title = body.get("title", "").strip() or None
235
235
  quest_id = body.get("quest_id", "").strip() or None
236
236
  source = body.get("source", "").strip() or "web"
237
+ preferred_connector_conversation_id = (
238
+ str(body.get("preferred_connector_conversation_id") or "").strip() or None
239
+ )
237
240
  requested_baseline_ref = body.get("requested_baseline_ref")
238
241
  startup_contract = body.get("startup_contract")
239
242
  auto_start = body.get("auto_start") is True
@@ -246,6 +249,7 @@ npm --prefix src/ui run build</pre>
246
249
  title=title,
247
250
  quest_id=quest_id,
248
251
  source=source,
252
+ preferred_connector_conversation_id=preferred_connector_conversation_id,
249
253
  requested_baseline_ref=requested_baseline_ref if isinstance(requested_baseline_ref, dict) else None,
250
254
  startup_contract=startup_contract if isinstance(startup_contract, dict) else None,
251
255
  )
@@ -577,6 +581,33 @@ npm --prefix src/ui run build</pre>
577
581
  return 400, {"ok": False, "message": str(exc)}
578
582
  return result
579
583
 
584
+ def terminal_attach(self, quest_id: str, session_id: str, body: dict | None = None) -> dict | tuple[int, dict]:
585
+ _unused = body or {}
586
+ quest_root = self.app.quest_service._quest_root(quest_id)
587
+ try:
588
+ session = self.app.bash_exec_service.get_session(quest_root, session_id)
589
+ except FileNotFoundError:
590
+ return 404, {"ok": False, "message": f"Unknown terminal session `{session_id}`."}
591
+ if str(session.get("kind") or "").lower() != "terminal":
592
+ return 400, {"ok": False, "message": "not_terminal_session"}
593
+ if str(session.get("status") or "").lower() in {"completed", "failed", "terminated"}:
594
+ return 409, {"ok": False, "message": "terminal_session_inactive", "session": session}
595
+ try:
596
+ token = self.app.bash_exec_service.issue_terminal_attach_token(quest_root, session_id)
597
+ except ValueError as exc:
598
+ return 409, {"ok": False, "message": str(exc), "session": session}
599
+ attach_port = self.app._terminal_attach_port
600
+ if not attach_port:
601
+ return 503, {"ok": False, "message": "terminal_attach_server_unavailable", "session": session}
602
+ return {
603
+ "ok": True,
604
+ "port": attach_port,
605
+ "path": "/terminal/attach",
606
+ "token": token["token"],
607
+ "expires_at": token["expires_at"],
608
+ "session": session,
609
+ }
610
+
580
611
  def terminal_restore(self, quest_id: str, session_id: str, path: str) -> dict | tuple[int, dict]:
581
612
  query = self.parse_query(path)
582
613
  commands_raw = ((query.get("commands") or ["10"])[0] or "10").strip()
@@ -780,7 +811,13 @@ npm --prefix src/ui run build</pre>
780
811
  query = self.parse_query(path)
781
812
  revision = ((query.get("revision") or [""])[0] or "").strip() or None
782
813
  mode = ((query.get("mode") or [""])[0] or "").strip() or None
783
- return self.app.quest_service.explorer(quest_id, revision=revision, mode=mode)
814
+ profile = ((query.get("profile") or [""])[0] or "").strip() or None
815
+ return self.app.quest_service.explorer(
816
+ quest_id,
817
+ revision=revision,
818
+ mode=mode,
819
+ profile=profile,
820
+ )
784
821
 
785
822
  def quest_search(self, quest_id: str, path: str) -> dict:
786
823
  query = self.parse_query(path)
@@ -1287,9 +1324,17 @@ npm --prefix src/ui run build</pre>
1287
1324
  return self.app.config_manager.test_named_text(body["name"], body["content"], live=bool(body.get("live", True)))
1288
1325
 
1289
1326
  def asset(self, asset_path: str) -> tuple[int, dict, bytes]:
1290
- asset_root = self.app.repo_root / "assets"
1291
- path = resolve_within(asset_root, asset_path)
1292
- if not path.exists() or not path.is_file():
1327
+ candidate_roots = [
1328
+ self.app.repo_root / "src" / "ui" / "public" / "assets",
1329
+ self.app.repo_root / "assets",
1330
+ ]
1331
+ path = None
1332
+ for root in candidate_roots:
1333
+ candidate = resolve_within(root, asset_path)
1334
+ if candidate.exists() and candidate.is_file():
1335
+ path = candidate
1336
+ break
1337
+ if path is None:
1293
1338
  return 404, {"Content-Type": "text/plain; charset=utf-8"}, b"Not Found"
1294
1339
  mime_type = mimetypes.guess_type(path.name)[0] or "application/octet-stream"
1295
1340
  return 200, {"Content-Type": mime_type}, path.read_bytes()
@@ -44,6 +44,7 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
44
44
  ("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/bash/sessions/(?P<bash_id>[^/]+)/stop$"), "bash_stop"),
45
45
  ("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/terminal/session/ensure$"), "terminal_session_ensure"),
46
46
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/terminal/history$"), "terminal_history"),
47
+ ("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/terminal/sessions/(?P<session_id>[^/]+)/attach$"), "terminal_attach"),
47
48
  ("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/terminal/sessions/(?P<session_id>[^/]+)/input$"), "terminal_input"),
48
49
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/terminal/sessions/(?P<session_id>[^/]+)/restore$"), "terminal_restore"),
49
50
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/terminal/sessions/(?P<session_id>[^/]+)/stream$"), "terminal_stream"),