@researai/deepscientist 1.5.13 → 1.5.15

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 (142) hide show
  1. package/README.md +8 -0
  2. package/assets/branding/logo-raster.png +0 -0
  3. package/bin/ds.js +134 -49
  4. package/docs/en/00_QUICK_START.md +2 -2
  5. package/docs/en/01_SETTINGS_REFERENCE.md +20 -4
  6. package/docs/en/03_QQ_CONNECTOR_GUIDE.md +19 -0
  7. package/docs/en/05_TUI_GUIDE.md +466 -96
  8. package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
  9. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +2 -0
  10. package/docs/en/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
  11. package/docs/en/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
  12. package/docs/en/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
  13. package/docs/en/README.md +8 -0
  14. package/docs/zh/00_QUICK_START.md +2 -2
  15. package/docs/zh/01_SETTINGS_REFERENCE.md +20 -4
  16. package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +19 -0
  17. package/docs/zh/05_TUI_GUIDE.md +465 -82
  18. package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
  19. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +2 -0
  20. package/docs/zh/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
  21. package/docs/zh/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
  22. package/docs/zh/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
  23. package/docs/zh/README.md +8 -0
  24. package/install.sh +2 -0
  25. package/package.json +1 -1
  26. package/pyproject.toml +1 -1
  27. package/src/deepscientist/__init__.py +1 -1
  28. package/src/deepscientist/artifact/charts.py +567 -0
  29. package/src/deepscientist/artifact/guidance.py +50 -10
  30. package/src/deepscientist/artifact/metrics.py +228 -5
  31. package/src/deepscientist/artifact/schemas.py +3 -0
  32. package/src/deepscientist/artifact/service.py +4004 -538
  33. package/src/deepscientist/bash_exec/models.py +23 -0
  34. package/src/deepscientist/bash_exec/monitor.py +147 -67
  35. package/src/deepscientist/bash_exec/runtime.py +218 -156
  36. package/src/deepscientist/bash_exec/service.py +79 -64
  37. package/src/deepscientist/bash_exec/shells.py +87 -0
  38. package/src/deepscientist/bridges/connectors.py +51 -2
  39. package/src/deepscientist/config/models.py +6 -3
  40. package/src/deepscientist/config/service.py +7 -2
  41. package/src/deepscientist/connector/lingzhu_support.py +23 -4
  42. package/src/deepscientist/connector/weixin_support.py +122 -1
  43. package/src/deepscientist/daemon/api/handlers.py +75 -4
  44. package/src/deepscientist/daemon/api/router.py +1 -0
  45. package/src/deepscientist/daemon/app.py +869 -236
  46. package/src/deepscientist/doctor.py +51 -0
  47. package/src/deepscientist/file_lock.py +48 -0
  48. package/src/deepscientist/gitops/diff.py +167 -1
  49. package/src/deepscientist/mcp/server.py +331 -21
  50. package/src/deepscientist/process_control.py +161 -0
  51. package/src/deepscientist/prompts/builder.py +275 -491
  52. package/src/deepscientist/quest/service.py +2336 -145
  53. package/src/deepscientist/quest/stage_views.py +305 -29
  54. package/src/deepscientist/runners/base.py +2 -0
  55. package/src/deepscientist/runners/codex.py +88 -5
  56. package/src/deepscientist/runners/runtime_overrides.py +17 -1
  57. package/src/deepscientist/shared.py +6 -1
  58. package/src/prompts/contracts/shared_interaction.md +13 -4
  59. package/src/prompts/system.md +984 -1985
  60. package/src/skills/analysis-campaign/SKILL.md +31 -2
  61. package/src/skills/analysis-campaign/references/artifact-orchestration.md +1 -1
  62. package/src/skills/analysis-campaign/references/writing-facing-slice-examples.md +65 -0
  63. package/src/skills/baseline/SKILL.md +267 -994
  64. package/src/skills/baseline/references/baseline-checklist-template.md +21 -32
  65. package/src/skills/baseline/references/baseline-plan-template.md +41 -57
  66. package/src/skills/decision/SKILL.md +19 -2
  67. package/src/skills/experiment/SKILL.md +8 -2
  68. package/src/skills/finalize/SKILL.md +18 -0
  69. package/src/skills/idea/SKILL.md +78 -0
  70. package/src/skills/idea/references/idea-generation-playbook.md +100 -0
  71. package/src/skills/idea/references/outline-seeding-example.md +60 -0
  72. package/src/skills/intake-audit/SKILL.md +1 -1
  73. package/src/skills/optimize/SKILL.md +1644 -0
  74. package/src/skills/rebuttal/SKILL.md +2 -1
  75. package/src/skills/review/SKILL.md +2 -1
  76. package/src/skills/write/SKILL.md +80 -12
  77. package/src/skills/write/references/outline-evidence-contract-example.md +107 -0
  78. package/src/tui/dist/app/AppContainer.js +1445 -52
  79. package/src/tui/dist/components/Composer.js +1 -1
  80. package/src/tui/dist/components/ConfigScreen.js +190 -36
  81. package/src/tui/dist/components/GradientStatusText.js +1 -20
  82. package/src/tui/dist/components/InputPrompt.js +41 -32
  83. package/src/tui/dist/components/LoadingIndicator.js +1 -1
  84. package/src/tui/dist/components/Logo.js +61 -38
  85. package/src/tui/dist/components/MainContent.js +10 -3
  86. package/src/tui/dist/components/WelcomePanel.js +4 -12
  87. package/src/tui/dist/components/messages/AssistantMessage.js +1 -1
  88. package/src/tui/dist/components/messages/BashExecOperationMessage.js +3 -3
  89. package/src/tui/dist/components/messages/OperationMessage.js +1 -1
  90. package/src/tui/dist/index.js +28 -1
  91. package/src/tui/dist/layouts/DefaultAppLayout.js +3 -3
  92. package/src/tui/dist/lib/api.js +17 -0
  93. package/src/tui/dist/lib/connectors.js +261 -0
  94. package/src/tui/dist/semantic-colors.js +29 -19
  95. package/src/tui/package.json +1 -1
  96. package/src/ui/dist/assets/{AiManusChatView-CnJcXynW.js → AiManusChatView-DDjbFnbt.js} +12 -12
  97. package/src/ui/dist/assets/{AnalysisPlugin-DeyzPEhV.js → AnalysisPlugin-Yb5IdmaU.js} +1 -1
  98. package/src/ui/dist/assets/CliPlugin-e64sreyu.js +31037 -0
  99. package/src/ui/dist/assets/{CodeEditorPlugin-B-xicq1e.js → CodeEditorPlugin-C4D2TIkU.js} +8 -8
  100. package/src/ui/dist/assets/{CodeViewerPlugin-DT54ysXa.js → CodeViewerPlugin-BVoNZIvC.js} +5 -5
  101. package/src/ui/dist/assets/{DocViewerPlugin-DQtKT-VD.js → DocViewerPlugin-CLChbllo.js} +3 -3
  102. package/src/ui/dist/assets/{GitDiffViewerPlugin-hqHbCfnv.js → GitDiffViewerPlugin-C4xeFyFQ.js} +20 -20
  103. package/src/ui/dist/assets/{ImageViewerPlugin-OcVo33jV.js → ImageViewerPlugin-OiMUAcLi.js} +5 -5
  104. package/src/ui/dist/assets/{LabCopilotPanel-DdGwhEUV.js → LabCopilotPanel-BjD2ThQF.js} +11 -11
  105. package/src/ui/dist/assets/{LabPlugin-Ciz1gDaX.js → LabPlugin-DQPg-NrB.js} +2 -2
  106. package/src/ui/dist/assets/{LatexPlugin-BhmjNQRC.js → LatexPlugin-CI05XAV9.js} +7 -7
  107. package/src/ui/dist/assets/{MarkdownViewerPlugin-BzdVH9Bx.js → MarkdownViewerPlugin-DpeBLYZf.js} +4 -4
  108. package/src/ui/dist/assets/{MarketplacePlugin-DmyHspXt.js → MarketplacePlugin-DolE58Q2.js} +3 -3
  109. package/src/ui/dist/assets/{NotebookEditor-BTVYRGkm.js → NotebookEditor-7Qm2rSWD.js} +11 -11
  110. package/src/ui/dist/assets/{NotebookEditor-BMXKrDRk.js → NotebookEditor-C1kWaxKi.js} +1 -1
  111. package/src/ui/dist/assets/{PdfLoader-CvcjJHXv.js → PdfLoader-BfOHw8Zw.js} +1 -1
  112. package/src/ui/dist/assets/{PdfMarkdownPlugin-DW2ej8Vk.js → PdfMarkdownPlugin-BulDREv1.js} +2 -2
  113. package/src/ui/dist/assets/{PdfViewerPlugin-CmlDxbhU.js → PdfViewerPlugin-C-daaOaL.js} +10 -10
  114. package/src/ui/dist/assets/{SearchPlugin-DAjQZPSv.js → SearchPlugin-CjpaiJ3A.js} +1 -1
  115. package/src/ui/dist/assets/{TextViewerPlugin-C-nVAZb_.js → TextViewerPlugin-BxIyqPQC.js} +5 -5
  116. package/src/ui/dist/assets/{VNCViewer-D7-dIYon.js → VNCViewer-HAg9mF7M.js} +10 -10
  117. package/src/ui/dist/assets/{bot-C_G4WtNI.js → bot-0DYntytV.js} +1 -1
  118. package/src/ui/dist/assets/{code-Cd7WfiWq.js → code-B20Slj_w.js} +1 -1
  119. package/src/ui/dist/assets/{file-content-B57zsL9y.js → file-content-DT24KFma.js} +1 -1
  120. package/src/ui/dist/assets/{file-diff-panel-DVoheLFq.js → file-diff-panel-DK13YPql.js} +1 -1
  121. package/src/ui/dist/assets/{file-socket-B5kXFxZP.js → file-socket-B4T2o4nR.js} +1 -1
  122. package/src/ui/dist/assets/{image-LLOjkMHF.js → image-DSeR_sDS.js} +1 -1
  123. package/src/ui/dist/assets/{index-hOUOWbW2.js → index-BrFje2Uk.js} +2 -2
  124. package/src/ui/dist/assets/{index-Dxa2eYMY.js → index-BwRJaoTl.js} +1 -1
  125. package/src/ui/dist/assets/{index-CLQauncb.js → index-D_E4281X.js} +5418 -28620
  126. package/src/ui/dist/assets/{index-C3r2iGrp.js → index-DnYB3xb1.js} +12 -12
  127. package/src/ui/dist/assets/{index-BQG-1s2o.css → index-G7AcWcMu.css} +43 -2
  128. package/src/ui/dist/assets/{monaco-BGGAEii3.js → monaco-LExaAN3Y.js} +1 -1
  129. package/src/ui/dist/assets/{pdf-effect-queue-DlEr1_y5.js → pdf-effect-queue-BJk5okWJ.js} +1 -1
  130. package/src/ui/dist/assets/{popover-CWJbJuYY.js → popover-D3Gg_FoV.js} +1 -1
  131. package/src/ui/dist/assets/{project-sync-CRJiucYO.js → project-sync-C_ygLlVU.js} +1 -1
  132. package/src/ui/dist/assets/{select-CoHB7pvH.js → select-CpAK6uWm.js} +2 -2
  133. package/src/ui/dist/assets/{sigma-D5aJWR8J.js → sigma-DEccaSgk.js} +1 -1
  134. package/src/ui/dist/assets/{square-check-big-DUK_mnkS.js → square-check-big-uUfyVsbD.js} +1 -1
  135. package/src/ui/dist/assets/{trash-ChU3SEE3.js → trash-CXvwwSe8.js} +1 -1
  136. package/src/ui/dist/assets/{useCliAccess-BrJBV3tY.js → useCliAccess-Bnop4mgR.js} +1 -1
  137. package/src/ui/dist/assets/{useFileDiffOverlay-C2OQaVWc.js → useFileDiffOverlay-B8eUAX0I.js} +1 -1
  138. package/src/ui/dist/assets/{wrap-text-C7Qqh-om.js → wrap-text-9vbOBpkW.js} +1 -1
  139. package/src/ui/dist/assets/{zoom-out-rtX0FKya.js → zoom-out-BgVMmOW4.js} +1 -1
  140. package/src/ui/dist/index.html +2 -2
  141. package/uv.lock +1 -1
  142. package/src/ui/dist/assets/CliPlugin-CB1YODQn.js +0 -5905
