cc-viewer 1.6.348 → 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 +13 -1
- package/cli.js +116 -37
- package/dist/assets/App-DTkacfRW.js +2 -0
- package/dist/assets/{MdxEditorPanel-ualoRFOL.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-B5Zy_CC_.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 +4 -3
- package/package.json +2 -1
- package/server/i18n.js +21 -141
- package/server/interceptor.js +211 -403
- package/server/lib/async-write-queue.js +12 -1
- package/server/lib/delta-reconstructor.js +3 -3
- package/server/lib/error-report.js +45 -0
- package/server/lib/im-log-watcher.js +68 -8
- 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/session-boundary.js +266 -0
- package/server/lib/session-pin-store.js +10 -19
- package/server/lib/stats-worker.js +138 -267
- 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/routes/events.js +30 -130
- package/server/routes/files-fs.js +4 -2
- package/server/routes/im.js +11 -2
- 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 +47 -65
- package/server/workspace-registry.js +24 -4
- package/dist/assets/App-VTo4NVdo.js +0 -2
- package/dist/assets/Mobile-CcmYvMm_.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-DCo650PG.js +0 -2
- package/dist/assets/mermaid.core-C2Parj1q.js +0 -7
- package/dist/assets/seqResourceLoaders-BVozq68i.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/logs.js
CHANGED
|
@@ -1,18 +1,46 @@
|
|
|
1
1
|
// Local log management routes (moved verbatim from server.js handleRequest).
|
|
2
|
-
import { existsSync, realpathSync, statSync, createReadStream,
|
|
2
|
+
import { existsSync, realpathSync, statSync, createReadStream, mkdtempSync, rmSync } from 'node:fs';
|
|
3
3
|
import { join, basename } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import AdmZip from 'adm-zip';
|
|
4
6
|
import { LOG_DIR } from '../../findcc.js';
|
|
5
|
-
import { _projectName,
|
|
6
|
-
import {
|
|
7
|
-
import { countLogEntries, streamRawEntriesAsync, readTailEntries
|
|
8
|
-
import {
|
|
7
|
+
import { _projectName, _v2Writer } from '../interceptor.js';
|
|
8
|
+
import { listV2Logs, listLocalLogs, countListedV1Files, deleteLogFiles, validateLogPath } from '../lib/log-management.js';
|
|
9
|
+
import { countLogEntries, streamRawEntriesAsync, readTailEntries } from '../lib/log-stream.js';
|
|
10
|
+
import { dirSizeSync } from '../lib/v2/layout.js';
|
|
11
|
+
import { extractV2Zip } from '../lib/log-zip.js';
|
|
12
|
+
import { startConvert, stopConvert, convertStatus } from '../lib/v2/convert-manager.js';
|
|
13
|
+
import { migrationStatus } from '../lib/v2/migrate-prompt.js';
|
|
14
|
+
|
|
15
|
+
// Cap for the in-memory zip build (adm-zip toBuffer holds the whole archive in
|
|
16
|
+
// heap) and the upload accumulation — the repo has an OOM history, so both the
|
|
17
|
+
// zipped source folder and the uploaded archive are bounded before buffering.
|
|
18
|
+
const MAX_UPLOAD = 300 * 1024 * 1024;
|
|
19
|
+
// Download-zip source cap; read per-request so CCV_MAX_ZIP_SOURCE is an ops
|
|
20
|
+
// tunable (and a test seam) without a restart.
|
|
21
|
+
const maxZipSource = () => Number(process.env.CCV_MAX_ZIP_SOURCE) || 300 * 1024 * 1024;
|
|
9
22
|
|
|
10
23
|
async function localLogs(req, res, parsedUrl, isLocal, deps) {
|
|
11
24
|
try {
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
25
|
+
// 1.7.0: the DEFAULT list is v2 sessions, unconditionally. `?view=v1`
|
|
26
|
+
// serves the legacy-file list for the modal's v1 view (list + migrate +
|
|
27
|
+
// open + delete for leftovers the converter never removes).
|
|
28
|
+
if (parsedUrl?.searchParams?.get('view') === 'v1') {
|
|
29
|
+
const v1 = await listLocalLogs(LOG_DIR, _projectName);
|
|
30
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
31
|
+
res.end(JSON.stringify(v1));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const result = listV2Logs(LOG_DIR, _projectName);
|
|
35
|
+
// Legacy v1 files are not in this list — surface two distinct signals:
|
|
36
|
+
// - _v1FileCount: v1 files ON DISK (gates the v1-view entry link; the
|
|
37
|
+
// converter never deletes sources, so this outlives a finished migration)
|
|
38
|
+
// - _unmigratedV1Count/Bytes: files still AWAITING migration (gates the
|
|
39
|
+
// migrate button + hint inside the v1 view and the startup prompt)
|
|
40
|
+
const mig = migrationStatus(LOG_DIR, _projectName || '');
|
|
41
|
+
result._unmigratedV1Count = mig.files;
|
|
42
|
+
result._unmigratedV1Bytes = mig.totalBytes;
|
|
43
|
+
result._v1FileCount = _projectName ? countListedV1Files(join(LOG_DIR, _projectName)) : 0;
|
|
16
44
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
17
45
|
res.end(JSON.stringify(result));
|
|
18
46
|
} catch (err) {
|
|
@@ -28,7 +56,69 @@ async function downloadLog(req, res, parsedUrl) {
|
|
|
28
56
|
res.end(JSON.stringify({ error: 'Invalid file name' }));
|
|
29
57
|
return;
|
|
30
58
|
}
|
|
31
|
-
|
|
59
|
+
// wire-v2 S5: rebuilt download for v2 sessions — the adapter stream as a v1
|
|
60
|
+
// `.jsonl`. `format=raw` (session-dir zip) is the S6a half of the S0 contract:
|
|
61
|
+
// a lossless zip of the whole session folder, the transport for round-tripping
|
|
62
|
+
// a v2 session (folder) back through the upload-and-parse flow.
|
|
63
|
+
if (file.startsWith('v2:')) {
|
|
64
|
+
if (parsedUrl.searchParams.get('format') === 'raw') {
|
|
65
|
+
try {
|
|
66
|
+
const sessionDir = validateLogPath(LOG_DIR, file);
|
|
67
|
+
if (dirSizeSync(sessionDir) > maxZipSource()) {
|
|
68
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
69
|
+
res.end(JSON.stringify({ error: 'Session too large to zip', code: 'TOO_LARGE' }));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const dirName = basename(sessionDir);
|
|
73
|
+
const zip = new AdmZip();
|
|
74
|
+
// Wrap the folder under `<dirName>/` and skip dotfiles (.DS_Store etc.).
|
|
75
|
+
// adm-zip passes the zip-internal path (incl. the `<dirName>/` root) to
|
|
76
|
+
// the filter, so match on any path segment starting with a dot.
|
|
77
|
+
zip.addLocalFolder(sessionDir, dirName, (n) => !/(^|\/)\./.test(n.replace(/\\/g, '/')));
|
|
78
|
+
const buf = zip.toBuffer();
|
|
79
|
+
res.writeHead(200, {
|
|
80
|
+
'Content-Type': 'application/zip',
|
|
81
|
+
'Content-Disposition': `attachment; filename="${encodeURIComponent(`${dirName}.zip`)}"`,
|
|
82
|
+
'Content-Length': buf.length,
|
|
83
|
+
});
|
|
84
|
+
res.end(buf);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (!res.headersSent) {
|
|
87
|
+
const status = err.code === 'NOT_FOUND' ? 404 : err.code === 'ACCESS_DENIED' ? 403 : 500;
|
|
88
|
+
// Don't leak internal messages (may contain absolute paths) on 500 —
|
|
89
|
+
// genericize like the upload handler does.
|
|
90
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
91
|
+
res.end(JSON.stringify({ error: status >= 500 ? 'server_error' : err.message }));
|
|
92
|
+
} else {
|
|
93
|
+
res.end();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const sessionDir = validateLogPath(LOG_DIR, file);
|
|
100
|
+
res.writeHead(200, {
|
|
101
|
+
'Content-Type': 'application/octet-stream',
|
|
102
|
+
'Content-Disposition': `attachment; filename="${encodeURIComponent(`${basename(sessionDir)}.v2-rebuilt.jsonl`)}"`,
|
|
103
|
+
'Transfer-Encoding': 'chunked',
|
|
104
|
+
});
|
|
105
|
+
await streamRawEntriesAsync(sessionDir, (raw) => {
|
|
106
|
+
res.write(raw);
|
|
107
|
+
res.write('\n---\n');
|
|
108
|
+
});
|
|
109
|
+
res.end();
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (!res.headersSent) {
|
|
112
|
+
const status = err.code === 'NOT_FOUND' ? 404 : err.code === 'ACCESS_DENIED' ? 403 : 500;
|
|
113
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
114
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
115
|
+
} else {
|
|
116
|
+
res.end();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (!file.endsWith('.jsonl')) {
|
|
32
122
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
33
123
|
res.end(JSON.stringify({ error: 'Invalid file type' }));
|
|
34
124
|
return;
|
|
@@ -48,10 +138,7 @@ async function downloadLog(req, res, parsedUrl) {
|
|
|
48
138
|
return;
|
|
49
139
|
}
|
|
50
140
|
const format = parsedUrl.searchParams.get('format');
|
|
51
|
-
|
|
52
|
-
const fileName = (format !== 'raw' && file.endsWith('.jsonl.zip'))
|
|
53
|
-
? basename(file).slice(0, -4)
|
|
54
|
-
: basename(file);
|
|
141
|
+
const fileName = basename(file);
|
|
55
142
|
// Delta storage: format=raw 下载原始文件;默认下载重建后的全量格式
|
|
56
143
|
if (format === 'raw') {
|
|
57
144
|
const stat = statSync(realPath);
|
|
@@ -89,17 +176,19 @@ async function localLog(req, res, parsedUrl) {
|
|
|
89
176
|
return;
|
|
90
177
|
}
|
|
91
178
|
|
|
92
|
-
//
|
|
93
|
-
|
|
179
|
+
// 验证文件类型:v2 寻址(spec §12);直连 .jsonl 保留为未迁移日志的逃生舱
|
|
180
|
+
// (代码与转换器共享,UI 无入口——1.7.0 决策)。
|
|
181
|
+
const isV2Ref = file.startsWith('v2:');
|
|
182
|
+
if (!isV2Ref && !file.endsWith('.jsonl')) {
|
|
94
183
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
95
|
-
res.end(JSON.stringify({ error: 'Invalid file type. Only .jsonl
|
|
184
|
+
res.end(JSON.stringify({ error: 'Invalid file type. Only .jsonl files are allowed.' }));
|
|
96
185
|
return;
|
|
97
186
|
}
|
|
98
187
|
|
|
99
188
|
try {
|
|
100
189
|
// 独立 SSE 流:直接向请求方返回 event-stream,不走 /events 广播
|
|
101
|
-
validateLogPath
|
|
102
|
-
const filePath =
|
|
190
|
+
// v2 寻址时 validateLogPath 返回 session 目录绝对路径,log-stream 据此分派适配器
|
|
191
|
+
const filePath = validateLogPath(LOG_DIR, file);
|
|
103
192
|
const limitVal = Math.min(parseInt(parsedUrl.searchParams.get('limit'), 10) || 0, 500);
|
|
104
193
|
|
|
105
194
|
res.writeHead(200, {
|
|
@@ -131,7 +220,7 @@ async function localLog(req, res, parsedUrl) {
|
|
|
131
220
|
res.end();
|
|
132
221
|
} catch (err) {
|
|
133
222
|
// 如果 headers 未发送,返回 JSON 错误;否则关闭连接
|
|
134
|
-
// 落 stderr 让用户在 ccv
|
|
223
|
+
// 落 stderr 让用户在 ccv 终端能看到具体原因(SSE onerror 在客户端
|
|
135
224
|
// 不携带错误明细,只能从服务端日志反查)
|
|
136
225
|
console.error('[local-log]', file, err && err.stack || err);
|
|
137
226
|
if (!res.headersSent) {
|
|
@@ -155,7 +244,7 @@ function deleteLogs(req, res, parsedUrl, isLocal, deps) {
|
|
|
155
244
|
res.end(JSON.stringify({ error: 'No files specified' }));
|
|
156
245
|
return;
|
|
157
246
|
}
|
|
158
|
-
const results = deleteLogFiles(LOG_DIR, files);
|
|
247
|
+
const results = deleteLogFiles(LOG_DIR, files, { liveSessionDir: _v2Writer.currentSessionDir() });
|
|
159
248
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
160
249
|
res.end(JSON.stringify({ results }));
|
|
161
250
|
} catch {
|
|
@@ -165,162 +254,115 @@ function deleteLogs(req, res, parsedUrl, isLocal, deps) {
|
|
|
165
254
|
});
|
|
166
255
|
}
|
|
167
256
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
176
|
-
res.end(JSON.stringify({ ok: true, merged }));
|
|
177
|
-
} catch (err) {
|
|
178
|
-
const status = err.code === 'NOT_FOUND' ? 404 : err.code === 'INVALID_INPUT' ? 400 : 500;
|
|
179
|
-
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
180
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
181
|
-
}
|
|
182
|
-
});
|
|
257
|
+
// wire-v2 S8: one-click v1→v2 migration of the CURRENT project, run by the
|
|
258
|
+
// resident convert-manager worker. GET = status snapshot for the modal's
|
|
259
|
+
// polling; POST {action:'start'|'stop'} controls the task. Local/LAN exposure
|
|
260
|
+
// via the existing auth middleware (same posture as delete-logs).
|
|
261
|
+
function getWireV2Convert(req, res) {
|
|
262
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
263
|
+
res.end(JSON.stringify(convertStatus(LOG_DIR, _projectName || '')));
|
|
183
264
|
}
|
|
184
265
|
|
|
185
|
-
function
|
|
266
|
+
function postWireV2Convert(req, res, parsedUrl, isLocal, deps) {
|
|
186
267
|
let body = '';
|
|
187
268
|
req.on('data', chunk => { body += chunk; if (body.length > deps.MAX_POST_BODY) req.destroy(); });
|
|
188
269
|
req.on('end', () => {
|
|
189
270
|
try {
|
|
190
|
-
const {
|
|
191
|
-
if (
|
|
271
|
+
const { action } = JSON.parse(body || '{}');
|
|
272
|
+
if (action !== 'start' && action !== 'stop') {
|
|
192
273
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
193
|
-
res.end(JSON.stringify({ error: '
|
|
274
|
+
res.end(JSON.stringify({ error: 'action must be "start" or "stop"' }));
|
|
194
275
|
return;
|
|
195
276
|
}
|
|
196
|
-
if (
|
|
277
|
+
if (action === 'start' && !_projectName) {
|
|
197
278
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
198
|
-
res.end(JSON.stringify({ error: '
|
|
279
|
+
res.end(JSON.stringify({ error: 'no active project' }));
|
|
199
280
|
return;
|
|
200
281
|
}
|
|
201
|
-
const result =
|
|
202
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
203
|
-
res.end(JSON.stringify(result));
|
|
204
|
-
} catch {
|
|
205
|
-
|
|
206
|
-
res.
|
|
282
|
+
const result = action === 'start' ? startConvert(LOG_DIR, _projectName) : stopConvert();
|
|
283
|
+
res.writeHead(result.ok ? 200 : 409, { 'Content-Type': 'application/json' });
|
|
284
|
+
res.end(JSON.stringify(result.ok ? { ok: true } : { error: result.error }));
|
|
285
|
+
} catch (err) {
|
|
286
|
+
const status = err instanceof SyntaxError ? 400 : 500;
|
|
287
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
288
|
+
res.end(JSON.stringify({ error: err?.message || 'internal error' }));
|
|
207
289
|
}
|
|
208
290
|
});
|
|
209
291
|
}
|
|
210
292
|
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
closeSync(fd);
|
|
224
|
-
}
|
|
225
|
-
const frameEnd = head.indexOf('\n---\n');
|
|
226
|
-
if (frameEnd <= 0) return null;
|
|
227
|
-
const entry = JSON.parse(head.slice(0, frameEnd));
|
|
228
|
-
return entry && entry.ccvRotationContext ? entry : null;
|
|
229
|
-
} catch {
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Cheap substring gate evaluated before JSON.parse: every teammate shape
|
|
235
|
-
// carries at least one of these markers in its raw JSON (external tag,
|
|
236
|
-
// proxy-mode system marker, native SDK prompt). Giant MainAgent checkpoint
|
|
237
|
-
// frames are skipped without parsing.
|
|
238
|
-
function teammateRawPrefilter(raw) {
|
|
239
|
-
return raw.includes('"teammate"')
|
|
240
|
-
|| raw.includes('agent in a team')
|
|
241
|
-
|| raw.includes('Agent Teammate Communication')
|
|
242
|
-
|| raw.includes('You are a Claude agent');
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Single-flight + short-TTL cache for the previous-segment scan. A real
|
|
246
|
-
// predecessor is ~300MB and the parse pass transiently costs hundreds of MB of
|
|
247
|
-
// RSS; every non-incremental cold load triggers the route, so concurrent
|
|
248
|
-
// viewers / rapid refreshes must share one scan. Keyed by path+size+mtime —
|
|
249
|
-
// the old segment can still GROW (external teammates keep appending to it),
|
|
250
|
-
// which changes the key and forces a rescan.
|
|
251
|
-
const PREV_SCAN_TTL_MS = 60 * 1000;
|
|
252
|
-
let _prevScanCache = null; // { key, promise, expiresAt }
|
|
253
|
-
|
|
254
|
-
function collectPrevSegmentCached(prev, predicate, opts) {
|
|
255
|
-
let st = null;
|
|
256
|
-
try { st = statSync(prev); } catch { }
|
|
257
|
-
const key = `${prev}|${st ? st.size : 0}|${st ? st.mtimeMs : 0}`;
|
|
258
|
-
const now = Date.now();
|
|
259
|
-
if (_prevScanCache && _prevScanCache.key === key && _prevScanCache.expiresAt > now) {
|
|
260
|
-
return _prevScanCache.promise;
|
|
293
|
+
// wire-v2 S6a: upload a v2-session zip (produced by `?format=raw` download),
|
|
294
|
+
// extract it to a temp dir, synthesize + stream it back as v1-shaped `\n---\n`
|
|
295
|
+
// raw entries for ephemeral viewing, then delete the temp dir. Nothing is
|
|
296
|
+
// written into LOG_DIR — view-only, parity with the legacy client-side `.jsonl`
|
|
297
|
+
// upload. Raw `application/zip` body (no multipart); uses its own MAX_UPLOAD
|
|
298
|
+
// cap, NOT deps.MAX_POST_BODY (which is ~10MB and would reject any real zip).
|
|
299
|
+
function uploadLogZip(req, res) {
|
|
300
|
+
const contentLength = parseInt(req.headers['content-length'] || '0', 10);
|
|
301
|
+
if (contentLength > MAX_UPLOAD) {
|
|
302
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
303
|
+
res.end(JSON.stringify({ error: 'File too large', code: 'TOO_LARGE' }));
|
|
304
|
+
return;
|
|
261
305
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
* registry — the sentinel may sit outside the client's load window)
|
|
277
|
-
* lines: one backfill entry per line (teammate-like, renderable only)
|
|
278
|
-
* last: { done: true, truncated, prevSegment }
|
|
279
|
-
*/
|
|
280
|
-
async function prevSegmentTeammates(req, res, parsedUrl, isLocal, deps) {
|
|
281
|
-
res.writeHead(200, { 'Content-Type': 'application/x-ndjson' });
|
|
282
|
-
try {
|
|
283
|
-
const current = LOG_FILE;
|
|
284
|
-
const sentinel = readHeadRotationContext(current);
|
|
285
|
-
const names = new Map(sentinel?.teammateNames?.filter((p) => Array.isArray(p) && p[0]) || []);
|
|
286
|
-
for (const [prefix, name] of getAgentSpawnRegistrySnapshot()) names.set(prefix, name);
|
|
287
|
-
res.write(JSON.stringify({
|
|
288
|
-
rotationContext: sentinel ? { from: sentinel.from || null } : null,
|
|
289
|
-
teammateNames: [...names.entries()],
|
|
290
|
-
}) + '\n');
|
|
291
|
-
// No live file (workspace pre-select) or resume placeholder → context only.
|
|
292
|
-
if (!current || current.endsWith('_temp.jsonl')) {
|
|
293
|
-
res.end(JSON.stringify({ done: true, truncated: false, prevSegment: null }) + '\n');
|
|
306
|
+
let chunks = [];
|
|
307
|
+
let totalSize = 0;
|
|
308
|
+
let aborted = false;
|
|
309
|
+
// A large remote upload can reset mid-flight; without an 'error' listener the
|
|
310
|
+
// socket error would crash the process (unhandled 'error' event).
|
|
311
|
+
req.on('error', () => { aborted = true; chunks = []; });
|
|
312
|
+
req.on('data', (chunk) => {
|
|
313
|
+
if (aborted) return;
|
|
314
|
+
totalSize += chunk.length;
|
|
315
|
+
if (totalSize > MAX_UPLOAD) {
|
|
316
|
+
aborted = true;
|
|
317
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
318
|
+
res.end(JSON.stringify({ error: 'File too large', code: 'TOO_LARGE' }));
|
|
319
|
+
req.destroy();
|
|
294
320
|
return;
|
|
295
321
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
322
|
+
chunks.push(chunk);
|
|
323
|
+
});
|
|
324
|
+
req.on('end', async () => {
|
|
325
|
+
if (aborted) return;
|
|
326
|
+
let tmpDir = null;
|
|
327
|
+
try {
|
|
328
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'ccv-upload-'));
|
|
329
|
+
const buf = Buffer.concat(chunks);
|
|
330
|
+
chunks = []; // free the per-chunk copies; adm-zip holds `buf` from here
|
|
331
|
+
const sessionDir = extractV2Zip(buf, tmpDir);
|
|
332
|
+
res.writeHead(200, {
|
|
333
|
+
'Content-Type': 'application/octet-stream',
|
|
334
|
+
'Transfer-Encoding': 'chunked',
|
|
335
|
+
});
|
|
336
|
+
await streamRawEntriesAsync(sessionDir, (raw) => {
|
|
337
|
+
res.write(raw);
|
|
338
|
+
res.write('\n---\n');
|
|
339
|
+
});
|
|
340
|
+
res.end();
|
|
341
|
+
} catch (err) {
|
|
342
|
+
const status = err?.status || 500;
|
|
343
|
+
if (status >= 500) console.error('[api/upload-log-zip]', err);
|
|
344
|
+
if (!res.headersSent) {
|
|
345
|
+
const safeError = status >= 500 ? 'server_error' : (err?.message || 'error');
|
|
346
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
347
|
+
res.end(JSON.stringify({ error: safeError, code: err?.code || 'unknown' }));
|
|
348
|
+
} else {
|
|
349
|
+
res.end();
|
|
350
|
+
}
|
|
351
|
+
} finally {
|
|
352
|
+
// Always reclaim the temp dir. The adapter has finished all reads by the
|
|
353
|
+
// time the await settles (cold v2 reads are bounded), so this is race-free
|
|
354
|
+
// even when the client disconnected mid-stream.
|
|
355
|
+
if (tmpDir) { try { rmSync(tmpDir, { recursive: true, force: true }); } catch {} }
|
|
301
356
|
}
|
|
302
|
-
|
|
303
|
-
!entry.inProgress && !entry.isHeartbeat && !entry.isCountTokens
|
|
304
|
-
&& !entry.ccvRotationContext
|
|
305
|
-
&& !(entry._deltaFormat && entry.mainAgent && !entry.teammate)
|
|
306
|
-
&& !!entry.timestamp
|
|
307
|
-
&& Array.isArray(entry.response?.body?.content) && entry.response.body.content.length > 0
|
|
308
|
-
&& isTeammateLikeEntry(entry)
|
|
309
|
-
), { rawPrefilter: teammateRawPrefilter });
|
|
310
|
-
for (const entry of entries) res.write(JSON.stringify(entry) + '\n');
|
|
311
|
-
res.end(JSON.stringify({ done: true, truncated, prevSegment: basename(prev) }) + '\n');
|
|
312
|
-
} catch (err) {
|
|
313
|
-
// Headers already sent — terminate the NDJSON stream with an error line.
|
|
314
|
-
try { res.end(JSON.stringify({ done: true, error: err?.message || 'internal error' }) + '\n'); } catch { }
|
|
315
|
-
}
|
|
357
|
+
});
|
|
316
358
|
}
|
|
317
359
|
|
|
318
360
|
export const logsRoutes = [
|
|
319
361
|
{ method: 'GET', match: 'exact', path: '/api/local-logs', handler: localLogs },
|
|
362
|
+
{ method: 'GET', match: 'exact', path: '/api/wire-v2-convert', handler: getWireV2Convert },
|
|
363
|
+
{ method: 'POST', match: 'exact', path: '/api/wire-v2-convert', handler: postWireV2Convert },
|
|
320
364
|
{ method: 'GET', match: 'exact', path: '/api/download-log', handler: downloadLog },
|
|
321
365
|
{ method: 'GET', match: 'exact', path: '/api/local-log', handler: localLog },
|
|
322
366
|
{ method: 'POST', match: 'exact', path: '/api/delete-logs', handler: deleteLogs },
|
|
323
|
-
{ method: 'POST', match: 'exact', path: '/api/
|
|
324
|
-
{ method: 'POST', match: 'exact', path: '/api/archive-logs', handler: archiveLogs },
|
|
325
|
-
{ method: 'GET', match: 'exact', path: '/api/prev-segment-teammates', handler: prevSegmentTeammates },
|
|
367
|
+
{ method: 'POST', match: 'exact', path: '/api/upload-log-zip', handler: uploadLogZip },
|
|
326
368
|
];
|
|
@@ -58,7 +58,6 @@ function preferencesGet(req, res, parsedUrl, isLocal, deps) {
|
|
|
58
58
|
prefs.logDir = LOG_DIR; // 始终返回当前运行时的日志目录
|
|
59
59
|
// 日志设置出厂默认"继承":键缺失(从未设置过)才注入;显式关闭持久化的是 null(键存在),不覆盖。
|
|
60
60
|
// 虚拟默认 —— 仅注入回包不落盘(GET 不写文件),直接读 preferences.json 的代码看不到该默认。
|
|
61
|
-
if (!('resumeAutoChoice' in prefs)) prefs.resumeAutoChoice = 'continue';
|
|
62
61
|
// home-friendly 展示形态:设了 CLAUDE_CONFIG_DIR 的用户看到真实路径,默认用户看到 "~/.claude"
|
|
63
62
|
// join() 而非字符串拼接,避免 Windows 分隔符不匹配导致比较失败
|
|
64
63
|
const _cDir = getClaudeConfigDir();
|
|
@@ -168,8 +167,6 @@ function preferencesPost(req, res, parsedUrl, isLocal, deps) {
|
|
|
168
167
|
delete prefs.prefsByProject; // fork blob 绝不回显(与 GET 一致)
|
|
169
168
|
stripImConfigs(prefs);
|
|
170
169
|
prefs.logDir = LOG_DIR;
|
|
171
|
-
// 与 GET 一致:回显里补齐 resumeAutoChoice 虚拟默认(已在上方落盘,文件不含该默认值)
|
|
172
|
-
if (!('resumeAutoChoice' in prefs)) prefs.resumeAutoChoice = 'continue';
|
|
173
170
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
174
171
|
res.end(JSON.stringify(prefs));
|
|
175
172
|
} catch (err) {
|
|
@@ -22,10 +22,9 @@ export function getInstallMethod({
|
|
|
22
22
|
return 'npm';
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function projectName(req, res
|
|
25
|
+
function projectName(req, res) {
|
|
26
26
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
27
|
-
|
|
28
|
-
res.end(JSON.stringify({ projectName: _projectName || '', instanceId: (deps && deps.instanceId) || null }));
|
|
27
|
+
res.end(JSON.stringify({ projectName: _projectName || '' }));
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
function projectDir(req, res) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Server-side "current session" pin — replaces the old per-browser localStorage pin so a
|
|
2
|
-
// project's
|
|
3
|
-
//
|
|
4
|
-
// - GET /api/session-pin → { pinnedSessionId
|
|
2
|
+
// project's current-session pointer is shared across every device hitting THIS ccv process.
|
|
3
|
+
// View state only → no loopback gating.
|
|
4
|
+
// - GET /api/session-pin → { pinnedSessionId }
|
|
5
5
|
// - POST /api/session-pin {pinnedSessionId: string|null} → persist + SSE-broadcast
|
|
6
6
|
// `_logDir` is the live per-project dir ('' = no active project → GET null / POST no-op).
|
|
7
7
|
import { _logDir } from '../interceptor.js';
|
|
@@ -13,9 +13,8 @@ function sendJson(res, code, obj) {
|
|
|
13
13
|
res.end(JSON.stringify(obj));
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function getHandler(req, res
|
|
17
|
-
|
|
18
|
-
sendJson(res, 200, { pinnedSessionId: readPin(_logDir, instanceId), instanceId });
|
|
16
|
+
function getHandler(req, res) {
|
|
17
|
+
sendJson(res, 200, { pinnedSessionId: readPin(_logDir) });
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
function postHandler(req, res, parsedUrl, isLocal, deps) {
|
|
@@ -27,8 +26,8 @@ function postHandler(req, res, parsedUrl, isLocal, deps) {
|
|
|
27
26
|
catch { return sendJson(res, 400, { error: 'Invalid JSON' }); }
|
|
28
27
|
const raw = incoming.pinnedSessionId;
|
|
29
28
|
const pinnedSessionId = (typeof raw === 'string' && raw) ? raw : null;
|
|
30
|
-
writePin(_logDir,
|
|
31
|
-
// Broadcast to THIS process's SSE clients so every device
|
|
29
|
+
writePin(_logDir, pinnedSessionId);
|
|
30
|
+
// Broadcast to THIS process's SSE clients so every device stays in sync.
|
|
32
31
|
sendEventToClients(deps.clients, 'session_pin', { pinnedSessionId });
|
|
33
32
|
sendJson(res, 200, { ok: true, pinnedSessionId });
|
|
34
33
|
});
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
// Workspace routes (moved verbatim from server.js handleRequest).
|
|
2
2
|
import { existsSync, statSync } from 'node:fs';
|
|
3
3
|
import { basename } from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { initForWorkspace, resetWorkspace, getLiveLogSource, markContinuedLaunch, markForkSession, markResumeSession, isContinuedLaunch } from '../interceptor.js';
|
|
5
|
+
import { LOG_DIR } from '../../findcc.js';
|
|
6
|
+
import { migrationStatus } from '../lib/v2/migrate-prompt.js';
|
|
7
|
+
import { reportSwallowed } from '../lib/error-report.js';
|
|
8
|
+
|
|
9
|
+
// P2: claude continuation flags — the workspace launcher injects `-c` itself
|
|
10
|
+
// (WorkspaceList's logCount heuristic), so argv scanning in cli.js never sees
|
|
11
|
+
// it; this is detection channel ② of interceptor.isContinuedLaunch().
|
|
12
|
+
const CONTINUE_FLAGS = new Set(['-c', '--continue', '-r', '--resume']);
|
|
13
|
+
// Explicit resume flags — continued for the migrate prompt, but `-c` folder
|
|
14
|
+
// adoption must NOT fire (it targets the latest main session, not the user's
|
|
15
|
+
// chosen one). Mirrors cli.js's CCV_CLAUDE_RESUME channel.
|
|
16
|
+
const RESUME_FLAGS = new Set(['-r', '--resume']);
|
|
6
17
|
import { unwatchAllWorkflows } from '../lib/workflow-watcher.js';
|
|
7
18
|
import { readClaudeProjectModel } from '../lib/context-watcher.js';
|
|
8
19
|
import { countLogEntries, streamRawEntriesAsync } from '../lib/log-stream.js';
|
|
@@ -49,18 +60,28 @@ function workspacesLaunch(req, res, parsedUrl, isLocal, deps) {
|
|
|
49
60
|
const result = initForWorkspace(wsPath);
|
|
50
61
|
process.env.CCV_PROJECT_DIR = wsPath;
|
|
51
62
|
|
|
52
|
-
//
|
|
53
|
-
|
|
63
|
+
// 启动日志监听(S6b: v2 live feed when the v2 writer is active)
|
|
64
|
+
deps.startLogWatch();
|
|
54
65
|
|
|
55
66
|
// 启动 stats worker(如果尚未启动)
|
|
56
67
|
if (!deps.statsWorker) deps.startStatsWorker();
|
|
57
68
|
deps.startStreamingStatusTimer();
|
|
58
69
|
|
|
70
|
+
// P2 检测通道②:workspace 启动器注入的 -c/--continue/-r/--resume(无论
|
|
71
|
+
// 是否走 PTY 分支都要打标——迁移提示的 continued 语义只取决于参数本身)。
|
|
72
|
+
const mergedArgs = [...deps.workspaceClaudeArgs, ...(Array.isArray(launchExtraArgs) ? launchExtraArgs : [])];
|
|
73
|
+
if (mergedArgs.some((a) => CONTINUE_FLAGS.has(a))) markContinuedLaunch();
|
|
74
|
+
// `--fork-session`: a continuation that mints a NEW session id on purpose —
|
|
75
|
+
// `-c` folder adoption must NOT fire for it.
|
|
76
|
+
if (mergedArgs.includes('--fork-session')) markForkSession();
|
|
77
|
+
// `-r`/`--resume`: user-chosen target session — adoption must not redirect
|
|
78
|
+
// it to the latest main session.
|
|
79
|
+
if (mergedArgs.some((a) => RESUME_FLAGS.has(a))) markResumeSession();
|
|
80
|
+
|
|
59
81
|
// 启动 PTY
|
|
60
82
|
const proxyPort = process.env.CCV_PROXY_PORT;
|
|
61
83
|
if (proxyPort) {
|
|
62
84
|
const { spawnClaude } = await import('../pty-manager.js');
|
|
63
|
-
const mergedArgs = [...deps.workspaceClaudeArgs, ...(Array.isArray(launchExtraArgs) ? launchExtraArgs : [])];
|
|
64
85
|
await spawnClaude(parseInt(proxyPort), wsPath, mergedArgs, deps.workspaceClaudePath, deps.workspaceIsNpmVersion, deps.actualPort, deps.protocol, deps.INTERNAL_TOKEN);
|
|
65
86
|
}
|
|
66
87
|
|
|
@@ -75,11 +96,15 @@ function workspacesLaunch(req, res, parsedUrl, isLocal, deps) {
|
|
|
75
96
|
});
|
|
76
97
|
|
|
77
98
|
// 流式分段广播以刷新会话区域,避免全量加载 OOM
|
|
78
|
-
|
|
99
|
+
// S6b: the live source is the v2 session dir when the v2 writer is
|
|
100
|
+
// active (a fresh workspace has no session yet → empty stream, the live
|
|
101
|
+
// feed picks up from the first request).
|
|
102
|
+
const wsReloadSource = getLiveLogSource();
|
|
103
|
+
const wsReloadTotal = await countLogEntries(wsReloadSource);
|
|
79
104
|
deps.clients.forEach(client => {
|
|
80
105
|
try { client.write(`event: load_start\ndata: ${JSON.stringify({ total: wsReloadTotal, incremental: false })}\n\n`); } catch {}
|
|
81
106
|
});
|
|
82
|
-
await streamRawEntriesAsync(
|
|
107
|
+
await streamRawEntriesAsync(wsReloadSource, (raw) => {
|
|
83
108
|
deps.clients.forEach(client => {
|
|
84
109
|
try { client.write('event: load_chunk\ndata: ['); client.write(raw.replace(/\n/g, '')); client.write(']\n\n'); } catch {}
|
|
85
110
|
});
|
|
@@ -88,6 +113,16 @@ function workspacesLaunch(req, res, parsedUrl, isLocal, deps) {
|
|
|
88
113
|
try { client.write(`event: load_end\ndata: {}\n\n`); } catch {}
|
|
89
114
|
});
|
|
90
115
|
|
|
116
|
+
// 1.7.0 迁移引导(P2):切进的项目仍有未转换 v1 日志 → 对存量连接广播
|
|
117
|
+
// migrate_prompt(新连接由 /events 的连接帧覆盖)。
|
|
118
|
+
try {
|
|
119
|
+
const mig = migrationStatus(LOG_DIR, result.projectName || '');
|
|
120
|
+
if (mig.pending) {
|
|
121
|
+
const frame = `event: migrate_prompt\ndata: ${JSON.stringify({ ...mig, continued: isContinuedLaunch() })}\n\n`;
|
|
122
|
+
deps.clients.forEach(client => { try { client.write(frame); } catch {} });
|
|
123
|
+
}
|
|
124
|
+
} catch (e) { reportSwallowed('sse.migrate_prompt', e); }
|
|
125
|
+
|
|
91
126
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
92
127
|
res.end(JSON.stringify({ ok: true, projectName: result.projectName }));
|
|
93
128
|
} catch (err) {
|
|
@@ -139,8 +174,8 @@ function workspacesStop(req, res, parsedUrl, isLocal, deps) {
|
|
|
139
174
|
]).then(() => {
|
|
140
175
|
// 接续原有清理流程
|
|
141
176
|
|
|
142
|
-
//
|
|
143
|
-
|
|
177
|
+
// 停止日志监听(v1 tail + v2 live feed)
|
|
178
|
+
deps.stopLogWatch();
|
|
144
179
|
unwatchAllWorkflows();
|
|
145
180
|
|
|
146
181
|
// 重置 interceptor 状态
|