@researai/deepscientist 1.5.2 → 1.5.4

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 (114) hide show
  1. package/README.md +22 -0
  2. package/bin/ds.js +399 -175
  3. package/docs/en/00_QUICK_START.md +22 -0
  4. package/docs/en/01_SETTINGS_REFERENCE.md +13 -4
  5. package/docs/en/99_ACKNOWLEDGEMENTS.md +1 -0
  6. package/docs/images/connectors/discord-setup-overview.svg +52 -0
  7. package/docs/images/connectors/feishu-setup-overview.svg +53 -0
  8. package/docs/images/connectors/slack-setup-overview.svg +51 -0
  9. package/docs/images/connectors/telegram-setup-overview.svg +55 -0
  10. package/docs/images/connectors/whatsapp-setup-overview.svg +51 -0
  11. package/docs/images/lingzhu/lingzhu-openclaw-config.svg +17 -0
  12. package/docs/images/lingzhu/lingzhu-platform-values.svg +16 -0
  13. package/docs/images/lingzhu/lingzhu-settings-overview.svg +30 -0
  14. package/docs/images/qq/tencent-cloud-qq-chat.png +0 -0
  15. package/docs/images/qq/tencent-cloud-qq-register.png +0 -0
  16. package/docs/images/quickstart/00-home.png +0 -0
  17. package/docs/images/quickstart/01-start-research.png +0 -0
  18. package/docs/images/quickstart/02-list-quest.png +0 -0
  19. package/docs/zh/00_QUICK_START.md +22 -0
  20. package/docs/zh/01_SETTINGS_REFERENCE.md +14 -5
  21. package/docs/zh/99_ACKNOWLEDGEMENTS.md +1 -0
  22. package/install.sh +120 -4
  23. package/package.json +8 -4
  24. package/pyproject.toml +1 -1
  25. package/src/deepscientist/__init__.py +1 -1
  26. package/src/deepscientist/artifact/service.py +1 -1
  27. package/src/deepscientist/bash_exec/monitor.py +23 -4
  28. package/src/deepscientist/bash_exec/runtime.py +3 -0
  29. package/src/deepscientist/bash_exec/service.py +132 -4
  30. package/src/deepscientist/bridges/base.py +12 -20
  31. package/src/deepscientist/bridges/connectors.py +2 -1
  32. package/src/deepscientist/channels/discord_gateway.py +27 -4
  33. package/src/deepscientist/channels/feishu_long_connection.py +41 -3
  34. package/src/deepscientist/channels/qq.py +524 -64
  35. package/src/deepscientist/channels/qq_gateway.py +24 -5
  36. package/src/deepscientist/channels/relay.py +429 -90
  37. package/src/deepscientist/channels/slack_socket.py +31 -7
  38. package/src/deepscientist/channels/telegram_polling.py +27 -3
  39. package/src/deepscientist/channels/whatsapp_local_session.py +32 -4
  40. package/src/deepscientist/cli.py +31 -1
  41. package/src/deepscientist/config/models.py +13 -43
  42. package/src/deepscientist/config/service.py +216 -157
  43. package/src/deepscientist/connector_profiles.py +346 -0
  44. package/src/deepscientist/connector_runtime.py +88 -43
  45. package/src/deepscientist/daemon/api/handlers.py +53 -16
  46. package/src/deepscientist/daemon/api/router.py +2 -2
  47. package/src/deepscientist/daemon/app.py +747 -228
  48. package/src/deepscientist/mcp/server.py +60 -7
  49. package/src/deepscientist/migration.py +114 -0
  50. package/src/deepscientist/network.py +78 -0
  51. package/src/deepscientist/prompts/builder.py +50 -4
  52. package/src/deepscientist/qq_profiles.py +186 -0
  53. package/src/deepscientist/quest/service.py +1 -1
  54. package/src/deepscientist/skills/installer.py +77 -1
  55. package/src/prompts/connectors/qq.md +42 -2
  56. package/src/prompts/system.md +162 -6
  57. package/src/skills/analysis-campaign/SKILL.md +19 -5
  58. package/src/skills/baseline/SKILL.md +66 -31
  59. package/src/skills/decision/SKILL.md +1 -1
  60. package/src/skills/experiment/SKILL.md +11 -5
  61. package/src/skills/finalize/SKILL.md +1 -1
  62. package/src/skills/idea/SKILL.md +246 -4
  63. package/src/skills/intake-audit/SKILL.md +1 -1
  64. package/src/skills/rebuttal/SKILL.md +1 -1
  65. package/src/skills/review/SKILL.md +1 -1
  66. package/src/skills/scout/SKILL.md +1 -1
  67. package/src/skills/write/SKILL.md +152 -2
  68. package/src/tui/package.json +1 -1
  69. package/src/ui/dist/assets/{AiManusChatView-CZpg376x.js → AiManusChatView-BGLArZRn.js} +14 -37
  70. package/src/ui/dist/assets/{AnalysisPlugin-CtHA22g3.js → AnalysisPlugin-BgDGSigG.js} +1 -1
  71. package/src/ui/dist/assets/{AutoFigurePlugin-BSWmLMmF.js → AutoFigurePlugin-B65HD7L4.js} +5 -5
  72. package/src/ui/dist/assets/{CliPlugin-CJ7jdm_s.js → CliPlugin-CUqgsFHC.js} +17 -110
  73. package/src/ui/dist/assets/{CodeEditorPlugin-DhInVGFf.js → CodeEditorPlugin-CF5EdvaS.js} +8 -8
  74. package/src/ui/dist/assets/{CodeViewerPlugin-D1n8S9r5.js → CodeViewerPlugin-DEeU063D.js} +5 -5
  75. package/src/ui/dist/assets/{DocViewerPlugin-C4XM_kqk.js → DocViewerPlugin-Df-FuDlZ.js} +3 -3
  76. package/src/ui/dist/assets/{GitDiffViewerPlugin-W6kS9r6v.js → GitDiffViewerPlugin-RAnNaRxM.js} +1 -1
  77. package/src/ui/dist/assets/{ImageViewerPlugin-DPeUx_Oz.js → ImageViewerPlugin-DXJ0ZJGg.js} +5 -5
  78. package/src/ui/dist/assets/{LabCopilotPanel-eAelUaub.js → LabCopilotPanel-BlO-sKsj.js} +10 -10
  79. package/src/ui/dist/assets/{LabPlugin-BbOrBxKY.js → LabPlugin-BajPZW5v.js} +1 -1
  80. package/src/ui/dist/assets/{LatexPlugin-C-HhkVXY.js → LatexPlugin-F1OEol8D.js} +7 -7
  81. package/src/ui/dist/assets/{MarkdownViewerPlugin-BDIzIBfh.js → MarkdownViewerPlugin-MhUupqwT.js} +4 -4
  82. package/src/ui/dist/assets/{MarketplacePlugin-DAOJphwr.js → MarketplacePlugin-DxhIEsv0.js} +3 -3
  83. package/src/ui/dist/assets/{NotebookEditor-BsoMvDoU.js → NotebookEditor-q7TkhewC.js} +1 -1
  84. package/src/ui/dist/assets/{PdfLoader-fiC7RtHf.js → PdfLoader-B8ZOTKFc.js} +1 -1
  85. package/src/ui/dist/assets/{PdfMarkdownPlugin-C5OxZBFK.js → PdfMarkdownPlugin-xFPvzvWh.js} +3 -3
  86. package/src/ui/dist/assets/{PdfViewerPlugin-CAbxQebk.js → PdfViewerPlugin-EjEcsIB8.js} +10 -10
  87. package/src/ui/dist/assets/{SearchPlugin-SE33Lb9B.js → SearchPlugin-ixY-1lgW.js} +1 -1
  88. package/src/ui/dist/assets/{Stepper-0Av7GfV7.js → Stepper-gYFK2Pgz.js} +1 -1
  89. package/src/ui/dist/assets/{TextViewerPlugin-Daf2gJDI.js → TextViewerPlugin-Cym6pv_n.js} +4 -4
  90. package/src/ui/dist/assets/{VNCViewer-BKrMUIOX.js → VNCViewer-BPmIHcmK.js} +9 -9
  91. package/src/ui/dist/assets/{bibtex-JBdOEe45.js → bibtex-Btv6Wi7f.js} +1 -1
  92. package/src/ui/dist/assets/{code-B0TDFCZz.js → code-BlG7g85c.js} +1 -1
  93. package/src/ui/dist/assets/{file-content-3YtrSacz.js → file-content-DBT5OfTZ.js} +1 -1
  94. package/src/ui/dist/assets/{file-diff-panel-CJEg5OG1.js → file-diff-panel-BWXYzqHk.js} +1 -1
  95. package/src/ui/dist/assets/{file-socket-CYQYdmB1.js → file-socket-wDlx6byM.js} +1 -1
  96. package/src/ui/dist/assets/{file-utils-Cd1C9Ppl.js → file-utils-Ba3nJmH0.js} +1 -1
  97. package/src/ui/dist/assets/{image-B33ctrvC.js → image-BwtCyguk.js} +1 -1
  98. package/src/ui/dist/assets/{index-BNQWqmJ2.js → index-B-2scqCJ.js} +11 -11
  99. package/src/ui/dist/assets/{index-BVXsmS7V.js → index-Bz5AaWL7.js} +52383 -51440
  100. package/src/ui/dist/assets/{index-Buw_N1VQ.js → index-CfRpE209.js} +2 -2
  101. package/src/ui/dist/assets/{index-9CLPVeZh.js → index-DcqvKzeJ.js} +1 -1
  102. package/src/ui/dist/assets/{index-SwmFAld3.css → index-DpMZw8aM.css} +49 -2
  103. package/src/ui/dist/assets/{message-square-D0cUJ9yU.js → message-square-BnlyWVH0.js} +1 -1
  104. package/src/ui/dist/assets/{monaco-UZLYkp2n.js → monaco-CXe0pAVe.js} +1 -1
  105. package/src/ui/dist/assets/{popover-CTeiY-dK.js → popover-BCHmVhHj.js} +1 -1
  106. package/src/ui/dist/assets/{project-sync-Dbs01Xky.js → project-sync-Brk6kaOD.js} +1 -1
  107. package/src/ui/dist/assets/{sigma-CM08S-xT.js → sigma-D72eSUep.js} +1 -1
  108. package/src/ui/dist/assets/{tooltip-pDtzvU9p.js → tooltip-BMWd0dqX.js} +1 -1
  109. package/src/ui/dist/assets/{trash-YvPCP-da.js → trash-BIt_eWIS.js} +1 -1
  110. package/src/ui/dist/assets/{useCliAccess-Bavi74Ac.js → useCliAccess-N1hkTRrR.js} +1 -1
  111. package/src/ui/dist/assets/{useFileDiffOverlay-CVXY6oeg.js → useFileDiffOverlay-DPRPv6rv.js} +1 -1
  112. package/src/ui/dist/assets/{wrap-text-Cf4flRW7.js → wrap-text-E5-UheyP.js} +1 -1
  113. package/src/ui/dist/assets/{zoom-out-Hb0Z1YpT.js → zoom-out-D4TR-ZZ_.js} +1 -1
  114. package/src/ui/dist/index.html +2 -2
