@researai/deepscientist 1.5.9 → 1.5.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/README.md +107 -94
  2. package/assets/branding/connector-qq.png +0 -0
  3. package/assets/branding/connector-rokid.png +0 -0
  4. package/assets/branding/connector-weixin.png +0 -0
  5. package/assets/branding/projects.png +0 -0
  6. package/bin/ds.js +168 -9
  7. package/docs/assets/branding/projects.png +0 -0
  8. package/docs/en/00_QUICK_START.md +308 -70
  9. package/docs/en/01_SETTINGS_REFERENCE.md +3 -0
  10. package/docs/en/02_START_RESEARCH_GUIDE.md +112 -0
  11. package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +62 -179
  12. package/docs/en/09_DOCTOR.md +41 -5
  13. package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +137 -0
  14. package/docs/en/11_LICENSE_AND_RISK.md +256 -0
  15. package/docs/en/12_GUIDED_WORKFLOW_TOUR.md +427 -0
  16. package/docs/en/13_CORE_ARCHITECTURE_GUIDE.md +297 -0
  17. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
  18. package/docs/en/99_ACKNOWLEDGEMENTS.md +4 -1
  19. package/docs/en/README.md +79 -0
  20. package/docs/images/lingzhu/rokid-agent-platform-create.png +0 -0
  21. package/docs/images/weixin/weixin-plugin-entry.png +0 -0
  22. package/docs/images/weixin/weixin-plugin-entry.svg +33 -0
  23. package/docs/images/weixin/weixin-qr-confirm.svg +30 -0
  24. package/docs/images/weixin/weixin-quest-media-flow.svg +44 -0
  25. package/docs/images/weixin/weixin-settings-bind.svg +57 -0
  26. package/docs/zh/00_QUICK_START.md +315 -74
  27. package/docs/zh/01_SETTINGS_REFERENCE.md +3 -0
  28. package/docs/zh/02_START_RESEARCH_GUIDE.md +112 -0
  29. package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +62 -193
  30. package/docs/zh/09_DOCTOR.md +41 -5
  31. package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +144 -0
  32. package/docs/zh/11_LICENSE_AND_RISK.md +256 -0
  33. package/docs/zh/12_GUIDED_WORKFLOW_TOUR.md +423 -0
  34. package/docs/zh/13_CORE_ARCHITECTURE_GUIDE.md +296 -0
  35. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +506 -0
  36. package/docs/zh/99_ACKNOWLEDGEMENTS.md +4 -1
  37. package/docs/zh/README.md +126 -0
  38. package/install.sh +0 -34
  39. package/package.json +2 -2
  40. package/pyproject.toml +1 -1
  41. package/src/deepscientist/__init__.py +1 -1
  42. package/src/deepscientist/annotations.py +343 -0
  43. package/src/deepscientist/artifact/arxiv.py +484 -37
  44. package/src/deepscientist/artifact/service.py +574 -108
  45. package/src/deepscientist/arxiv_library.py +275 -0
  46. package/src/deepscientist/bash_exec/service.py +9 -0
  47. package/src/deepscientist/bridges/builtins.py +2 -0
  48. package/src/deepscientist/bridges/connectors.py +447 -0
  49. package/src/deepscientist/channels/__init__.py +2 -0
  50. package/src/deepscientist/channels/builtins.py +3 -1
  51. package/src/deepscientist/channels/qq.py +1 -1
  52. package/src/deepscientist/channels/qq_gateway.py +1 -1
  53. package/src/deepscientist/channels/relay.py +7 -1
  54. package/src/deepscientist/channels/weixin.py +59 -0
  55. package/src/deepscientist/channels/weixin_ilink.py +317 -0
  56. package/src/deepscientist/config/models.py +22 -2
  57. package/src/deepscientist/config/service.py +431 -60
  58. package/src/deepscientist/connector/__init__.py +4 -0
  59. package/src/deepscientist/connector/connector_profiles.py +481 -0
  60. package/src/deepscientist/connector/lingzhu_support.py +668 -0
  61. package/src/deepscientist/connector/qq_profiles.py +206 -0
  62. package/src/deepscientist/connector/weixin_support.py +663 -0
  63. package/src/deepscientist/connector_profiles.py +1 -374
  64. package/src/deepscientist/connector_runtime.py +2 -0
  65. package/src/deepscientist/daemon/api/handlers.py +165 -5
  66. package/src/deepscientist/daemon/api/router.py +13 -1
  67. package/src/deepscientist/daemon/app.py +1130 -61
  68. package/src/deepscientist/doctor.py +5 -2
  69. package/src/deepscientist/gitops/diff.py +120 -29
  70. package/src/deepscientist/lingzhu_support.py +1 -182
  71. package/src/deepscientist/mcp/server.py +11 -4
  72. package/src/deepscientist/prompts/builder.py +15 -0
  73. package/src/deepscientist/qq_profiles.py +1 -196
  74. package/src/deepscientist/quest/node_traces.py +23 -0
  75. package/src/deepscientist/quest/service.py +112 -43
  76. package/src/deepscientist/quest/stage_views.py +71 -5
  77. package/src/deepscientist/runners/codex.py +55 -3
  78. package/src/deepscientist/weixin_support.py +1 -0
  79. package/src/prompts/connectors/lingzhu.md +3 -1
  80. package/src/prompts/connectors/weixin.md +230 -0
  81. package/src/prompts/system.md +2 -0
  82. package/src/tui/package.json +1 -1
  83. package/src/ui/dist/assets/{AiManusChatView-BKZ103sn.js → AiManusChatView-D0mTXG4-.js} +156 -48
  84. package/src/ui/dist/assets/{AnalysisPlugin-mTTzGAlK.js → AnalysisPlugin-Db0cTXxm.js} +1 -1
  85. package/src/ui/dist/assets/{CliPlugin-BH58n3GY.js → CliPlugin-DrV8je02.js} +164 -9
  86. package/src/ui/dist/assets/{CodeEditorPlugin-BKGRUH7e.js → CodeEditorPlugin-QXMSCH71.js} +8 -8
  87. package/src/ui/dist/assets/{CodeViewerPlugin-BMADwFWJ.js → CodeViewerPlugin-7hhtWj_E.js} +5 -5
  88. package/src/ui/dist/assets/{DocViewerPlugin-ZOnTIHLN.js → DocViewerPlugin-BWMSnRJe.js} +3 -3
  89. package/src/ui/dist/assets/{GitDiffViewerPlugin-CQ7h1Djm.js → GitDiffViewerPlugin-7J9h9Vy_.js} +20 -21
  90. package/src/ui/dist/assets/{ImageViewerPlugin-GVS5MsnC.js → ImageViewerPlugin-CHJl_0lr.js} +5 -5
  91. package/src/ui/dist/assets/{LabCopilotPanel-BZNv1JML.js → LabCopilotPanel-1qSow1es.js} +11 -11
  92. package/src/ui/dist/assets/{LabPlugin-TWcJsdQA.js → LabPlugin-eQpPPCEp.js} +2 -1
  93. package/src/ui/dist/assets/{LatexPlugin-DIjHiR2x.js → LatexPlugin-BwRfi89Z.js} +7 -7
  94. package/src/ui/dist/assets/{MarkdownViewerPlugin-D3ooGAH0.js → MarkdownViewerPlugin-836PVQWV.js} +4 -4
  95. package/src/ui/dist/assets/{MarketplacePlugin-DfVfE9hN.js → MarketplacePlugin-C2y_556i.js} +3 -3
  96. package/src/ui/dist/assets/{NotebookEditor-s8JhzuX1.js → NotebookEditor-BRzJbGsn.js} +12 -12
  97. package/src/ui/dist/assets/{NotebookEditor-DDl0_Mc0.js → NotebookEditor-DIX7Mlzu.js} +1 -1
  98. package/src/ui/dist/assets/{PdfLoader-C2Sf6SJM.js → PdfLoader-DzRaTAlq.js} +14 -7
  99. package/src/ui/dist/assets/{PdfMarkdownPlugin-CXFLoIsa.js → PdfMarkdownPlugin-DZUfIUnp.js} +73 -6
  100. package/src/ui/dist/assets/{PdfViewerPlugin-BYTmz2fK.js → PdfViewerPlugin-BwtICzue.js} +103 -34
  101. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +3627 -0
  102. package/src/ui/dist/assets/{SearchPlugin-CjWBI1O9.js → SearchPlugin-DHeIAMsx.js} +1 -1
  103. package/src/ui/dist/assets/{TextViewerPlugin-DdOBU3-S.js → TextViewerPlugin-C3tCmFox.js} +5 -4
  104. package/src/ui/dist/assets/{VNCViewer-B8HGgLwQ.js → VNCViewer-CQsKVm3t.js} +10 -10
  105. package/src/ui/dist/assets/bot-BEA2vWuK.js +21 -0
  106. package/src/ui/dist/assets/branding/logo-rokid.png +0 -0
  107. package/src/ui/dist/assets/browser-BAcuE0Xj.js +2895 -0
  108. package/src/ui/dist/assets/{code-BWAY76JP.js → code-XfbSR8K2.js} +1 -1
  109. package/src/ui/dist/assets/{file-content-C1NwU5oQ.js → file-content-BjxNaIfy.js} +1 -1
  110. package/src/ui/dist/assets/{file-diff-panel-CywslwB9.js → file-diff-panel-D_lLVQk0.js} +1 -1
  111. package/src/ui/dist/assets/{file-socket-B4kzuOBQ.js → file-socket-D9x_5vlY.js} +1 -1
  112. package/src/ui/dist/assets/{image-D-NZM-6P.js → image-BhWT33W1.js} +1 -1
  113. package/src/ui/dist/assets/{index-DHZJ_0TI.js → index--c4iXtuy.js} +12 -12
  114. package/src/ui/dist/assets/{index-BdM1Gqfr.js → index-BDxipwrC.js} +2 -2
  115. package/src/ui/dist/assets/{index-7Chr1g9c.js → index-DZTZ8mWP.js} +14221 -9523
  116. package/src/ui/dist/assets/{index-DGIYDuTv.css → index-Dqj-Mjb4.css} +2 -13
  117. package/src/ui/dist/assets/index-PJbSbPTy.js +25 -0
  118. package/src/ui/dist/assets/{monaco-Cb2uKKe6.js → monaco-K8izTGgo.js} +1 -1
  119. package/src/ui/dist/assets/{pdf-effect-queue-DSw_D3RV.js → pdf-effect-queue-DfBors6y.js} +16 -1
  120. package/src/ui/dist/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
  121. package/src/ui/dist/assets/{popover-Bg72DGgT.js → popover-yFK1J4fL.js} +1 -1
  122. package/src/ui/dist/assets/{project-sync-Ce_0BglY.js → project-sync-PENr2zcz.js} +1 -74
  123. package/src/ui/dist/assets/select-CAbJDfYv.js +1690 -0
  124. package/src/ui/dist/assets/{sigma-DPaACDrh.js → sigma-DEuYJqTl.js} +1 -1
  125. package/src/ui/dist/assets/{index-CDxNdQdz.js → square-check-big-omoSUmcd.js} +2 -13
  126. package/src/ui/dist/assets/{trash-BvTgE5__.js → trash--F119N47.js} +1 -1
  127. package/src/ui/dist/assets/{useCliAccess-CgPeMOwP.js → useCliAccess-D31UR23I.js} +1 -1
  128. package/src/ui/dist/assets/{useFileDiffOverlay-xPhz7P5B.js → useFileDiffOverlay-BH6KcMzq.js} +1 -1
  129. package/src/ui/dist/assets/{wrap-text-C3Un3YQr.js → wrap-text-CZ613PM5.js} +1 -1
  130. package/src/ui/dist/assets/{zoom-out-BgxLa0Ri.js → zoom-out-BgDLAv3z.js} +1 -1
  131. package/src/ui/dist/index.html +2 -2
  132. package/src/ui/dist/assets/AutoFigurePlugin-BGxN8Umr.css +0 -3056
  133. package/src/ui/dist/assets/AutoFigurePlugin-C_wWw4AP.js +0 -8149
  134. package/src/ui/dist/assets/PdfViewerPlugin-BJXtIwj_.css +0 -260
  135. package/src/ui/dist/assets/Stepper-B0Dd8CxK.js +0 -158
  136. package/src/ui/dist/assets/bibtex-CKaefIN2.js +0 -189
  137. package/src/ui/dist/assets/file-utils-H2fjA46S.js +0 -109
  138. package/src/ui/dist/assets/message-square-BzjLiXir.js +0 -16
  139. package/src/ui/dist/assets/pdfjs-DU1YE8WO.js +0 -3
  140. package/src/ui/dist/assets/tooltip-C_mA6R0w.js +0 -108
