@researai/deepscientist 1.5.9 → 1.5.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +107 -94
  2. package/assets/branding/connector-qq.png +0 -0
  3. package/assets/branding/connector-rokid.png +0 -0
  4. package/assets/branding/connector-weixin.png +0 -0
  5. package/assets/branding/projects.png +0 -0
  6. package/bin/ds.js +168 -9
  7. package/docs/assets/branding/projects.png +0 -0
  8. package/docs/en/00_QUICK_START.md +308 -70
  9. package/docs/en/01_SETTINGS_REFERENCE.md +3 -0
  10. package/docs/en/02_START_RESEARCH_GUIDE.md +112 -0
  11. package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +62 -179
  12. package/docs/en/09_DOCTOR.md +41 -5
  13. package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +137 -0
  14. package/docs/en/11_LICENSE_AND_RISK.md +256 -0
  15. package/docs/en/12_GUIDED_WORKFLOW_TOUR.md +427 -0
  16. package/docs/en/13_CORE_ARCHITECTURE_GUIDE.md +297 -0
  17. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
  18. package/docs/en/99_ACKNOWLEDGEMENTS.md +4 -1
  19. package/docs/en/README.md +79 -0
  20. package/docs/images/lingzhu/rokid-agent-platform-create.png +0 -0
  21. package/docs/images/weixin/weixin-plugin-entry.png +0 -0
  22. package/docs/images/weixin/weixin-plugin-entry.svg +33 -0
  23. package/docs/images/weixin/weixin-qr-confirm.svg +30 -0
  24. package/docs/images/weixin/weixin-quest-media-flow.svg +44 -0
  25. package/docs/images/weixin/weixin-settings-bind.svg +57 -0
  26. package/docs/zh/00_QUICK_START.md +315 -74
  27. package/docs/zh/01_SETTINGS_REFERENCE.md +3 -0
  28. package/docs/zh/02_START_RESEARCH_GUIDE.md +112 -0
  29. package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +62 -193
  30. package/docs/zh/09_DOCTOR.md +41 -5
  31. package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +144 -0
  32. package/docs/zh/11_LICENSE_AND_RISK.md +256 -0
  33. package/docs/zh/12_GUIDED_WORKFLOW_TOUR.md +423 -0
  34. package/docs/zh/13_CORE_ARCHITECTURE_GUIDE.md +296 -0
  35. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
  36. package/docs/zh/99_ACKNOWLEDGEMENTS.md +4 -1
  37. package/docs/zh/README.md +126 -0
  38. package/install.sh +0 -34
  39. package/package.json +2 -2
  40. package/pyproject.toml +1 -1
  41. package/src/deepscientist/__init__.py +1 -1
  42. package/src/deepscientist/annotations.py +343 -0
  43. package/src/deepscientist/artifact/arxiv.py +484 -37
  44. package/src/deepscientist/artifact/service.py +574 -108
  45. package/src/deepscientist/arxiv_library.py +275 -0
  46. package/src/deepscientist/bash_exec/service.py +9 -0
  47. package/src/deepscientist/bridges/builtins.py +2 -0
  48. package/src/deepscientist/bridges/connectors.py +447 -0
  49. package/src/deepscientist/channels/__init__.py +2 -0
  50. package/src/deepscientist/channels/builtins.py +3 -1
  51. package/src/deepscientist/channels/qq.py +1 -1
  52. package/src/deepscientist/channels/qq_gateway.py +1 -1
  53. package/src/deepscientist/channels/relay.py +7 -1
  54. package/src/deepscientist/channels/weixin.py +59 -0
  55. package/src/deepscientist/channels/weixin_ilink.py +317 -0
  56. package/src/deepscientist/config/models.py +22 -2
  57. package/src/deepscientist/config/service.py +431 -60
  58. package/src/deepscientist/connector/__init__.py +4 -0
  59. package/src/deepscientist/connector/connector_profiles.py +481 -0
  60. package/src/deepscientist/connector/lingzhu_support.py +668 -0
  61. package/src/deepscientist/connector/qq_profiles.py +206 -0
  62. package/src/deepscientist/connector/weixin_support.py +663 -0
  63. package/src/deepscientist/connector_profiles.py +1 -374
  64. package/src/deepscientist/connector_runtime.py +2 -0
  65. package/src/deepscientist/daemon/api/handlers.py +165 -5
  66. package/src/deepscientist/daemon/api/router.py +13 -1
  67. package/src/deepscientist/daemon/app.py +1130 -61
  68. package/src/deepscientist/doctor.py +5 -2
  69. package/src/deepscientist/gitops/diff.py +120 -29
  70. package/src/deepscientist/lingzhu_support.py +1 -182
  71. package/src/deepscientist/mcp/server.py +11 -4
  72. package/src/deepscientist/prompts/builder.py +15 -0
  73. package/src/deepscientist/qq_profiles.py +1 -196
  74. package/src/deepscientist/quest/node_traces.py +23 -0
  75. package/src/deepscientist/quest/service.py +112 -43
  76. package/src/deepscientist/quest/stage_views.py +71 -5
  77. package/src/deepscientist/runners/codex.py +55 -3
  78. package/src/deepscientist/weixin_support.py +1 -0
  79. package/src/prompts/connectors/lingzhu.md +3 -1
  80. package/src/prompts/connectors/weixin.md +230 -0
  81. package/src/prompts/system.md +2 -0
  82. package/src/tui/package.json +1 -1
  83. package/src/ui/dist/assets/{AiManusChatView-BKZ103sn.js → AiManusChatView-D0mTXG4-.js} +156 -48
  84. package/src/ui/dist/assets/{AnalysisPlugin-mTTzGAlK.js → AnalysisPlugin-Db0cTXxm.js} +1 -1
  85. package/src/ui/dist/assets/{CliPlugin-BH58n3GY.js → CliPlugin-DrV8je02.js} +164 -9
  86. package/src/ui/dist/assets/{CodeEditorPlugin-BKGRUH7e.js → CodeEditorPlugin-QXMSCH71.js} +8 -8
  87. package/src/ui/dist/assets/{CodeViewerPlugin-BMADwFWJ.js → CodeViewerPlugin-7hhtWj_E.js} +5 -5
  88. package/src/ui/dist/assets/{DocViewerPlugin-ZOnTIHLN.js → DocViewerPlugin-BWMSnRJe.js} +3 -3
  89. package/src/ui/dist/assets/{GitDiffViewerPlugin-CQ7h1Djm.js → GitDiffViewerPlugin-7J9h9Vy_.js} +20 -21
  90. package/src/ui/dist/assets/{ImageViewerPlugin-GVS5MsnC.js → ImageViewerPlugin-CHJl_0lr.js} +5 -5
  91. package/src/ui/dist/assets/{LabCopilotPanel-BZNv1JML.js → LabCopilotPanel-1qSow1es.js} +11 -11
  92. package/src/ui/dist/assets/{LabPlugin-TWcJsdQA.js → LabPlugin-eQpPPCEp.js} +2 -1
  93. package/src/ui/dist/assets/{LatexPlugin-DIjHiR2x.js → LatexPlugin-BwRfi89Z.js} +7 -7
  94. package/src/ui/dist/assets/{MarkdownViewerPlugin-D3ooGAH0.js → MarkdownViewerPlugin-836PVQWV.js} +4 -4
  95. package/src/ui/dist/assets/{MarketplacePlugin-DfVfE9hN.js → MarketplacePlugin-C2y_556i.js} +3 -3
  96. package/src/ui/dist/assets/{NotebookEditor-s8JhzuX1.js → NotebookEditor-BRzJbGsn.js} +12 -12
  97. package/src/ui/dist/assets/{NotebookEditor-DDl0_Mc0.js → NotebookEditor-DIX7Mlzu.js} +1 -1
  98. package/src/ui/dist/assets/{PdfLoader-C2Sf6SJM.js → PdfLoader-DzRaTAlq.js} +14 -7
  99. package/src/ui/dist/assets/{PdfMarkdownPlugin-CXFLoIsa.js → PdfMarkdownPlugin-DZUfIUnp.js} +73 -6
  100. package/src/ui/dist/assets/{PdfViewerPlugin-BYTmz2fK.js → PdfViewerPlugin-BwtICzue.js} +103 -34
  101. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +3627 -0
  102. package/src/ui/dist/assets/{SearchPlugin-CjWBI1O9.js → SearchPlugin-DHeIAMsx.js} +1 -1
  103. package/src/ui/dist/assets/{TextViewerPlugin-DdOBU3-S.js → TextViewerPlugin-C3tCmFox.js} +5 -4
  104. package/src/ui/dist/assets/{VNCViewer-B8HGgLwQ.js → VNCViewer-CQsKVm3t.js} +10 -10
  105. package/src/ui/dist/assets/bot-BEA2vWuK.js +21 -0
  106. package/src/ui/dist/assets/branding/logo-rokid.png +0 -0
  107. package/src/ui/dist/assets/browser-BAcuE0Xj.js +2895 -0
  108. package/src/ui/dist/assets/{code-BWAY76JP.js → code-XfbSR8K2.js} +1 -1
  109. package/src/ui/dist/assets/{file-content-C1NwU5oQ.js → file-content-BjxNaIfy.js} +1 -1
  110. package/src/ui/dist/assets/{file-diff-panel-CywslwB9.js → file-diff-panel-D_lLVQk0.js} +1 -1
  111. package/src/ui/dist/assets/{file-socket-B4kzuOBQ.js → file-socket-D9x_5vlY.js} +1 -1
  112. package/src/ui/dist/assets/{image-D-NZM-6P.js → image-BhWT33W1.js} +1 -1
  113. package/src/ui/dist/assets/{index-DHZJ_0TI.js → index--c4iXtuy.js} +12 -12
  114. package/src/ui/dist/assets/{index-BdM1Gqfr.js → index-BDxipwrC.js} +2 -2
  115. package/src/ui/dist/assets/{index-7Chr1g9c.js → index-DZTZ8mWP.js} +14221 -9523
  116. package/src/ui/dist/assets/{index-DGIYDuTv.css → index-Dqj-Mjb4.css} +2 -13
  117. package/src/ui/dist/assets/index-PJbSbPTy.js +25 -0
  118. package/src/ui/dist/assets/{monaco-Cb2uKKe6.js → monaco-K8izTGgo.js} +1 -1
  119. package/src/ui/dist/assets/{pdf-effect-queue-DSw_D3RV.js → pdf-effect-queue-DfBors6y.js} +16 -1
  120. package/src/ui/dist/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
  121. package/src/ui/dist/assets/{popover-Bg72DGgT.js → popover-yFK1J4fL.js} +1 -1
  122. package/src/ui/dist/assets/{project-sync-Ce_0BglY.js → project-sync-PENr2zcz.js} +1 -74
  123. package/src/ui/dist/assets/select-CAbJDfYv.js +1690 -0
  124. package/src/ui/dist/assets/{sigma-DPaACDrh.js → sigma-DEuYJqTl.js} +1 -1
  125. package/src/ui/dist/assets/{index-CDxNdQdz.js → square-check-big-omoSUmcd.js} +2 -13
  126. package/src/ui/dist/assets/{trash-BvTgE5__.js → trash--F119N47.js} +1 -1
  127. package/src/ui/dist/assets/{useCliAccess-CgPeMOwP.js → useCliAccess-D31UR23I.js} +1 -1
  128. package/src/ui/dist/assets/{useFileDiffOverlay-xPhz7P5B.js → useFileDiffOverlay-BH6KcMzq.js} +1 -1
  129. package/src/ui/dist/assets/{wrap-text-C3Un3YQr.js → wrap-text-CZ613PM5.js} +1 -1
  130. package/src/ui/dist/assets/{zoom-out-BgxLa0Ri.js → zoom-out-BgDLAv3z.js} +1 -1
  131. package/src/ui/dist/index.html +2 -2
  132. package/src/ui/dist/assets/AutoFigurePlugin-BGxN8Umr.css +0 -3056
  133. package/src/ui/dist/assets/AutoFigurePlugin-C_wWw4AP.js +0 -8149
  134. package/src/ui/dist/assets/PdfViewerPlugin-BJXtIwj_.css +0 -260
  135. package/src/ui/dist/assets/Stepper-B0Dd8CxK.js +0 -158
  136. package/src/ui/dist/assets/bibtex-CKaefIN2.js +0 -189
  137. package/src/ui/dist/assets/file-utils-H2fjA46S.js +0 -109
  138. package/src/ui/dist/assets/message-square-BzjLiXir.js +0 -16
  139. package/src/ui/dist/assets/pdfjs-DU1YE8WO.js +0 -3
  140. package/src/ui/dist/assets/tooltip-C_mA6R0w.js +0 -108