@@ -4,11 +4,12 @@ import json
4
4
  import threading
5
5
  from pathlib import Path
6
6
  from typing import Any, Callable
7
- from urllib.request import Request, urlopen
7
+ from urllib.request import Request
8
8
 
9
9
  from websockets.exceptions import ConnectionClosed
10
- from websockets.sync.client import connect as websocket_connect
11
10
 
11
+ from ..connector_runtime import format_conversation_id
12
+ from ..network import urlopen_with_proxy as urlopen, websocket_connect_with_proxy as websocket_connect
12
13
  from ..shared import read_json, utc_now, write_json
13
14
 
14
15
 
@@ -20,15 +21,23 @@ class SlackSocketModeService:
20
21
  config: dict[str, Any],
21
22
  on_event: Callable[[dict[str, Any]], None],
22
23
  log: Callable[[str, str], None] | None = None,
24
+ profile_id: str | None = None,
25
+ profile_label: str | None = None,
26
+ encode_profile_id: bool = False,
23
27
  ) -> None:
24
28
  self.home = home
25
29
  self.config = config
26
30
  self.on_event = on_event
27
31
  self.log = log or self._default_log
32
+ self.profile_id = str(profile_id or "").strip() or None
33
+ self.profile_label = str(profile_label or "").strip() or None
34
+ self._encode_profile_id = bool(encode_profile_id and self.profile_id)
28
35
  self._thread: threading.Thread | None = None