@@ -1,374 +1 @@
1
- from __future__ import annotations
2
-
3
- from copy import deepcopy
4
- from typing import Any
5
-
6
- from .connector_runtime import infer_connector_transport
7
- from .shared import slugify
8
-
9
-
10
- PROFILEABLE_CONNECTOR_NAMES = ("telegram", "discord", "slack", "feishu", "whatsapp")
11
-
12
-
13
- def _normalize_secret_pair(payload: dict[str, Any], direct_key: str, env_key: str) -> None:
14
- direct = _as_text(payload.get(direct_key))
15
- env_name = _as_text(payload.get(env_key))
16
- payload[direct_key] = direct
17
- payload[env_key] = None if direct else env_name
18
-
19
-
20
- CONNECTOR_PROFILE_SPECS: dict[str, dict[str, Any]] = {
21
- "telegram": {
22
- "profile_id_prefix": "telegram-profile",
23
- "shared_fields": (
24
- "enabled",
25
- "profiles",
26
- "transport",
27
- "bot_name",
28
- "bot_token",
29
- "bot_token_env",
30
- "command_prefix",
31
- "dm_policy",
32
- "allow_from",
33
- "group_policy",
34
- "group_allow_from",
35
- "groups",
36
- "require_mention_in_groups",
37
- "auto_bind_dm_to_active_quest",
38
- ),
39
- "profile_defaults": {
40
- "profile_id": None,
41
- "enabled": True,
42
- "transport": "polling",
43
- "bot_name": "DeepScientist",
44
- "bot_token": None,
45
- "bot_token_env": None,
46
- },
47
- "profile_fields": (
48
- "enabled",
49
- "transport",
50
- "bot_name",
51
- "bot_token",
52
- "bot_token_env",
53
- ),
54
- "migration_keys": ("bot_token",),
55
- "label_fields": ("bot_name",),
56
- "id_fields": ("bot_name",),
57
- "secret_pairs": (("bot_token", "bot_token_env"),),
58
- },
59
- "discord": {
60
- "profile_id_prefix": "discord-profile",
61
- "shared_fields": (
62
- "enabled",
63
- "profiles",
64
- "transport",
65
- "bot_name",
66
- "bot_token",
67
- "bot_token_env",
68
- "command_prefix",
69
- "application_id",
70
- "dm_policy",
71
- "allow_from",
72
- "group_policy",
73
- "group_allow_from",
74
- "groups",
75
- "require_mention_in_groups",
76
- "auto_bind_dm_to_active_quest",
77
- "guild_allowlist",
78
- ),
79
- "profile_defaults": {
80
- "profile_id": None,
81
- "enabled": True,
82
- "transport": "gateway",
83
- "bot_name": "DeepScientist",
84
- "bot_token": None,
85
- "bot_token_env": None,
86
- "application_id": None,
87
- },
88
- "profile_fields": (
89
- "enabled",
90
- "transport",
91
- "bot_name",
92
- "bot_token",
93
- "bot_token_env",
94
- "application_id",
95
- ),
96
- "migration_keys": ("bot_token", "application_id"),
97
- "label_fields": ("bot_name", "application_id"),
98
- "id_fields": ("application_id", "bot_name"),
99
- "secret_pairs": (("bot_token", "bot_token_env"),),
100
- },
101
- "slack": {
102
- "profile_id_prefix": "slack-profile",
103
- "shared_fields": (
104
- "enabled",
105
- "profiles",
106
- "transport",
107
- "bot_name",
108
- "bot_token",
109
- "bot_token_env",
110
- "bot_user_id",
111
- "app_token",
112
- "app_token_env",
113
- "command_prefix",
114
- "dm_policy",
115
- "allow_from",
116
- "group_policy",
117
- "group_allow_from",
118
- "groups",
119
- "require_mention_in_groups",
120
- "auto_bind_dm_to_active_quest",
121
- ),
122
- "profile_defaults": {
123
- "profile_id": None,
124
- "enabled": True,
125
- "transport": "socket_mode",
126
- "bot_name": "DeepScientist",
127
- "bot_token": None,
128
- "bot_token_env": None,
129
- "bot_user_id": None,
130
- "app_token": None,
131
- "app_token_env": None,
132
- },
133
- "profile_fields": (
134
- "enabled",
135
- "transport",
136
- "bot_name",
137
- "bot_token",
138
- "bot_token_env",
139
- "bot_user_id",
140
- "app_token",
141
- "app_token_env",
142
- ),
143
- "migration_keys": ("bot_token", "bot_user_id", "app_token"),
144
- "label_fields": ("bot_name", "bot_user_id"),
145
- "id_fields": ("bot_user_id", "bot_name"),
146
- "secret_pairs": (
147
- ("bot_token", "bot_token_env"),
148
- ("app_token", "app_token_env"),
149
- ),
150
- },
151
- "feishu": {
152
- "profile_id_prefix": "feishu-profile",
153
- "shared_fields": (
154
- "enabled",
155
- "profiles",
156
- "transport",
157
- "bot_name",
158
- "app_id",
159
- "app_secret",
160
- "app_secret_env",
161
- "api_base_url",
162
- "command_prefix",
163
- "dm_policy",
164
- "allow_from",
165
- "group_policy",
166
- "group_allow_from",
167
- "groups",
168
- "require_mention_in_groups",
169
- "auto_bind_dm_to_active_quest",
170
- ),
171
- "profile_defaults": {
172
- "profile_id": None,
173
- "enabled": True,
174
- "transport": "long_connection",
175
- "bot_name": "DeepScientist",
176
- "app_id": None,
177
- "app_secret": None,
178
- "app_secret_env": None,
179
- "api_base_url": "https://open.feishu.cn",
180
- },
181
- "profile_fields": (
182
- "enabled",
183
- "transport",
184
- "bot_name",
185
- "app_id",
186
- "app_secret",
187
- "app_secret_env",
188
- "api_base_url",
189
- ),
190
- "migration_keys": ("app_id", "app_secret"),
191
- "label_fields": ("bot_name", "app_id"),
192
- "id_fields": ("app_id", "bot_name"),
193
- "secret_pairs": (("app_secret", "app_secret_env"),),
194
- },
195
- "whatsapp": {
196
- "profile_id_prefix": "whatsapp-profile",
197
- "shared_fields": (
198
- "enabled",
199
- "profiles",
200
- "transport",
201
- "bot_name",
202
- "auth_method",
203
- "session_dir",
204
- "command_prefix",
205
- "dm_policy",
206
- "allow_from",
207
- "group_policy",
208
- "group_allow_from",
209
- "groups",
210
- "auto_bind_dm_to_active_quest",
211
- ),
212
- "profile_defaults": {
213
- "profile_id": None,
214
- "enabled": True,
215
- "transport": "local_session",
216
- "bot_name": "DeepScientist",
217
- "auth_method": "qr_browser",
218
- "session_dir": "~/.deepscientist/connectors/whatsapp",
219
- },
220
- "profile_fields": (
221
- "enabled",
222
- "transport",
223
- "bot_name",
224
- "auth_method",
225
- "session_dir",
226
- ),
227
- "migration_keys": ("session_dir",),
228
- "label_fields": ("bot_name",),
229
- "id_fields": ("bot_name",),
230
- "secret_pairs": (),
231
- },
232
- }
233
-
234
-
235
- def _as_text(value: Any) -> str | None:
236
- text = str(value or "").strip()
237
- return text or None
238
-
239
-
240
- def _profile_seed(connector_name: str, raw: dict[str, Any], *, index: int) -> str:
241
- spec = CONNECTOR_PROFILE_SPECS[connector_name]
242
- explicit = _as_text(raw.get("profile_id"))
243
- if explicit:
244
- return explicit
245
- for key in spec["id_fields"]:
246
- candidate = _as_text(raw.get(key))
247
- if candidate:
248
- return f"{connector_name}-{candidate}"
249
- return f"{spec['profile_id_prefix']}-{index:03d}"
250
-
251
-
252
- def _unique_profile_id(seed: str, *, prefix: str, used: set[str]) -> str:
253
- base = slugify(seed, default=prefix)
254
- candidate = base
255
- suffix = 2
256
- while candidate in used:
257
- candidate = f"{base}-{suffix}"
258
- suffix += 1
259
- used.add(candidate)
260
- return candidate
261
-
262
-
263
- def default_connector_profile(connector_name: str) -> dict[str, Any]:
264
- spec = CONNECTOR_PROFILE_SPECS[connector_name]
265
- return deepcopy(spec["profile_defaults"])
266
-
267
-
268
- def connector_profile_label(connector_name: str, profile: dict[str, Any] | None) -> str:
269
- if not isinstance(profile, dict):
270
- return connector_name.capitalize()
271
- spec = CONNECTOR_PROFILE_SPECS[connector_name]
272
- parts = [_as_text(profile.get(key)) for key in spec["label_fields"]]
273
- filtered = [item for item in parts if item]
274
- return " · ".join(filtered) if filtered else connector_name.capitalize()
275
-
276
-
277
- def normalize_connector_config(connector_name: str, config: dict[str, Any] | None) -> dict[str, Any]:
278
- if connector_name not in CONNECTOR_PROFILE_SPECS:
279
- raise KeyError(f"Connector `{connector_name}` does not support generic profile normalization.")
280
- spec = CONNECTOR_PROFILE_SPECS[connector_name]
281
- payload = deepcopy(config or {})
282
- shared = {
283
- key: deepcopy(payload.get(key))
284
- for key in spec["shared_fields"]
285
- if key in payload
286
- }
287
- shared["profiles"] = []
288
- for direct_key, env_key in spec.get("secret_pairs", ()):
289
- _normalize_secret_pair(shared, direct_key, env_key)
290
-
291
- raw_profiles = payload.get("profiles")
292
- items = list(raw_profiles) if isinstance(raw_profiles, list) else []
293
- has_direct_migration_value = any(_as_text(payload.get(key)) for key in spec["migration_keys"])
294
- has_env_only_secret = bool(payload.get("enabled")) and any(
295
- _as_text(payload.get(env_key))
296
- for _, env_key in spec.get("secret_pairs", ())
297
- )
298
- if not items and (has_direct_migration_value or has_env_only_secret):
299
- items = [{key: payload.get(key) for key in spec["profile_fields"]}]
300
-
301
- used_ids: set[str] = set()
302
- profiles: list[dict[str, Any]] = []
303
- for index, raw in enumerate(items, start=1):
304
- if not isinstance(raw, dict):
305
- continue
306
- current = default_connector_profile(connector_name)
307
- for key in ("profile_id", *spec["profile_fields"]):
308
- if key in raw:
309
- current[key] = deepcopy(raw.get(key))
310
- current["enabled"] = bool(current.get("enabled", True))
311
- for key in spec["profile_fields"]:
312
- if key in {"enabled", "transport", "mode"}:
313
- continue
314
- if isinstance(current.get(key), list):
315
- continue
316
- if current.get(key) is None:
317
- continue
318
- current[key] = _as_text(current.get(key))
319
- current["transport"] = infer_connector_transport(connector_name, current)
320
- if "mode" in spec["profile_defaults"] or current.get("mode") is not None:
321
- current["mode"] = _as_text(current.get("mode")) or str(spec["profile_defaults"].get("mode") or "")
322
- for direct_key, env_key in spec.get("secret_pairs", ()):
323
- _normalize_secret_pair(current, direct_key, env_key)
324
- current["profile_id"] = _unique_profile_id(
325
- _profile_seed(connector_name, current, index=index),
326
- prefix=str(spec["profile_id_prefix"]),
327
- used=used_ids,
328
- )
329
- profiles.append(current)
330
-
331
- shared["transport"] = infer_connector_transport(connector_name, shared)
332
- shared["profiles"] = profiles
333
- if len(profiles) == 1:
334
- for key in spec["profile_fields"]:
335
- shared[key] = profiles[0].get(key)
336
- elif len(profiles) > 1:
337
- for direct_key, env_key in spec.get("secret_pairs", ()):
338
- shared[direct_key] = None
339
- shared[env_key] = None
340
- return shared
341
-
342
-
343
- def list_connector_profiles(connector_name: str, config: dict[str, Any] | None) -> list[dict[str, Any]]:
344
- normalized = normalize_connector_config(connector_name, config)
345
- profiles = normalized.get("profiles")
346
- return [dict(item) for item in profiles] if isinstance(profiles, list) else []
347
-
348
-
349
- def find_connector_profile(
350
- connector_name: str,
351
- config: dict[str, Any] | None,
352
- *,
353
- profile_id: str | None = None,
354
- ) -> dict[str, Any] | None:
355
- normalized_profile_id = _as_text(profile_id)
356
- for profile in list_connector_profiles(connector_name, config):
357
- if normalized_profile_id and str(profile.get("profile_id") or "").strip() == normalized_profile_id:
358
- return profile
359
- return None
360
-
361
-
362
- def merge_connector_profile_config(
363
- connector_name: str,
364
- shared_config: dict[str, Any] | None,
365
- profile: dict[str, Any],
366
- ) -> dict[str, Any]:
367
- normalized = normalize_connector_config(connector_name, shared_config)
368
- merged = deepcopy(normalized)
369
- merged.pop("profiles", None)
370
- for key in CONNECTOR_PROFILE_SPECS[connector_name]["profile_fields"]:
371
- merged[key] = profile.get(key)
372
- merged["profile_id"] = str(profile.get("profile_id") or "").strip() or None
373
- merged["enabled"] = bool(normalized.get("enabled", False)) and bool(profile.get("enabled", True))
374
- return merged
1
+ from .connector.connector_profiles import * # noqa: F401,F403
@@ -22,6 +22,8 @@ def infer_connector_transport(name: str, config: dict[str, Any] | None) -> str:
22
22
 
