@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.
- package/README.md +107 -94
- package/assets/branding/connector-qq.png +0 -0
- package/assets/branding/connector-rokid.png +0 -0
- package/assets/branding/connector-weixin.png +0 -0
- package/assets/branding/projects.png +0 -0
- package/bin/ds.js +168 -9
- package/docs/assets/branding/projects.png +0 -0
- package/docs/en/00_QUICK_START.md +308 -70
- package/docs/en/01_SETTINGS_REFERENCE.md +3 -0
- package/docs/en/02_START_RESEARCH_GUIDE.md +112 -0
- package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +62 -179
- package/docs/en/09_DOCTOR.md +41 -5
- package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +137 -0
- package/docs/en/11_LICENSE_AND_RISK.md +256 -0
- package/docs/en/12_GUIDED_WORKFLOW_TOUR.md +427 -0
- package/docs/en/13_CORE_ARCHITECTURE_GUIDE.md +297 -0
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
- package/docs/en/99_ACKNOWLEDGEMENTS.md +4 -1
- package/docs/en/README.md +79 -0
- package/docs/images/lingzhu/rokid-agent-platform-create.png +0 -0
- package/docs/images/weixin/weixin-plugin-entry.png +0 -0
- package/docs/images/weixin/weixin-plugin-entry.svg +33 -0
- package/docs/images/weixin/weixin-qr-confirm.svg +30 -0
- package/docs/images/weixin/weixin-quest-media-flow.svg +44 -0
- package/docs/images/weixin/weixin-settings-bind.svg +57 -0
- package/docs/zh/00_QUICK_START.md +315 -74
- package/docs/zh/01_SETTINGS_REFERENCE.md +3 -0
- package/docs/zh/02_START_RESEARCH_GUIDE.md +112 -0
- package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +62 -193
- package/docs/zh/09_DOCTOR.md +41 -5
- package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +144 -0
- package/docs/zh/11_LICENSE_AND_RISK.md +256 -0
- package/docs/zh/12_GUIDED_WORKFLOW_TOUR.md +423 -0
- package/docs/zh/13_CORE_ARCHITECTURE_GUIDE.md +296 -0
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +4 -1
- package/docs/zh/README.md +126 -0
- package/install.sh +0 -34
- package/package.json +2 -2
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/annotations.py +343 -0
- package/src/deepscientist/artifact/arxiv.py +484 -37
- package/src/deepscientist/artifact/service.py +574 -108
- package/src/deepscientist/arxiv_library.py +275 -0
- package/src/deepscientist/bash_exec/service.py +9 -0
- package/src/deepscientist/bridges/builtins.py +2 -0
- package/src/deepscientist/bridges/connectors.py +447 -0
- package/src/deepscientist/channels/__init__.py +2 -0
- package/src/deepscientist/channels/builtins.py +3 -1
- package/src/deepscientist/channels/qq.py +1 -1
- package/src/deepscientist/channels/qq_gateway.py +1 -1
- package/src/deepscientist/channels/relay.py +7 -1
- package/src/deepscientist/channels/weixin.py +59 -0
- package/src/deepscientist/channels/weixin_ilink.py +317 -0
- package/src/deepscientist/config/models.py +22 -2
- package/src/deepscientist/config/service.py +431 -60
- package/src/deepscientist/connector/__init__.py +4 -0
- package/src/deepscientist/connector/connector_profiles.py +481 -0
- package/src/deepscientist/connector/lingzhu_support.py +668 -0
- package/src/deepscientist/connector/qq_profiles.py +206 -0
- package/src/deepscientist/connector/weixin_support.py +663 -0
- package/src/deepscientist/connector_profiles.py +1 -374
- package/src/deepscientist/connector_runtime.py +2 -0
- package/src/deepscientist/daemon/api/handlers.py +165 -5
- package/src/deepscientist/daemon/api/router.py +13 -1
- package/src/deepscientist/daemon/app.py +1130 -61
- package/src/deepscientist/doctor.py +5 -2
- package/src/deepscientist/gitops/diff.py +120 -29
- package/src/deepscientist/lingzhu_support.py +1 -182
- package/src/deepscientist/mcp/server.py +11 -4
- package/src/deepscientist/prompts/builder.py +15 -0
- package/src/deepscientist/qq_profiles.py +1 -196
- package/src/deepscientist/quest/node_traces.py +23 -0
- package/src/deepscientist/quest/service.py +112 -43
- package/src/deepscientist/quest/stage_views.py +71 -5
- package/src/deepscientist/runners/codex.py +55 -3
- package/src/deepscientist/weixin_support.py +1 -0
- package/src/prompts/connectors/lingzhu.md +3 -1
- package/src/prompts/connectors/weixin.md +230 -0
- package/src/prompts/system.md +2 -0
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-BKZ103sn.js → AiManusChatView-D0mTXG4-.js} +156 -48
- package/src/ui/dist/assets/{AnalysisPlugin-mTTzGAlK.js → AnalysisPlugin-Db0cTXxm.js} +1 -1
- package/src/ui/dist/assets/{CliPlugin-BH58n3GY.js → CliPlugin-DrV8je02.js} +164 -9
- package/src/ui/dist/assets/{CodeEditorPlugin-BKGRUH7e.js → CodeEditorPlugin-QXMSCH71.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-BMADwFWJ.js → CodeViewerPlugin-7hhtWj_E.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-ZOnTIHLN.js → DocViewerPlugin-BWMSnRJe.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-CQ7h1Djm.js → GitDiffViewerPlugin-7J9h9Vy_.js} +20 -21
- package/src/ui/dist/assets/{ImageViewerPlugin-GVS5MsnC.js → ImageViewerPlugin-CHJl_0lr.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-BZNv1JML.js → LabCopilotPanel-1qSow1es.js} +11 -11
- package/src/ui/dist/assets/{LabPlugin-TWcJsdQA.js → LabPlugin-eQpPPCEp.js} +2 -1
- package/src/ui/dist/assets/{LatexPlugin-DIjHiR2x.js → LatexPlugin-BwRfi89Z.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-D3ooGAH0.js → MarkdownViewerPlugin-836PVQWV.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-DfVfE9hN.js → MarketplacePlugin-C2y_556i.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-s8JhzuX1.js → NotebookEditor-BRzJbGsn.js} +12 -12
- package/src/ui/dist/assets/{NotebookEditor-DDl0_Mc0.js → NotebookEditor-DIX7Mlzu.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-C2Sf6SJM.js → PdfLoader-DzRaTAlq.js} +14 -7
- package/src/ui/dist/assets/{PdfMarkdownPlugin-CXFLoIsa.js → PdfMarkdownPlugin-DZUfIUnp.js} +73 -6
- package/src/ui/dist/assets/{PdfViewerPlugin-BYTmz2fK.js → PdfViewerPlugin-BwtICzue.js} +103 -34
- package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +3627 -0
- package/src/ui/dist/assets/{SearchPlugin-CjWBI1O9.js → SearchPlugin-DHeIAMsx.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-DdOBU3-S.js → TextViewerPlugin-C3tCmFox.js} +5 -4
- package/src/ui/dist/assets/{VNCViewer-B8HGgLwQ.js → VNCViewer-CQsKVm3t.js} +10 -10
- package/src/ui/dist/assets/bot-BEA2vWuK.js +21 -0
- package/src/ui/dist/assets/branding/logo-rokid.png +0 -0
- package/src/ui/dist/assets/browser-BAcuE0Xj.js +2895 -0
- package/src/ui/dist/assets/{code-BWAY76JP.js → code-XfbSR8K2.js} +1 -1
- package/src/ui/dist/assets/{file-content-C1NwU5oQ.js → file-content-BjxNaIfy.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-CywslwB9.js → file-diff-panel-D_lLVQk0.js} +1 -1
- package/src/ui/dist/assets/{file-socket-B4kzuOBQ.js → file-socket-D9x_5vlY.js} +1 -1
- package/src/ui/dist/assets/{image-D-NZM-6P.js → image-BhWT33W1.js} +1 -1
- package/src/ui/dist/assets/{index-DHZJ_0TI.js → index--c4iXtuy.js} +12 -12
- package/src/ui/dist/assets/{index-BdM1Gqfr.js → index-BDxipwrC.js} +2 -2
- package/src/ui/dist/assets/{index-7Chr1g9c.js → index-DZTZ8mWP.js} +14221 -9523
- package/src/ui/dist/assets/{index-DGIYDuTv.css → index-Dqj-Mjb4.css} +2 -13
- package/src/ui/dist/assets/index-PJbSbPTy.js +25 -0
- package/src/ui/dist/assets/{monaco-Cb2uKKe6.js → monaco-K8izTGgo.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-DSw_D3RV.js → pdf-effect-queue-DfBors6y.js} +16 -1
- package/src/ui/dist/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
- package/src/ui/dist/assets/{popover-Bg72DGgT.js → popover-yFK1J4fL.js} +1 -1
- package/src/ui/dist/assets/{project-sync-Ce_0BglY.js → project-sync-PENr2zcz.js} +1 -74
- package/src/ui/dist/assets/select-CAbJDfYv.js +1690 -0
- package/src/ui/dist/assets/{sigma-DPaACDrh.js → sigma-DEuYJqTl.js} +1 -1
- package/src/ui/dist/assets/{index-CDxNdQdz.js → square-check-big-omoSUmcd.js} +2 -13
- package/src/ui/dist/assets/{trash-BvTgE5__.js → trash--F119N47.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-CgPeMOwP.js → useCliAccess-D31UR23I.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-xPhz7P5B.js → useFileDiffOverlay-BH6KcMzq.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-C3Un3YQr.js → wrap-text-CZ613PM5.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-BgxLa0Ri.js → zoom-out-BgDLAv3z.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/src/ui/dist/assets/AutoFigurePlugin-BGxN8Umr.css +0 -3056
- package/src/ui/dist/assets/AutoFigurePlugin-C_wWw4AP.js +0 -8149
- package/src/ui/dist/assets/PdfViewerPlugin-BJXtIwj_.css +0 -260
- package/src/ui/dist/assets/Stepper-B0Dd8CxK.js +0 -158
- package/src/ui/dist/assets/bibtex-CKaefIN2.js +0 -189
- package/src/ui/dist/assets/file-utils-H2fjA46S.js +0 -109
- package/src/ui/dist/assets/message-square-BzjLiXir.js +0 -16
- package/src/ui/dist/assets/pdfjs-DU1YE8WO.js +0 -3
- package/src/ui/dist/assets/tooltip-C_mA6R0w.js +0 -108
|
@@ -6,6 +6,8 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
from ..shared import ensure_dir, read_json, sha256_text, utc_now, write_json
|
|
8
8
|
|
|
9
|
+
NODE_TRACE_SCHEMA_VERSION = 2
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
def _format_state_label(value: str | None) -> str:
|
|
11
13
|
normalized = str(value or "").strip().replace("_", " ").replace("-", " ")
|
|
@@ -36,6 +38,23 @@ def _normalize_branch_name(value: object, *, fallback: str) -> str:
|
|
|
36
38
|
return text or fallback
|
|
37
39
|
|
|
38
40
|
|
|
41
|
+
def _infer_stage_from_branch_name(value: object) -> str | None:
|
|
42
|
+
normalized = str(value or "").strip().lower()
|
|
43
|
+
if not normalized:
|
|
44
|
+
return None
|
|
45
|
+
if normalized.startswith("analysis/"):
|
|
46
|
+
return "analysis"
|
|
47
|
+
if normalized.startswith("run/"):
|
|
48
|
+
return "experiment"
|
|
49
|
+
if normalized.startswith("idea/"):
|
|
50
|
+
return "idea"
|
|
51
|
+
if normalized.startswith("paper/") or normalized.startswith("write/"):
|
|
52
|
+
return "writing"
|
|
53
|
+
if normalized.startswith("baseline/"):
|
|
54
|
+
return "baseline"
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
39
58
|
def _infer_stage_from_skill(skill_id: object) -> str | None:
|
|
40
59
|
normalized = str(skill_id or "").strip().lower()
|
|
41
60
|
if not normalized:
|
|
@@ -240,6 +259,7 @@ def _resolve_entry_context(
|
|
|
240
259
|
or _infer_stage_from_artifact((((run_artifact or {}).get("record") or {}) if run_artifact else {}))
|
|
241
260
|
or (run_context or {}).get("stage_key")
|
|
242
261
|
or _infer_stage_from_event_type(raw_event_type)
|
|
262
|
+
or _infer_stage_from_branch_name(branch_name)
|
|
243
263
|
or "general"
|
|
244
264
|
)
|
|
245
265
|
return {
|
|
@@ -440,6 +460,7 @@ class QuestNodeTraceManager:
|
|
|
440
460
|
source_signature = sha256_text(
|
|
441
461
|
json.dumps(
|
|
442
462
|
{
|
|
463
|
+
"schema_version": NODE_TRACE_SCHEMA_VERSION,
|
|
443
464
|
"entries": workflow.get("entries") or [],
|
|
444
465
|
"branch": (snapshot or {}).get("branch"),
|
|
445
466
|
},
|
|
@@ -451,6 +472,7 @@ class QuestNodeTraceManager:
|
|
|
451
472
|
if (
|
|
452
473
|
isinstance(existing, dict)
|
|
453
474
|
and existing.get("quest_id") == quest_id
|
|
475
|
+
and existing.get("schema_version") == NODE_TRACE_SCHEMA_VERSION
|
|
454
476
|
and existing.get("source_signature") == source_signature
|
|
455
477
|
and isinstance(existing.get("items"), list)
|
|
456
478
|
):
|
|
@@ -545,6 +567,7 @@ class QuestNodeTraceManager:
|
|
|
545
567
|
|
|
546
568
|
payload = {
|
|
547
569
|
"quest_id": quest_id,
|
|
570
|
+
"schema_version": NODE_TRACE_SCHEMA_VERSION,
|
|
548
571
|
"generated_at": utc_now(),
|
|
549
572
|
"source_signature": source_signature,
|
|
550
573
|
"items": items,
|
|
@@ -19,7 +19,7 @@ except ImportError: # pragma: no cover
|
|
|
19
19
|
|
|
20
20
|
from ..artifact.metrics import build_metrics_timeline, extract_latest_metric
|
|
21
21
|
from ..config import ConfigManager
|
|
22
|
-
from ..connector_runtime import conversation_identity_key, normalize_conversation_id
|
|
22
|
+
from ..connector_runtime import conversation_identity_key, normalize_conversation_id, parse_conversation_id
|
|
23
23
|
from ..gitops import current_branch, export_git_graph, head_commit, init_repo
|
|
24
24
|
from ..home import repo_root
|
|
25
25
|
from ..registries import BaselineRegistry
|
|
@@ -64,6 +64,35 @@ class QuestService:
|
|
|
64
64
|
def _quest_root(self, quest_id: str) -> Path:
|
|
65
65
|
return self.quests_root / quest_id
|
|
66
66
|
|
|
67
|
+
def _normalized_binding_sources(self, sources: list[Any] | None) -> list[str]:
|
|
68
|
+
local_present = False
|
|
69
|
+
external_source: str | None = None
|
|
70
|
+
for raw in sources or []:
|
|
71
|
+
normalized = self._normalize_binding_source(raw)
|
|
72
|
+
if not normalized:
|
|
73
|
+
continue
|
|
74
|
+
if normalized == "local:default":
|
|
75
|
+
local_present = True
|
|
76
|
+
continue
|
|
77
|
+
parsed = parse_conversation_id(normalized)
|
|
78
|
+
connector = str((parsed or {}).get("connector") or "").strip().lower()
|
|
79
|
+
if connector == "local":
|
|
80
|
+
local_present = True
|
|
81
|
+
continue
|
|
82
|
+
external_source = normalized
|
|
83
|
+
if external_source:
|
|
84
|
+
return ["local:default", external_source]
|
|
85
|
+
if local_present:
|
|
86
|
+
return ["local:default"]
|
|
87
|
+
return ["local:default"]
|
|
88
|
+
|
|
89
|
+
def _binding_sources_payload(self, quest_root: Path) -> dict[str, list[str]]:
|
|
90
|
+
bindings_path = quest_root / ".ds" / "bindings.json"
|
|
91
|
+
payload = read_json(bindings_path, {"sources": ["local:default"]})
|
|
92
|
+
raw_sources = payload.get("sources") if isinstance(payload, dict) else ["local:default"]
|
|
93
|
+
sources = self._normalized_binding_sources(raw_sources if isinstance(raw_sources, list) else ["local:default"])
|
|
94
|
+
return {"sources": sources}
|
|
95
|
+
|
|
67
96
|
def preferred_locale(self, quest_root: Path | None = None) -> str:
|
|
68
97
|
if quest_root is not None:
|
|
69
98
|
try:
|
|
@@ -738,7 +767,7 @@ class QuestService:
|
|
|
738
767
|
"last_artifact_interact_at": runtime_state.get("last_artifact_interact_at"),
|
|
739
768
|
"last_delivered_batch_id": runtime_state.get("last_delivered_batch_id"),
|
|
740
769
|
"last_delivered_at": runtime_state.get("last_delivered_at"),
|
|
741
|
-
"bound_conversations":
|
|
770
|
+
"bound_conversations": self._binding_sources_payload(quest_root).get("sources") or ["local:default"],
|
|
742
771
|
"created_at": quest_yaml.get("created_at"),
|
|
743
772
|
"updated_at": quest_yaml.get("updated_at"),
|
|
744
773
|
"branch": research_state.get("current_workspace_branch") or research_state.get("research_head_branch"),
|
|
@@ -1093,7 +1122,7 @@ class QuestService:
|
|
|
1093
1122
|
"last_artifact_interact_at": runtime_state.get("last_artifact_interact_at"),
|
|
1094
1123
|
"last_delivered_batch_id": runtime_state.get("last_delivered_batch_id"),
|
|
1095
1124
|
"last_delivered_at": runtime_state.get("last_delivered_at"),
|
|
1096
|
-
"bound_conversations":
|
|
1125
|
+
"bound_conversations": self._binding_sources_payload(quest_root).get("sources") or ["local:default"],
|
|
1097
1126
|
"created_at": quest_yaml.get("created_at"),
|
|
1098
1127
|
"updated_at": quest_yaml.get("updated_at"),
|
|
1099
1128
|
"branch": current_branch(workspace_root),
|
|
@@ -1362,61 +1391,30 @@ class QuestService:
|
|
|
1362
1391
|
def bind_source(self, quest_id: str, source: str) -> dict:
|
|
1363
1392
|
quest_root = self._quest_root(quest_id)
|
|
1364
1393
|
bindings_path = quest_root / ".ds" / "bindings.json"
|
|
1365
|
-
bindings =
|
|
1394
|
+
bindings = self._binding_sources_payload(quest_root)
|
|
1366
1395
|
normalized_source = self._normalize_binding_source(source)
|
|
1367
|
-
|
|
1368
|
-
changed =
|
|
1369
|
-
replaced = False
|
|
1370
|
-
sources: list[str] = []
|
|
1371
|
-
for item in list(bindings.get("sources") or []):
|
|
1372
|
-
existing = self._normalize_binding_source(str(item))
|
|
1373
|
-
if conversation_identity_key(existing) == normalized_key:
|
|
1374
|
-
if not replaced:
|
|
1375
|
-
sources.append(normalized_source)
|
|
1376
|
-
replaced = True
|
|
1377
|
-
if existing != normalized_source:
|
|
1378
|
-
changed = True
|
|
1379
|
-
else:
|
|
1380
|
-
changed = True
|
|
1381
|
-
continue
|
|
1382
|
-
sources.append(existing)
|
|
1383
|
-
if existing != item:
|
|
1384
|
-
changed = True
|
|
1385
|
-
if not replaced:
|
|
1386
|
-
sources.append(normalized_source)
|
|
1387
|
-
changed = True
|
|
1396
|
+
next_sources = self._normalized_binding_sources([*(bindings.get("sources") or []), normalized_source])
|
|
1397
|
+
changed = list(bindings.get("sources") or []) != next_sources
|
|
1388
1398
|
if changed:
|
|
1389
|
-
bindings["sources"] =
|
|
1399
|
+
bindings["sources"] = next_sources
|
|
1390
1400
|
write_json(bindings_path, bindings)
|
|
1391
1401
|
return bindings
|
|
1392
1402
|
|
|
1393
1403
|
def binding_sources(self, quest_id: str) -> list[str]:
|
|
1394
1404
|
quest_root = self._quest_root(quest_id)
|
|
1395
|
-
|
|
1396
|
-
bindings = read_json(bindings_path, {"sources": ["local:default"]})
|
|
1397
|
-
sources = [self._normalize_binding_source(item) for item in (bindings.get("sources") or [])]
|
|
1398
|
-
return [item for item in sources if item]
|
|
1405
|
+
return list(self._binding_sources_payload(quest_root).get("sources") or ["local:default"])
|
|
1399
1406
|
|
|
1400
1407
|
def set_binding_sources(self, quest_id: str, sources: list[str]) -> dict:
|
|
1401
1408
|
quest_root = self._quest_root(quest_id)
|
|
1402
1409
|
bindings_path = quest_root / ".ds" / "bindings.json"
|
|
1403
|
-
|
|
1404
|
-
ordered: list[str] = []
|
|
1405
|
-
seen: set[str] = set()
|
|
1406
|
-
for item in normalized_sources:
|
|
1407
|
-
key = conversation_identity_key(item)
|
|
1408
|
-
if not item or key in seen:
|
|
1409
|
-
continue
|
|
1410
|
-
seen.add(key)
|
|
1411
|
-
ordered.append(item)
|
|
1412
|
-
payload = {"sources": ordered}
|
|
1410
|
+
payload = {"sources": self._normalized_binding_sources(sources)}
|
|
1413
1411
|
write_json(bindings_path, payload)
|
|
1414
1412
|
return payload
|
|
1415
1413
|
|
|
1416
1414
|
def unbind_source(self, quest_id: str, source: str) -> dict:
|
|
1417
1415
|
quest_root = self._quest_root(quest_id)
|
|
1418
1416
|
bindings_path = quest_root / ".ds" / "bindings.json"
|
|
1419
|
-
bindings =
|
|
1417
|
+
bindings = self._binding_sources_payload(quest_root)
|
|
1420
1418
|
normalized_source = self._normalize_binding_source(source)
|
|
1421
1419
|
normalized_key = conversation_identity_key(normalized_source)
|
|
1422
1420
|
changed = False
|
|
@@ -1429,8 +1427,11 @@ class QuestService:
|
|
|
1429
1427
|
sources.append(existing)
|
|
1430
1428
|
if existing != item:
|
|
1431
1429
|
changed = True
|
|
1430
|
+
normalized_sources = self._normalized_binding_sources(sources)
|
|
1431
|
+
if normalized_sources != list(bindings.get("sources") or []):
|
|
1432
|
+
changed = True
|
|
1432
1433
|
if changed:
|
|
1433
|
-
bindings["sources"] =
|
|
1434
|
+
bindings["sources"] = normalized_sources
|
|
1434
1435
|
write_json(bindings_path, bindings)
|
|
1435
1436
|
return bindings
|
|
1436
1437
|
|
|
@@ -1824,6 +1825,12 @@ class QuestService:
|
|
|
1824
1825
|
resolved_selection = dict(selection or {})
|
|
1825
1826
|
selection_ref = str(resolved_selection.get("selection_ref") or "").strip()
|
|
1826
1827
|
selection_type = str(resolved_selection.get("selection_type") or "stage_node").strip() or None
|
|
1828
|
+
if (
|
|
1829
|
+
selection_type == "branch_node"
|
|
1830
|
+
and selection_ref
|
|
1831
|
+
and not str(resolved_selection.get("branch_name") or "").strip()
|
|
1832
|
+
):
|
|
1833
|
+
resolved_selection["branch_name"] = selection_ref
|
|
1827
1834
|
trace = None
|
|
1828
1835
|
if selection_ref:
|
|
1829
1836
|
try:
|
|
@@ -2080,7 +2087,18 @@ class QuestService:
|
|
|
2080
2087
|
if document_id.startswith(("questpath::", "memory::"))
|
|
2081
2088
|
else workspace_root
|
|
2082
2089
|
)
|
|
2083
|
-
|
|
2090
|
+
try:
|
|
2091
|
+
path, writable, scope, source_kind = self._resolve_document(resolution_root, document_id)
|
|
2092
|
+
except FileNotFoundError:
|
|
2093
|
+
legacy_relative = None
|
|
2094
|
+
if document_id.startswith("path::"):
|
|
2095
|
+
legacy_relative = document_id.split("::", 1)[1].lstrip("/")
|
|
2096
|
+
if legacy_relative and legacy_relative.startswith("literature/arxiv/"):
|
|
2097
|
+
path, writable, scope, source_kind = self._resolve_document(
|
|
2098
|
+
quest_root, f"questpath::{legacy_relative}"
|
|
2099
|
+
)
|
|
2100
|
+
else:
|
|
2101
|
+
raise
|
|
2084
2102
|
renderer_hint, mime_type = self._renderer_hint_for(path)
|
|
2085
2103
|
is_text = self._is_text_document(path, mime_type, renderer_hint)
|
|
2086
2104
|
content = read_text(path) if is_text else ""
|
|
@@ -3549,7 +3567,35 @@ def _tool_name(event: dict, item: dict) -> str:
|
|
|
3549
3567
|
return "tool"
|
|
3550
3568
|
|
|
3551
3569
|
|
|
3570
|
+
def _structured_text(value: object) -> str:
|
|
3571
|
+
if value is None:
|
|
3572
|
+
return ""
|
|
3573
|
+
if isinstance(value, str):
|
|
3574
|
+
return value.strip()
|
|
3575
|
+
try:
|
|
3576
|
+
return json.dumps(value, ensure_ascii=False, indent=2)
|
|
3577
|
+
except TypeError:
|
|
3578
|
+
return str(value)
|
|
3579
|
+
|
|
3580
|
+
|
|
3581
|
+
def _is_bash_exec_item(event: dict, item: dict) -> bool:
|
|
3582
|
+
server = str(item.get("server") or event.get("server") or "").strip()
|
|
3583
|
+
tool = str(item.get("tool") or event.get("tool") or "").strip()
|
|
3584
|
+
return server == "bash_exec" and tool == "bash_exec"
|
|
3585
|
+
|
|
3586
|
+
|
|
3552
3587
|
def _tool_args(event: dict, item: dict) -> str:
|
|
3588
|
+
if _is_bash_exec_item(event, item):
|
|
3589
|
+
for value in (
|
|
3590
|
+
item.get("arguments"),
|
|
3591
|
+
event.get("arguments"),
|
|
3592
|
+
item.get("input"),
|
|
3593
|
+
event.get("input"),
|
|
3594
|
+
):
|
|
3595
|
+
text = _structured_text(value)
|
|
3596
|
+
if text:
|
|
3597
|
+
return text
|
|
3598
|
+
return ""
|
|
3553
3599
|
for value in (
|
|
3554
3600
|
item.get("command"),
|
|
3555
3601
|
item.get("query"),
|
|
@@ -3569,6 +3615,21 @@ def _tool_args(event: dict, item: dict) -> str:
|
|
|
3569
3615
|
|
|
3570
3616
|
|
|
3571
3617
|
def _tool_output(event: dict, item: dict) -> str:
|
|
3618
|
+
if _is_bash_exec_item(event, item):
|
|
3619
|
+
for value in (
|
|
3620
|
+
item.get("result"),
|
|
3621
|
+
item.get("output"),
|
|
3622
|
+
item.get("content"),
|
|
3623
|
+
event.get("result"),
|
|
3624
|
+
event.get("output"),
|
|
3625
|
+
event.get("content"),
|
|
3626
|
+
item.get("aggregated_output"),
|
|
3627
|
+
event.get("aggregated_output"),
|
|
3628
|
+
):
|
|
3629
|
+
text = _structured_text(value)
|
|
3630
|
+
if text:
|
|
3631
|
+
return text
|
|
3632
|
+
return ""
|
|
3572
3633
|
for value in (
|
|
3573
3634
|
item.get("aggregated_output"),
|
|
3574
3635
|
item.get("changes"),
|
|
@@ -3611,17 +3672,25 @@ def _mcp_tool_metadata(*, quest_id: str, run_id: str, server: str, tool: str, it
|
|
|
3611
3672
|
for key in ("command", "workdir", "mode", "timeout_seconds", "comment"):
|
|
3612
3673
|
if key in arguments:
|
|
3613
3674
|
metadata[key] = arguments.get(key)
|
|
3675
|
+
if server == "bash_exec" and tool == "bash_exec" and isinstance(arguments.get("id"), str):
|
|
3676
|
+
metadata["bash_id"] = arguments.get("id")
|
|
3614
3677
|
result_payload = _mcp_result_payload(item)
|
|
3615
3678
|
if server == "bash_exec" and tool == "bash_exec":
|
|
3616
3679
|
for key in (
|
|
3617
3680
|
"bash_id",
|
|
3618
3681
|
"status",
|
|
3682
|
+
"command",
|
|
3683
|
+
"workdir",
|
|
3684
|
+
"cwd",
|
|
3685
|
+
"kind",
|
|
3686
|
+
"comment",
|
|
3619
3687
|
"started_at",
|
|
3620
3688
|
"finished_at",
|
|
3621
3689
|
"exit_code",
|
|
3622
3690
|
"stop_reason",
|
|
3623
3691
|
"last_progress",
|
|
3624
3692
|
"log_path",
|
|
3693
|
+
"watchdog_after_seconds",
|
|
3625
3694
|
):
|
|
3626
3695
|
if key in result_payload:
|
|
3627
3696
|
metadata[key] = result_payload.get(key)
|
|
@@ -153,8 +153,68 @@ class QuestStageViewBuilder:
|
|
|
153
153
|
return candidate
|
|
154
154
|
return self.quest_root
|
|
155
155
|
|
|
156
|
+
def _infer_stage_from_branch_name(self) -> str | None:
|
|
157
|
+
normalized = str(self.branch_name or "").strip().lower()
|
|
158
|
+
if not normalized:
|
|
159
|
+
return None
|
|
160
|
+
if normalized.startswith("analysis/"):
|
|
161
|
+
return "analysis"
|
|
162
|
+
if normalized.startswith("run/"):
|
|
163
|
+
return "experiment"
|
|
164
|
+
if normalized.startswith("idea/"):
|
|
165
|
+
return "idea"
|
|
166
|
+
if normalized.startswith("paper/") or normalized.startswith("write/"):
|
|
167
|
+
return "paper"
|
|
168
|
+
if normalized.startswith("baseline/"):
|
|
169
|
+
return "baseline"
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def _has_paper_state(self) -> bool:
|
|
173
|
+
paper_root = self._paper_root()
|
|
174
|
+
return bool(
|
|
175
|
+
self._paper_candidates()
|
|
176
|
+
or (paper_root / "selected_outline.json").exists()
|
|
177
|
+
or (paper_root / "draft.md").exists()
|
|
178
|
+
or self._paper_bundle_manifest()
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def _resolve_effective_stage_key(self) -> str:
|
|
182
|
+
normalized = normalize_stage_key(self.stage_key)
|
|
183
|
+
if normalized in {"baseline", "idea", "experiment", "analysis", "paper"}:
|
|
184
|
+
return normalized
|
|
185
|
+
if normalized != "general":
|
|
186
|
+
return normalized
|
|
187
|
+
|
|
188
|
+
inferred = self._infer_stage_from_branch_name()
|
|
189
|
+
if inferred:
|
|
190
|
+
return inferred
|
|
191
|
+
if self._analysis_stage_items(None):
|
|
192
|
+
return "analysis"
|
|
193
|
+
if self._experiment_stage_items():
|
|
194
|
+
return "experiment"
|
|
195
|
+
if self._idea_stage_items():
|
|
196
|
+
return "idea"
|
|
197
|
+
if self._has_paper_state():
|
|
198
|
+
return "paper"
|
|
199
|
+
if self._baseline_stage_items():
|
|
200
|
+
return "baseline"
|
|
201
|
+
return normalized
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def _artifact_detail(item: dict[str, Any] | None, payload: dict[str, Any]) -> dict[str, Any] | None:
|
|
205
|
+
if not isinstance(payload, dict) or not payload:
|
|
206
|
+
return None
|
|
207
|
+
record = dict(item or {})
|
|
208
|
+
return {
|
|
209
|
+
"artifact_id": payload.get("artifact_id") or payload.get("id"),
|
|
210
|
+
"artifact_kind": payload.get("kind"),
|
|
211
|
+
"artifact_path": record.get("path"),
|
|
212
|
+
"payload": payload,
|
|
213
|
+
}
|
|
214
|
+
|
|
156
215
|
def build(self) -> dict[str, Any]:
|
|
157
216
|
selection_type = str(self.selection.get("selection_type") or "").strip()
|
|
217
|
+
self.stage_key = self._resolve_effective_stage_key()
|
|
158
218
|
if selection_type == "branch_node" and self.stage_key not in {"experiment", "analysis", "paper"}:
|
|
159
219
|
return self._build_branch()
|
|
160
220
|
if self.stage_key == "baseline":
|
|
@@ -855,7 +915,8 @@ class QuestStageViewBuilder:
|
|
|
855
915
|
"draft_markdown": draft_markdown,
|
|
856
916
|
"literature_files": literature_files,
|
|
857
917
|
"decision_reason": payload.get("reason"),
|
|
858
|
-
}
|
|
918
|
+
},
|
|
919
|
+
"latest_artifact": self._artifact_detail(latest, payload),
|
|
859
920
|
},
|
|
860
921
|
lineage_intent=lineage_intent,
|
|
861
922
|
idea_draft_path=draft_md_rel_path,
|
|
@@ -1084,7 +1145,8 @@ class QuestStageViewBuilder:
|
|
|
1084
1145
|
else None,
|
|
1085
1146
|
"analysis_summary_path": self._relative_path_or_raw(analysis_summary_path),
|
|
1086
1147
|
"analysis_summary_markdown": analysis_summary_markdown,
|
|
1087
|
-
}
|
|
1148
|
+
},
|
|
1149
|
+
"latest_artifact": self._artifact_detail(latest_experiment_item or latest_idea_item, latest_experiment_payload or latest_idea_payload),
|
|
1088
1150
|
},
|
|
1089
1151
|
lineage_intent=lineage_intent,
|
|
1090
1152
|
idea_draft_path=idea_draft_rel_path,
|
|
@@ -1198,7 +1260,8 @@ class QuestStageViewBuilder:
|
|
|
1198
1260
|
"trace_summary": trace_summary,
|
|
1199
1261
|
"trace_markdown": trace_markdown,
|
|
1200
1262
|
"trace_actions": self._recent_trace_actions(),
|
|
1201
|
-
}
|
|
1263
|
+
},
|
|
1264
|
+
"latest_artifact": self._artifact_detail(latest, payload),
|
|
1202
1265
|
},
|
|
1203
1266
|
)
|
|
1204
1267
|
|
|
@@ -1388,10 +1451,12 @@ class QuestStageViewBuilder:
|
|
|
1388
1451
|
"todo_manifest_markdown": self._markdown_body_for_path(manifest.get("todo_manifest_path")),
|
|
1389
1452
|
"summary_path": self._relative_path_or_raw(summary_path) if summary_path else None,
|
|
1390
1453
|
"summary_markdown": summary_markdown,
|
|
1454
|
+
"manifest_payload": manifest,
|
|
1391
1455
|
"trace_summary": trace_summary,
|
|
1392
1456
|
"trace_markdown": self._trace_markdown(),
|
|
1393
1457
|
"trace_actions": self._recent_trace_actions(),
|
|
1394
|
-
}
|
|
1458
|
+
},
|
|
1459
|
+
"latest_artifact": self._artifact_detail(latest, latest_payload),
|
|
1395
1460
|
},
|
|
1396
1461
|
)
|
|
1397
1462
|
|
|
@@ -1532,6 +1597,7 @@ class QuestStageViewBuilder:
|
|
|
1532
1597
|
"latex_root_path": latex_root_rel,
|
|
1533
1598
|
"main_tex_path": main_tex_rel,
|
|
1534
1599
|
},
|
|
1535
|
-
}
|
|
1600
|
+
},
|
|
1601
|
+
"latest_artifact": self._artifact_detail(paper_items[-1] if paper_items else None, self._payload(paper_items[-1] if paper_items else {})),
|
|
1536
1602
|
},
|
|
1537
1603
|
)
|
|
@@ -35,6 +35,17 @@ def _compact_text(value: object, *, limit: int = 1200) -> str:
|
|
|
35
35
|
return text[: limit - 1].rstrip() + "…"
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
def _structured_text(value: object) -> str:
|
|
39
|
+
if value is None:
|
|
40
|
+
return ""
|
|
41
|
+
if isinstance(value, str):
|
|
42
|
+
return value.strip()
|
|
43
|
+
try:
|
|
44
|
+
return json.dumps(value, ensure_ascii=False, indent=2)
|
|
45
|
+
except TypeError:
|
|
46
|
+
return str(value)
|
|
47
|
+
|
|
48
|
+
|
|
38
49
|
def _iter_event_texts(event: dict[str, Any]) -> list[str]:
|
|
39
50
|
texts: list[str] = []
|
|
40
51
|
for key in ("text", "content", "message"):
|
|
@@ -184,7 +195,24 @@ def _tool_name(event: dict[str, Any], item: dict[str, Any]) -> str:
|
|
|
184
195
|
return "tool"
|
|
185
196
|
|
|
186
197
|
|
|
198
|
+
def _is_bash_exec_item(event: dict[str, Any], item: dict[str, Any]) -> bool:
|
|
199
|
+
server = str(item.get("server") or event.get("server") or "").strip()
|
|
200
|
+
tool = str(item.get("tool") or event.get("tool") or "").strip()
|
|
201
|
+
return server == "bash_exec" and tool == "bash_exec"
|
|
202
|
+
|
|
203
|
+
|
|
187
204
|
def _tool_args(event: dict[str, Any], item: dict[str, Any]) -> str:
|
|
205
|
+
if _is_bash_exec_item(event, item):
|
|
206
|
+
for value in (
|
|
207
|
+
item.get("arguments"),
|
|
208
|
+
event.get("arguments"),
|
|
209
|
+
item.get("input"),
|
|
210
|
+
event.get("input"),
|
|
211
|
+
):
|
|
212
|
+
text = _structured_text(value)
|
|
213
|
+
if text:
|
|
214
|
+
return text
|
|
215
|
+
return ""
|
|
188
216
|
for value in (
|
|
189
217
|
item.get("command"),
|
|
190
218
|
item.get("query"),
|
|
@@ -204,6 +232,21 @@ def _tool_args(event: dict[str, Any], item: dict[str, Any]) -> str:
|
|
|
204
232
|
|
|
205
233
|
|
|
206
234
|
def _tool_output(event: dict[str, Any], item: dict[str, Any]) -> str:
|
|
235
|
+
if _is_bash_exec_item(event, item):
|
|
236
|
+
for value in (
|
|
237
|
+
item.get("result"),
|
|
238
|
+
item.get("output"),
|
|
239
|
+
item.get("content"),
|
|
240
|
+
event.get("result"),
|
|
241
|
+
event.get("output"),
|
|
242
|
+
event.get("content"),
|
|
243
|
+
item.get("aggregated_output"),
|
|
244
|
+
event.get("aggregated_output"),
|
|
245
|
+
):
|
|
246
|
+
text = _structured_text(value)
|
|
247
|
+
if text:
|
|
248
|
+
return text
|
|
249
|
+
return ""
|
|
207
250
|
for value in (
|
|
208
251
|
item.get("aggregated_output"),
|
|
209
252
|
item.get("changes"),
|
|
@@ -253,10 +296,12 @@ def _mcp_tool_metadata(
|
|
|
253
296
|
metadata["workdir"] = arguments.get("workdir")
|
|
254
297
|
if isinstance(arguments.get("mode"), str):
|
|
255
298
|
metadata["mode"] = arguments.get("mode")
|
|
256
|
-
if
|
|
299
|
+
if arguments.get("timeout_seconds") is not None:
|
|
257
300
|
metadata["timeout_seconds"] = arguments.get("timeout_seconds")
|
|
258
301
|
if "comment" in arguments:
|
|
259
302
|
metadata["comment"] = arguments.get("comment")
|
|
303
|
+
if server == "bash_exec" and tool == "bash_exec" and isinstance(arguments.get("id"), str):
|
|
304
|
+
metadata["bash_id"] = arguments.get("id")
|
|
260
305
|
metadata["session_id"] = f"quest:{quest_id}"
|
|
261
306
|
metadata["agent_id"] = "pi"
|
|
262
307
|
metadata["agent_instance_id"] = run_id
|
|
@@ -266,12 +311,18 @@ def _mcp_tool_metadata(
|
|
|
266
311
|
for key in (
|
|
267
312
|
"bash_id",
|
|
268
313
|
"status",
|
|
314
|
+
"command",
|
|
315
|
+
"workdir",
|
|
316
|
+
"cwd",
|
|
317
|
+
"kind",
|
|
318
|
+
"comment",
|
|
269
319
|
"started_at",
|
|
270
320
|
"finished_at",
|
|
271
321
|
"exit_code",
|
|
272
322
|
"stop_reason",
|
|
273
323
|
"last_progress",
|
|
274
324
|
"log_path",
|
|
325
|
+
"watchdog_after_seconds",
|
|
275
326
|
):
|
|
276
327
|
if key in result_payload:
|
|
277
328
|
metadata[key] = result_payload.get(key)
|
|
@@ -758,6 +809,7 @@ class CodexRunner:
|
|
|
758
809
|
workspace_root = request.worktree_root or request.quest_root
|
|
759
810
|
resolved_binary = resolve_runner_binary(self.binary, runner_name="codex")
|
|
760
811
|
resolved_runner_config = runner_config if isinstance(runner_config, dict) else self._load_runner_config()
|
|
812
|
+
normalized_model = str(request.model or "").strip()
|
|
761
813
|
command = [
|
|
762
814
|
resolved_binary or self.binary,
|
|
763
815
|
"--search",
|
|
@@ -766,9 +818,9 @@ class CodexRunner:
|
|
|
766
818
|
"--cd",
|
|
767
819
|
str(workspace_root),
|
|
768
820
|
"--skip-git-repo-check",
|
|
769
|
-
"--model",
|
|
770
|
-
request.model,
|
|
771
821
|
]
|
|
822
|
+
if normalized_model.lower() not in {"", "inherit", "default", "codex-default"}:
|
|
823
|
+
command.extend(["--model", normalized_model])
|
|
772
824
|
if request.approval_policy:
|
|
773
825
|
command.extend(["-c", f'approval_policy="{request.approval_policy}"'])
|
|
774
826
|
reasoning_effort = request.reasoning_effort
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .connector.weixin_support import * # noqa: F401,F403
|
|
@@ -11,5 +11,7 @@
|
|
|
11
11
|
- lingzhu_safety_rule: request only actions that are clearly justified by the current quest and understandable to the human user
|
|
12
12
|
- lingzhu_text_rule: even when requesting `surface_actions`, always include a clear text explanation of what is happening and why
|
|
13
13
|
- lingzhu_reply_style_rule: for Lingzhu-facing user-visible text sent through `artifact.interact(...)`, keep the message clear, concise, respectful, and high-information-density
|
|
14
|
-
- lingzhu_reply_length_rule: for each Lingzhu-facing `artifact.interact(...)` message, normally
|
|
14
|
+
- lingzhu_reply_length_rule: for each Lingzhu-facing `artifact.interact(...)` message, normally keep the text within about 20 Chinese characters or one very short sentence unless the user explicitly asks for more detail
|
|
15
15
|
- lingzhu_summary_first_rule: in Lingzhu-facing `artifact.interact(...)` messages, usually give only the synopsis and key facts needed for the user's next decision or understanding; avoid long preambles, repetition, and low-signal detail
|
|
16
|
+
- lingzhu_task_gate_rule: only treat a Lingzhu user utterance as a new quest instruction when the text explicitly starts with `我现在的任务是`; otherwise assume the device is polling for queued progress or buffered replies
|
|
17
|
+
- lingzhu_poll_rule: when Lingzhu is polling rather than giving a new task, return only the buffered progress checkpoints or the latest short status; do not reinterpret the poll text as a fresh instruction
|