@researai/deepscientist 1.5.8 → 1.5.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/LICENSE +186 -21
  2. package/README.md +108 -95
  3. package/assets/branding/connector-qq.png +0 -0
  4. package/assets/branding/connector-rokid.png +0 -0
  5. package/assets/branding/connector-weixin.png +0 -0
  6. package/assets/branding/projects.png +0 -0
  7. package/bin/ds.js +172 -13
  8. package/docs/assets/branding/projects.png +0 -0
  9. package/docs/en/00_QUICK_START.md +308 -70
  10. package/docs/en/01_SETTINGS_REFERENCE.md +3 -0
  11. package/docs/en/02_START_RESEARCH_GUIDE.md +112 -0
  12. package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +62 -179
  13. package/docs/en/09_DOCTOR.md +41 -5
  14. package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +137 -0
  15. package/docs/en/11_LICENSE_AND_RISK.md +256 -0
  16. package/docs/en/12_GUIDED_WORKFLOW_TOUR.md +427 -0
  17. package/docs/en/13_CORE_ARCHITECTURE_GUIDE.md +297 -0
  18. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
  19. package/docs/en/99_ACKNOWLEDGEMENTS.md +4 -1
  20. package/docs/en/README.md +79 -0
  21. package/docs/images/lingzhu/rokid-agent-platform-create.png +0 -0
  22. package/docs/images/weixin/weixin-plugin-entry.png +0 -0
  23. package/docs/images/weixin/weixin-plugin-entry.svg +33 -0
  24. package/docs/images/weixin/weixin-qr-confirm.svg +30 -0
  25. package/docs/images/weixin/weixin-quest-media-flow.svg +44 -0
  26. package/docs/images/weixin/weixin-settings-bind.svg +57 -0
  27. package/docs/zh/00_QUICK_START.md +315 -74
  28. package/docs/zh/01_SETTINGS_REFERENCE.md +3 -0
  29. package/docs/zh/02_START_RESEARCH_GUIDE.md +112 -0
  30. package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +62 -193
  31. package/docs/zh/09_DOCTOR.md +41 -5
  32. package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +144 -0
  33. package/docs/zh/11_LICENSE_AND_RISK.md +256 -0
  34. package/docs/zh/12_GUIDED_WORKFLOW_TOUR.md +423 -0
  35. package/docs/zh/13_CORE_ARCHITECTURE_GUIDE.md +296 -0
  36. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
  37. package/docs/zh/99_ACKNOWLEDGEMENTS.md +4 -1
  38. package/docs/zh/README.md +126 -0
  39. package/install.sh +0 -34
  40. package/package.json +3 -3
  41. package/pyproject.toml +2 -2
  42. package/src/deepscientist/__init__.py +1 -1
  43. package/src/deepscientist/annotations.py +343 -0
  44. package/src/deepscientist/artifact/arxiv.py +484 -37
  45. package/src/deepscientist/artifact/metrics.py +1 -3
  46. package/src/deepscientist/artifact/service.py +1347 -111
  47. package/src/deepscientist/arxiv_library.py +275 -0
  48. package/src/deepscientist/bash_exec/service.py +9 -0
  49. package/src/deepscientist/bridges/builtins.py +2 -0
  50. package/src/deepscientist/bridges/connectors.py +447 -0
  51. package/src/deepscientist/channels/__init__.py +2 -0
  52. package/src/deepscientist/channels/builtins.py +3 -1
  53. package/src/deepscientist/channels/qq.py +1 -1
  54. package/src/deepscientist/channels/qq_gateway.py +1 -1
  55. package/src/deepscientist/channels/relay.py +7 -1
  56. package/src/deepscientist/channels/weixin.py +59 -0
  57. package/src/deepscientist/channels/weixin_ilink.py +317 -0
  58. package/src/deepscientist/config/models.py +22 -2
  59. package/src/deepscientist/config/service.py +431 -60
  60. package/src/deepscientist/connector/__init__.py +4 -0
  61. package/src/deepscientist/connector/connector_profiles.py +481 -0
  62. package/src/deepscientist/connector/lingzhu_support.py +668 -0
  63. package/src/deepscientist/connector/qq_profiles.py +206 -0
  64. package/src/deepscientist/connector/weixin_support.py +663 -0
  65. package/src/deepscientist/connector_profiles.py +1 -374
  66. package/src/deepscientist/connector_runtime.py +2 -0
  67. package/src/deepscientist/daemon/api/handlers.py +295 -5
  68. package/src/deepscientist/daemon/api/router.py +16 -1
  69. package/src/deepscientist/daemon/app.py +1130 -61
  70. package/src/deepscientist/doctor.py +5 -2
  71. package/src/deepscientist/gitops/diff.py +120 -29
  72. package/src/deepscientist/lingzhu_support.py +1 -182
  73. package/src/deepscientist/mcp/server.py +14 -5
  74. package/src/deepscientist/prompts/builder.py +29 -1
  75. package/src/deepscientist/qq_profiles.py +1 -196
  76. package/src/deepscientist/quest/node_traces.py +152 -2
  77. package/src/deepscientist/quest/service.py +169 -43
  78. package/src/deepscientist/quest/stage_views.py +172 -9
  79. package/src/deepscientist/registries/baseline.py +56 -4
  80. package/src/deepscientist/runners/codex.py +55 -3
  81. package/src/deepscientist/weixin_support.py +1 -0
  82. package/src/prompts/connectors/lingzhu.md +3 -1
  83. package/src/prompts/connectors/weixin.md +230 -0
  84. package/src/prompts/system.md +9 -0
  85. package/src/skills/idea/SKILL.md +16 -0
  86. package/src/skills/idea/references/literature-survey-template.md +24 -0
  87. package/src/skills/idea/references/related-work-playbook.md +4 -0
  88. package/src/skills/idea/references/selection-gate.md +9 -0
  89. package/src/skills/write/SKILL.md +1 -1
  90. package/src/tui/package.json +1 -1
  91. package/src/ui/dist/assets/{AiManusChatView-m2FNtwbn.js → AiManusChatView-D0mTXG4-.js} +156 -48
  92. package/src/ui/dist/assets/{AnalysisPlugin-BMTF8EGL.js → AnalysisPlugin-Db0cTXxm.js} +1 -1
  93. package/src/ui/dist/assets/{CliPlugin-BEOWgxCI.js → CliPlugin-DrV8je02.js} +164 -9
  94. package/src/ui/dist/assets/{CodeEditorPlugin-BCXvjqmb.js → CodeEditorPlugin-QXMSCH71.js} +8 -8
  95. package/src/ui/dist/assets/{CodeViewerPlugin-DaJcy3nD.js → CodeViewerPlugin-7hhtWj_E.js} +5 -5
  96. package/src/ui/dist/assets/{DocViewerPlugin-ByfeIq4K.js → DocViewerPlugin-BWMSnRJe.js} +3 -3
  97. package/src/ui/dist/assets/{GitDiffViewerPlugin-Cksf3VZ-.js → GitDiffViewerPlugin-7J9h9Vy_.js} +20 -21
  98. package/src/ui/dist/assets/{ImageViewerPlugin-CFz-OsTS.js → ImageViewerPlugin-CHJl_0lr.js} +5 -5
  99. package/src/ui/dist/assets/{LabCopilotPanel-CJ1cJzoX.js → LabCopilotPanel-1qSow1es.js} +11 -11
  100. package/src/ui/dist/assets/{LabPlugin-BF3dVJwa.js → LabPlugin-eQpPPCEp.js} +2 -1
  101. package/src/ui/dist/assets/{LatexPlugin-DDkwZ6Sj.js → LatexPlugin-BwRfi89Z.js} +7 -7
  102. package/src/ui/dist/assets/{MarkdownViewerPlugin-HAuvurcT.js → MarkdownViewerPlugin-836PVQWV.js} +4 -4
  103. package/src/ui/dist/assets/{MarketplacePlugin-BtoTYy2C.js → MarketplacePlugin-C2y_556i.js} +3 -3
  104. package/src/ui/dist/assets/{NotebookEditor-CSJYx7b-.js → NotebookEditor-BRzJbGsn.js} +12 -12
  105. package/src/ui/dist/assets/{NotebookEditor-DQgRezm_.js → NotebookEditor-DIX7Mlzu.js} +1 -1
  106. package/src/ui/dist/assets/{PdfLoader-DPa_-fv6.js → PdfLoader-DzRaTAlq.js} +14 -7
  107. package/src/ui/dist/assets/{PdfMarkdownPlugin-BZpXOEjm.js → PdfMarkdownPlugin-DZUfIUnp.js} +73 -6
  108. package/src/ui/dist/assets/{PdfViewerPlugin-BT8a6wGR.js → PdfViewerPlugin-BwtICzue.js} +103 -34
  109. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +3627 -0
  110. package/src/ui/dist/assets/{SearchPlugin-D_blveZi.js → SearchPlugin-DHeIAMsx.js} +1 -1
  111. package/src/ui/dist/assets/{TextViewerPlugin-Btx0M3hX.js → TextViewerPlugin-C3tCmFox.js} +5 -4
  112. package/src/ui/dist/assets/{VNCViewer-DImJO4rO.js → VNCViewer-CQsKVm3t.js} +10 -10
  113. package/src/ui/dist/assets/bot-BEA2vWuK.js +21 -0
  114. package/src/ui/dist/assets/branding/logo-rokid.png +0 -0
  115. package/src/ui/dist/assets/browser-BAcuE0Xj.js +2895 -0
  116. package/src/ui/dist/assets/{code-BUfXGJSl.js → code-XfbSR8K2.js} +1 -1
  117. package/src/ui/dist/assets/{file-content-VqamwI3X.js → file-content-BjxNaIfy.js} +1 -1
  118. package/src/ui/dist/assets/{file-diff-panel-C_wOoS7a.js → file-diff-panel-D_lLVQk0.js} +1 -1
  119. package/src/ui/dist/assets/{file-socket-D2bTuMVP.js → file-socket-D9x_5vlY.js} +1 -1
  120. package/src/ui/dist/assets/{image-BZkGJ4mM.js → image-BhWT33W1.js} +1 -1
  121. package/src/ui/dist/assets/{index-DdRW6RMJ.js → index--c4iXtuy.js} +12 -12
  122. package/src/ui/dist/assets/{index-CxkvSeKw.js → index-BDxipwrC.js} +2 -2
  123. package/src/ui/dist/assets/{index-DjggJovS.js → index-DZTZ8mWP.js} +14934 -9613
  124. package/src/ui/dist/assets/{index-DXZ1daiJ.css → index-Dqj-Mjb4.css} +2 -13
  125. package/src/ui/dist/assets/index-PJbSbPTy.js +25 -0
  126. package/src/ui/dist/assets/{monaco-DHMc7kKM.js → monaco-K8izTGgo.js} +1 -1
  127. package/src/ui/dist/assets/{pdf-effect-queue-DSw_D3RV.js → pdf-effect-queue-DfBors6y.js} +16 -1
  128. package/src/ui/dist/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
  129. package/src/ui/dist/assets/{popover-B85oCgCS.js → popover-yFK1J4fL.js} +1 -1
  130. package/src/ui/dist/assets/{project-sync-DOMCcPac.js → project-sync-PENr2zcz.js} +1 -74
  131. package/src/ui/dist/assets/select-CAbJDfYv.js +1690 -0
  132. package/src/ui/dist/assets/{sigma-BO2rQrl3.js → sigma-DEuYJqTl.js} +1 -1
  133. package/src/ui/dist/assets/{index-D9QIGcmc.js → square-check-big-omoSUmcd.js} +2 -13
  134. package/src/ui/dist/assets/{trash-BsVEH_dV.js → trash--F119N47.js} +1 -1
  135. package/src/ui/dist/assets/{useCliAccess-b8L6JuZm.js → useCliAccess-D31UR23I.js} +1 -1
  136. package/src/ui/dist/assets/{useFileDiffOverlay-BY7uA9hV.js → useFileDiffOverlay-BH6KcMzq.js} +1 -1
  137. package/src/ui/dist/assets/{wrap-text-BwyVuUIK.js → wrap-text-CZ613PM5.js} +1 -1
  138. package/src/ui/dist/assets/{zoom-out-RDpLugQP.js → zoom-out-BgDLAv3z.js} +1 -1
  139. package/src/ui/dist/index.html +2 -2
  140. package/src/ui/dist/assets/AutoFigurePlugin-BGxN8Umr.css +0 -3056
  141. package/src/ui/dist/assets/AutoFigurePlugin-DxPdMUNb.js +0 -8149
  142. package/src/ui/dist/assets/PdfViewerPlugin-BJXtIwj_.css +0 -260
  143. package/src/ui/dist/assets/Stepper-DH2k75Vo.js +0 -158
  144. package/src/ui/dist/assets/bibtex-B-Hqu0Sg.js +0 -189
  145. package/src/ui/dist/assets/file-utils--zJCPN1i.js +0 -109
  146. package/src/ui/dist/assets/message-square-FUIPIhU2.js +0 -16
  147. package/src/ui/dist/assets/pdfjs-DU1YE8WO.js +0 -3
  148. package/src/ui/dist/assets/tooltip-B1OspAkx.js +0 -108