23
23
  if normalized == "qq":
24
24
  return "gateway_direct"
25
+ if normalized == "weixin":
26
+ return "ilink_long_poll"
25
27
  if normalized == "telegram":
26
28
  return "polling"
27
29
  if normalized == "discord":
@@ -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,6 +207,20 @@ 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
 
@@ -265,6 +278,18 @@ npm --prefix src/ui run build</pre>
265
278
  "no",
266
279
  "off",
267
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
+ }
268
293
  requested_baseline_ref = body.get("requested_baseline_ref")
269
294
  startup_contract = body.get("startup_contract")
270
295
  auto_start = body.get("auto_start") is True
@@ -289,6 +314,7 @@ npm --prefix src/ui run build</pre>
289
314
  preferred_connector_conversation_id=preferred_connector_conversation_id,
290
315
  requested_connector_bindings=requested_connector_bindings,
291
316
  force_connector_rebind=force_connector_rebind,
317
+ auto_bind_latest_connectors=auto_bind_latest_connectors,
292
318
  requested_baseline_ref=requested_baseline_ref if isinstance(requested_baseline_ref, dict) else None,
293
319
  startup_contract=startup_contract if isinstance(startup_contract, dict) else None,
294
320
  )
@@ -416,6 +442,7 @@ npm --prefix src/ui run build</pre>
416
442
  }
