@researai/deepscientist 1.5.9 → 1.5.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +112 -99
  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 +519 -63
  7. package/docs/assets/branding/projects.png +0 -0
  8. package/docs/en/00_QUICK_START.md +338 -68
  9. package/docs/en/01_SETTINGS_REFERENCE.md +14 -0
  10. package/docs/en/02_START_RESEARCH_GUIDE.md +180 -4
  11. package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +62 -179
  12. package/docs/en/09_DOCTOR.md +66 -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 +446 -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/15_CODEX_PROVIDER_SETUP.md +284 -0
  19. package/docs/en/99_ACKNOWLEDGEMENTS.md +4 -1
  20. package/docs/en/README.md +83 -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 +345 -72
  28. package/docs/zh/01_SETTINGS_REFERENCE.md +14 -0
  29. package/docs/zh/02_START_RESEARCH_GUIDE.md +181 -3
  30. package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +62 -193
  31. package/docs/zh/09_DOCTOR.md +68 -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 +442 -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/15_CODEX_PROVIDER_SETUP.md +285 -0
  38. package/docs/zh/99_ACKNOWLEDGEMENTS.md +4 -1
  39. package/docs/zh/README.md +129 -0
  40. package/install.sh +0 -34
  41. package/package.json +2 -2
  42. package/pyproject.toml +1 -1
  43. package/src/deepscientist/__init__.py +1 -1
  44. package/src/deepscientist/annotations.py +343 -0
  45. package/src/deepscientist/artifact/arxiv.py +484 -37
  46. package/src/deepscientist/artifact/service.py +574 -108
  47. package/src/deepscientist/arxiv_library.py +275 -0
  48. package/src/deepscientist/bash_exec/monitor.py +7 -5
  49. package/src/deepscientist/bash_exec/service.py +93 -21
  50. package/src/deepscientist/bridges/builtins.py +2 -0
  51. package/src/deepscientist/bridges/connectors.py +447 -0
  52. package/src/deepscientist/channels/__init__.py +2 -0
  53. package/src/deepscientist/channels/builtins.py +3 -1
  54. package/src/deepscientist/channels/local.py +3 -3
  55. package/src/deepscientist/channels/qq.py +8 -8
  56. package/src/deepscientist/channels/qq_gateway.py +1 -1
  57. package/src/deepscientist/channels/relay.py +14 -8
  58. package/src/deepscientist/channels/weixin.py +59 -0
  59. package/src/deepscientist/channels/weixin_ilink.py +388 -0
  60. package/src/deepscientist/config/models.py +23 -2
  61. package/src/deepscientist/config/service.py +539 -67
  62. package/src/deepscientist/connector/__init__.py +4 -0
  63. package/src/deepscientist/connector/connector_profiles.py +481 -0
  64. package/src/deepscientist/connector/lingzhu_support.py +668 -0
  65. package/src/deepscientist/connector/qq_profiles.py +206 -0
  66. package/src/deepscientist/connector/weixin_support.py +663 -0
  67. package/src/deepscientist/connector_profiles.py +1 -374
  68. package/src/deepscientist/connector_runtime.py +2 -0
  69. package/src/deepscientist/daemon/api/handlers.py +165 -5
  70. package/src/deepscientist/daemon/api/router.py +13 -1
  71. package/src/deepscientist/daemon/app.py +1444 -67
  72. package/src/deepscientist/doctor.py +4 -5
  73. package/src/deepscientist/gitops/diff.py +120 -29
  74. package/src/deepscientist/lingzhu_support.py +1 -182
  75. package/src/deepscientist/mcp/server.py +135 -7
  76. package/src/deepscientist/prompts/builder.py +128 -11
  77. package/src/deepscientist/qq_profiles.py +1 -196
  78. package/src/deepscientist/quest/node_traces.py +23 -0
  79. package/src/deepscientist/quest/service.py +359 -74
  80. package/src/deepscientist/quest/stage_views.py +71 -5
  81. package/src/deepscientist/runners/codex.py +170 -19
  82. package/src/deepscientist/runners/runtime_overrides.py +6 -0
  83. package/src/deepscientist/shared.py +33 -14
  84. package/src/deepscientist/weixin_support.py +1 -0
  85. package/src/prompts/connectors/lingzhu.md +3 -1
  86. package/src/prompts/connectors/qq.md +2 -1
  87. package/src/prompts/connectors/weixin.md +231 -0
  88. package/src/prompts/contracts/shared_interaction.md +4 -1
  89. package/src/prompts/system.md +61 -9
  90. package/src/skills/analysis-campaign/SKILL.md +46 -6
  91. package/src/skills/analysis-campaign/references/campaign-plan-template.md +21 -8
  92. package/src/skills/baseline/SKILL.md +1 -1
  93. package/src/skills/decision/SKILL.md +1 -1
  94. package/src/skills/experiment/SKILL.md +1 -1
  95. package/src/skills/finalize/SKILL.md +1 -1
  96. package/src/skills/idea/SKILL.md +1 -1
  97. package/src/skills/intake-audit/SKILL.md +1 -1
  98. package/src/skills/rebuttal/SKILL.md +74 -1
  99. package/src/skills/rebuttal/references/response-letter-template.md +55 -11
  100. package/src/skills/review/SKILL.md +118 -1
  101. package/src/skills/review/references/experiment-todo-template.md +23 -0
  102. package/src/skills/review/references/review-report-template.md +16 -0
  103. package/src/skills/review/references/revision-log-template.md +4 -0
  104. package/src/skills/scout/SKILL.md +1 -1
  105. package/src/skills/write/SKILL.md +168 -7
  106. package/src/skills/write/references/paper-experiment-matrix-template.md +131 -0
  107. package/src/tui/package.json +1 -1
  108. package/src/ui/dist/assets/{AiManusChatView-BKZ103sn.js → AiManusChatView-CnJcXynW.js} +156 -48
  109. package/src/ui/dist/assets/{AnalysisPlugin-mTTzGAlK.js → AnalysisPlugin-DeyzPEhV.js} +1 -1
  110. package/src/ui/dist/assets/{CliPlugin-BH58n3GY.js → CliPlugin-CB1YODQn.js} +164 -9
  111. package/src/ui/dist/assets/{CodeEditorPlugin-BKGRUH7e.js → CodeEditorPlugin-B-xicq1e.js} +8 -8
  112. package/src/ui/dist/assets/{CodeViewerPlugin-BMADwFWJ.js → CodeViewerPlugin-DT54ysXa.js} +5 -5
  113. package/src/ui/dist/assets/{DocViewerPlugin-ZOnTIHLN.js → DocViewerPlugin-DQtKT-VD.js} +3 -3
  114. package/src/ui/dist/assets/{GitDiffViewerPlugin-CQ7h1Djm.js → GitDiffViewerPlugin-hqHbCfnv.js} +20 -21
  115. package/src/ui/dist/assets/{ImageViewerPlugin-GVS5MsnC.js → ImageViewerPlugin-OcVo33jV.js} +5 -5
  116. package/src/ui/dist/assets/{LabCopilotPanel-BZNv1JML.js → LabCopilotPanel-DdGwhEUV.js} +11 -11
  117. package/src/ui/dist/assets/{LabPlugin-TWcJsdQA.js → LabPlugin-Ciz1gDaX.js} +2 -1
  118. package/src/ui/dist/assets/{LatexPlugin-DIjHiR2x.js → LatexPlugin-BhmjNQRC.js} +37 -11
  119. package/src/ui/dist/assets/{MarkdownViewerPlugin-D3ooGAH0.js → MarkdownViewerPlugin-BzdVH9Bx.js} +4 -4
  120. package/src/ui/dist/assets/{MarketplacePlugin-DfVfE9hN.js → MarketplacePlugin-DmyHspXt.js} +3 -3
  121. package/src/ui/dist/assets/{NotebookEditor-DDl0_Mc0.js → NotebookEditor-BMXKrDRk.js} +1 -1
  122. package/src/ui/dist/assets/{NotebookEditor-s8JhzuX1.js → NotebookEditor-BTVYRGkm.js} +12 -12
  123. package/src/ui/dist/assets/{PdfLoader-C2Sf6SJM.js → PdfLoader-CvcjJHXv.js} +14 -7
  124. package/src/ui/dist/assets/{PdfMarkdownPlugin-CXFLoIsa.js → PdfMarkdownPlugin-DW2ej8Vk.js} +73 -6
  125. package/src/ui/dist/assets/{PdfViewerPlugin-BYTmz2fK.js → PdfViewerPlugin-CmlDxbhU.js} +103 -34
  126. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +3627 -0
  127. package/src/ui/dist/assets/{SearchPlugin-CjWBI1O9.js → SearchPlugin-DAjQZPSv.js} +1 -1
  128. package/src/ui/dist/assets/{TextViewerPlugin-DdOBU3-S.js → TextViewerPlugin-C-nVAZb_.js} +5 -4
  129. package/src/ui/dist/assets/{VNCViewer-B8HGgLwQ.js → VNCViewer-D7-dIYon.js} +10 -10
  130. package/src/ui/dist/assets/bot-C_G4WtNI.js +21 -0
  131. package/src/ui/dist/assets/branding/logo-rokid.png +0 -0
  132. package/src/ui/dist/assets/browser-BAcuE0Xj.js +2895 -0
  133. package/src/ui/dist/assets/{code-BWAY76JP.js → code-Cd7WfiWq.js} +1 -1
  134. package/src/ui/dist/assets/{file-content-C1NwU5oQ.js → file-content-B57zsL9y.js} +1 -1
  135. package/src/ui/dist/assets/{file-diff-panel-CywslwB9.js → file-diff-panel-DVoheLFq.js} +1 -1
  136. package/src/ui/dist/assets/{file-socket-B4kzuOBQ.js → file-socket-B5kXFxZP.js} +1 -1
  137. package/src/ui/dist/assets/{image-D-NZM-6P.js → image-LLOjkMHF.js} +1 -1
  138. package/src/ui/dist/assets/{index-DGIYDuTv.css → index-BQG-1s2o.css} +40 -13
  139. package/src/ui/dist/assets/{index-DHZJ_0TI.js → index-C3r2iGrp.js} +12 -12
  140. package/src/ui/dist/assets/{index-7Chr1g9c.js → index-CLQauncb.js} +15050 -9561
  141. package/src/ui/dist/assets/index-Dxa2eYMY.js +25 -0
  142. package/src/ui/dist/assets/{index-BdM1Gqfr.js → index-hOUOWbW2.js} +2 -2
  143. package/src/ui/dist/assets/{monaco-Cb2uKKe6.js → monaco-BGGAEii3.js} +1 -1
  144. package/src/ui/dist/assets/{pdf-effect-queue-DSw_D3RV.js → pdf-effect-queue-DlEr1_y5.js} +16 -1
  145. package/src/ui/dist/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
  146. package/src/ui/dist/assets/{popover-Bg72DGgT.js → popover-CWJbJuYY.js} +1 -1
  147. package/src/ui/dist/assets/{project-sync-Ce_0BglY.js → project-sync-CRJiucYO.js} +18 -77
  148. package/src/ui/dist/assets/select-CoHB7pvH.js +1690 -0
  149. package/src/ui/dist/assets/{sigma-DPaACDrh.js → sigma-D5aJWR8J.js} +1 -1
  150. package/src/ui/dist/assets/{index-CDxNdQdz.js → square-check-big-DUK_mnkS.js} +2 -13
  151. package/src/ui/dist/assets/{trash-BvTgE5__.js → trash-ChU3SEE3.js} +1 -1
  152. package/src/ui/dist/assets/{useCliAccess-CgPeMOwP.js → useCliAccess-BrJBV3tY.js} +1 -1
  153. package/src/ui/dist/assets/{useFileDiffOverlay-xPhz7P5B.js → useFileDiffOverlay-C2OQaVWc.js} +1 -1
  154. package/src/ui/dist/assets/{wrap-text-C3Un3YQr.js → wrap-text-C7Qqh-om.js} +1 -1
  155. package/src/ui/dist/assets/{zoom-out-BgxLa0Ri.js → zoom-out-rtX0FKya.js} +1 -1
  156. package/src/ui/dist/index.html +2 -2
  157. package/src/ui/dist/assets/AutoFigurePlugin-BGxN8Umr.css +0 -3056
  158. package/src/ui/dist/assets/AutoFigurePlugin-C_wWw4AP.js +0 -8149
  159. package/src/ui/dist/assets/PdfViewerPlugin-BJXtIwj_.css +0 -260
  160. package/src/ui/dist/assets/Stepper-B0Dd8CxK.js +0 -158
  161. package/src/ui/dist/assets/bibtex-CKaefIN2.js +0 -189
  162. package/src/ui/dist/assets/file-utils-H2fjA46S.js +0 -109
  163. package/src/ui/dist/assets/message-square-BzjLiXir.js +0 -16
  164. package/src/ui/dist/assets/pdfjs-DU1YE8WO.js +0 -3
  165. package/src/ui/dist/assets/tooltip-C_mA6R0w.js +0 -108
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from ..connector_runtime import parse_conversation_id
7
+ from ..bridges import get_connector_bridge
8
+ from .relay import GenericRelayChannel
9
+
10
+
11
+ class WeixinRelayChannel(GenericRelayChannel):
12
+ name = "weixin"
13
+
14
+ def __init__(self, home: Path, config: dict[str, Any] | None = None) -> None:
15
+ super().__init__(home, "weixin", config)
16
+
17
+ def normalize_inbound(self, payload: dict[str, Any]) -> dict[str, Any]:
18
+ normalized = super().normalize_inbound(payload)
19
+ attachments = [dict(item) for item in (payload.get("attachments") or []) if isinstance(item, dict)]
20
+ if not normalized.get("accepted", False):
21
+ if "raw_event" in payload and isinstance(payload.get("raw_event"), dict):
22
+ normalized["raw_event"] = dict(payload["raw_event"])
23
+ if str(payload.get("context_token") or "").strip():
24
+ normalized["context_token"] = str(payload.get("context_token") or "").strip()
25
+ if attachments:
26
+ normalized["attachments"] = attachments
27
+ return normalized
28
+ if "raw_event" in payload and isinstance(payload.get("raw_event"), dict):
29
+ normalized["raw_event"] = dict(payload["raw_event"])
30
+ if str(payload.get("context_token") or "").strip():
31
+ normalized["context_token"] = str(payload.get("context_token") or "").strip()
32
+ if attachments:
33
+ normalized["attachments"] = attachments
34
+ return normalized
35
+
36
+ def status(self) -> dict[str, Any]:
37
+ payload = super().status()
38
+ details = dict(payload.get("details") or {})
39
+ details.update(
40
+ {
41
+ "base_url": str(self.config.get("base_url") or "").strip() or None,
42
+ "cdn_base_url": str(self.config.get("cdn_base_url") or "").strip() or None,
43
+ "account_id": str(self.config.get("account_id") or "").strip() or None,
44
+ "login_user_id": str(self.config.get("login_user_id") or "").strip() or None,
45
+ }
46
+ )
47
+ payload["details"] = details
48
+ return payload
49
+
50
+ def _deliver(self, record: dict[str, Any]) -> dict[str, Any] | None:
51
+ delivery_config = dict(self.config)
52
+ parsed = parse_conversation_id(record.get("conversation_id"))
53
+ if parsed is not None:
54
+ delivery_config["conversation_id"] = parsed.get("conversation_id")
55
+ delivery_config["_connector_root"] = str(self.root)
56
+ bridge = get_connector_bridge(self.name)
57
+ if bridge is None:
58
+ return None
59
+ return bridge.deliver(record, delivery_config)
@@ -0,0 +1,388 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ import time
5
+ from pathlib import Path
6
+ from typing import Any, Callable
7
+
8
+ from ..connector_runtime import format_conversation_id
9
+ from ..shared import ensure_dir, read_json, utc_now, write_json
10
+ from ..connector.weixin_support import (
11
+ DEFAULT_WEIXIN_LONG_POLL_TIMEOUT_MS,
12
+ SESSION_EXPIRED_ERRCODE,
13
+ download_weixin_message_attachment,
14
+ get_weixin_updates,
15
+ load_weixin_get_updates_buf,
16
+ normalize_weixin_base_url,
17
+ normalize_weixin_cdn_base_url,
18
+ remember_weixin_context_token,
19
+ save_weixin_get_updates_buf,
20
+ )
21
+
22
+ _SESSION_RETRY_INITIAL_SECONDS = 5.0
23
+ _SESSION_RETRY_MAX_SECONDS = 60.0
24
+ _POLL_RETRY_INITIAL_SECONDS = 2.0
25
+ _POLL_RETRY_MAX_SECONDS = 30.0
26
+
27
+
28
+ class WeixinIlinkService:
29
+ def __init__(
30
+ self,
31
+ *,
32
+ home: Path,
33
+ config: dict[str, Any],
34
+ on_event: Callable[[dict[str, Any]], None],
35
+ log: Callable[[str, str], None] | None = None,
36
+ ) -> None:
37
+ self.home = home
38
+ self.config = config
39
+ self.on_event = on_event
40
+ self.log = log or self._default_log
41
+ self._thread: threading.Thread | None = None
42
+ self._stop_event = threading.Event()
43
+ self._root = home / "logs" / "connectors" / "weixin"
44
+ self._runtime_path = self._root / "runtime.json"
45
+
46
+ def start(self) -> bool:
47
+ enabled = bool(self.config.get("enabled", False))
48
+ transport = str(self.config.get("transport") or "ilink_long_poll").strip().lower()
49
+ token = self._secret("bot_token", "bot_token_env")
50
+ account_id = str(self.config.get("account_id") or "").strip()
51
+ if not enabled:
52
+ self._write_state(
53
+ enabled=False,
54
+ transport="ilink_long_poll",
55
+ connected=False,
56
+ connection_state="disabled",
57
+ auth_state="disabled",
58
+ updated_at=utc_now(),
59
+ )
60
+ return False
61
+ if transport != "ilink_long_poll":
62
+ return False
63
+ if not token or not account_id:
64
+ self._write_state(
65
+ enabled=True,
66
+ transport="ilink_long_poll",
67
+ connected=False,
68
+ connection_state="needs_credentials",
69
+ auth_state="missing_credentials",
70
+ account_id=account_id or None,
71
+ login_user_id=str(self.config.get("login_user_id") or "").strip() or None,
72
+ base_url=normalize_weixin_base_url(self.config.get("base_url")),
73
+ cdn_base_url=normalize_weixin_cdn_base_url(self.config.get("cdn_base_url")),
74
+ updated_at=utc_now(),
75
+ )
76
+ return False
77
+ if self._thread is not None and self._thread.is_alive():
78
+ return True
79
+ self._stop_event.clear()
80
+ self._thread = threading.Thread(
81
+ target=self._run,
82
+ daemon=True,
83
+ name="deepscientist-weixin-ilink",
84
+ )
85
+ self._thread.start()
86
+ return True
87
+
88
+ def stop(self, timeout: float = 5.0) -> None:
89
+ self._stop_event.set()
90
+ if self._thread is not None:
91
+ self._thread.join(timeout=timeout)
92
+
93
+ def _run(self) -> None:
94
+ timeout_ms = DEFAULT_WEIXIN_LONG_POLL_TIMEOUT_MS
95
+ retry_until = 0.0
96
+ retry_reason: str | None = None
97
+ sync_buf = load_weixin_get_updates_buf(self._root)
98
+ base_url = normalize_weixin_base_url(self.config.get("base_url"))
99
+ cdn_base_url = normalize_weixin_cdn_base_url(self.config.get("cdn_base_url"))
100
+ account_id = str(self.config.get("account_id") or "").strip()
101
+ login_user_id = str(self.config.get("login_user_id") or "").strip() or None
102
+ token = self._secret("bot_token", "bot_token_env")
103
+ route_tag = str(self.config.get("route_tag") or "").strip() or None
104
+ session_retry_seconds = _SESSION_RETRY_INITIAL_SECONDS
105
+ poll_retry_seconds = _POLL_RETRY_INITIAL_SECONDS
106
+ session_expired_count = 0
107
+ session_expired_since: str | None = None
108
+
109
+ self._write_state(
110
+ enabled=True,
111
+ transport="ilink_long_poll",
112
+ connected=False,
113
+ connection_state="starting",
114
+ auth_state="ready",
115
+ account_id=account_id or None,
116
+ login_user_id=login_user_id,
117
+ base_url=base_url,
118
+ cdn_base_url=cdn_base_url,
119
+ retry_reason=None,
120
+ retry_after_seconds=None,
121
+ pause_until=None,
122
+ updated_at=utc_now(),
123
+ )
124
+
125
+ while not self._stop_event.is_set():
126
+ now = time.time()
127
+ if retry_until > now:
128
+ retry_after_seconds = max(int(retry_until - now + 0.999), 1)
129
+ state_patch: dict[str, Any] = {
130
+ "connected": False,
131
+ "connection_state": "connecting" if retry_reason == "session_expired" else "error",
132
+ "auth_state": "ready" if token and account_id else "missing_credentials",
133
+ "retry_reason": retry_reason,
134
+ "retry_after_seconds": retry_after_seconds,
135
+ "session_expired_count": session_expired_count or None,
136
+ "session_expired_since": session_expired_since,
137
+ "pause_until": None,
138
+ "updated_at": utc_now(),
139
+ }
140
+ if retry_reason == "session_expired":
141
+ state_patch["last_error"] = f"session expired ({SESSION_EXPIRED_ERRCODE}); retrying automatically"
142
+ self._write_state(**state_patch)
143
+ self._stop_event.wait(min(max(retry_until - now, 0.5), 5.0))
144
+ continue
145
+ try:
146
+ response = get_weixin_updates(
147
+ base_url=base_url,
148
+ token=token,
149
+ get_updates_buf=sync_buf,
150
+ route_tag=route_tag,
151
+ timeout_ms=timeout_ms,
152
+ )
153
+ long_poll_timeout_ms = int(response.get("longpolling_timeout_ms") or 0)
154
+ if long_poll_timeout_ms > 0:
155
+ timeout_ms = long_poll_timeout_ms
156
+ errcode = int(response.get("errcode") or 0)
157
+ retcode = int(response.get("ret") or 0)
158
+ if errcode == SESSION_EXPIRED_ERRCODE or retcode == SESSION_EXPIRED_ERRCODE:
159
+ session_expired_count += 1
160
+ if session_expired_since is None:
161
+ session_expired_since = utc_now()
162
+ if sync_buf:
163
+ sync_buf = ""
164
+ save_weixin_get_updates_buf(self._root, "")
165
+ retry_delay_seconds = session_retry_seconds
166
+ retry_after_seconds = max(int(retry_delay_seconds + 0.999), 1)
167
+ session_retry_seconds = min(session_retry_seconds * 2.0, _SESSION_RETRY_MAX_SECONDS)
168
+ retry_reason = "session_expired"
169
+ retry_until = time.time() + retry_delay_seconds
170
+ timeout_ms = DEFAULT_WEIXIN_LONG_POLL_TIMEOUT_MS
171
+ self.log(
172
+ "warning",
173
+ (
174
+ "weixin.ilink: session expired; cleared sync state and "
175
+ f"retrying in {retry_after_seconds}s"
176
+ ),
177
+ )
178
+ self._write_state(
179
+ connected=False,
180
+ connection_state="connecting",
181
+ auth_state="ready" if token and account_id else "missing_credentials",
182
+ last_error=f"session expired ({SESSION_EXPIRED_ERRCODE}); retrying automatically",
183
+ retry_reason=retry_reason,
184
+ retry_after_seconds=retry_after_seconds,
185
+ session_expired_count=session_expired_count,
186
+ session_expired_since=session_expired_since,
187
+ pause_until=None,
188
+ updated_at=utc_now(),
189
+ )
190
+ continue
191
+ if errcode or retcode:
192
+ raise RuntimeError(
193
+ str(response.get("errmsg") or f"getupdates failed with ret={retcode} errcode={errcode}")
194
+ )
195
+ next_sync_buf = str(response.get("get_updates_buf") or "").strip()
196
+ if next_sync_buf:
197
+ sync_buf = next_sync_buf
198
+ save_weixin_get_updates_buf(self._root, sync_buf)
199
+ if session_expired_count > 0:
200
+ self.log(
201
+ "info",
202
+ f"weixin.ilink: session recovered after {session_expired_count} retry attempt(s)",
203
+ )
204
+ retry_reason = None
205
+ retry_until = 0.0
206
+ session_retry_seconds = _SESSION_RETRY_INITIAL_SECONDS
207
+ poll_retry_seconds = _POLL_RETRY_INITIAL_SECONDS
208
+ session_expired_count = 0
209
+ session_expired_since = None
210
+ self._write_state(
211
+ connected=True,
212
+ connection_state="connected",
213
+ auth_state="ready",
214
+ last_error=None,
215
+ retry_reason=None,
216
+ retry_after_seconds=None,
217
+ session_expired_count=None,
218
+ session_expired_since=None,
219
+ pause_until=None,
220
+ updated_at=utc_now(),
221
+ )
222
+ for message in response.get("msgs") or []:
223
+ if not isinstance(message, dict):
224
+ continue
225
+ event = self._normalize_message(message)
226
+ if event is None:
227
+ continue
228
+ self.on_event(event)
229
+ self._write_state(
230
+ connected=True,
231
+ connection_state="connected",
232
+ auth_state="ready",
233
+ last_conversation_id=event.get("conversation_id"),
234
+ last_event_at=utc_now(),
235
+ updated_at=utc_now(),
236
+ )
237
+ except Exception as exc:
238
+ if self._stop_event.is_set():
239
+ break
240
+ retry_reason = "poll_error"
241
+ retry_delay_seconds = poll_retry_seconds
242
+ retry_after_seconds = max(int(retry_delay_seconds + 0.999), 1)
243
+ retry_until = time.time() + retry_delay_seconds
244
+ poll_retry_seconds = min(poll_retry_seconds * 2.0, _POLL_RETRY_MAX_SECONDS)
245
+ self.log(
246
+ "warning",
247
+ f"weixin.ilink: polling failed: {exc}; retrying in {retry_after_seconds}s",
248
+ )
249
+ self._write_state(
250
+ connected=False,
251
+ connection_state="error",
252
+ auth_state="ready" if token and account_id else "missing_credentials",
253
+ last_error=str(exc),
254
+ retry_reason=retry_reason,
255
+ retry_after_seconds=retry_after_seconds,
256
+ session_expired_count=session_expired_count or None,
257
+ session_expired_since=session_expired_since,
258
+ pause_until=None,
259
+ updated_at=utc_now(),
260
+ )
261
+ self._stop_event.wait(retry_delay_seconds)
262
+ self._write_state(
263
+ connected=False,
264
+ connection_state="stopped",
265
+ retry_reason=None,
266
+ retry_after_seconds=None,
267
+ pause_until=None,
268
+ updated_at=utc_now(),
269
+ )
270
+
271
+ def _normalize_message(self, payload: dict[str, Any]) -> dict[str, Any] | None:
272
+ sender_id = str(payload.get("from_user_id") or "").strip()
273
+ if not sender_id:
274
+ return None
275
+ text = self._message_text(payload)
276
+ attachments = self._message_attachments(payload)
277
+ context_token = str(payload.get("context_token") or "").strip()
278
+ conversation_id = format_conversation_id("weixin", "direct", sender_id)
279
+ if context_token:
280
+ remember_weixin_context_token(
281
+ self._root,
282
+ user_id=sender_id,
283
+ context_token=context_token,
284
+ account_id=str(self.config.get("account_id") or "").strip() or None,
285
+ conversation_id=conversation_id,
286
+ message_id=str(payload.get("message_id") or payload.get("client_id") or payload.get("seq") or "").strip() or None,
287
+ updated_at=utc_now(),
288
+ )
289
+ if not text and not attachments:
290
+ return None
291
+ return {
292
+ "chat_type": "direct",
293
+ "group_id": "",
294
+ "direct_id": sender_id,
295
+ "sender_id": sender_id,
296
+ "sender_name": sender_id,
297
+ "message_id": str(payload.get("message_id") or payload.get("client_id") or payload.get("seq") or "").strip(),
298
+ "conversation_id": conversation_id,
299
+ "text": text,
300
+ "mentioned": False,
301
+ "attachments": attachments,
302
+ "context_token": context_token or None,
303
+ "raw_event": payload,
304
+ }
305
+
306
+ @staticmethod
307
+ def _message_text(payload: dict[str, Any]) -> str:
308
+ for item in payload.get("item_list") or []:
309
+ if not isinstance(item, dict):
310
+ continue
311
+ if int(item.get("type") or 0) == 1:
312
+ text_item = item.get("text_item") if isinstance(item.get("text_item"), dict) else {}
313
+ text = str(text_item.get("text") or "").strip()
314
+ if text:
315
+ return text
316
+ if int(item.get("type") or 0) == 3:
317
+ voice_item = item.get("voice_item") if isinstance(item.get("voice_item"), dict) else {}
318
+ text = str(voice_item.get("text") or "").strip()
319
+ if text:
320
+ return text
321
+ return ""
322
+
323
+ def _message_attachments(self, payload: dict[str, Any]) -> list[dict[str, Any]]:
324
+ item_list = payload.get("item_list") if isinstance(payload.get("item_list"), list) else []
325
+ message_key = str(payload.get("message_id") or payload.get("client_id") or payload.get("seq") or "").strip() or "weixin"
326
+ dest_dir = ensure_dir(self._root / "tmp" / "inbound")
327
+ attachments: list[dict[str, Any]] = []
328
+ seen_paths: set[str] = set()
329
+
330
+ def append_attachment(item: dict[str, Any], *, suffix: str) -> None:
331
+ try:
332
+ attachment = download_weixin_message_attachment(
333
+ item=item,
334
+ dest_dir=dest_dir,
335
+ cdn_base_url=normalize_weixin_cdn_base_url(self.config.get("cdn_base_url")),
336
+ prefix=f"{message_key}-{suffix}",
337
+ )
338
+ except Exception as exc:
339
+ self.log("warning", f"weixin.ilink: failed to materialize inbound attachment: {exc}")
340
+ return
341
+ if not isinstance(attachment, dict):
342
+ return
343
+ path = str(attachment.get("path") or "").strip()
344
+ if path and path in seen_paths:
345
+ return
346
+ if path:
347
+ seen_paths.add(path)
348
+ attachments.append(attachment)
349
+
350
+ for index, item in enumerate(item_list, start=1):
351
+ if not isinstance(item, dict):
352
+ continue
353
+ append_attachment(item, suffix=f"item-{index}")
354
+
355
+ if attachments:
356
+ return attachments
357
+
358
+ for index, item in enumerate(item_list, start=1):
359
+ if not isinstance(item, dict):
360
+ continue
361
+ ref_message = item.get("ref_msg") if isinstance(item.get("ref_msg"), dict) else {}
362
+ ref_item = ref_message.get("message_item") if isinstance(ref_message.get("message_item"), dict) else None
363
+ if not isinstance(ref_item, dict):
364
+ continue
365
+ append_attachment(ref_item, suffix=f"ref-{index}")
366
+ return attachments
367
+
368
+ def _secret(self, key: str, env_key: str) -> str:
369
+ direct = str(self.config.get(key) or "").strip()
370
+ if direct:
371
+ return direct
372
+ env_name = str(self.config.get(env_key) or "").strip()
373
+ if not env_name:
374
+ return ""
375
+ from os import environ
376
+
377
+ return str(environ.get(env_name) or "").strip()
378
+
379
+ def _write_state(self, **patch: Any) -> None:
380
+ state = read_json(self._runtime_path, {}) or {}
381
+ if not isinstance(state, dict):
382
+ state = {}
383
+ state.update(patch)
384
+ write_json(self._runtime_path, state)
385
+
386
+ @staticmethod
387
+ def _default_log(level: str, message: str) -> None:
388
+ print(f"[{level}] {message}")
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
  CONFIG_NAMES = ("config", "runners", "connectors", "plugins", "mcp_servers")
