@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
@@ -0,0 +1,317 @@
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_PAUSE_SECONDS = 60 * 60
23
+
24
+
25
+ class WeixinIlinkService:
26
+ def __init__(
27
+ self,
28
+ *,
29
+ home: Path,
30
+ config: dict[str, Any],
31
+ on_event: Callable[[dict[str, Any]], None],
32
+ log: Callable[[str, str], None] | None = None,
33
+ ) -> None:
34
+ self.home = home
35
+ self.config = config
36
+ self.on_event = on_event
37
+ self.log = log or self._default_log
38
+ self._thread: threading.Thread | None = None
39
+ self._stop_event = threading.Event()
40
+ self._root = home / "logs" / "connectors" / "weixin"
41
+ self._runtime_path = self._root / "runtime.json"
42
+
43
+ def start(self) -> bool:
44
+ enabled = bool(self.config.get("enabled", False))
45
+ transport = str(self.config.get("transport") or "ilink_long_poll").strip().lower()
46
+ token = self._secret("bot_token", "bot_token_env")
47
+ account_id = str(self.config.get("account_id") or "").strip()
48
+ if not enabled:
49
+ self._write_state(
50
+ enabled=False,
51
+ transport="ilink_long_poll",
52
+ connected=False,
53
+ connection_state="disabled",
54
+ auth_state="disabled",
55
+ updated_at=utc_now(),
56
+ )
57
+ return False
58
+ if transport != "ilink_long_poll":
59
+ return False
60
+ if not token or not account_id:
61
+ self._write_state(
62
+ enabled=True,
63
+ transport="ilink_long_poll",
64
+ connected=False,
65
+ connection_state="needs_credentials",
66
+ auth_state="missing_credentials",
67
+ account_id=account_id or None,
68
+ login_user_id=str(self.config.get("login_user_id") or "").strip() or None,
69
+ base_url=normalize_weixin_base_url(self.config.get("base_url")),
70
+ cdn_base_url=normalize_weixin_cdn_base_url(self.config.get("cdn_base_url")),
71
+ updated_at=utc_now(),
72
+ )
73
+ return False
74
+ if self._thread is not None and self._thread.is_alive():
75
+ return True
76
+ self._stop_event.clear()
77
+ self._thread = threading.Thread(
78
+ target=self._run,
79
+ daemon=True,
80
+ name="deepscientist-weixin-ilink",
81
+ )
82
+ self._thread.start()
83
+ return True
84
+
85
+ def stop(self, timeout: float = 5.0) -> None:
86
+ self._stop_event.set()
87
+ if self._thread is not None:
88
+ self._thread.join(timeout=timeout)
89
+
90
+ def _run(self) -> None:
91
+ timeout_ms = DEFAULT_WEIXIN_LONG_POLL_TIMEOUT_MS
92
+ pause_until = 0.0
93
+ sync_buf = load_weixin_get_updates_buf(self._root)
94
+ base_url = normalize_weixin_base_url(self.config.get("base_url"))
95
+ cdn_base_url = normalize_weixin_cdn_base_url(self.config.get("cdn_base_url"))
96
+ account_id = str(self.config.get("account_id") or "").strip()
97
+ login_user_id = str(self.config.get("login_user_id") or "").strip() or None
98
+ token = self._secret("bot_token", "bot_token_env")
99
+ route_tag = str(self.config.get("route_tag") or "").strip() or None
100
+
101
+ self._write_state(
102
+ enabled=True,
103
+ transport="ilink_long_poll",
104
+ connected=False,
105
+ connection_state="starting",
106
+ auth_state="ready",
107
+ account_id=account_id or None,
108
+ login_user_id=login_user_id,
109
+ base_url=base_url,
110
+ cdn_base_url=cdn_base_url,
111
+ updated_at=utc_now(),
112
+ )
113
+
114
+ while not self._stop_event.is_set():
115
+ now = time.time()
116
+ if pause_until > now:
117
+ self._write_state(
118
+ connected=False,
119
+ connection_state="paused",
120
+ auth_state="error",
121
+ pause_until=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(pause_until)),
122
+ updated_at=utc_now(),
123
+ )
124
+ self._stop_event.wait(min(max(pause_until - now, 1.0), 5.0))
125
+ continue
126
+ try:
127
+ response = get_weixin_updates(
128
+ base_url=base_url,
129
+ token=token,
130
+ get_updates_buf=sync_buf,
131
+ route_tag=route_tag,
132
+ timeout_ms=timeout_ms,
133
+ )
134
+ long_poll_timeout_ms = int(response.get("longpolling_timeout_ms") or 0)
135
+ if long_poll_timeout_ms > 0:
136
+ timeout_ms = long_poll_timeout_ms
137
+ errcode = int(response.get("errcode") or 0)
138
+ retcode = int(response.get("ret") or 0)
139
+ if errcode == SESSION_EXPIRED_ERRCODE or retcode == SESSION_EXPIRED_ERRCODE:
140
+ pause_until = time.time() + _SESSION_PAUSE_SECONDS
141
+ self.log("warning", "weixin.ilink: session expired; pausing for one hour")
142
+ self._write_state(
143
+ connected=False,
144
+ connection_state="paused",
145
+ auth_state="error",
146
+ last_error=f"session expired ({SESSION_EXPIRED_ERRCODE})",
147
+ pause_until=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(pause_until)),
148
+ updated_at=utc_now(),
149
+ )
150
+ continue
151
+ if errcode or retcode:
152
+ raise RuntimeError(
153
+ str(response.get("errmsg") or f"getupdates failed with ret={retcode} errcode={errcode}")
154
+ )
155
+ next_sync_buf = str(response.get("get_updates_buf") or "").strip()
156
+ if next_sync_buf:
157
+ sync_buf = next_sync_buf
158
+ save_weixin_get_updates_buf(self._root, sync_buf)
159
+ self._write_state(
160
+ connected=True,
161
+ connection_state="connected",
162
+ auth_state="ready",
163
+ last_error=None,
164
+ pause_until=None,
165
+ updated_at=utc_now(),
166
+ )
167
+ for message in response.get("msgs") or []:
168
+ if not isinstance(message, dict):
169
+ continue
170
+ event = self._normalize_message(message)
171
+ if event is None:
172
+ continue
173
+ self.on_event(event)
174
+ self._write_state(
175
+ connected=True,
176
+ connection_state="connected",
177
+ auth_state="ready",
178
+ last_conversation_id=event.get("conversation_id"),
179
+ last_event_at=utc_now(),
180
+ updated_at=utc_now(),
181
+ )
182
+ except Exception as exc:
183
+ if self._stop_event.is_set():
184
+ break
185
+ self.log("warning", f"weixin.ilink: polling failed: {exc}")
186
+ self._write_state(
187
+ connected=False,
188
+ connection_state="error",
189
+ auth_state="ready" if token and account_id else "missing_credentials",
190
+ last_error=str(exc),
191
+ updated_at=utc_now(),
192
+ )
193
+ self._stop_event.wait(2.0)
194
+ self._write_state(
195
+ connected=False,
196
+ connection_state="stopped",
197
+ updated_at=utc_now(),
198
+ )
199
+
200
+ def _normalize_message(self, payload: dict[str, Any]) -> dict[str, Any] | None:
201
+ sender_id = str(payload.get("from_user_id") or "").strip()
202
+ if not sender_id:
203
+ return None
204
+ text = self._message_text(payload)
205
+ attachments = self._message_attachments(payload)
206
+ context_token = str(payload.get("context_token") or "").strip()
207
+ conversation_id = format_conversation_id("weixin", "direct", sender_id)
208
+ if context_token:
209
+ remember_weixin_context_token(
210
+ self._root,
211
+ user_id=sender_id,
212
+ context_token=context_token,
213
+ account_id=str(self.config.get("account_id") or "").strip() or None,
214
+ conversation_id=conversation_id,
215
+ message_id=str(payload.get("message_id") or payload.get("client_id") or payload.get("seq") or "").strip() or None,
216
+ updated_at=utc_now(),
217
+ )
218
+ if not text and not attachments:
219
+ return None
220
+ return {
221
+ "chat_type": "direct",
222
+ "group_id": "",
223
+ "direct_id": sender_id,
224
+ "sender_id": sender_id,
225
+ "sender_name": sender_id,
226
+ "message_id": str(payload.get("message_id") or payload.get("client_id") or payload.get("seq") or "").strip(),
227
+ "conversation_id": conversation_id,
228
+ "text": text,
229
+ "mentioned": False,
230
+ "attachments": attachments,
231
+ "context_token": context_token or None,
232
+ "raw_event": payload,
233
+ }
234
+
235
+ @staticmethod
236
+ def _message_text(payload: dict[str, Any]) -> str:
237
+ for item in payload.get("item_list") or []:
238
+ if not isinstance(item, dict):
239
+ continue
240
+ if int(item.get("type") or 0) == 1:
241
+ text_item = item.get("text_item") if isinstance(item.get("text_item"), dict) else {}
242
+ text = str(text_item.get("text") or "").strip()
243
+ if text:
244
+ return text
245
+ if int(item.get("type") or 0) == 3:
246
+ voice_item = item.get("voice_item") if isinstance(item.get("voice_item"), dict) else {}
247
+ text = str(voice_item.get("text") or "").strip()
248
+ if text:
249
+ return text
250
+ return ""
251
+
252
+ def _message_attachments(self, payload: dict[str, Any]) -> list[dict[str, Any]]:
253
+ item_list = payload.get("item_list") if isinstance(payload.get("item_list"), list) else []
254
+ message_key = str(payload.get("message_id") or payload.get("client_id") or payload.get("seq") or "").strip() or "weixin"
255
+ dest_dir = ensure_dir(self._root / "tmp" / "inbound")
256
+ attachments: list[dict[str, Any]] = []
257
+ seen_paths: set[str] = set()
258
+
259
+ def append_attachment(item: dict[str, Any], *, suffix: str) -> None:
260
+ try:
261
+ attachment = download_weixin_message_attachment(
262
+ item=item,
263
+ dest_dir=dest_dir,
264
+ cdn_base_url=normalize_weixin_cdn_base_url(self.config.get("cdn_base_url")),
265
+ prefix=f"{message_key}-{suffix}",
266
+ )
267
+ except Exception as exc:
268
+ self.log("warning", f"weixin.ilink: failed to materialize inbound attachment: {exc}")
269
+ return
270
+ if not isinstance(attachment, dict):
271
+ return
272
+ path = str(attachment.get("path") or "").strip()
273
+ if path and path in seen_paths:
274
+ return
275
+ if path:
276
+ seen_paths.add(path)
277
+ attachments.append(attachment)
278
+
279
+ for index, item in enumerate(item_list, start=1):
280
+ if not isinstance(item, dict):
281
+ continue
282
+ append_attachment(item, suffix=f"item-{index}")
283
+
284
+ if attachments:
285
+ return attachments
286
+
287
+ for index, item in enumerate(item_list, start=1):
288
+ if not isinstance(item, dict):
289
+ continue
290
+ ref_message = item.get("ref_msg") if isinstance(item.get("ref_msg"), dict) else {}
291
+ ref_item = ref_message.get("message_item") if isinstance(ref_message.get("message_item"), dict) else None
292
+ if not isinstance(ref_item, dict):
293
+ continue
294
+ append_attachment(ref_item, suffix=f"ref-{index}")
295
+ return attachments
296
+
297
+ def _secret(self, key: str, env_key: str) -> str:
298
+ direct = str(self.config.get(key) or "").strip()
299
+ if direct:
300
+ return direct
301
+ env_name = str(self.config.get(env_key) or "").strip()
302
+ if not env_name:
303
+ return ""
304
+ from os import environ
305
+
306
+ return str(environ.get(env_name) or "").strip()
307
+
308
+ def _write_state(self, **patch: Any) -> None:
309
+ state = read_json(self._runtime_path, {}) or {}
310
+ if not isinstance(state, dict):
311
+ state = {}
312
+ state.update(patch)
313
+ write_json(self._runtime_path, state)
314
+
315
+ @staticmethod
316
+ def _default_log(level: str, message: str) -> None:
317
+ 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:
@@ -148,6 +148,26 @@ def default_connectors() -> dict:
148
148
  "enable_markdown_send": False,
149
149
  "enable_file_upload_experimental": False,
150
150
  },
151
+ "weixin": {
152
+ "enabled": False,
153
+ "transport": "ilink_long_poll",
154
+ "bot_name": "DeepScientist",
155
+ "command_prefix": "/",
156
+ "base_url": "https://ilinkai.weixin.qq.com",
157
+ "cdn_base_url": "https://novac2c.cdn.weixin.qq.com/c2c",
158
+ "bot_type": "3",
159
+ "bot_token": None,
160
+ "bot_token_env": None,
161
+ "account_id": None,
162
+ "login_user_id": None,
163
+ "route_tag": None,
164
+ "dm_policy": "pairing",
165
+ "allow_from": [],
166
+ "group_policy": "disabled",
167
+ "group_allow_from": [],
168
+ "groups": [],
169
+ "auto_bind_dm_to_active_quest": True,
170
+ },
151
171
  "telegram": {
152
172
  "enabled": False,
153
173
  "profiles": [],