29
36
  self._stop_event = threading.Event()
30
37
  self._connection = None
31
38
  self._root = home / "logs" / "connectors" / "slack"
39
+ if self.profile_id:
40
+ self._root = self._root / "profiles" / self.profile_id
32
41
  self._runtime_path = self._root / "runtime.json"
33
42
 
34
43
  def start(self) -> bool:
@@ -64,7 +73,7 @@ class SlackSocketModeService:
64
73
  self._thread = threading.Thread(
65
74
  target=self._run,
66
75
  daemon=True,
67
- name="deepscientist-slack-socket-mode",
76
+ name=f"deepscientist-slack-socket-mode-{self.profile_id or 'default'}",
68
77
  )
69
78
  self._thread.start()
70
79
  return True
@@ -208,14 +217,15 @@ class SlackSocketModeService:
208
217
  "sender_id": sender_id,
209
218
  "sender_name": str(event.get("username") or sender_id).strip(),
210
219
  "message_id": str(event.get("ts") or event.get("event_ts") or "").strip(),
211
- "conversation_id": f"slack:{chat_type}:{channel_id}",
220
+ "conversation_id": self._conversation_id(chat_type, channel_id),
221
+ "profile_id": self.profile_id,
222
+ "profile_label": self.profile_label,
212
223
  "text": normalized_text,
