cc-viewer 1.6.345 → 1.7.0
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 +14 -2
- package/cli.js +116 -37
- package/dist/assets/App-DTkacfRW.js +2 -0
- package/dist/assets/App-dYPa5-df.css +1 -0
- package/dist/assets/{MdxEditorPanel-C8ITEOzT.js → MdxEditorPanel-C5Q6uUyH.js} +1 -1
- package/dist/assets/Mobile-MoBp9c8k.js +1 -0
- package/dist/assets/{_baseUniq-CG81mgN_.js → _baseUniq-D9_E5ZDp.js} +1 -1
- package/dist/assets/{arc-C161J4f2.js → arc-D4iFrlhb.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BmbDMdn9.js → architectureDiagram-Q4EWVU46-f1neo7hg.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-DXpMvdxd.js → blockDiagram-DXYQGD6D-DWi9e9tG.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-8Mypg0IU.js → c4Diagram-AHTNJAMY-DAdj49A2.js} +1 -1
- package/dist/assets/{channel-B2khdmvc.js → channel-B3VthT0m.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-I61r1g6z.js → chunk-4BX2VUAB-BrPcAHV3.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-VFPlRE5E.js → chunk-4TB4RGXK-Bvopc1V9.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-DvCamg_6.js → chunk-55IACEB6-Juw-7Z3i.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-Dj4Q8RwR.js → chunk-EDXVE4YY-DVYP9tpB.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-_bgQuuJh.js → chunk-FMBD7UC4-VQ7VOXl3.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-3to38LyN.js → chunk-OYMX7WX6-F8Rj4ZUZ.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Zl26wKKz.js → chunk-QZHKN3VN-Dn7K3yi9.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-RLYPrlh0.js → chunk-YZCP3GAM-UjO-KTsn.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DmSozxoO.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-DmSozxoO.js +1 -0
- package/dist/assets/clone-BQtvy9Ae.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-CsYOjJ0V.js → cose-bilkent-S5V4N54A-BDhNj8rq.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-C3YXXaFU.js → dagre-KV5264BT-BzdcDuyt.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-DUCOg1ld.js → diagram-5BDNPKRD-DP_SX65N.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-BtlLl_zX.js → diagram-G4DWMVQ6-B-pBFw8Q.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-1Qp9xfOk.js → diagram-MMDJMWI5-6XSfN1eN.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-Cj7VMp3L.js → diagram-TYMM5635-DJvGxvpE.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-CVZVn541.js → erDiagram-SMLLAGMA-CpqdpGYj.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-CiU9Fkkv.js → flowDiagram-DWJPFMVM-C9m7wjwI.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-aRaG97ID.js → ganttDiagram-T4ZO3ILL-CU0XOfY3.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-Cr2LJlvX.js → gitGraphDiagram-UUTBAWPF-ipyGULnP.js} +1 -1
- package/dist/assets/{graph-B59lDyMn.js → graph-BR2KmES9.js} +1 -1
- package/dist/assets/{index-C1ptnCpq.js → index--wgRW_OB.js} +1 -1
- package/dist/assets/index-Ac8vnV3v.js +2 -0
- package/dist/assets/{index-CRAWA8PC.js → index-C-be29ey.js} +1 -1
- package/dist/assets/{index-CSM1Kw8l.js → index-C16fiVhv.js} +1 -1
- package/dist/assets/{index-Ddk-kg22.js → index-D5YruOKD.js} +1 -1
- package/dist/assets/{index-BnhNIjbj.js → index-D5tSeMU7.js} +1 -1
- package/dist/assets/{index-vFEQq-NB.js → index-DuZQ6FqR.js} +1 -1
- package/dist/assets/{index-CO2hZ1s4.js → index-OWzSX2T8.js} +1 -1
- package/dist/assets/{infoDiagram-42DDH7IO-CnO22bw8.js → infoDiagram-42DDH7IO-BQWQjfFX.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-ByNzpScH.js → ishikawaDiagram-UXIWVN3A-Df-KH_Hz.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-yaVaCWPq.js → journeyDiagram-VCZTEJTY-CMbB_4G9.js} +1 -1
- package/dist/assets/{jszip.min-BdVFP3OW.js → jszip.min-B6k2t9Ch.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-RIPR_RQ3.js → kanban-definition-6JOO6SKY-CImdlKN3.js} +1 -1
- package/dist/assets/{layout-0rzel0v_.js → layout-DQl4aw09.js} +1 -1
- package/dist/assets/{linear-sktN0tB6.js → linear-CLD3aPlK.js} +1 -1
- package/dist/assets/mermaid.core-CN35dvKJ.js +7 -0
- package/dist/assets/{min-rspGOkz_.js → min-_D86henJ.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-BsVzZzTz.js → mindmap-definition-QFDTVHPH-2JVcu1nk.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-CsoWsw4f.js → pieDiagram-DEJITSTG-Dik-m5u7.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-CTMKuCAI.js → quadrantDiagram-34T5L4WZ-Bm5l40Jq.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-C67256te.js → requirementDiagram-MS252O5E-Ck9w5rTj.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-X8Y7KnW0.js → sankeyDiagram-XADWPNL6-BbGVCFEf.js} +1 -1
- package/dist/assets/seqResourceLoaders-Cd_TAXTb.js +2 -0
- package/dist/assets/{seqResourceLoaders-B3TSSS6T.css → seqResourceLoaders-CxuD1CYl.css} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-Cils0TAp.js → sequenceDiagram-FGHM5R23-vjvEdksk.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-DFnohvpG.js → stateDiagram-FHFEXIEX-DTqMoiB2.js} +1 -1
- package/dist/assets/{stateDiagram-v2-QKLJ7IA2-BTX_4o81.js → stateDiagram-v2-QKLJ7IA2-CNbAwnhu.js} +1 -1
- package/dist/assets/{timeline-definition-GMOUNBTQ-DrIYFrzZ.js → timeline-definition-GMOUNBTQ-Cw8O-52d.js} +1 -1
- package/dist/assets/{vendor-antd-D5HmSDSg.js → vendor-antd-DI7JL-mE.js} +1 -1
- package/dist/assets/{vendor-codemirror-C5yuj7ze.js → vendor-codemirror-B9c49dtM.js} +1 -1
- package/dist/assets/{vendor-mdxeditor-BcEooTvb.js → vendor-mdxeditor-B6cpBtIE.js} +2 -2
- package/dist/assets/{vendor-qrcode-d9LCn0-9.js → vendor-qrcode-Dn90-s-u.js} +1 -1
- package/dist/assets/{vendor-virtuoso-K9m4MN6a.js → vendor-virtuoso-CpdSKD_6.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-lRqBYTr9.js → vennDiagram-DHZGUBPP-DCuqymb7.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-e8YfxpOe.js → wardley-RL74JXVD-BJ20wJUU.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-Bxrg774h.js → wardleyDiagram-NUSXRM2D-Vz6US3qt.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-DWhZqhq8.js → xychartDiagram-5P7HB3ND-CzkLKrmi.js} +1 -1
- package/dist/index.html +4 -4
- package/findcc.js +79 -7
- package/package.json +2 -1
- package/server/i18n.js +21 -141
- package/server/interceptor.js +211 -403
- package/server/lib/adapters/dingtalk-adapter.js +16 -2
- package/server/lib/adapters/discord-adapter.js +28 -6
- package/server/lib/adapters/feishu-adapter.js +50 -4
- package/server/lib/adapters/wecom-adapter.js +24 -0
- package/server/lib/async-write-queue.js +12 -1
- package/server/lib/create_system_prompt.js +13 -7
- package/server/lib/delta-reconstructor.js +3 -3
- package/server/lib/error-report.js +45 -0
- package/server/lib/im-bridge-core.js +71 -4
- package/server/lib/im-lock.js +16 -2
- package/server/lib/im-log-watcher.js +68 -8
- package/server/lib/im-process-manager.js +7 -1
- package/server/lib/interceptor-core.js +19 -174
- package/server/lib/log-management.js +214 -247
- package/server/lib/log-stream.js +59 -108
- package/server/lib/log-watcher.js +34 -272
- package/server/lib/log-zip.js +148 -0
- package/server/lib/model-system-prompts.js +10 -6
- package/server/lib/session-boundary.js +266 -0
- package/server/lib/session-pin-store.js +10 -19
- package/server/lib/spawn-model-resolver.js +109 -0
- package/server/lib/stats-worker.js +138 -267
- package/server/lib/system-prompt-files.js +7 -3
- package/server/lib/system-prompt-render.js +79 -0
- package/server/lib/user-prompt-extract.js +250 -0
- package/server/lib/v2/adapter.js +1059 -0
- package/server/lib/v2/blob-store.js +44 -0
- package/server/lib/v2/conversation-store.js +217 -0
- package/server/lib/v2/convert-manager.js +79 -0
- package/server/lib/v2/convert-worker.js +42 -0
- package/server/lib/v2/convert.js +409 -0
- package/server/lib/v2/identity.js +204 -0
- package/server/lib/v2/journal.js +62 -0
- package/server/lib/v2/layout.js +167 -0
- package/server/lib/v2/live-feed.js +451 -0
- package/server/lib/v2/migrate-prompt.js +54 -0
- package/server/lib/v2/replay.js +191 -0
- package/server/lib/v2/session-select.js +222 -0
- package/server/lib/v2/singleflight.js +104 -0
- package/server/lib/v2/v2-writer.js +513 -0
- package/server/lib/v2/verify.js +233 -0
- package/server/pty-manager.js +115 -36
- package/server/routes/dingtalk.js +9 -3
- package/server/routes/events.js +30 -130
- package/server/routes/files-fs.js +4 -2
- package/server/routes/im.js +67 -20
- package/server/routes/logs.js +194 -152
- package/server/routes/preferences.js +0 -3
- package/server/routes/project-meta.js +2 -3
- package/server/routes/session-pin.js +7 -8
- package/server/routes/workspaces.js +44 -9
- package/server/server.js +59 -66
- package/server/workspace-registry.js +24 -4
- package/dist/assets/App-BjHJ9KJ6.css +0 -1
- package/dist/assets/App-CCmR0aki.js +0 -2
- package/dist/assets/Mobile-DCHg97f1.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BXnyUgsj.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-BXnyUgsj.js +0 -1
- package/dist/assets/clone-md-Mc1-T.js +0 -1
- package/dist/assets/index-CfPXhStV.js +0 -2
- package/dist/assets/mermaid.core-C2Parj1q.js +0 -7
- package/dist/assets/seqResourceLoaders-CAYxnMSH.js +0 -2
- package/server/lib/instance-registry.js +0 -57
- package/server/lib/jsonl-archive.js +0 -180
- package/server/lib/teammate-detect.js +0 -32
- package/server/lib/zip-safety.js +0 -68
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// 变化时触发——与 findRecentLog 的「最新真实日志」语义对齐,过滤流式临时文件噪声。
|
|
13
13
|
// - watchImpl / mkdirImpl 可注入,便于确定性单测(无需真实 FS 时序)。
|
|
14
14
|
|
|
15
|
-
import { watch as fsWatch, mkdirSync, existsSync } from 'node:fs';
|
|
15
|
+
import { watch as fsWatch, mkdirSync, existsSync, readdirSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
|
|
18
18
|
// fs.watch 一次文件写会抖多次(macOS/Linux 尤甚),通常 50~300ms 内合并;选 250ms 平衡实时性与稳定性。
|
|
@@ -21,14 +21,17 @@ const DEFAULT_DEBOUNCE_MS = 250;
|
|
|
21
21
|
// 杜绝被污染的 id 经 `IM_${id}` 拼进路径造成穿越(与路由正则 /^[a-z0-9_-]+$/ 同集)。
|
|
22
22
|
const PLATFORM_ID_RE = /^[a-z0-9_-]+$/;
|
|
23
23
|
|
|
24
|
-
export function createImLogWatcher({ getLogDir, onChange, debounceMs = DEFAULT_DEBOUNCE_MS, watchImpl, mkdirImpl, existsImpl } = {}) {
|
|
24
|
+
export function createImLogWatcher({ getLogDir, onChange, debounceMs = DEFAULT_DEBOUNCE_MS, watchImpl, mkdirImpl, existsImpl, readdirImpl } = {}) {
|
|
25
25
|
const _watch = watchImpl || fsWatch;
|
|
26
26
|
const _mkdir = mkdirImpl || ((d) => { try { mkdirSync(d, { recursive: true }); } catch { /* ignore */ } });
|
|
27
27
|
const _exists = existsImpl || existsSync;
|
|
28
|
+
const _readdir = readdirImpl || ((d) => { try { return readdirSync(d); } catch { return []; } });
|
|
28
29
|
const _onChange = typeof onChange === 'function' ? onChange : () => {};
|
|
29
30
|
const _getLogDir = typeof getLogDir === 'function' ? getLogDir : () => '';
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
// platformId -> { w, dir, sessionsW, sidWatchers: Map<sid, FSWatcher> }
|
|
33
|
+
// 连同所属目录登记,便于 LOG_DIR 切换检测。
|
|
34
|
+
const watchers = new Map();
|
|
32
35
|
const timers = new Map(); // platformId -> timeout
|
|
33
36
|
let _disposed = false;
|
|
34
37
|
|
|
@@ -51,6 +54,52 @@ export function createImLogWatcher({ getLogDir, onChange, debounceMs = DEFAULT_D
|
|
|
51
54
|
return true;
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
function _closeReg(reg) {
|
|
58
|
+
try { reg.w.close(); } catch { /* ignore */ }
|
|
59
|
+
if (reg.sessionsW) { try { reg.sessionsW.close(); } catch { /* ignore */ } }
|
|
60
|
+
for (const sw of reg.sidWatchers.values()) { try { sw.close(); } catch { /* ignore */ } }
|
|
61
|
+
reg.sidWatchers.clear();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// wire-v2: session appends land at IM_<id>/sessions/<sid>/journal.jsonl —
|
|
65
|
+
// two levels below the platform dir, invisible to its non-recursive watch.
|
|
66
|
+
// Per-sid watchers see journal appends (direct children of the sid dir);
|
|
67
|
+
// the sessions/-level watcher sees new sid dirs appear.
|
|
68
|
+
function _watchSid(platformId, reg, sid) {
|
|
69
|
+
if (reg.sidWatchers.has(sid)) return;
|
|
70
|
+
const sidDir = join(reg.dir, 'sessions', sid);
|
|
71
|
+
let sw;
|
|
72
|
+
try {
|
|
73
|
+
sw = _watch(sidDir, (_eventType, filename) => {
|
|
74
|
+
if (_relevant(filename)) _schedule(platformId);
|
|
75
|
+
});
|
|
76
|
+
} catch { return; }
|
|
77
|
+
if (sw && typeof sw.on === 'function') {
|
|
78
|
+
sw.on('error', () => { try { sw.close(); } catch { /* ignore */ } reg.sidWatchers.delete(sid); });
|
|
79
|
+
}
|
|
80
|
+
reg.sidWatchers.set(sid, sw);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function _ensureSessionsWatch(platformId, reg) {
|
|
84
|
+
const sessionsDir = join(reg.dir, 'sessions');
|
|
85
|
+
if (!_exists(sessionsDir)) return; // re-tried on the next platform-dir event
|
|
86
|
+
if (!reg.sessionsW) {
|
|
87
|
+
let sw;
|
|
88
|
+
try {
|
|
89
|
+
sw = _watch(sessionsDir, (_eventType, filename) => {
|
|
90
|
+
// New session dir → a fresh conversation exists; watch it and refresh.
|
|
91
|
+
if (filename) _watchSid(platformId, reg, String(filename));
|
|
92
|
+
_schedule(platformId);
|
|
93
|
+
});
|
|
94
|
+
} catch { return; }
|
|
95
|
+
if (sw && typeof sw.on === 'function') {
|
|
96
|
+
sw.on('error', () => { try { sw.close(); } catch { /* ignore */ } reg.sessionsW = null; });
|
|
97
|
+
}
|
|
98
|
+
reg.sessionsW = sw;
|
|
99
|
+
}
|
|
100
|
+
for (const sid of _readdir(sessionsDir)) _watchSid(platformId, reg, sid);
|
|
101
|
+
}
|
|
102
|
+
|
|
54
103
|
function ensure(platformId) {
|
|
55
104
|
if (_disposed || !platformId) return;
|
|
56
105
|
if (!PLATFORM_ID_RE.test(platformId)) return; // 纵深防御:非白名单字符不放行(防 `IM_${id}` 路径穿越)
|
|
@@ -62,29 +111,40 @@ export function createImLogWatcher({ getLogDir, onChange, debounceMs = DEFAULT_D
|
|
|
62
111
|
// 幂等仅当「目录路径未变且仍存在」时成立;否则关旧重建:
|
|
63
112
|
// - reg.dir !== dir:LOG_DIR 运行时切换(切项目),旧 watcher 还盯着旧目录,新目录无人监听;
|
|
64
113
|
// - !exists(dir):目录被删(某些平台 fs.watch 删目录不报 error,留下永不恢复的幽灵监听)。
|
|
65
|
-
if (reg.dir === dir && _exists(dir))
|
|
66
|
-
|
|
114
|
+
if (reg.dir === dir && _exists(dir)) {
|
|
115
|
+
_ensureSessionsWatch(platformId, reg); // sessions/ may have appeared since
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
_closeReg(reg);
|
|
67
119
|
watchers.delete(platformId);
|
|
68
120
|
}
|
|
69
121
|
_mkdir(dir);
|
|
70
122
|
let w;
|
|
123
|
+
const newReg = { w: null, dir, sessionsW: null, sidWatchers: new Map() };
|
|
71
124
|
try {
|
|
72
125
|
w = _watch(dir, (_eventType, filename) => {
|
|
126
|
+
// v2: 'sessions' appearing (or any event) is the moment to arm the
|
|
127
|
+
// deeper watchers; legacy v1 .jsonl writes keep firing directly.
|
|
128
|
+
if (String(filename || '') === 'sessions' || !filename) {
|
|
129
|
+
_ensureSessionsWatch(platformId, newReg);
|
|
130
|
+
}
|
|
73
131
|
if (_relevant(filename)) _schedule(platformId);
|
|
74
132
|
});
|
|
75
133
|
} catch { return; }
|
|
76
134
|
// watcher 出错(目录被删等)→ 关闭并撤销登记,下次 ensure 可重建。
|
|
77
135
|
if (w && typeof w.on === 'function') {
|
|
78
|
-
w.on('error', () => {
|
|
136
|
+
w.on('error', () => { _closeReg(newReg); watchers.delete(platformId); });
|
|
79
137
|
}
|
|
80
|
-
|
|
138
|
+
newReg.w = w;
|
|
139
|
+
watchers.set(platformId, newReg);
|
|
140
|
+
_ensureSessionsWatch(platformId, newReg);
|
|
81
141
|
}
|
|
82
142
|
|
|
83
143
|
function dispose() {
|
|
84
144
|
_disposed = true;
|
|
85
145
|
for (const t of timers.values()) clearTimeout(t);
|
|
86
146
|
timers.clear();
|
|
87
|
-
for (const reg of watchers.values())
|
|
147
|
+
for (const reg of watchers.values()) _closeReg(reg);
|
|
88
148
|
watchers.clear();
|
|
89
149
|
}
|
|
90
150
|
|
|
@@ -139,7 +139,7 @@ export async function stopImProcess(id, opts = {}) {
|
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
141
|
* 查询某 IM 的进程状态(供 header chip / 路由)。
|
|
142
|
-
* @returns {Promise<{state:string, running:boolean, connected:boolean, pid:number|null, port:number|null, startedAt:string|null}>}
|
|
142
|
+
* @returns {Promise<{state:string, running:boolean, connected:boolean, connectionState:string, lastError:string|null, pid:number|null, port:number|null, startedAt:string|null}>}
|
|
143
143
|
*/
|
|
144
144
|
export async function getImProcessStatus(id, opts = {}) {
|
|
145
145
|
const live = await getImLiveness(id, opts);
|
|
@@ -147,6 +147,12 @@ export async function getImProcessStatus(id, opts = {}) {
|
|
|
147
147
|
state: live.state,
|
|
148
148
|
running: live.state === 'ready' || live.state === 'booting' || live.state === 'hung',
|
|
149
149
|
connected: live.state === 'ready' && !!live.connected,
|
|
150
|
+
// Tri-state IM link status from the worker's own report; a non-ready process cannot be
|
|
151
|
+
// connected or reconnecting, so it reads as disconnected.
|
|
152
|
+
connectionState: live.state === 'ready'
|
|
153
|
+
? (live.connectionState || (live.connected ? 'connected' : 'disconnected'))
|
|
154
|
+
: 'disconnected',
|
|
155
|
+
lastError: live.lastError ?? null,
|
|
150
156
|
pid: live.lock?.pid ?? null,
|
|
151
157
|
port: live.lock?.port ?? null,
|
|
152
158
|
startedAt: live.lock?.startedAt ?? null,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { join, basename } from 'node:path';
|
|
1
|
+
import { readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
4
3
|
|
|
5
4
|
const SUBAGENT_SYSTEM_RE = /(?:command execution|file search|planning) specialist|general-purpose agent|security monitor|performing a web search/i;
|
|
6
5
|
|
|
@@ -15,7 +14,9 @@ const SUBAGENT_BILLING_RE = /cc_is_subagent=true\b/;
|
|
|
15
14
|
// 两处服务端实现(本文件 + kv-cache-analyzer)由 test/interceptor-core-mainagent.test.js 互校防漂移;
|
|
16
15
|
// 前端 contentFilter 那份由 test/content-filter-unit.test.js 单测覆盖。
|
|
17
16
|
const TEAMMATE_SYSTEM_RE = /running as an agent in a team|Agent Teammate Communication/i;
|
|
18
|
-
//
|
|
17
|
+
// 1.7.0: the last external consumer (teammate-detect.js) retired with the
|
|
18
|
+
// prev-segment backfill; kept unexported-in-spirit for the KEEP-IN-SYNC trio
|
|
19
|
+
// above (tests still cross-check the three copies).
|
|
19
20
|
export { TEAMMATE_SYSTEM_RE, SUBAGENT_BILLING_RE };
|
|
20
21
|
|
|
21
22
|
// Rotation carry-forward: prompt-prefix → teammate-name pairs extracted from a
|
|
@@ -40,20 +41,6 @@ export function extractAgentSpawnPairs(responseBody) {
|
|
|
40
41
|
return pairs;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
// Parses the first frame of a log-file head string; returns the entry when it
|
|
44
|
-
// is a rotation-context sentinel, else null. Callers pass a BOUNDED head read
|
|
45
|
-
// (never a whole segment).
|
|
46
|
-
export function parseRotationContextHead(headString) {
|
|
47
|
-
try {
|
|
48
|
-
const frameEnd = headString.indexOf('\n---\n');
|
|
49
|
-
if (frameEnd <= 0) return null;
|
|
50
|
-
const entry = JSON.parse(headString.slice(0, frameEnd));
|
|
51
|
-
return entry && entry.ccvRotationContext ? entry : null;
|
|
52
|
-
} catch {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
44
|
export function getSystemText(body) {
|
|
58
45
|
const system = body?.system;
|
|
59
46
|
if (typeof system === 'string') return system;
|
|
@@ -298,29 +285,27 @@ export function createStreamAssembler() {
|
|
|
298
285
|
};
|
|
299
286
|
}
|
|
300
287
|
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
288
|
+
// Log filename prefix (single source): v1 files were always `<project>_<ts>.jsonl`
|
|
289
|
+
// (`<pid>__`-prefixed variants came from the removed multi-instance feature and
|
|
290
|
+
// are excluded by the matcher below). Kept for the v1 lookups that still exist
|
|
291
|
+
// (migration-era readers, e.g. findRecentLog for IM).
|
|
292
|
+
export function logFilePrefix(projectName) {
|
|
293
|
+
return `${projectName}_`;
|
|
305
294
|
}
|
|
306
295
|
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const p = logFilePrefix(projectName, instanceId);
|
|
314
|
-
if (instanceId) return (f) => f.startsWith(p);
|
|
296
|
+
// v1 log filename ownership test: starts with `<project>_` and does NOT carry
|
|
297
|
+
// the legacy `<pid>__<project>_` instance mark (any instance-tagged file
|
|
298
|
+
// contains `__<project>_` by construction; untagged files never do, even when
|
|
299
|
+
// the project name itself contains `__`).
|
|
300
|
+
export function logFileMatcher(projectName) {
|
|
301
|
+
const p = logFilePrefix(projectName);
|
|
315
302
|
const pidMark = `__${projectName}_`;
|
|
316
303
|
return (f) => f.startsWith(p) && !f.includes(pidMark);
|
|
317
304
|
}
|
|
318
305
|
|
|
319
|
-
|
|
320
|
-
// 现有 2 参调用方与单测零改动。
|
|
321
|
-
export function findRecentLog(dir, projectName, instanceId = null) {
|
|
306
|
+
export function findRecentLog(dir, projectName) {
|
|
322
307
|
try {
|
|
323
|
-
const owns = logFileMatcher(projectName
|
|
308
|
+
const owns = logFileMatcher(projectName);
|
|
324
309
|
const files = readdirSync(dir)
|
|
325
310
|
// 排除 *_temp.jsonl:临时文件是未完成的写入态(resume 流程中途产物),
|
|
326
311
|
// 不应被当作"最近完整日志"(否则 _temp 因 sort 排在正式文件之后会被误选)。
|
|
@@ -333,114 +318,6 @@ export function findRecentLog(dir, projectName, instanceId = null) {
|
|
|
333
318
|
return null;
|
|
334
319
|
}
|
|
335
320
|
|
|
336
|
-
// 首启接管(仅 --pid 实例):本 pid 还没有自己的日志时,把项目里最近的【无标签】日志原子 rename 成
|
|
337
|
-
// `<pid>__<project>_<ts>.jsonl`,并入该 pid 血脉(move 而非 copy:不双计入统计)。返回接管后的新路径,或 null。
|
|
338
|
-
//
|
|
339
|
-
// 这是 best-effort 便利特性,不是核心隔离(核心隔离=各 pid 读自己的日志,不依赖接管)。为不误抢一个仍存活的
|
|
340
|
-
// 无 pid 实例(评审标记的 live-writer / 卡在 resume 的窄边角),双重守卫:① 项目里若存在任何无标签 `*_temp.jsonl`
|
|
341
|
-
// (某个无 pid 实例正卡在 resume / 在写)→ 放弃;② 无标签日志 mtime 必须早于 freshnessMs(默认 5min)。仍有理论窄缝
|
|
342
|
-
// (进程挂起 >5min 且无 temp),属可接受的便利取舍。并发下原子 rename 只一个赢、输者 throw→吞→null(回退全新)。
|
|
343
|
-
export function claimUntaggedLog(dir, projectName, instanceId, { freshnessMs = 300000 } = {}) {
|
|
344
|
-
if (!instanceId) return null;
|
|
345
|
-
try {
|
|
346
|
-
const untagged = findRecentLog(dir, projectName, null); // 最近无标签日志(已排除 pid 文件 + temp)
|
|
347
|
-
if (!untagged) return null;
|
|
348
|
-
// ① 存在无标签 temp ⇒ 有无 pid 实例正活动/卡在 resume → 不接管。
|
|
349
|
-
const owns = logFileMatcher(projectName, null);
|
|
350
|
-
const hasUntaggedTemp = readdirSync(dir).some(f => owns(f) && f.endsWith('_temp.jsonl'));
|
|
351
|
-
if (hasUntaggedTemp) return null;
|
|
352
|
-
let st;
|
|
353
|
-
try { st = statSync(untagged); } catch { return null; }
|
|
354
|
-
if (Date.now() - st.mtimeMs < freshnessMs) return null; // ② 可能是活动写者 → 不碰
|
|
355
|
-
const claimed = join(dir, `${instanceId}__${basename(untagged)}`);
|
|
356
|
-
if (existsSync(claimed)) return null; // 极端撞名 → 放弃
|
|
357
|
-
renameSyncWithRetry(untagged, claimed);
|
|
358
|
-
return claimed;
|
|
359
|
-
} catch { return null; }
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
export function cleanupTempFiles(dir, projectName, instanceId = null) {
|
|
363
|
-
try {
|
|
364
|
-
const owns = logFileMatcher(projectName, instanceId);
|
|
365
|
-
const tempFiles = readdirSync(dir)
|
|
366
|
-
.filter(f => owns(f) && f.endsWith('_temp.jsonl'));
|
|
367
|
-
for (const f of tempFiles) {
|
|
368
|
-
try {
|
|
369
|
-
const tempPath = join(dir, f);
|
|
370
|
-
const newPath = tempPath.replace('_temp.jsonl', '.jsonl');
|
|
371
|
-
if (existsSync(newPath)) {
|
|
372
|
-
const tempContent = readFileSync(tempPath, 'utf-8');
|
|
373
|
-
if (tempContent.trim()) {
|
|
374
|
-
appendFileSync(newPath, tempContent);
|
|
375
|
-
}
|
|
376
|
-
unlinkSync(tempPath);
|
|
377
|
-
} else {
|
|
378
|
-
// 只有非空 temp 文件才 rename,空文件直接删除
|
|
379
|
-
const sz = statSync(tempPath).size;
|
|
380
|
-
if (sz > 0) {
|
|
381
|
-
renameSyncWithRetry(tempPath, newPath);
|
|
382
|
-
} else {
|
|
383
|
-
unlinkSync(tempPath);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
} catch { }
|
|
387
|
-
}
|
|
388
|
-
} catch { }
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
export function migrateConversationContext(oldFile, newFile) {
|
|
392
|
-
try {
|
|
393
|
-
const content = readFileSync(oldFile, 'utf-8');
|
|
394
|
-
if (!content.trim()) return;
|
|
395
|
-
|
|
396
|
-
const parts = content.split('\n---\n').filter(p => p.trim());
|
|
397
|
-
if (parts.length === 0) return;
|
|
398
|
-
|
|
399
|
-
let originIndex = -1;
|
|
400
|
-
for (let i = parts.length - 1; i >= 0; i--) {
|
|
401
|
-
if (!/"mainAgent"\s*:\s*true/.test(parts[i])) continue;
|
|
402
|
-
try {
|
|
403
|
-
const entry = JSON.parse(parts[i]);
|
|
404
|
-
if (entry.mainAgent) {
|
|
405
|
-
const msgs = entry.body?.messages;
|
|
406
|
-
// Delta storage: 使用 _totalMessageCount(delta 条目)或 msgs.length(旧格式)
|
|
407
|
-
const msgCount = entry._totalMessageCount || (Array.isArray(msgs) ? msgs.length : 0);
|
|
408
|
-
if (msgCount === 1) {
|
|
409
|
-
originIndex = i;
|
|
410
|
-
break;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
} catch { }
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if (originIndex < 0) return;
|
|
417
|
-
|
|
418
|
-
let migrationStart = originIndex;
|
|
419
|
-
if (originIndex > 0) {
|
|
420
|
-
try {
|
|
421
|
-
const prevContent = parts[originIndex - 1];
|
|
422
|
-
if (prevContent.trim().startsWith('{')) {
|
|
423
|
-
const prev = JSON.parse(prevContent);
|
|
424
|
-
if (isPreflightEntry(prev)) {
|
|
425
|
-
migrationStart = originIndex - 1;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
} catch { }
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const migratedParts = parts.slice(migrationStart);
|
|
432
|
-
writeFileSync(newFile, migratedParts.join('\n---\n') + '\n---\n');
|
|
433
|
-
|
|
434
|
-
const remainingParts = parts.slice(0, migrationStart);
|
|
435
|
-
if (remainingParts.length > 0) {
|
|
436
|
-
writeFileSync(oldFile, remainingParts.join('\n---\n') + '\n---\n');
|
|
437
|
-
} else {
|
|
438
|
-
// 所有内容已迁移到新文件,清空旧文件(不能删除,watcher 需要检测 truncation 来触发轮转)
|
|
439
|
-
writeFileSync(oldFile, '');
|
|
440
|
-
}
|
|
441
|
-
} catch { }
|
|
442
|
-
}
|
|
443
|
-
|
|
444
321
|
/**
|
|
445
322
|
* 计算单条 message 的轻量身份指纹,用于 delta storage 的 in-place last-msg replace 检测。
|
|
446
323
|
* 仅服务端 interceptor 使用 —— 触发 Plan C checkpoint 让客户端拿到 wire 真实内容。
|
|
@@ -479,38 +356,6 @@ export function fingerprintMsg(m) {
|
|
|
479
356
|
return (m.role || '?') + ':' + snip.replace(/\s+/g, ' ').slice(0, 80);
|
|
480
357
|
}
|
|
481
358
|
|
|
482
|
-
/**
|
|
483
|
-
* Rotate log file when it exceeds maxSize.
|
|
484
|
-
* Creates a new file (no content migration) and appends '\n' to old file
|
|
485
|
-
* to trigger fs.watchFile callback for watcher migration.
|
|
486
|
-
*
|
|
487
|
-
* initialContent (optional) is written INTO the new file at creation time —
|
|
488
|
-
* used for the rotation-context sentinel. It must be baked into creation
|
|
489
|
-
* rather than queued afterwards: the old-file trigger byte below fires the
|
|
490
|
-
* watcher's rotation-follow after a short debounce, and a late-flushing
|
|
491
|
-
* queued write can land between the new file's initial stream read and its
|
|
492
|
-
* lastByteOffset snapshot, in which case it is never delivered to clients.
|
|
493
|
-
*
|
|
494
|
-
* @param {string} currentFile - current log file path
|
|
495
|
-
* @param {string} newFile - new log file path to rotate to
|
|
496
|
-
* @param {number} maxSize - max file size in bytes
|
|
497
|
-
* @param {string} [initialContent] - content the new file is created with
|
|
498
|
-
* @returns {{ rotated: boolean, oldFile?: string, newFile?: string }}
|
|
499
|
-
*/
|
|
500
|
-
export function rotateLogFile(currentFile, newFile, maxSize, initialContent = '') {
|
|
501
|
-
try {
|
|
502
|
-
if (!existsSync(currentFile)) return { rotated: false };
|
|
503
|
-
const size = statSync(currentFile).size;
|
|
504
|
-
if (size < maxSize) return { rotated: false };
|
|
505
|
-
// 不迁移旧内容,创建新文件(立即创建,避免 watcher 时序窗口)
|
|
506
|
-
try { writeFileSync(newFile, initialContent); } catch { }
|
|
507
|
-
// 触发旧文件 watcher 回调,使其检测到文件变更并切换到新文件
|
|
508
|
-
try { appendFileSync(currentFile, '\n'); } catch { }
|
|
509
|
-
return { rotated: true, oldFile: currentFile, newFile };
|
|
510
|
-
} catch { }
|
|
511
|
-
return { rotated: false };
|
|
512
|
-
}
|
|
513
|
-
|
|
514
359
|
/**
|
|
515
360
|
* 在原始 JSON 字符串上定向替换顶层 "model" 字段的值,避免对巨型 wire body
|
|
516
361
|
* (`-c` 重启后的全量 checkpoint 请求可达数十 MB)做二次 JSON.parse + 全量 re-stringify。
|