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
package/server/routes/events.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// SSE event stream + log-registration / resume / turn-end routes (moved verbatim from server.js).
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { _projectName, getLiveLogSource, isContinuedLaunch } from '../interceptor.js';
|
|
5
5
|
import { LOG_DIR } from '../../findcc.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { streamRawEntriesAsync } from '../lib/log-stream.js';
|
|
7
|
+
import { migrationStatus } from '../lib/v2/migrate-prompt.js';
|
|
8
|
+
import { reportSwallowed } from '../lib/error-report.js';
|
|
8
9
|
import { awaitDrainOrClose } from '../lib/sse-backpressure.js';
|
|
9
10
|
import { enrichRawIfNeeded } from '../lib/enrich-plan-input.js';
|
|
10
11
|
import { validateLogPath } from '../lib/log-management.js';
|
|
@@ -49,77 +50,6 @@ function turnEndNotify(req, res, parsedUrl, isLocal, deps) {
|
|
|
49
50
|
});
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
// 注册新的日志文件进行 watch(供新进程复用旧服务时调用)
|
|
53
|
-
function registerLog(req, res, parsedUrl, isLocal, deps) {
|
|
54
|
-
let body = '';
|
|
55
|
-
req.on('data', chunk => { body += chunk; if (body.length > deps.MAX_POST_BODY) req.destroy(); });
|
|
56
|
-
req.on('end', () => {
|
|
57
|
-
try {
|
|
58
|
-
const { logFile } = JSON.parse(body);
|
|
59
|
-
if (logFile && typeof logFile === 'string' && logFile.startsWith(LOG_DIR) && existsSync(logFile)) {
|
|
60
|
-
watchLogFile(deps.logWatcherOpts(logFile));
|
|
61
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
62
|
-
res.end(JSON.stringify({ ok: true }));
|
|
63
|
-
} else {
|
|
64
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
65
|
-
res.end(JSON.stringify({ error: 'Invalid log file path' }));
|
|
66
|
-
}
|
|
67
|
-
} catch {
|
|
68
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
69
|
-
res.end(JSON.stringify({ error: 'Invalid request body' }));
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// 用户选择继续/新开日志
|
|
75
|
-
function resumeChoice(req, res, parsedUrl, isLocal, deps) {
|
|
76
|
-
let body = '';
|
|
77
|
-
req.on('data', chunk => { body += chunk; if (body.length > deps.MAX_POST_BODY) req.destroy(); });
|
|
78
|
-
req.on('end', async () => {
|
|
79
|
-
try {
|
|
80
|
-
const { choice } = JSON.parse(body);
|
|
81
|
-
if (choice !== 'continue' && choice !== 'new') {
|
|
82
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
83
|
-
res.end(JSON.stringify({ error: 'Invalid choice' }));
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
const result = resolveResumeChoice(choice);
|
|
87
|
-
if (!result) {
|
|
88
|
-
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
89
|
-
res.end(JSON.stringify({ error: 'Already resolved' }));
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
// 重新 watch 最终的日志文件
|
|
93
|
-
watchLogFile(deps.logWatcherOpts(result.logFile));
|
|
94
|
-
// 广播 resume_resolved + full_reload
|
|
95
|
-
const resolvedData = JSON.stringify({ logFile: result.logFile });
|
|
96
|
-
deps.clients.forEach(client => {
|
|
97
|
-
try {
|
|
98
|
-
client.write(`event: resume_resolved\ndata: ${resolvedData}\n\n`);
|
|
99
|
-
} catch { }
|
|
100
|
-
});
|
|
101
|
-
// 流式分段广播 full_reload,避免全量加载 OOM
|
|
102
|
-
const reloadTotal = await countLogEntries(LOG_FILE);
|
|
103
|
-
deps.clients.forEach(client => {
|
|
104
|
-
try { client.write(`event: load_start\ndata: ${JSON.stringify({ total: reloadTotal, incremental: false })}\n\n`); } catch { }
|
|
105
|
-
});
|
|
106
|
-
await streamRawEntriesAsync(LOG_FILE, (raw) => {
|
|
107
|
-
deps.clients.forEach(client => {
|
|
108
|
-
try { client.write('event: load_chunk\ndata: ['); client.write(raw.replace(/\n/g, '')); client.write(']\n\n'); } catch { }
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
deps.clients.forEach(client => {
|
|
112
|
-
try { client.write(`event: load_end\ndata: {}\n\n`); } catch { }
|
|
113
|
-
});
|
|
114
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
115
|
-
res.end(JSON.stringify({ ok: true, logFile: result.logFile }));
|
|
116
|
-
} catch {
|
|
117
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
118
|
-
res.end(JSON.stringify({ error: 'Invalid request body' }));
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
53
|
// SSE endpoint
|
|
124
54
|
// 构造「有新版」徽标的 SSE 帧(pending = {version, source})。pending 为空返回 null。
|
|
125
55
|
// 抽成纯函数便于单测帧格式;events() 在新连接上调用它补推,使版本徽标跨刷新持续显示。
|
|
@@ -157,16 +87,22 @@ async function events(req, res, parsedUrl, isLocal, deps) {
|
|
|
157
87
|
console.warn(`[server_config] SSE write failed (turnEndDebounceMs=${deps.turnEndDebounceMs}):`, err && err.message);
|
|
158
88
|
}
|
|
159
89
|
|
|
160
|
-
// 如果有待决的 resume 选择,发送 resume_prompt 事件
|
|
161
|
-
if (_resumeState) {
|
|
162
|
-
res.write(`event: resume_prompt\ndata: ${JSON.stringify({ recentFileName: _resumeState.recentFileName })}\n\n`);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
90
|
// 补推「有新版」徽标:复用启动检查缓存的结果,让刷新/新标签页连上即恢复徽标。
|
|
166
91
|
// 置于 deps.clients.push(res) 之前 → 新客户端仅得一份,不与一次性广播竞态。
|
|
167
92
|
const updFrame = sseUpdateBadgeFrame(deps.pendingMajorUpdate);
|
|
168
93
|
if (updFrame) res.write(updFrame);
|
|
169
94
|
|
|
95
|
+
// 1.7.0 迁移引导(P2):当前项目仍有未转换的 v1 日志 → 连接即推 migrate_prompt。
|
|
96
|
+
// 是否弹窗由客户端决定(「不再提醒」偏好在客户端;continued=true 时无视 dismissed
|
|
97
|
+
// 再提醒一次——`-c` 续接的对话前半段在旧格式里,不迁移就看不到)。工作区切换后的
|
|
98
|
+
// 提示由 workspaces launch 路由对存量连接广播(本帧只覆盖新连接)。
|
|
99
|
+
try {
|
|
100
|
+
const mig = migrationStatus(LOG_DIR, _projectName || '');
|
|
101
|
+
if (mig.pending) {
|
|
102
|
+
res.write(`event: migrate_prompt\ndata: ${JSON.stringify({ ...mig, continued: isContinuedLaunch() })}\n\n`);
|
|
103
|
+
}
|
|
104
|
+
} catch (e) { reportSwallowed('sse.migrate_prompt', e); }
|
|
105
|
+
|
|
170
106
|
// 增量加载参数:移动端带 since/cc/project 请求增量数据
|
|
171
107
|
const sinceParam = parsedUrl.searchParams.get('since');
|
|
172
108
|
const ccParam = parseInt(parsedUrl.searchParams.get('cc'), 10) || 0;
|
|
@@ -202,10 +138,15 @@ async function events(req, res, parsedUrl, isLocal, deps) {
|
|
|
202
138
|
// ring=3 的容错意义:团队会话末尾常有连续 teammate 伪 mainAgent 条目,单记忆位会被
|
|
203
139
|
// 挤掉真实 mainAgent;子串预过滤的理论误伤(键/值恰为 "teammate" 的真实条目)也由
|
|
204
140
|
// 环内更早候选兜底。
|
|
141
|
+
// KEEP IN SYNC: server/lib/v2/adapter.js MAINAGENT_RING_DEPTH — on the v2 path
|
|
142
|
+
// the adapter's mainAgentRing (that depth) wholesale REPLACES this ring, so a
|
|
143
|
+
// mismatch would silently give v1 and v2 sessions different candidate depths.
|
|
205
144
|
const MAINAGENT_SCAN_RING = 3;
|
|
206
145
|
const mainAgentRawRing = [];
|
|
207
146
|
|
|
208
|
-
|
|
147
|
+
// S6b: the cold-load source is the current v2 session dir when the v2
|
|
148
|
+
// writer is active (adapter stream), else the v1 file.
|
|
149
|
+
const coldLoadResult = await streamRawEntriesAsync(getLiveLogSource(), async (raw) => {
|
|
209
150
|
// 直接发送原始 JSON 字符串,不做 parse/reconstruct/stringify
|
|
210
151
|
// ExitPlanMode V2 空 input 的条目按需补全 plan / planFilePath,其它原样透传
|
|
211
152
|
if (res.destroyed || !res.writable) return;
|
|
@@ -255,6 +196,14 @@ async function events(req, res, parsedUrl, isLocal, deps) {
|
|
|
255
196
|
|
|
256
197
|
res.write(`event: load_end\ndata: {}\n\n`);
|
|
257
198
|
|
|
199
|
+
// S10a: v2 sources no longer invoke onScan (the two-pass window never
|
|
200
|
+
// stringifies out-of-window entries) — the adapter returns the newest-main
|
|
201
|
+
// raws as `mainAgentRing` instead. v1 sources still fill the ring via onScan.
|
|
202
|
+
if (Array.isArray(coldLoadResult?.mainAgentRing) && coldLoadResult.mainAgentRing.length > 0) {
|
|
203
|
+
mainAgentRawRing.length = 0;
|
|
204
|
+
mainAgentRawRing.push(...coldLoadResult.mainAgentRing);
|
|
205
|
+
}
|
|
206
|
+
|
|
258
207
|
// Pass 1 入环的候选 newest-first 校验 + parse(≤K 次)。kv_cache 与 context_window
|
|
259
208
|
// 各自取"最新一条能提供该值的真实 mainAgent"—— 与旧版逐条覆盖语义等价(环深度内)。
|
|
260
209
|
for (let ri = mainAgentRawRing.length - 1; ri >= 0 && (!latestKvCache || !latestContextWindow); ri--) {
|
|
@@ -328,7 +277,7 @@ async function requests(req, res) {
|
|
|
328
277
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
329
278
|
res.write('[');
|
|
330
279
|
let first = true;
|
|
331
|
-
await streamRawEntriesAsync(
|
|
280
|
+
await streamRawEntriesAsync(getLiveLogSource(), (raw) => {
|
|
332
281
|
if (!first) res.write(',');
|
|
333
282
|
res.write(enrichRawIfNeeded(raw));
|
|
334
283
|
first = false;
|
|
@@ -337,57 +286,8 @@ async function requests(req, res) {
|
|
|
337
286
|
res.end();
|
|
338
287
|
}
|
|
339
288
|
|
|
340
|
-
// 分页历史条目端点:移动端"加载更多"按需拉取
|
|
341
|
-
// 支持可选 ?file= 参数指定目标文件(用于本地日志分页),默认使用活跃会话文件
|
|
342
|
-
async function entriesPage(req, res, parsedUrl) {
|
|
343
|
-
const before = parsedUrl.searchParams.get('before');
|
|
344
|
-
const limitVal = Math.min(parseInt(parsedUrl.searchParams.get('limit'), 10) || 100, 500);
|
|
345
|
-
if (!before || isNaN(new Date(before).getTime())) {
|
|
346
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
347
|
-
res.end(JSON.stringify({ error: 'missing or invalid "before" parameter' }));
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
const file = parsedUrl.searchParams.get('file');
|
|
351
|
-
let targetFile = LOG_FILE;
|
|
352
|
-
if (file) {
|
|
353
|
-
if (file.includes('..') || (!file.endsWith('.jsonl') && !file.endsWith('.jsonl.zip'))) {
|
|
354
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
355
|
-
res.end(JSON.stringify({ error: 'Invalid file name' }));
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
try { validateLogPath(LOG_DIR, file); } catch (e) {
|
|
359
|
-
const status = e.code === 'NOT_FOUND' ? 404 : e.code === 'ACCESS_DENIED' ? 403 : 400;
|
|
360
|
-
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
361
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
targetFile = join(LOG_DIR, file);
|
|
365
|
-
}
|
|
366
|
-
try {
|
|
367
|
-
const result = await readPagedEntries(targetFile, { before, limit: limitVal });
|
|
368
|
-
// entries 是原始 JSON 字符串数组,parse 后返回给客户端
|
|
369
|
-
// ExitPlanMode V2 空 input 的条目用 enrichRawIfNeeded 在 raw 阶段补全
|
|
370
|
-
const entries = result.entries.map(raw => {
|
|
371
|
-
try { return JSON.parse(enrichRawIfNeeded(raw)); } catch { return null; }
|
|
372
|
-
}).filter(Boolean);
|
|
373
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
374
|
-
res.end(JSON.stringify({
|
|
375
|
-
entries,
|
|
376
|
-
hasMore: result.hasMore,
|
|
377
|
-
oldestTimestamp: result.oldestTimestamp,
|
|
378
|
-
count: entries.length,
|
|
379
|
-
}));
|
|
380
|
-
} catch (err) {
|
|
381
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
382
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
289
|
export const eventsRoutes = [
|
|
387
290
|
{ method: 'POST', match: 'exact', path: '/api/turn-end-notify', handler: turnEndNotify },
|
|
388
|
-
{ method: 'POST', match: 'exact', path: '/api/register-log', handler: registerLog },
|
|
389
|
-
{ method: 'POST', match: 'exact', path: '/api/resume-choice', handler: resumeChoice },
|
|
390
291
|
{ method: 'GET', match: 'exact', path: '/events', handler: events },
|
|
391
292
|
{ method: 'GET', match: 'exact', path: '/api/requests', handler: requests },
|
|
392
|
-
{ method: 'GET', match: 'exact', path: '/api/entries/page', handler: entriesPage },
|
|
393
293
|
];
|
|
@@ -8,7 +8,7 @@ import { homedir, tmpdir } from 'node:os';
|
|
|
8
8
|
import { execFile, spawn } from 'node:child_process';
|
|
9
9
|
import { bumpWorkspacesVersion } from '../lib/file-access-policy.js';
|
|
10
10
|
import { validateImportDir } from '../lib/file-api.js';
|
|
11
|
-
import {
|
|
11
|
+
import { PROFILE_PATH, _projectName, _logDir } from '../interceptor.js';
|
|
12
12
|
import { LOG_DIR, getClaudeConfigDir } from '../../findcc.js';
|
|
13
13
|
|
|
14
14
|
function upload(req, res, parsedUrl, isLocal, deps) {
|
|
@@ -844,7 +844,9 @@ function createDir(req, res, parsedUrl, isLocal, deps) {
|
|
|
844
844
|
}
|
|
845
845
|
|
|
846
846
|
function openLogDir(req, res) {
|
|
847
|
-
|
|
847
|
+
// Project dir works for both formats (legacy v1 files and the v2 sessions/
|
|
848
|
+
// tree are siblings under it); LOG_DIR is the pre-workspace fallback.
|
|
849
|
+
const dir = _logDir || LOG_DIR;
|
|
848
850
|
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'explorer' : 'xdg-open';
|
|
849
851
|
execFile(cmd, [dir], { windowsHide: true }, () => {});
|
|
850
852
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
package/server/routes/im.js
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
// runs with --dangerously-skip-permissions.
|
|
9
9
|
// POST /api/im/:platform/test — loopback-only; validate creds (fetch an access token).
|
|
10
10
|
// POST /api/im/:platform/process — loopback-only; {action:start|stop|restart} the detached worker.
|
|
11
|
+
// 'start' requires stored creds (400 `missing …` otherwise) and
|
|
12
|
+
// also persists enabled:true (merged into the stored config):
|
|
13
|
+
// a worker spawned while the config says disabled no-ops in
|
|
14
|
+
// im-bridge-core and would not survive a restart reconcile,
|
|
15
|
+
// so "start" must mean "enable + spawn".
|
|
11
16
|
// GET /api/im/:platform/logs — resolve the worker's latest .jsonl (for the records popup).
|
|
12
17
|
//
|
|
13
18
|
// Architecture: IM adapters no longer run in the main ccv. Each enabled IM runs as an independent
|
|
@@ -16,6 +21,7 @@
|
|
|
16
21
|
// reports its own in-process adapter (deps.im.getBridgeStatus) — that's what the manager probes.
|
|
17
22
|
import { getDescriptor, loadConfig, loadState, saveConfig } from '../lib/im-config.js';
|
|
18
23
|
import { findRecentLog } from '../lib/interceptor-core.js';
|
|
24
|
+
import { listV2Sessions } from '../lib/v2/adapter.js';
|
|
19
25
|
import { readSenders } from '../lib/im-senders.js';
|
|
20
26
|
import { readImAppendSystem, writeImAppendSystem, buildImAppendSystemPreset, MAX_IM_APPEND_SYSTEM_CHARS } from '../lib/im-append-system.js';
|
|
21
27
|
import { imDir } from '../lib/im-lock.js';
|
|
@@ -56,6 +62,27 @@ function secretKeys(id) {
|
|
|
56
62
|
return getDescriptor(id).fields.filter((f) => f.type === 'secret').map((f) => f.key);
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
/** Required cred/secret field keys that are empty in `cfg` (same gate as the /test route). */
|
|
66
|
+
function missingCreds(id, cfg) {
|
|
67
|
+
return getDescriptor(id).fields
|
|
68
|
+
.filter((f) => (f.type === 'cred' || f.type === 'secret') && !cfg[f.key])
|
|
69
|
+
.map((f) => f.key);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Audit every path that flips a platform on with an empty sender allowlist (bind-first-conversation
|
|
73
|
+
// under --dangerously-skip-permissions). Shared by config POST and process 'start' so no enabling
|
|
74
|
+
// path can bypass the server-side warning — headless callers never see the frontend toast.
|
|
75
|
+
function warnIfEmptyAllowlist(id, cfg) {
|
|
76
|
+
const allowField = getDescriptor(id).allowListField;
|
|
77
|
+
// 过滤空白项后再判空:saveConfig 会 normalize(trim+丢空),若只看原始长度,[" "] 这类全空白
|
|
78
|
+
// 白名单会被当成"已配置"而漏掉审计告警,但实际保存的是空名单(与 dingtalk 路由保持一致)。
|
|
79
|
+
const raw = Array.isArray(cfg[allowField]) ? cfg[allowField] : [];
|
|
80
|
+
const list = raw.filter((s) => typeof s === 'string' && s.trim());
|
|
81
|
+
if (list.length === 0) {
|
|
82
|
+
console.warn(`[CC Viewer] IM ${id} enabled with EMPTY allowlist — bind-first-conversation; the first conversation to message can drive this --dangerously-skip-permissions session`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
59
86
|
function readBody(req, deps, cb) {
|
|
60
87
|
let body = '';
|
|
61
88
|
req.on('data', (chunk) => {
|
|
@@ -79,16 +106,22 @@ async function imStatus(req, res, parsedUrl, isLocal, deps) {
|
|
|
79
106
|
} else {
|
|
80
107
|
// MAIN: the adapter runs in a detached worker, not here. Resolve process+connection via manager.
|
|
81
108
|
processInfo = await deps.im.getProcessStatus(id);
|
|
82
|
-
connection = {
|
|
109
|
+
connection = {
|
|
110
|
+
running: processInfo.running,
|
|
111
|
+
connected: processInfo.connected,
|
|
112
|
+
connectionState: processInfo.connectionState,
|
|
113
|
+
lastError: processInfo.lastError ?? null,
|
|
114
|
+
};
|
|
83
115
|
}
|
|
84
116
|
|
|
85
117
|
res.writeHead(200, JSON_HEADERS);
|
|
86
118
|
if (!isLocal) {
|
|
87
119
|
// Loopback gate: a token-authorized LAN client sees only what the header chip needs.
|
|
120
|
+
// connectionState is passed through as-is (no defaulting); lastError stays loopback-only.
|
|
88
121
|
res.end(JSON.stringify({
|
|
89
122
|
enabled: state.enabled,
|
|
90
123
|
hasSecret: state.hasSecret,
|
|
91
|
-
connection: { running: connection.running, connected: connection.connected },
|
|
124
|
+
connection: { running: connection.running, connected: connection.connected, connectionState: connection.connectionState },
|
|
92
125
|
}));
|
|
93
126
|
return;
|
|
94
127
|
}
|
|
@@ -124,16 +157,7 @@ function imConfigPost(req, res, parsedUrl, isLocal, deps) {
|
|
|
124
157
|
// bind-first-conversation(im-bridge-core.js)——首个向机器人发消息的会话被绑定,该会话内任何人
|
|
125
158
|
// 都可无审批驱动本地会话。这里打一条服务端审计(curl/headless 启用走不到前端 toast),
|
|
126
159
|
// PreToolUse permissions.deny 硬拦截(perm-bridge/im-deny,独立于白名单)仍然生效。
|
|
127
|
-
if (incoming.enabled)
|
|
128
|
-
const allowField = getDescriptor(id).allowListField;
|
|
129
|
-
// 过滤空白项后再判空:saveConfig 会 normalize(trim+丢空),若只看原始长度,[" "] 这类全空白
|
|
130
|
-
// 白名单会被当成"已配置"而漏掉审计告警,但实际保存的是空名单(与 dingtalk 路由保持一致)。
|
|
131
|
-
const raw = Array.isArray(incoming[allowField]) ? incoming[allowField] : [];
|
|
132
|
-
const list = raw.filter((s) => typeof s === 'string' && s.trim());
|
|
133
|
-
if (list.length === 0) {
|
|
134
|
-
console.warn(`[CC Viewer] IM ${id} enabled with EMPTY allowlist — bind-first-conversation; the first conversation to message can drive this --dangerously-skip-permissions session`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
160
|
+
if (incoming.enabled) warnIfEmptyAllowlist(id, incoming);
|
|
137
161
|
const saved = saveConfig(id, incoming);
|
|
138
162
|
// applyProcess(默认 true,保持旧调用方语义):前端 onBlur 自动保存传 false → 仅存盘、不驱动进程,
|
|
139
163
|
// 否则每次输入框失焦都会重启 worker。显式「启动/停止」按钮则不传(=true),沿用下述驱动逻辑。
|
|
@@ -151,7 +175,7 @@ function imConfigPost(req, res, parsedUrl, isLocal, deps) {
|
|
|
151
175
|
}
|
|
152
176
|
res.writeHead(200, JSON_HEADERS);
|
|
153
177
|
// 乐观返回:worker 刚 spawn 尚未就绪,避免回包瞬间显示"已停止";chip 轮询会很快收敛到真实态。
|
|
154
|
-
res.end(JSON.stringify({ ...loadState(id), connection: { running: !!saved.enabled, connected: false } }));
|
|
178
|
+
res.end(JSON.stringify({ ...loadState(id), connection: { running: !!saved.enabled, connected: false, connectionState: 'disconnected' } }));
|
|
155
179
|
});
|
|
156
180
|
}
|
|
157
181
|
|
|
@@ -165,9 +189,7 @@ function imTestPost(req, res, parsedUrl, isLocal, deps) {
|
|
|
165
189
|
const stored = loadConfig(id);
|
|
166
190
|
const cfg = {};
|
|
167
191
|
for (const f of getDescriptor(id).fields) cfg[f.key] = incoming[f.key] || stored[f.key];
|
|
168
|
-
const missing =
|
|
169
|
-
.filter((f) => (f.type === 'cred' || f.type === 'secret') && !cfg[f.key])
|
|
170
|
-
.map((f) => f.key);
|
|
192
|
+
const missing = missingCreds(id, cfg);
|
|
171
193
|
if (missing.length) {
|
|
172
194
|
res.writeHead(200, JSON_HEADERS);
|
|
173
195
|
res.end(JSON.stringify({ ok: false, detail: `missing ${missing.join('/')}` }));
|
|
@@ -195,8 +217,25 @@ function imProcessPost(req, res, parsedUrl, isLocal, deps) {
|
|
|
195
217
|
try {
|
|
196
218
|
if (action === 'stop') await deps.im.stopProcess(id);
|
|
197
219
|
else if (action === 'restart') await deps.im.restartProcess(id);
|
|
198
|
-
else if (action === 'start')
|
|
199
|
-
|
|
220
|
+
else if (action === 'start') {
|
|
221
|
+
const cfg = loadConfig(id);
|
|
222
|
+
// Credential gate (same as /test): without creds the spawned worker's bridge no-ops
|
|
223
|
+
// forever, and persisting enabled:true would make reconcile respawn that zombie on
|
|
224
|
+
// every server restart. Reject up front — nothing is persisted, nothing is spawned.
|
|
225
|
+
const missing = missingCreds(id, cfg);
|
|
226
|
+
if (missing.length) {
|
|
227
|
+
res.writeHead(400, JSON_HEADERS);
|
|
228
|
+
res.end(JSON.stringify({ ok: false, error: `missing ${missing.join('/')}`, detail: `missing ${missing.join('/')}` }));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// Persist enabled:true first (read-merge-write keeps creds/allowlist intact): a worker
|
|
232
|
+
// spawned with enabled:false no-ops its bridge, and reconcile would not respawn it.
|
|
233
|
+
if (!cfg.enabled) {
|
|
234
|
+
warnIfEmptyAllowlist(id, cfg);
|
|
235
|
+
saveConfig(id, { ...cfg, enabled: true });
|
|
236
|
+
}
|
|
237
|
+
await deps.im.startProcess(id);
|
|
238
|
+
} else {
|
|
200
239
|
res.writeHead(400, JSON_HEADERS);
|
|
201
240
|
res.end(JSON.stringify({ error: 'action must be start|stop|restart' }));
|
|
202
241
|
return;
|
|
@@ -218,8 +257,16 @@ function imLogs(req, res, parsedUrl, isLocal, deps) {
|
|
|
218
257
|
const project = `IM_${id}`;
|
|
219
258
|
let latest = null;
|
|
220
259
|
try {
|
|
221
|
-
|
|
222
|
-
|
|
260
|
+
// wire-v2: prefer the newest v2 session (by meta.startTs, journal mtime as
|
|
261
|
+
// tie-break); fall back to legacy v1 files until they are migrated.
|
|
262
|
+
const sessions = listV2Sessions(join(LOG_DIR, project)).filter((s) => !s.leader);
|
|
263
|
+
if (sessions.length > 0) {
|
|
264
|
+
sessions.sort((a, b) => (a.startTs < b.startTs ? 1 : a.startTs > b.startTs ? -1 : 0));
|
|
265
|
+
latest = `v2:${project}/${sessions[0].sid}`; // 直接喂给 /api/local-log?file=
|
|
266
|
+
} else {
|
|
267
|
+
const abs = findRecentLog(join(LOG_DIR, project), project); // 已排除 *_temp.jsonl
|
|
268
|
+
if (abs) latest = `${project}/${basename(abs)}`; // 相对 LOG_DIR
|
|
269
|
+
}
|
|
223
270
|
} catch { /* 无目录/无日志 → latest=null */ }
|
|
224
271
|
res.writeHead(200, JSON_HEADERS);
|
|
225
272
|
res.end(JSON.stringify({ project, latest }));
|