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
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// Wire Format v2 — S4 dual-write consistency verifier (plan gate methodology).
|
|
2
|
+
//
|
|
3
|
+
// Compares one v1 JSONL file against the v2 session dirs of the same project,
|
|
4
|
+
// per request, on WIRE TRUTH:
|
|
5
|
+
// ① per-request wire-messages digest: v1 incremental delta reconstruction vs
|
|
6
|
+
// v2 mechanical replay (this is trajectory-grade — every intermediate
|
|
7
|
+
// state is compared, not just the cold final one);
|
|
8
|
+
// ② per-request tools/system content hash: v1 inline value hashed with the
|
|
9
|
+
// BlobStore formula vs the v2 journal's CAS ref (byte-equality by CAS);
|
|
10
|
+
// ③ teammate entries verified the same per-request way (v1's leader file
|
|
11
|
+
// holds them inline; v2 holds them in the teammate's own session dir —
|
|
12
|
+
// matching is by `${ts}|${url}`, so no re-join ordering is needed here);
|
|
13
|
+
// ④ inflight/error accounting: v1 keys that never completed vs v2
|
|
14
|
+
// req-without-done;
|
|
15
|
+
// ⑤ journal msgTo vs replayed length integrity (internal v2 consistency).
|
|
16
|
+
//
|
|
17
|
+
// v1-only requests are counted, not failed: history from before dual-write was
|
|
18
|
+
// enabled (or while it was toggled off) legitimately has no v2 twin.
|
|
19
|
+
//
|
|
20
|
+
// KNOWN LIMITATION (review): the ① digest is built on the client-shared
|
|
21
|
+
// messageFingerprint, which keys tool_result blocks on tool_use_id ONLY — a
|
|
22
|
+
// divergence confined to tool_result BODY content between v1 and v2 collides
|
|
23
|
+
// to identical digests and is invisible to this gate. The write side now uses
|
|
24
|
+
// the content-aware fingerprintMsg for its judgement (conversation-store.js),
|
|
25
|
+
// which removes the known producer of that divergence class; the residual risk
|
|
26
|
+
// is a v2 bug that corrupts tool_result content while preserving ids.
|
|
27
|
+
|
|
28
|
+
import { statSync } from 'node:fs';
|
|
29
|
+
import { dirname } from 'node:path';
|
|
30
|
+
import { iterateRawEntriesAsync } from '../log-stream.js';
|
|
31
|
+
import { createIncrementalReconstructor } from '../delta-reconstructor.js';
|
|
32
|
+
import { listSessionIds, readSession, indexSession, messagesDigest, blobRefOf } from './replay.js';
|
|
33
|
+
|
|
34
|
+
const MAX_REPORTED_DIFFS = 50;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Verify one v1 file against its project's v2 sessions.
|
|
38
|
+
* @param {string} v1File - absolute path to a v1 .jsonl (its dirname is the project dir)
|
|
39
|
+
* @param {object} [opts]
|
|
40
|
+
* @param {string} [opts.projectDir] - override project dir (defaults to dirname(v1File))
|
|
41
|
+
* @param {string} [opts.sessionsDirName] - session-dir name under the project
|
|
42
|
+
* dir; the offline converter verifies its staging area ('sessions-migrating')
|
|
43
|
+
* before promoting. Defaults to the live 'sessions'.
|
|
44
|
+
* @param {(p: {entriesScanned: number}) => void} [opts.onProgress] - called
|
|
45
|
+
* every 1000 scanned v1 entries and once after the scan completes, so a
|
|
46
|
+
* long-running verify of one big file can surface live progress. Callback
|
|
47
|
+
* errors are swallowed — progress must never break the verification.
|
|
48
|
+
* @returns {Promise<object>} report (see shape at the bottom)
|
|
49
|
+
*/
|
|
50
|
+
export async function verifyV1File(v1File, opts = {}) {
|
|
51
|
+
const projectDir = opts.projectDir || dirname(v1File);
|
|
52
|
+
const sessionsDirName = opts.sessionsDirName || 'sessions';
|
|
53
|
+
const onProgress = typeof opts.onProgress === 'function' ? opts.onProgress : null;
|
|
54
|
+
|
|
55
|
+
// ---- v2 side: index every session of the project --------------------------
|
|
56
|
+
// `${ts}|${url}` is NOT unique in real logs (same-millisecond countTokens
|
|
57
|
+
// bursts, §3.6 rotation twins) — keep ALL records per key and match each v1
|
|
58
|
+
// entry to its best unmatched candidate below (real-data finding 2026-07-14).
|
|
59
|
+
const v2Index = new Map(); // key → rec[]
|
|
60
|
+
const integrity = [];
|
|
61
|
+
const v2Sessions = [];
|
|
62
|
+
const unsupportedSessions = []; // reader version gate (spec §14): a session
|
|
63
|
+
// this build can't read means the gate has a coverage hole — FAIL, don't skip.
|
|
64
|
+
for (const sid of listSessionIds(projectDir, sessionsDirName)) {
|
|
65
|
+
const session = readSession(projectDir, sid, sessionsDirName);
|
|
66
|
+
// Report the session IDENTITY (UUID from meta), not the dir name — the dir
|
|
67
|
+
// is `<ts>_<uuid>` post task C; falls back to the dir name on torn meta.
|
|
68
|
+
v2Sessions.push((session.meta && session.meta.sessionId) || sid);
|
|
69
|
+
if (session.unsupported) {
|
|
70
|
+
unsupportedSessions.push({ sessionId: sid, wireFormat: session.wireFormat });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const { index, integrity: viol } = indexSession(session);
|
|
74
|
+
integrity.push(...viol);
|
|
75
|
+
for (const [key, recs] of index) {
|
|
76
|
+
const arr = v2Index.get(key);
|
|
77
|
+
if (arr) arr.push(...recs); else v2Index.set(key, [...recs]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---- v1 side: stream, reconstruct incrementally, compare ------------------
|
|
82
|
+
const reconstructor = createIncrementalReconstructor();
|
|
83
|
+
const diffs = [];
|
|
84
|
+
const inflightV1 = new Set(); // keys seen only as inProgress
|
|
85
|
+
const counters = {
|
|
86
|
+
v1Completed: 0, v1Skipped: 0, matched: 0, v1Only: 0, v1DuplicateKey: 0,
|
|
87
|
+
heartbeatsV1: 0, countTokensV1: 0, teammateV1: 0, staleOrBroken: 0,
|
|
88
|
+
};
|
|
89
|
+
// Per-session suspicion tally, keyed by the v2 session dir name (the same
|
|
90
|
+
// value `listSessionIds` / the converter's promote loop iterate). UNCAPPED —
|
|
91
|
+
// unlike `diffs` (bounded by MAX_REPORTED_DIFFS for memory/reporting), this
|
|
92
|
+
// must hold EVERY suspect session so the converter can quarantine the full
|
|
93
|
+
// set; capping it would silently promote corrupt sessions past diff #50.
|
|
94
|
+
const suspectSessions = new Map(); // sessionId → { count, reasons: string[] }
|
|
95
|
+
const MAX_REASONS = 5;
|
|
96
|
+
const flagSession = (sessionId, reason) => {
|
|
97
|
+
if (!sessionId) return;
|
|
98
|
+
let rec = suspectSessions.get(sessionId);
|
|
99
|
+
if (!rec) suspectSessions.set(sessionId, rec = { count: 0, reasons: [] });
|
|
100
|
+
rec.count++;
|
|
101
|
+
if (rec.reasons.length < MAX_REASONS) rec.reasons.push(reason);
|
|
102
|
+
};
|
|
103
|
+
const addDiff = (d) => {
|
|
104
|
+
flagSession(d.sessionId, `${d.type}@seq${d.seq}`);
|
|
105
|
+
if (diffs.length < MAX_REPORTED_DIFFS) diffs.push(d);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const onScan = (raw) => {
|
|
109
|
+
let entry;
|
|
110
|
+
try { entry = JSON.parse(raw); } catch { return; }
|
|
111
|
+
if (entry.ccvRotationContext) return;
|
|
112
|
+
const key = `${entry.timestamp}|${entry.url}`;
|
|
113
|
+
if (entry.inProgress) { inflightV1.add(key); return; }
|
|
114
|
+
inflightV1.delete(key);
|
|
115
|
+
|
|
116
|
+
// Incremental reconstruction restores the full wire messages for delta
|
|
117
|
+
// entries (teammate/sub entries pass through untouched, already full).
|
|
118
|
+
reconstructor.reconstruct(entry);
|
|
119
|
+
if (entry._staleReorder || entry._reconstructBroken) { counters.staleOrBroken++; return; }
|
|
120
|
+
counters.v1Completed++;
|
|
121
|
+
if (entry.isHeartbeat) counters.heartbeatsV1++;
|
|
122
|
+
if (entry.isCountTokens) counters.countTokensV1++;
|
|
123
|
+
if (entry.teammate) counters.teammateV1++;
|
|
124
|
+
|
|
125
|
+
const candidates = v2Index.get(key);
|
|
126
|
+
if (!candidates) { counters.v1Only++; return; }
|
|
127
|
+
const v1Digest = entry.body && Array.isArray(entry.body.messages) ? messagesDigest(entry.body.messages) : null;
|
|
128
|
+
const unmatched = candidates.filter(c => !c._matched);
|
|
129
|
+
if (unmatched.length === 0) {
|
|
130
|
+
// Every v2 twin already consumed: this v1 line is a duplicate completed
|
|
131
|
+
// write of the same request (§3.6 rotation race) — count, don't fail.
|
|
132
|
+
counters.v1DuplicateKey++;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Best candidate: digest-equal first (same-key bursts carry different
|
|
136
|
+
// payloads — pairing by content avoids cross-matching twins), else FIFO.
|
|
137
|
+
const v2 = (v1Digest && unmatched.find(c => c.digest === v1Digest)) || unmatched[0];
|
|
138
|
+
counters.matched++;
|
|
139
|
+
v2._matched = true;
|
|
140
|
+
|
|
141
|
+
// ① wire-messages digest (skip heartbeats — no conversation on either side)
|
|
142
|
+
if (v2.conv && v1Digest != null) {
|
|
143
|
+
if (v1Digest !== v2.digest) {
|
|
144
|
+
addDiff({ type: 'messages-digest', sessionId: v2.sessionId, key, seq: v2.seq, conv: v2.conv, v1: v1Digest, v2: v2.digest, v1Len: entry.body.messages.length, v2Len: v2.len });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ② tools/system CAS equality
|
|
148
|
+
const v1Tools = blobRefOf(entry.body && entry.body.tools);
|
|
149
|
+
const v1Sys = blobRefOf(entry.body && entry.body.system);
|
|
150
|
+
if (v1Tools !== v2.toolsRef) addDiff({ type: 'tools-ref', sessionId: v2.sessionId, key, seq: v2.seq, v1: v1Tools, v2: v2.toolsRef });
|
|
151
|
+
if (v1Sys !== v2.sysRef) addDiff({ type: 'system-ref', sessionId: v2.sessionId, key, seq: v2.seq, v1: v1Sys, v2: v2.sysRef });
|
|
152
|
+
// ③ teammate attribution
|
|
153
|
+
if (!!entry.teammate !== (v2.kind === 'teammate')) {
|
|
154
|
+
addDiff({ type: 'teammate-kind', sessionId: v2.sessionId, key, seq: v2.seq, v1: !!entry.teammate, v2: v2.kind });
|
|
155
|
+
}
|
|
156
|
+
// ④ completion accounting
|
|
157
|
+
if (!v2.done) addDiff({ type: 'missing-done', sessionId: v2.sessionId, key, seq: v2.seq });
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Single streaming pass (1MB chunks): verify must stay memory-bounded on
|
|
161
|
+
// 300MB soak files — no dedup map, no second pass (review P2).
|
|
162
|
+
let entriesScanned = 0;
|
|
163
|
+
for await (const raw of iterateRawEntriesAsync(v1File)) {
|
|
164
|
+
onScan(raw);
|
|
165
|
+
entriesScanned++;
|
|
166
|
+
if (onProgress && entriesScanned % 1000 === 0) {
|
|
167
|
+
try { onProgress({ entriesScanned }); } catch { /* progress must never break the verify */ }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (onProgress) {
|
|
171
|
+
try { onProgress({ entriesScanned }); } catch { /* progress must never break the verify */ }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// v2 records never matched by any v1 entry in THIS file: only suspicious if
|
|
175
|
+
// their req/done both exist (a live v2-only write) — they may simply belong
|
|
176
|
+
// to another v1 segment (rotation), so count, don't fail.
|
|
177
|
+
let v2Unmatched = 0;
|
|
178
|
+
const v2ReqWithoutDone = [];
|
|
179
|
+
for (const [key, recs] of v2Index) {
|
|
180
|
+
for (const rec of recs) {
|
|
181
|
+
if (!rec._matched) v2Unmatched++;
|
|
182
|
+
if (!rec.done) v2ReqWithoutDone.push(key);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Integrity violations and unsupported-wire sessions are collected during the
|
|
187
|
+
// v2-side indexing pass above (before `flagSession` exists), so fold them into
|
|
188
|
+
// the per-session tally here — they condemn a session just as a diff does.
|
|
189
|
+
for (const v of integrity) flagSession(v.sessionId, `integrity@seq${v.seq}`);
|
|
190
|
+
for (const u of unsupportedSessions) flagSession(u.sessionId, `unsupported:${u.wireFormat}`);
|
|
191
|
+
|
|
192
|
+
const fileSize = (() => { try { return statSync(v1File).size; } catch { return 0; } })();
|
|
193
|
+
const report = {
|
|
194
|
+
v1File,
|
|
195
|
+
projectDir,
|
|
196
|
+
fileSize,
|
|
197
|
+
v2Sessions,
|
|
198
|
+
counters: { ...counters, v2Requests: v2Index.size, v2Unmatched, v1InflightOnly: inflightV1.size, v2ReqWithoutDone: v2ReqWithoutDone.length },
|
|
199
|
+
diffs,
|
|
200
|
+
integrity,
|
|
201
|
+
unsupportedSessions,
|
|
202
|
+
// Complete (uncapped) set of suspect sessions for the converter to quarantine.
|
|
203
|
+
suspectSessions: [...suspectSessions.entries()].map(([sessionId, r]) => ({ sessionId, count: r.count, reasons: r.reasons })),
|
|
204
|
+
ok: diffs.length === 0 && integrity.length === 0 && unsupportedSessions.length === 0,
|
|
205
|
+
};
|
|
206
|
+
return report;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Human-readable one-screen rendering for the CLI. */
|
|
210
|
+
export function renderReport(report) {
|
|
211
|
+
const c = report.counters;
|
|
212
|
+
const lines = [];
|
|
213
|
+
lines.push(`wire-v2 verify — ${report.v1File}`);
|
|
214
|
+
lines.push(` project: ${report.projectDir}`);
|
|
215
|
+
lines.push(` v2 sessions scanned: ${report.v2Sessions.length}, v2 requests indexed: ${c.v2Requests}`);
|
|
216
|
+
lines.push(` v1 completed: ${c.v1Completed} (heartbeat ${c.heartbeatsV1}, countTokens ${c.countTokensV1}, teammate ${c.teammateV1}, stale/broken skipped ${c.staleOrBroken})`);
|
|
217
|
+
lines.push(` matched: ${c.matched} v1-only (no v2 twin — pre-dual-write history): ${c.v1Only} v1-duplicate-key (rotation twins): ${c.v1DuplicateKey || 0} v2-unmatched (other segments): ${c.v2Unmatched}`);
|
|
218
|
+
lines.push(` inflight: v1-only ${c.v1InflightOnly}, v2 req-without-done ${c.v2ReqWithoutDone}`);
|
|
219
|
+
if (report.unsupportedSessions && report.unsupportedSessions.length > 0) {
|
|
220
|
+
lines.push(` UNSUPPORTED WIRE FORMAT (reader version gate): ${report.unsupportedSessions.length}`);
|
|
221
|
+
for (const u of report.unsupportedSessions.slice(0, 10)) lines.push(` ${u.sessionId} wireFormat=${u.wireFormat}`);
|
|
222
|
+
}
|
|
223
|
+
if (report.integrity.length > 0) {
|
|
224
|
+
lines.push(` INTEGRITY VIOLATIONS (journal msgTo vs replay): ${report.integrity.length}`);
|
|
225
|
+
for (const v of report.integrity.slice(0, 10)) lines.push(` ${v.sessionId} seq=${v.seq} conv=${v.conv} msgTo=${v.msgTo} replayed=${v.replayedLen}`);
|
|
226
|
+
}
|
|
227
|
+
if (report.diffs.length > 0) {
|
|
228
|
+
lines.push(` DIFFS: ${report.diffs.length}${report.diffs.length >= 50 ? '+ (capped)' : ''}`);
|
|
229
|
+
for (const d of report.diffs.slice(0, 10)) lines.push(` [${d.type}] seq=${d.seq} ${d.key}\n v1=${JSON.stringify(d.v1)} v2=${JSON.stringify(d.v2)}`);
|
|
230
|
+
}
|
|
231
|
+
lines.push(report.ok ? ' RESULT: OK — zero diffs' : ' RESULT: FAILED');
|
|
232
|
+
return lines.join('\n');
|
|
233
|
+
}
|
package/server/pty-manager.js
CHANGED
|
@@ -8,8 +8,9 @@ import { prepareEmbeddedShellSpawn, stripClaudeNoFlickerUnlessOptedIn, applyClau
|
|
|
8
8
|
import { killPtyTree } from './lib/term-signals.js';
|
|
9
9
|
import { findSafeSliceStart, splitTrailingIncomplete } from './lib/ansi-safe-slice.js';
|
|
10
10
|
import { buildSystemPromptFileArgs } from './lib/system-prompt-files.js';
|
|
11
|
+
import { renderSystemPromptFileArgs } from './lib/system-prompt-render.js';
|
|
11
12
|
import { MODEL_PROMPT_DIR } from './lib/model-system-prompts.js';
|
|
12
|
-
import {
|
|
13
|
+
import { resolveSpawnModel } from './lib/spawn-model-resolver.js';
|
|
13
14
|
|
|
14
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
16
|
const __dirname = dirname(__filename);
|
|
@@ -50,20 +51,31 @@ export function _setPtyImportForTests(fn) {
|
|
|
50
51
|
_ptyImportForTests = fn;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
// spawn
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
export
|
|
60
|
-
|
|
54
|
+
// spawn 时解析「当前生效配置」下的模型 id 供模型定制 system prompt 匹配(resolveSpawnModel:
|
|
55
|
+
// 激活的三方 proxy profile 模型映射 > env CLAUDE_MODEL/ANTHROPIC_MODEL > settings.json;
|
|
56
|
+
// 无实时配置信号 → null → 不注入模型条目)。旧判据读 ~/.claude.json 的 lastModelUsage
|
|
57
|
+
// ——那是上次会话的使用统计而非配置,陈旧记录会把三方模型的 override 提示词强加给
|
|
58
|
+
// 官方模型会话(review round:deepseek 残留记录事故)。
|
|
59
|
+
// NODE_TEST_CONTEXT 屏障保留:resolveSpawnModel 会读 process.env 的模型变量,开发机
|
|
60
|
+
// shell export 会漏进单测(机器状态依赖);测试用 _setSpawnModelReaderForTests 显式注入。
|
|
61
|
+
// env/reader 参数化只为可测性(见 test/pty-manager.test.js 的 guard 单测)。
|
|
62
|
+
export function _defaultSpawnModelReader(c, env = process.env, reader = resolveSpawnModel) {
|
|
63
|
+
return env.NODE_TEST_CONTEXT ? null : reader(c, env);
|
|
61
64
|
}
|
|
62
65
|
let _spawnModelReader = _defaultSpawnModelReader;
|
|
63
66
|
export function _setSpawnModelReaderForTests(fn) {
|
|
64
67
|
_spawnModelReader = fn || _defaultSpawnModelReader;
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
// 启动兜底的时间源与窗口:spawn 后在窗口内死亡视为「引导期死亡」。真实引导崩溃 <1s,
|
|
71
|
+
// 5s 足够;更长的窗口只会扩大「用户快速主动退出」的误报面(评审取值)。
|
|
72
|
+
// _now 可注入:兜底用例需要拨表模拟「存活超窗后退出」。
|
|
73
|
+
const SYS_PROMPT_BOOT_WINDOW_MS = 5000;
|
|
74
|
+
let _now = Date.now;
|
|
75
|
+
export function _setNowForTests(fn) {
|
|
76
|
+
_now = fn || Date.now;
|
|
77
|
+
}
|
|
78
|
+
|
|
67
79
|
async function getPty() {
|
|
68
80
|
if (typeof _ptyImportForTests === 'function') {
|
|
69
81
|
return _ptyImportForTests();
|
|
@@ -152,8 +164,15 @@ const _thinkingDisplayRejectedPaths = new Set();
|
|
|
152
164
|
// 启动目录里的 CC_SYSTEM.md / CC_APPEND_SYSTEM.md 自动注入 --system-prompt-file/--append-system-prompt-file。
|
|
153
165
|
// 若目标 claude(或三方 fork/wrapper)不识别该 flag,onExit 检测 "unknown option" 后把 claudePath 记入此集,
|
|
154
166
|
// 下次 spawn 跳过注入并去 flag 重启(对齐 _thinkingDisplayRejectedPaths 自愈)。
|
|
167
|
+
// 语义:永久(进程级)——"unknown option" 是该二进制不支持 flag 的确定性能力信号。
|
|
155
168
|
const _systemPromptFileRejectedPaths = new Set();
|
|
156
169
|
|
|
170
|
+
// 一次性跳过令牌:启动兜底一级的放宽半支(引导窗口内非信号 exit≠0)覆盖的是**瞬态**崩溃
|
|
171
|
+
// (API key 失效/网络抖动/与注入无关的秒退),不能写进上面的永久拒绝集——否则一次瞬态故障
|
|
172
|
+
// 会在整个 ccv 进程生命周期内静默禁用该二进制的注入(review P1)。令牌在下一次 spawn 被
|
|
173
|
+
// 消费(delete):恰好保证去注入重试一次,之后的 spawn 恢复正常注入尝试。
|
|
174
|
+
const _skipInjectionOncePaths = new Set();
|
|
175
|
+
|
|
157
176
|
// 内部重启(-c 重试 / flag 自愈)时抑制一次注入提示,避免终端重复打印同一行。
|
|
158
177
|
let _suppressNextSpawnNotice = false;
|
|
159
178
|
|
|
@@ -172,9 +191,10 @@ export function _markThinkingDisplayRejected(claudePath) {
|
|
|
172
191
|
_thinkingDisplayRejectedPaths.add(claudePath);
|
|
173
192
|
}
|
|
174
193
|
|
|
175
|
-
// 仅用于测试/内部:清空 system-prompt-file 拒绝集
|
|
194
|
+
// 仅用于测试/内部:清空 system-prompt-file 拒绝集(连同一次性跳过令牌,保持用例间干净)
|
|
176
195
|
export function _clearSystemPromptFileRejectedPaths() {
|
|
177
196
|
_systemPromptFileRejectedPaths.clear();
|
|
197
|
+
_skipInjectionOncePaths.clear();
|
|
178
198
|
}
|
|
179
199
|
|
|
180
200
|
// 仅用于测试:查询路径是否已被标记为不支持 --system-prompt-file
|
|
@@ -299,21 +319,40 @@ async function _spawnClaudeImpl(proxyPort, cwd, extraArgs = [], claudePath = nul
|
|
|
299
319
|
// LOG_DIR 内的 spawn(IM worker 工作目录 = <LOG_DIR>/IM_<id>/)跳过模型匹配:
|
|
300
320
|
// IM 人格依赖默认 sentinel CC_APPEND_SYSTEM.md 注入,全局模型条目不得静默取代它。
|
|
301
321
|
const spawnDir = cwd || process.cwd();
|
|
322
|
+
// insideLogDir 留在 try 外:下方 onExit 的启动兜底门控也要用它。
|
|
302
323
|
const insideLogDir = spawnDir === LOG_DIR || spawnDir.startsWith(LOG_DIR + sep);
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
|
|
324
|
+
// 整个 system prompt 构建 + 渲染管道包在 try-catch 里(PR#128):任何意外抛错(模型解析、
|
|
325
|
+
// buildSystemPromptFileArgs 文件系统竞态、渲染的 git 子进程异常)都走兜底——按「没命中任何
|
|
326
|
+
// 条目」处理,launch 不带 --system-prompt-file/--append-system-prompt-file,claude 用自身
|
|
327
|
+
// 默认 system prompt 启动。注入失败绝不能阻断 spawn。
|
|
328
|
+
let sysPrompt = { args: [], loaded: [], model: null };
|
|
329
|
+
// 一次性跳过令牌在进入管道前无条件消费(delete):放宽半支的兜底只跳过紧随其后的这一次注入。
|
|
330
|
+
// 若放在 build 之后,build 抛错时令牌残留、会再多跳过一次(违反 exactly-once,review)。
|
|
331
|
+
const skipOnce = _skipInjectionOncePaths.delete(claudePath);
|
|
332
|
+
try {
|
|
333
|
+
const resolvedModelId = insideLogDir ? null : _spawnModelReader(spawnDir);
|
|
334
|
+
sysPrompt = buildSystemPromptFileArgs(spawnDir, finalExtraArgs, process.env, {
|
|
335
|
+
modelId: resolvedModelId,
|
|
336
|
+
globalModelDir: join(LOG_DIR, MODEL_PROMPT_DIR),
|
|
337
|
+
});
|
|
338
|
+
if (_systemPromptFileRejectedPaths.has(claudePath) || skipOnce) {
|
|
339
|
+
sysPrompt = { args: [], loaded: [], model: null };
|
|
340
|
+
} else if (resolvedModelId && !sysPrompt.model && !sysPrompt.suppressed
|
|
341
|
+
&& (existsSync(join(spawnDir, MODEL_PROMPT_DIR)) || existsSync(join(LOG_DIR, MODEL_PROMPT_DIR)))) {
|
|
342
|
+
// The one diagnostic case worth a warning: a system_prompt dir is configured
|
|
343
|
+
// but the resolved model matched no entry (likely a misnamed file). Intentional
|
|
344
|
+
// skips (CCV_DISABLE_AUTO_SYSTEM_PROMPT=1, or a manual --system-prompt flag
|
|
345
|
+
// suppressing a matched entry) carry `suppressed` and stay quiet. The
|
|
346
|
+
// successful-injection notice is emitted below via emitSpawnNotice (with
|
|
347
|
+
// internal-restart suppression); no-modelId spawns are the normal quiet path.
|
|
348
|
+
console.warn(`[CC Viewer] model-specific prompt: modelId="${resolvedModelId}" resolved from active config but no matching entry found in workspace or global ${MODEL_PROMPT_DIR}/`);
|
|
349
|
+
}
|
|
350
|
+
// Resolve `${...}` template variables in the injected files (editor stores them literal —
|
|
351
|
+
// the substitution documented by the editor's parameter reference happens here, at launch).
|
|
352
|
+
sysPrompt = renderSystemPromptFileArgs(sysPrompt, { cwd: spawnDir, modelId: resolvedModelId });
|
|
353
|
+
} catch (err) {
|
|
354
|
+
console.warn('[CC Viewer] system prompt build/render failed, launching without injected prompt:', err?.message || err);
|
|
309
355
|
sysPrompt = { args: [], loaded: [], model: null };
|
|
310
|
-
} else if (resolvedModelId && !sysPrompt.model
|
|
311
|
-
&& (existsSync(join(spawnDir, MODEL_PROMPT_DIR)) || existsSync(join(LOG_DIR, MODEL_PROMPT_DIR)))) {
|
|
312
|
-
// The one diagnostic case worth a warning: a system_prompt dir is configured
|
|
313
|
-
// but the resolved model matched no entry (likely a misnamed file). The
|
|
314
|
-
// successful-injection notice is emitted below via emitSpawnNotice (with
|
|
315
|
-
// internal-restart suppression); no-modelId spawns are the normal quiet path.
|
|
316
|
-
console.warn(`[CC Viewer] model-specific prompt: modelId="${resolvedModelId}" resolved but no matching entry found in workspace or global ${MODEL_PROMPT_DIR}/`);
|
|
317
356
|
}
|
|
318
357
|
const launchArgs = sysPrompt.args.length ? [...finalExtraArgs, ...sysPrompt.args] : finalExtraArgs;
|
|
319
358
|
|
|
@@ -330,6 +369,9 @@ async function _spawnClaudeImpl(proxyPort, cwd, extraArgs = [], claudePath = nul
|
|
|
330
369
|
outputBuffer = '';
|
|
331
370
|
currentWorkspacePath = cwd || process.cwd();
|
|
332
371
|
lastWorkspacePath = currentWorkspacePath;
|
|
372
|
+
// Boot-window anchor for the injection fallback tiers below (same clock as the
|
|
373
|
+
// comparison — _now(), never Date.now(), so tests can steer both ends together).
|
|
374
|
+
const spawnedAt = _now();
|
|
333
375
|
|
|
334
376
|
ptyProcess = pty.spawn(command, args, {
|
|
335
377
|
name: 'xterm-256color',
|
|
@@ -342,13 +384,8 @@ async function _spawnClaudeImpl(proxyPort, cwd, extraArgs = [], claudePath = nul
|
|
|
342
384
|
// --allow-dangerously-skip-permissions only enables a later toggle, so it must NOT count.
|
|
343
385
|
ptySkipPermissions = extraArgs.includes('--dangerously-skip-permissions');
|
|
344
386
|
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
const modelSuffix = sysPrompt.model ? ` (model match: ${sysPrompt.model})` : '';
|
|
348
|
-
emitSpawnNotice(`[CC Viewer] loaded ${sysPrompt.loaded.join(', ')} as system prompt${modelSuffix}`);
|
|
349
|
-
}
|
|
350
|
-
_suppressNextSpawnNotice = false;
|
|
351
|
-
|
|
387
|
+
// PTY 事件处理器必须紧随 spawn 注册(PR#128):若子进程在 onExit 挂载前就退出(二进制缺失/
|
|
388
|
+
// 秒崩/拒绝注入 flag),exit 事件会丢失——句柄释放后事件循环可能排空。注入提示挪到注册之后。
|
|
352
389
|
ptyProcess.onData((data) => {
|
|
353
390
|
outputBuffer += data;
|
|
354
391
|
if (outputBuffer.length > MAX_BUFFER) {
|
|
@@ -363,12 +400,16 @@ async function _spawnClaudeImpl(proxyPort, cwd, extraArgs = [], claudePath = nul
|
|
|
363
400
|
}
|
|
364
401
|
});
|
|
365
402
|
|
|
366
|
-
ptyProcess.onExit(({ exitCode }) => {
|
|
403
|
+
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
367
404
|
flushBatch(true);
|
|
368
405
|
lastExitCode = exitCode;
|
|
369
406
|
ptyProcess = null;
|
|
370
407
|
ptyKind = null;
|
|
371
408
|
ptySkipPermissions = false;
|
|
409
|
+
// 引导期死亡:spawn 后窗口内退出。窗口外的退出一律不属于「注入拖崩启动」的兜底范围。
|
|
410
|
+
// 单次取 _now(评审):一级/二级共用同一时刻,注入的 fake clock 不会在分支间发散。
|
|
411
|
+
const elapsedMs = _now() - spawnedAt;
|
|
412
|
+
const diedInBootWindow = elapsedMs < SYS_PROMPT_BOOT_WINDOW_MS;
|
|
372
413
|
|
|
373
414
|
// Auto-retry without -c/--continue if "No conversation found"
|
|
374
415
|
// 注意:早退 return 会跳过下方的 exitListeners 广播——第一次失败的 pty 死亡对消费者
|
|
@@ -399,18 +440,48 @@ async function _spawnClaudeImpl(proxyPort, cwd, extraArgs = [], claudePath = nul
|
|
|
399
440
|
return;
|
|
400
441
|
}
|
|
401
442
|
|
|
402
|
-
//
|
|
403
|
-
//
|
|
443
|
+
// 事后兜底一级:注入过 system-prompt 文件的 claude 非正常死亡时,跳过注入重启一次
|
|
444
|
+
// (对齐上面的 --thinking-display 自愈;该确诊分支必须在前——用户自传 --thinking-display
|
|
445
|
+
// 且有注入时这里最多浪费一次去注入重试,第二次即广播真实报错)。两个半支的**持久性不同**:
|
|
446
|
+
// - 精确半支(原语义):输出含 "unknown option --system-prompt-file" —— 确诊该二进制不支持
|
|
447
|
+
// flag(稳定能力信号)→ 写永久拒绝集;任何场景(含 IM worker,否则它永远起不来)都自愈。
|
|
448
|
+
// - 放宽半支(启动兜底):引导窗口内 exit≠0 —— 注入**可能**是拖崩启动的原因(也可能是无关的
|
|
449
|
+
// 瞬态故障)→ 只发一次性跳过令牌,绝不写永久集(review P1:一次瞬态崩溃不得在整个进程
|
|
450
|
+
// 生命周期内静默禁用注入)。门控 !signal(用户 Ctrl-C/关标签/切 workspace 的 killPtyTree
|
|
451
|
+
// 都是信号终止,不是引导崩溃,盲目重启会把用户刚关的会话强行拉回来;Windows ConPTY 无
|
|
452
|
+
// POSIX 信号语义,已知限制:Ctrl-C 可能多一次无害重试)与 !insideLogDir(IM worker
|
|
453
|
+
// 「去注入重启」= 剥离 CC_APPEND_SYSTEM.md 人格后存活,比崩溃更难排查——IM 秒退只广播
|
|
454
|
+
// 真实报错)。
|
|
455
|
+
// 无死循环:respawn 时拒绝集/令牌把 loaded 清空 → 本分支不再命中,恰好重试一次。
|
|
456
|
+
// 一级重试只去掉注入、其余参数原样;根因是别的(如 API key 失效)时首次报错已实时流入
|
|
457
|
+
// 终端 scrollback 不丢失,第二次照常死亡并广播。
|
|
458
|
+
const unknownSysFileFlag = /unknown option ['"]--(append-)?system-prompt-file/i.test(outputBuffer);
|
|
459
|
+
const injectedBootCrash = !insideLogDir && !signal && diedInBootWindow;
|
|
404
460
|
const sysFileRejected = sysPrompt.loaded.length > 0 && exitCode !== 0
|
|
405
|
-
&&
|
|
461
|
+
&& (unknownSysFileFlag || injectedBootCrash);
|
|
406
462
|
if (sysFileRejected) {
|
|
407
|
-
|
|
408
|
-
|
|
463
|
+
if (unknownSysFileFlag) {
|
|
464
|
+
console.error('[CC Viewer] claude rejected --system-prompt-file, marking as unsupported and retrying without injection');
|
|
465
|
+
_systemPromptFileRejectedPaths.add(claudePath);
|
|
466
|
+
} else {
|
|
467
|
+
console.error(`[CC Viewer] claude exited (code ${exitCode}) ${Math.round(elapsedMs / 1000)}s after launch with injected system prompt (${sysPrompt.loaded.join(', ')}); retrying once without injection`);
|
|
468
|
+
_skipInjectionOncePaths.add(claudePath);
|
|
469
|
+
}
|
|
409
470
|
_suppressNextSpawnNotice = true;
|
|
471
|
+
// 措辞留有余地(评审):引导期死亡可能与注入无关(API key/网络等),不断言因果。
|
|
472
|
+
emitSpawnNotice(`[CC Viewer] claude exited during boot (code ${exitCode}); the injected system prompt may or may not be the cause — retrying once without ${sysPrompt.loaded.join(', ')}`);
|
|
410
473
|
spawnClaude(proxyPort, cwd, extraArgs, claudePath, isNpmVersion, serverPort, serverProtocol, internalToken);
|
|
411
474
|
return;
|
|
412
475
|
}
|
|
413
476
|
|
|
477
|
+
// 事后兜底二级:注入过且 exit=0 秒退 —— 无法与「用户主动快速 /exit」区分(日常高频),
|
|
478
|
+
// 所以只打诊断提示、不自动重启、不加入拒绝集(自动关停会因使用习惯静默禁用注入,评审否决),
|
|
479
|
+
// 也不早退——照常广播 exit,前端退出横幅路径与用户正常 /exit 完全一致。
|
|
480
|
+
// !insideLogDir:IM worker 的 pty 数据流可能被桥接转发,诊断行不该漏进 IM 会话(评审)。
|
|
481
|
+
if (sysPrompt.loaded.length > 0 && exitCode === 0 && diedInBootWindow && !insideLogDir) {
|
|
482
|
+
emitSpawnNotice(`[CC Viewer] claude exited ${Math.round(elapsedMs / 1000)}s after launch with an injected system prompt (${sysPrompt.loaded.join(', ')}). If this keeps happening the injected prompt may be incompatible — remove the entry or set CCV_DISABLE_AUTO_SYSTEM_PROMPT=1 to skip injection.`);
|
|
483
|
+
}
|
|
484
|
+
|
|
414
485
|
// 保留 lastWorkspacePath,不清除,用于 respawn
|
|
415
486
|
currentWorkspacePath = null;
|
|
416
487
|
for (const cb of exitListeners) {
|
|
@@ -418,6 +489,14 @@ async function _spawnClaudeImpl(proxyPort, cwd, extraArgs = [], claudePath = nul
|
|
|
418
489
|
}
|
|
419
490
|
});
|
|
420
491
|
|
|
492
|
+
// 注入了 system prompt 文件时向终端打印一行提示(可见性/安全);内部重启已抑制以免重复。
|
|
493
|
+
// 必须在 onData/onExit 注册之后再打(PR#128),缩小「子进程在处理器挂载前退出」的丢事件窗口。
|
|
494
|
+
if (sysPrompt.loaded.length && !_suppressNextSpawnNotice) {
|
|
495
|
+
const modelSuffix = sysPrompt.model ? ` (model match: ${sysPrompt.model})` : '';
|
|
496
|
+
emitSpawnNotice(`[CC Viewer] loaded ${sysPrompt.loaded.join(', ')} as system prompt${modelSuffix}`);
|
|
497
|
+
}
|
|
498
|
+
_suppressNextSpawnNotice = false;
|
|
499
|
+
|
|
421
500
|
return ptyProcess;
|
|
422
501
|
}
|
|
423
502
|
|
|
@@ -27,14 +27,20 @@ async function dingtalkStatus(req, res, parsedUrl, isLocal, deps) {
|
|
|
27
27
|
conn = deps.dingtalk.getBridgeStatus();
|
|
28
28
|
} else {
|
|
29
29
|
processInfo = await deps.dingtalk.getProcessStatus();
|
|
30
|
-
conn = {
|
|
30
|
+
conn = {
|
|
31
|
+
running: processInfo.running,
|
|
32
|
+
connected: processInfo.connected,
|
|
33
|
+
connectionState: processInfo.connectionState,
|
|
34
|
+
lastError: processInfo.lastError ?? null,
|
|
35
|
+
};
|
|
31
36
|
}
|
|
32
37
|
res.writeHead(200, JSON_HEADERS);
|
|
33
38
|
if (!isLocal) {
|
|
39
|
+
// connectionState passes through as-is; lastError stays loopback-only (mirrors routes/im.js).
|
|
34
40
|
res.end(JSON.stringify({
|
|
35
41
|
enabled: loadDingTalkState().enabled,
|
|
36
42
|
hasSecret: loadDingTalkState().hasSecret,
|
|
37
|
-
connection: { running: conn.running, connected: conn.connected },
|
|
43
|
+
connection: { running: conn.running, connected: conn.connected, connectionState: conn.connectionState },
|
|
38
44
|
}));
|
|
39
45
|
return;
|
|
40
46
|
}
|
|
@@ -93,7 +99,7 @@ function dingtalkConfigPost(req, res, parsedUrl, isLocal, deps) {
|
|
|
93
99
|
console.error('[CC Viewer] IM config apply failed for dingtalk:', e?.message || e);
|
|
94
100
|
}
|
|
95
101
|
res.writeHead(200, JSON_HEADERS);
|
|
96
|
-
res.end(JSON.stringify({ ...loadDingTalkState(), connection: { running: !!(saved?.enabled ?? incoming.enabled), connected: false } }));
|
|
102
|
+
res.end(JSON.stringify({ ...loadDingTalkState(), connection: { running: !!(saved?.enabled ?? incoming.enabled), connected: false, connectionState: 'disconnected' } }));
|
|
97
103
|
});
|
|
98
104
|
}
|
|
99
105
|
|