@@ -77,7 +77,6 @@ class ApiHandlers:
77
77
  "productApis": False,
78
78
  "socketIo": False,
79
79
  "notifications": False,
80
- "broadcasts": False,
81
80
  "points": False,
82
81
  "arxiv": True,
83
82
  "cliFrontend": False,
@@ -208,9 +207,31 @@ npm --prefix src/ui run build</pre>
208
207
  def connectors_availability(self) -> dict:
209
208
  return self.app.connector_availability_summary()
210
209
 
210
+ def weixin_login_qr_start(self, body: dict | None = None) -> dict:
211
+ payload = body if isinstance(body, dict) else {}
212
+ return self.app.start_weixin_login_qr(force=bool(payload.get("force")))
213
+
214
+ def weixin_login_qr_wait(self, body: dict | None = None) -> dict:
215
+ payload = body if isinstance(body, dict) else {}
216
+ return self.app.wait_weixin_login_qr(
217
+ session_key=str(payload.get("session_key") or "").strip(),
218
+ timeout_ms=int(payload.get("timeout_ms") or 1_500),
219
+ )
220
+
221
+ def lingzhu_health(self) -> dict:
222
+ return self.app.lingzhu_health_payload()
223
+
211
224
  def baselines(self) -> list[dict]:
212
225
  return self.app.artifact_service.baselines.list_entries()