213
224
  "mentioned": mentioned,
214
225
  "raw_event": payload,
215
226
  }
216
227
 
217
- @staticmethod
218
- def _normalize_slash_command(payload: dict[str, Any]) -> dict[str, Any] | None:
228
+ def _normalize_slash_command(self, payload: dict[str, Any]) -> dict[str, Any] | None:
219
229
  channel_id = str(payload.get("channel_id") or "").strip()
220
230
  if not channel_id:
221
231
  return None
@@ -232,12 +242,22 @@ class SlackSocketModeService:
232
242
  "sender_id": sender_id,
233
243
  "sender_name": str(payload.get("user_name") or sender_id).strip(),
234
244
  "message_id": str(payload.get("trigger_id") or "").strip(),
235
- "conversation_id": f"slack:{chat_type}:{channel_id}",
245
+ "conversation_id": self._conversation_id(chat_type, channel_id),
246
+ "profile_id": self.profile_id,
247
+ "profile_label": self.profile_label,
236
248
  "text": combined,
237
249
  "mentioned": True,
238
250
  "raw_event": payload,
239
251
  }
240
252
 
253
+ def _conversation_id(self, chat_type: str, chat_id: str) -> str:
254
+ return format_conversation_id(
255
+ "slack",
256
+ chat_type,
257
+ chat_id,
258
+ profile_id=self.profile_id if self._encode_profile_id else None,
259
+ )
260
+
241
261
  @staticmethod
242
262
  def _infer_channel_type(channel_id: str) -> str:
243
263
  if str(channel_id).startswith("D"):
@@ -321,6 +341,10 @@ class SlackSocketModeService:
321
341
  state = read_json(self._runtime_path, {}) or {}
322
342
  if not isinstance(state, dict):
323
343
  state = {}
344
+ if self.profile_id:
345
+ state["profile_id"] = self.profile_id
346
+ if self.profile_label:
347
+ state["profile_label"] = self.profile_label
324
348
  state.update(patch)
325
349
  write_json(self._runtime_path, state)
326
350
 
@@ -4,8 +4,10 @@ import json
4
4
  import threading
5
5
  from pathlib import Path
6
6
  from typing import Any, Callable
7
- from urllib.request import Request, urlopen
7
+ from urllib.request import Request
8
8
 
9
+ from ..connector_runtime import format_conversation_id
10
+ from ..network import urlopen_with_proxy as urlopen
9
11
  from ..shared import read_json, utc_now, write_json
10
12
 
11
13
 
@@ -17,14 +19,22 @@ class TelegramPollingService:
17
19
  config: dict[str, Any],
18
20
  on_event: Callable[[dict[str, Any]], None],
19
21
  log: Callable[[str, str], None] | None = None,
22
+ profile_id: str | None = None,
23
+ profile_label: str | None = None,
24
+ encode_profile_id: bool = False,
20
25
  ) -> None:
21
26
  self.home = home
22
27
  self.config = config
23
28
  self.on_event = on_event
24
29
  self.log = log or self._default_log