417
443
 
418
444
  def quest_bindings(self, quest_id: str, body: dict) -> dict | tuple[int, dict]:
445
+ previous_external = self.app._quest_external_binding(quest_id)
419
446
  requested_bindings = (
420
447
  [dict(item) for item in body.get("bindings") if isinstance(item, dict)]
421
448
  if isinstance(body.get("bindings"), list)
@@ -426,10 +453,24 @@ npm --prefix src/ui run build</pre>
426
453
  force_raw = body.get("force")
427
454
  force = bool(force_raw) and str(force_raw).strip().lower() not in {"0", "false", "no", "off"}
428
455
  if requested_bindings:
429
- return self.app.update_quest_bindings(quest_id, requested_bindings, force=force)
430
- if connector_name:
431
- return self.app.update_quest_connector_binding(quest_id, connector_name, conversation_id, force=force)
432
- 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
+ }
433
474
 
434
475
  def quest_session(self, quest_id: str) -> dict:
435
476
  snapshot = self.app.quest_service.snapshot_fast(quest_id)
@@ -1113,6 +1154,125 @@ npm --prefix src/ui run build</pre>
1113
1154
  def runs(self, quest_id: str) -> list[dict]:
1114
1155
  return self.app.quest_service.snapshot(quest_id).get("recent_runs", [])
1115
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
+
1116
1276
  def quest_memory(self, quest_id: str) -> list[dict]:
1117
1277
  quest_service = self._fresh_quest_service()
1118
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,6 +17,8 @@ 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"),
@@ -77,6 +81,14 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
77
81
  ("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/commands$"), "command"),
78
82
  ("POST", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/control$"), "quest_control"),
79
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"),
80
92
  ("POST", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/init$"), "latex_init"),
81
93
  ("POST", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/compile$"), "latex_compile"),
82
94
  ("GET", re.compile(r"^/api/v1/projects/(?P<project_id>[^/]+)/latex/(?P<folder_id>[^/]+)/builds$"), "latex_builds"),