@researai/deepscientist 1.5.0 → 1.5.1
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.
- package/AGENTS.md +26 -0
- package/README.md +19 -179
- package/assets/connectors/lingzhu/openclaw-bridge/README.md +124 -0
- package/assets/connectors/lingzhu/openclaw-bridge/index.ts +162 -0
- package/assets/connectors/lingzhu/openclaw-bridge/openclaw.plugin.json +145 -0
- package/assets/connectors/lingzhu/openclaw-bridge/package.json +35 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/cli.ts +180 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/config.ts +196 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/debug-log.ts +111 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/events.ts +4 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/http-handler.ts +1133 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/image-cache.ts +75 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/lingzhu-tools.ts +246 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/transform.ts +541 -0
- package/assets/connectors/lingzhu/openclaw-bridge/src/types.ts +131 -0
- package/assets/connectors/lingzhu/openclaw-bridge/tsconfig.json +14 -0
- package/assets/connectors/lingzhu/openclaw.lingzhu.config.template.json +39 -0
- package/bin/ds.js +233 -53
- package/docs/en/00_QUICK_START.md +134 -0
- package/docs/en/01_SETTINGS_REFERENCE.md +1104 -0
- package/docs/en/02_START_RESEARCH_GUIDE.md +404 -0
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +325 -0
- package/docs/en/04_LINGZHU_CONNECTOR_GUIDE.md +216 -0
- package/docs/en/05_TUI_GUIDE.md +141 -0
- package/docs/en/06_RUNTIME_AND_CANVAS.md +679 -0
- package/docs/en/07_MEMORY_AND_MCP.md +253 -0
- package/docs/en/08_FIGURE_STYLE_GUIDE.md +97 -0
- package/docs/en/09_DOCTOR.md +108 -0
- package/docs/en/90_ARCHITECTURE.md +245 -0
- package/docs/en/91_DEVELOPMENT.md +195 -0
- package/docs/en/99_ACKNOWLEDGEMENTS.md +29 -0
- package/docs/zh/00_QUICK_START.md +134 -0
- package/docs/zh/01_SETTINGS_REFERENCE.md +1137 -0
- package/docs/zh/02_START_RESEARCH_GUIDE.md +414 -0
- package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +324 -0
- package/docs/zh/04_LINGZHU_CONNECTOR_GUIDE.md +230 -0
- package/docs/zh/05_TUI_GUIDE.md +128 -0
- package/docs/zh/06_RUNTIME_AND_CANVAS.md +271 -0
- package/docs/zh/07_MEMORY_AND_MCP.md +235 -0
- package/docs/zh/08_FIGURE_STYLE_GUIDE.md +97 -0
- package/docs/zh/09_DOCTOR.md +112 -0
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +29 -0
- package/install.sh +32 -8
- package/package.json +4 -2
- package/pyproject.toml +1 -1
- package/src/deepscientist/artifact/guidance.py +9 -2
- package/src/deepscientist/artifact/service.py +482 -22
- package/src/deepscientist/bash_exec/monitor.py +27 -5
- package/src/deepscientist/bash_exec/runtime.py +639 -0
- package/src/deepscientist/bash_exec/service.py +99 -16
- package/src/deepscientist/bridges/base.py +3 -0
- package/src/deepscientist/bridges/connectors.py +292 -13
- package/src/deepscientist/channels/qq.py +19 -2
- package/src/deepscientist/channels/relay.py +1 -0
- package/src/deepscientist/cli.py +32 -25
- package/src/deepscientist/config/models.py +28 -2
- package/src/deepscientist/config/service.py +201 -6
- package/src/deepscientist/connector_runtime.py +2 -0
- package/src/deepscientist/daemon/api/handlers.py +50 -5
- package/src/deepscientist/daemon/api/router.py +1 -0
- package/src/deepscientist/daemon/app.py +442 -15
- package/src/deepscientist/doctor.py +444 -0
- package/src/deepscientist/home.py +1 -0
- package/src/deepscientist/latex_runtime.py +17 -4
- package/src/deepscientist/lingzhu_support.py +182 -0
- package/src/deepscientist/mcp/server.py +49 -2
- package/src/deepscientist/prompts/builder.py +181 -58
- package/src/deepscientist/quest/layout.py +1 -0
- package/src/deepscientist/quest/service.py +63 -2
- package/src/deepscientist/quest/stage_views.py +19 -1
- package/src/deepscientist/runtime_tools/__init__.py +16 -0
- package/src/deepscientist/runtime_tools/builtins.py +19 -0
- package/src/deepscientist/runtime_tools/models.py +29 -0
- package/src/deepscientist/runtime_tools/registry.py +40 -0
- package/src/deepscientist/runtime_tools/service.py +59 -0
- package/src/deepscientist/runtime_tools/tinytex.py +25 -0
- package/src/deepscientist/tinytex.py +276 -0
- package/src/prompts/connectors/lingzhu.md +12 -0
- package/src/prompts/connectors/qq.md +121 -0
- package/src/prompts/system.md +177 -33
- package/src/skills/analysis-campaign/SKILL.md +22 -6
- package/src/skills/baseline/SKILL.md +5 -4
- package/src/skills/decision/SKILL.md +4 -3
- package/src/skills/experiment/SKILL.md +5 -4
- package/src/skills/finalize/SKILL.md +5 -4
- package/src/skills/idea/SKILL.md +5 -4
- package/src/skills/intake-audit/SKILL.md +277 -0
- package/src/skills/intake-audit/references/state-audit-template.md +41 -0
- package/src/skills/rebuttal/SKILL.md +407 -0
- package/src/skills/rebuttal/references/action-plan-template.md +63 -0
- package/src/skills/rebuttal/references/evidence-update-template.md +30 -0
- package/src/skills/rebuttal/references/response-letter-template.md +113 -0
- package/src/skills/rebuttal/references/review-matrix-template.md +55 -0
- package/src/skills/review/SKILL.md +293 -0
- package/src/skills/review/references/experiment-todo-template.md +29 -0
- package/src/skills/review/references/review-report-template.md +83 -0
- package/src/skills/review/references/revision-log-template.md +40 -0
- package/src/skills/scout/SKILL.md +5 -4
- package/src/skills/write/SKILL.md +7 -3
- package/src/tui/dist/components/WelcomePanel.js +17 -43
- package/src/tui/dist/components/messages/BashExecOperationMessage.js +3 -2
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-7v-dHngU.js → AiManusChatView-w5lF2Ttt.js} +109 -575
- package/src/ui/dist/assets/{AnalysisPlugin-B_Xmz-KE.js → AnalysisPlugin-DJOED79I.js} +1 -1
- package/src/ui/dist/assets/{AutoFigurePlugin-Cko-0tm1.js → AutoFigurePlugin-DaG61Y0M.js} +63 -8
- package/src/ui/dist/assets/{CliPlugin-BsU0ht7q.js → CliPlugin-CV4LqUB_.js} +43 -609
- package/src/ui/dist/assets/{CodeEditorPlugin-DcMMP0Rt.js → CodeEditorPlugin-DylfAea4.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-BqoQ5QyY.js → CodeViewerPlugin-F7saY0LM.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-D7eHNhU6.js → DocViewerPlugin-COP0c7jf.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-DLJN42T5.js → GitDiffViewerPlugin-CAS05pT9.js} +1 -1
- package/src/ui/dist/assets/{ImageViewerPlugin-gJMV7MOu.js → ImageViewerPlugin-Bco1CN_w.js} +5 -6
- package/src/ui/dist/assets/{LabCopilotPanel-B857sfxP.js → LabCopilotPanel-CvMlCD99.js} +12 -15
- package/src/ui/dist/assets/LabPlugin-BYankkE4.js +2676 -0
- package/src/ui/dist/assets/LabPlugin-D9jVIo0A.css +2698 -0
- package/src/ui/dist/assets/{LatexPlugin-DWKEo-Wj.js → LatexPlugin-LDSMR-t-.js} +16 -16
- package/src/ui/dist/assets/{MarkdownViewerPlugin-DBzoEmhv.js → MarkdownViewerPlugin-B7o80jgm.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-DoHc-8vo.js → MarketplacePlugin-CM6ZOcpC.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-CKjKH-yS.js → NotebookEditor-Dc61cXmK.js} +3 -3
- package/src/ui/dist/assets/{PdfLoader-zFoL0VPo.js → PdfLoader-DWowuQwx.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-DXPaL9Nt.js → PdfMarkdownPlugin-BsJM1q_a.js} +3 -3
- package/src/ui/dist/assets/{PdfViewerPlugin-DhK8qCFp.js → PdfViewerPlugin-DB2eEEFQ.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-CdSi6krf.js → SearchPlugin-CraThSvt.js} +1 -1
- package/src/ui/dist/assets/{Stepper-V-WiDQJl.js → Stepper-CgocRTPq.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-hIs1Efiu.js → TextViewerPlugin-B1JGhKtd.js} +4 -4
- package/src/ui/dist/assets/{VNCViewer-DG8b0q2X.js → VNCViewer-CclFC7FM.js} +9 -10
- package/src/ui/dist/assets/{bibtex-HDac6fVW.js → bibtex-D3IKsMl7.js} +1 -1
- package/src/ui/dist/assets/{code-BnBeNxBc.js → code-BP37Xx0p.js} +1 -1
- package/src/ui/dist/assets/{file-content-IRQ3jHb8.js → file-content-BAJSu-9r.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DZoQ9I6r.js → file-diff-panel-DUGeCTuy.js} +1 -1
- package/src/ui/dist/assets/{file-socket-BMCdLc-P.js → file-socket-CXc1Ojf7.js} +1 -1
- package/src/ui/dist/assets/{file-utils-CltILB3w.js → file-utils-2J21jt7M.js} +1 -1
- package/src/ui/dist/assets/{image-Boe6ffhu.js → image-CMMmgvcn.js} +1 -1
- package/src/ui/dist/assets/{index-BlplpvE1.js → index-BaVumsQT.js} +2 -2
- package/src/ui/dist/assets/{index-DZqJ-qAM.js → index-CWgMgpow.js} +60 -2154
- package/src/ui/dist/assets/{index-DO43pFZP.js → index-DmwmJmbW.js} +6372 -8434
- package/src/ui/dist/assets/{index-Bq2bvfkl.css → index-KGt-z-dD.css} +225 -2920
- package/src/ui/dist/assets/{index-2Zf65FZt.js → index-s7aHnNQ4.js} +1 -1
- package/src/ui/dist/assets/{message-square-mUHn_Ssb.js → message-square-CQRfX0Am.js} +1 -1
- package/src/ui/dist/assets/{monaco-fe0arNEU.js → monaco-B4TbdsrF.js} +1 -1
- package/src/ui/dist/assets/{popover-D_7i19qU.js → popover-B8Rokodk.js} +1 -1
- package/src/ui/dist/assets/{project-sync-DyVGrU7H.js → project-sync-D_i96KH4.js} +2 -8
- package/src/ui/dist/assets/{sigma-BzazRyxQ.js → sigma-D12PnzCN.js} +1 -1
- package/src/ui/dist/assets/{tooltip-DN_yjHFH.js → tooltip-B6YrI4aJ.js} +1 -1
- package/src/ui/dist/assets/trash-Bc8jGp0V.js +32 -0
- package/src/ui/dist/assets/{useCliAccess-DV2L2Qxy.js → useCliAccess-mXVCYSZ-.js} +12 -42
- package/src/ui/dist/assets/{useFileDiffOverlay-DyTj-p_V.js → useFileDiffOverlay-Bg6b9H9K.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-ozYHtUwq.js → wrap-text-Drh5GEnL.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-BN9MUyCQ.js → zoom-out-CJj9DZLn.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/assets/fonts/Inter-Variable.ttf +0 -0
- package/assets/fonts/NotoSerifSC-Regular-C94HN_ZN.ttf +0 -0
- package/assets/fonts/NunitoSans-Variable.ttf +0 -0
- package/assets/fonts/Satoshi-Medium-ByP-Zb-9.woff2 +0 -0
- package/assets/fonts/SourceSans3-Variable.ttf +0 -0
- package/assets/fonts/ds-fonts.css +0 -83
- package/src/ui/dist/assets/Inter-Variable-VF2RPR_K.ttf +0 -0
- package/src/ui/dist/assets/LabPlugin-bL7rpic8.js +0 -43
- package/src/ui/dist/assets/NotoSerifSC-Regular-C94HN_ZN-C94HN_ZN.ttf +0 -0
- package/src/ui/dist/assets/NunitoSans-Variable-B_ZymHAd.ttf +0 -0
- package/src/ui/dist/assets/Satoshi-Medium-ByP-Zb-9-GkA34YXu.woff2 +0 -0
- package/src/ui/dist/assets/SourceSans3-Variable-CD-WOsSK.ttf +0 -0
- package/src/ui/dist/assets/info-CcsK_htA.js +0 -18
- package/src/ui/dist/assets/user-plus-BusDx-hF.js +0 -79
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import base64
|
|
3
4
|
import json
|
|
5
|
+
import mimetypes
|
|
4
6
|
import os
|
|
5
7
|
import shutil
|
|
6
8
|
import threading
|
|
@@ -8,11 +10,14 @@ import time
|
|
|
8
10
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
from typing import Any
|
|
11
|
-
from urllib.parse import urlencode, urlparse
|
|
13
|
+
from urllib.parse import parse_qs, urlencode, urlparse
|
|
14
|
+
from urllib.request import Request, urlopen
|
|
12
15
|
|
|
13
16
|
from ..artifact import ArtifactService
|
|
14
17
|
from ..bash_exec import BashExecService
|
|
18
|
+
from ..bash_exec.runtime import TerminalClient
|
|
15
19
|
from ..bridges import get_connector_bridge, register_builtin_connector_bridges
|
|
20
|
+
from ..bridges.connectors import QQConnectorBridge
|
|
16
21
|
from ..channels import QQRelayChannel, get_channel_factory, list_channel_names, register_builtin_channels
|
|
17
22
|
from ..channels.discord_gateway import DiscordGatewayService
|
|
18
23
|
from ..channels.feishu_long_connection import FeishuLongConnectionService
|
|
@@ -31,14 +36,24 @@ from ..prompts.builder import STANDARD_SKILLS
|
|
|
31
36
|
from ..quest import QuestService
|
|
32
37
|
from ..runners import CodexRunner, RunRequest, get_runner_factory, register_builtin_runners
|
|
33
38
|
from ..runtime_logs import JsonlLogger
|
|
34
|
-
from ..shared import append_jsonl, generate_id, read_json, read_jsonl, read_text, resolve_within, run_command, utc_now, which
|
|
39
|
+
from ..shared import append_jsonl, ensure_dir, generate_id, read_json, read_jsonl, read_text, resolve_within, run_command, slugify, utc_now, which, write_json
|
|
35
40
|
from ..skills import SkillInstaller
|
|
36
41
|
from ..team import SingleTeamService
|
|
37
42
|
from .api import ApiHandlers, match_route
|
|
38
43
|
from .sessions import SessionStore
|
|
44
|
+
from websockets.datastructures import Headers
|
|
45
|
+
from websockets.exceptions import ConnectionClosed
|
|
46
|
+
from websockets.http11 import Request as WebSocketRequest
|
|
47
|
+
from websockets.http11 import Response
|
|
48
|
+
from websockets.sync.server import Server as WebSocketServer
|
|
49
|
+
from websockets.sync.server import ServerConnection, serve as websocket_serve
|
|
50
|
+
|
|
51
|
+
TERMINAL_STREAM_IDLE_SLEEP_SECONDS = 0.02
|
|
39
52
|
|
|
40
53
|
|
|
41
54
|
class DaemonApp:
|
|
55
|
+
_MAX_INBOUND_ATTACHMENT_BYTES = 25 * 1024 * 1024
|
|
56
|
+
|
|
42
57
|
def __init__(self, home: Path) -> None:
|
|
43
58
|
self.home = home.resolve()
|
|
44
59
|
self.daemon_id = str(os.environ.get("DS_DAEMON_ID") or "").strip() or generate_id("daemon")
|
|
@@ -87,6 +102,10 @@ class DaemonApp:
|
|
|
87
102
|
self._turn_lock = threading.Lock()
|
|
88
103
|
self._turn_state: dict[str, dict[str, object]] = {}
|
|
89
104
|
self._server: ThreadingHTTPServer | None = None
|
|
105
|
+
self._terminal_attach_server: WebSocketServer | None = None
|
|
106
|
+
self._terminal_attach_thread: threading.Thread | None = None
|
|
107
|
+
self._terminal_attach_host: str | None = None
|
|
108
|
+
self._terminal_attach_port: int | None = None
|
|
90
109
|
self._shutdown_requested = threading.Event()
|
|
91
110
|
self._qq_gateway: QQGatewayService | None = None
|
|
92
111
|
self._telegram_polling: TelegramPollingService | None = None
|
|
@@ -96,6 +115,196 @@ class DaemonApp:
|
|
|
96
115
|
self._whatsapp_local_session: WhatsAppLocalSessionService | None = None
|
|
97
116
|
self.handlers = ApiHandlers(self)
|
|
98
117
|
|
|
118
|
+
def list_connector_statuses(self) -> list[dict[str, object]]:
|
|
119
|
+
items = [channel.status() for channel in self.channels.values()]
|
|
120
|
+
lingzhu_config = self.connectors_config.get("lingzhu")
|
|
121
|
+
if isinstance(lingzhu_config, dict):
|
|
122
|
+
items.append(self.config_manager.lingzhu_snapshot(lingzhu_config))
|
|
123
|
+
return items
|
|
124
|
+
|
|
125
|
+
def _process_terminal_attach_request(
|
|
126
|
+
self,
|
|
127
|
+
connection: ServerConnection,
|
|
128
|
+
request: WebSocketRequest,
|
|
129
|
+
) -> Response | None:
|
|
130
|
+
query = parse_qs(urlparse(request.path).query)
|
|
131
|
+
token = str((query.get("token") or [""])[0] or "").strip()
|
|
132
|
+
if not token:
|
|
133
|
+
return Response(
|
|
134
|
+
400,
|
|
135
|
+
"Bad Request",
|
|
136
|
+
Headers({"Content-Type": "text/plain; charset=utf-8"}),
|
|
137
|
+
b"Missing terminal attach token.",
|
|
138
|
+
)
|
|
139
|
+
attach_token, runtime = self.bash_exec_service.resolve_terminal_attach_token(token)
|
|
140
|
+
if attach_token is None:
|
|
141
|
+
return Response(
|
|
142
|
+
404,
|
|
143
|
+
"Not Found",
|
|
144
|
+
Headers({"Content-Type": "text/plain; charset=utf-8"}),
|
|
145
|
+
b"Terminal attach token is invalid or expired.",
|
|
146
|
+
)
|
|
147
|
+
if runtime is None:
|
|
148
|
+
return Response(
|
|
149
|
+
409,
|
|
150
|
+
"Conflict",
|
|
151
|
+
Headers({"Content-Type": "text/plain; charset=utf-8"}),
|
|
152
|
+
b"Terminal runtime is no longer active.",
|
|
153
|
+
)
|
|
154
|
+
setattr(connection, "_ds_terminal_attach_token", token)
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
def _handle_terminal_attach_connection(self, connection: ServerConnection) -> None:
|
|
158
|
+
token_value = str(getattr(connection, "_ds_terminal_attach_token", "") or "").strip()
|
|
159
|
+
attach_token, runtime = self.bash_exec_service.consume_terminal_attach_token(token_value)
|
|
160
|
+
if attach_token is None or runtime is None:
|
|
161
|
+
try:
|
|
162
|
+
connection.close(code=1011, reason="terminal_attach_unavailable")
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
send_lock = threading.Lock()
|
|
168
|
+
client = TerminalClient(
|
|
169
|
+
client_id=generate_id("tclient"),
|
|
170
|
+
send_text=connection.send,
|
|
171
|
+
send_binary=connection.send,
|
|
172
|
+
close=connection.close,
|
|
173
|
+
send_lock=send_lock,
|
|
174
|
+
)
|
|
175
|
+
runtime.attach_client(client)
|
|
176
|
+
try:
|
|
177
|
+
session = self.bash_exec_service.get_session(attach_token.quest_root, attach_token.bash_id)
|
|
178
|
+
with send_lock:
|
|
179
|
+
connection.send(
|
|
180
|
+
json.dumps(
|
|
181
|
+
{
|
|
182
|
+
"type": "ready",
|
|
183
|
+
"bash_id": attach_token.bash_id,
|
|
184
|
+
"status": session.get("status"),
|
|
185
|
+
"cwd": session.get("cwd"),
|
|
186
|
+
"workdir": session.get("workdir"),
|
|
187
|
+
},
|
|
188
|
+
ensure_ascii=False,
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
for chunk in runtime.snapshot_replay():
|
|
192
|
+
if chunk:
|
|
193
|
+
connection.send(chunk)
|
|
194
|
+
while True:
|
|
195
|
+
try:
|
|
196
|
+
message = connection.recv()
|
|
197
|
+
except ConnectionClosed:
|
|
198
|
+
break
|
|
199
|
+
if message is None:
|
|
200
|
+
break
|
|
201
|
+
if isinstance(message, bytes):
|
|
202
|
+
runtime.write_binary_input(message)
|
|
203
|
+
continue
|
|
204
|
+
try:
|
|
205
|
+
payload = json.loads(message)
|
|
206
|
+
except json.JSONDecodeError:
|
|
207
|
+
continue
|
|
208
|
+
if not isinstance(payload, dict):
|
|
209
|
+
continue
|
|
210
|
+
message_type = str(payload.get("type") or "").strip().lower()
|
|
211
|
+
if message_type == "input":
|
|
212
|
+
self.bash_exec_service.append_terminal_input(
|
|
213
|
+
attach_token.quest_root,
|
|
214
|
+
attach_token.bash_id,
|
|
215
|
+
data=str(payload.get("data") or ""),
|
|
216
|
+
source="web-pty",
|
|
217
|
+
)
|
|
218
|
+
continue
|
|
219
|
+
if message_type == "binary_input":
|
|
220
|
+
raw = str(payload.get("data") or "")
|
|
221
|
+
if raw:
|
|
222
|
+
runtime.write_binary_input(base64.b64decode(raw))
|
|
223
|
+
continue
|
|
224
|
+
if message_type == "resize":
|
|
225
|
+
cols = int(payload.get("cols") or 0)
|
|
226
|
+
rows = int(payload.get("rows") or 0)
|
|
227
|
+
self.bash_exec_service.resize_terminal_session(
|
|
228
|
+
attach_token.quest_root,
|
|
229
|
+
attach_token.bash_id,
|
|
230
|
+
cols=cols,
|
|
231
|
+
rows=rows,
|
|
232
|
+
)
|
|
233
|
+
continue
|
|
234
|
+
if message_type == "detach":
|
|
235
|
+
break
|
|
236
|
+
if message_type == "ping":
|
|
237
|
+
with send_lock:
|
|
238
|
+
connection.send(json.dumps({"type": "pong"}, ensure_ascii=False))
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
try:
|
|
241
|
+
with send_lock:
|
|
242
|
+
connection.send(
|
|
243
|
+
json.dumps(
|
|
244
|
+
{"type": "error", "message": str(exc)},
|
|
245
|
+
ensure_ascii=False,
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
except Exception:
|
|
249
|
+
pass
|
|
250
|
+
finally:
|
|
251
|
+
runtime.detach_client(client.client_id)
|
|
252
|
+
try:
|
|
253
|
+
connection.close()
|
|
254
|
+
except Exception:
|
|
255
|
+
pass
|
|
256
|
+
|
|
257
|
+
def _start_terminal_attach_server(self, host: str, port: int) -> None:
|
|
258
|
+
if self._terminal_attach_server is not None:
|
|
259
|
+
return
|
|
260
|
+
candidates: list[int] = []
|
|
261
|
+
if port > 0 and port < 65535:
|
|
262
|
+
candidates.append(port + 1)
|
|
263
|
+
candidates.append(0)
|
|
264
|
+
last_error: Exception | None = None
|
|
265
|
+
for candidate in candidates:
|
|
266
|
+
try:
|
|
267
|
+
server = websocket_serve(
|
|
268
|
+
self._handle_terminal_attach_connection,
|
|
269
|
+
host=host,
|
|
270
|
+
port=candidate,
|
|
271
|
+
process_request=self._process_terminal_attach_request,
|
|
272
|
+
compression=None,
|
|
273
|
+
max_size=None,
|
|
274
|
+
max_queue=None,
|
|
275
|
+
)
|
|
276
|
+
self._terminal_attach_server = server
|
|
277
|
+
self._terminal_attach_host = host
|
|
278
|
+
self._terminal_attach_port = int(server.socket.getsockname()[1])
|
|
279
|
+
thread = threading.Thread(
|
|
280
|
+
target=server.serve_forever,
|
|
281
|
+
daemon=True,
|
|
282
|
+
name="deepscientist-terminal-attach",
|
|
283
|
+
)
|
|
284
|
+
thread.start()
|
|
285
|
+
self._terminal_attach_thread = thread
|
|
286
|
+
return
|
|
287
|
+
except OSError as exc:
|
|
288
|
+
last_error = exc
|
|
289
|
+
continue
|
|
290
|
+
if last_error is not None:
|
|
291
|
+
raise last_error
|
|
292
|
+
|
|
293
|
+
def _stop_terminal_attach_server(self) -> None:
|
|
294
|
+
server = self._terminal_attach_server
|
|
295
|
+
thread = self._terminal_attach_thread
|
|
296
|
+
self._terminal_attach_server = None
|
|
297
|
+
self._terminal_attach_thread = None
|
|
298
|
+
self._terminal_attach_host = None
|
|
299
|
+
self._terminal_attach_port = None
|
|
300
|
+
if server is not None:
|
|
301
|
+
try:
|
|
302
|
+
server.shutdown()
|
|
303
|
+
except Exception:
|
|
304
|
+
pass
|
|
305
|
+
if thread is not None and thread.is_alive():
|
|
306
|
+
thread.join(timeout=1.0)
|
|
307
|
+
|
|
99
308
|
def get_runner(self, name: str):
|
|
100
309
|
normalized = str(name or "").strip().lower()
|
|
101
310
|
try:
|
|
@@ -200,6 +409,7 @@ class DaemonApp:
|
|
|
200
409
|
source: str = "local",
|
|
201
410
|
announce_connector_binding: bool = True,
|
|
202
411
|
exclude_conversation_id: str | None = None,
|
|
412
|
+
preferred_connector_conversation_id: str | None = None,
|
|
203
413
|
requested_baseline_ref: dict[str, object] | None = None,
|
|
204
414
|
startup_contract: dict[str, object] | None = None,
|
|
205
415
|
) -> dict:
|
|
@@ -248,13 +458,48 @@ class DaemonApp:
|
|
|
248
458
|
raise RuntimeError(
|
|
249
459
|
f"Quest creation failed because the requested baseline `{baseline_id}` could not be attached and confirmed: {exc}"
|
|
250
460
|
) from exc
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
461
|
+
preferred_binding = normalize_conversation_id(preferred_connector_conversation_id)
|
|
462
|
+
preferred_parsed = parse_conversation_id(preferred_binding)
|
|
463
|
+
if (
|
|
464
|
+
preferred_binding
|
|
465
|
+
and preferred_parsed
|
|
466
|
+
and str(preferred_parsed.get("connector") or "").strip().lower() in self.channels
|
|
467
|
+
and str(preferred_parsed.get("connector") or "").strip().lower() != "local"
|
|
468
|
+
):
|
|
469
|
+
connector_name = str(preferred_parsed.get("connector") or "").strip().lower()
|
|
470
|
+
result = self.update_quest_binding(snapshot["quest_id"], preferred_binding, force=True)
|
|
471
|
+
if isinstance(result, tuple):
|
|
472
|
+
self.logger.log(
|
|
473
|
+
"warning",
|
|
474
|
+
"quest.preferred_connector_binding_failed",
|
|
475
|
+
quest_id=snapshot.get("quest_id"),
|
|
476
|
+
conversation_id=preferred_binding,
|
|
477
|
+
status=result[0],
|
|
478
|
+
message=str(result[1].get("message") or "Unable to bind preferred connector target."),
|
|
479
|
+
)
|
|
480
|
+
elif announce_connector_binding:
|
|
481
|
+
channel = self._channel_with_bindings(connector_name)
|
|
482
|
+
channel.send(
|
|
483
|
+
{
|
|
484
|
+
"conversation_id": preferred_binding,
|
|
485
|
+
"quest_id": snapshot["quest_id"],
|
|
486
|
+
"kind": "ack",
|
|
487
|
+
"message": self._quest_created_connector_message(
|
|
488
|
+
connector_name,
|
|
489
|
+
quest_id=snapshot["quest_id"],
|
|
490
|
+
goal=goal,
|
|
491
|
+
previous_quest_id=str(result.get("previous_quest_id") or "").strip() or None,
|
|
492
|
+
),
|
|
493
|
+
}
|
|
494
|
+
)
|
|
495
|
+
elif not preferred_binding:
|
|
496
|
+
self._auto_bind_connectors_to_latest_quest(
|
|
497
|
+
snapshot["quest_id"],
|
|
498
|
+
goal=goal,
|
|
499
|
+
source=source,
|
|
500
|
+
announce=announce_connector_binding,
|
|
501
|
+
exclude_conversation_id=exclude_conversation_id,
|
|
502
|
+
)
|
|
258
503
|
return snapshot
|
|
259
504
|
|
|
260
505
|
def schedule_turn(self, quest_id: str, *, reason: str = "user_message") -> dict:
|
|
@@ -650,7 +895,8 @@ class DaemonApp:
|
|
|
650
895
|
if state.get("stop_requested"):
|
|
651
896
|
return
|
|
652
897
|
snapshot = self.quest_service.snapshot(quest_id)
|
|
653
|
-
|
|
898
|
+
runtime_status = str(snapshot.get("runtime_status") or snapshot.get("status") or "").strip()
|
|
899
|
+
if runtime_status in {"stopped", "paused", "completed", "error"} and not snapshot.get("active_run_id"):
|
|
654
900
|
return
|
|
655
901
|
latest_user_message = self._latest_user_message(quest_id)
|
|
656
902
|
if turn_reason != "auto_continue" and latest_user_message is None:
|
|
@@ -1411,6 +1657,8 @@ class DaemonApp:
|
|
|
1411
1657
|
interrupted_quests.append(quest_id)
|
|
1412
1658
|
self._shutdown_requested.set()
|
|
1413
1659
|
self._stop_background_connectors()
|
|
1660
|
+
self._stop_terminal_attach_server()
|
|
1661
|
+
self.bash_exec_service.shutdown()
|
|
1414
1662
|
self.logger.log(
|
|
1415
1663
|
"info",
|
|
1416
1664
|
"daemon.shutdown_requested",
|
|
@@ -1545,6 +1793,7 @@ class DaemonApp:
|
|
|
1545
1793
|
self.sessions.bind(quest_id, normalized)
|
|
1546
1794
|
self.quest_service.bind_source(quest_id, "local:default")
|
|
1547
1795
|
self.quest_service.bind_source(quest_id, normalized)
|
|
1796
|
+
removed = self._unbind_external_bindings(quest_id, preserve={normalized})
|
|
1548
1797
|
snapshot = self.quest_service.snapshot(quest_id)
|
|
1549
1798
|
previous_quest_id = str(existing_bound or "").strip() or None
|
|
1550
1799
|
if previous_quest_id == quest_id:
|
|
@@ -1560,7 +1809,7 @@ class DaemonApp:
|
|
|
1560
1809
|
"quest_id": quest_id,
|
|
1561
1810
|
"conversation_id": normalized,
|
|
1562
1811
|
"snapshot": snapshot,
|
|
1563
|
-
"removed_conversations":
|
|
1812
|
+
"removed_conversations": removed,
|
|
1564
1813
|
"conflicts_resolved": [item.get("quest_id") for item in deduped_conflicts if item.get("quest_id")],
|
|
1565
1814
|
"previous_quest_id": previous_quest_id,
|
|
1566
1815
|
}
|
|
@@ -2127,11 +2376,21 @@ class DaemonApp:
|
|
|
2127
2376
|
)
|
|
2128
2377
|
|
|
2129
2378
|
self.sessions.bind(quest_id, conversation_id)
|
|
2379
|
+
materialized_attachments = self._materialize_connector_attachments(
|
|
2380
|
+
quest_id=quest_id,
|
|
2381
|
+
connector_name=connector_name,
|
|
2382
|
+
conversation_id=conversation_id,
|
|
2383
|
+
message_id=str(message.get("message_id") or "").strip() or None,
|
|
2384
|
+
attachments=[dict(item) for item in (message.get("attachments") or []) if isinstance(item, dict)],
|
|
2385
|
+
)
|
|
2130
2386
|
self.submit_user_message(
|
|
2131
2387
|
quest_id,
|
|
2132
|
-
text=
|
|
2388
|
+
text=self._connector_message_text_with_attachment_notice(
|
|
2389
|
+
original_text=text,
|
|
2390
|
+
attachments=materialized_attachments,
|
|
2391
|
+
),
|
|
2133
2392
|
source=conversation_id,
|
|
2134
|
-
attachments=
|
|
2393
|
+
attachments=materialized_attachments,
|
|
2135
2394
|
)
|
|
2136
2395
|
return channel.send(
|
|
2137
2396
|
{
|
|
@@ -2148,6 +2407,171 @@ class DaemonApp:
|
|
|
2148
2407
|
}
|
|
2149
2408
|
)
|
|
2150
2409
|
|
|
2410
|
+
def _connector_message_text_with_attachment_notice(
|
|
2411
|
+
self,
|
|
2412
|
+
*,
|
|
2413
|
+
original_text: str,
|
|
2414
|
+
attachments: list[dict[str, object]],
|
|
2415
|
+
) -> str:
|
|
2416
|
+
base = str(original_text or "").strip()
|
|
2417
|
+
if not attachments:
|
|
2418
|
+
return base
|
|
2419
|
+
lines: list[str] = []
|
|
2420
|
+
if base:
|
|
2421
|
+
lines.extend([base, ""])
|
|
2422
|
+
lines.append(
|
|
2423
|
+
self._polite_copy(
|
|
2424
|
+
zh="系统提示:用户刚刚发送了附件。请优先阅读这些 quest 本地文件,再继续处理这条请求:",
|
|
2425
|
+
en="System note: the user just sent attachments. Read these quest-local files first before continuing this request:",
|
|
2426
|
+
)
|
|
2427
|
+
)
|
|
2428
|
+
for index, item in enumerate(attachments, start=1):
|
|
2429
|
+
label = str(
|
|
2430
|
+
item.get("name")
|
|
2431
|
+
or item.get("quest_relative_path")
|
|
2432
|
+
or item.get("path")
|
|
2433
|
+
or item.get("url")
|
|
2434
|
+
or f"attachment-{index}"
|
|
2435
|
+
).strip()
|
|
2436
|
+
content_type = str(item.get("content_type") or "").strip()
|
|
2437
|
+
location = str(item.get("path") or item.get("url") or "unavailable").strip()
|
|
2438
|
+
error = str(item.get("download_error") or "").strip()
|
|
2439
|
+
suffix = f" ({content_type})" if content_type else ""
|
|
2440
|
+
if error:
|
|
2441
|
+
lines.append(f"- {label}{suffix}: {location} | download_error={error}")
|
|
2442
|
+
else:
|
|
2443
|
+
lines.append(f"- {label}{suffix}: {location}")
|
|
2444
|
+
return "\n".join(lines).strip()
|
|
2445
|
+
|
|
2446
|
+
def _materialize_connector_attachments(
|
|
2447
|
+
self,
|
|
2448
|
+
*,
|
|
2449
|
+
quest_id: str,
|
|
2450
|
+
connector_name: str,
|
|
2451
|
+
conversation_id: str,
|
|
2452
|
+
message_id: str | None,
|
|
2453
|
+
attachments: list[dict[str, object]],
|
|
2454
|
+
) -> list[dict[str, object]]:
|
|
2455
|
+
if not attachments:
|
|
2456
|
+
return []
|
|
2457
|
+
quest_root = self.home / "quests" / quest_id
|
|
2458
|
+
batch_slug = slugify(message_id or generate_id("userfile"), default=generate_id("userfile"))
|
|
2459
|
+
batch_root = ensure_dir(quest_root / "userfiles" / connector_name / batch_slug)
|
|
2460
|
+
materialized: list[dict[str, object]] = []
|
|
2461
|
+
for index, raw_item in enumerate(attachments, start=1):
|
|
2462
|
+
materialized.append(
|
|
2463
|
+
self._materialize_single_connector_attachment(
|
|
2464
|
+
connector_name=connector_name,
|
|
2465
|
+
quest_root=quest_root,
|
|
2466
|
+
batch_root=batch_root,
|
|
2467
|
+
index=index,
|
|
2468
|
+
attachment=dict(raw_item),
|
|
2469
|
+
)
|
|
2470
|
+
)
|
|
2471
|
+
write_json(
|
|
2472
|
+
batch_root / "manifest.json",
|
|
2473
|
+
{
|
|
2474
|
+
"connector": connector_name,
|
|
2475
|
+
"quest_id": quest_id,
|
|
2476
|
+
"conversation_id": conversation_id,
|
|
2477
|
+
"message_id": message_id,
|
|
2478
|
+
"materialized_at": utc_now(),
|
|
2479
|
+
"attachments": materialized,
|
|
2480
|
+
},
|
|
2481
|
+
)
|
|
2482
|
+
return materialized
|
|
2483
|
+
|
|
2484
|
+
def _materialize_single_connector_attachment(
|
|
2485
|
+
self,
|
|
2486
|
+
*,
|
|
2487
|
+
connector_name: str,
|
|
2488
|
+
quest_root: Path,
|
|
2489
|
+
batch_root: Path,
|
|
2490
|
+
index: int,
|
|
2491
|
+
attachment: dict[str, object],
|
|
2492
|
+
) -> dict[str, object]:
|
|
2493
|
+
resolved = dict(attachment)
|
|
2494
|
+
name = str(resolved.get("name") or "").strip()
|
|
2495
|
+
content_type = str(resolved.get("content_type") or "").strip()
|
|
2496
|
+
url = str(resolved.get("url") or "").strip()
|
|
2497
|
+
target_path = batch_root / self._connector_attachment_filename(index=index, name=name, content_type=content_type)
|
|
2498
|
+
resolved["manifest_path"] = str(batch_root / "manifest.json")
|
|
2499
|
+
resolved["batch_path"] = str(batch_root)
|
|
2500
|
+
if not url:
|
|
2501
|
+
resolved["materialized"] = False
|
|
2502
|
+
resolved["download_error"] = "missing_download_url"
|
|
2503
|
+
return resolved
|
|
2504
|
+
try:
|
|
2505
|
+
size_bytes = self._download_connector_attachment(
|
|
2506
|
+
connector_name=connector_name,
|
|
2507
|
+
url=url,
|
|
2508
|
+
target_path=target_path,
|
|
2509
|
+
)
|
|
2510
|
+
resolved["path"] = str(target_path)
|
|
2511
|
+
resolved["quest_relative_path"] = str(target_path.relative_to(quest_root))
|
|
2512
|
+
resolved["size_bytes"] = int(size_bytes)
|
|
2513
|
+
resolved["materialized"] = True
|
|
2514
|
+
resolved["downloaded_at"] = utc_now()
|
|
2515
|
+
return resolved
|
|
2516
|
+
except Exception as exc:
|
|
2517
|
+
if target_path.exists():
|
|
2518
|
+
target_path.unlink(missing_ok=True)
|
|
2519
|
+
resolved["materialized"] = False
|
|
2520
|
+
resolved["download_error"] = str(exc)
|
|
2521
|
+
return resolved
|
|
2522
|
+
|
|
2523
|
+
def _download_connector_attachment(
|
|
2524
|
+
self,
|
|
2525
|
+
*,
|
|
2526
|
+
connector_name: str,
|
|
2527
|
+
url: str,
|
|
2528
|
+
target_path: Path,
|
|
2529
|
+
) -> int:
|
|
2530
|
+
request = Request(url)
|
|
2531
|
+
for key, value in self._connector_attachment_headers(connector_name).items():
|
|
2532
|
+
request.add_header(key, value)
|
|
2533
|
+
ensure_dir(target_path.parent)
|
|
2534
|
+
total = 0
|
|
2535
|
+
with urlopen(request, timeout=20) as response: # noqa: S310
|
|
2536
|
+
with target_path.open("wb") as handle:
|
|
2537
|
+
while True:
|
|
2538
|
+
chunk = response.read(65536)
|
|
2539
|
+
if not chunk:
|
|
2540
|
+
break
|
|
2541
|
+
total += len(chunk)
|
|
2542
|
+
if total > self._MAX_INBOUND_ATTACHMENT_BYTES:
|
|
2543
|
+
raise ValueError(
|
|
2544
|
+
f"attachment exceeds max inbound size limit ({self._MAX_INBOUND_ATTACHMENT_BYTES} bytes)"
|
|
2545
|
+
)
|
|
2546
|
+
handle.write(chunk)
|
|
2547
|
+
return total
|
|
2548
|
+
|
|
2549
|
+
def _connector_attachment_headers(self, connector_name: str) -> dict[str, str]:
|
|
2550
|
+
if str(connector_name or "").strip().lower() != "qq":
|
|
2551
|
+
return {}
|
|
2552
|
+
config = self.connectors_config.get("qq", {})
|
|
2553
|
+
if not isinstance(config, dict):
|
|
2554
|
+
return {}
|
|
2555
|
+
app_id = str(config.get("app_id") or "").strip()
|
|
2556
|
+
app_secret = QQConnectorBridge.read_secret(config, "app_secret", "app_secret_env")
|
|
2557
|
+
if not app_id or not app_secret:
|
|
2558
|
+
return {}
|
|
2559
|
+
return {
|
|
2560
|
+
"Authorization": f"QQBot {QQConnectorBridge._access_token(app_id, app_secret)}",
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
@staticmethod
|
|
2564
|
+
def _connector_attachment_filename(*, index: int, name: str, content_type: str) -> str:
|
|
2565
|
+
raw_name = str(name or "").strip()
|
|
2566
|
+
suffix = Path(raw_name).suffix if raw_name else ""
|
|
2567
|
+
if not suffix and content_type:
|
|
2568
|
+
suffix = mimetypes.guess_extension(content_type, strict=False) or ""
|
|
2569
|
+
if suffix and not suffix.startswith("."):
|
|
2570
|
+
suffix = f".{suffix}"
|
|
2571
|
+
stem_source = Path(raw_name).stem if raw_name else f"attachment-{index:03d}"
|
|
2572
|
+
stem = slugify(stem_source, default=f"attachment-{index:03d}")
|
|
2573
|
+
return f"{stem}{suffix or '.bin'}"
|
|
2574
|
+
|
|
2151
2575
|
def _qq_channel(self) -> QQRelayChannel:
|
|
2152
2576
|
return self.channels["qq"] # type: ignore[return-value]
|
|
2153
2577
|
|
|
@@ -3141,7 +3565,7 @@ class DaemonApp:
|
|
|
3141
3565
|
handler.wfile.write(b": keep-alive\n\n")
|
|
3142
3566
|
handler.wfile.flush()
|
|
3143
3567
|
heartbeat_at = time.monotonic()
|
|
3144
|
-
time.sleep(
|
|
3568
|
+
time.sleep(TERMINAL_STREAM_IDLE_SLEEP_SECONDS)
|
|
3145
3569
|
except (BrokenPipeError, ConnectionResetError, TimeoutError):
|
|
3146
3570
|
return
|
|
3147
3571
|
|
|
@@ -3243,7 +3667,7 @@ class DaemonApp:
|
|
|
3243
3667
|
headers=dict(self.headers.items()),
|
|
3244
3668
|
body=body,
|
|
3245
3669
|
)
|
|
3246
|
-
elif route_name in {"document_open", "document_asset_upload", "chat", "command", "quest_control", "config_save", "quest_create", "quest_baseline_binding", "run_create", "qq_inbound", "connector_inbound", "docs_open", "admin_shutdown", "bash_stop", "quest_settings", "quest_bindings", "quest_delete", "terminal_session_ensure", "terminal_input", "stage_view", "latex_init", "latex_compile"}:
|
|
3670
|
+
elif route_name in {"document_open", "document_asset_upload", "chat", "command", "quest_control", "config_save", "quest_create", "quest_baseline_binding", "run_create", "qq_inbound", "connector_inbound", "docs_open", "admin_shutdown", "bash_stop", "quest_settings", "quest_bindings", "quest_delete", "terminal_session_ensure", "terminal_attach", "terminal_input", "stage_view", "latex_init", "latex_compile"}:
|
|
3247
3671
|
payload = result(**params, body=body)
|
|
3248
3672
|
elif route_name == "config_validate":
|
|
3249
3673
|
payload = result(body)
|
|
@@ -3288,6 +3712,7 @@ class DaemonApp:
|
|
|
3288
3712
|
server.daemon_threads = True
|
|
3289
3713
|
self._server = server
|
|
3290
3714
|
self._shutdown_requested.clear()
|
|
3715
|
+
self._start_terminal_attach_server(host, port)
|
|
3291
3716
|
self._start_background_connectors()
|
|
3292
3717
|
print(f"DeepScientist daemon listening on http://{host}:{port}")
|
|
3293
3718
|
try:
|
|
@@ -3296,5 +3721,7 @@ class DaemonApp:
|
|
|
3296
3721
|
pass
|
|
3297
3722
|
finally:
|
|
3298
3723
|
self._stop_background_connectors()
|
|
3724
|
+
self._stop_terminal_attach_server()
|
|
3725
|
+
self.bash_exec_service.shutdown()
|
|
3299
3726
|
self._server = None
|
|
3300
3727
|
server.server_close()
|