213
226
 
227
+ def baseline_delete(self, baseline_id: str) -> dict | tuple[int, dict]:
228
+ try:
229
+ return self.app.artifact_service.delete_baseline(baseline_id)
230
+ except FileNotFoundError as exc:
231
+ return 404, {"ok": False, "message": str(exc), "baseline_id": baseline_id}
232
+ except ValueError as exc:
233
+ return 400, {"ok": False, "message": str(exc), "baseline_id": baseline_id}
234
+
214
235
  def qq_bindings(self) -> list[dict]:
215
236
  return self.app.list_qq_bindings()
216
237
 
@@ -257,6 +278,18 @@ npm --prefix src/ui run build</pre>
257
278
  "no",
258
279
  "off",
259
280
  }
281
+ auto_bind_latest_connectors_raw = body.get("auto_bind_latest_connectors")
282
+ if auto_bind_latest_connectors_raw is None:
283
+ auto_bind_latest_connectors = True
284
+ else:
285
+ auto_bind_latest_connectors = bool(auto_bind_latest_connectors_raw) and str(
286
+ auto_bind_latest_connectors_raw
287
+ ).strip().lower() not in {
288
+ "0",
289
+ "false",
290
+ "no",
291
+ "off",
292
+ }
260
293
  requested_baseline_ref = body.get("requested_baseline_ref")