30
+ self.profile_id = str(profile_id or "").strip() or None
31
+ self.profile_label = str(profile_label or "").strip() or None
32
+ self._encode_profile_id = bool(encode_profile_id and self.profile_id)
25
33
  self._thread: threading.Thread | None = None
26
34
  self._stop_event = threading.Event()
27
35
  self._root = home / "logs" / "connectors" / "telegram"
36
+ if self.profile_id:
37
+ self._root = self._root / "profiles" / self.profile_id
28
38
  self._runtime_path = self._root / "runtime.json"
29
39
 
30
40
  def start(self) -> bool:
@@ -59,7 +69,7 @@ class TelegramPollingService:
59
69
  self._thread = threading.Thread(
60
70
  target=self._run,
61
71
  daemon=True,
62
- name="deepscientist-telegram-polling",
72
+ name=f"deepscientist-telegram-polling-{self.profile_id or 'default'}",
63
73
  )
64
74
  self._thread.start()
65
75
  return True
@@ -219,12 +229,22 @@ class TelegramPollingService:
219
229
  "sender_id": sender_id,
220
230
  "sender_name": sender_name,
221
231
  "message_id": str(message.get("message_id") or "").strip(),
222
- "conversation_id": f"telegram:{chat_type}:{chat_id}",
232
+ "conversation_id": self._conversation_id(chat_type, chat_id),
233
+ "profile_id": self.profile_id,
234
+ "profile_label": self.profile_label,
223
235
  "text": normalized_text,
224
236
  "mentioned": mentioned,
225
237
  "raw_event": update,
226
238
  }
227
239
 
240
+ def _conversation_id(self, chat_type: str, chat_id: str) -> str:
241
+ return format_conversation_id(
242
+ "telegram",
243
+ chat_type,
244
+ chat_id,
245
+ profile_id=self.profile_id if self._encode_profile_id else None,
246
+ )
247
+
228
248
  @staticmethod
229
249
  def _normalize_command_target(text: str, *, bot_name: str) -> str:
230
250
  cleaned = str(text or "").strip()
@@ -272,6 +292,10 @@ class TelegramPollingService:
272
292
  state = read_json(self._runtime_path, {}) or {}
273
293
  if not isinstance(state, dict):
274
294
  state = {}
295
+ if self.profile_id:
296
+ state["profile_id"] = self.profile_id
297
+ if self.profile_label:
298
+ state["profile_label"] = self.profile_label
275
299
  state.update(patch)
276
300
  write_json(self._runtime_path, state)
277
301
 
@@ -5,6 +5,7 @@ import threading
5
5
  from pathlib import Path
6
6
  from typing import Any, Callable
7
7
 
8
+ from ..connector_runtime import format_conversation_id, parse_conversation_id
8
9
  from ..shared import append_jsonl, ensure_dir, read_json, utc_now, write_json
9
10
 
10
11
 
@@ -16,14 +17,22 @@ class WhatsAppLocalSessionService:
16
17
  config: dict[str, Any],
17
18
  on_event: Callable[[dict[str, Any]], None],
18
19
  log: Callable[[str, str], None] | None = None,
20
+ profile_id: str | None = None,
21
+ profile_label: str | None = None,
22
+ encode_profile_id: bool = False,
19
23
  ) -> None:
20
24
  self.home = home
21
25
  self.config = config
22
26
  self.on_event = on_event
23
27
  self.log = log or self._default_log
28
+ self.profile_id = str(profile_id or "").strip() or None
29
+ self.profile_label = str(profile_label or "").strip() or None
30
+ self._encode_profile_id = bool(encode_profile_id and self.profile_id)
24
31
  self._thread: threading.Thread | None = None
25
32
  self._stop_event = threading.Event()
26
33
  self._root = home / "logs" / "connectors" / "whatsapp"
34
+ if self.profile_id:
35
+ self._root = self._root / "profiles" / self.profile_id
27
36
  self._runtime_path = self._root / "runtime.json"
28
37
  self._cursor_path = self._root / "local_session.cursor.json"
29
38
 
@@ -60,7 +69,7 @@ class WhatsAppLocalSessionService:
60
69
  self._thread = threading.Thread(
61
70
  target=self._run,
62
71
  daemon=True,
63
- name="deepscientist-whatsapp-local-session",
72
+ name=f"deepscientist-whatsapp-local-session-{self.profile_id or 'default'}",
64
73
  )
65
74
  self._thread.start()