@@ -265,7 +265,8 @@ def _check_codex(config_manager: ConfigManager) -> dict[str, Any]:
265
265
  errors=[f"Runner binary `{binary}` could not be resolved."],
266
266
  guidance=[
267
267
  "Run `npm install -g @researai/deepscientist` again so the bundled Codex dependency is installed.",
268
- "Then run `codex` once and complete login.",
268
+ "If `codex` is still missing, install it explicitly with `npm install -g @openai/codex`.",
269
+ "Then run `codex --login` (or `codex`) once and complete login.",
269
270
  ],
270
271
  details={"binary": binary},
271
272
  )
@@ -285,7 +286,9 @@ def _check_codex(config_manager: ConfigManager) -> dict[str, Any]:
285
286
  details={"resolved_binary": resolved_binary},
286
287
  )
287
288
  if not probe_guidance:
288
- probe_guidance = ["Run `codex` manually once and complete login, then retry `ds doctor`."]
289
+ probe_guidance = [
290
+ "Run `codex --login` (or `codex`) manually once and complete login, then retry `ds doctor`.",
291
+ ]
289
292
  return _make_check(
290
293
  check_id="codex",
291
294
  label="Codex CLI",
@@ -299,45 +299,41 @@ def _collect_branch_state(repo: Path) -> dict[str, dict[str, Any]]:
299
299
  continue
300
300
  state = branch_state[branch_name]
301
301
  state.setdefault("branch", branch_name)
302
- state["updated_at"] = record.get("updated_at") or state.get("updated_at")
303
- if record.get("kind") == "run":
304
- state["run_id"] = record.get("run_id") or state.get("run_id")
305
- state["run_kind"] = record.get("run_kind") or state.get("run_kind")
302
+ artifact_sort_key = _artifact_record_sort_key(record, path)
303
+ current_artifact_sort_key = state.get("_latest_artifact_sort_key")
304
+ if current_artifact_sort_key is None or artifact_sort_key > current_artifact_sort_key:
305
+ state["_latest_artifact_sort_key"] = artifact_sort_key
306
+ state["updated_at"] = record.get("updated_at") or record.get("created_at") or state.get("updated_at")
306
307
  if record.get("idea_id"):
307
308
  state["idea_id"] = record.get("idea_id")
308
309
  if record.get("parent_branch"):
309
310
  state["parent_branch"] = record.get("parent_branch")
310
311
  if record.get("worktree_root"):
311
312
  state["worktree_root"] = record.get("worktree_root")
312
- latest_metric = extract_latest_metric(record)
313
+ resolved_run_result = _resolve_run_result_payload(repo, record)
314
+ latest_metric = extract_latest_metric(resolved_run_result if record.get("kind") == "run" else record)
313
315
  if latest_metric is not None:
314
316
  state["latest_metric"] = latest_metric
315
317
  if record.get("kind") == "run":
316
- state["latest_result"] = {
317
- "run_id": record.get("run_id"),
318
- "run_kind": record.get("run_kind"),
319
- "status": record.get("status"),
320
- "summary": record.get("summary") or record.get("reason"),
321
- "verdict": record.get("verdict"),
322
- "paths": record.get("paths") or {},
323
- "details": record.get("details") or {},
324
- "metrics_summary": record.get("metrics_summary") or {},
325
- "metric_rows": record.get("metric_rows") or [],
326
- "metric_contract": record.get("metric_contract") or {},
327
- "baseline_ref": record.get("baseline_ref") or {},
328
- "baseline_comparisons": record.get("baseline_comparisons") or {},
329
- "progress_eval": record.get("progress_eval") or {},
330
- "evaluation_summary": record.get("evaluation_summary")
331
- or ((record.get("details") or {}) if isinstance(record.get("details"), dict) else {}).get("evaluation_summary")
332
- or {},
333
- "files_changed": record.get("files_changed") or [],
334
- "evidence_paths": record.get("evidence_paths") or [],
335
- "updated_at": record.get("updated_at"),
336
- }
337
- state["breakthrough"] = bool(((record.get("progress_eval") or {}).get("breakthrough")))
338
- state["breakthrough_level"] = ((record.get("progress_eval") or {}).get("breakthrough_level"))
318
+ candidate_sort_key = _run_result_sort_key(record, resolved_run_result, path)
319
+ current_sort_key = state.get("_latest_result_sort_key")
320
+ if current_sort_key is None or candidate_sort_key > current_sort_key:
321
+ state["_latest_result_sort_key"] = candidate_sort_key
322
+ state["latest_result"] = resolved_run_result
323
+ state["run_id"] = resolved_run_result.get("run_id") or state.get("run_id")
324
+ state["run_kind"] = resolved_run_result.get("run_kind") or state.get("run_kind")
325
+ progress_eval = (
326
+ resolved_run_result.get("progress_eval")
327
+ if isinstance(resolved_run_result.get("progress_eval"), dict)
328
+ else {}
329
+ )
330
+ state["breakthrough"] = bool(progress_eval.get("breakthrough"))
331
+ state["breakthrough_level"] = progress_eval.get("breakthrough_level")
339
332
  if record.get("summary") or record.get("message") or record.get("reason"):
340
- state["latest_summary"] = record.get("summary") or record.get("message") or record.get("reason")
333
+ current_summary_sort_key = state.get("_latest_summary_sort_key")
334
+ if current_summary_sort_key is None or artifact_sort_key > current_summary_sort_key:
335
+ state["_latest_summary_sort_key"] = artifact_sort_key
336
+ state["latest_summary"] = record.get("summary") or record.get("message") or record.get("reason")
341
337
  state["recent_artifacts"].append(
342
338
  {
343
339
  "artifact_id": record.get("artifact_id"),
@@ -346,12 +342,107 @@ def _collect_branch_state(repo: Path) -> dict[str, dict[str, Any]]:
346
342
  "reason": record.get("reason"),
347
343
  "updated_at": record.get("updated_at"),
348
344
  "status": record.get("status"),
345
+ "_sort_key": artifact_sort_key,
349
346
  }
350
347
  )
348
+ state["recent_artifacts"].sort(key=lambda item: item.get("_sort_key") or ("", 0, ""))
351
349
  state["recent_artifacts"] = state["recent_artifacts"][-4:]
350
+ for state in branch_state.values():
351
+ latest_result = state.get("latest_result")
352
+ if isinstance(latest_result, dict):
353
+ result_metric = extract_latest_metric(latest_result)
354
+ if result_metric is not None:
355
+ state["latest_metric"] = result_metric
356
+ for item in state.get("recent_artifacts", []):
357
+ if isinstance(item, dict):
358
+ item.pop("_sort_key", None)
352
359
  return branch_state
353
360
 
354
361
 
362
+ def _resolve_run_result_payload(repo: Path, record: dict[str, Any]) -> dict[str, Any]:
363
+ details = dict(record.get("details") or {}) if isinstance(record.get("details"), dict) else {}
364
+ paths = dict(record.get("paths") or {}) if isinstance(record.get("paths"), dict) else {}
365
+ result_payload: dict[str, Any] = {}
366
+ result_json_path = _resolve_result_json_path(repo, paths.get("result_json"))
367
+ if result_json_path and result_json_path.exists():
368
+ loaded = read_json(result_json_path, {})
369
+ if isinstance(loaded, dict):
370
+ result_payload = loaded
371
+
372
+ evaluation_summary = (
373
+ record.get("evaluation_summary")
374
+ or details.get("evaluation_summary")
375
+ or result_payload.get("evaluation_summary")
376
+ or {}
377
+ )
378
+ progress_eval = record.get("progress_eval")
379
+ if not isinstance(progress_eval, dict):
380
+ progress_eval = result_payload.get("progress_eval") if isinstance(result_payload.get("progress_eval"), dict) else {}
381
+
382
+ return {
383
+ "run_id": record.get("run_id") or result_payload.get("run_id"),
384
+ "run_kind": record.get("run_kind") or result_payload.get("run_kind"),
385
+ "status": record.get("status") or result_payload.get("status"),
386
+ "summary": record.get("summary") or record.get("reason") or result_payload.get("summary"),
387
+ "verdict": record.get("verdict") or result_payload.get("verdict"),
388
+ "paths": paths or (result_payload.get("paths") if isinstance(result_payload.get("paths"), dict) else {}) or {},
389
+ "details": details,
390
+ "metrics_summary": record.get("metrics_summary") or result_payload.get("metrics_summary") or {},
391
+ "metric_rows": record.get("metric_rows") or result_payload.get("metric_rows") or [],
392
+ "metric_contract": record.get("metric_contract") or result_payload.get("metric_contract") or {},
393
+ "baseline_ref": record.get("baseline_ref") or result_payload.get("baseline_ref") or {},
394
+ "baseline_comparisons": record.get("baseline_comparisons") or result_payload.get("baseline_comparisons") or {},
395
+ "progress_eval": progress_eval or {},
396
+ "evaluation_summary": evaluation_summary or {},
397
+ "files_changed": record.get("files_changed") or result_payload.get("files_changed") or [],
398
+ "evidence_paths": record.get("evidence_paths") or result_payload.get("evidence_paths") or [],
399
+ "updated_at": record.get("updated_at") or result_payload.get("updated_at"),
400
+ }
401
+
402
+
403
+ def _resolve_result_json_path(repo: Path, raw_path: object) -> Path | None:
404
+ normalized = str(raw_path or "").strip()
405
+ if not normalized:
406
+ return None
407
+ candidate = Path(normalized).expanduser()
408
+ if candidate.is_absolute():
409
+ return candidate
410
+ return repo / candidate
411
+
412
+
413
+ def _artifact_record_sort_key(record: dict[str, Any], path: Path) -> tuple[str, int, str]:
414
+ updated_at = str(record.get("updated_at") or record.get("created_at") or "").strip()
415
+ try:
416
+ mtime_ns = path.stat().st_mtime_ns
417
+ except OSError:
418
+ mtime_ns = 0
419
+ return (updated_at, mtime_ns, str(path))
420
+
421
+
422
+ def _run_result_sort_key(record: dict[str, Any], payload: dict[str, Any], path: Path) -> tuple[int, str, int, str]:
423
+ quality = 0
424
+ if extract_latest_metric(payload):
425
+ quality += 8
426
+ if payload.get("baseline_comparisons"):
427
+ quality += 4
428
+ if payload.get("progress_eval"):
429
+ quality += 4
430
+ if payload.get("metrics_summary"):
431
+ quality += 3
432
+ if payload.get("metric_rows"):
433
+ quality += 3
434
+ if payload.get("verdict"):
435
+ quality += 2
436
+ paths = payload.get("paths") if isinstance(payload.get("paths"), dict) else {}
437
+ if paths.get("result_json"):
438
+ quality += 2
439
+ if paths.get("run_md"):
440
+ quality += 1
441
+ updated_at = str(payload.get("updated_at") or record.get("updated_at") or record.get("created_at") or "")
442
+ _, mtime_ns, path_str = _artifact_record_sort_key(record, path)
443
+ return (quality, updated_at, mtime_ns, path_str)
444
+
445
+
355
446
  def _classify_ref(ref: str, state: dict[str, Any]) -> dict[str, str]:
356
447
  run_id = str(state.get("run_id") or "")
357
448
  run_kind = str(state.get("run_kind") or "")
@@ -1,182 +1 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import secrets
5
- from typing import Any
6
- from urllib.parse import urlparse
7
-
8
-
9
- DEFAULT_LINGZHU_GATEWAY_PORT = 18789
10
- DEFAULT_LINGZHU_LOCAL_HOST = "127.0.0.1"
11
- DEFAULT_LINGZHU_AGENT_ID = "main"
12
- DEFAULT_LINGZHU_SESSION_NAMESPACE = "lingzhu"
13
-
14
- _AUTH_AK_SEGMENTS = (8, 4, 4, 4, 12)
15
- _AUTH_AK_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789"
16
-
17
-
18
- def generate_lingzhu_auth_ak() -> str:
19
- parts: list[str] = []
20
- for segment_length in _AUTH_AK_SEGMENTS:
21
- parts.append("".join(secrets.choice(_AUTH_AK_CHARS) for _ in range(segment_length)))
22
- return "-".join(parts)
23
-
24
-
25
- def lingzhu_local_host(config: dict[str, Any] | None) -> str:
26
- value = str((config or {}).get("local_host") or DEFAULT_LINGZHU_LOCAL_HOST).strip()
27
- return value or DEFAULT_LINGZHU_LOCAL_HOST
28
-
29
-
30
- def lingzhu_gateway_port(config: dict[str, Any] | None) -> int:
31
- raw = (config or {}).get("gateway_port")
32
- try:
33
- value = int(raw)
34
- except (TypeError, ValueError):
35
- return DEFAULT_LINGZHU_GATEWAY_PORT
36
- if value < 1 or value > 65535:
37
- return DEFAULT_LINGZHU_GATEWAY_PORT
38
- return value
39
-
40
-
41
- def normalize_public_base_url(value: Any) -> str | None:
42
- text = str(value or "").strip()
43
- if not text:
44
- return None
45
- parsed = urlparse(text)
46
- if parsed.scheme not in {"http", "https"} or not parsed.netloc:
47
- return None
48
- return text.rstrip("/")
49
-
50
-
51
- def lingzhu_local_base_url(config: dict[str, Any] | None) -> str:
52
- return f"http://{lingzhu_local_host(config)}:{lingzhu_gateway_port(config)}"
53
-
54
-
55
- def lingzhu_public_base_url(config: dict[str, Any] | None) -> str | None:
56
- return normalize_public_base_url((config or {}).get("public_base_url"))
57
-
58
-
59
- def lingzhu_health_url(config: dict[str, Any] | None, *, public: bool = False) -> str | None:
60
- base = lingzhu_public_base_url(config) if public else lingzhu_local_base_url(config)
61
- if not base:
62
- return None
63
- return f"{base}/metis/agent/api/health"
64
-
65
-
66
- def lingzhu_sse_url(config: dict[str, Any] | None, *, public: bool = False) -> str | None:
67
- base = lingzhu_public_base_url(config) if public else lingzhu_local_base_url(config)
68
- if not base:
69
- return None
70
- return f"{base}/metis/agent/api/sse"
71
-
72
-
73
- def lingzhu_agent_id(config: dict[str, Any] | None) -> str:
74
- value = str((config or {}).get("agent_id") or DEFAULT_LINGZHU_AGENT_ID).strip()
75
- return value or DEFAULT_LINGZHU_AGENT_ID
76
-
77
-
78
- def lingzhu_probe_payload(
79
- config: dict[str, Any] | None,
80
- *,
81
- message_id: str = "ds-lingzhu-probe-001",
82
- text: str = "你好",
83
- ) -> dict[str, Any]:
84
- return {
85
- "message_id": message_id,
86
- "agent_id": lingzhu_agent_id(config),
87
- "message": [
88
- {
89
- "role": "user",
90
- "type": "text",
91
- "text": text,
92
- }
93
- ],
94
- }
95
-
96
-
97
- def lingzhu_generated_openclaw_config(config: dict[str, Any] | None) -> dict[str, Any]:
98
- resolved = dict(config or {})
99
- return {
100
- "gateway": {
101
- "port": lingzhu_gateway_port(resolved),
102
- "http": {
103
- "endpoints": {
104
- "chatCompletions": {
105
- "enabled": True,
106
- }
107
- }
108
- },
109
- },
110
- "plugins": {
111
- "entries": {
112
- "lingzhu": {
113
- "enabled": bool(resolved.get("enabled", False)),
114
- "config": {
115
- "authAk": str(resolved.get("auth_ak") or "").strip(),
116
- "agentId": lingzhu_agent_id(resolved),
117
- "includeMetadata": bool(resolved.get("include_metadata", True)),
118
- "requestTimeoutMs": int(resolved.get("request_timeout_ms") or 60000),
119
- "systemPrompt": str(resolved.get("system_prompt") or ""),
120
- "defaultNavigationMode": str(resolved.get("default_navigation_mode") or "0"),
121
- "enableFollowUp": bool(resolved.get("enable_follow_up", True)),
122
- "followUpMaxCount": int(resolved.get("follow_up_max_count") or 3),
123
- "maxImageBytes": int(resolved.get("max_image_bytes") or 5 * 1024 * 1024),
124
- "sessionMode": str(resolved.get("session_mode") or "per_user"),
125
- "sessionNamespace": str(
126
- resolved.get("session_namespace") or DEFAULT_LINGZHU_SESSION_NAMESPACE
127
- ),
128
- "autoReceiptAck": bool(resolved.get("auto_receipt_ack", True)),
129
- "visibleProgressHeartbeat": bool(
130
- resolved.get("visible_progress_heartbeat", True)
131
- ),
132
- "visibleProgressHeartbeatSec": int(
133
- resolved.get("visible_progress_heartbeat_sec") or 10
134
- ),
135
- "debugLogging": bool(resolved.get("debug_logging", False)),
136
- "debugLogPayloads": bool(resolved.get("debug_log_payloads", False)),
137
- "debugLogDir": str(resolved.get("debug_log_dir") or ""),
138
- "enableExperimentalNativeActions": bool(
139
- resolved.get("enable_experimental_native_actions", False)
140
- ),
141
- },
142
- }
143
- }
144
- },
145
- }
146
-
147
-
148
- def lingzhu_generated_openclaw_config_text(config: dict[str, Any] | None) -> str:
149
- return json.dumps(lingzhu_generated_openclaw_config(config), indent=2, ensure_ascii=False)
150
-
151
-
152
- def lingzhu_generated_curl(config: dict[str, Any] | None, *, text: str = "你好") -> str:
153
- auth_ak = str((config or {}).get("auth_ak") or "").strip()
154
- payload = lingzhu_probe_payload(config, text=text)
155
- endpoint_url = lingzhu_sse_url(config) or ""
156
- return (
157
- f"curl -X POST '{endpoint_url}' \\\n"
158
- f" --header 'Authorization: Bearer {auth_ak}' \\\n"
159
- " --header 'Content-Type: application/json' \\\n"
160
- f" --data '{json.dumps(payload, ensure_ascii=False)}'"
161
- )
162
-
163
-
164
- def lingzhu_supported_commands(*, experimental_enabled: bool) -> list[str]:
165
- commands = [
166
- "take_photo",
167
- "take_navigation",
168
- "control_calendar",
169
- "notify_agent_off",
170
- ]
171
- if experimental_enabled:
172
- commands.extend(
173
- [
174
- "send_notification",
175
- "send_toast",
176
- "speak_tts",
177
- "start_video_record",
178
- "stop_video_record",
179
- "open_custom_view",
180
- ]
181
- )
182
- return commands
1
+ from .connector.lingzhu_support import * # noqa: F401,F403
@@ -680,16 +680,23 @@ def build_artifact_server(context: McpContext) -> FastMCP:
680
680
  @server.tool(
681
681
  name="arxiv",
682
682
  description=(
683
- "Read an identified arXiv paper by id. "
684
- "Use full_text=false for the overview/abstract path and full_text=true when the full paper body is needed."
683
+ "Interact with the quest-local arXiv library. "
684
+ "Use mode='read' to read one paper by id with local-first automatic persistence, "
685
+ "or mode='list' to list the saved arXiv items for the current quest."
685
686
  ),
686
687
  )
687
688
  def arxiv(
688
- paper_id: str,
689
+ paper_id: str | None = None,
690
+ mode: str = "read",
689
691
  full_text: bool = False,
690
692
  comment: str | dict[str, Any] | None = None,
691
693
  ) -> dict[str, Any]:
692
- return service.arxiv(paper_id, full_text=full_text)
694
+ return service.arxiv(
695
+ paper_id,
696
+ mode=mode,
697
+ full_text=full_text,
698
+ quest_root=context.require_quest_root(),
699
+ )
693
700
 
694
701
  @server.tool(name="refresh_summary", description="Refresh SUMMARY.md from recent artifact state.")
695
702
  def refresh_summary(
@@ -283,6 +283,21 @@ class PromptBuilder:
283
283
  "- qq_structured_delivery_rule: when you want native QQ markdown or native QQ image/file delivery, request it through artifact.interact(connector_hints=..., attachments=[...]) instead of inventing connector-specific inline tag syntax.",
284
284
  ]
285
285
  )