261
294
  startup_contract = body.get("startup_contract")
262
295
  auto_start = body.get("auto_start") is True
@@ -281,6 +314,7 @@ npm --prefix src/ui run build</pre>
281
314
  preferred_connector_conversation_id=preferred_connector_conversation_id,
282
315
  requested_connector_bindings=requested_connector_bindings,
283
316
  force_connector_rebind=force_connector_rebind,
317
+ auto_bind_latest_connectors=auto_bind_latest_connectors,
284
318
  requested_baseline_ref=requested_baseline_ref if isinstance(requested_baseline_ref, dict) else None,
285
319
  startup_contract=startup_contract if isinstance(startup_contract, dict) else None,
286
320
  )
@@ -408,6 +442,7 @@ npm --prefix src/ui run build</pre>
408
442
  }
409
443
 
410
444
  def quest_bindings(self, quest_id: str, body: dict) -> dict | tuple[int, dict]:
445
+ previous_external = self.app._quest_external_binding(quest_id)
411
446
  requested_bindings = (
412
447
  [dict(item) for item in body.get("bindings") if isinstance(item, dict)]
413
448
  if isinstance(body.get("bindings"), list)
@@ -418,10 +453,24 @@ npm --prefix src/ui run build</pre>
418
453
  force_raw = body.get("force")
419
454
  force = bool(force_raw) and str(force_raw).strip().lower() not in {"0", "false", "no", "off"}
420
455
  if requested_bindings:
421
- return self.app.update_quest_bindings(quest_id, requested_bindings, force=force)
422
- if connector_name:
423
- return self.app.update_quest_connector_binding(quest_id, connector_name, conversation_id, force=force)
424
- return self.app.update_quest_binding(quest_id, conversation_id, force=force)
456
+ result = self.app.update_quest_bindings(quest_id, requested_bindings, force=force)
457
+ elif connector_name:
458
+ result = self.app.update_quest_connector_binding(quest_id, connector_name, conversation_id, force=force)
459
+ else:
460
+ result = self.app.update_quest_binding(quest_id, conversation_id, force=force)
461
+ if isinstance(result, tuple):
462
+ return result
463
+ current_external = self.app._quest_external_binding(quest_id)
464
+ transition = self.app._binding_transition_summary(
465
+ quest_id=quest_id,
466
+ previous_conversation_id=previous_external,
467
+ current_conversation_id=current_external,
468
+ )
469
+ self.app._announce_binding_transition(transition, notify_new=True, notify_old=True)
470
+ return {
471
+ **result,
472
+ "binding_transition": transition,
473
+ }
425
474
 
426
475
  def quest_session(self, quest_id: str) -> dict:
427
476
  snapshot = self.app.quest_service.snapshot_fast(quest_id)
@@ -721,6 +770,28 @@ npm --prefix src/ui run build</pre>
721
770
  def workflow(self, quest_id: str) -> dict:
722
771
  return self.app.quest_service.workflow(quest_id)
723
772
 
773
+ def quest_layout(self, quest_id: str) -> dict:
774
+ quest_root = self._fresh_quest_service()._quest_root(quest_id)
775
+ payload = self.app.quest_service.read_lab_canvas_state(quest_root)
776
+ return {
777
+ "layout_json": payload.get("layout_json") if isinstance(payload.get("layout_json"), dict) else {},
778
+ "updated_at": payload.get("updated_at"),
779
+ }
780
+
781
+ def quest_layout_update(self, quest_id: str, body: dict) -> dict | tuple[int, dict]:
782
+ quest_root = self._fresh_quest_service()._quest_root(quest_id)
783
+ raw_layout = body.get("layout_json")
784
+ if raw_layout is not None and not isinstance(raw_layout, dict):
785
+ return 400, {"ok": False, "message": "`layout_json` must be an object."}
786
+ payload = self.app.quest_service.update_lab_canvas_state(
787
+ quest_root,
788
+ layout_json=dict(raw_layout or {}),
789
+ )
790
+ return {
791
+ "layout_json": payload.get("layout_json") if isinstance(payload.get("layout_json"), dict) else {},
792
+ "updated_at": payload.get("updated_at"),
793
+ }
794
+
724
795
  def node_traces(self, quest_id: str, path: str) -> dict:
725
796
  query = self.parse_query(path)
726
797
  selection_type = ((query.get("selection_type") or [""])[0] or "").strip() or None
@@ -754,6 +825,14 @@ npm --prefix src/ui run build</pre>
754
825
  payload["active_workspace_ref"] = active_workspace_branch
755
826
  payload["research_head_ref"] = research_head_branch
756
827
  payload["workspace_mode"] = str(research_state.get("workspace_mode") or "quest").strip() or "quest"
828
+ quest_data = self.app.quest_service.read_quest_yaml(quest_root)
829
+ active_anchor = str(quest_data.get("active_anchor") or "").strip().lower()
830
+ active_analysis_campaign_id = str(research_state.get("active_analysis_campaign_id") or "").strip() or None
831
+ current_workspace_branch = str(research_state.get("current_workspace_branch") or "").strip() or None
832
+ workspace_mode = str(research_state.get("workspace_mode") or "").strip().lower() or "quest"
833
+ paper_parent_branch = str(research_state.get("paper_parent_branch") or "").strip() or None
834
+ paper_parent_run_id = str(research_state.get("paper_parent_run_id") or "").strip() or None
835
+ next_pending_slice_id = str(research_state.get("next_pending_slice_id") or "").strip() or None
757
836
  try:
758
837
  branch_summary = self.app.artifact_service.list_research_branches(quest_root)
759
838
  except Exception:
@@ -763,6 +842,96 @@ npm --prefix src/ui run build</pre>
763
842
  for item in (branch_summary.get("branches") or [])
764
843
  if str(item.get("branch_name") or "").strip()
765
844
  }