66
75
  return True
@@ -165,11 +174,16 @@ class WhatsAppLocalSessionService:
165
174
  )
166
175
  write_json(self._cursor_path, {"offset": offset})
167
176
 
168
- @staticmethod
169
- def _normalize_entry(payload: dict[str, Any]) -> dict[str, Any] | None:
177
+ def _normalize_entry(self, payload: dict[str, Any]) -> dict[str, Any] | None:
170
178
  if isinstance(payload.get("normalized"), dict):
171
179
  normalized = dict(payload["normalized"])
172
180
  normalized.setdefault("raw_event", payload)
181
+ parsed = parse_conversation_id(normalized.get("conversation_id"))
182
+ chat_type = str((parsed or {}).get("chat_type") or normalized.get("chat_type") or "direct").strip().lower() or "direct"
183
+ chat_id = str((parsed or {}).get("chat_id") or normalized.get("group_id") or normalized.get("direct_id") or "").strip() or "unknown"
184
+ normalized["conversation_id"] = self._conversation_id(chat_type, chat_id)
185
+ normalized["profile_id"] = self.profile_id
186
+ normalized["profile_label"] = self.profile_label
173
187
  return normalized
174
188
  conversation_id = str(payload.get("conversation_id") or "").strip()
175
189
  chat_type = str(payload.get("chat_type") or "").strip().lower()
@@ -201,12 +215,22 @@ class WhatsAppLocalSessionService:
201
215
  "sender_id": sender_id or chat_id,
202
216
  "sender_name": sender_name or sender_id or chat_id,
203
217
  "message_id": message_id,
204
- "conversation_id": conversation_id or f"whatsapp:{chat_type}:{chat_id}",
218
+ "conversation_id": self._conversation_id(chat_type, chat_id),
219
+ "profile_id": self.profile_id,
220
+ "profile_label": self.profile_label,
205
221
  "text": text,
206
222
  "mentioned": False,
207
223
  "raw_event": payload,
208
224
  }
209
225
 
226
+ def _conversation_id(self, chat_type: str, chat_id: str) -> str:
227
+ return format_conversation_id(
228
+ "whatsapp",
229
+ chat_type,
230
+ chat_id,
231
+ profile_id=self.profile_id if self._encode_profile_id else None,
232
+ )
233
+
210
234
  def _session_dir(self) -> Path | None:
211
235
  raw = str(self.config.get("session_dir") or "").strip()
212
236
  if not raw:
@@ -217,6 +241,10 @@ class WhatsAppLocalSessionService:
217
241
  state = read_json(self._runtime_path, {}) or {}
218
242
  if not isinstance(state, dict):
219
243
  state = {}
244
+ if self.profile_id:
245
+ state["profile_id"] = self.profile_id
246
+ if self.profile_label:
247
+ state["profile_label"] = self.profile_label
220
248
  state.update(patch)
221
249
  write_json(self._runtime_path, state)
222
250
 
@@ -9,7 +9,7 @@ import sys
9
9
  import webbrowser
10
10
  from pathlib import Path
11
11
  from urllib.error import URLError
12
- from urllib.request import Request, urlopen
12
+ from urllib.request import Request
13
13
 
14
14
  from .artifact import ArtifactService
15
15
  from .config import ConfigManager
@@ -17,6 +17,8 @@ from .daemon import DaemonApp
17
17
  from .doctor import render_doctor_report, run_doctor
18
18
  from .home import default_home, ensure_home_layout, repo_root
19
19
  from .memory import MemoryService
20
+ from .migration import migrate_deepscientist_root
21
+ from .network import configure_runtime_proxy, urlopen_with_proxy as urlopen
20
22
  from .prompts import PromptBuilder
21
23
  from .quest import QuestService
22
24
  from .registries import BaselineRegistry
@@ -36,6 +38,7 @@ def _local_ui_url(host: str, port: int) -> str:
36
38
  def build_parser() -> argparse.ArgumentParser:
37
39
  parser = argparse.ArgumentParser(prog="ds", description="DeepScientist Core skeleton")
38
40
  parser.add_argument("--home", default=None, help="Override DeepScientist home")
41
+ parser.add_argument("--proxy", default=None, help="Explicit outbound HTTP/WS proxy, for example `http://127.0.0.1:7890`.")
39
42
 
