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
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// Passive registry of instance ids (`--pid`) used per project — a memory aid only.
|
|
2
|
-
//
|
|
3
|
-
// On startup with `--pid X`, the server appends X to `{projectDir}/.instances.json` so the
|
|
4
|
-
// CLI banner can remind the user which ids they've used ("history: alpha, beta") next time.
|
|
5
|
-
// This is NOT a liveness / auto-numbering registry: entries are never probed or auto-assigned,
|
|
6
|
-
// just remembered (deduped, most-recent-last, capped). `logDir` is the per-project dir;
|
|
7
|
-
// '' (no active project) → record no-ops / list returns [].
|
|
8
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
9
|
-
import { join } from 'node:path';
|
|
10
|
-
import { randomBytes } from 'node:crypto';
|
|
11
|
-
import { renameSyncWithRetry } from './file-api.js';
|
|
12
|
-
import { withFileLockAsync } from './async-file-lock.js';
|
|
13
|
-
import { sanitizeInstanceId } from './session-pin-store.js';
|
|
14
|
-
|
|
15
|
-
const MAX_INSTANCES = 50; // cap the list; oldest entries drop off (memory aid, not an audit log)
|
|
16
|
-
|
|
17
|
-
function instancesFile(logDir) { return join(logDir, '.instances.json'); }
|
|
18
|
-
|
|
19
|
-
function readListRaw(logDir) {
|
|
20
|
-
try {
|
|
21
|
-
const file = instancesFile(logDir);
|
|
22
|
-
if (!existsSync(file)) return [];
|
|
23
|
-
const arr = JSON.parse(readFileSync(file, 'utf-8'));
|
|
24
|
-
return Array.isArray(arr) ? arr.filter((x) => typeof x === 'string' && x) : [];
|
|
25
|
-
} catch { return []; }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Previously-used instance ids for this project, oldest → newest. No project → []. */
|
|
29
|
-
export function listInstances(logDir) {
|
|
30
|
-
if (!logDir) return [];
|
|
31
|
-
return readListRaw(logDir);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Record an instance id: dedupe (move-to-end so "most recent" wins), cap at MAX_INSTANCES.
|
|
36
|
-
* Cross-process safe via the shared async file lock + atomic tmp→rename. No project /
|
|
37
|
-
* empty id → no-op.
|
|
38
|
-
*/
|
|
39
|
-
export async function recordInstance(logDir, instanceId) {
|
|
40
|
-
if (!logDir) return;
|
|
41
|
-
const id = sanitizeInstanceId(instanceId);
|
|
42
|
-
if (!id) return;
|
|
43
|
-
await withFileLockAsync(join(logDir, '.instances.lock'), () => {
|
|
44
|
-
const list = readListRaw(logDir).filter((x) => x !== id);
|
|
45
|
-
list.push(id);
|
|
46
|
-
while (list.length > MAX_INSTANCES) list.shift();
|
|
47
|
-
const file = instancesFile(logDir);
|
|
48
|
-
const tmp = `${file}.tmp-${process.pid}-${randomBytes(4).toString('hex')}`;
|
|
49
|
-
try {
|
|
50
|
-
writeFileSync(tmp, JSON.stringify(list));
|
|
51
|
-
renameSyncWithRetry(tmp, file);
|
|
52
|
-
} catch (err) {
|
|
53
|
-
try { unlinkSync(tmp); } catch {}
|
|
54
|
-
throw err;
|
|
55
|
-
}
|
|
56
|
-
}, { ensureDir: logDir });
|
|
57
|
-
}
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
existsSync, statSync, lstatSync, mkdirSync, renameSync, unlinkSync,
|
|
3
|
-
writeFileSync, readFileSync, readdirSync, rmSync,
|
|
4
|
-
} from 'node:fs';
|
|
5
|
-
import { join, basename } from 'node:path';
|
|
6
|
-
import { tmpdir } from 'node:os';
|
|
7
|
-
import { createHash } from 'node:crypto';
|
|
8
|
-
import { createRequire } from 'node:module';
|
|
9
|
-
import { validateZipEntries } from './zip-safety.js';
|
|
10
|
-
|
|
11
|
-
const require = createRequire(import.meta.url);
|
|
12
|
-
const AdmZip = require('adm-zip');
|
|
13
|
-
|
|
14
|
-
const CACHE_ROOT_NAME = 'ccv-extract';
|
|
15
|
-
const RENAME_RETRY = 3;
|
|
16
|
-
const RENAME_DELAY_MS = 50;
|
|
17
|
-
const CACHE_TTL_MS = 7 * 24 * 3600 * 1000;
|
|
18
|
-
// 与 server/lib/log-management.js 的 MAX_MERGE_SIZE 对齐:merge 上限 400MB,归档 / 读回 zip
|
|
19
|
-
// 必须容纳这一规模,否则自家产物会被 zip-safety 默认 50MB 阈值拦住造成永久不可读。
|
|
20
|
-
const ARCHIVE_MAX_BYTES = 400 * 1024 * 1024;
|
|
21
|
-
|
|
22
|
-
function syncSleep(ms) {
|
|
23
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function renameWithRetry(from, to) {
|
|
27
|
-
let lastErr;
|
|
28
|
-
for (let i = 0; i < RENAME_RETRY; i++) {
|
|
29
|
-
try { renameSync(from, to); return; }
|
|
30
|
-
catch (err) {
|
|
31
|
-
lastErr = err;
|
|
32
|
-
if (i < RENAME_RETRY - 1 && (err.code === 'EBUSY' || err.code === 'EPERM' || err.code === 'EACCES')) {
|
|
33
|
-
syncSleep(RENAME_DELAY_MS);
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
throw err;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
throw lastErr;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function getExtractCacheRoot() {
|
|
43
|
-
return join(tmpdir(), CACHE_ROOT_NAME);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function pathHash(absPath) {
|
|
47
|
-
return createHash('sha1').update(absPath).digest('hex').slice(0, 16);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function archiveJsonl(absJsonlPath) {
|
|
51
|
-
if (!absJsonlPath.endsWith('.jsonl')) return { ok: false, error: 'Not a .jsonl file' };
|
|
52
|
-
if (!existsSync(absJsonlPath)) return { ok: false, error: 'Source file not found' };
|
|
53
|
-
|
|
54
|
-
// 拒绝 symlink 源:避免归档目标外文件并删除链接造成信息错乱
|
|
55
|
-
try {
|
|
56
|
-
const lst = lstatSync(absJsonlPath);
|
|
57
|
-
if (!lst.isFile()) return { ok: false, error: 'Source is not a regular file' };
|
|
58
|
-
} catch (err) {
|
|
59
|
-
return { ok: false, error: err.message };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const zipPath = absJsonlPath + '.zip';
|
|
63
|
-
if (existsSync(zipPath)) {
|
|
64
|
-
return { ok: false, skipped: 'target-exists', error: 'Target .zip already exists' };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const tmpPath = `${zipPath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
68
|
-
try {
|
|
69
|
-
const zip = new AdmZip();
|
|
70
|
-
zip.addLocalFile(absJsonlPath);
|
|
71
|
-
for (const e of zip.getEntries()) {
|
|
72
|
-
if (e.header) e.header.flags = (e.header.flags || 0) | 0x800;
|
|
73
|
-
}
|
|
74
|
-
zip.writeZip(tmpPath);
|
|
75
|
-
|
|
76
|
-
if (!existsSync(tmpPath) || statSync(tmpPath).size === 0) {
|
|
77
|
-
throw new Error('Generated zip is empty');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
renameWithRetry(tmpPath, zipPath);
|
|
81
|
-
try { unlinkSync(absJsonlPath); }
|
|
82
|
-
catch (err) {
|
|
83
|
-
// 罕见:zip 已 rename 成功但原 .jsonl 删除失败(AV 扫描 / Windows 句柄占用 / 权限)。
|
|
84
|
-
// 此时两份共存,下次列表 regex 会同时显示,再次归档会撞"Target exists"死锁。
|
|
85
|
-
// 回滚策略:删掉刚 rename 出来的 zip,原文件原位保留,调用方收到 failed → 用户可重试。
|
|
86
|
-
try { unlinkSync(zipPath); } catch { /* zip rollback 失败也容忍:极端边界 */ }
|
|
87
|
-
return { ok: false, error: `archived but failed to delete source: ${err.message}` };
|
|
88
|
-
}
|
|
89
|
-
return { ok: true, zipPath };
|
|
90
|
-
} catch (err) {
|
|
91
|
-
try { if (existsSync(tmpPath)) unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
92
|
-
return { ok: false, error: err.message };
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function resolveJsonlPath(absPath) {
|
|
97
|
-
if (typeof absPath !== 'string') return absPath;
|
|
98
|
-
if (absPath.endsWith('.jsonl') && existsSync(absPath)) return absPath;
|
|
99
|
-
|
|
100
|
-
let zipPath = null;
|
|
101
|
-
if (absPath.endsWith('.jsonl.zip')) zipPath = absPath;
|
|
102
|
-
else if (absPath.endsWith('.jsonl')) {
|
|
103
|
-
const candidate = absPath + '.zip';
|
|
104
|
-
if (existsSync(candidate)) zipPath = candidate;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (!zipPath || !existsSync(zipPath)) return absPath;
|
|
108
|
-
return extractZipSync(zipPath);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function extractZipSync(zipPath) {
|
|
112
|
-
const cacheKey = pathHash(zipPath);
|
|
113
|
-
const cacheDir = join(getExtractCacheRoot(), cacheKey);
|
|
114
|
-
const targetName = basename(zipPath, '.zip');
|
|
115
|
-
const tmpFile = join(cacheDir, targetName);
|
|
116
|
-
const sidecar = join(cacheDir, '.meta.json');
|
|
117
|
-
|
|
118
|
-
const zipStat = statSync(zipPath);
|
|
119
|
-
const expectedMeta = { srcMtimeMs: zipStat.mtimeMs, srcSize: zipStat.size };
|
|
120
|
-
|
|
121
|
-
if (existsSync(tmpFile) && existsSync(sidecar)) {
|
|
122
|
-
try {
|
|
123
|
-
const meta = JSON.parse(readFileSync(sidecar, 'utf-8'));
|
|
124
|
-
if (meta.srcMtimeMs === expectedMeta.srcMtimeMs && meta.srcSize === expectedMeta.srcSize) {
|
|
125
|
-
return tmpFile;
|
|
126
|
-
}
|
|
127
|
-
} catch { /* fall through to re-extract */ }
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
mkdirSync(cacheDir, { recursive: true });
|
|
131
|
-
const zip = new AdmZip(zipPath);
|
|
132
|
-
const entries = zip.getEntries();
|
|
133
|
-
|
|
134
|
-
validateZipEntries(entries, cacheDir, {
|
|
135
|
-
maxEntries: 5,
|
|
136
|
-
requireExtension: '.jsonl',
|
|
137
|
-
maxPerFile: ARCHIVE_MAX_BYTES,
|
|
138
|
-
maxTotal: ARCHIVE_MAX_BYTES,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const fileEntry = entries.find(e => !e.isDirectory && e.entryName.toLowerCase().endsWith('.jsonl'));
|
|
142
|
-
if (!fileEntry) {
|
|
143
|
-
throw Object.assign(new Error('No .jsonl entry found in zip'), { code: 'ZIP_UNSAFE' });
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const partial = `${tmpFile}.partial.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
147
|
-
try {
|
|
148
|
-
writeFileSync(partial, fileEntry.getData());
|
|
149
|
-
renameWithRetry(partial, tmpFile);
|
|
150
|
-
writeFileSync(sidecar, JSON.stringify(expectedMeta));
|
|
151
|
-
} catch (err) {
|
|
152
|
-
try { if (existsSync(partial)) unlinkSync(partial); } catch { /* ignore */ }
|
|
153
|
-
throw err;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return tmpFile;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function cleanupExtractCache() {
|
|
160
|
-
try {
|
|
161
|
-
const root = getExtractCacheRoot();
|
|
162
|
-
if (!existsSync(root)) return;
|
|
163
|
-
const now = Date.now();
|
|
164
|
-
for (const ent of readdirSync(root, { withFileTypes: true })) {
|
|
165
|
-
if (!ent.isDirectory()) continue;
|
|
166
|
-
const subDir = join(root, ent.name);
|
|
167
|
-
try {
|
|
168
|
-
const stat = statSync(subDir);
|
|
169
|
-
// 用 mtimeMs(解压完成时间)而非 atimeMs:macOS / Linux 默认 noatime/relatime
|
|
170
|
-
// 下 atime 几乎不更新,会让 cache 永不过期或被错误提前回收。mtime 由 mkdirSync /
|
|
171
|
-
// 重新解压 rename 触发,能准确反映"最近一次解压时间"。
|
|
172
|
-
if (now - stat.mtimeMs > CACHE_TTL_MS) {
|
|
173
|
-
rmSync(subDir, { recursive: true, force: true });
|
|
174
|
-
}
|
|
175
|
-
} catch { /* tolerate one bad entry */ }
|
|
176
|
-
}
|
|
177
|
-
} catch (err) {
|
|
178
|
-
console.warn('[ccv-extract] cleanup failed:', err.message);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { getSystemText, TEAMMATE_SYSTEM_RE } from './interceptor-core.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Server-side teammate detection for the previous-segment backfill filter
|
|
5
|
-
* (/api/prev-segment-teammates). Mirrors the client's three teammate shapes:
|
|
6
|
-
*
|
|
7
|
-
* 1. External-process teammate — interceptor persisted `entry.teammate`
|
|
8
|
-
* (--agent-name / --parent-session-id spawn).
|
|
9
|
-
* 2. Proxy-mode teammate — system prompt carries the team-communication
|
|
10
|
-
* marker (TEAMMATE_SYSTEM_RE, shared with interceptor-core.js).
|
|
11
|
-
* 3. Native (same-process Agent-tool) teammate — SDK agent system prompt
|
|
12
|
-
* ("You are a Claude agent", NOT "You are Claude Code") plus a
|
|
13
|
-
* SendMessage tool; plain subagents are never granted SendMessage.
|
|
14
|
-
*
|
|
15
|
-
* KEEP IN SYNC with src/utils/teammateDetector.js (native rule) and
|
|
16
|
-
* src/utils/contentFilter.js (TEAMMATE_SYSTEM_RE). Drift is pinned by the
|
|
17
|
-
* parity cases in test/teammate-detect.test.js (pattern follows
|
|
18
|
-
* test/interceptor-core-mainagent.test.js).
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
// Mirror of src/utils/teammateDetector.js NATIVE_TEAMMATE_RE.
|
|
22
|
-
const NATIVE_TEAMMATE_RE = /You are a Claude agent/i;
|
|
23
|
-
|
|
24
|
-
export function isTeammateLikeEntry(entry) {
|
|
25
|
-
if (!entry || !entry.body) return false;
|
|
26
|
-
if (entry.teammate) return true;
|
|
27
|
-
const sysText = getSystemText(entry.body);
|
|
28
|
-
if (TEAMMATE_SYSTEM_RE.test(sysText)) return true;
|
|
29
|
-
if (!NATIVE_TEAMMATE_RE.test(sysText)) return false;
|
|
30
|
-
const tools = entry.body.tools;
|
|
31
|
-
return Array.isArray(tools) && tools.some((t) => t && t.name === 'SendMessage');
|
|
32
|
-
}
|
package/server/lib/zip-safety.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { resolve, sep, posix } from 'node:path';
|
|
2
|
-
|
|
3
|
-
const MAX_PER_FILE_DEFAULT = 50 * 1024 * 1024;
|
|
4
|
-
const MAX_TOTAL_DEFAULT = 200 * 1024 * 1024;
|
|
5
|
-
|
|
6
|
-
function isSymlinkEntry(entry) {
|
|
7
|
-
const unixMode = (entry.attr >>> 16) & 0xffff;
|
|
8
|
-
return (unixMode & 0o170000) === 0o120000;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function toPosix(name) {
|
|
12
|
-
return name.split('\\').join('/');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function isSafeEntryName(name) {
|
|
16
|
-
if (!name || typeof name !== 'string') return false;
|
|
17
|
-
if (name.includes('\x00')) return false;
|
|
18
|
-
const slashed = toPosix(name);
|
|
19
|
-
if (slashed.startsWith('/')) return false;
|
|
20
|
-
if (/^[A-Za-z]:/.test(slashed)) return false;
|
|
21
|
-
const norm = posix.normalize(slashed);
|
|
22
|
-
if (norm === '..' || norm.startsWith('../') || norm.includes('/../')) return false;
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function isWithinTargetDir(entryName, targetDir) {
|
|
27
|
-
const target = resolve(targetDir);
|
|
28
|
-
const dest = resolve(target, toPosix(entryName));
|
|
29
|
-
return dest === target || dest.startsWith(target + sep);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function validateZipEntries(entries, targetDir, opts = {}) {
|
|
33
|
-
const {
|
|
34
|
-
maxEntries = Infinity,
|
|
35
|
-
maxPerFile = MAX_PER_FILE_DEFAULT,
|
|
36
|
-
maxTotal = MAX_TOTAL_DEFAULT,
|
|
37
|
-
requireExtension = null,
|
|
38
|
-
} = opts;
|
|
39
|
-
let fileCount = 0;
|
|
40
|
-
let totalSize = 0;
|
|
41
|
-
for (const e of entries) {
|
|
42
|
-
if (e.isDirectory) continue;
|
|
43
|
-
fileCount++;
|
|
44
|
-
if (fileCount > maxEntries) {
|
|
45
|
-
throw Object.assign(new Error('Too many entries in zip'), { code: 'ZIP_TOO_MANY' });
|
|
46
|
-
}
|
|
47
|
-
if (isSymlinkEntry(e)) {
|
|
48
|
-
throw Object.assign(new Error('Symlinks not allowed in zip'), { code: 'ZIP_UNSAFE' });
|
|
49
|
-
}
|
|
50
|
-
if (!isSafeEntryName(e.entryName)) {
|
|
51
|
-
throw Object.assign(new Error(`Unsafe zip entry name: ${e.entryName}`), { code: 'ZIP_UNSAFE' });
|
|
52
|
-
}
|
|
53
|
-
if (!isWithinTargetDir(e.entryName, targetDir)) {
|
|
54
|
-
throw Object.assign(new Error(`Zip entry escapes target dir: ${e.entryName}`), { code: 'ZIP_UNSAFE' });
|
|
55
|
-
}
|
|
56
|
-
if (requireExtension && !e.entryName.toLowerCase().endsWith(requireExtension.toLowerCase())) {
|
|
57
|
-
throw Object.assign(new Error(`Disallowed extension: ${e.entryName}`), { code: 'ZIP_UNSAFE' });
|
|
58
|
-
}
|
|
59
|
-
const sizeRaw = e.header?.size || 0;
|
|
60
|
-
if (sizeRaw > maxPerFile) {
|
|
61
|
-
throw Object.assign(new Error('File too large in archive'), { code: 'ZIP_BOMB' });
|
|
62
|
-
}
|
|
63
|
-
totalSize += sizeRaw;
|
|
64
|
-
if (totalSize > maxTotal) {
|
|
65
|
-
throw Object.assign(new Error('Archive expands too large'), { code: 'ZIP_BOMB' });
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|