845
+ active_campaign = {}
846
+ if active_analysis_campaign_id:
847
+ try:
848
+ active_campaign = self.app.artifact_service.get_analysis_campaign(
849
+ quest_root,
850
+ campaign_id=active_analysis_campaign_id,
851
+ )
852
+ except Exception:
853
+ active_campaign = {}
854
+ campaign_parent_branch = (
855
+ str(active_campaign.get("parent_branch") or "").strip() or None
856
+ if isinstance(active_campaign, dict)
857
+ else None
858
+ )
859
+ campaign_slices = [
860
+ dict(item)
861
+ for item in ((active_campaign or {}).get("slices") or [])
862
+ if isinstance(item, dict)
863
+ ]
864
+ campaign_total_slices = len(campaign_slices)
865
+ campaign_completed_slices = sum(
866
+ 1 for item in campaign_slices if str(item.get("status") or "").strip().lower() == "completed"
867
+ )
868
+ slice_by_branch = {
869
+ str(item.get("branch") or "").strip(): item
870
+ for item in campaign_slices
871
+ if str(item.get("branch") or "").strip()
872
+ }
873
+
874
+ def resolve_workflow_state(ref: str, summary: dict[str, object] | None) -> dict[str, object]:
875
+ branch_kind = self.app.artifact_service._branch_kind_from_name(ref)
876
+ has_main_result = bool((summary or {}).get("has_main_result"))
877
+ workflow_state: dict[str, object] = {
878
+ "analysis_state": "none",
879
+ "writing_state": "not_ready",
880
+ "analysis_campaign_id": active_analysis_campaign_id,
881
+ "total_slices": campaign_total_slices or None,
882
+ "completed_slices": campaign_completed_slices or None,
883
+ "next_pending_slice_id": next_pending_slice_id,
884
+ "paper_parent_branch": paper_parent_branch,
885
+ "paper_parent_run_id": paper_parent_run_id,
886
+ "status_reason": None,
887
+ }
888
+ if branch_kind == "analysis":
889
+ slice_entry = slice_by_branch.get(ref)
890
+ slice_status = str((slice_entry or {}).get("status") or "pending").strip().lower() or "pending"
891
+ if slice_status == "completed":
892
+ workflow_state["analysis_state"] = "completed"
893
+ workflow_state["status_reason"] = "Analysis slice completed."
894
+ elif ref == current_workspace_branch or str((slice_entry or {}).get("slice_id") or "").strip() == next_pending_slice_id:
895
+ workflow_state["analysis_state"] = "active"
896
+ workflow_state["status_reason"] = (
897
+ f"Analysis {campaign_completed_slices}/{campaign_total_slices} done"
898
+ if campaign_total_slices
899
+ else "Analysis slice active."
900
+ )
901
+ else:
902
+ workflow_state["analysis_state"] = "pending"
903
+ workflow_state["status_reason"] = "Analysis slice pending."
904
+ return workflow_state
905
+ if branch_kind == "paper":
906
+ if ref == current_workspace_branch and workspace_mode == "paper":
907
+ workflow_state["writing_state"] = "completed" if active_anchor == "finalize" else "active"
908
+ workflow_state["status_reason"] = (
909
+ "Writing finalized." if active_anchor == "finalize" else "Writing workspace active."
910
+ )
911
+ else:
912
+ workflow_state["writing_state"] = "ready"
913
+ workflow_state["status_reason"] = "Writing workspace prepared."
914
+ return workflow_state
915
+ if campaign_parent_branch and ref == campaign_parent_branch:
916
+ workflow_state["analysis_state"] = "completed" if next_pending_slice_id is None else "active"
917
+ if has_main_result:
918
+ workflow_state["writing_state"] = "ready" if next_pending_slice_id is None else "blocked_by_analysis"
919
+ workflow_state["status_reason"] = (
920
+ "Analysis complete. Ready for writing."
921
+ if next_pending_slice_id is None
922
+ else (
923
+ f"Analysis {campaign_completed_slices}/{campaign_total_slices} done"
924
+ + (f" · next: {next_pending_slice_id}" if next_pending_slice_id else "")
925
+ )
926
+ )
927
+ return workflow_state
928
+ if has_main_result:
929
+ workflow_state["writing_state"] = "ready"
930
+ workflow_state["status_reason"] = "Main experiment recorded. Ready for writing."
931
+ return workflow_state
932
+ workflow_state["status_reason"] = "Awaiting main experiment result."
933
+ return workflow_state
934
+
766
935
  for node in payload.get("nodes", []):
