@researai/deepscientist 1.5.1 → 1.5.3
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/README.md +69 -1
- package/bin/ds.js +2239 -153
- package/docs/en/00_QUICK_START.md +60 -20
- package/docs/en/01_SETTINGS_REFERENCE.md +20 -20
- package/docs/en/02_START_RESEARCH_GUIDE.md +11 -11
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +10 -10
- package/docs/en/05_TUI_GUIDE.md +1 -1
- package/docs/en/09_DOCTOR.md +48 -4
- package/docs/en/90_ARCHITECTURE.md +4 -2
- package/docs/zh/00_QUICK_START.md +60 -20
- package/docs/zh/01_SETTINGS_REFERENCE.md +21 -21
- package/docs/zh/02_START_RESEARCH_GUIDE.md +19 -19
- package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +10 -10
- package/docs/zh/05_TUI_GUIDE.md +1 -1
- package/docs/zh/09_DOCTOR.md +46 -4
- package/install.sh +125 -8
- package/package.json +2 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +6 -1
- package/src/deepscientist/artifact/service.py +553 -26
- package/src/deepscientist/bash_exec/monitor.py +23 -4
- package/src/deepscientist/bash_exec/runtime.py +3 -0
- package/src/deepscientist/bash_exec/service.py +132 -4
- package/src/deepscientist/bridges/base.py +10 -19
- package/src/deepscientist/channels/discord_gateway.py +25 -2
- package/src/deepscientist/channels/feishu_long_connection.py +41 -3
- package/src/deepscientist/channels/qq.py +524 -64
- package/src/deepscientist/channels/qq_gateway.py +22 -3
- package/src/deepscientist/channels/relay.py +429 -90
- package/src/deepscientist/channels/slack_socket.py +29 -5
- package/src/deepscientist/channels/telegram_polling.py +25 -2
- package/src/deepscientist/channels/whatsapp_local_session.py +32 -4
- package/src/deepscientist/cli.py +27 -0
- package/src/deepscientist/config/models.py +6 -40
- package/src/deepscientist/config/service.py +165 -156
- package/src/deepscientist/connector_profiles.py +346 -0
- package/src/deepscientist/connector_runtime.py +88 -43
- package/src/deepscientist/daemon/api/handlers.py +65 -11
- package/src/deepscientist/daemon/api/router.py +4 -2
- package/src/deepscientist/daemon/app.py +772 -219
- package/src/deepscientist/doctor.py +69 -2
- package/src/deepscientist/gitops/diff.py +3 -0
- package/src/deepscientist/home.py +25 -2
- package/src/deepscientist/mcp/context.py +3 -1
- package/src/deepscientist/mcp/server.py +66 -7
- package/src/deepscientist/migration.py +114 -0
- package/src/deepscientist/prompts/builder.py +71 -3
- package/src/deepscientist/qq_profiles.py +186 -0
- package/src/deepscientist/quest/layout.py +1 -0
- package/src/deepscientist/quest/service.py +70 -12
- package/src/deepscientist/quest/stage_views.py +46 -0
- package/src/deepscientist/runners/codex.py +2 -0
- package/src/deepscientist/shared.py +44 -17
- package/src/prompts/connectors/lingzhu.md +3 -0
- package/src/prompts/connectors/qq.md +42 -2
- package/src/prompts/system.md +123 -10
- package/src/skills/analysis-campaign/SKILL.md +35 -6
- package/src/skills/baseline/SKILL.md +73 -32
- package/src/skills/decision/SKILL.md +4 -3
- package/src/skills/experiment/SKILL.md +28 -6
- package/src/skills/finalize/SKILL.md +5 -2
- package/src/skills/idea/SKILL.md +2 -2
- package/src/skills/intake-audit/SKILL.md +2 -2
- package/src/skills/rebuttal/SKILL.md +4 -2
- package/src/skills/review/SKILL.md +4 -2
- package/src/skills/scout/SKILL.md +2 -2
- package/src/skills/write/SKILL.md +2 -2
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-w5lF2Ttt.js → AiManusChatView-qzChi9uh.js} +67 -94
- package/src/ui/dist/assets/{AnalysisPlugin-DJOED79I.js → AnalysisPlugin-CcC_-UqN.js} +1 -1
- package/src/ui/dist/assets/{AutoFigurePlugin-DaG61Y0M.js → AutoFigurePlugin-DD8LkJLe.js} +5 -5
- package/src/ui/dist/assets/{CliPlugin-CV4LqUB_.js → CliPlugin-DJJFfVmW.js} +17 -110
- package/src/ui/dist/assets/{CodeEditorPlugin-DylfAea4.js → CodeEditorPlugin-CrjkHNLh.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-F7saY0LM.js → CodeViewerPlugin-obnD6G5R.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-COP0c7jf.js → DocViewerPlugin-DB9SUQVd.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-CAS05pT9.js → GitDiffViewerPlugin-DZLlNlD2.js} +1 -1
- package/src/ui/dist/assets/{ImageViewerPlugin-Bco1CN_w.js → ImageViewerPlugin-BGwfDZ0Y.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-CvMlCD99.js → LabCopilotPanel-dfLptQcR.js} +10 -10
- package/src/ui/dist/assets/{LabPlugin-BYankkE4.js → LabPlugin-CeGjAl3A.js} +1 -1
- package/src/ui/dist/assets/{LatexPlugin-LDSMR-t-.js → LatexPlugin-BBJ7kd1V.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-B7o80jgm.js → MarkdownViewerPlugin-DKZi7BcB.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-CM6ZOcpC.js → MarketplacePlugin-C_k-9jD0.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-Dc61cXmK.js → NotebookEditor-4R88_BMO.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-DWowuQwx.js → PdfLoader-DwEFQLrw.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-BsJM1q_a.js → PdfMarkdownPlugin-D-jdsqF8.js} +3 -3
- package/src/ui/dist/assets/{PdfViewerPlugin-DB2eEEFQ.js → PdfViewerPlugin-CmeBGDY0.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-CraThSvt.js → SearchPlugin-Dlz2WKJ4.js} +1 -1
- package/src/ui/dist/assets/{Stepper-CgocRTPq.js → Stepper-ClOgzWM3.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-B1JGhKtd.js → TextViewerPlugin-DDQWxibk.js} +4 -4
- package/src/ui/dist/assets/{VNCViewer-CclFC7FM.js → VNCViewer-CJXT0Nm8.js} +9 -9
- package/src/ui/dist/assets/{bibtex-D3IKsMl7.js → bibtex-DLr4Rtk4.js} +1 -1
- package/src/ui/dist/assets/{code-BP37Xx0p.js → code-DgKK408Y.js} +1 -1
- package/src/ui/dist/assets/{file-content-BAJSu-9r.js → file-content-6HBqQnvQ.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-DUGeCTuy.js → file-diff-panel-Dhu0TbBM.js} +1 -1
- package/src/ui/dist/assets/{file-socket-CXc1Ojf7.js → file-socket-CP3iwVZG.js} +1 -1
- package/src/ui/dist/assets/{file-utils-2J21jt7M.js → file-utils-BsS-Aw68.js} +1 -1
- package/src/ui/dist/assets/{image-CMMmgvcn.js → image-ByeK-Zcv.js} +1 -1
- package/src/ui/dist/assets/{index-DmwmJmbW.js → index-BLjo5--a.js} +33610 -31016
- package/src/ui/dist/assets/{index-CWgMgpow.js → index-BdsE0uRz.js} +11 -11
- package/src/ui/dist/assets/{index-s7aHnNQ4.js → index-C-eX-N6A.js} +1 -1
- package/src/ui/dist/assets/{index-KGt-z-dD.css → index-CuQhlrR-.css} +2747 -2
- package/src/ui/dist/assets/{index-BaVumsQT.js → index-DyremSIv.js} +2 -2
- package/src/ui/dist/assets/{message-square-CQRfX0Am.js → message-square-DnagiLnc.js} +1 -1
- package/src/ui/dist/assets/{monaco-B4TbdsrF.js → monaco-4kBFeprs.js} +1 -1
- package/src/ui/dist/assets/{popover-B8Rokodk.js → popover-hRCXZzs2.js} +1 -1
- package/src/ui/dist/assets/{project-sync-D_i96KH4.js → project-sync-O_85YuP6.js} +1 -1
- package/src/ui/dist/assets/{sigma-D12PnzCN.js → sigma-DvKopSnL.js} +1 -1
- package/src/ui/dist/assets/{tooltip-B6YrI4aJ.js → tooltip-BmlPc6kc.js} +1 -1
- package/src/ui/dist/assets/{trash-Bc8jGp0V.js → trash-n-UvdZFR.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-mXVCYSZ-.js → useCliAccess-WDd3_wIh.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-Bg6b9H9K.js → useFileDiffOverlay-rXLIL2NF.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-Drh5GEnL.js → wrap-text-qIYQ4a_W.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-CJj9DZLn.js → zoom-out-fZXCEFsy.js} +1 -1
- package/src/ui/dist/index.html +2 -2
- package/uv.lock +1155 -0
- package/src/ui/dist/assets/LabPlugin-D9jVIo0A.css +0 -2698
|
@@ -202,6 +202,18 @@ def _terminate_process(process: subprocess.Popen[bytes], process_group_id: int |
|
|
|
202
202
|
process.kill()
|
|
203
203
|
|
|
204
204
|
|
|
205
|
+
def _terminate_process_force(process: subprocess.Popen[bytes], process_group_id: int | None) -> None:
|
|
206
|
+
if process.poll() is not None:
|
|
207
|
+
return
|
|
208
|
+
if isinstance(process_group_id, int) and process_group_id > 0:
|
|
209
|
+
try:
|
|
210
|
+
os.killpg(process_group_id, signal.SIGKILL)
|
|
211
|
+
except ProcessLookupError:
|
|
212
|
+
return
|
|
213
|
+
else:
|
|
214
|
+
process.kill()
|
|
215
|
+
|
|
216
|
+
|
|
205
217
|
def _drain_buffer(
|
|
206
218
|
buffer: str,
|
|
207
219
|
append_line,
|
|
@@ -350,12 +362,15 @@ def run_monitor(session_dir: Path) -> int:
|
|
|
350
362
|
},
|
|
351
363
|
)
|
|
352
364
|
progress = _parse_progress_marker(line)
|
|
365
|
+
output_updates: dict[str, Any] = {}
|
|
366
|
+
if stream not in {"system", "prompt"}:
|
|
367
|
+
output_updates = {"last_output_at": timestamp, "last_output_seq": seq}
|
|
353
368
|
if progress is not None:
|
|
354
369
|
progress.setdefault("ts", timestamp)
|
|
355
370
|
_atomic_write_json(progress_path, progress)
|
|
356
|
-
update_meta(last_progress=progress, latest_seq=seq)
|
|
371
|
+
update_meta(last_progress=progress, latest_seq=seq, **output_updates)
|
|
357
372
|
else:
|
|
358
|
-
update_meta(latest_seq=seq)
|
|
373
|
+
update_meta(latest_seq=seq, **output_updates)
|
|
359
374
|
|
|
360
375
|
master_fd: int | None = None
|
|
361
376
|
slave_fd: int | None = None
|
|
@@ -410,16 +425,20 @@ def run_monitor(session_dir: Path) -> int:
|
|
|
410
425
|
if not stop_requested and stop_request_path.exists():
|
|
411
426
|
request = read_json(stop_request_path, {}) or {}
|
|
412
427
|
stop_reason = str(request.get("reason") or "user_stop").strip() or "user_stop"
|
|
428
|
+
force_stop = bool(request.get("force"))
|
|
413
429
|
update_meta(
|
|
414
430
|
status="terminating",
|
|
415
431
|
stop_reason=stop_reason,
|
|
416
432
|
stopped_by_user_id=str(request.get("user_id") or meta.get("stopped_by_user_id") or meta.get("agent_id") or "agent"),
|
|
417
433
|
)
|
|
418
434
|
append_line(
|
|
419
|
-
f"
|
|
435
|
+
f"{'Force t' if force_stop else 'T'}ermination requested: {stop_reason}",
|
|
420
436
|
stream="system",
|
|
421
437
|
)
|
|
422
|
-
|
|
438
|
+
if force_stop:
|
|
439
|
+
_terminate_process_force(process, process_group_id)
|
|
440
|
+
else:
|
|
441
|
+
_terminate_process(process, process_group_id)
|
|
423
442
|
stop_requested = True
|
|
424
443
|
|
|
425
444
|
if deadline is not None and time.monotonic() >= deadline and process.poll() is None and not stop_requested:
|
|
@@ -390,6 +390,9 @@ class TerminalRuntime:
|
|
|
390
390
|
progress.setdefault("ts", timestamp)
|
|
391
391
|
_atomic_write_json(self.meta_path.parent / "progress.json", progress)
|
|
392
392
|
meta["last_progress"] = progress
|
|
393
|
+
if stream not in {"system", "prompt"}:
|
|
394
|
+
meta["last_output_at"] = timestamp
|
|
395
|
+
meta["last_output_seq"] = seq
|
|
393
396
|
meta["latest_seq"] = seq
|
|
394
397
|
meta["updated_at"] = timestamp
|
|
395
398
|
_atomic_write_json(self.meta_path, meta)
|
|
@@ -11,6 +11,7 @@ import sys
|
|
|
11
11
|
import tempfile
|
|
12
12
|
import threading
|
|
13
13
|
import time
|
|
14
|
+
from datetime import UTC, datetime
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from typing import Any
|
|
16
17
|
|
|
@@ -26,6 +27,7 @@ DEFAULT_LOG_TAIL_LIMIT = 200
|
|
|
26
27
|
DEFAULT_POLL_INTERVAL_SECONDS = 0.35
|
|
27
28
|
TERMINAL_STATUSES = {"completed", "failed", "terminated"}
|
|
28
29
|
DEFAULT_TERMINAL_SESSION_ID = "terminal-main"
|
|
30
|
+
BASH_WATCHDOG_AFTER_SECONDS = 1800
|
|
29
31
|
INPUT_ESCAPE_SEQUENCE_RE = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b[@-_]")
|
|
30
32
|
|
|
31
33
|
|
|
@@ -89,6 +91,48 @@ def _parse_progress_marker(line: str) -> dict[str, Any] | None:
|
|
|
89
91
|
return payload
|
|
90
92
|
|
|
91
93
|
|
|
94
|
+
def _parse_timestamp(value: object) -> datetime | None:
|
|
95
|
+
normalized = _normalize_string(value)
|
|
96
|
+
if not normalized:
|
|
97
|
+
return None
|
|
98
|
+
try:
|
|
99
|
+
parsed = datetime.fromisoformat(normalized)
|
|
100
|
+
except ValueError:
|
|
101
|
+
return None
|
|
102
|
+
if parsed.tzinfo is None:
|
|
103
|
+
parsed = parsed.replace(tzinfo=UTC)
|
|
104
|
+
return parsed.astimezone(UTC)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _age_seconds(value: object, *, now: datetime | None = None) -> int | None:
|
|
108
|
+
parsed = _parse_timestamp(value)
|
|
109
|
+
if parsed is None:
|
|
110
|
+
return None
|
|
111
|
+
current = now or datetime.now(UTC)
|
|
112
|
+
return max(0, int((current - parsed).total_seconds()))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _latest_timestamp(*values: object) -> str | None:
|
|
116
|
+
latest_raw: str | None = None
|
|
117
|
+
latest_dt: datetime | None = None
|
|
118
|
+
for value in values:
|
|
119
|
+
normalized = _normalize_string(value)
|
|
120
|
+
parsed = _parse_timestamp(normalized)
|
|
121
|
+
if parsed is None:
|
|
122
|
+
continue
|
|
123
|
+
if latest_dt is None or parsed >= latest_dt:
|
|
124
|
+
latest_dt = parsed
|
|
125
|
+
latest_raw = normalized
|
|
126
|
+
return latest_raw
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _compact_command(command: object, *, max_length: int = 140) -> str:
|
|
130
|
+
normalized = " ".join(str(command or "").split())
|
|
131
|
+
if len(normalized) <= max_length:
|
|
132
|
+
return normalized
|
|
133
|
+
return normalized[: max(0, max_length - 3)].rstrip() + "..."
|
|
134
|
+
|
|
135
|
+
|
|
92
136
|
class BashExecService:
|
|
93
137
|
def __init__(self, home: Path) -> None:
|
|
94
138
|
self.home = home
|
|
@@ -224,8 +268,45 @@ class BashExecService:
|
|
|
224
268
|
payload["status"] = _coerce_session_status(payload.get("status"))
|
|
225
269
|
payload["last_progress"] = payload.get("last_progress") or read_json(self.progress_path(quest_root, str(payload.get("bash_id") or "")), None)
|
|
226
270
|
payload["kind"] = str(payload.get("kind") or "exec")
|
|
271
|
+
payload = self._enrich_watchdog_fields(payload)
|
|
227
272
|
return payload
|
|
228
273
|
|
|
274
|
+
@staticmethod
|
|
275
|
+
def _enrich_watchdog_fields(payload: dict[str, Any]) -> dict[str, Any]:
|
|
276
|
+
current = datetime.now(UTC)
|
|
277
|
+
last_progress = payload.get("last_progress")
|
|
278
|
+
last_progress_at = None
|
|
279
|
+
if isinstance(last_progress, dict):
|
|
280
|
+
last_progress_at = _normalize_string(last_progress.get("ts")) or None
|
|
281
|
+
last_output_at = _normalize_string(payload.get("last_output_at")) or None
|
|
282
|
+
latest_signal_at = _latest_timestamp(last_output_at, last_progress_at, payload.get("started_at"))
|
|
283
|
+
payload["last_progress_at"] = last_progress_at
|
|
284
|
+
payload["run_age_seconds"] = _age_seconds(payload.get("started_at"), now=current)
|
|
285
|
+
payload["status_age_seconds"] = _age_seconds(payload.get("updated_at"), now=current)
|
|
286
|
+
payload["silent_seconds"] = _age_seconds(last_output_at or payload.get("started_at"), now=current)
|
|
287
|
+
payload["progress_age_seconds"] = _age_seconds(last_progress_at, now=current)
|
|
288
|
+
payload["latest_signal_at"] = latest_signal_at
|
|
289
|
+
payload["signal_age_seconds"] = _age_seconds(latest_signal_at, now=current)
|
|
290
|
+
payload["watchdog_after_seconds"] = BASH_WATCHDOG_AFTER_SECONDS
|
|
291
|
+
payload["watchdog_overdue"] = (
|
|
292
|
+
payload.get("status") in {"running", "terminating"}
|
|
293
|
+
and isinstance(payload.get("signal_age_seconds"), int)
|
|
294
|
+
and int(payload["signal_age_seconds"]) >= BASH_WATCHDOG_AFTER_SECONDS
|
|
295
|
+
)
|
|
296
|
+
return payload
|
|
297
|
+
|
|
298
|
+
@staticmethod
|
|
299
|
+
def format_history_line(session: dict[str, Any]) -> str:
|
|
300
|
+
timestamp = (
|
|
301
|
+
_normalize_string(session.get("started_at"))
|
|
302
|
+
or _normalize_string(session.get("updated_at"))
|
|
303
|
+
or _normalize_string(session.get("finished_at"))
|
|
304
|
+
or "unknown-time"
|
|
305
|
+
)
|
|
306
|
+
command = _compact_command(session.get("command"))
|
|
307
|
+
bash_id = _normalize_string(session.get("bash_id") or session.get("id")) or "unknown-id"
|
|
308
|
+
return f"{timestamp} | {command} | {bash_id}"
|
|
309
|
+
|
|
229
310
|
@staticmethod
|
|
230
311
|
def _summary_session_payload(meta: dict[str, Any]) -> dict[str, Any]:
|
|
231
312
|
return {
|
|
@@ -233,6 +314,7 @@ class BashExecService:
|
|
|
233
314
|
"command": meta.get("command"),
|
|
234
315
|
"kind": meta.get("kind") or "exec",
|
|
235
316
|
"label": meta.get("label"),
|
|
317
|
+
"comment": meta.get("comment"),
|
|
236
318
|
"workdir": meta.get("workdir"),
|
|
237
319
|
"status": _coerce_session_status(meta.get("status")),
|
|
238
320
|
"exit_code": meta.get("exit_code"),
|
|
@@ -241,6 +323,8 @@ class BashExecService:
|
|
|
241
323
|
"finished_at": meta.get("finished_at"),
|
|
242
324
|
"updated_at": meta.get("updated_at"),
|
|
243
325
|
"last_progress": meta.get("last_progress"),
|
|
326
|
+
"last_output_at": meta.get("last_output_at"),
|
|
327
|
+
"last_output_seq": meta.get("last_output_seq"),
|
|
244
328
|
}
|
|
245
329
|
|
|
246
330
|
@staticmethod
|
|
@@ -469,6 +553,7 @@ class BashExecService:
|
|
|
469
553
|
*,
|
|
470
554
|
limit: int = DEFAULT_LOG_TAIL_LIMIT,
|
|
471
555
|
before_seq: int | None = None,
|
|
556
|
+
after_seq: int | None = None,
|
|
472
557
|
order: str = "asc",
|
|
473
558
|
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
|
474
559
|
if not self.meta_path(quest_root, bash_id).exists():
|
|
@@ -487,10 +572,13 @@ class BashExecService:
|
|
|
487
572
|
else:
|
|
488
573
|
time.sleep(0.03)
|
|
489
574
|
entries = read_jsonl(self.log_path(quest_root, bash_id))
|
|
575
|
+
latest_seq = int(entries[-1].get("seq") or 0) if entries else 0
|
|
490
576
|
normalized_before = before_seq if isinstance(before_seq, int) and before_seq > 0 else None
|
|
577
|
+
normalized_after = after_seq if isinstance(after_seq, int) and after_seq >= 0 else None
|
|
578
|
+
if normalized_after is not None:
|
|
579
|
+
entries = [entry for entry in entries if int(entry.get("seq") or 0) > normalized_after]
|
|
491
580
|
if normalized_before is not None:
|
|
492
581
|
entries = [entry for entry in entries if int(entry.get("seq") or 0) < normalized_before]
|
|
493
|
-
latest_seq = int(entries[-1].get("seq") or 0) if entries else 0
|
|
494
582
|
normalized_limit = max(1, limit)
|
|
495
583
|
truncated = len(entries) > normalized_limit
|
|
496
584
|
selected = entries[-normalized_limit:]
|
|
@@ -501,6 +589,8 @@ class BashExecService:
|
|
|
501
589
|
"tail_limit": normalized_limit,
|
|
502
590
|
"tail_start_seq": tail_start_seq if truncated else tail_start_seq,
|
|
503
591
|
"latest_seq": latest_seq or None,
|
|
592
|
+
"after_seq": normalized_after,
|
|
593
|
+
"before_seq": normalized_before,
|
|
504
594
|
}
|
|
505
595
|
return selected, meta
|
|
506
596
|
|
|
@@ -521,7 +611,15 @@ class BashExecService:
|
|
|
521
611
|
return session
|
|
522
612
|
time.sleep(max(0.1, poll_interval))
|
|
523
613
|
|
|
524
|
-
def request_stop(
|
|
614
|
+
def request_stop(
|
|
615
|
+
self,
|
|
616
|
+
quest_root: Path,
|
|
617
|
+
bash_id: str,
|
|
618
|
+
*,
|
|
619
|
+
reason: str | None = None,
|
|
620
|
+
user_id: str | None = None,
|
|
621
|
+
force: bool = False,
|
|
622
|
+
) -> dict[str, Any]:
|
|
525
623
|
session = self.get_session(quest_root, bash_id)
|
|
526
624
|
status = _normalize_string(session.get("status")).lower()
|
|
527
625
|
if status in TERMINAL_STATUSES:
|
|
@@ -530,6 +628,7 @@ class BashExecService:
|
|
|
530
628
|
"reason": _normalize_string(reason) or "user_stop",
|
|
531
629
|
"user_id": _normalize_string(user_id) or _normalize_string(session.get("agent_id")) or "agent",
|
|
532
630
|
"requested_at": utc_now(),
|
|
631
|
+
"force": bool(force),
|
|
533
632
|
}
|
|
534
633
|
_atomic_write_json(self.stop_request_path(quest_root, bash_id), request_payload)
|
|
535
634
|
meta = read_json(self.meta_path(quest_root, bash_id), {})
|
|
@@ -540,12 +639,18 @@ class BashExecService:
|
|
|
540
639
|
self._write_meta(quest_root, bash_id, meta)
|
|
541
640
|
runtime = self._terminal_runtime_manager.get_runtime(quest_root, bash_id)
|
|
542
641
|
if runtime is not None:
|
|
543
|
-
runtime.stop(reason=request_payload["reason"], force=
|
|
642
|
+
runtime.stop(reason=request_payload["reason"], force=bool(force))
|
|
544
643
|
else:
|
|
545
644
|
process_group_id = meta.get("process_group_id")
|
|
645
|
+
process_pid = meta.get("process_pid")
|
|
546
646
|
if isinstance(process_group_id, int) and process_group_id > 0:
|
|
547
647
|
try:
|
|
548
|
-
os.killpg(process_group_id, signal.SIGTERM)
|
|
648
|
+
os.killpg(process_group_id, signal.SIGKILL if force else signal.SIGTERM)
|
|
649
|
+
except ProcessLookupError:
|
|
650
|
+
pass
|
|
651
|
+
elif isinstance(process_pid, int) and process_pid > 0:
|
|
652
|
+
try:
|
|
653
|
+
os.kill(process_pid, signal.SIGKILL if force else signal.SIGTERM)
|
|
549
654
|
except ProcessLookupError:
|
|
550
655
|
pass
|
|
551
656
|
return self._session_payload(quest_root, meta)
|
|
@@ -561,6 +666,7 @@ class BashExecService:
|
|
|
561
666
|
workdir_display: str,
|
|
562
667
|
timeout_seconds: int | None,
|
|
563
668
|
env_keys: list[str],
|
|
669
|
+
comment: str | dict[str, Any] | None = None,
|
|
564
670
|
kind: str = "exec",
|
|
565
671
|
) -> dict[str, Any]:
|
|
566
672
|
quest_root = context.require_quest_root().resolve()
|
|
@@ -582,6 +688,7 @@ class BashExecService:
|
|
|
582
688
|
"agent_instance_id": agent_instance_id,
|
|
583
689
|
"started_by_user_id": started_by_user_id,
|
|
584
690
|
"stopped_by_user_id": None,
|
|
691
|
+
"comment": comment,
|
|
585
692
|
"command": command,
|
|
586
693
|
"workdir": workdir_display,
|
|
587
694
|
"cwd": str(cwd),
|
|
@@ -592,6 +699,8 @@ class BashExecService:
|
|
|
592
699
|
"exit_code": None,
|
|
593
700
|
"stop_reason": None,
|
|
594
701
|
"last_progress": None,
|
|
702
|
+
"last_output_at": None,
|
|
703
|
+
"last_output_seq": None,
|
|
595
704
|
"started_at": timestamp,
|
|
596
705
|
"finished_at": None,
|
|
597
706
|
"updated_at": timestamp,
|
|
@@ -638,6 +747,7 @@ class BashExecService:
|
|
|
638
747
|
workdir: str | None = None,
|
|
639
748
|
env: dict[str, Any] | None = None,
|
|
640
749
|
timeout_seconds: int | None = None,
|
|
750
|
+
comment: str | dict[str, Any] | None = None,
|
|
641
751
|
) -> dict[str, Any]:
|
|
642
752
|
if not _normalize_string(command):
|
|
643
753
|
raise ValueError("command_required")
|
|
@@ -656,6 +766,7 @@ class BashExecService:
|
|
|
656
766
|
workdir_display=workdir_display,
|
|
657
767
|
timeout_seconds=timeout_seconds,
|
|
658
768
|
env_keys=sorted(env_payload),
|
|
769
|
+
comment=comment,
|
|
659
770
|
kind="exec",
|
|
660
771
|
)
|
|
661
772
|
self.terminal_log_path(quest_root, bash_id).touch()
|
|
@@ -668,6 +779,7 @@ class BashExecService:
|
|
|
668
779
|
"bash_id": bash_id,
|
|
669
780
|
"quest_id": meta["quest_id"],
|
|
670
781
|
"command": command,
|
|
782
|
+
"comment": comment,
|
|
671
783
|
"workdir": workdir_display,
|
|
672
784
|
"mode": mode,
|
|
673
785
|
"started_at": meta["started_at"],
|
|
@@ -725,6 +837,8 @@ class BashExecService:
|
|
|
725
837
|
"exit_code": None,
|
|
726
838
|
"stop_reason": None,
|
|
727
839
|
"last_progress": None,
|
|
840
|
+
"last_output_at": None,
|
|
841
|
+
"last_output_seq": None,
|
|
728
842
|
"started_at": timestamp,
|
|
729
843
|
"finished_at": None,
|
|
730
844
|
"updated_at": timestamp,
|
|
@@ -1054,6 +1168,9 @@ class BashExecService:
|
|
|
1054
1168
|
"bash_id": session["bash_id"],
|
|
1055
1169
|
"log_path": session.get("log_path"),
|
|
1056
1170
|
"status": session.get("status"),
|
|
1171
|
+
"kind": session.get("kind"),
|
|
1172
|
+
"comment": session.get("comment"),
|
|
1173
|
+
"label": session.get("label"),
|
|
1057
1174
|
"command": session.get("command"),
|
|
1058
1175
|
"workdir": session.get("workdir"),
|
|
1059
1176
|
"started_at": session.get("started_at"),
|
|
@@ -1061,6 +1178,17 @@ class BashExecService:
|
|
|
1061
1178
|
"exit_code": session.get("exit_code"),
|
|
1062
1179
|
"stop_reason": session.get("stop_reason"),
|
|
1063
1180
|
"last_progress": session.get("last_progress"),
|
|
1181
|
+
"last_progress_at": session.get("last_progress_at"),
|
|
1182
|
+
"last_output_at": session.get("last_output_at"),
|
|
1183
|
+
"last_output_seq": session.get("last_output_seq"),
|
|
1184
|
+
"run_age_seconds": session.get("run_age_seconds"),
|
|
1185
|
+
"status_age_seconds": session.get("status_age_seconds"),
|
|
1186
|
+
"silent_seconds": session.get("silent_seconds"),
|
|
1187
|
+
"progress_age_seconds": session.get("progress_age_seconds"),
|
|
1188
|
+
"latest_signal_at": session.get("latest_signal_at"),
|
|
1189
|
+
"signal_age_seconds": session.get("signal_age_seconds"),
|
|
1190
|
+
"watchdog_after_seconds": session.get("watchdog_after_seconds"),
|
|
1191
|
+
"watchdog_overdue": session.get("watchdog_overdue"),
|
|
1064
1192
|
}
|
|
1065
1193
|
if include_log:
|
|
1066
1194
|
result["log"] = self.read_terminal_log(quest_root, str(session["bash_id"]))
|
|
@@ -10,6 +10,8 @@ from urllib.error import URLError
|
|
|
10
10
|
from urllib.parse import parse_qs
|
|
11
11
|
from urllib.request import Request, urlopen
|
|
12
12
|
|
|
13
|
+
from ..connector_runtime import parse_conversation_id
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
@dataclass
|
|
15
17
|
class BridgeWebhookResult:
|
|
@@ -52,19 +54,6 @@ class BaseConnectorBridge:
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
def deliver(self, payload: dict[str, Any], config: dict[str, Any]) -> dict[str, Any] | None:
|
|
55
|
-
relay_url = str(config.get("relay_url") or "").strip()
|
|
56
|
-
if relay_url:
|
|
57
|
-
envelope = {
|
|
58
|
-
"bridge_version": "deepscientist-connector-bridge/v1",
|
|
59
|
-
"connector": self.name,
|
|
60
|
-
"payload": self.format_outbound(payload, config),
|
|
61
|
-
"normalized_payload": payload,
|
|
62
|
-
}
|
|
63
|
-
headers = {"Content-Type": "application/json; charset=utf-8"}
|
|
64
|
-
token = str(config.get("relay_auth_token") or "").strip()
|
|
65
|
-
if token:
|
|
66
|
-
headers["Authorization"] = f"Bearer {token}"
|
|
67
|
-
return self._post_json(relay_url, envelope, headers=headers)
|
|
68
57
|
return self.deliver_direct(payload, config)
|
|
69
58
|
|
|
70
59
|
def deliver_direct(self, payload: dict[str, Any], config: dict[str, Any]) -> dict[str, Any] | None:
|
|
@@ -72,18 +61,20 @@ class BaseConnectorBridge:
|
|
|
72
61
|
|
|
73
62
|
@staticmethod
|
|
74
63
|
def extract_target(conversation_id: Any) -> dict[str, str]:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if len(parts) == 3:
|
|
64
|
+
parsed = parse_conversation_id(conversation_id)
|
|
65
|
+
if parsed is not None:
|
|
78
66
|
return {
|
|
79
|
-
"connector":
|
|
80
|
-
"chat_type":
|
|
81
|
-
"chat_id":
|
|
67
|
+
"connector": str(parsed.get("connector") or ""),
|
|
68
|
+
"chat_type": str(parsed.get("chat_type") or ""),
|
|
69
|
+
"chat_id": str(parsed.get("chat_id") or ""),
|
|
70
|
+
"profile_id": str(parsed.get("profile_id") or ""),
|
|
82
71
|
}
|
|
72
|
+
raw = str(conversation_id or "").strip()
|
|
83
73
|
return {
|
|
84
74
|
"connector": "",
|
|
85
75
|
"chat_type": "",
|
|
86
76
|
"chat_id": raw,
|
|
77
|
+
"profile_id": "",
|
|
87
78
|
}
|
|
88
79
|
|
|
89
80
|
@staticmethod
|
|
@@ -10,6 +10,7 @@ from urllib.request import Request, urlopen
|
|
|
10
10
|
from websockets.exceptions import ConnectionClosed
|
|
11
11
|
from websockets.sync.client import connect as websocket_connect
|
|
12
12
|
|
|
13
|
+
from ..connector_runtime import format_conversation_id
|
|
13
14
|
from ..shared import read_json, utc_now, write_json
|
|
14
15
|
|
|
15
16
|
|
|
@@ -24,11 +25,17 @@ class DiscordGatewayService:
|
|
|
24
25
|
config: dict[str, Any],
|
|
25
26
|
on_event: Callable[[dict[str, Any]], None],
|
|
26
27
|
log: Callable[[str, str], None] | None = None,
|
|
28
|
+
profile_id: str | None = None,
|
|
29
|
+
profile_label: str | None = None,
|
|
30
|
+
encode_profile_id: bool = False,
|
|
27
31
|
) -> None:
|
|
28
32
|
self.home = home
|
|
29
33
|
self.config = config
|
|
30
34
|
self.on_event = on_event
|
|
31
35
|
self.log = log or self._default_log
|
|
36
|
+
self.profile_id = str(profile_id or "").strip() or None
|
|
37
|
+
self.profile_label = str(profile_label or "").strip() or None
|
|
38
|
+
self._encode_profile_id = bool(encode_profile_id and self.profile_id)
|
|
32
39
|
self._thread: threading.Thread | None = None
|
|
33
40
|
self._stop_event = threading.Event()
|
|
34
41
|
self._heartbeat_stop = threading.Event()
|
|
@@ -37,6 +44,8 @@ class DiscordGatewayService:
|
|
|
37
44
|
self._seq: int | None = None
|
|
38
45
|
self._bot_user_id: str | None = None
|
|
39
46
|
self._root = home / "logs" / "connectors" / "discord"
|
|
47
|
+
if self.profile_id:
|
|
48
|
+
self._root = self._root / "profiles" / self.profile_id
|
|
40
49
|
self._runtime_path = self._root / "runtime.json"
|
|
41
50
|
|
|
42
51
|
def start(self) -> bool:
|
|
@@ -71,7 +80,7 @@ class DiscordGatewayService:
|
|
|
71
80
|
self._thread = threading.Thread(
|
|
72
81
|
target=self._run,
|
|
73
82
|
daemon=True,
|
|
74
|
-
name="deepscientist-discord-gateway",
|
|
83
|
+
name=f"deepscientist-discord-gateway-{self.profile_id or 'default'}",
|
|
75
84
|
)
|
|
76
85
|
self._thread.start()
|
|
77
86
|
return True
|
|
@@ -288,12 +297,22 @@ class DiscordGatewayService:
|
|
|
288
297
|
"sender_id": sender_id,
|
|
289
298
|
"sender_name": str(author.get("global_name") or author.get("username") or sender_id).strip(),
|
|
290
299
|
"message_id": str(data.get("id") or "").strip(),
|
|
291
|
-
"conversation_id":
|
|
300
|
+
"conversation_id": self._conversation_id(chat_type, channel_id),
|
|
301
|
+
"profile_id": self.profile_id,
|
|
302
|
+
"profile_label": self.profile_label,
|
|
292
303
|
"text": normalized_text,
|
|
293
304
|
"mentioned": mentioned,
|
|
294
305
|
"raw_event": data,
|
|
295
306
|
}
|
|
296
307
|
|
|
308
|
+
def _conversation_id(self, chat_type: str, chat_id: str) -> str:
|
|
309
|
+
return format_conversation_id(
|
|
310
|
+
"discord",
|
|
311
|
+
chat_type,
|
|
312
|
+
chat_id,
|
|
313
|
+
profile_id=self.profile_id if self._encode_profile_id else None,
|
|
314
|
+
)
|
|
315
|
+
|
|
297
316
|
@staticmethod
|
|
298
317
|
def _strip_bot_mention(text: str, bot_user_id: str) -> str:
|
|
299
318
|
cleaned = str(text or "").strip()
|
|
@@ -354,6 +373,10 @@ class DiscordGatewayService:
|
|
|
354
373
|
state = read_json(self._runtime_path, {}) or {}
|
|
355
374
|
if not isinstance(state, dict):
|
|
356
375
|
state = {}
|
|
376
|
+
if self.profile_id:
|
|
377
|
+
state["profile_id"] = self.profile_id
|
|
378
|
+
if self.profile_label:
|
|
379
|
+
state["profile_label"] = self.profile_label
|
|
357
380
|
state.update(patch)
|
|
358
381
|
write_json(self._runtime_path, state)
|
|
359
382
|
|
|
@@ -9,6 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
from typing import Any, Callable
|
|
10
10
|
|
|
11
11
|
from ..bridges.connectors import FeishuConnectorBridge
|
|
12
|
+
from ..connector_runtime import format_conversation_id, parse_conversation_id
|
|
12
13
|
from ..shared import read_json, utc_now, write_json
|
|
13
14
|
|
|
14
15
|
|
|
@@ -29,17 +30,25 @@ class FeishuLongConnectionService:
|
|
|
29
30
|
config: dict[str, Any],
|
|
30
31
|
on_event: Callable[[dict[str, Any]], None],
|
|
31
32
|
log: Callable[[str, str], None] | None = None,
|
|
33
|
+
profile_id: str | None = None,
|
|
34
|
+
profile_label: str | None = None,
|
|
35
|
+
encode_profile_id: bool = False,
|
|
32
36
|
) -> None:
|
|
33
37
|
self.home = home
|
|
34
38
|
self.config = config
|
|
35
39
|
self.on_event = on_event
|
|
36
40
|
self.log = log or self._default_log
|
|
41
|
+
self.profile_id = str(profile_id or "").strip() or None
|
|
42
|
+
self.profile_label = str(profile_label or "").strip() or None
|
|
43
|
+
self._encode_profile_id = bool(encode_profile_id and self.profile_id)
|
|
37
44
|
self._thread: threading.Thread | None = None
|
|
38
45
|
self._stop_event = threading.Event()
|
|
39
46
|
self._loop: asyncio.AbstractEventLoop | None = None
|
|
40
47
|
self._async_stop: asyncio.Event | None = None
|
|
41
48
|
self._client: Any = None
|
|
42
49
|
self._root = home / "logs" / "connectors" / "feishu"
|
|
50
|
+
if self.profile_id:
|
|
51
|
+
self._root = self._root / "profiles" / self.profile_id
|
|
43
52
|
self._runtime_path = self._root / "runtime.json"
|
|
44
53
|
|
|
45
54
|
def start(self) -> bool:
|
|
@@ -85,7 +94,7 @@ class FeishuLongConnectionService:
|
|
|
85
94
|
self._thread = threading.Thread(
|
|
86
95
|
target=self._run,
|
|
87
96
|
daemon=True,
|
|
88
|
-
name="deepscientist-feishu-long-connection",
|
|
97
|
+
name=f"deepscientist-feishu-long-connection-{self.profile_id or 'default'}",
|
|
89
98
|
)
|
|
90
99
|
self._thread.start()
|
|
91
100
|
return True
|
|
@@ -201,16 +210,41 @@ class FeishuLongConnectionService:
|
|
|
201
210
|
config=self.config,
|
|
202
211
|
)
|
|
203
212
|
for event in result.events:
|
|
204
|
-
self.
|
|
213
|
+
normalized = self._normalize_event(event)
|
|
214
|
+
self.on_event(normalized)
|
|
205
215
|
self._write_state(
|
|
206
216
|
connected=True,
|
|
207
217
|
connection_state="connected",
|
|
208
218
|
auth_state="ready",
|
|
209
219
|
last_event_at=utc_now(),
|
|
210
|
-
last_conversation_id=
|
|
220
|
+
last_conversation_id=normalized.get("conversation_id"),
|
|
211
221
|
updated_at=utc_now(),
|
|
212
222
|
)
|
|
213
223
|
|
|
224
|
+
def _normalize_event(self, event: dict[str, Any]) -> dict[str, Any]:
|
|
225
|
+
normalized = dict(event)
|
|
226
|
+
chat_type = str(normalized.get("chat_type") or "direct").strip().lower() or "direct"
|
|
227
|
+
group_id = str(normalized.get("group_id") or "").strip()
|
|
228
|
+
direct_id = str(normalized.get("direct_id") or "").strip()
|
|
229
|
+
chat_id = group_id if chat_type == "group" else direct_id
|
|
230
|
+
if not chat_id:
|
|
231
|
+
parsed = parse_conversation_id(normalized.get("conversation_id"))
|
|
232
|
+
if parsed is not None:
|
|
233
|
+
chat_type = str(parsed.get("chat_type") or chat_type).strip().lower() or chat_type
|
|
234
|
+
chat_id = str(parsed.get("chat_id") or "").strip()
|
|
235
|
+
normalized["conversation_id"] = self._conversation_id(chat_type, chat_id or "unknown")
|
|
236
|
+
normalized["profile_id"] = self.profile_id
|
|
237
|
+
normalized["profile_label"] = self.profile_label
|
|
238
|
+
return normalized
|
|
239
|
+
|
|
240
|
+
def _conversation_id(self, chat_type: str, chat_id: str) -> str:
|
|
241
|
+
return format_conversation_id(
|
|
242
|
+
"feishu",
|
|
243
|
+
chat_type,
|
|
244
|
+
chat_id,
|
|
245
|
+
profile_id=self.profile_id if self._encode_profile_id else None,
|
|
246
|
+
)
|
|
247
|
+
|
|
214
248
|
@staticmethod
|
|
215
249
|
def _sdk_bundle() -> dict[str, Any] | None:
|
|
216
250
|
try:
|
|
@@ -239,6 +273,10 @@ class FeishuLongConnectionService:
|
|
|
239
273
|
state = read_json(self._runtime_path, {}) or {}
|
|
240
274
|
if not isinstance(state, dict):
|
|
241
275
|
state = {}
|
|
276
|
+
if self.profile_id:
|
|
277
|
+
state["profile_id"] = self.profile_id
|
|
278
|
+
if self.profile_label:
|
|
279
|
+
state["profile_label"] = self.profile_label
|
|
242
280
|
state.update(patch)
|
|
243
281
|
write_json(self._runtime_path, state)
|
|
244
282
|
|