@researai/deepscientist 1.5.6 → 1.5.8
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 +32 -0
- package/bin/ds.js +274 -18
- package/docs/en/07_MEMORY_AND_MCP.md +40 -3
- package/docs/en/99_ACKNOWLEDGEMENTS.md +1 -0
- package/docs/zh/07_MEMORY_AND_MCP.md +40 -3
- package/docs/zh/99_ACKNOWLEDGEMENTS.md +1 -0
- package/install.sh +34 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/acp/envelope.py +1 -0
- package/src/deepscientist/artifact/metrics.py +813 -80
- package/src/deepscientist/artifact/schemas.py +1 -0
- package/src/deepscientist/artifact/service.py +1101 -99
- package/src/deepscientist/bash_exec/monitor.py +1 -1
- package/src/deepscientist/bash_exec/service.py +17 -9
- package/src/deepscientist/channels/qq.py +17 -0
- package/src/deepscientist/channels/relay.py +16 -0
- package/src/deepscientist/cli.py +1 -1
- package/src/deepscientist/config/models.py +12 -6
- package/src/deepscientist/config/service.py +75 -2
- package/src/deepscientist/connector_profiles.py +34 -6
- package/src/deepscientist/daemon/api/handlers.py +290 -15
- package/src/deepscientist/daemon/api/router.py +2 -0
- package/src/deepscientist/daemon/app.py +521 -23
- package/src/deepscientist/gitops/diff.py +6 -10
- package/src/deepscientist/mcp/server.py +188 -39
- package/src/deepscientist/prompts/builder.py +71 -22
- package/src/deepscientist/qq_profiles.py +19 -9
- package/src/deepscientist/quest/layout.py +1 -0
- package/src/deepscientist/quest/service.py +83 -34
- package/src/deepscientist/quest/stage_views.py +74 -29
- package/src/deepscientist/runners/codex.py +32 -14
- package/src/deepscientist/runners/runtime_overrides.py +46 -0
- package/src/deepscientist/skills/installer.py +7 -0
- package/src/prompts/connectors/qq.md +1 -1
- package/src/prompts/contracts/shared_interaction.md +14 -0
- package/src/prompts/system.md +134 -30
- package/src/skills/analysis-campaign/SKILL.md +34 -8
- package/src/skills/analysis-campaign/references/campaign-checklist-template.md +41 -0
- package/src/skills/analysis-campaign/references/campaign-plan-template.md +68 -0
- package/src/skills/baseline/SKILL.md +145 -32
- package/src/skills/baseline/references/baseline-checklist-template.md +57 -0
- package/src/skills/baseline/references/baseline-plan-template.md +105 -0
- package/src/skills/decision/SKILL.md +12 -8
- package/src/skills/experiment/SKILL.md +51 -9
- package/src/skills/experiment/references/main-experiment-checklist-template.md +52 -0
- package/src/skills/experiment/references/main-experiment-plan-template.md +79 -0
- package/src/skills/figure-polish/SKILL.md +1 -0
- package/src/skills/finalize/SKILL.md +3 -8
- package/src/skills/idea/SKILL.md +2 -8
- package/src/skills/intake-audit/SKILL.md +2 -8
- package/src/skills/rebuttal/SKILL.md +2 -8
- package/src/skills/review/SKILL.md +2 -8
- package/src/skills/scout/SKILL.md +2 -8
- package/src/skills/write/SKILL.md +52 -16
- package/src/skills/write/templates/DEEPSCIENTIST_NOTES.md +21 -0
- package/src/skills/write/templates/README.md +408 -0
- package/src/skills/write/templates/UPSTREAM_LICENSE.txt +21 -0
- package/src/skills/write/templates/aaai2026/README.md +534 -0
- package/src/skills/write/templates/aaai2026/aaai2026-unified-supp.tex +144 -0
- package/src/skills/write/templates/aaai2026/aaai2026-unified-template.tex +952 -0
- package/src/skills/write/templates/aaai2026/aaai2026.bib +111 -0
- package/src/skills/write/templates/aaai2026/aaai2026.bst +1493 -0
- package/src/skills/write/templates/aaai2026/aaai2026.sty +315 -0
- package/src/skills/write/templates/acl/README.md +50 -0
- package/src/skills/write/templates/acl/acl.sty +312 -0
- package/src/skills/write/templates/acl/acl_latex.tex +377 -0
- package/src/skills/write/templates/acl/acl_lualatex.tex +101 -0
- package/src/skills/write/templates/acl/acl_natbib.bst +1940 -0
- package/src/skills/write/templates/acl/anthology.bib.txt +26 -0
- package/src/skills/write/templates/acl/custom.bib +70 -0
- package/src/skills/write/templates/acl/formatting.md +326 -0
- package/src/skills/write/templates/asplos2027/main.tex +459 -0
- package/src/skills/write/templates/asplos2027/references.bib +135 -0
- package/src/skills/write/templates/colm2025/README.md +3 -0
- package/src/skills/write/templates/colm2025/colm2025_conference.bib +11 -0
- package/src/skills/write/templates/colm2025/colm2025_conference.bst +1440 -0
- package/src/skills/write/templates/colm2025/colm2025_conference.sty +218 -0
- package/src/skills/write/templates/colm2025/colm2025_conference.tex +305 -0
- package/src/skills/write/templates/colm2025/fancyhdr.sty +485 -0
- package/src/skills/write/templates/colm2025/math_commands.tex +508 -0
- package/src/skills/write/templates/colm2025/natbib.sty +1246 -0
- package/src/skills/write/templates/iclr2026/fancyhdr.sty +485 -0
- package/src/skills/write/templates/iclr2026/iclr2026_conference.bib +24 -0
- package/src/skills/write/templates/iclr2026/iclr2026_conference.bst +1440 -0
- package/src/skills/write/templates/iclr2026/iclr2026_conference.sty +246 -0
- package/src/skills/write/templates/iclr2026/iclr2026_conference.tex +414 -0
- package/src/skills/write/templates/iclr2026/math_commands.tex +508 -0
- package/src/skills/write/templates/iclr2026/natbib.sty +1246 -0
- package/src/skills/write/templates/icml2026/algorithm.sty +79 -0
- package/src/skills/write/templates/icml2026/algorithmic.sty +201 -0
- package/src/skills/write/templates/icml2026/example_paper.bib +75 -0
- package/src/skills/write/templates/icml2026/example_paper.tex +662 -0
- package/src/skills/write/templates/icml2026/fancyhdr.sty +864 -0
- package/src/skills/write/templates/icml2026/icml2026.bst +1443 -0
- package/src/skills/write/templates/icml2026/icml2026.sty +767 -0
- package/src/skills/write/templates/neurips2025/Makefile +36 -0
- package/src/skills/write/templates/neurips2025/extra_pkgs.tex +53 -0
- package/src/skills/write/templates/neurips2025/main.tex +38 -0
- package/src/skills/write/templates/neurips2025/neurips.sty +382 -0
- package/src/skills/write/templates/nsdi2027/main.tex +426 -0
- package/src/skills/write/templates/nsdi2027/references.bib +151 -0
- package/src/skills/write/templates/nsdi2027/usenix-2020-09.sty +83 -0
- package/src/skills/write/templates/osdi2026/main.tex +429 -0
- package/src/skills/write/templates/osdi2026/references.bib +150 -0
- package/src/skills/write/templates/osdi2026/usenix-2020-09.sty +83 -0
- package/src/skills/write/templates/sosp2026/main.tex +532 -0
- package/src/skills/write/templates/sosp2026/references.bib +148 -0
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-BGLArZRn.js → AiManusChatView-m2FNtwbn.js} +110 -14
- package/src/ui/dist/assets/{AnalysisPlugin-BgDGSigG.js → AnalysisPlugin-BMTF8EGL.js} +1 -1
- package/src/ui/dist/assets/{AutoFigurePlugin-B65HD7L4.js → AutoFigurePlugin-DxPdMUNb.js} +5 -5
- package/src/ui/dist/assets/{CliPlugin-CUqgsFHC.js → CliPlugin-BEOWgxCI.js} +9 -9
- package/src/ui/dist/assets/{CodeEditorPlugin-CF5EdvaS.js → CodeEditorPlugin-BCXvjqmb.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-DEeU063D.js → CodeViewerPlugin-DaJcy3nD.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-Df-FuDlZ.js → DocViewerPlugin-ByfeIq4K.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-RAnNaRxM.js → GitDiffViewerPlugin-Cksf3VZ-.js} +830 -86
- package/src/ui/dist/assets/{ImageViewerPlugin-DXJ0ZJGg.js → ImageViewerPlugin-CFz-OsTS.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-BlO-sKsj.js → LabCopilotPanel-CJ1cJzoX.js} +10 -10
- package/src/ui/dist/assets/{LabPlugin-BajPZW5v.js → LabPlugin-BF3dVJwa.js} +1 -1
- package/src/ui/dist/assets/{LatexPlugin-F1OEol8D.js → LatexPlugin-DDkwZ6Sj.js} +7 -7
- package/src/ui/dist/assets/{MarkdownViewerPlugin-MhUupqwT.js → MarkdownViewerPlugin-HAuvurcT.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-DxhIEsv0.js → MarketplacePlugin-BtoTYy2C.js} +3 -3
- package/src/ui/dist/assets/{index-B-2scqCJ.js → NotebookEditor-CSJYx7b-.js} +12 -155
- package/src/ui/dist/assets/{NotebookEditor-q7TkhewC.js → NotebookEditor-DQgRezm_.js} +1 -1
- package/src/ui/dist/assets/{PdfLoader-B8ZOTKFc.js → PdfLoader-DPa_-fv6.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-xFPvzvWh.js → PdfMarkdownPlugin-BZpXOEjm.js} +3 -3
- package/src/ui/dist/assets/{PdfViewerPlugin-EjEcsIB8.js → PdfViewerPlugin-BT8a6wGR.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-ixY-1lgW.js → SearchPlugin-D_blveZi.js} +1 -1
- package/src/ui/dist/assets/{Stepper-gYFK2Pgz.js → Stepper-DH2k75Vo.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-Cym6pv_n.js → TextViewerPlugin-Btx0M3hX.js} +4 -4
- package/src/ui/dist/assets/{VNCViewer-BPmIHcmK.js → VNCViewer-DImJO4rO.js} +9 -9
- package/src/ui/dist/assets/{bibtex-Btv6Wi7f.js → bibtex-B-Hqu0Sg.js} +1 -1
- package/src/ui/dist/assets/{code-BlG7g85c.js → code-BUfXGJSl.js} +1 -1
- package/src/ui/dist/assets/{file-content-DBT5OfTZ.js → file-content-VqamwI3X.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-BWXYzqHk.js → file-diff-panel-C_wOoS7a.js} +1 -1
- package/src/ui/dist/assets/{file-socket-wDlx6byM.js → file-socket-D2bTuMVP.js} +1 -1
- package/src/ui/dist/assets/{file-utils-Ba3nJmH0.js → file-utils--zJCPN1i.js} +1 -1
- package/src/ui/dist/assets/{image-BwtCyguk.js → image-BZkGJ4mM.js} +1 -1
- package/src/ui/dist/assets/{index-CfRpE209.js → index-CxkvSeKw.js} +2 -2
- package/src/ui/dist/assets/{index-DcqvKzeJ.js → index-D9QIGcmc.js} +1 -1
- package/src/ui/dist/assets/{index-DpMZw8aM.css → index-DXZ1daiJ.css} +163 -34
- package/src/ui/dist/assets/index-DdRW6RMJ.js +159 -0
- package/src/ui/dist/assets/{index-Bz5AaWL7.js → index-DjggJovS.js} +2948 -1565
- package/src/ui/dist/assets/{message-square-BnlyWVH0.js → message-square-FUIPIhU2.js} +1 -1
- package/src/ui/dist/assets/{monaco-CXe0pAVe.js → monaco-DHMc7kKM.js} +1 -1
- package/src/ui/dist/assets/{popover-BCHmVhHj.js → popover-B85oCgCS.js} +1 -1
- package/src/ui/dist/assets/{project-sync-Brk6kaOD.js → project-sync-DOMCcPac.js} +1 -1
- package/src/ui/dist/assets/{sigma-D72eSUep.js → sigma-BO2rQrl3.js} +1 -1
- package/src/ui/dist/assets/{tooltip-BMWd0dqX.js → tooltip-B1OspAkx.js} +1 -1
- package/src/ui/dist/assets/{trash-BIt_eWIS.js → trash-BsVEH_dV.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-N1hkTRrR.js → useCliAccess-b8L6JuZm.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-DPRPv6rv.js → useFileDiffOverlay-BY7uA9hV.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-E5-UheyP.js → wrap-text-BwyVuUIK.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-D4TR-ZZ_.js → zoom-out-RDpLugQP.js} +1 -1
- package/src/ui/dist/index.html +5 -2
- /package/src/ui/dist/assets/{index-CccQYZjX.css → NotebookEditor-CccQYZjX.css} +0 -0
|
@@ -29,16 +29,24 @@ from ..channels.slack_socket import SlackSocketModeService
|
|
|
29
29
|
from ..channels.telegram_polling import TelegramPollingService
|
|
30
30
|
from ..channels.whatsapp_local_session import WhatsAppLocalSessionService
|
|
31
31
|
from ..cloud import CloudLinkService
|
|
32
|
-
from ..connector_profiles import
|
|
32
|
+
from ..connector_profiles import (
|
|
33
|
+
CONNECTOR_PROFILE_SPECS,
|
|
34
|
+
PROFILEABLE_CONNECTOR_NAMES,
|
|
35
|
+
connector_profile_label,
|
|
36
|
+
list_connector_profiles,
|
|
37
|
+
merge_connector_profile_config,
|
|
38
|
+
normalize_connector_config,
|
|
39
|
+
)
|
|
33
40
|
from ..connector_runtime import conversation_identity_key, format_conversation_id, normalize_conversation_id, parse_conversation_id
|
|
34
41
|
from ..config import ConfigManager
|
|
42
|
+
from ..config.models import SYSTEM_CONNECTOR_NAMES
|
|
35
43
|
from ..home import repo_root
|
|
36
44
|
from ..memory import MemoryService
|
|
37
45
|
from ..network import urlopen_with_proxy as urlopen
|
|
38
46
|
from ..latex_runtime import QuestLatexService
|
|
39
47
|
from ..prompts import PromptBuilder
|
|
40
48
|
from ..prompts.builder import STANDARD_SKILLS
|
|
41
|
-
from ..qq_profiles import list_qq_profiles, merge_qq_profile_config
|
|
49
|
+
from ..qq_profiles import list_qq_profiles, merge_qq_profile_config, normalize_qq_connector_config
|
|
42
50
|
from ..quest import QuestService
|
|
43
51
|
from ..runners import CodexRunner, RunRequest, get_runner_factory, register_builtin_runners
|
|
44
52
|
from ..runtime_logs import JsonlLogger
|
|
@@ -73,7 +81,8 @@ class DaemonApp:
|
|
|
73
81
|
self.daemon_managed_by = str(os.environ.get("DS_DAEMON_MANAGED_BY") or "manual").strip() or "manual"
|
|
74
82
|
self.repo_root = repo_root()
|
|
75
83
|
self.config_manager = ConfigManager(home)
|
|
76
|
-
self.
|
|
84
|
+
self.runtime_config = self.config_manager.load_runtime_config()
|
|
85
|
+
self.runners_config = self.config_manager.load_runners_config()
|
|
77
86
|
self.connectors_config = self.config_manager.load_named_normalized("connectors")
|
|
78
87
|
self.skill_installer = SkillInstaller(self.repo_root, home)
|
|
79
88
|
self.quest_service = QuestService(home, skill_installer=self.skill_installer)
|
|
@@ -83,7 +92,7 @@ class DaemonApp:
|
|
|
83
92
|
self.bash_exec_service = BashExecService(home)
|
|
84
93
|
self.team_service = SingleTeamService(home)
|
|
85
94
|
self.cloud_service = CloudLinkService(home)
|
|
86
|
-
config = self.
|
|
95
|
+
config = self.runtime_config
|
|
87
96
|
skill_config = config.get("skills") if isinstance(config.get("skills"), dict) else {}
|
|
88
97
|
self.skill_sync_summary = self.skill_installer.ensure_release_sync(
|
|
89
98
|
installed_version=__version__,
|
|
@@ -138,9 +147,13 @@ class DaemonApp:
|
|
|
138
147
|
|
|
139
148
|
def list_connector_statuses(self) -> list[dict[str, object]]:
|
|
140
149
|
title_by_quest = self._quest_titles_by_id()
|
|
141
|
-
items = [
|
|
150
|
+
items = [
|
|
151
|
+
self._augment_connector_status(channel.status(), title_by_quest=title_by_quest)
|
|
152
|
+
for name, channel in self.channels.items()
|
|
153
|
+
if name == "local" or self._is_connector_system_enabled(name)
|
|
154
|
+
]
|
|
142
155
|
lingzhu_config = self.connectors_config.get("lingzhu")
|
|
143
|
-
if isinstance(lingzhu_config, dict):
|
|
156
|
+
if isinstance(lingzhu_config, dict) and self._is_connector_system_enabled("lingzhu"):
|
|
144
157
|
items.append(self._augment_connector_status(self.config_manager.lingzhu_snapshot(lingzhu_config), title_by_quest=title_by_quest))
|
|
145
158
|
return items
|
|
146
159
|
|
|
@@ -339,6 +352,8 @@ class DaemonApp:
|
|
|
339
352
|
continue
|
|
340
353
|
if connector_name not in self.channels:
|
|
341
354
|
continue
|
|
355
|
+
if not self._is_connector_system_enabled(connector_name):
|
|
356
|
+
continue
|
|
342
357
|
if parsed is not None and str(parsed.get("connector") or "").strip().lower() != connector_name:
|
|
343
358
|
continue
|
|
344
359
|
normalized_by_connector[connector_name] = {
|
|
@@ -452,19 +467,203 @@ class DaemonApp:
|
|
|
452
467
|
b"Terminal attach token is invalid or expired.",
|
|
453
468
|
)
|
|
454
469
|
if runtime is None:
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
470
|
+
try:
|
|
471
|
+
session = self.bash_exec_service.get_session(attach_token.quest_root, attach_token.bash_id)
|
|
472
|
+
except FileNotFoundError:
|
|
473
|
+
return Response(
|
|
474
|
+
404,
|
|
475
|
+
"Not Found",
|
|
476
|
+
Headers({"Content-Type": "text/plain; charset=utf-8"}),
|
|
477
|
+
b"Terminal session is no longer available.",
|
|
478
|
+
)
|
|
479
|
+
status = str(session.get("status") or "").strip().lower()
|
|
480
|
+
kind = str(session.get("kind") or "").strip().lower()
|
|
481
|
+
if status in {"completed", "failed", "terminated"} or kind not in {"exec"}:
|
|
482
|
+
return Response(
|
|
483
|
+
409,
|
|
484
|
+
"Conflict",
|
|
485
|
+
Headers({"Content-Type": "text/plain; charset=utf-8"}),
|
|
486
|
+
b"Terminal runtime is no longer active.",
|
|
487
|
+
)
|
|
461
488
|
setattr(connection, "_ds_terminal_attach_token", token)
|
|
462
489
|
return None
|
|
463
490
|
|
|
491
|
+
def _handle_logged_terminal_attach_connection(
|
|
492
|
+
self,
|
|
493
|
+
connection: ServerConnection,
|
|
494
|
+
*,
|
|
495
|
+
attach_token,
|
|
496
|
+
send_lock: threading.Lock,
|
|
497
|
+
) -> None:
|
|
498
|
+
session = self.bash_exec_service.get_session(attach_token.quest_root, attach_token.bash_id)
|
|
499
|
+
stop_event = threading.Event()
|
|
500
|
+
|
|
501
|
+
with send_lock:
|
|
502
|
+
connection.send(
|
|
503
|
+
json.dumps(
|
|
504
|
+
{
|
|
505
|
+
"type": "ready",
|
|
506
|
+
"bash_id": attach_token.bash_id,
|
|
507
|
+
"status": session.get("status"),
|
|
508
|
+
"cwd": session.get("cwd"),
|
|
509
|
+
"workdir": session.get("workdir"),
|
|
510
|
+
},
|
|
511
|
+
ensure_ascii=False,
|
|
512
|
+
)
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
def _encode_exec_log_entry(entry: dict[str, Any]) -> bytes:
|
|
516
|
+
line = str(entry.get("line") or "")
|
|
517
|
+
stream = str(entry.get("stream") or "").strip().lower()
|
|
518
|
+
if line.startswith("__DS_PROGRESS__") or line.startswith("__DS_BASH_STATUS__"):
|
|
519
|
+
return b""
|
|
520
|
+
if line.startswith("__DS_BASH_CR__"):
|
|
521
|
+
payload = line[len("__DS_BASH_CR__") :].lstrip()
|
|
522
|
+
return f"\r\x1b[K{payload}".encode("utf-8", errors="replace")
|
|
523
|
+
if stream in {"prompt", "partial"}:
|
|
524
|
+
return line.encode("utf-8", errors="replace")
|
|
525
|
+
if stream == "system" and not line.strip():
|
|
526
|
+
return b""
|
|
527
|
+
return f"{line}\n".encode("utf-8", errors="replace")
|
|
528
|
+
|
|
529
|
+
def _relay_output() -> None:
|
|
530
|
+
last_seq = 0
|
|
531
|
+
try:
|
|
532
|
+
entries, meta = self.bash_exec_service.read_log_entries(
|
|
533
|
+
attach_token.quest_root,
|
|
534
|
+
attach_token.bash_id,
|
|
535
|
+
limit=2000,
|
|
536
|
+
order="asc",
|
|
537
|
+
)
|
|
538
|
+
if isinstance(meta.get("latest_seq"), int):
|
|
539
|
+
last_seq = int(meta["latest_seq"])
|
|
540
|
+
elif entries:
|
|
541
|
+
last_seq = max(int(item.get("seq") or 0) for item in entries)
|
|
542
|
+
for entry in entries:
|
|
543
|
+
payload = _encode_exec_log_entry(entry)
|
|
544
|
+
if not payload:
|
|
545
|
+
continue
|
|
546
|
+
with send_lock:
|
|
547
|
+
connection.send(payload)
|
|
548
|
+
|
|
549
|
+
while not stop_event.is_set():
|
|
550
|
+
entries, _meta = self.bash_exec_service.read_log_entries(
|
|
551
|
+
attach_token.quest_root,
|
|
552
|
+
attach_token.bash_id,
|
|
553
|
+
limit=400,
|
|
554
|
+
after_seq=last_seq,
|
|
555
|
+
order="asc",
|
|
556
|
+
)
|
|
557
|
+
for entry in entries:
|
|
558
|
+
last_seq = max(last_seq, int(entry.get("seq") or 0))
|
|
559
|
+
payload = _encode_exec_log_entry(entry)
|
|
560
|
+
if not payload:
|
|
561
|
+
continue
|
|
562
|
+
with send_lock:
|
|
563
|
+
connection.send(payload)
|
|
564
|
+
current = self.bash_exec_service.get_session(
|
|
565
|
+
attach_token.quest_root,
|
|
566
|
+
attach_token.bash_id,
|
|
567
|
+
)
|
|
568
|
+
status = str(current.get("status") or "").strip().lower()
|
|
569
|
+
if status in {"completed", "failed", "terminated"}:
|
|
570
|
+
with send_lock:
|
|
571
|
+
connection.send(
|
|
572
|
+
json.dumps(
|
|
573
|
+
{
|
|
574
|
+
"type": "exit",
|
|
575
|
+
"bash_id": attach_token.bash_id,
|
|
576
|
+
"status": current.get("status"),
|
|
577
|
+
"exit_code": current.get("exit_code"),
|
|
578
|
+
"stop_reason": current.get("stop_reason"),
|
|
579
|
+
"finished_at": current.get("finished_at"),
|
|
580
|
+
},
|
|
581
|
+
ensure_ascii=False,
|
|
582
|
+
)
|
|
583
|
+
)
|
|
584
|
+
return
|
|
585
|
+
time.sleep(TERMINAL_STREAM_IDLE_SLEEP_SECONDS)
|
|
586
|
+
except Exception as exc:
|
|
587
|
+
if stop_event.is_set():
|
|
588
|
+
return
|
|
589
|
+
try:
|
|
590
|
+
with send_lock:
|
|
591
|
+
connection.send(
|
|
592
|
+
json.dumps({"type": "error", "message": str(exc)}, ensure_ascii=False)
|
|
593
|
+
)
|
|
594
|
+
except Exception:
|
|
595
|
+
pass
|
|
596
|
+
|
|
597
|
+
relay_thread = threading.Thread(
|
|
598
|
+
target=_relay_output,
|
|
599
|
+
daemon=True,
|
|
600
|
+
name=f"exec-attach-{attach_token.bash_id}",
|
|
601
|
+
)
|
|
602
|
+
relay_thread.start()
|
|
603
|
+
try:
|
|
604
|
+
while True:
|
|
605
|
+
try:
|
|
606
|
+
message = connection.recv()
|
|
607
|
+
except ConnectionClosed:
|
|
608
|
+
break
|
|
609
|
+
if message is None:
|
|
610
|
+
break
|
|
611
|
+
if isinstance(message, bytes):
|
|
612
|
+
self.bash_exec_service.append_terminal_input(
|
|
613
|
+
attach_token.quest_root,
|
|
614
|
+
attach_token.bash_id,
|
|
615
|
+
data=message.decode("utf-8", errors="replace"),
|
|
616
|
+
source="web-pty",
|
|
617
|
+
)
|
|
618
|
+
continue
|
|
619
|
+
try:
|
|
620
|
+
payload = json.loads(message)
|
|
621
|
+
except json.JSONDecodeError:
|
|
622
|
+
continue
|
|
623
|
+
if not isinstance(payload, dict):
|
|
624
|
+
continue
|
|
625
|
+
message_type = str(payload.get("type") or "").strip().lower()
|
|
626
|
+
if message_type == "input":
|
|
627
|
+
self.bash_exec_service.append_terminal_input(
|
|
628
|
+
attach_token.quest_root,
|
|
629
|
+
attach_token.bash_id,
|
|
630
|
+
data=str(payload.get("data") or ""),
|
|
631
|
+
source="web-pty",
|
|
632
|
+
)
|
|
633
|
+
continue
|
|
634
|
+
if message_type == "binary_input":
|
|
635
|
+
raw = str(payload.get("data") or "")
|
|
636
|
+
if raw:
|
|
637
|
+
self.bash_exec_service.append_terminal_input(
|
|
638
|
+
attach_token.quest_root,
|
|
639
|
+
attach_token.bash_id,
|
|
640
|
+
data=base64.b64decode(raw).decode("utf-8", errors="replace"),
|
|
641
|
+
source="web-pty",
|
|
642
|
+
)
|
|
643
|
+
continue
|
|
644
|
+
if message_type == "resize":
|
|
645
|
+
cols = int(payload.get("cols") or 0)
|
|
646
|
+
rows = int(payload.get("rows") or 0)
|
|
647
|
+
self.bash_exec_service.resize_terminal_session(
|
|
648
|
+
attach_token.quest_root,
|
|
649
|
+
attach_token.bash_id,
|
|
650
|
+
cols=cols,
|
|
651
|
+
rows=rows,
|
|
652
|
+
)
|
|
653
|
+
continue
|
|
654
|
+
if message_type == "detach":
|
|
655
|
+
break
|
|
656
|
+
if message_type == "ping":
|
|
657
|
+
with send_lock:
|
|
658
|
+
connection.send(json.dumps({"type": "pong"}, ensure_ascii=False))
|
|
659
|
+
finally:
|
|
660
|
+
stop_event.set()
|
|
661
|
+
relay_thread.join(timeout=1)
|
|
662
|
+
|
|
464
663
|
def _handle_terminal_attach_connection(self, connection: ServerConnection) -> None:
|
|
465
664
|
token_value = str(getattr(connection, "_ds_terminal_attach_token", "") or "").strip()
|
|
466
665
|
attach_token, runtime = self.bash_exec_service.consume_terminal_attach_token(token_value)
|
|
467
|
-
if attach_token is None
|
|
666
|
+
if attach_token is None:
|
|
468
667
|
try:
|
|
469
668
|
connection.close(code=1011, reason="terminal_attach_unavailable")
|
|
470
669
|
except Exception:
|
|
@@ -472,6 +671,31 @@ class DaemonApp:
|
|
|
472
671
|
return
|
|
473
672
|
|
|
474
673
|
send_lock = threading.Lock()
|
|
674
|
+
if runtime is None:
|
|
675
|
+
try:
|
|
676
|
+
self._handle_logged_terminal_attach_connection(
|
|
677
|
+
connection,
|
|
678
|
+
attach_token=attach_token,
|
|
679
|
+
send_lock=send_lock,
|
|
680
|
+
)
|
|
681
|
+
except Exception as exc:
|
|
682
|
+
try:
|
|
683
|
+
with send_lock:
|
|
684
|
+
connection.send(
|
|
685
|
+
json.dumps(
|
|
686
|
+
{"type": "error", "message": str(exc)},
|
|
687
|
+
ensure_ascii=False,
|
|
688
|
+
)
|
|
689
|
+
)
|
|
690
|
+
except Exception:
|
|
691
|
+
pass
|
|
692
|
+
finally:
|
|
693
|
+
try:
|
|
694
|
+
connection.close()
|
|
695
|
+
except Exception:
|
|
696
|
+
pass
|
|
697
|
+
return
|
|
698
|
+
|
|
475
699
|
client = TerminalClient(
|
|
476
700
|
client_id=generate_id("tclient"),
|
|
477
701
|
send_text=connection.send,
|
|
@@ -628,6 +852,43 @@ class DaemonApp:
|
|
|
628
852
|
factory = get_channel_factory(name)
|
|
629
853
|
return factory(home=self.home, app=self, config=self.connectors_config.get(name, {}))
|
|
630
854
|
|
|
855
|
+
def _system_enabled_connector_names(self) -> set[str]:
|
|
856
|
+
connectors = self.runtime_config.get("connectors") if isinstance(self.runtime_config.get("connectors"), dict) else {}
|
|
857
|
+
system_enabled = connectors.get("system_enabled") if isinstance(connectors.get("system_enabled"), dict) else {}
|
|
858
|
+
return {
|
|
859
|
+
name
|
|
860
|
+
for name in SYSTEM_CONNECTOR_NAMES
|
|
861
|
+
if bool(system_enabled.get(name, name == "qq"))
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
def _is_connector_system_enabled(self, connector_name: str) -> bool:
|
|
865
|
+
normalized = str(connector_name or "").strip().lower()
|
|
866
|
+
if normalized == "local":
|
|
867
|
+
return True
|
|
868
|
+
enabled = self._system_enabled_connector_names()
|
|
869
|
+
if normalized in enabled:
|
|
870
|
+
return True
|
|
871
|
+
if normalized in SYSTEM_CONNECTOR_NAMES:
|
|
872
|
+
return False
|
|
873
|
+
return True
|
|
874
|
+
|
|
875
|
+
def reload_runtime_config(self, *, restart_background: bool = True) -> dict[str, object]:
|
|
876
|
+
previous_enabled = self._system_enabled_connector_names()
|
|
877
|
+
self.runtime_config = self.config_manager.load_runtime_config()
|
|
878
|
+
logging_config = self.runtime_config.get("logging") if isinstance(self.runtime_config.get("logging"), dict) else {}
|
|
879
|
+
self.logger.level = str(logging_config.get("level") or "info").strip().lower() or "info"
|
|
880
|
+
enabled = self._system_enabled_connector_names()
|
|
881
|
+
restarted = False
|
|
882
|
+
if restart_background and self._server is not None and enabled != previous_enabled:
|
|
883
|
+
self._stop_background_connectors()
|
|
884
|
+
self._start_background_connectors()
|
|
885
|
+
restarted = True
|
|
886
|
+
return {
|
|
887
|
+
"ok": True,
|
|
888
|
+
"system_enabled_connectors": sorted(enabled),
|
|
889
|
+
"restarted_background_connectors": restarted,
|
|
890
|
+
}
|
|
891
|
+
|
|
631
892
|
def reload_connectors_config(self, *, restart_background: bool = True) -> dict[str, object]:
|
|
632
893
|
self.connectors_config = self.config_manager.load_named_normalized("connectors")
|
|
633
894
|
register_builtin_channels(home=self.home, connectors_config=self.connectors_config)
|
|
@@ -643,13 +904,32 @@ class DaemonApp:
|
|
|
643
904
|
return {
|
|
644
905
|
"ok": True,
|
|
645
906
|
"connectors": sorted(
|
|
646
|
-
name
|
|
907
|
+
name
|
|
908
|
+
for name, config in self.connectors_config.items()
|
|
909
|
+
if not str(name).startswith("_")
|
|
910
|
+
and isinstance(config, dict)
|
|
911
|
+
and self._is_connector_system_enabled(str(name))
|
|
647
912
|
),
|
|
648
913
|
}
|
|
649
914
|
|
|
915
|
+
def reload_runners_config(self) -> dict[str, object]:
|
|
916
|
+
self.runners_config = self.config_manager.load_runners_config()
|
|
917
|
+
codex_config = self.runners_config.get("codex", {})
|
|
918
|
+
if isinstance(codex_config, dict):
|
|
919
|
+
self.codex_runner.binary = str(codex_config.get("binary") or "codex")
|
|
920
|
+
return {
|
|
921
|
+
"ok": True,
|
|
922
|
+
"runners": sorted(name for name, config in self.runners_config.items() if isinstance(config, dict)),
|
|
923
|
+
"codex": {
|
|
924
|
+
"binary": self.codex_runner.binary,
|
|
925
|
+
"approval_policy": codex_config.get("approval_policy") if isinstance(codex_config, dict) else None,
|
|
926
|
+
"sandbox_mode": codex_config.get("sandbox_mode") if isinstance(codex_config, dict) else None,
|
|
927
|
+
"mcp_tool_timeout_sec": codex_config.get("mcp_tool_timeout_sec") if isinstance(codex_config, dict) else None,
|
|
928
|
+
},
|
|
929
|
+
}
|
|
930
|
+
|
|
650
931
|
def _preferred_locale(self) -> str:
|
|
651
|
-
|
|
652
|
-
return str(config.get("default_locale") or "en-US").lower()
|
|
932
|
+
return str(self.runtime_config.get("default_locale") or "en-US").lower()
|
|
653
933
|
|
|
654
934
|
def _polite_copy(self, *, zh: str, en: str) -> str:
|
|
655
935
|
return zh if self._preferred_locale().startswith("zh") else en
|
|
@@ -718,7 +998,7 @@ class DaemonApp:
|
|
|
718
998
|
exclude_conversation_id: str | None = None,
|
|
719
999
|
preferred_connector_conversation_id: str | None = None,
|
|
720
1000
|
requested_connector_bindings: list[dict[str, object]] | None = None,
|
|
721
|
-
force_connector_rebind: bool =
|
|
1001
|
+
force_connector_rebind: bool = True,
|
|
722
1002
|
requested_baseline_ref: dict[str, object] | None = None,
|
|
723
1003
|
startup_contract: dict[str, object] | None = None,
|
|
724
1004
|
) -> dict:
|
|
@@ -2090,6 +2370,210 @@ class DaemonApp:
|
|
|
2090
2370
|
channel = self._channel_with_bindings(connector_name)
|
|
2091
2371
|
return channel.list_bindings()
|
|
2092
2372
|
|
|
2373
|
+
def delete_connector_profile(self, connector_name: str, profile_id: str) -> dict | tuple[int, dict]:
|
|
2374
|
+
normalized_connector = str(connector_name or "").strip().lower()
|
|
2375
|
+
normalized_profile_id = str(profile_id or "").strip()
|
|
2376
|
+
if not normalized_connector:
|
|
2377
|
+
return 400, {"ok": False, "message": "Connector name is required."}
|
|
2378
|
+
if not normalized_profile_id:
|
|
2379
|
+
return 400, {"ok": False, "message": "Profile id is required."}
|
|
2380
|
+
if normalized_connector != "qq" and normalized_connector not in PROFILEABLE_CONNECTOR_NAMES:
|
|
2381
|
+
return 400, {"ok": False, "message": f"Connector `{normalized_connector}` does not support profile deletion."}
|
|
2382
|
+
|
|
2383
|
+
connectors = self.config_manager.load_named_normalized("connectors")
|
|
2384
|
+
connector_config = connectors.get(normalized_connector)
|
|
2385
|
+
if not isinstance(connector_config, dict):
|
|
2386
|
+
return 404, {"ok": False, "message": f"Unknown connector `{normalized_connector}`."}
|
|
2387
|
+
|
|
2388
|
+
if normalized_connector == "qq":
|
|
2389
|
+
profiles = list_qq_profiles(connector_config)
|
|
2390
|
+
else:
|
|
2391
|
+
profiles = list_connector_profiles(normalized_connector, connector_config)
|
|
2392
|
+
if not profiles:
|
|
2393
|
+
return 404, {"ok": False, "message": f"Connector `{normalized_connector}` has no configured profiles."}
|
|
2394
|
+
|
|
2395
|
+
remaining_profiles = [
|
|
2396
|
+
dict(item)
|
|
2397
|
+
for item in profiles
|
|
2398
|
+
if str(item.get("profile_id") or "").strip() != normalized_profile_id
|
|
2399
|
+
]
|
|
2400
|
+
if len(remaining_profiles) == len(profiles):
|
|
2401
|
+
return 404, {
|
|
2402
|
+
"ok": False,
|
|
2403
|
+
"message": f"Profile `{normalized_profile_id}` was not found under connector `{normalized_connector}`.",
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
deleting_last_profile = len(remaining_profiles) == 0
|
|
2407
|
+
related_conversations = self._connector_profile_bound_conversations(
|
|
2408
|
+
normalized_connector,
|
|
2409
|
+
normalized_profile_id,
|
|
2410
|
+
include_all_if_single_profile=deleting_last_profile,
|
|
2411
|
+
)
|
|
2412
|
+
for conversation_id in related_conversations:
|
|
2413
|
+
self._unbind_connector_conversation_everywhere(normalized_connector, conversation_id)
|
|
2414
|
+
|
|
2415
|
+
next_connector_config = dict(connector_config)
|
|
2416
|
+
next_connector_config["profiles"] = remaining_profiles
|
|
2417
|
+
if deleting_last_profile:
|
|
2418
|
+
next_connector_config["enabled"] = False
|
|
2419
|
+
if normalized_connector == "qq":
|
|
2420
|
+
for key in ("app_id", "app_secret", "app_secret_env", "main_chat_id"):
|
|
2421
|
+
next_connector_config[key] = None
|
|
2422
|
+
else:
|
|
2423
|
+
profile_spec = CONNECTOR_PROFILE_SPECS.get(normalized_connector, {})
|
|
2424
|
+
for key in profile_spec.get("profile_fields", ()):
|
|
2425
|
+
if key in {"enabled", "transport", "mode"}:
|
|
2426
|
+
continue
|
|
2427
|
+
next_connector_config[key] = None
|
|
2428
|
+
if normalized_connector == "qq":
|
|
2429
|
+
connectors[normalized_connector] = normalize_qq_connector_config(next_connector_config)
|
|
2430
|
+
else:
|
|
2431
|
+
connectors[normalized_connector] = normalize_connector_config(normalized_connector, next_connector_config)
|
|
2432
|
+
save_result = self.config_manager.save_named_payload("connectors", connectors)
|
|
2433
|
+
if not bool(save_result.get("ok")):
|
|
2434
|
+
return 409, {
|
|
2435
|
+
"ok": False,
|
|
2436
|
+
"message": "Failed to persist connector profile deletion.",
|
|
2437
|
+
"errors": save_result.get("errors") or [],
|
|
2438
|
+
"warnings": save_result.get("warnings") or [],
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
self._cleanup_connector_profile_runtime(
|
|
2442
|
+
normalized_connector,
|
|
2443
|
+
normalized_profile_id,
|
|
2444
|
+
clear_all=deleting_last_profile,
|
|
2445
|
+
)
|
|
2446
|
+
self.reload_connectors_config()
|
|
2447
|
+
snapshot = next(
|
|
2448
|
+
(item for item in self.list_connector_statuses() if str(item.get("name") or "").strip().lower() == normalized_connector),
|
|
2449
|
+
None,
|
|
2450
|
+
)
|
|
2451
|
+
return {
|
|
2452
|
+
"ok": True,
|
|
2453
|
+
"connector": normalized_connector,
|
|
2454
|
+
"profile_id": normalized_profile_id,
|
|
2455
|
+
"deleted": True,
|
|
2456
|
+
"deleted_bound_conversations": related_conversations,
|
|
2457
|
+
"remaining_profile_count": len(remaining_profiles),
|
|
2458
|
+
"snapshot": snapshot,
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
def _connector_profile_bound_conversations(
|
|
2462
|
+
self,
|
|
2463
|
+
connector_name: str,
|
|
2464
|
+
profile_id: str,
|
|
2465
|
+
*,
|
|
2466
|
+
include_all_if_single_profile: bool = False,
|
|
2467
|
+
) -> list[str]:
|
|
2468
|
+
normalized_connector = str(connector_name or "").strip().lower()
|
|
2469
|
+
normalized_profile_id = str(profile_id or "").strip()
|
|
2470
|
+
if not normalized_connector or not normalized_profile_id:
|
|
2471
|
+
return []
|
|
2472
|
+
try:
|
|
2473
|
+
channel = self._channel_with_bindings(normalized_connector)
|
|
2474
|
+
except Exception:
|
|
2475
|
+
return []
|
|
2476
|
+
conversations: list[str] = []
|
|
2477
|
+
seen: set[str] = set()
|
|
2478
|
+
for item in channel.list_bindings():
|
|
2479
|
+
if not isinstance(item, dict):
|
|
2480
|
+
continue
|
|
2481
|
+
conversation_id = str(item.get("conversation_id") or "").strip()
|
|
2482
|
+
if not conversation_id:
|
|
2483
|
+
continue
|
|
2484
|
+
item_profile_id = str(item.get("profile_id") or "").strip()
|
|
2485
|
+
if not include_all_if_single_profile and item_profile_id != normalized_profile_id:
|
|
2486
|
+
continue
|
|
2487
|
+
identity = conversation_identity_key(conversation_id)
|
|
2488
|
+
if identity in seen:
|
|
2489
|
+
continue
|
|
2490
|
+
seen.add(identity)
|
|
2491
|
+
conversations.append(conversation_id)
|
|
2492
|
+
return conversations
|
|
2493
|
+
|
|
2494
|
+
def _unbind_connector_conversation_everywhere(self, connector_name: str, conversation_id: str) -> bool:
|
|
2495
|
+
normalized_connector = str(connector_name or "").strip().lower()
|
|
2496
|
+
normalized_conversation_id = normalize_conversation_id(conversation_id)
|
|
2497
|
+
if not normalized_connector or not normalized_conversation_id:
|
|
2498
|
+
return False
|
|
2499
|
+
try:
|
|
2500
|
+
channel = self._channel_with_bindings(normalized_connector)
|
|
2501
|
+
except Exception:
|
|
2502
|
+
return False
|
|
2503
|
+
bound_quest_id = str(channel.resolve_bound_quest(normalized_conversation_id) or "").strip() or None
|
|
2504
|
+
removed = channel.unbind_conversation(
|
|
2505
|
+
normalized_conversation_id,
|
|
2506
|
+
quest_id=bound_quest_id,
|
|
2507
|
+
)
|
|
2508
|
+
if bound_quest_id:
|
|
2509
|
+
self.sessions.unbind(bound_quest_id, normalized_conversation_id)
|
|
2510
|
+
self.quest_service.unbind_source(bound_quest_id, normalized_conversation_id)
|
|
2511
|
+
return removed
|
|
2512
|
+
|
|
2513
|
+
def _cleanup_connector_profile_runtime(
|
|
2514
|
+
self,
|
|
2515
|
+
connector_name: str,
|
|
2516
|
+
profile_id: str,
|
|
2517
|
+
*,
|
|
2518
|
+
clear_all: bool = False,
|
|
2519
|
+
) -> None:
|
|
2520
|
+
normalized_connector = str(connector_name or "").strip().lower()
|
|
2521
|
+
normalized_profile_id = str(profile_id or "").strip()
|
|
2522
|
+
if not normalized_connector or not normalized_profile_id:
|
|
2523
|
+
return
|
|
2524
|
+
connector_root = self.home / "logs" / "connectors" / normalized_connector
|
|
2525
|
+
profile_root = connector_root / "profiles" / normalized_profile_id
|
|
2526
|
+
shutil.rmtree(profile_root, ignore_errors=True)
|
|
2527
|
+
|
|
2528
|
+
def matches_profile(payload: object, *, conversation_id: str | None = None) -> bool:
|
|
2529
|
+
if clear_all:
|
|
2530
|
+
return True
|
|
2531
|
+
item = payload if isinstance(payload, dict) else {}
|
|
2532
|
+
item_profile_id = str(item.get("profile_id") or "").strip() if isinstance(item, dict) else ""
|
|
2533
|
+
if item_profile_id:
|
|
2534
|
+
return item_profile_id == normalized_profile_id
|
|
2535
|
+
parsed = parse_conversation_id(conversation_id or "")
|
|
2536
|
+
return str((parsed or {}).get("profile_id") or "").strip() == normalized_profile_id
|
|
2537
|
+
|
|
2538
|
+
bindings_path = connector_root / "bindings.json"
|
|
2539
|
+
bindings_payload = read_json(bindings_path, {"bindings": {}})
|
|
2540
|
+
binding_map = bindings_payload.get("bindings")
|
|
2541
|
+
if isinstance(binding_map, dict):
|
|
2542
|
+
filtered_bindings = {
|
|
2543
|
+
key: value
|
|
2544
|
+
for key, value in binding_map.items()
|
|
2545
|
+
if not matches_profile(value, conversation_id=str(key))
|
|
2546
|
+
}
|
|
2547
|
+
bindings_payload["bindings"] = filtered_bindings
|
|
2548
|
+
write_json(bindings_path, bindings_payload)
|
|
2549
|
+
|
|
2550
|
+
state_path = connector_root / "state.json"
|
|
2551
|
+
state_payload = read_json(state_path, {})
|
|
2552
|
+
if isinstance(state_payload, dict):
|
|
2553
|
+
for key in ("recent_conversations", "known_targets"):
|
|
2554
|
+
raw_items = state_payload.get(key)
|
|
2555
|
+
if isinstance(raw_items, list):
|
|
2556
|
+
state_payload[key] = [
|
|
2557
|
+
item
|
|
2558
|
+
for item in raw_items
|
|
2559
|
+
if not matches_profile(
|
|
2560
|
+
item,
|
|
2561
|
+
conversation_id=str((item or {}).get("conversation_id") or "") if isinstance(item, dict) else "",
|
|
2562
|
+
)
|
|
2563
|
+
]
|
|
2564
|
+
if clear_all:
|
|
2565
|
+
state_payload["last_conversation_id"] = None
|
|
2566
|
+
else:
|
|
2567
|
+
last_conversation_id = str(state_payload.get("last_conversation_id") or "").strip()
|
|
2568
|
+
if last_conversation_id and matches_profile({}, conversation_id=last_conversation_id):
|
|
2569
|
+
state_payload["last_conversation_id"] = None
|
|
2570
|
+
write_json(state_path, state_payload)
|
|
2571
|
+
|
|
2572
|
+
if clear_all:
|
|
2573
|
+
runtime_path = connector_root / "runtime.json"
|
|
2574
|
+
if runtime_path.exists():
|
|
2575
|
+
write_json(runtime_path, {})
|
|
2576
|
+
|
|
2093
2577
|
def preview_connector_binding_conflicts(
|
|
2094
2578
|
self,
|
|
2095
2579
|
requested_bindings: list[dict[str, object]] | None,
|
|
@@ -2439,6 +2923,13 @@ class DaemonApp:
|
|
|
2439
2923
|
}
|
|
2440
2924
|
|
|
2441
2925
|
def handle_connector_inbound(self, connector_name: str, body: dict) -> dict:
|
|
2926
|
+
if not self._is_connector_system_enabled(connector_name):
|
|
2927
|
+
return {
|
|
2928
|
+
"ok": True,
|
|
2929
|
+
"accepted": False,
|
|
2930
|
+
"reason": "system_disabled",
|
|
2931
|
+
"message": f"Connector `{connector_name}` is disabled at the system level.",
|
|
2932
|
+
}
|
|
2442
2933
|
channel = self._channel_with_bindings(connector_name)
|
|
2443
2934
|
ingested = channel.ingest(body)
|
|
2444
2935
|
if not ingested.get("accepted", False):
|
|
@@ -3103,6 +3594,8 @@ class DaemonApp:
|
|
|
3103
3594
|
for connector_name, channel in self.channels.items():
|
|
3104
3595
|
if connector_name == "local":
|
|
3105
3596
|
continue
|
|
3597
|
+
if not self._is_connector_system_enabled(connector_name):
|
|
3598
|
+
continue
|
|
3106
3599
|
connector_config = self.connectors_config.get(connector_name, {})
|
|
3107
3600
|
if not isinstance(connector_config, dict):
|
|
3108
3601
|
continue
|
|
@@ -3296,6 +3789,8 @@ class DaemonApp:
|
|
|
3296
3789
|
}
|
|
3297
3790
|
|
|
3298
3791
|
def _maybe_auto_bind_connector_conversation(self, connector_name: str, conversation_id: str) -> dict | None:
|
|
3792
|
+
if not self._is_connector_system_enabled(connector_name):
|
|
3793
|
+
return None
|
|
3299
3794
|
connector_config = self.connectors_config.get(connector_name, {})
|
|
3300
3795
|
if not isinstance(connector_config, dict):
|
|
3301
3796
|
return None
|
|
@@ -3519,6 +4014,8 @@ class DaemonApp:
|
|
|
3519
4014
|
)
|
|
3520
4015
|
|
|
3521
4016
|
def _profiled_connector_configs(self, connector_name: str) -> list[tuple[str, str | None, dict[str, Any]]]:
|
|
4017
|
+
if not self._is_connector_system_enabled(connector_name):
|
|
4018
|
+
return []
|
|
3522
4019
|
connector_config = self.connectors_config.get(connector_name, {})
|
|
3523
4020
|
if not isinstance(connector_config, dict):
|
|
3524
4021
|
return []
|
|
@@ -3536,7 +4033,7 @@ class DaemonApp:
|
|
|
3536
4033
|
|
|
3537
4034
|
def _start_background_connectors(self) -> None:
|
|
3538
4035
|
qq_config = self.connectors_config.get("qq", {})
|
|
3539
|
-
if isinstance(qq_config, dict) and not self._qq_gateways:
|
|
4036
|
+
if self._is_connector_system_enabled("qq") and isinstance(qq_config, dict) and not self._qq_gateways:
|
|
3540
4037
|
profiles = list_qq_profiles(qq_config)
|
|
3541
4038
|
encode_profile_id = len(profiles) > 1
|
|
3542
4039
|
for profile in profiles:
|
|
@@ -3556,7 +4053,7 @@ class DaemonApp:
|
|
|
3556
4053
|
)
|
|
3557
4054
|
if gateway.start():
|
|
3558
4055
|
self._qq_gateways[profile_id] = gateway
|
|
3559
|
-
if not self._telegram_polling:
|
|
4056
|
+
if self._is_connector_system_enabled("telegram") and not self._telegram_polling:
|
|
3560
4057
|
for profile_id, profile_label, profile_config in self._profiled_connector_configs("telegram"):
|
|
3561
4058
|
polling = TelegramPollingService(
|
|
3562
4059
|
home=self.home,
|
|
@@ -3574,7 +4071,7 @@ class DaemonApp:
|
|
|
3574
4071
|
)
|
|
3575
4072
|
if polling.start():
|
|
3576
4073
|
self._telegram_polling[profile_id] = polling
|
|
3577
|
-
if not self._slack_socket:
|
|
4074
|
+
if self._is_connector_system_enabled("slack") and not self._slack_socket:
|
|
3578
4075
|
for profile_id, profile_label, profile_config in self._profiled_connector_configs("slack"):
|
|
3579
4076
|
slack = SlackSocketModeService(
|
|
3580
4077
|
home=self.home,
|
|
@@ -3592,7 +4089,7 @@ class DaemonApp:
|
|
|
3592
4089
|
)
|
|
3593
4090
|
if slack.start():
|
|
3594
4091
|
self._slack_socket[profile_id] = slack
|
|
3595
|
-
if not self._discord_gateway:
|
|
4092
|
+
if self._is_connector_system_enabled("discord") and not self._discord_gateway:
|
|
3596
4093
|
for profile_id, profile_label, profile_config in self._profiled_connector_configs("discord"):
|
|
3597
4094
|
discord = DiscordGatewayService(
|
|
3598
4095
|
home=self.home,
|
|
@@ -3610,7 +4107,7 @@ class DaemonApp:
|
|
|
3610
4107
|
)
|
|
3611
4108
|
if discord.start():
|
|
3612
4109
|
self._discord_gateway[profile_id] = discord
|
|
3613
|
-
if not self._feishu_long_connection:
|
|
4110
|
+
if self._is_connector_system_enabled("feishu") and not self._feishu_long_connection:
|
|
3614
4111
|
for profile_id, profile_label, profile_config in self._profiled_connector_configs("feishu"):
|
|
3615
4112
|
feishu = FeishuLongConnectionService(
|
|
3616
4113
|
home=self.home,
|
|
@@ -3628,7 +4125,7 @@ class DaemonApp:
|
|
|
3628
4125
|
)
|
|
3629
4126
|
if feishu.start():
|
|
3630
4127
|
self._feishu_long_connection[profile_id] = feishu
|
|
3631
|
-
if not self._whatsapp_local_session:
|
|
4128
|
+
if self._is_connector_system_enabled("whatsapp") and not self._whatsapp_local_session:
|
|
3632
4129
|
for profile_id, profile_label, profile_config in self._profiled_connector_configs("whatsapp"):
|
|
3633
4130
|
whatsapp = WhatsAppLocalSessionService(
|
|
3634
4131
|
home=self.home,
|
|
@@ -4259,6 +4756,7 @@ class DaemonApp:
|
|
|
4259
4756
|
"git_commit",
|
|
4260
4757
|
"git_diff_file",
|
|
4261
4758
|
"git_commit_file",
|
|
4759
|
+
"file_change_diff",
|
|
4262
4760
|
"explorer",
|
|
4263
4761
|
"quest_search",
|
|
4264
4762
|
"node_traces",
|