767
936
  ref = str(node.get("ref") or "").strip()
768
937
  if not ref:
@@ -770,6 +939,7 @@ npm --prefix src/ui run build</pre>
770
939
  summary = branch_summary_by_name.get(ref)
771
940
  node["active_workspace"] = ref == active_workspace_branch
772
941
  node["research_head"] = ref == research_head_branch
942
+ node["workflow_state"] = resolve_workflow_state(ref, summary if isinstance(summary, dict) else None)
773
943
  if not isinstance(summary, dict):
774
944
  continue
775
945
  node["branch_no"] = summary.get("branch_no")
@@ -784,6 +954,7 @@ npm --prefix src/ui run build</pre>
784
954
  node["idea_draft_path"] = summary.get("idea_draft_path")
785
955
  node["latest_main_experiment"] = summary.get("latest_main_experiment")
786
956
  node["experiment_count"] = summary.get("experiment_count")
957
+ node["has_main_result"] = summary.get("has_main_result")
787
958
  return payload
788
959
 
789
960
  def git_log(self, quest_id: str, path: str) -> dict:
@@ -983,6 +1154,125 @@ npm --prefix src/ui run build</pre>
983
1154
  def runs(self, quest_id: str) -> list[dict]:
984
1155
  return self.app.quest_service.snapshot(quest_id).get("recent_runs", [])
985
1156
 