@@ -3,9 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  import os
5
5
  import re
6
- import shlex
7
6
  import shutil
8
- import signal
9
7
  import subprocess
10
8
  import sys
11
9
  import tempfile
@@ -17,7 +15,9 @@ from pathlib import Path
17
15
  from typing import Any
18
16
 
19
17
  from ..mcp.context import McpContext
18
+ from ..process_control import is_process_alive, process_session_popen_kwargs, terminate_process_ids
20
19
  from ..shared import append_jsonl, ensure_dir, generate_id, iter_jsonl, read_json, read_jsonl, read_jsonl_tail, utc_now
20
+ from .shells import build_exec_shell_launch, build_terminal_shell_launch
21
21
  from .runtime import TerminalRuntimeManager
22
22
 
23
23
  BASH_STATUS_MARKER_PREFIX = "__DS_BASH_STATUS__"
@@ -114,26 +114,6 @@ def _session_sort_key(session: dict[str, Any]) -> tuple[str, str]:
114
114
  )
115
115
 
116
116
 
117
- def _is_process_alive(pid: object) -> bool:
118
- if not isinstance(pid, int) or pid <= 0:
119
- return False
120
- proc_stat_path = Path("/proc") / str(pid) / "stat"
121
- if proc_stat_path.exists():
122
- try:
123
- parts = proc_stat_path.read_text(encoding="utf-8").split()
124
- except OSError:
125
- parts = []
126
- if len(parts) >= 3 and parts[2] == "Z":
127
- return False
128
- try:
129
- os.kill(pid, 0)
130
- except ProcessLookupError:
131
- return False
132
- except PermissionError:
133
- return True
134
- return True
135
-
136
-
137
117
  def _parse_progress_marker(line: str) -> dict[str, Any] | None:
138
118
  if not line.startswith(BASH_PROGRESS_PREFIX):
139
119
  return None
@@ -518,20 +498,14 @@ class BashExecService:
518
498
  return self._session_payload(quest_root, read_json(meta_path, meta) or meta)
519
499
  monitor_pid = meta.get("monitor_pid")
520
500
  process_pid = meta.get("process_pid")
521
- if kind == "terminal" and _is_process_alive(process_pid):
522
- process_group_id = meta.get("process_group_id")
523
- if isinstance(process_group_id, int) and process_group_id > 0:
524
- try:
525
- os.killpg(process_group_id, signal.SIGTERM)
526
- except ProcessLookupError:
527
- pass
528
- elif isinstance(process_pid, int) and process_pid > 0:
529
- try:
530
- os.kill(process_pid, signal.SIGTERM)
531
- except ProcessLookupError:
532
- pass
501
+ if kind == "terminal" and is_process_alive(process_pid):
502
+ terminate_process_ids(
503
+ process_pid=process_pid if isinstance(process_pid, int) else None,
504
+ process_group_id=meta.get("process_group_id") if isinstance(meta.get("process_group_id"), int) else None,
505
+ force=False,
506
+ )
533
507
  time.sleep(0.05)
534
- if kind != "terminal" and (_is_process_alive(process_pid) or _is_process_alive(monitor_pid)):
508
+ if kind != "terminal" and (is_process_alive(process_pid) or is_process_alive(monitor_pid)):
535
509
  return self._session_payload(quest_root, meta)