7
7
  REQUIRED_CONFIG_NAMES = ("config", "runners", "connectors")
8
8
  OPTIONAL_CONFIG_NAMES = ("plugins", "mcp_servers")
9
- SYSTEM_CONNECTOR_NAMES = ("qq", "telegram", "discord", "slack", "feishu", "whatsapp", "lingzhu")
9
+ SYSTEM_CONNECTOR_NAMES = ("qq", "weixin", "telegram", "discord", "slack", "feishu", "whatsapp", "lingzhu")
10
10
 
11
11
 
12
12
  @dataclass(frozen=True)
@@ -22,7 +22,7 @@ def config_filename(name: str) -> str:
22
22
 
23
23
 
24
24
  def default_system_enabled_connectors() -> dict[str, bool]:
25
- return {name: name == "qq" for name in SYSTEM_CONNECTOR_NAMES}
25
+ return {name: name in {"qq", "weixin", "lingzhu"} for name in SYSTEM_CONNECTOR_NAMES}
26
26
 
27
27
 
28
28
  def default_config(home: Path) -> dict:
@@ -95,6 +95,7 @@ def default_runners() -> dict:
95
95
  "enabled": True,
96
96
  "binary": "codex",
97
97
  "config_dir": "~/.codex",
98
+ "profile": "",
98
99
  "model": "gpt-5.4",
99
100
  "model_reasoning_effort": "xhigh",
100
101
  "approval_policy": "on-request",
@@ -148,6 +149,26 @@ def default_connectors() -> dict:
148
149
  "enable_markdown_send": False,
149
150
  "enable_file_upload_experimental": False,
150
151
  },
152
+ "weixin": {
153
+ "enabled": False,
154
+ "transport": "ilink_long_poll",
155
+ "bot_name": "DeepScientist",
156
+ "command_prefix": "/",
157
+ "base_url": "https://ilinkai.weixin.qq.com",
158
+ "cdn_base_url": "https://novac2c.cdn.weixin.qq.com/c2c",
159
+ "bot_type": "3",
160
+ "bot_token": None,
161
+ "bot_token_env": None,
162
+ "account_id": None,
163
+ "login_user_id": None,
164
+ "route_tag": None,
165
+ "dm_policy": "pairing",
166
+ "allow_from": [],
167
+ "group_policy": "disabled",
168
+ "group_allow_from": [],
169
+ "groups": [],
170
+ "auto_bind_dm_to_active_quest": True,
171
+ },
151
172
  "telegram": {
152
173
  "enabled": False,
153
174
  "profiles": [],