286
+ elif connector == "weixin":
287
+ lines.extend(
288
+ [
289
+ "- weixin_surface_rule: Weixin is a concise operator surface, not a full artifact browser.",
290
+ "- weixin_default_mode: keep outbound replies concise, respectful, text-first, and progress-aware.",
291
+ "- weixin_length_rule: for ordinary Weixin progress replies, normally use only 2 to 4 short sentences, or 3 very short bullets at most.",
292
+ "- weixin_summary_first_rule: start with the user-facing conclusion, then the immediate meaning, then the next action.",
293
+ "- weixin_progress_shape_rule: make the current task, the main difficulty or latest real progress, and the next concrete next step explicit whenever possible.",
294
+ "- weixin_eta_rule: for important long-running phases, include a rough ETA or next check-in window when it is helpful and defensible.",
295
+ "- weixin_internal_detail_rule: do not proactively dump file inventories, path lists, retry counters, or monitor-log style telemetry unless the user asked for them or they explain a real risk.",
296
+ "- weixin_context_token_rule: reply continuity is managed by the runtime through `context_token`; do not invent your own reply token scheme.",
297
+ "- weixin_media_rule: when you want native Weixin image, video, or file delivery, request it through artifact.interact(..., attachments=[...]) with `connector_delivery={'weixin': {'media_kind': ...}}` instead of inventing connector-specific inline tag syntax.",
298
+ "- weixin_inbound_media_rule: inbound Weixin image, video, and file messages can arrive as quest-local attachments under `userfiles/weixin/...`; read those files when the user sent media.",
299
+ ]
300
+ )
286
301
  else:
287
302
  lines.append("- connector_media_rule: if the active surface is not QQ, keep using the general artifact interaction discipline for milestone delivery.")
288
303
 
@@ -1,196 +1 @@
1
- from __future__ import annotations
2
-
3
- from copy import deepcopy
4
- from typing import Any
5
-
6
- from .shared import slugify
7
-
8
-
9
- QQ_PROFILE_ID_PREFIX = "qq-profile"
10
- def default_qq_profile() -> dict[str, Any]:
11
- return {
12
- "profile_id": None,
13
- "enabled": True,
14
- "app_id": None,
15
- "app_secret": None,
16
- "app_secret_env": None,
17
- "bot_name": "DeepScientist",
18
- "main_chat_id": None,
19
- }
20
-
21
-
22
- def _as_text(value: Any) -> str | None:
23
- text = str(value or "").strip()
24
- return text or None
25
-
26
-
27
- def _normalize_secret_pair(payload: dict[str, Any], direct_key: str, env_key: str) -> None:
28
- direct = _as_text(payload.get(direct_key))
29
- env_name = _as_text(payload.get(env_key))
30
- payload[direct_key] = direct
31
- payload[env_key] = None if direct else env_name
32
-
33
-
34
- def _profile_id_seed(*, profile_id: Any, app_id: Any, bot_name: Any, index: int) -> str:
35
- explicit = _as_text(profile_id)
36
- if explicit:
37
- return explicit
38
- app_text = _as_text(app_id)
39
- if app_text:
40
- return f"qq-{app_text}"
41
- bot_text = slugify(str(bot_name or "").strip(), default="")
42
- if bot_text:
43
- return f"{QQ_PROFILE_ID_PREFIX}-{bot_text}"
44
- return f"{QQ_PROFILE_ID_PREFIX}-{index:03d}"
45
-
46
-
47
- def _unique_profile_id(seed: str, *, used: set[str]) -> str:
48
- base = slugify(seed, default=QQ_PROFILE_ID_PREFIX)
49
- candidate = base
50
- suffix = 2
51
- while candidate in used:
52
- candidate = f"{base}-{suffix}"
53
- suffix += 1
54
- used.add(candidate)
55
- return candidate
56
-
57
-
58
- def list_qq_profiles(config: dict[str, Any] | None) -> list[dict[str, Any]]:
59
- normalized = normalize_qq_connector_config(config)
60
- profiles = normalized.get("profiles")
61
- return [dict(item) for item in profiles] if isinstance(profiles, list) else []
62
-
63
-
64
- def find_qq_profile(
65
- config: dict[str, Any] | None,
66
- *,
67
- profile_id: str | None = None,
68
- app_id: str | None = None,
69
- ) -> dict[str, Any] | None:
70
- normalized_profile_id = _as_text(profile_id)
71
- normalized_app_id = _as_text(app_id)
72
- for profile in list_qq_profiles(config):
73
- if normalized_profile_id and str(profile.get("profile_id") or "").strip() == normalized_profile_id:
74
- return profile
75
- if normalized_app_id and str(profile.get("app_id") or "").strip() == normalized_app_id:
76
- return profile
77
- return None
78
-
79
-
80
- def merge_qq_profile_config(shared_config: dict[str, Any] | None, profile: dict[str, Any]) -> dict[str, Any]:
81
- normalized = normalize_qq_connector_config(shared_config)
82
- merged = deepcopy(normalized)
83
- merged.pop("profiles", None)
84
- app_secret = _as_text(profile.get("app_secret"))
85
- app_secret_env = _as_text(profile.get("app_secret_env"))
86
- merged.update(
87
- {
88
- "profile_id": str(profile.get("profile_id") or "").strip() or None,
89
- "app_id": _as_text(profile.get("app_id")),
90
- "app_secret": app_secret,
91
- "app_secret_env": None if app_secret else app_secret_env,
92
- "bot_name": _as_text(profile.get("bot_name")) or str(normalized.get("bot_name") or "DeepScientist"),
93
- "main_chat_id": _as_text(profile.get("main_chat_id")),
94
- "enabled": bool(normalized.get("enabled", False)) and bool(profile.get("enabled", True)),
95
- "transport": "gateway_direct",
96
- }
97
- )
98
- return merged
99
-
100
-
101
- def qq_profile_label(profile: dict[str, Any] | None) -> str:
102
- if not isinstance(profile, dict):
103
- return "QQ"
104
- bot_name = _as_text(profile.get("bot_name"))
105
- app_id = _as_text(profile.get("app_id"))
106
- if bot_name and app_id:
107
- return f"{bot_name} · {app_id}"
108
- if bot_name:
109
- return bot_name
110
- if app_id:
111
- return f"QQ · {app_id}"
112
- return "QQ"
113
-
114
-
115
- def normalize_qq_connector_config(config: dict[str, Any] | None) -> dict[str, Any]:
116
- payload = deepcopy(config or {})
117
- shared_defaults = {
118
- "enabled": False,
119
- "transport": "gateway_direct",
120
- "app_id": None,
121
- "app_secret": None,
122
- "app_secret_env": None,
123
- "bot_name": "DeepScientist",
124
- "command_prefix": "/",
125
- "main_chat_id": None,
126
- "require_at_in_groups": True,
127
- "auto_bind_dm_to_active_quest": True,
128
- "gateway_restart_on_config_change": True,
129
- "auto_send_main_experiment_png": True,
130
- "auto_send_analysis_summary_png": True,
131
- "auto_send_slice_png": True,
132
- "auto_send_paper_pdf": True,
133
- "enable_markdown_send": False,
134
- "enable_file_upload_experimental": False,
135
- "profiles": [],
136
- }
137
- shared = {**shared_defaults, **payload}
138
- shared["transport"] = "gateway_direct"
139
- shared["command_prefix"] = _as_text(shared.get("command_prefix")) or "/"
140
- shared["bot_name"] = _as_text(shared.get("bot_name")) or "DeepScientist"
141
- _normalize_secret_pair(shared, "app_secret", "app_secret_env")
142
-
143
- raw_profiles = payload.get("profiles")
144
- items = list(raw_profiles) if isinstance(raw_profiles, list) else []
145
- legacy_profile_seed = {
146
- "app_id": payload.get("app_id"),
147
- "app_secret": payload.get("app_secret"),
148
- "app_secret_env": payload.get("app_secret_env"),
149
- "bot_name": payload.get("bot_name"),
150
- "main_chat_id": payload.get("main_chat_id"),
151
- }
152
- if not items:
153
- has_direct_profile_seed = any(_as_text(legacy_profile_seed.get(key)) for key in ("app_id", "app_secret", "main_chat_id"))
154
- has_env_profile_seed = bool(payload.get("enabled")) and bool(_as_text(legacy_profile_seed.get("app_secret_env")))
155
- if has_direct_profile_seed or has_env_profile_seed:
156
- items = [legacy_profile_seed]
157
-
158
- profiles: list[dict[str, Any]] = []
159
- used_ids: set[str] = set()
160
- for index, raw in enumerate(items, start=1):
161
- if not isinstance(raw, dict):
162
- continue
163
- current = {**default_qq_profile(), **raw}
164
- current["enabled"] = bool(current.get("enabled", True))
165
- current["app_id"] = _as_text(current.get("app_id"))
166
- current["app_secret"] = _as_text(current.get("app_secret"))
167
- current["app_secret_env"] = _as_text(current.get("app_secret_env")) or shared["app_secret_env"]
168
- _normalize_secret_pair(current, "app_secret", "app_secret_env")
169
- current["bot_name"] = _as_text(current.get("bot_name")) or shared["bot_name"]
170
- current["main_chat_id"] = _as_text(current.get("main_chat_id"))
171
- current["profile_id"] = _unique_profile_id(
172
- _profile_id_seed(
173
- profile_id=current.get("profile_id"),
174
- app_id=current.get("app_id"),
175
- bot_name=current.get("bot_name"),
176
- index=index,
177
- ),
178
- used=used_ids,
179
- )
180
- profiles.append(current)
181
-
182
- shared["profiles"] = profiles
183
- if len(profiles) == 1:
184
- mirror = profiles[0]
185
- shared["app_id"] = mirror.get("app_id")
186
- shared["app_secret"] = mirror.get("app_secret")
187
- shared["app_secret_env"] = mirror.get("app_secret_env")
188
- shared["bot_name"] = mirror.get("bot_name")
189
- shared["main_chat_id"] = mirror.get("main_chat_id")
190
- else:
191
- shared["app_id"] = None
192
- shared["app_secret"] = None
193
- shared["app_secret_env"] = None
194
- shared["main_chat_id"] = None
195
-
196
- return shared
1
+ from .connector.qq_profiles import * # noqa: F401,F403