cc-viewer 1.6.348 → 1.7.1
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-gUSmU6Ny.js +2 -0
- package/dist/assets/{MdxEditorPanel-ualoRFOL.js → MdxEditorPanel-DAR39A3S.js} +1 -1
- package/dist/assets/Mobile-qfRsRth0.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-B51Ci0GL.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-B5Zy_CC_.css → seqResourceLoaders-DFrHmgnK.css} +1 -1
- package/dist/assets/seqResourceLoaders-DZJxMMaR.js +2 -0
- 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 +3 -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 +62 -274
- 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 +1124 -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 +558 -0
- package/server/lib/v2/meta-rows.js +360 -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 +528 -0
- package/server/lib/v2/verify.js +233 -0
- package/server/lib/wire-compress.js +167 -0
- package/server/routes/events.js +120 -150
- package/server/routes/files-fs.js +4 -2
- package/server/routes/im.js +11 -2
- package/server/routes/logs.js +209 -164
- 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/v2.js +60 -0
- package/server/routes/workspaces.js +50 -14
- package/server/server.js +70 -66
- package/server/workspace-registry.js +24 -4
- package/src/utils/apiUrl.js +26 -0
- package/src/utils/askFallback.js +21 -0
- package/src/utils/askOptionDesc.js +55 -0
- package/src/utils/askPortalMatcher.js +54 -0
- package/src/utils/autoApproveOptions.js +27 -0
- package/src/utils/avatarAnimationPostPass.js +49 -0
- package/src/utils/builtinPresets.js +18 -0
- package/src/utils/clearCheckpoint.js +7 -0
- package/src/utils/commandValidator.js +45 -0
- package/src/utils/contentFilter.js +620 -0
- package/src/utils/contextRaw.js +18 -0
- package/src/utils/contextSidebarNavigation.js +16 -0
- package/src/utils/contextTurns.js +117 -0
- package/src/utils/displayScaleHelper.js +47 -0
- package/src/utils/effectiveModel.js +13 -0
- package/src/utils/entry-slim.js +563 -0
- package/src/utils/entryCache.js +144 -0
- package/src/utils/errorReport.js +6 -0
- package/src/utils/fileExpandedPathsStorage.js +70 -0
- package/src/utils/fileIcons.jsx +36 -0
- package/src/utils/fileOpen.js +37 -0
- package/src/utils/formatters.js +48 -0
- package/src/utils/gitApi.js +50 -0
- package/src/utils/gitTreeBuilder.js +19 -0
- package/src/utils/helpers.js +846 -0
- package/src/utils/identityHeal.js +99 -0
- package/src/utils/imConnState.js +59 -0
- package/src/utils/imOrigin.js +24 -0
- package/src/utils/imTr.js +7 -0
- package/src/utils/imageCompress.js +83 -0
- package/src/utils/imageDownscale.js +28 -0
- package/src/utils/imageResize.js +112 -0
- package/src/utils/ingestPipeline.js +47 -0
- package/src/utils/lazyWithReload.js +53 -0
- package/src/utils/markdown.js +30 -0
- package/src/utils/markdownIncremental.js +39 -0
- package/src/utils/markdownProfiler.js +113 -0
- package/src/utils/mdExtensionDetect.js +64 -0
- package/src/utils/memoryLinkParser.js +44 -0
- package/src/utils/modalMask.js +15 -0
- package/src/utils/monotime.js +6 -0
- package/src/utils/pinnedMenu.js +55 -0
- package/src/utils/presetShortcuts.js +14 -0
- package/src/utils/projectAlias.js +135 -0
- package/src/utils/promptClassifier.js +112 -0
- package/src/utils/promptDetect.js +196 -0
- package/src/utils/promptNav.js +60 -0
- package/src/utils/ptyChunkBuilder.js +251 -0
- package/src/utils/quickMenuHoverIntent.js +35 -0
- package/src/utils/rateLimitParser.js +119 -0
- package/src/utils/readResultPool.js +106 -0
- package/src/utils/refreshCachedItemProp.js +30 -0
- package/src/utils/requestType.js +208 -0
- package/src/utils/resizeCalc.js +21 -0
- package/src/utils/resolveLocalized.js +17 -0
- package/src/utils/searchApi.js +47 -0
- package/src/utils/searchReplace.js +51 -0
- package/src/utils/seqResourceLoaders.js +79 -0
- package/src/utils/sessionManager.js +471 -0
- package/src/utils/sessionMerge.js +180 -0
- package/src/utils/skillModalController.js +111 -0
- package/src/utils/skillsParser.js +116 -0
- package/src/utils/slashCommandLabels.js +80 -0
- package/src/utils/splitDragCalc.js +73 -0
- package/src/utils/stickyBottomController.js +643 -0
- package/src/utils/svgSanitize.js +48 -0
- package/src/utils/systemTags.js +47 -0
- package/src/utils/tClaude.js +20 -0
- package/src/utils/teamModalBuilder.js +351 -0
- package/src/utils/teamSessionParser.js +190 -0
- package/src/utils/teammateAvatars.js +194 -0
- package/src/utils/teammateDetector.js +121 -0
- package/src/utils/termDiag.js +114 -0
- package/src/utils/terminalClipboard.js +117 -0
- package/src/utils/terminalWriteQueue.js +398 -0
- package/src/utils/thinkingMerge.js +49 -0
- package/src/utils/toolCatalog.js +29 -0
- package/src/utils/toolResultBuilder.js +393 -0
- package/src/utils/toolResultClassifier.js +16 -0
- package/src/utils/toolResultCore.js +167 -0
- package/src/utils/toolsDiff.js +44 -0
- package/src/utils/toolsXmlFormatter.js +3 -0
- package/src/utils/ultraplanController.js +109 -0
- package/src/utils/ultraplanExperts.js +86 -0
- package/src/utils/ultraplanTemplates.js +154 -0
- package/src/utils/userImageRefs.js +49 -0
- package/src/utils/v3Assembler.js +112 -0
- package/src/utils/v3Rows.js +48 -0
- package/src/utils/voicePackPlayer.js +233 -0
- package/src/utils/webSearchGrouping.js +111 -0
- package/src/utils/workflowFormat.js +52 -0
- package/src/utils/workflowRuns.js +118 -0
- package/src/utils/workflowStore.js +94 -0
- 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,11 +1,23 @@
|
|
|
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';
|
|
20
|
+
import { sseWrite } from '../lib/wire-compress.js';
|
|
9
21
|
|
|
10
22
|
function workspacesList(req, res, parsedUrl, isLocal, deps) {
|
|
11
23
|
import('../workspace-registry.js').then(async ({ getWorkspaces }) => {
|
|
@@ -49,18 +61,28 @@ function workspacesLaunch(req, res, parsedUrl, isLocal, deps) {
|
|
|
49
61
|
const result = initForWorkspace(wsPath);
|
|
50
62
|
process.env.CCV_PROJECT_DIR = wsPath;
|
|
51
63
|
|
|
52
|
-
//
|
|
53
|
-
|
|
64
|
+
// 启动日志监听(S6b: v2 live feed when the v2 writer is active)
|
|
65
|
+
deps.startLogWatch();
|
|
54
66
|
|
|
55
67
|
// 启动 stats worker(如果尚未启动)
|
|
56
68
|
if (!deps.statsWorker) deps.startStatsWorker();
|
|
57
69
|
deps.startStreamingStatusTimer();
|
|
58
70
|
|
|
71
|
+
// P2 检测通道②:workspace 启动器注入的 -c/--continue/-r/--resume(无论
|
|
72
|
+
// 是否走 PTY 分支都要打标——迁移提示的 continued 语义只取决于参数本身)。
|
|
73
|
+
const mergedArgs = [...deps.workspaceClaudeArgs, ...(Array.isArray(launchExtraArgs) ? launchExtraArgs : [])];
|
|
74
|
+
if (mergedArgs.some((a) => CONTINUE_FLAGS.has(a))) markContinuedLaunch();
|
|
75
|
+
// `--fork-session`: a continuation that mints a NEW session id on purpose —
|
|
76
|
+
// `-c` folder adoption must NOT fire for it.
|
|
77
|
+
if (mergedArgs.includes('--fork-session')) markForkSession();
|
|
78
|
+
// `-r`/`--resume`: user-chosen target session — adoption must not redirect
|
|
79
|
+
// it to the latest main session.
|
|
80
|
+
if (mergedArgs.some((a) => RESUME_FLAGS.has(a))) markResumeSession();
|
|
81
|
+
|
|
59
82
|
// 启动 PTY
|
|
60
83
|
const proxyPort = process.env.CCV_PROXY_PORT;
|
|
61
84
|
if (proxyPort) {
|
|
62
85
|
const { spawnClaude } = await import('../pty-manager.js');
|
|
63
|
-
const mergedArgs = [...deps.workspaceClaudeArgs, ...(Array.isArray(launchExtraArgs) ? launchExtraArgs : [])];
|
|
64
86
|
await spawnClaude(parseInt(proxyPort), wsPath, mergedArgs, deps.workspaceClaudePath, deps.workspaceIsNpmVersion, deps.actualPort, deps.protocol, deps.INTERNAL_TOKEN);
|
|
65
87
|
}
|
|
66
88
|
|
|
@@ -70,24 +92,38 @@ function workspacesLaunch(req, res, parsedUrl, isLocal, deps) {
|
|
|
70
92
|
const startedPayload = `event: workspace_started\ndata: ${JSON.stringify({ projectName: result.projectName, path: wsPath, claudeProjectModel: readClaudeProjectModel(wsPath) })}\n\n`;
|
|
71
93
|
deps.clients.forEach(client => {
|
|
72
94
|
try {
|
|
73
|
-
client
|
|
95
|
+
sseWrite(client, startedPayload);
|
|
74
96
|
} catch {}
|
|
75
97
|
});
|
|
76
98
|
|
|
77
99
|
// 流式分段广播以刷新会话区域,避免全量加载 OOM
|
|
78
|
-
|
|
100
|
+
// S6b: the live source is the v2 session dir when the v2 writer is
|
|
101
|
+
// active (a fresh workspace has no session yet → empty stream, the live
|
|
102
|
+
// feed picks up from the first request).
|
|
103
|
+
const wsReloadSource = getLiveLogSource();
|
|
104
|
+
const wsReloadTotal = await countLogEntries(wsReloadSource);
|
|
79
105
|
deps.clients.forEach(client => {
|
|
80
|
-
try { client
|
|
106
|
+
try { sseWrite(client, `event: load_start\ndata: ${JSON.stringify({ total: wsReloadTotal, incremental: false })}\n\n`); } catch {}
|
|
81
107
|
});
|
|
82
|
-
await streamRawEntriesAsync(
|
|
108
|
+
await streamRawEntriesAsync(wsReloadSource, (raw) => {
|
|
83
109
|
deps.clients.forEach(client => {
|
|
84
|
-
try { client
|
|
110
|
+
try { sseWrite(client, 'event: load_chunk\ndata: ['); sseWrite(client, raw.replace(/\n/g, '')); sseWrite(client, ']\n\n'); } catch {}
|
|
85
111
|
});
|
|
86
112
|
});
|
|
87
113
|
deps.clients.forEach(client => {
|
|
88
|
-
try { client
|
|
114
|
+
try { sseWrite(client, `event: load_end\ndata: {}\n\n`); } catch {}
|
|
89
115
|
});
|
|
90
116
|
|
|
117
|
+
// 1.7.0 迁移引导(P2):切进的项目仍有未转换 v1 日志 → 对存量连接广播
|
|
118
|
+
// migrate_prompt(新连接由 /events 的连接帧覆盖)。
|
|
119
|
+
try {
|
|
120
|
+
const mig = migrationStatus(LOG_DIR, result.projectName || '');
|
|
121
|
+
if (mig.pending) {
|
|
122
|
+
const frame = `event: migrate_prompt\ndata: ${JSON.stringify({ ...mig, continued: isContinuedLaunch() })}\n\n`;
|
|
123
|
+
deps.clients.forEach(client => { try { sseWrite(client, frame); } catch {} });
|
|
124
|
+
}
|
|
125
|
+
} catch (e) { reportSwallowed('sse.migrate_prompt', e); }
|
|
126
|
+
|
|
91
127
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
92
128
|
res.end(JSON.stringify({ ok: true, projectName: result.projectName }));
|
|
93
129
|
} catch (err) {
|
|
@@ -139,8 +175,8 @@ function workspacesStop(req, res, parsedUrl, isLocal, deps) {
|
|
|
139
175
|
]).then(() => {
|
|
140
176
|
// 接续原有清理流程
|
|
141
177
|
|
|
142
|
-
//
|
|
143
|
-
|
|
178
|
+
// 停止日志监听(v1 tail + v2 live feed)
|
|
179
|
+
deps.stopLogWatch();
|
|
144
180
|
unwatchAllWorkflows();
|
|
145
181
|
|
|
146
182
|
// 重置 interceptor 状态
|
|
@@ -150,7 +186,7 @@ function workspacesStop(req, res, parsedUrl, isLocal, deps) {
|
|
|
150
186
|
// 通知所有 SSE 客户端
|
|
151
187
|
deps.clients.forEach(client => {
|
|
152
188
|
try {
|
|
153
|
-
client
|
|
189
|
+
sseWrite(client, `event: workspace_stopped\ndata: {}\n\n`);
|
|
154
190
|
} catch {}
|
|
155
191
|
});
|
|
156
192
|
|
package/server/server.js
CHANGED
|
@@ -9,6 +9,7 @@ import { execFile, exec, spawn } from 'node:child_process';
|
|
|
9
9
|
import { promisify } from 'node:util';
|
|
10
10
|
import { Worker } from 'node:worker_threads';
|
|
11
11
|
import { isPathContained } from './lib/file-api.js';
|
|
12
|
+
import { sseWrite, isWireV3Enabled } from './lib/wire-compress.js';
|
|
12
13
|
import { setEntry as askStoreSetEntry, deleteEntry as askStoreDeleteEntry, pruneStale as askStorePruneStale, markAnswered as askStoreMarkAnswered, markCancelled as askStoreMarkCancelled, loadAskStore as askStoreLoad } from './lib/ask-store.js';
|
|
13
14
|
import { ASK_TIMEOUT_MS, ASK_WAITER_REAP_INTERVAL_MS, ASK_WAITER_LIVENESS_MS } from './lib/ask-constants.js';
|
|
14
15
|
import { reapDeadAskWaiters, sweepOrphanedDiskAsks } from './lib/ask-reaper.js';
|
|
@@ -33,6 +34,7 @@ import { searchRoutes } from './routes/search.js';
|
|
|
33
34
|
import { workspacesRoutes } from './routes/workspaces.js';
|
|
34
35
|
import { expertRoutes } from './routes/expert.js';
|
|
35
36
|
import { eventsRoutes } from './routes/events.js';
|
|
37
|
+
import { v2Routes } from './routes/v2.js';
|
|
36
38
|
import { askPermRoutes } from './routes/ask-perm.js';
|
|
37
39
|
import { teamRoutes } from './routes/team.js';
|
|
38
40
|
import { authRoutes } from './routes/auth.js';
|
|
@@ -74,18 +76,19 @@ function execWithStdin(cmd, args, input, options) {
|
|
|
74
76
|
child.stdin.end();
|
|
75
77
|
});
|
|
76
78
|
}
|
|
77
|
-
import {
|
|
78
|
-
import {
|
|
79
|
+
import { _initPromise, _projectName, _logDir, _v2Writer, streamingState, resetStreamingState, PROFILE_PATH, setLivePort, getImLiveText, resetImLiveText } from './interceptor.js';
|
|
80
|
+
import { V2LiveFeed } from './lib/v2/live-feed.js';
|
|
81
|
+
import { sanitizePathComponent } from './lib/v2/layout.js';
|
|
82
|
+
import { maybeResumeConvert } from './lib/v2/convert-manager.js';
|
|
79
83
|
import { LOG_DIR, setLogDir, getClaudeConfigDir, isBrowserOpenSuppressed } from '../findcc.js';
|
|
80
84
|
import { t, getLang, setLang } from './i18n.js';
|
|
81
85
|
import { loadAuthConfig, loadAuthState, saveAuthConfig, clearProjectOverride, generatePassword, decideAuth, parseCookies, renderLoginPage, localeFromAcceptLanguage } from './lib/auth.js';
|
|
82
86
|
import { checkAndUpdate } from './lib/updater.js';
|
|
83
87
|
import { loadPlugins, runWaterfallHook, runParallelHook } from './lib/plugin-loader.js';
|
|
84
88
|
import { CONTEXT_WINDOW_FILE, readModelContextSize } from './lib/context-watcher.js';
|
|
85
|
-
import {
|
|
89
|
+
import { sendEventToClients, sendToClients } from './lib/log-watcher.js';
|
|
86
90
|
import { createImLogWatcher } from './lib/im-log-watcher.js';
|
|
87
91
|
import { unwatchAllWorkflows } from './lib/workflow-watcher.js';
|
|
88
|
-
import { cleanupExtractCache } from './lib/jsonl-archive.js';
|
|
89
92
|
import { backupConfigs } from './lib/config-backup.js';
|
|
90
93
|
import { normalizeBasePath, validateBasePath, stripBasePath } from './lib/base-path.js';
|
|
91
94
|
import { createHardenedCleanup } from './lib/term-signals.js';
|
|
@@ -111,12 +114,6 @@ try {
|
|
|
111
114
|
const isCliMode = process.env.CCV_CLI_MODE === '1';
|
|
112
115
|
const isSdkMode = process.env.CCV_SDK_MODE === '1';
|
|
113
116
|
const isWorkspaceMode = process.env.CCV_WORKSPACE_MODE === '1';
|
|
114
|
-
// Instance id from `ccv --pid <name>` (sanitized in cli.js). null = default mode (no
|
|
115
|
-
// per-instance isolation): the session-pin file falls back to the project-shared key.
|
|
116
|
-
const INSTANCE_ID = process.env.CCV_INSTANCE_ID || null;
|
|
117
|
-
// Remember this id under the project so the CLI banner can list past ids next run (memory
|
|
118
|
-
// aid only). Fire-and-forget; no-ops when there's no active project (e.g. workspace mode).
|
|
119
|
-
if (INSTANCE_ID && _logDir) recordInstance(_logDir, INSTANCE_ID).catch(() => {});
|
|
120
117
|
const _defaultProxyProfiles = { active: 'max', profiles: [{ id: 'max', name: 'Default' }] };
|
|
121
118
|
const _maskApiKey = (k) => k && typeof k === 'string' && k.length > 4 ? '****' + k.slice(-4) : k ? '****' : '';
|
|
122
119
|
const _maskProfiles = (data) => {
|
|
@@ -305,9 +302,12 @@ let _launchCallback = null;
|
|
|
305
302
|
export function setLaunchCallback(fn) { _launchCallback = fn; }
|
|
306
303
|
export function setWorkspaceLaunched(v) { _workspaceLaunched = v; }
|
|
307
304
|
export function initPostLaunch() {
|
|
308
|
-
|
|
305
|
+
startLogWatch();
|
|
309
306
|
if (!statsWorker) startStatsWorker();
|
|
310
307
|
startStreamingStatusTimer();
|
|
308
|
+
// wire-v2 S8: a migration the previous process left unfinished resumes here
|
|
309
|
+
// (resident-until-done, user decision; file-level checkpoints make it cheap).
|
|
310
|
+
try { if (_projectName) maybeResumeConvert(LOG_DIR, _projectName); } catch (err) { console.error('[CC Viewer] wire-v2 convert resume failed:', err.message); }
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
// Global POST body size limit (10MB) to prevent OOM from malicious/buggy clients
|
|
@@ -317,6 +317,22 @@ const MAX_POST_BODY = 10 * 1024 * 1024;
|
|
|
317
317
|
// 防止长会话把数十 MB 历史一次性灌进浏览器导致 renderer OOM。
|
|
318
318
|
// 用户显式 ?limit=0 可恢复全量加载(power-user 逃生口)。
|
|
319
319
|
const DEFAULT_EVENTS_LIMIT = 1000;
|
|
320
|
+
// Wire v3 flag (V3.S6: DEFAULT ON) — read ONCE at startup; a read/UI-shape
|
|
321
|
+
// toggle the server and client must agree on per process, not per request
|
|
322
|
+
// (unlike wire-compress's per-connection content negotiation). Broadcast to
|
|
323
|
+
// clients via the server_config SSE frame; tests inject deps.wireV3 directly.
|
|
324
|
+
// CCV_WIRE_V3=0 (or =off) is the escape hatch back to the legacy full-entry
|
|
325
|
+
// wire — kept (not deleted) for one release cycle before the legacy live UI
|
|
326
|
+
// path is removed; v1 legacy FILES always use the legacy pipeline regardless.
|
|
327
|
+
const WIRE_V3 = isWireV3Enabled(process.env.CCV_WIRE_V3);
|
|
328
|
+
// Build stamp broadcast in server_config: a tab that survives a server
|
|
329
|
+
// upgrade reconnects with a STALE JS bundle that lacks the new wire's
|
|
330
|
+
// listeners (silent empty session). The fresh bundle compares this stamp on
|
|
331
|
+
// reconnect and reloads itself on mismatch (review P2-a; heals upgrades from
|
|
332
|
+
// this version onward — older bundles predate the guard).
|
|
333
|
+
const SERVER_BUILD = (() => {
|
|
334
|
+
try { return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8')).version || ''; } catch { return ''; }
|
|
335
|
+
})();
|
|
320
336
|
// SSE 单客户端 backpressure 容忍上限:连续未排空 > 此时长则视为 dead 客户端剔除。
|
|
321
337
|
// 调高至 30s:大会话首屏/重连重放时,渲染器(尤其 Windows 浏览器,大 DOM layout 更重)
|
|
322
338
|
// 可能短暂忙到来不及排空 socket。过早剔除会触发「断开→EventSource 自动重连→再次重放」
|
|
@@ -418,16 +434,35 @@ const MIME_TYPES = {
|
|
|
418
434
|
'.woff2': 'font/woff2',
|
|
419
435
|
};
|
|
420
436
|
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
437
|
+
// S6b: v2 live feed — the live channel when the v2 writer is active. Lazy
|
|
438
|
+
// singleton so worker/test paths that never watch anything pay nothing.
|
|
439
|
+
let _v2LiveFeed = null;
|
|
440
|
+
function _ensureV2LiveFeed() {
|
|
441
|
+
if (!_v2LiveFeed) {
|
|
442
|
+
_v2LiveFeed = new V2LiveFeed({
|
|
443
|
+
clients,
|
|
444
|
+
getClaudePid,
|
|
445
|
+
runParallelHook,
|
|
446
|
+
// The stats worker's v2 units are session dirs (stats-worker.js v9).
|
|
447
|
+
notifyStatsWorker,
|
|
448
|
+
wireV3: WIRE_V3,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
return _v2LiveFeed;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Start the live channel for the current project (v2 live feed). No-op until
|
|
455
|
+
* a project is bound (workspace mode defers to the launch route). */
|
|
456
|
+
function startLogWatch() {
|
|
457
|
+
if (!_projectName) return;
|
|
458
|
+
const feed = _ensureV2LiveFeed();
|
|
459
|
+
feed.start(join(LOG_DIR, sanitizePathComponent(_projectName)));
|
|
460
|
+
_v2Writer.setOnActivity((dir) => feed.tick(dir));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function stopLogWatch() {
|
|
464
|
+
if (_v2LiveFeed) _v2LiveFeed.stop();
|
|
465
|
+
_v2Writer.setOnActivity(null);
|
|
431
466
|
}
|
|
432
467
|
|
|
433
468
|
function getLocalIp() {
|
|
@@ -516,7 +551,8 @@ const deps = {
|
|
|
516
551
|
persistAskEntry: _persistAskEntry,
|
|
517
552
|
persistAskDelete: _persistAskDelete,
|
|
518
553
|
notifyParentPending: _notifyParentPending,
|
|
519
|
-
|
|
554
|
+
startLogWatch,
|
|
555
|
+
stopLogWatch,
|
|
520
556
|
scheduleTurnEndBroadcast: _scheduleTurnEndBroadcast,
|
|
521
557
|
ensureImWatch: _ensureImWatch,
|
|
522
558
|
maskProfiles: _maskProfiles,
|
|
@@ -574,11 +610,12 @@ const deps = {
|
|
|
574
610
|
WINDOWS_RESERVED_NAMES,
|
|
575
611
|
DEFAULT_EVENTS_LIMIT,
|
|
576
612
|
SSE_BACKPRESSURE_TIMEOUT_MS,
|
|
613
|
+
wireV3: WIRE_V3,
|
|
614
|
+
serverBuild: SERVER_BUILD,
|
|
577
615
|
IGNORED_PATTERNS,
|
|
578
616
|
isCliMode,
|
|
579
617
|
isSdkMode,
|
|
580
618
|
isWorkspaceMode,
|
|
581
|
-
instanceId: INSTANCE_ID,
|
|
582
619
|
defaultProxyProfiles: _defaultProxyProfiles,
|
|
583
620
|
};
|
|
584
621
|
|
|
@@ -607,6 +644,7 @@ const _routes = [
|
|
|
607
644
|
...workspacesRoutes,
|
|
608
645
|
...expertRoutes,
|
|
609
646
|
...eventsRoutes,
|
|
647
|
+
...v2Routes,
|
|
610
648
|
...askPermRoutes,
|
|
611
649
|
...teamRoutes,
|
|
612
650
|
...dingtalkRoutes,
|
|
@@ -855,9 +893,6 @@ export async function startViewer() {
|
|
|
855
893
|
// 加载插件(需要在创建服务器之前,以便通过 hook 获取 HTTPS 证书)
|
|
856
894
|
await loadPlugins();
|
|
857
895
|
|
|
858
|
-
// 清理过期解压缓存(fire-and-forget;任何错误吞掉)
|
|
859
|
-
setImmediate(() => { try { cleanupExtractCache(); } catch { /* ignore */ } });
|
|
860
|
-
|
|
861
896
|
// 启动期配置备份:preferences/profile/workspaces → LOG_DIR 外的 cc-viewer-config-backups/
|
|
862
897
|
// (滚动留 10 份)。2026-06-06 事故:配置随 LOG_DIR 整树丢失后无处可恢复。fire-and-forget。
|
|
863
898
|
setImmediate(() => { try { backupConfigs(); } catch { /* ignore */ } });
|
|
@@ -1025,9 +1060,11 @@ export async function startViewer() {
|
|
|
1025
1060
|
// 工作区模式下延迟到选择工作区后再启动监听
|
|
1026
1061
|
if (!isWorkspaceMode) {
|
|
1027
1062
|
readModelContextSize(); // Cache model→size mapping at startup
|
|
1028
|
-
|
|
1063
|
+
startLogWatch();
|
|
1029
1064
|
startStatsWorker();
|
|
1030
1065
|
startStreamingStatusTimer();
|
|
1066
|
+
// wire-v2 S8: resume an unfinished migration (see initPostLaunch)
|
|
1067
|
+
try { if (_projectName) maybeResumeConvert(LOG_DIR, _projectName); } catch (err) { console.error('[CC Viewer] wire-v2 convert resume failed:', err.message); }
|
|
1031
1068
|
}
|
|
1032
1069
|
// CLI 模式下启动 WebSocket 服务 (必须 await,否则插件 hook 拿不到 upgrade listeners)
|
|
1033
1070
|
if (isCliMode) {
|
|
@@ -2004,15 +2041,6 @@ export function getAuthConfig() {
|
|
|
2004
2041
|
return authConfig;
|
|
2005
2042
|
}
|
|
2006
2043
|
|
|
2007
|
-
// Instance id (`--pid`) + the project's previously-used ids — printed by cli.js in the
|
|
2008
|
-
// CLI-mode startup banner so the user can recall which id to reuse with `-c` next time.
|
|
2009
|
-
export function getInstanceId() {
|
|
2010
|
-
return INSTANCE_ID;
|
|
2011
|
-
}
|
|
2012
|
-
export function getKnownInstances() {
|
|
2013
|
-
try { return listInstances(_logDir); } catch { return []; }
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
2044
|
// In-process broadcast helper for the `turn_end` SSE event. Two callers:
|
|
2017
2045
|
// 1. /api/turn-end-notify POST handler (PTY/CLI mode via Stop hook bridge)
|
|
2018
2046
|
// 2. cli.js runSdkMode → sdkManager.initSdkSession({ onTurnEnd }) (SDK mode —
|
|
@@ -2238,28 +2266,12 @@ async function _doStop() {
|
|
|
2238
2266
|
new Promise(r => setTimeout(r, 3000)),
|
|
2239
2267
|
]);
|
|
2240
2268
|
} catch { }
|
|
2241
|
-
|
|
2242
|
-
if (_resumeState && _resumeState.tempFile) {
|
|
2243
|
-
try {
|
|
2244
|
-
const { tempFile } = _resumeState;
|
|
2245
|
-
if (existsSync(tempFile)) {
|
|
2246
|
-
// 只有非空 temp 文件才 rename 为正式文件,空文件直接删除
|
|
2247
|
-
const sz = statSync(tempFile).size;
|
|
2248
|
-
if (sz > 0) {
|
|
2249
|
-
const newPath = tempFile.replace('_temp.jsonl', '.jsonl');
|
|
2250
|
-
renameSync(tempFile, newPath);
|
|
2251
|
-
} else {
|
|
2252
|
-
unlinkSync(tempFile);
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
} catch { }
|
|
2256
|
-
}
|
|
2257
|
-
unwatchAll();
|
|
2269
|
+
stopLogWatch();
|
|
2258
2270
|
unwatchAllWorkflows();
|
|
2259
2271
|
unwatchFile(CONTEXT_WINDOW_FILE);
|
|
2260
2272
|
clients.forEach(client => client.end());
|
|
2261
2273
|
// Truncate in place (not `clients = []`) so the array reference stays stable across
|
|
2262
|
-
// stop/start cycles — deps.clients
|
|
2274
|
+
// stop/start cycles — deps.clients holds this same reference.
|
|
2263
2275
|
clients.length = 0;
|
|
2264
2276
|
if (server) {
|
|
2265
2277
|
// 销毁所有活跃连接,防止 keep-alive 阻止进程退出
|
|
@@ -2385,7 +2397,7 @@ if (!isWorkspaceMode) {
|
|
|
2385
2397
|
pendingMajorUpdate = { version: result.remoteVersion, source: result.status };
|
|
2386
2398
|
const payload = JSON.stringify(pendingMajorUpdate);
|
|
2387
2399
|
clients.forEach(client => {
|
|
2388
|
-
try { client
|
|
2400
|
+
try { sseWrite(client, `event: update_major_available\ndata: ${payload}\n\n`); } catch { }
|
|
2389
2401
|
});
|
|
2390
2402
|
} else if (result.status === 'upgrading_in_background') {
|
|
2391
2403
|
console.error(`[CC Viewer] background upgrade to ${result.remoteVersion} started (active after next launch)`);
|
|
@@ -2399,17 +2411,9 @@ if (!isWorkspaceMode) {
|
|
|
2399
2411
|
});
|
|
2400
2412
|
}
|
|
2401
2413
|
|
|
2402
|
-
//
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
try {
|
|
2406
|
-
if (existsSync(_resumeState.tempFile)) {
|
|
2407
|
-
const newPath = _resumeState.tempFile.replace('_temp.jsonl', '.jsonl');
|
|
2408
|
-
renameSync(_resumeState.tempFile, newPath);
|
|
2409
|
-
}
|
|
2410
|
-
} catch { }
|
|
2411
|
-
}
|
|
2412
|
-
}
|
|
2414
|
+
// 进程退出钩子(v1 时代负责临时文件转正;v2 无暂存文件,保留空挂点防
|
|
2415
|
+
// 未来清理动作无处可挂)
|
|
2416
|
+
function handleExit() {}
|
|
2413
2417
|
// 防御性单次注册:ESM cache 已保证模块顶层只执行一次,但若未来重构把这段
|
|
2414
2418
|
// 移入函数(cleanupViewer 等)或用 dev hot-reload,缺守卫会导致 handler 堆积
|
|
2415
2419
|
// → 进程退出时多次调用 stopViewer。globalThis flag 兼容所有 import 路径。
|
|
@@ -3,6 +3,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from '
|
|
|
3
3
|
import { readdir, stat } from 'node:fs/promises';
|
|
4
4
|
import { renameSyncWithRetry } from './lib/file-api.js';
|
|
5
5
|
import { withFileLockAsync } from './lib/async-file-lock.js';
|
|
6
|
+
import { dirSizeSync } from './lib/v2/layout.js';
|
|
6
7
|
import { join, basename, resolve } from 'node:path';
|
|
7
8
|
import { randomBytes } from 'node:crypto';
|
|
8
9
|
import { LOG_DIR } from '../findcc.js';
|
|
@@ -92,19 +93,38 @@ export async function removeWorkspace(id) {
|
|
|
92
93
|
export async function getWorkspaces() {
|
|
93
94
|
const list = loadWorkspaces();
|
|
94
95
|
const enriched = await Promise.all(list.map(async (w) => {
|
|
95
|
-
|
|
96
|
+
// wire-v2 (1.7.0): logs live in per-session dirs under sessions/. The v1
|
|
97
|
+
// *.jsonl glob is kept as unmigratedV1Count — the combined logCount must
|
|
98
|
+
// stay non-zero for a declined-migration workspace, because the launcher's
|
|
99
|
+
// `logCount > 0 → auto -c` heuristic hangs off it (WorkspaceList.jsx).
|
|
100
|
+
let sessionCount = 0;
|
|
101
|
+
let unmigratedV1Count = 0;
|
|
96
102
|
let totalSize = 0;
|
|
97
103
|
const logDir = join(LOG_DIR, w.projectName);
|
|
98
104
|
try {
|
|
99
105
|
const files = await readdir(logDir);
|
|
100
106
|
for (const f of files) {
|
|
101
|
-
if (f.endsWith('.jsonl')) {
|
|
102
|
-
|
|
107
|
+
if (f.endsWith('.jsonl') && !f.endsWith('_temp.jsonl')) {
|
|
108
|
+
unmigratedV1Count++;
|
|
103
109
|
try { totalSize += (await stat(join(logDir, f))).size; } catch { }
|
|
104
110
|
}
|
|
105
111
|
}
|
|
106
112
|
} catch { }
|
|
107
|
-
|
|
113
|
+
try {
|
|
114
|
+
const sids = await readdir(join(logDir, 'sessions'), { withFileTypes: true });
|
|
115
|
+
for (const e of sids) {
|
|
116
|
+
if (!e.isDirectory()) continue;
|
|
117
|
+
const sessionDir = join(logDir, 'sessions', e.name);
|
|
118
|
+
try {
|
|
119
|
+
await stat(join(sessionDir, 'journal.jsonl'));
|
|
120
|
+
// Folder size, not journal size — conv/blob/response files carry
|
|
121
|
+
// most of a session's bytes (journal alone undercounts ~12x).
|
|
122
|
+
totalSize += dirSizeSync(sessionDir);
|
|
123
|
+
sessionCount++;
|
|
124
|
+
} catch { /* dir without a journal is not a session yet */ }
|
|
125
|
+
}
|
|
126
|
+
} catch { }
|
|
127
|
+
return { ...w, logCount: sessionCount + unmigratedV1Count, sessionCount, unmigratedV1Count, totalSize };
|
|
108
128
|
}));
|
|
109
129
|
return enriched.sort((a, b) => new Date(b.lastUsed) - new Date(a.lastUsed));
|
|
110
130
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// 从 URL 中提取 LAN 访问 token,附加到所有 API 请求 / WebSocket 握手
|
|
2
|
+
const _urlToken = new URLSearchParams(window.location.search).get('token');
|
|
3
|
+
|
|
4
|
+
// 反向代理子路径部署: 从 <base> 标签或 SSR 注入的全局变量读取 base path,
|
|
5
|
+
// 使 API/WebSocket 等动态请求也能正确路由到代理后端。
|
|
6
|
+
export function getBasePath() {
|
|
7
|
+
if (typeof window !== 'undefined' && window.__CCV_BASE_PATH__) return window.__CCV_BASE_PATH__;
|
|
8
|
+
if (typeof document === 'undefined' || !document.querySelector) return '';
|
|
9
|
+
const base = document.querySelector('base');
|
|
10
|
+
if (base && base.href) return base.getAttribute('href');
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 把 token 追加到任意 URL(HTTP path 或 ws:// 完整 URL 皆可)。无 token 时原样返回。
|
|
15
|
+
// WS 握手必须也带 token —— 否则启用鉴权后远程「?token=」终端会被 socket.destroy()。
|
|
16
|
+
export function appendToken(url) {
|
|
17
|
+
if (!_urlToken) return url;
|
|
18
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
19
|
+
return `${url}${sep}token=${_urlToken}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function apiUrl(path) {
|
|
23
|
+
const base = getBasePath();
|
|
24
|
+
const fullPath = base ? base.replace(/\/$/, '') + path : path;
|
|
25
|
+
return appendToken(fullPath);
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Decision helper for ApprovalModal's fallback AskQuestionForm.
|
|
2
|
+
//
|
|
3
|
+
// The Question modal body is normally filled by the transcript tool_use block
|
|
4
|
+
// portaling an AskQuestionForm into the modal's ask slot (see askPortalMatcher.js).
|
|
5
|
+
// When no block portals in — a stale replayed ask whose block is old history, or a
|
|
6
|
+
// fresh ask arriving before transcript ingest — the modal would show an empty body.
|
|
7
|
+
// The fallback renders a form directly from the authoritative pending-ask broadcast.
|
|
8
|
+
//
|
|
9
|
+
// Contract: `slotOccupied` is DOM occupancy of the ask slot (MutationObserver on
|
|
10
|
+
// childList + sync initial read); `graceElapsed` flips true ASK_FALLBACK_GRACE_MS
|
|
11
|
+
// after the ask becomes visible, giving the real portal one commit cycle so fresh
|
|
12
|
+
// asks never flash the fallback before the portaled form mounts.
|
|
13
|
+
|
|
14
|
+
export const ASK_FALLBACK_GRACE_MS = 120;
|
|
15
|
+
|
|
16
|
+
export function shouldRenderAskFallback({ isAskActive, slotOccupied, graceElapsed, questions, submitHandler }) {
|
|
17
|
+
if (!isAskActive || slotOccupied || !graceElapsed) return false;
|
|
18
|
+
if (!Array.isArray(questions) || questions.length === 0) return false;
|
|
19
|
+
if (typeof submitHandler !== 'function') return false;
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { isPlaceholderAskId } from './askPortalMatcher.js';
|
|
2
|
+
|
|
3
|
+
// AskUserQuestion options[].description is schema-optional; centralize fallback
|
|
4
|
+
// so AskQuestionForm and ChatMessage recap stay aligned.
|
|
5
|
+
|
|
6
|
+
export function optionAriaLabel(opt) {
|
|
7
|
+
if (!opt || opt.label == null) return '';
|
|
8
|
+
return opt.description
|
|
9
|
+
? `${opt.label}: ${opt.description}`
|
|
10
|
+
: String(opt.label);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function hasOptionDescription(opt) {
|
|
14
|
+
return Boolean(opt && opt.description);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Pick the questions the interactive AskUserQuestion card actually renders.
|
|
18
|
+
//
|
|
19
|
+
// The modal body is filled by the inline tool_use block (reconstructed from the log
|
|
20
|
+
// stream) portaling into the modal's askSlot — it does not read pendingAsk.questions
|
|
21
|
+
// (the authoritative copy broadcast over WS ask-hook-pending / sdk-ask-pending, fully
|
|
22
|
+
// parsed server-side before the hook fires) directly. During streaming of a large
|
|
23
|
+
// payload there is a window where the modal is already open but the reconstructed
|
|
24
|
+
// block's input is still partial JSON. Crucially, partial-JSON parsing materializes
|
|
25
|
+
// the outer questions[] array shape before the element content arrives, so the
|
|
26
|
+
// streamed copy can be HOLLOW AT EQUAL LENGTH (e.g. two question objects with empty
|
|
27
|
+
// text/options while big options[].preview strings are still streaming — the
|
|
28
|
+
// 2026-07-04 blank-popup regression; a strictly-longer length heuristic missed it).
|
|
29
|
+
//
|
|
30
|
+
// So: for the currently pending ask (toolId === lastPendingAskId, id-matched), always
|
|
31
|
+
// prefer the authoritative copy. It is complete by construction, and the submit path
|
|
32
|
+
// (handleAskQuestionSubmit in askFlowController) maps answer indices through the same
|
|
33
|
+
// authoritative _askHookQuestions copy — rendering it keeps rendered indices aligned
|
|
34
|
+
// with the mapping array. Historical blocks never match lastPendingAskId and always
|
|
35
|
+
// keep their own streamed questions (history view unchanged).
|
|
36
|
+
//
|
|
37
|
+
// Legacy/no-id servers use placeholder pendingAsk ids ('__ask__', 'ask_<ts>_<rnd>')
|
|
38
|
+
// that can never equal the real tool_use id. There the authoritative copy is only
|
|
39
|
+
// borrowed when the streamed render would otherwise be completely empty — it can fill
|
|
40
|
+
// a blank popup but never override visible content (a stale legacy pendingAsk must
|
|
41
|
+
// not inject a previous ask's questions over a rendered one).
|
|
42
|
+
export function resolveAskQuestions(streamedQuestions, toolId, lastPendingAskId, pendingAsk) {
|
|
43
|
+
const streamed = Array.isArray(streamedQuestions) ? streamedQuestions : [];
|
|
44
|
+
if (!pendingAsk || toolId == null || toolId !== lastPendingAskId) {
|
|
45
|
+
return streamed;
|
|
46
|
+
}
|
|
47
|
+
const idMatches = pendingAsk.id === toolId;
|
|
48
|
+
if (!idMatches && !(isPlaceholderAskId(pendingAsk.id) && streamed.length === 0)) {
|
|
49
|
+
return streamed;
|
|
50
|
+
}
|
|
51
|
+
const authoritative = Array.isArray(pendingAsk.questions) ? pendingAsk.questions : [];
|
|
52
|
+
if (authoritative.length === 0) return streamed; // defensive: WS handlers guarantee non-empty
|
|
53
|
+
if (streamed.length > authoritative.length) return streamed; // never shrink (belt-and-braces)
|
|
54
|
+
return authoritative;
|
|
55
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portal 决策:判断 inline AskQuestionForm(在 ChatMessage 里渲染的某条 tool_use)
|
|
3
|
+
* 是否应该 createPortal 到 ApprovalModal 的 askSlot。
|
|
4
|
+
*
|
|
5
|
+
* activeAskId 三种合法形态(与 server 端协议绑定,改一边必须改另一边):
|
|
6
|
+
*
|
|
7
|
+
* 1. `toolu_xxx` — SDK 模式 / 新版 Claude Code 透传的真实 tool_use_id
|
|
8
|
+
* → 走 strict 匹配:activeAskId === toolId
|
|
9
|
+
*
|
|
10
|
+
* 2. `__ask__` — LEGACY 老 server(pre-Map 协议)的单槽占位
|
|
11
|
+
* → 走 owner 通配:仅 owner-idx 锁定的那条 ChatMessage(lastPendingAskId === toolId)命中
|
|
12
|
+
*
|
|
13
|
+
* 3. `ask_${ts}_${rnd}` — 老 Claude Code PreToolUse hook payload 不带 tool_use_id 时
|
|
14
|
+
* server.js 自生成的 fallback id(修复 modal 标题在但内容空白的根因)
|
|
15
|
+
* → 走 owner 通配:与 __ask__ 同语义
|
|
16
|
+
*
|
|
17
|
+
* 协议锚点:
|
|
18
|
+
* - server 端 fallback id 生成:server.js(搜索 `ask_${Date.now()}` 定位)
|
|
19
|
+
* - server 端 LEGACY 占位推送:server.js 推送 ask-hook-pending 事件时 id 为 '__ask__'
|
|
20
|
+
* - 前端 setState pendingAsk:src/components/chat/ChatView.jsx
|
|
21
|
+
*
|
|
22
|
+
* 改 server 端 fallback id 格式时,必须同步更新此文件的 isFallbackId 检测。
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const FALLBACK_ID_PREFIX = 'ask_';
|
|
26
|
+
const LEGACY_PLACEHOLDER_ID = '__ask__';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* True when the given pending-ask id is a placeholder that can never equal a real
|
|
30
|
+
* tool_use id: the LEGACY single-slot '__ask__' or a server-generated 'ask_<ts>_<rnd>'
|
|
31
|
+
* fallback id. Consumers (e.g. resolveAskQuestions) use this to apply the same
|
|
32
|
+
* owner-wildcard semantics as shouldPortalAskForm on the legacy paths.
|
|
33
|
+
*/
|
|
34
|
+
export function isPlaceholderAskId(id) {
|
|
35
|
+
return id === LEGACY_PLACEHOLDER_ID
|
|
36
|
+
|| (typeof id === 'string' && id.startsWith(FALLBACK_ID_PREFIX));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {*} activeAskId ApprovalPortalContext.Consumer 提供的 activeAskId(modal 当前 active ask 的 id)
|
|
41
|
+
* @param {*} toolId 当前 ChatMessage 里 tool_use 块的 id(toolu_xxx)
|
|
42
|
+
* @param {*} lastPendingAskId ChatMessage 收到的 owner-idx 派生信号;非 owner 永远为 null
|
|
43
|
+
* @returns {boolean} true 表示当前 ChatMessage 应该 portal 到 askSlot
|
|
44
|
+
*/
|
|
45
|
+
export function shouldPortalAskForm(activeAskId, toolId, lastPendingAskId) {
|
|
46
|
+
if (activeAskId == null) return false;
|
|
47
|
+
// strict:SDK / 新 Claude Code 真实 tool_use_id
|
|
48
|
+
if (String(activeAskId) === String(toolId)) return true;
|
|
49
|
+
// owner 通配:__ask__(LEGACY 老 server)∪ ask_*(server fallback)
|
|
50
|
+
// 必须同时满足 owner-idx 锁定(lastPendingAskId === toolId),否则会让历史所有真实 toolId
|
|
51
|
+
// 都被通配命中,重现旧的双份 portal bug
|
|
52
|
+
if (isPlaceholderAskId(activeAskId) && lastPendingAskId === toolId) return true;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// 自动审批档位唯一事实源 —— 全部消费点共用同一组档位,防止各处字面量发散:
|
|
2
|
+
// 1. TerminalPanel 终端工具栏四芒星快捷菜单(权限/Plan 两行级联档位)
|
|
3
|
+
// 2. ChatInputBar 对话输入框四芒星快捷菜单(隐藏终端时,同款两行)
|
|
4
|
+
// 3. AppHeader 设置抽屉的权限/Plan 两个 Select
|
|
5
|
+
// 4. Mobile 设置面板的权限/Plan 两个 Select
|
|
6
|
+
// 三态语义(持久化于 preferences.autoApproveSeconds / approvalModal.planAutoApproveSeconds):
|
|
7
|
+
// 0 = 关闭(手动审批)
|
|
8
|
+
// > 0 = 倒计时该秒数后自动批准
|
|
9
|
+
// -1 = 免审批(AUTO_APPROVE_INSTANT):请求到达即放行,从源头绕过审批面板
|
|
10
|
+
// 本模块不依赖 helpers.js(它带 .svg?raw 等 Vite 专属 import),保持 node --test 可直接引用。
|
|
11
|
+
export const AUTO_APPROVE_INSTANT = -1;
|
|
12
|
+
|
|
13
|
+
export const PERM_AUTO_APPROVE_OPTIONS = [0, 3, 5, 10, AUTO_APPROVE_INSTANT];
|
|
14
|
+
export const PLAN_AUTO_APPROVE_OPTIONS = [0, 10, 30, 60, AUTO_APPROVE_INSTANT];
|
|
15
|
+
|
|
16
|
+
// 单个档位值 -> 显示文案;t 由调用方注入,避免本模块依赖前端 i18n。
|
|
17
|
+
// 档位之外的历史值(如 Plan 旧 3/5)同样落在 `${v}s` 分支正常显示。
|
|
18
|
+
export function autoApproveValueLabel(v, t) {
|
|
19
|
+
if (v === AUTO_APPROVE_INSTANT) return t('ui.permission.autoApprove.instant');
|
|
20
|
+
if (!v) return t('ui.permission.autoApprove.off');
|
|
21
|
+
return `${v}s`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 档位数组 -> antd Select options
|
|
25
|
+
export function autoApproveSelectOptions(values, t) {
|
|
26
|
+
return values.map(v => ({ value: v, label: autoApproveValueLabel(v, t) }));
|
|
27
|
+
}
|