536
510
  stop_reason = _normalize_string(meta.get("stop_reason"))
537
511
  meta["status"] = "terminated" if stop_reason else "failed"
@@ -566,6 +540,10 @@ class BashExecService:
566
540
  normalized_agent_instance_ids = {item for item in (agent_instance_ids or []) if item}
567
541
  normalized_agent_ids = {item for item in (agent_ids or []) if item}
568
542
  normalized_chat_session = _normalize_string(chat_session_id)
543
+ if normalized_status in {"running", "terminating"}:
544
+ summary = self.summary(quest_root)
545
+ if int(summary.get("running_count") or 0) <= 0:
546
+ return []
569
547
  sessions: list[dict[str, Any]] = []
570
548
  for bash_id in self._list_session_ids(quest_root):
571
549
  try:
@@ -717,18 +695,11 @@ class BashExecService:
717
695
  if runtime is not None:
718
696
  runtime.stop(reason=request_payload["reason"], force=bool(force))
719
697
  else:
720
- process_group_id = meta.get("process_group_id")
721
- process_pid = meta.get("process_pid")
722
- if isinstance(process_group_id, int) and process_group_id > 0:
723
- try:
724
- os.killpg(process_group_id, signal.SIGKILL if force else signal.SIGTERM)
725
- except ProcessLookupError:
726
- pass
727
- elif isinstance(process_pid, int) and process_pid > 0:
728
- try:
729
- os.kill(process_pid, signal.SIGKILL if force else signal.SIGTERM)
730
- except ProcessLookupError:
731
- pass
698
+ terminate_process_ids(
699
+ process_pid=meta.get("process_pid") if isinstance(meta.get("process_pid"), int) else None,
700
+ process_group_id=meta.get("process_group_id") if isinstance(meta.get("process_group_id"), int) else None,
701
+ force=bool(force),
702
+ )
732
703
  return self._session_payload(quest_root, meta)
733
704
 
734
705
  def _build_initial_meta(
@@ -737,6 +708,7 @@ class BashExecService:
737
708
  context: McpContext,
738
709
  bash_id: str,
739
710
  command: str,
711
+ launch_argv: list[str] | None,
740
712
  mode: str,
741
713
  cwd: Path,
742
714
  workdir_display: str,
@@ -744,6 +716,8 @@ class BashExecService:
744
716
  env_keys: list[str],
745
717
  comment: str | dict[str, Any] | None = None,
746
718
  kind: str = "exec",
719
+ shell_family: str | None = None,
720
+ shell_name: str | None = None,
747
721
  ) -> dict[str, Any]:
748
722
  quest_root = context.require_quest_root().resolve()
749
723
  session_id = _normalize_string(context.conversation_id) or f"quest:{context.quest_id or quest_root.name}"
@@ -766,6 +740,9 @@ class BashExecService:
766
740
  "stopped_by_user_id": None,
767
741
  "comment": comment,
768
742
  "command": command,
743
+ "launch_argv": list(launch_argv or []),
744
+ "shell_family": shell_family,
745
+ "shell_name": shell_name,
769
746
  "workdir": workdir_display,
770
747
  "cwd": str(cwd),
771
748
  "kind": kind,
@@ -808,7 +785,7 @@ class BashExecService:
808
785
  stderr=monitor_log_handle,
809
786
  cwd=str(quest_root),
810
787
  env=monitor_env,
811
- start_new_session=True,
788
+ **process_session_popen_kwargs(hide_window=True),
812
789
  )
813
790
  finally:
814
791
  monitor_log_handle.close()
@@ -833,10 +810,12 @@ class BashExecService:
833
810
  session_dir = self.session_dir(quest_root, bash_id)
834
811
  ensure_dir(session_dir)
835
812
  env_payload = {str(key): str(value) for key, value in (env or {}).items() if value is not None}
813
+ launch = build_exec_shell_launch(command)
836
814
  meta = self._build_initial_meta(
837
815
  context=context,
838
816
  bash_id=bash_id,
839
817
  command=command,
818
+ launch_argv=launch.argv,
840
819
  mode=mode,
841
820
  cwd=cwd,
842
821
  workdir_display=workdir_display,
@@ -844,6 +823,8 @@ class BashExecService:
844
823
  env_keys=sorted(env_payload),
845
824
  comment=comment,
846
825
  kind="exec",
826
+ shell_family=launch.family,
827
+ shell_name=launch.shell_name,
847
828
  )
848
829
  self.terminal_log_path(quest_root, bash_id).touch()
849
830
  self.log_path(quest_root, bash_id).touch()
@@ -880,10 +861,14 @@ class BashExecService:
880
861
  cwd: Path,
881
862
  workdir_display: str,
882
863
  command: str,
864
+ launch_argv: list[str] | None,
883
865
  source: str,
884
866
  conversation_id: str | None,
885
867
  user_id: str | None,
886
868
  env_keys: list[str],
869
+ shell_family: str | None = None,
870
+ shell_name: str | None = None,
871
+ transport_preference: str | None = None,
887
872
  ) -> dict[str, Any]:
888
873
  timestamp = utc_now()
889
874
  session_id = _normalize_string(conversation_id) or f"quest:{quest_id}:terminal"
@@ -903,6 +888,10 @@ class BashExecService:
903
888
  "stopped_by_user_id": None,
904
889
  "label": label,
905
890
  "command": command,
891
+ "launch_argv": list(launch_argv or []),
892
+ "shell_family": shell_family,
893
+ "shell_name": shell_name,
894
+ "transport_preference": transport_preference,
906
895
  "workdir": workdir_display,
907
896
  "cwd": str(cwd),
908
897
  "kind": "terminal",
@@ -984,29 +973,49 @@ class BashExecService:
984
973
  self.line_buffer_path(resolved_quest_root, bash_id),
985
974
  {"buffer": "", "updated_at": utc_now()},
986
975
  )
987
- terminal_rc_path = self.terminal_rc_path(resolved_quest_root, bash_id)
988
- terminal_rc_path.write_text(
989
- "\n".join(
990
- [
991
- "PS1='\\w$ '",
992
- "PS2='> '",
993
- 'PROMPT_COMMAND=\'printf "__DS_TERMINAL_PROMPT__ cwd=%q ts=%s\\n" "$PWD" "$(date -u +%FT%TZ)" >> "${DS_TERMINAL_PROMPT_PATH}"\'',
994
- "bind 'set enable-bracketed-paste off' >/dev/null 2>&1 || true",
995
- "",
996
- ]
997
- ),
998
- encoding="utf-8",
999
- )
976
+ terminal_script_path = session_dir / ("terminal.ps1" if os.name == "nt" else "terminal.rc")
1000
977
  stop_request = self.stop_request_path(resolved_quest_root, bash_id)
1001
978
  if stop_request.exists():
1002
979
  stop_request.unlink()
1003
980
 
1004
981
  env_payload = {
1005
- "TERM": "xterm-256color",
1006
- "COLORTERM": "truecolor",
1007
982
  "DS_TERMINAL_PROMPT_PATH": str(self.prompt_events_path(resolved_quest_root, bash_id)),
1008
983
  }
1009
- command = f"exec bash --noprofile --rcfile {shlex.quote(str(terminal_rc_path))} -i"
984
+ if os.name != "nt":
985
+ terminal_script_path.write_text(
986
+ "\n".join(
987
+ [
988
+ "PS1='\\w$ '",
989
+ "PS2='> '",
990
+ 'PROMPT_COMMAND=\'printf "__DS_TERMINAL_PROMPT__ cwd_b64=%s ts=%s\\n" "$(printf "%s" "$PWD" | base64 | tr -d "\\n")" "$(date -u +%FT%TZ)" >> "${DS_TERMINAL_PROMPT_PATH}"\'',
991
+ "bind 'set enable-bracketed-paste off' >/dev/null 2>&1 || true",
992
+ "",
993
+ ]
994
+ ),
995
+ encoding="utf-8",
996
+ )
997
+ env_payload["TERM"] = "xterm-256color"
998
+ env_payload["COLORTERM"] = "truecolor"
999
+ else:
1000
+ terminal_script_path.write_text(
1001
+ "\n".join(
1002
+ [
1003
+ "$global:__dsPromptPath = $env:DS_TERMINAL_PROMPT_PATH",
1004
+ "function global:prompt {",
1005
+ " $cwdBytes = [System.Text.Encoding]::UTF8.GetBytes((Get-Location).Path)",
1006
+ " $cwdB64 = [Convert]::ToBase64String($cwdBytes)",
1007
+ ' $ts = [DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")',
1008
+ ' Add-Content -LiteralPath $global:__dsPromptPath -Value "__DS_TERMINAL_PROMPT__ cwd_b64=$cwdB64 ts=$ts"',
1009
+ ' return "PS $((Get-Location).Path)> "',
1010
+ "}",
1011
+ "try { Set-PSReadLineOption -BellStyle None | Out-Null } catch {}",
1012
+ "",
1013
+ ]
1014
+ ),
1015
+ encoding="utf-8",
1016
+ )
1017
+ launch = build_terminal_shell_launch(terminal_script_path)
1018
+ command = " ".join(launch.argv)
1010
1019
  resolved_label = _normalize_string(label) or previous_label
1011
1020
  meta = self._build_terminal_meta(
1012
1021
  quest_root=resolved_quest_root,
@@ -1016,10 +1025,14 @@ class BashExecService:
1016
1025
  cwd=target_cwd,
1017
1026
  workdir_display=workdir_display,
1018
1027
  command=command,
1028
+ launch_argv=launch.argv,
1019
1029
  source=source,
1020
1030
  conversation_id=conversation_id,
1021
1031
  user_id=user_id,
1022
1032
  env_keys=sorted(env_payload),
1033
+ shell_family=launch.family,
1034
+ shell_name=launch.shell_name,
1035
+ transport_preference="pipe" if os.name == "nt" else "pty",
1023
1036
  )
1024
1037
  self._write_meta(resolved_quest_root, bash_id, meta)