1157
+ def arxiv_list(self, path: str = "") -> dict | tuple[int, dict]:
1158
+ query = self.parse_query(path)
1159
+ quest_id = ((query.get("project_id") or [""])[0] or "").strip()
1160
+ if not quest_id:
1161
+ return 400, {"ok": False, "message": "`project_id` is required."}
1162
+ quest_root = self.app.quest_service._quest_root(quest_id)
1163
+ return self.app.artifact_service.arxiv(mode="list", quest_root=quest_root)
1164
+
1165
+ def arxiv_import(self, body: dict | None = None) -> dict | tuple[int, dict]:
1166
+ body = body or {}
1167
+ quest_id = str(body.get("project_id") or "").strip()
1168
+ paper_id = str(body.get("arxiv_id") or "").strip()
1169
+ if not quest_id:
1170
+ return 400, {"ok": False, "message": "`project_id` is required."}
1171
+ if not paper_id:
1172
+ return 400, {"ok": False, "message": "`arxiv_id` is required."}
1173
+ quest_root = self.app.quest_service._quest_root(quest_id)
1174
+ result = self.app.artifact_service.arxiv(
1175
+ paper_id,
1176
+ mode="read",
1177
+ full_text=False,
1178
+ quest_root=quest_root,
1179
+ )
1180
+ if not result.get("ok"):
1181
+ return 400, result
1182
+ return {
1183
+ "status": str(result.get("status") or "processing"),
1184
+ "metadata_status": str(result.get("metadata_status") or ""),
1185
+ "metadata_pending": bool(result.get("metadata_pending")),
1186
+ "title": str(result.get("title") or ""),
1187
+ "message": str(result.get("message") or ""),
1188
+ "abs_url": str(result.get("abs_url") or ""),
1189
+ "file_id": str(result.get("file_id") or ""),
1190
+ "document_id": str(result.get("document_id") or ""),
1191
+ "arxiv_id": str(result.get("paper_id") or paper_id),
1192
+ }
1193
+
1194
+ def annotations_file(self, file_id: str, path: str = "") -> dict | tuple[int, dict]:
1195
+ try:
1196
+ return self.app.annotation_service.list_annotations(file_id)
1197
+ except FileNotFoundError as exc:
1198
+ return 404, {"ok": False, "message": str(exc), "file_id": file_id}
1199
+ except ValueError as exc:
1200
+ return 400, {"ok": False, "message": str(exc), "file_id": file_id}
1201
+
1202
+ def annotations_project(self, project_id: str, path: str = "") -> dict | tuple[int, dict]:
1203
+ query = self.parse_query(path)
1204
+ search_query = ((query.get("q") or [""])[0] or "").strip() or None
1205
+ color = ((query.get("color") or [""])[0] or "").strip() or None
1206
+ tag = ((query.get("tag") or [""])[0] or "").strip() or None
1207
+ page_raw = ((query.get("page") or [""])[0] or "").strip()
1208
+ limit_raw = ((query.get("limit") or ["100"])[0] or "100").strip()
1209
+ try:
1210
+ page = int(page_raw) if page_raw else None
1211
+ except ValueError:
1212
+ page = None
1213
+ try:
1214
+ limit = max(1, min(int(limit_raw), 500))
1215
+ except ValueError:
1216
+ limit = 100
1217
+ try:
1218
+ return self.app.annotation_service.search_annotations(
1219
+ project_id,
1220
+ query=search_query,
1221
+ color=color,
1222
+ tag=tag,
1223
+ page=page,
1224
+ limit=limit,
1225
+ )
1226
+ except FileNotFoundError as exc:
1227
+ return 404, {"ok": False, "message": str(exc), "project_id": project_id}
1228
+
1229
+ def annotation_create(self, body: dict) -> dict | tuple[int, dict]:
1230
+ file_id = str(body.get("file_id") or "").strip()
1231
+ if not file_id:
1232
+ return 400, {"ok": False, "message": "`file_id` is required."}
1233
+ try:
1234
+ return self.app.annotation_service.create_annotation(
1235
+ file_id=file_id,
1236
+ position=body.get("position"),
1237
+ content=body.get("content"),
1238
+ comment=body.get("comment"),
1239
+ kind=body.get("kind"),
1240
+ color=body.get("color"),
1241
+ tags=body.get("tags"),
1242
+ )
1243
+ except FileNotFoundError as exc:
1244
+ return 404, {"ok": False, "message": str(exc), "file_id": file_id}
1245
+ except ValueError as exc:
1246
+ return 400, {"ok": False, "message": str(exc), "file_id": file_id}
1247
+
1248
+ def annotation_detail(self, annotation_id: str) -> dict | tuple[int, dict]:
1249
+ try:
1250
+ return self.app.annotation_service.get_annotation(annotation_id)
1251
+ except FileNotFoundError as exc:
1252
+ return 404, {"ok": False, "message": str(exc), "annotation_id": annotation_id}
1253
+
1254
+ def annotation_update(self, annotation_id: str, body: dict) -> dict | tuple[int, dict]:
1255
+ try:
1256
+ return self.app.annotation_service.update_annotation(
1257
+ annotation_id,
1258
+ comment=body.get("comment") if "comment" in body else None,
1259
+ kind=body.get("kind") if "kind" in body else None,
1260
+ position=body.get("position") if "position" in body else None,
1261
+ content=body.get("content") if "content" in body else None,
1262
+ color=body.get("color") if "color" in body else None,
1263
+ tags=body.get("tags") if "tags" in body else None,
1264
+ )
1265
+ except FileNotFoundError as exc:
1266
+ return 404, {"ok": False, "message": str(exc), "annotation_id": annotation_id}
1267
+ except ValueError as exc:
1268
+ return 400, {"ok": False, "message": str(exc), "annotation_id": annotation_id}
1269
+
1270
+ def annotation_delete(self, annotation_id: str) -> dict | tuple[int, dict]:
1271
+ try:
1272
+ return self.app.annotation_service.delete_annotation(annotation_id)
1273
+ except FileNotFoundError as exc:
1274
+ return 404, {"ok": False, "message": str(exc), "annotation_id": annotation_id}
1275
+
986
1276
  def quest_memory(self, quest_id: str) -> list[dict]:
987
1277
  quest_service = self._fresh_quest_service()