40
43
  subparsers = parser.add_subparsers(dest="command", required=True)
41
44
 
@@ -109,6 +112,9 @@ def build_parser() -> argparse.ArgumentParser:
109
112
  config_edit.add_argument("name", choices=("config", "runners", "connectors", "plugins", "mcp_servers"))
110
113
  config_subparsers.add_parser("validate")
111
114
 
115
+ migrate_parser = subparsers.add_parser("migrate")
116
+ migrate_parser.add_argument("target")
117
+
112
118
  return parser
113
119
 
114
120
 
@@ -445,9 +451,31 @@ def config_validate_command(home: Path) -> int:
445
451
  return 0
446
452
 
447
453
 
454
+ def migrate_command(home: Path, target: str) -> int:
455
+ try:
456
+ payload = migrate_deepscientist_root(home, Path(target))
457
+ except ValueError as exc:
458
+ print(
459
+ json.dumps(
460
+ {
461
+ "ok": False,
462
+ "source": str(home.expanduser().resolve()),
463
+ "target": str(Path(target).expanduser().resolve()),
464
+ "message": str(exc),
465
+ },
466
+ ensure_ascii=False,
467
+ indent=2,
468
+ )
469
+ )
470
+ return 1
471
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
472
+ return 0
473
+
474
+
448
475
  def main(argv: list[str] | None = None) -> int:
449
476
  parser = build_parser()
450
477
  args = parser.parse_args(argv)
478
+ configure_runtime_proxy(args.proxy)
451
479
  home = resolve_home(args)
452
480
 
453
481
  if args.command == "init":
@@ -492,6 +520,8 @@ def main(argv: list[str] | None = None) -> int:
492
520
  return config_edit_command(home, args.name)
493
521
  if args.command == "config" and args.config_command == "validate":
494
522
  return config_validate_command(home)
523
+ if args.command == "migrate":
524
+ return migrate_command(home, args.target)
495
525
  parser.error(f"Unknown command: {args.command}")
496
526
  return 1
497
527
 
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from pathlib import Path
5
5
 
6
-
7
6
  CONFIG_NAMES = ("config", "runners", "connectors", "plugins", "mcp_servers")
8
7
  REQUIRED_CONFIG_NAMES = ("config", "runners", "connectors")
9
8
  OPTIONAL_CONFIG_NAMES = ("plugins", "mcp_servers")
@@ -57,6 +56,10 @@ def default_config(home: Path) -> dict:
57
56
  "codex_ready": False,
58
57
  "codex_last_checked_at": None,
59
58
  "codex_last_result": {},
59
+ "locale_source": "default",
60
+ "locale_initialized_from_browser": False,
61
+ "locale_initialized_at": None,
62
+ "locale_initialized_browser_locale": None,
60
63
  },
