@researai/deepscientist 1.5.11 → 1.5.12
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 +8 -8
- package/bin/ds.js +358 -61
- package/docs/en/00_QUICK_START.md +35 -3
- package/docs/en/01_SETTINGS_REFERENCE.md +11 -0
- package/docs/en/02_START_RESEARCH_GUIDE.md +68 -4
- package/docs/en/09_DOCTOR.md +28 -3
- package/docs/en/12_GUIDED_WORKFLOW_TOUR.md +21 -2
- package/docs/en/15_CODEX_PROVIDER_SETUP.md +284 -0
- package/docs/en/README.md +4 -0
- package/docs/zh/00_QUICK_START.md +34 -2
- package/docs/zh/01_SETTINGS_REFERENCE.md +11 -0
- package/docs/zh/02_START_RESEARCH_GUIDE.md +69 -3
- package/docs/zh/09_DOCTOR.md +28 -1
- package/docs/zh/12_GUIDED_WORKFLOW_TOUR.md +21 -2
- package/docs/zh/15_CODEX_PROVIDER_SETUP.md +285 -0
- package/docs/zh/README.md +4 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/bash_exec/monitor.py +7 -5
- package/src/deepscientist/bash_exec/service.py +84 -21
- package/src/deepscientist/channels/local.py +3 -3
- package/src/deepscientist/channels/qq.py +7 -7
- package/src/deepscientist/channels/relay.py +7 -7
- package/src/deepscientist/channels/weixin_ilink.py +90 -19
- package/src/deepscientist/config/models.py +1 -0
- package/src/deepscientist/config/service.py +121 -20
- package/src/deepscientist/daemon/app.py +314 -6
- package/src/deepscientist/doctor.py +1 -5
- package/src/deepscientist/mcp/server.py +124 -3
- package/src/deepscientist/prompts/builder.py +113 -11
- package/src/deepscientist/quest/service.py +247 -31
- package/src/deepscientist/runners/codex.py +121 -22
- package/src/deepscientist/runners/runtime_overrides.py +6 -0
- package/src/deepscientist/shared.py +33 -14
- package/src/prompts/connectors/qq.md +2 -1
- package/src/prompts/connectors/weixin.md +2 -1
- package/src/prompts/contracts/shared_interaction.md +4 -1
- package/src/prompts/system.md +59 -9
- package/src/skills/analysis-campaign/SKILL.md +46 -6
- package/src/skills/analysis-campaign/references/campaign-plan-template.md +21 -8
- package/src/skills/baseline/SKILL.md +1 -1
- package/src/skills/decision/SKILL.md +1 -1
- package/src/skills/experiment/SKILL.md +1 -1
- package/src/skills/finalize/SKILL.md +1 -1
- package/src/skills/idea/SKILL.md +1 -1
- package/src/skills/intake-audit/SKILL.md +1 -1
- package/src/skills/rebuttal/SKILL.md +74 -1
- package/src/skills/rebuttal/references/response-letter-template.md +55 -11
- package/src/skills/review/SKILL.md +118 -1
- package/src/skills/review/references/experiment-todo-template.md +23 -0
- package/src/skills/review/references/review-report-template.md +16 -0
- package/src/skills/review/references/revision-log-template.md +4 -0
- package/src/skills/scout/SKILL.md +1 -1
- package/src/skills/write/SKILL.md +168 -7
- package/src/skills/write/references/paper-experiment-matrix-template.md +131 -0
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/{AiManusChatView-D0mTXG4-.js → AiManusChatView-CnJcXynW.js} +12 -12
- package/src/ui/dist/assets/{AnalysisPlugin-Db0cTXxm.js → AnalysisPlugin-DeyzPEhV.js} +1 -1
- package/src/ui/dist/assets/{CliPlugin-DrV8je02.js → CliPlugin-CB1YODQn.js} +9 -9
- package/src/ui/dist/assets/{CodeEditorPlugin-QXMSCH71.js → CodeEditorPlugin-B-xicq1e.js} +8 -8
- package/src/ui/dist/assets/{CodeViewerPlugin-7hhtWj_E.js → CodeViewerPlugin-DT54ysXa.js} +5 -5
- package/src/ui/dist/assets/{DocViewerPlugin-BWMSnRJe.js → DocViewerPlugin-DQtKT-VD.js} +3 -3
- package/src/ui/dist/assets/{GitDiffViewerPlugin-7J9h9Vy_.js → GitDiffViewerPlugin-hqHbCfnv.js} +20 -20
- package/src/ui/dist/assets/{ImageViewerPlugin-CHJl_0lr.js → ImageViewerPlugin-OcVo33jV.js} +5 -5
- package/src/ui/dist/assets/{LabCopilotPanel-1qSow1es.js → LabCopilotPanel-DdGwhEUV.js} +11 -11
- package/src/ui/dist/assets/{LabPlugin-eQpPPCEp.js → LabPlugin-Ciz1gDaX.js} +2 -2
- package/src/ui/dist/assets/{LatexPlugin-BwRfi89Z.js → LatexPlugin-BhmjNQRC.js} +37 -11
- package/src/ui/dist/assets/{MarkdownViewerPlugin-836PVQWV.js → MarkdownViewerPlugin-BzdVH9Bx.js} +4 -4
- package/src/ui/dist/assets/{MarketplacePlugin-C2y_556i.js → MarketplacePlugin-DmyHspXt.js} +3 -3
- package/src/ui/dist/assets/{NotebookEditor-DIX7Mlzu.js → NotebookEditor-BMXKrDRk.js} +1 -1
- package/src/ui/dist/assets/{NotebookEditor-BRzJbGsn.js → NotebookEditor-BTVYRGkm.js} +11 -11
- package/src/ui/dist/assets/{PdfLoader-DzRaTAlq.js → PdfLoader-CvcjJHXv.js} +1 -1
- package/src/ui/dist/assets/{PdfMarkdownPlugin-DZUfIUnp.js → PdfMarkdownPlugin-DW2ej8Vk.js} +2 -2
- package/src/ui/dist/assets/{PdfViewerPlugin-BwtICzue.js → PdfViewerPlugin-CmlDxbhU.js} +10 -10
- package/src/ui/dist/assets/{SearchPlugin-DHeIAMsx.js → SearchPlugin-DAjQZPSv.js} +1 -1
- package/src/ui/dist/assets/{TextViewerPlugin-C3tCmFox.js → TextViewerPlugin-C-nVAZb_.js} +5 -5
- package/src/ui/dist/assets/{VNCViewer-CQsKVm3t.js → VNCViewer-D7-dIYon.js} +10 -10
- package/src/ui/dist/assets/{bot-BEA2vWuK.js → bot-C_G4WtNI.js} +1 -1
- package/src/ui/dist/assets/{code-XfbSR8K2.js → code-Cd7WfiWq.js} +1 -1
- package/src/ui/dist/assets/{file-content-BjxNaIfy.js → file-content-B57zsL9y.js} +1 -1
- package/src/ui/dist/assets/{file-diff-panel-D_lLVQk0.js → file-diff-panel-DVoheLFq.js} +1 -1
- package/src/ui/dist/assets/{file-socket-D9x_5vlY.js → file-socket-B5kXFxZP.js} +1 -1
- package/src/ui/dist/assets/{image-BhWT33W1.js → image-LLOjkMHF.js} +1 -1
- package/src/ui/dist/assets/{index-Dqj-Mjb4.css → index-BQG-1s2o.css} +40 -2
- package/src/ui/dist/assets/{index--c4iXtuy.js → index-C3r2iGrp.js} +12 -12
- package/src/ui/dist/assets/{index-DZTZ8mWP.js → index-CLQauncb.js} +911 -120
- package/src/ui/dist/assets/{index-PJbSbPTy.js → index-Dxa2eYMY.js} +1 -1
- package/src/ui/dist/assets/{index-BDxipwrC.js → index-hOUOWbW2.js} +2 -2
- package/src/ui/dist/assets/{monaco-K8izTGgo.js → monaco-BGGAEii3.js} +1 -1
- package/src/ui/dist/assets/{pdf-effect-queue-DfBors6y.js → pdf-effect-queue-DlEr1_y5.js} +1 -1
- package/src/ui/dist/assets/{popover-yFK1J4fL.js → popover-CWJbJuYY.js} +1 -1
- package/src/ui/dist/assets/{project-sync-PENr2zcz.js → project-sync-CRJiucYO.js} +18 -4
- package/src/ui/dist/assets/{select-CAbJDfYv.js → select-CoHB7pvH.js} +2 -2
- package/src/ui/dist/assets/{sigma-DEuYJqTl.js → sigma-D5aJWR8J.js} +1 -1
- package/src/ui/dist/assets/{square-check-big-omoSUmcd.js → square-check-big-DUK_mnkS.js} +1 -1
- package/src/ui/dist/assets/{trash--F119N47.js → trash-ChU3SEE3.js} +1 -1
- package/src/ui/dist/assets/{useCliAccess-D31UR23I.js → useCliAccess-BrJBV3tY.js} +1 -1
- package/src/ui/dist/assets/{useFileDiffOverlay-BH6KcMzq.js → useFileDiffOverlay-C2OQaVWc.js} +1 -1
- package/src/ui/dist/assets/{wrap-text-CZ613PM5.js → wrap-text-C7Qqh-om.js} +1 -1
- package/src/ui/dist/assets/{zoom-out-BgDLAv3z.js → zoom-out-rtX0FKya.js} +1 -1
- package/src/ui/dist/index.html +2 -2
|
@@ -11,12 +11,13 @@ import sys
|
|
|
11
11
|
import tempfile
|
|
12
12
|
import threading
|
|
13
13
|
import time
|
|
14
|
+
from collections import deque
|
|
14
15
|
from datetime import UTC, datetime
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
from typing import Any
|
|
17
18
|
|
|
18
19
|
from ..mcp.context import McpContext
|
|
19
|
-
from ..shared import append_jsonl, ensure_dir, generate_id, read_json, read_jsonl, utc_now
|
|
20
|
+
from ..shared import append_jsonl, ensure_dir, generate_id, iter_jsonl, read_json, read_jsonl, read_jsonl_tail, utc_now
|
|
20
21
|
from .runtime import TerminalRuntimeManager
|
|
21
22
|
|
|
22
23
|
BASH_STATUS_MARKER_PREFIX = "__DS_BASH_STATUS__"
|
|
@@ -24,6 +25,9 @@ BASH_CARRIAGE_RETURN_PREFIX = "__DS_BASH_CR__"
|
|
|
24
25
|
BASH_PROGRESS_PREFIX = "__DS_PROGRESS__"
|
|
25
26
|
BASH_TERMINAL_PROMPT_PREFIX = "__DS_TERMINAL_PROMPT__"
|
|
26
27
|
DEFAULT_LOG_TAIL_LIMIT = 200
|
|
28
|
+
DEFAULT_INLINE_BASH_LOG_LINE_LIMIT = 2000
|
|
29
|
+
DEFAULT_INLINE_BASH_LOG_HEAD_LINES = 500
|
|
30
|
+
DEFAULT_INLINE_BASH_LOG_TAIL_LINES = 1500
|
|
27
31
|
DEFAULT_POLL_INTERVAL_SECONDS = 0.35
|
|
28
32
|
TERMINAL_STATUSES = {"completed", "failed", "terminated"}
|
|
29
33
|
DEFAULT_TERMINAL_SESSION_ID = "terminal-main"
|
|
@@ -46,6 +50,52 @@ def _atomic_write_json(path: Path, payload: Any) -> None:
|
|
|
46
50
|
temp_path.replace(path)
|
|
47
51
|
|
|
48
52
|
|
|
53
|
+
def _count_jsonl_records(path: Path) -> int:
|
|
54
|
+
return sum(1 for _ in iter_jsonl(path))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _build_terminal_log_preview_payload(path: Path) -> dict[str, Any]:
|
|
58
|
+
if not path.exists():
|
|
59
|
+
return {
|
|
60
|
+
"log": "",
|
|
61
|
+
"log_line_count": 0,
|
|
62
|
+
"log_truncated": False,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
head_lines: list[str] = []
|
|
66
|
+
tail_lines: deque[str] = deque(maxlen=DEFAULT_INLINE_BASH_LOG_TAIL_LINES)
|
|
67
|
+
total = 0
|
|
68
|
+
with path.open("r", encoding="utf-8", errors="replace") as handle:
|
|
69
|
+
for raw_line in handle:
|
|
70
|
+
line = raw_line.rstrip("\n")
|
|
71
|
+
total += 1
|
|
72
|
+
if total <= DEFAULT_INLINE_BASH_LOG_HEAD_LINES:
|
|
73
|
+
head_lines.append(line)
|
|
74
|
+
tail_lines.append(line)
|
|
75
|
+
|
|
76
|
+
if total <= DEFAULT_INLINE_BASH_LOG_LINE_LIMIT:
|
|
77
|
+
return {
|
|
78
|
+
"log": "\n".join(list(tail_lines)),
|
|
79
|
+
"log_line_count": total,
|
|
80
|
+
"log_truncated": False,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
omitted = max(0, total - DEFAULT_INLINE_BASH_LOG_HEAD_LINES - DEFAULT_INLINE_BASH_LOG_TAIL_LINES)
|
|
84
|
+
marker = (
|
|
85
|
+
"[... omitted "
|
|
86
|
+
f"{omitted} lines from the middle of this log. "
|
|
87
|
+
"Use bash_exec(mode='read', id=..., start=..., tail=...) for a specific window.]"
|
|
88
|
+
)
|
|
89
|
+
return {
|
|
90
|
+
"log": "\n".join(head_lines + [marker] + list(tail_lines)),
|
|
91
|
+
"log_line_count": total,
|
|
92
|
+
"log_truncated": True,
|
|
93
|
+
"log_preview_head_lines": DEFAULT_INLINE_BASH_LOG_HEAD_LINES,
|
|
94
|
+
"log_preview_tail_lines": DEFAULT_INLINE_BASH_LOG_TAIL_LINES,
|
|
95
|
+
"log_preview_omitted_lines": omitted,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
49
99
|
def _normalize_string(value: object) -> str:
|
|
50
100
|
return str(value or "").strip()
|
|
51
101
|
|
|
@@ -568,7 +618,8 @@ class BashExecService:
|
|
|
568
618
|
if not self.meta_path(quest_root, bash_id).exists():
|
|
569
619
|
raise FileNotFoundError(f"Unknown bash session `{bash_id}`.")
|
|
570
620
|
deadline = time.monotonic() + 0.6
|
|
571
|
-
|
|
621
|
+
path = self.log_path(quest_root, bash_id)
|
|
622
|
+
entries = read_jsonl_tail(path, max(1, limit))
|
|
572
623
|
while time.monotonic() < deadline:
|
|
573
624
|
if any(str(entry.get("stream") or "") not in {"system", "prompt"} for entry in entries):
|
|
574
625
|
break
|
|
@@ -580,24 +631,33 @@ class BashExecService:
|
|
|
580
631
|
time.sleep(0.05)
|
|
581
632
|
else:
|
|
582
633
|
time.sleep(0.03)
|
|
583
|
-
entries =
|
|
634
|
+
entries = read_jsonl_tail(path, max(1, limit))
|
|
584
635
|
latest_seq = int(entries[-1].get("seq") or 0) if entries else 0
|
|
585
636
|
normalized_before = before_seq if isinstance(before_seq, int) and before_seq > 0 else None
|
|
586
637
|
normalized_after = after_seq if isinstance(after_seq, int) and after_seq >= 0 else None
|
|
587
|
-
if normalized_after is not None:
|
|
588
|
-
entries = [entry for entry in entries if int(entry.get("seq") or 0) > normalized_after]
|
|
589
|
-
if normalized_before is not None:
|
|
590
|
-
entries = [entry for entry in entries if int(entry.get("seq") or 0) < normalized_before]
|
|
591
|
-
selection_pool = entries
|
|
592
|
-
if prefer_visible:
|
|
593
|
-
visible_entries = [
|
|
594
|
-
entry for entry in entries if str(entry.get("stream") or "") not in {"system", "prompt"}
|
|
595
|
-
]
|
|
596
|
-
if visible_entries:
|
|
597
|
-
selection_pool = visible_entries
|
|
598
638
|
normalized_limit = max(1, limit)
|
|
599
|
-
|
|
600
|
-
|
|
639
|
+
selection_pool: deque[dict[str, Any]] = deque(maxlen=normalized_limit)
|
|
640
|
+
visible_pool: deque[dict[str, Any]] = deque(maxlen=normalized_limit)
|
|
641
|
+
total_filtered = 0
|
|
642
|
+
for entry in iter_jsonl(path):
|
|
643
|
+
seq = int(entry.get("seq") or 0)
|
|
644
|
+
latest_seq = max(latest_seq, seq)
|
|
645
|
+
if normalized_after is not None and seq <= normalized_after:
|
|
646
|
+
continue
|
|
647
|
+
if normalized_before is not None and seq >= normalized_before:
|
|
648
|
+
continue
|
|
649
|
+
total_filtered += 1
|
|
650
|
+
selection_pool.append(entry)
|
|
651
|
+
if str(entry.get("stream") or "") not in {"system", "prompt"}:
|
|
652
|
+
visible_pool.append(entry)
|
|
653
|
+
selected_source: list[dict[str, Any]]
|
|
654
|
+
if prefer_visible and visible_pool:
|
|
655
|
+
selected_source = list(visible_pool)
|
|
656
|
+
truncated = total_filtered > len(visible_pool)
|
|
657
|
+
else:
|
|
658
|
+
selected_source = list(selection_pool)
|
|
659
|
+
truncated = total_filtered > len(selection_pool)
|
|
660
|
+
selected = selected_source[-normalized_limit:]
|
|
601
661
|
if order == "desc":
|
|
602
662
|
selected = list(reversed(selected))
|
|
603
663
|
tail_start_seq = int(selected[0].get("seq") or 0) if selected else None
|
|
@@ -868,7 +928,7 @@ class BashExecService:
|
|
|
868
928
|
"last_input_at": None,
|
|
869
929
|
"last_prompt_at": None,
|
|
870
930
|
"last_command": None,
|
|
871
|
-
"history_count":
|
|
931
|
+
"history_count": _count_jsonl_records(self.history_path(quest_root, bash_id)),
|
|
872
932
|
}
|
|
873
933
|
|
|
874
934
|
def ensure_terminal_session(
|
|
@@ -918,7 +978,7 @@ class BashExecService:
|
|
|
918
978
|
self.prompt_events_path(resolved_quest_root, bash_id).touch()
|
|
919
979
|
_atomic_write_json(
|
|
920
980
|
self.input_cursor_path(resolved_quest_root, bash_id),
|
|
921
|
-
{"offset":
|
|
981
|
+
{"offset": _count_jsonl_records(self.input_path(resolved_quest_root, bash_id)), "updated_at": utc_now()},
|
|
922
982
|
)
|
|
923
983
|
_atomic_write_json(
|
|
924
984
|
self.line_buffer_path(resolved_quest_root, bash_id),
|
|
@@ -1072,7 +1132,7 @@ class BashExecService:
|
|
|
1072
1132
|
append_jsonl(self.history_path(quest_root, bash_id), item)
|
|
1073
1133
|
meta = read_json(self.meta_path(quest_root, bash_id), {})
|
|
1074
1134
|
meta["last_command"] = completed[-1]["command"]
|
|
1075
|
-
meta["history_count"] =
|
|
1135
|
+
meta["history_count"] = _count_jsonl_records(self.history_path(quest_root, bash_id))
|
|
1076
1136
|
meta["updated_at"] = utc_now()
|
|
1077
1137
|
meta["last_input_at"] = utc_now()
|
|
1078
1138
|
self._write_meta(quest_root, bash_id, meta)
|
|
@@ -1138,7 +1198,7 @@ class BashExecService:
|
|
|
1138
1198
|
before_seq=None,
|
|
1139
1199
|
order="asc",
|
|
1140
1200
|
)
|
|
1141
|
-
history =
|
|
1201
|
+
history = read_jsonl_tail(self.history_path(quest_root, bash_id), max(1, command_limit))
|
|
1142
1202
|
latest_commands = [
|
|
1143
1203
|
{
|
|
1144
1204
|
"command_id": item.get("command_id"),
|
|
@@ -1208,7 +1268,7 @@ class BashExecService:
|
|
|
1208
1268
|
"watchdog_overdue": session.get("watchdog_overdue"),
|
|
1209
1269
|
}
|
|
1210
1270
|
if include_log:
|
|
1211
|
-
result
|
|
1271
|
+
result.update(self._log_preview_payload(quest_root, str(session["bash_id"])))
|
|
1212
1272
|
if export_log or _normalize_string(export_log_to):
|
|
1213
1273
|
cwd, _ = self.resolve_workdir(context, str(session.get("workdir") or ""))
|
|
1214
1274
|
result.update(
|
|
@@ -1221,3 +1281,6 @@ class BashExecService:
|
|
|
1221
1281
|
)
|
|
1222
1282
|
)
|
|
1223
1283
|
return result
|
|
1284
|
+
|
|
1285
|
+
def _log_preview_payload(self, quest_root: Path, bash_id: str) -> dict[str, Any]:
|
|
1286
|
+
return _build_terminal_log_preview_payload(self.terminal_log_path(quest_root, bash_id))
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
-
from ..shared import append_jsonl, ensure_dir, read_jsonl, utc_now
|
|
6
|
+
from ..shared import append_jsonl, count_jsonl, ensure_dir, read_jsonl, utc_now
|
|
7
7
|
from .base import BaseChannel
|
|
8
8
|
|
|
9
9
|
|
|
@@ -27,6 +27,6 @@ class LocalChannel(BaseChannel):
|
|
|
27
27
|
return {
|
|
28
28
|
"name": self.name,
|
|
29
29
|
"display_mode": self.display_mode,
|
|
30
|
-
"inbox_count":
|
|
31
|
-
"outbox_count":
|
|
30
|
+
"inbox_count": count_jsonl(self.root / "inbox.jsonl"),
|
|
31
|
+
"outbox_count": count_jsonl(self.root / "outbox.jsonl"),
|
|
32
32
|
}
|
|
@@ -6,7 +6,7 @@ from typing import Any
|
|
|
6
6
|
from ..connector_runtime import build_discovered_target, conversation_identity_key, format_conversation_id, merge_discovered_targets, parse_conversation_id
|
|
7
7
|
from ..bridges import get_connector_bridge
|
|
8
8
|
from ..connector.qq_profiles import find_qq_profile, list_qq_profiles, merge_qq_profile_config, qq_profile_label
|
|
9
|
-
from ..shared import append_jsonl, ensure_dir, generate_id, read_json, read_jsonl, utc_now, write_json
|
|
9
|
+
from ..shared import append_jsonl, count_jsonl, ensure_dir, generate_id, read_json, read_jsonl, read_jsonl_tail, utc_now, write_json
|
|
10
10
|
from .base import BaseChannel
|
|
11
11
|
|
|
12
12
|
|
|
@@ -387,9 +387,9 @@ class QQRelayChannel(BaseChannel):
|
|
|
387
387
|
"main_chat_id": main_chat_id,
|
|
388
388
|
"last_conversation_id": last_conversation_id,
|
|
389
389
|
"last_error": last_error,
|
|
390
|
-
"inbox_count":
|
|
391
|
-
"outbox_count":
|
|
392
|
-
"ignored_count":
|
|
390
|
+
"inbox_count": count_jsonl(self.inbox_path),
|
|
391
|
+
"outbox_count": count_jsonl(self.outbox_path),
|
|
392
|
+
"ignored_count": count_jsonl(self.ignored_path),
|
|
393
393
|
"binding_count": len(bindings),
|
|
394
394
|
"bindings": bindings,
|
|
395
395
|
"known_targets": known_targets,
|
|
@@ -947,15 +947,15 @@ class QQRelayChannel(BaseChannel):
|
|
|
947
947
|
|
|
948
948
|
def _recent_events(self) -> list[dict[str, Any]]:
|
|
949
949
|
events: list[dict[str, Any]] = []
|
|
950
|
-
for record in
|
|
950
|
+
for record in read_jsonl_tail(self.inbox_path, self.recent_event_limit):
|
|
951
951
|
event = self._build_recent_event("inbound", record)
|
|
952
952
|
if event is not None:
|
|
953
953
|
events.append(event)
|
|
954
|
-
for record in
|
|
954
|
+
for record in read_jsonl_tail(self.outbox_path, self.recent_event_limit):
|
|
955
955
|
event = self._build_recent_event("outbound", record)
|
|
956
956
|
if event is not None:
|
|
957
957
|
events.append(event)
|
|
958
|
-
for record in
|
|
958
|
+
for record in read_jsonl_tail(self.ignored_path, self.recent_event_limit):
|
|
959
959
|
event = self._build_recent_event("ignored", record)
|
|
960
960
|
if event is not None:
|
|
961
961
|
events.append(event)
|
|
@@ -21,7 +21,7 @@ from ..connector.connector_profiles import (
|
|
|
21
21
|
merge_connector_profile_config,
|
|
22
22
|
)
|
|
23
23
|
from ..bridges import get_connector_bridge
|
|
24
|
-
from ..shared import append_jsonl, ensure_dir, generate_id, read_json, read_jsonl, utc_now, write_json
|
|
24
|
+
from ..shared import append_jsonl, count_jsonl, ensure_dir, generate_id, read_json, read_jsonl, read_jsonl_tail, utc_now, write_json
|
|
25
25
|
from .base import BaseChannel
|
|
26
26
|
|
|
27
27
|
|
|
@@ -412,9 +412,9 @@ class GenericRelayChannel(BaseChannel):
|
|
|
412
412
|
),
|
|
413
413
|
str(runtime_state.get("last_error") or "").strip() or None if isinstance(runtime_state, dict) else None,
|
|
414
414
|
),
|
|
415
|
-
"inbox_count":
|
|
416
|
-
"outbox_count":
|
|
417
|
-
"ignored_count":
|
|
415
|
+
"inbox_count": count_jsonl(self.inbox_path),
|
|
416
|
+
"outbox_count": count_jsonl(self.outbox_path),
|
|
417
|
+
"ignored_count": count_jsonl(self.ignored_path),
|
|
418
418
|
"binding_count": len(bindings),
|
|
419
419
|
"bindings": bindings,
|
|
420
420
|
"known_targets": known_targets,
|
|
@@ -894,15 +894,15 @@ class GenericRelayChannel(BaseChannel):
|
|
|
894
894
|
|
|
895
895
|
def _recent_events(self) -> list[dict[str, Any]]:
|
|
896
896
|
events: list[dict[str, Any]] = []
|
|
897
|
-
for record in
|
|
897
|
+
for record in read_jsonl_tail(self.inbox_path, self.recent_event_limit):
|
|
898
898
|
event = self._build_recent_event("inbound", record)
|
|
899
899
|
if event is not None:
|
|
900
900
|
events.append(event)
|
|
901
|
-
for record in
|
|
901
|
+
for record in read_jsonl_tail(self.outbox_path, self.recent_event_limit):
|
|
902
902
|
event = self._build_recent_event("outbound", record)
|
|
903
903
|
if event is not None:
|
|
904
904
|
events.append(event)
|
|
905
|
-
for record in
|
|
905
|
+
for record in read_jsonl_tail(self.ignored_path, self.recent_event_limit):
|
|
906
906
|
event = self._build_recent_event("ignored", record)
|
|
907
907
|
if event is not None:
|
|
908
908
|
events.append(event)
|
|
@@ -19,7 +19,10 @@ from ..connector.weixin_support import (
|
|
|
19
19
|
save_weixin_get_updates_buf,
|
|
20
20
|
)
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
_SESSION_RETRY_INITIAL_SECONDS = 5.0
|
|
23
|
+
_SESSION_RETRY_MAX_SECONDS = 60.0
|
|
24
|
+
_POLL_RETRY_INITIAL_SECONDS = 2.0
|
|
25
|
+
_POLL_RETRY_MAX_SECONDS = 30.0
|
|
23
26
|
|
|
24
27
|
|
|
25
28
|
class WeixinIlinkService:
|
|
@@ -89,7 +92,8 @@ class WeixinIlinkService:
|
|
|
89
92
|
|
|
90
93
|
def _run(self) -> None:
|
|
91
94
|
timeout_ms = DEFAULT_WEIXIN_LONG_POLL_TIMEOUT_MS
|
|
92
|
-
|
|
95
|
+
retry_until = 0.0
|
|
96
|
+
retry_reason: str | None = None
|
|
93
97
|
sync_buf = load_weixin_get_updates_buf(self._root)
|
|
94
98
|
base_url = normalize_weixin_base_url(self.config.get("base_url"))
|
|
95
99
|
cdn_base_url = normalize_weixin_cdn_base_url(self.config.get("cdn_base_url"))
|
|
@@ -97,6 +101,10 @@ class WeixinIlinkService:
|
|
|
97
101
|
login_user_id = str(self.config.get("login_user_id") or "").strip() or None
|
|
98
102
|
token = self._secret("bot_token", "bot_token_env")
|
|
99
103
|
route_tag = str(self.config.get("route_tag") or "").strip() or None
|
|
104
|
+
session_retry_seconds = _SESSION_RETRY_INITIAL_SECONDS
|
|
105
|
+
poll_retry_seconds = _POLL_RETRY_INITIAL_SECONDS
|
|
106
|
+
session_expired_count = 0
|
|
107
|
+
session_expired_since: str | None = None
|
|
100
108
|
|
|
101
109
|
self._write_state(
|
|
102
110
|
enabled=True,
|
|
@@ -108,20 +116,31 @@ class WeixinIlinkService:
|
|
|
108
116
|
login_user_id=login_user_id,
|
|
109
117
|
base_url=base_url,
|
|
110
118
|
cdn_base_url=cdn_base_url,
|
|
119
|
+
retry_reason=None,
|
|
120
|
+
retry_after_seconds=None,
|
|
121
|
+
pause_until=None,
|
|
111
122
|
updated_at=utc_now(),
|
|
112
123
|
)
|
|
113
124
|
|
|
114
125
|
while not self._stop_event.is_set():
|
|
115
126
|
now = time.time()
|
|
116
|
-
if
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
if retry_until > now:
|
|
128
|
+
retry_after_seconds = max(int(retry_until - now + 0.999), 1)
|
|
129
|
+
state_patch: dict[str, Any] = {
|
|
130
|
+
"connected": False,
|
|
131
|
+
"connection_state": "connecting" if retry_reason == "session_expired" else "error",
|
|
132
|
+
"auth_state": "ready" if token and account_id else "missing_credentials",
|
|
133
|
+
"retry_reason": retry_reason,
|
|
134
|
+
"retry_after_seconds": retry_after_seconds,
|
|
135
|
+
"session_expired_count": session_expired_count or None,
|
|
136
|
+
"session_expired_since": session_expired_since,
|
|
137
|
+
"pause_until": None,
|
|
138
|
+
"updated_at": utc_now(),
|
|
139
|
+
}
|
|
140
|
+
if retry_reason == "session_expired":
|
|
141
|
+
state_patch["last_error"] = f"session expired ({SESSION_EXPIRED_ERRCODE}); retrying automatically"
|
|
142
|
+
self._write_state(**state_patch)
|
|
143
|
+
self._stop_event.wait(min(max(retry_until - now, 0.5), 5.0))
|
|
125
144
|
continue
|
|
126
145
|
try:
|
|
127
146
|
response = get_weixin_updates(
|
|
@@ -137,14 +156,35 @@ class WeixinIlinkService:
|
|
|
137
156
|
errcode = int(response.get("errcode") or 0)
|
|
138
157
|
retcode = int(response.get("ret") or 0)
|
|
139
158
|
if errcode == SESSION_EXPIRED_ERRCODE or retcode == SESSION_EXPIRED_ERRCODE:
|
|
140
|
-
|
|
141
|
-
|
|
159
|
+
session_expired_count += 1
|
|
160
|
+
if session_expired_since is None:
|
|
161
|
+
session_expired_since = utc_now()
|
|
162
|
+
if sync_buf:
|
|
163
|
+
sync_buf = ""
|
|
164
|
+
save_weixin_get_updates_buf(self._root, "")
|
|
165
|
+
retry_delay_seconds = session_retry_seconds
|
|
166
|
+
retry_after_seconds = max(int(retry_delay_seconds + 0.999), 1)
|
|
167
|
+
session_retry_seconds = min(session_retry_seconds * 2.0, _SESSION_RETRY_MAX_SECONDS)
|
|
168
|
+
retry_reason = "session_expired"
|
|
169
|
+
retry_until = time.time() + retry_delay_seconds
|
|
170
|
+
timeout_ms = DEFAULT_WEIXIN_LONG_POLL_TIMEOUT_MS
|
|
171
|
+
self.log(
|
|
172
|
+
"warning",
|
|
173
|
+
(
|
|
174
|
+
"weixin.ilink: session expired; cleared sync state and "
|
|
175
|
+
f"retrying in {retry_after_seconds}s"
|
|
176
|
+
),
|
|
177
|
+
)
|
|
142
178
|
self._write_state(
|
|
143
179
|
connected=False,
|
|
144
|
-
connection_state="
|
|
145
|
-
auth_state="
|
|
146
|
-
last_error=f"session expired ({SESSION_EXPIRED_ERRCODE})",
|
|
147
|
-
|
|
180
|
+
connection_state="connecting",
|
|
181
|
+
auth_state="ready" if token and account_id else "missing_credentials",
|
|
182
|
+
last_error=f"session expired ({SESSION_EXPIRED_ERRCODE}); retrying automatically",
|
|
183
|
+
retry_reason=retry_reason,
|
|
184
|
+
retry_after_seconds=retry_after_seconds,
|
|
185
|
+
session_expired_count=session_expired_count,
|
|
186
|
+
session_expired_since=session_expired_since,
|
|
187
|
+
pause_until=None,
|
|
148
188
|
updated_at=utc_now(),
|
|
149
189
|
)
|
|
150
190
|
continue
|
|
@@ -156,11 +196,26 @@ class WeixinIlinkService:
|
|
|
156
196
|
if next_sync_buf:
|
|
157
197
|
sync_buf = next_sync_buf
|
|
158
198
|
save_weixin_get_updates_buf(self._root, sync_buf)
|
|
199
|
+
if session_expired_count > 0:
|
|
200
|
+
self.log(
|
|
201
|
+
"info",
|
|
202
|
+
f"weixin.ilink: session recovered after {session_expired_count} retry attempt(s)",
|
|
203
|
+
)
|
|
204
|
+
retry_reason = None
|
|
205
|
+
retry_until = 0.0
|
|
206
|
+
session_retry_seconds = _SESSION_RETRY_INITIAL_SECONDS
|
|
207
|
+
poll_retry_seconds = _POLL_RETRY_INITIAL_SECONDS
|
|
208
|
+
session_expired_count = 0
|
|
209
|
+
session_expired_since = None
|
|
159
210
|
self._write_state(
|
|
160
211
|
connected=True,
|
|
161
212
|
connection_state="connected",
|
|
162
213
|
auth_state="ready",
|
|
163
214
|
last_error=None,
|
|
215
|
+
retry_reason=None,
|
|
216
|
+
retry_after_seconds=None,
|
|
217
|
+
session_expired_count=None,
|
|
218
|
+
session_expired_since=None,
|
|
164
219
|
pause_until=None,
|
|
165
220
|
updated_at=utc_now(),
|
|
166
221
|
)
|
|
@@ -182,18 +237,34 @@ class WeixinIlinkService:
|
|
|
182
237
|
except Exception as exc:
|
|
183
238
|
if self._stop_event.is_set():
|
|
184
239
|
break
|
|
185
|
-
|
|
240
|
+
retry_reason = "poll_error"
|
|
241
|
+
retry_delay_seconds = poll_retry_seconds
|
|
242
|
+
retry_after_seconds = max(int(retry_delay_seconds + 0.999), 1)
|
|
243
|
+
retry_until = time.time() + retry_delay_seconds
|
|
244
|
+
poll_retry_seconds = min(poll_retry_seconds * 2.0, _POLL_RETRY_MAX_SECONDS)
|
|
245
|
+
self.log(
|
|
246
|
+
"warning",
|
|
247
|
+
f"weixin.ilink: polling failed: {exc}; retrying in {retry_after_seconds}s",
|
|
248
|
+
)
|
|
186
249
|
self._write_state(
|
|
187
250
|
connected=False,
|
|
188
251
|
connection_state="error",
|
|
189
252
|
auth_state="ready" if token and account_id else "missing_credentials",
|
|
190
253
|
last_error=str(exc),
|
|
254
|
+
retry_reason=retry_reason,
|
|
255
|
+
retry_after_seconds=retry_after_seconds,
|
|
256
|
+
session_expired_count=session_expired_count or None,
|
|
257
|
+
session_expired_since=session_expired_since,
|
|
258
|
+
pause_until=None,
|
|
191
259
|
updated_at=utc_now(),
|
|
192
260
|
)
|
|
193
|
-
self._stop_event.wait(
|
|
261
|
+
self._stop_event.wait(retry_delay_seconds)
|
|
194
262
|
self._write_state(
|
|
195
263
|
connected=False,
|
|
196
264
|
connection_state="stopped",
|
|
265
|
+
retry_reason=None,
|
|
266
|
+
retry_after_seconds=None,
|
|
267
|
+
pause_until=None,
|
|
197
268
|
updated_at=utc_now(),
|
|
198
269
|
)
|
|
199
270
|
|