988
1278
  return self._fresh_memory_service().list_cards(
@@ -6,7 +6,9 @@ import re
6
6
  ROUTES: list[tuple[str, re.Pattern[str], str]] = [
7
7
  ("GET", re.compile(r"^/$"), "root"),
8
8
  ("GET", re.compile(r"^/ui/(?P<ui_path>.+)$"), "ui_asset"),
9
- ("GET", re.compile(r"^/(?P<spa_path>(?!api(?:/|$)|ui(?:/|$)|assets(?:/|$)).+)$"), "spa_root"),
9
+ ("GET", re.compile(r"^/metis/agent/api/health$"), "lingzhu_health"),
10
+ ("POST", re.compile(r"^/metis/agent/api/sse$"), "lingzhu_sse"),
11
+ ("GET", re.compile(r"^/(?P<spa_path>(?!api(?:/|$)|metis(?:/|$)|ui(?:/|$)|assets(?:/|$)).+)$"), "spa_root"),
10
12
  ("GET", re.compile(r"^/api/health$"), "health"),
11
13
  ("GET", re.compile(r"^/api/system/update$"), "system_update"),
12
14
  ("POST", re.compile(r"^/api/system/update$"), "system_update_action"),
@@ -15,12 +17,15 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
15
17
  ("GET", re.compile(r"^/api/acp/status$"), "acp_status"),
16
18
  ("GET", re.compile(r"^/api/connectors$"), "connectors"),
17
19
  ("GET", re.compile(r"^/api/connectors/availability$"), "connectors_availability"),
20
+ ("POST", re.compile(r"^/api/connectors/weixin/login/qr/start$"), "weixin_login_qr_start"),
21
+ ("POST", re.compile(r"^/api/connectors/weixin/login/qr/wait$"), "weixin_login_qr_wait"),
18
22
  ("GET", re.compile(r"^/api/connectors/qq/bindings$"), "qq_bindings"),
19
23
  ("POST", re.compile(r"^/api/connectors/qq/inbound$"), "qq_inbound"),
20
24
  ("DELETE", re.compile(r"^/api/connectors/(?P<connector>[^/]+)/profiles/(?P<profile_id>[^/]+)$"), "connector_profile_delete"),
21
25
  ("GET", re.compile(r"^/api/connectors/(?P<connector>[^/]+)/bindings$"), "connector_bindings"),
22
26
  ("POST", re.compile(r"^/api/connectors/(?P<connector>[^/]+)/inbound$"), "connector_inbound"),
23
27
  ("GET", re.compile(r"^/api/baselines$"), "baselines"),
28
+ ("DELETE", re.compile(r"^/api/baselines/(?P<baseline_id>[^/]+)$"), "baseline_delete"),
24
29
  ("GET", re.compile(r"^/api/quests$"), "quests"),
25
30
  ("GET", re.compile(r"^/api/quest-id/next$"), "quest_next_id"),
26
31
  ("POST", re.compile(r"^/api/quests$"), "quest_create"),
@@ -35,6 +40,8 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
35
40
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/events$"), "quest_events"),
36
41
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/artifacts$"), "quest_artifacts"),
37
42
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/workflow$"), "workflow"),
43
+ ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/layout$"), "quest_layout"),
44
+ ("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/layout$"), "quest_layout_update"),
38
45
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/node-traces$"), "node_traces"),
39
46
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/node-traces/(?P<node_ref>.+)$"), "node_trace"),
40
47
  ("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/stage-view$"), "stage_view"),
@@ -74,6 +81,14 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
74
81
  ("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/commands$"), "command"),
75
82
  ("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/control$"), "quest_control"),
76
83
  ("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/runs$"), "run_create"),
84
+ ("GET", re.compile(r"^/api/v1/arxiv/list$"), "arxiv_list"),
85
+ ("POST", re.compile(r"^/api/v1/arxiv/import$"), "arxiv_import"),
86
+ ("GET", re.compile(r"^/api/v1/annotations/file/(?P<file_id>.+)$"), "annotations_file"),
87
+ ("GET", re.compile(r"^/api/v1/annotations/project/(?P<project_id>[^/]+)$"), "annotations_project"),
88
+ ("POST", re.compile(r"^/api/v1/annotations/?$"), "annotation_create"),
89
+ ("GET", re.compile(r"^/api/v1/annotations/(?P<annotation_id>[^/]+)$"), "annotation_detail"),
90
+ ("PATCH", re.compile(r"^/api/v1/annotations/(?P<annotation_id>[^/]+)$"), "annotation_update"),
91
+ ("DELETE", re.compile(r"^/api/v1/annotations/(?P<annotation_id>[^/]+)$"), "annotation_delete"),
77
92
  ("POST", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/init$"), "latex_init"),
78
93
  ("POST", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/compile$"), "latex_compile"),
79
94
  ("GET", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/builds$"), "latex_builds"),