1025
1038
  append_jsonl(
@@ -1042,7 +1055,9 @@ class BashExecService:
1042
1055
  prompt_events_path=self.prompt_events_path(resolved_quest_root, bash_id),
1043
1056
  env_payload=env_payload,
1044
1057
  command=command,
1058
+ launch_argv=launch.argv,
1045
1059
  cwd=target_cwd,
1060
+ transport_preference="pipe" if os.name == "nt" else "pty",
1046
1061
  )
1047
1062
  meta["updated_at"] = utc_now()
1048
1063
  self._write_meta(resolved_quest_root, bash_id, meta)
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import os
5
+ from pathlib import Path
6
+ import shutil
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class ShellLaunchSpec:
11
+ argv: list[str]
12
+ family: str
13
+ shell_name: str
14
+
15
+
16
+ def _resolve_windows_shell(*, interactive: bool) -> tuple[str, str]:
17
+ candidates: list[tuple[str, str]] = []
18
+ if not interactive:
19
+ candidates.extend(
20
+ [
21
+ ("bash.exe", "bash"),
22
+ ("bash", "bash"),
23
+ ]
24
+ )
25
+ candidates.extend(
26
+ [
27
+ ("pwsh", "powershell"),
28
+ ("powershell.exe", "powershell"),
29
+ ]
30
+ )
31
+ if not interactive:
32
+ candidates.append(("cmd.exe", "cmd"))
33
+ for binary, family in candidates:
34
+ resolved = shutil.which(binary)
35
+ if resolved:
36
+ return resolved, family
37
+ fallback = "cmd.exe" if not interactive else "powershell.exe"
38
+ return fallback, "cmd" if fallback == "cmd.exe" else "powershell"
39
+
40
+
41
+ def build_exec_shell_launch(command: str) -> ShellLaunchSpec:
42
+ normalized = str(command or "").strip()
43
+ if os.name != "nt":
44
+ return ShellLaunchSpec(
45
+ argv=["bash", "-lc", normalized],
46
+ family="bash",
47
+ shell_name="bash",
48
+ )
49
+ binary, family = _resolve_windows_shell(interactive=False)
50
+ if family == "bash":
51
+ return ShellLaunchSpec(
52
+ argv=[binary, "-lc", normalized],
53
+ family=family,
54
+ shell_name=Path(binary).name,
55
+ )
56
+ if family == "cmd":
57
+ return ShellLaunchSpec(
58
+ argv=[binary, "/d", "/s", "/c", normalized],
59
+ family=family,
60
+ shell_name=Path(binary).name,
61
+ )
62
+ return ShellLaunchSpec(
63
+ argv=[binary, "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", normalized],
64
+ family=family,
65
+ shell_name=Path(binary).name,
66
+ )
67
+
68
+
69
+ def build_terminal_shell_launch(script_path: Path) -> ShellLaunchSpec:
70
+ if os.name != "nt":
71
+ return ShellLaunchSpec(
72
+ argv=["bash", "--noprofile", "--rcfile", str(script_path), "-i"],
73
+ family="bash",
74
+ shell_name="bash",
75
+ )
76
+ binary, family = _resolve_windows_shell(interactive=True)
77
+ if family == "powershell":
78
+ return ShellLaunchSpec(
79
+ argv=[binary, "-NoLogo", "-NoProfile", "-NoExit", "-ExecutionPolicy", "Bypass", "-File", str(script_path)],
80
+ family=family,
81
+ shell_name=Path(binary).name,
82
+ )
83
+ return ShellLaunchSpec(
84
+ argv=[binary, "/q", "/k", str(script_path)],
85
+ family=family,
86
+ shell_name=Path(binary).name,
87
+ )
@@ -18,8 +18,11 @@ from ..connector.weixin_support import (
18
18
  WEIXIN_UPLOAD_MEDIA_FILE,
19
19
  WEIXIN_UPLOAD_MEDIA_IMAGE,
20
20
  WEIXIN_UPLOAD_MEDIA_VIDEO,
21
+ clear_weixin_context_send_state,
21
22
  download_weixin_remote_attachment,
23
+ get_weixin_context_entry,
22
24
  get_weixin_context_token,
25
+ mark_weixin_context_stale,
23
26
  normalize_weixin_base_url,
24
27
  normalize_weixin_cdn_base_url,
25
28
  send_weixin_message,
@@ -558,10 +561,11 @@ class QQConnectorBridge(BaseConnectorBridge):
558
561
  connector_delivery = item.get("connector_delivery") if isinstance(item.get("connector_delivery"), dict) else {}
559
562
  qq_delivery = connector_delivery.get("qq") if isinstance(connector_delivery.get("qq"), dict) else {}
560
563
  media_kind = str(qq_delivery.get("media_kind") or "").strip().lower()
564
+ allow_internal_auto_media = bool(qq_delivery.get("allow_internal_auto_media"))
561
565
  if media_kind not in {"image", "file"}:
562
566
  residual_items.append(item)
563
567
  continue
564
- if not native_enabled:
568
+ if not native_enabled and not allow_internal_auto_media:
565
569
  issues.append(
566
570
  {
567
571
  "attachment_index": index,
@@ -781,7 +785,9 @@ class WeixinConnectorBridge(BaseConnectorBridge):
781
785
  name = "weixin"
782
786
  _MEDIA_ITEM_TYPES = {2, 4, 5}
783
787
  _MEDIA_SEND_INITIAL_DELAY_SECONDS = 0.8
784
- _MEDIA_SEND_RETRY_DELAYS_SECONDS = (1.5, 3.0)
788
+ _TEXT_SEND_RETRY_DELAYS_SECONDS = (0.8, 1.5, 3.0)
789
+ _MEDIA_SEND_RETRY_DELAYS_SECONDS = (1.5, 3.0, 5.0)
790
+ _STALE_CONTEXT_SUPPRESSED_KINDS = {"progress", "milestone", "assistant", "summary", "ack"}
785
791
 
786
792
  def deliver(self, payload: dict[str, Any], config: dict[str, Any]) -> dict[str, Any] | None:
787
793
  return self.deliver_direct(payload, config)
@@ -800,14 +806,26 @@ class WeixinConnectorBridge(BaseConnectorBridge):
800
806
  "error": "Weixin outbound target is empty.",
801
807
  }
802
808
  connector_root = self._connector_root(config)
809
+ kind = str(payload.get("kind") or "").strip().lower()
810
+ context_entry = get_weixin_context_entry(connector_root, to_user_id)
803
811
  context_token = get_weixin_context_token(connector_root, to_user_id)
804
812
  if not context_token:
813
+ if kind in self._STALE_CONTEXT_SUPPRESSED_KINDS:
814
+ return self._queued_context_wait_response(
815
+ reason=f"Weixin context_token is missing for `{to_user_id}`. Waiting for the next inbound message.",
816
+ )
805
817
  return {
806
818
  "ok": False,
807
819
  "queued": False,
808
820
  "transport": "weixin-ilink",
809
821
  "error": f"Weixin context_token is missing for `{to_user_id}`. Wait for one inbound message first.",
810
822
  }
823
+ if bool(context_entry.get("stale_context")) and kind in self._STALE_CONTEXT_SUPPRESSED_KINDS:
824
+ stale_since = str(context_entry.get("stale_since") or "").strip()
825
+ warning = "Weixin outbound is paused until the next inbound message refreshes context_token."
826
+ if stale_since:
827
+ warning = f"{warning} stale_since={stale_since}"
828
+ return self._queued_context_wait_response(reason=warning)
811
829
 
812
830
  native_attachments, residual_attachments, warnings = self._partition_native_attachments(payload.get("attachments"))
813
831
  rendered_text = self.render_text(payload.get("text"), residual_attachments)
@@ -895,6 +913,20 @@ class WeixinConnectorBridge(BaseConnectorBridge):
895
913
  else:
896
914
  warnings.append("Weixin outbound payload contained neither text nor sendable attachments.")
897
915
  except Exception as exc:
916
+ if "ret=-2" in str(exc or "").lower():
917
+ mark_weixin_context_stale(
918
+ connector_root,
919
+ user_id=to_user_id,
920
+ error=str(exc),
921
+ kind=kind or None,
922
+ )
923
+ if kind in self._STALE_CONTEXT_SUPPRESSED_KINDS:
924
+ queued = self._queued_context_wait_response(
925
+ reason="Weixin send hit stale context and was deferred until the next inbound refresh.",
926
+ )
927
+ queued["parts"] = parts
928
+ queued["warnings"] = [*warnings, *(queued.get("warnings") or [])]
929
+ return queued
898
930
  return {
899
931
  "ok": False,
900
932
  "queued": False,
@@ -909,6 +941,12 @@ class WeixinConnectorBridge(BaseConnectorBridge):
909
941
  error_messages = [str(item.get("error") or "").strip() for item in failed if str(item.get("error") or "").strip()]
910
942
  error_messages.extend(warnings)
911
943
  last_success = succeeded[-1] if succeeded else {}
944
+ if succeeded:
945
+ clear_weixin_context_send_state(
946
+ connector_root,
947
+ user_id=to_user_id,
948
+ kind=kind or None,
949
+ )
912
950
  return {
913
951
  "ok": bool(succeeded),
914
952
  "queued": False,
@@ -920,6 +958,17 @@ class WeixinConnectorBridge(BaseConnectorBridge):
920
958
  "error": "; ".join(error_messages) if error_messages else None,
921
959
  }
922
960
 
961
+ @staticmethod
962
+ def _queued_context_wait_response(*, reason: str) -> dict[str, Any]:
963
+ return {
964
+ "ok": False,
965
+ "queued": True,
966
+ "transport": "weixin-ilink",
967
+ "parts": [],
968
+ "warnings": [str(reason or "").strip()],
969
+ "error": None,
970
+ }
971
+
923
972
  @staticmethod
924
973
  def _partition_native_attachments(
925
974
  attachments: Any,
@@ -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 in {"qq", "weixin", "lingzhu"} for name in SYSTEM_CONNECTOR_NAMES}
25
+ return {name: name in {"qq", "weixin", "telegram", "feishu", "whatsapp", "lingzhu"} for name in SYSTEM_CONNECTOR_NAMES}
26
26
 
27
27
 
28
28
  def default_config(home: Path) -> dict:
@@ -98,8 +98,8 @@ def default_runners() -> dict:
98
98
  "profile": "",
99
99
  "model": "gpt-5.4",
100
100
  "model_reasoning_effort": "xhigh",
101
- "approval_policy": "on-request",
102
- "sandbox_mode": "workspace-write",
101
+ "approval_policy": "never",
102
+ "sandbox_mode": "danger-full-access",
103
103
  "retry_on_failure": True,
104
104
  "retry_max_attempts": 5,
105
105
  "retry_initial_backoff_sec": 10.0,
@@ -154,6 +154,9 @@ def default_connectors() -> dict:
154
154
  "transport": "ilink_long_poll",
155
155
  "bot_name": "DeepScientist",
156
156
  "command_prefix": "/",
157
+ "auto_send_main_experiment_png": True,
158
+ "stale_replay_latest_limit": 5,
159
+ "stale_replay_interval_seconds": 2.0,
157
160
  "base_url": "https://ilinkai.weixin.qq.com",
158
161
  "cdn_base_url": "https://novac2c.cdn.weixin.qq.com/c2c",
159
162
  "bot_type": "3",
@@ -48,6 +48,7 @@ from .models import (
48
48
  ConfigFileInfo,
49
49
  SYSTEM_CONNECTOR_NAMES,
50
50
  config_filename,
51
+ default_system_enabled_connectors,
51
52
  default_payload,
52
53
  )
53
54
 
@@ -112,8 +113,9 @@ class ConfigManager:
112
113
  config = self.load_runtime_config()
113
114
  connectors = config.get("connectors") if isinstance(config.get("connectors"), dict) else {}
114
115
  system_enabled = connectors.get("system_enabled") if isinstance(connectors.get("system_enabled"), dict) else {}
116
+ defaults = default_system_enabled_connectors()
115
117
  return {
116
- name: self._coerce_bool(system_enabled.get(name), default=name in {"qq", "weixin"})
118
+ name: self._coerce_bool(system_enabled.get(name), default=defaults.get(name, False))
117
119
  for name in SYSTEM_CONNECTOR_NAMES
118
120
  }
119
121
 
@@ -1207,7 +1209,10 @@ Use **Test** when the file exposes runtime dependencies.
1207
1209
  env_key = str(key or "").strip()
1208
1210
  if not env_key or value is None:
1209
1211
  continue
1210
- resolved[env_key] = str(value)
1212
+ env_value = str(value)
1213
+ if env_value == "":
1214
+ continue
1215
+ resolved[env_key] = env_value
1211
1216
  return resolved
1212
1217
 
1213
1218
  def _prepare_codex_probe_home(
@@ -15,6 +15,9 @@ DEFAULT_LINGZHU_AGENT_ID = "main"
15
15
  DEFAULT_LINGZHU_SESSION_NAMESPACE = "lingzhu"
16
16
  DEFAULT_LINGZHU_TASK_PREFIX = "我现在的任务是"
17
17
  DEFAULT_LINGZHU_PASSIVE_CHAT_TYPE = "passive"
18
+ _LINGZHU_COMMAND_PUNCTUATION = "::,,。.;;!!??、~~`'\"“”‘’()()【】[]《》<>"
19
+ _LINGZHU_COMMAND_PUNCT_TRANSLATION = str.maketrans({char: " " for char in _LINGZHU_COMMAND_PUNCTUATION})
20
+ _LINGZHU_PREFIX_SEPARATORS_CLASS = re.escape(_LINGZHU_COMMAND_PUNCTUATION) + r"\s"
18
21
 
19
22
  _AUTH_AK_SEGMENTS = (8, 4, 4, 4, 12)
20
23
  _AUTH_AK_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789"
@@ -237,12 +240,28 @@ def lingzhu_extract_user_text(messages: Any) -> str:
237
240
  return "\n".join(parts).strip()
238
241
 
239
242
 
240
- def lingzhu_extract_task_text(text: Any) -> str | None:
243
+ def lingzhu_normalize_command_text(text: Any) -> str:
241
244
  normalized = str(text or "").strip()
242
- if not normalized.startswith(DEFAULT_LINGZHU_TASK_PREFIX):
245
+ if not normalized:
246
+ return ""
247
+ normalized = normalized.translate(_LINGZHU_COMMAND_PUNCT_TRANSLATION)
248
+ normalized = re.sub(r"\s+", " ", normalized)
249
+ return normalized.strip()
250
+
251
+
252
+ def lingzhu_extract_task_text(text: Any) -> str | None:
253
+ raw_text = str(text or "").strip()
254
+ if not raw_text:
255
+ return None
256
+ prefix_pattern = r"^[{separators}]*{prefix}[{separators}]*".format(
257
+ separators=_LINGZHU_PREFIX_SEPARATORS_CLASS,
258
+ prefix="".join(f"{re.escape(char)}[{_LINGZHU_PREFIX_SEPARATORS_CLASS}]*" for char in DEFAULT_LINGZHU_TASK_PREFIX),
259
+ )
260
+ matched = re.match(prefix_pattern, raw_text)
261
+ if matched is None:
243
262
  return None
244
- remainder = normalized[len(DEFAULT_LINGZHU_TASK_PREFIX) :].strip()
245
- remainder = remainder.lstrip("::,,。.;;!!?? ")
263
+ remainder = raw_text[matched.end() :].strip()
264
+ remainder = remainder.lstrip(_LINGZHU_COMMAND_PUNCTUATION + " \t\r\n")
246
265
  return remainder or None
247
266
 
248
267