61
64
  "connectors": {
62
65
  "auto_ack": True,
@@ -92,9 +95,9 @@ def default_runners() -> dict:
92
95
  "sandbox_mode": "workspace-write",
93
96
  "retry_on_failure": True,
94
97
  "retry_max_attempts": 5,
95
- "retry_initial_backoff_sec": 1.0,
96
- "retry_backoff_multiplier": 2.0,
97
- "retry_max_backoff_sec": 8.0,
98
+ "retry_initial_backoff_sec": 10.0,
99
+ "retry_backoff_multiplier": 6.0,
100
+ "retry_max_backoff_sec": 1800.0,
98
101
  # Increase MCP tool timeout so codex can wait for long `bash_exec(mode='await', ...)`
99
102
  # or other durable MCP calls without prematurely timing out.
100
103
  # Mirrors DS_2027's `codex.mcp_tool_timeout_sec` default.
@@ -122,6 +125,7 @@ def default_connectors() -> dict:
122
125
  "qq": {
123
126
  "enabled": False,
124
127
  "transport": "gateway_direct",
128
+ "profiles": [],
125
129
  "app_id": None,
126
130
  "app_secret": None,
127
131
  "app_secret_env": "QQ_APP_SECRET",
@@ -140,17 +144,12 @@ def default_connectors() -> dict:
140
144
  },
141
145
  "telegram": {
142
146
  "enabled": False,
147
+ "profiles": [],
143
148
  "transport": "polling",
144
- "mode": "relay",
145
149
  "bot_name": "DeepScientist",
146
150
  "command_prefix": "/",
147
151
  "bot_token": None,
148
152
  "bot_token_env": "TELEGRAM_BOT_TOKEN",
149
- "webhook_secret": None,
150
- "webhook_secret_env": "TELEGRAM_WEBHOOK_SECRET",
151
- "public_callback_url": None,
152
- "relay_url": None,
153
- "relay_auth_token": None,
154
153
  "dm_policy": "pairing",
155
154
  "allow_from": [],
156
155
  "group_policy": "open",
@@ -161,18 +160,13 @@ def default_connectors() -> dict:
161
160
  },
162
161
  "discord": {
163
162
  "enabled": False,
163
+ "profiles": [],
164
164
  "transport": "gateway",
165
- "mode": "relay",
166
165
  "bot_name": "DeepScientist",
167
166
  "command_prefix": "/",
168
167
  "bot_token": None,
169
168
  "bot_token_env": "DISCORD_BOT_TOKEN",
170
169
  "application_id": None,
171
- "public_key": None,
172
- "public_key_env": "DISCORD_PUBLIC_KEY",
173
- "public_interactions_url": None,
174
- "relay_url": None,
175
- "relay_auth_token": None,
176
170
  "dm_policy": "pairing",
177
171
  "allow_from": [],
178
172
  "group_policy": "open",
@@ -184,8 +178,8 @@ def default_connectors() -> dict:
184
178
  },
185
179
  "slack": {
186
180
  "enabled": False,
181
+ "profiles": [],
187
182
  "transport": "socket_mode",
188
- "mode": "relay",
189
183
  "bot_name": "DeepScientist",
190
184
  "command_prefix": "/",
191
185
  "bot_token": None,
@@ -193,11 +187,6 @@ def default_connectors() -> dict:
193
187
  "bot_user_id": None,
194
188
  "app_token": None,
195
189
  "app_token_env": "SLACK_APP_TOKEN",
196
- "signing_secret": None,
197
- "signing_secret_env": "SLACK_SIGNING_SECRET",
198
- "public_callback_url": None,
199
- "relay_url": None,
200
- "relay_auth_token": None,
201
190
  "dm_policy": "pairing",
202
191
  "allow_from": [],
203
192
  "group_policy": "open",
@@ -208,21 +197,14 @@ def default_connectors() -> dict:
208
197
  },
209
198
  "feishu": {
210
199
  "enabled": False,
200
+ "profiles": [],
211
201
  "transport": "long_connection",
212
- "mode": "relay",
213
202
  "bot_name": "DeepScientist",
214
203
  "command_prefix": "/",
215
204
  "app_id": None,
216
205
  "app_secret": None,
217
206
  "app_secret_env": "FEISHU_APP_SECRET",
218
- "verification_token": None,
219
- "verification_token_env": "FEISHU_VERIFICATION_TOKEN",
220
- "encrypt_key": None,
221
- "encrypt_key_env": "FEISHU_ENCRYPT_KEY",
222
207
  "api_base_url": "https://open.feishu.cn",
223
- "public_callback_url": None,
224
- "relay_url": None,
225
- "relay_auth_token": None,
226
208
  "dm_policy": "pairing",
227
209
  "allow_from": [],
228
210
  "group_policy": "open",
@@ -233,24 +215,12 @@ def default_connectors() -> dict:
233
215
  },
234
216
  "whatsapp": {
235
217
  "enabled": False,
218
+ "profiles": [],
236
219
  "transport": "local_session",
237
- "mode": "relay",
238
220
  "bot_name": "DeepScientist",
239
221
  "command_prefix": "/",
240
222
  "auth_method": "qr_browser",
241
223
  "session_dir": "~/.deepscientist/connectors/whatsapp",
242
- "provider": "relay",
243
- "access_token": None,
244
- "access_token_env": "WHATSAPP_ACCESS_TOKEN",
245
- "phone_number_id": None,
246
- "business_account_id": None,
247
- "verify_token": None,
248
- "verify_token_env": "WHATSAPP_VERIFY_TOKEN",
249
- "api_base_url": "https://graph.facebook.com",
250
- "api_version": "v21.0",
251
- "public_callback_url": None,
252
- "relay_url": None,
253
- "relay_auth_token": None,
254
224
  "dm_policy": "pairing",
255
225
  "allow_from": [],
256
226
  "group_policy": "allowlist",