cc-viewer 1.6.333 → 1.6.335
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 +9 -0
- package/cli.js +6 -1
- package/dist/assets/App-CEJXKHHN.css +1 -0
- package/dist/assets/App-CuBCEhei.js +2 -0
- package/dist/assets/{MdxEditorPanel-B0pZ244F.js → MdxEditorPanel-BrnxiPH_.js} +1 -1
- package/dist/assets/{MdxEditorPanel-1vSx6ymU.css → MdxEditorPanel-Dpn-4ydO.css} +1 -1
- package/dist/assets/Mobile-B5V_-6Hc.css +1 -0
- package/dist/assets/{Mobile-ZwY4z6l3.js → Mobile-RbK1_88q.js} +1 -1
- package/dist/assets/{_baseUniq-DWlfWgt4.js → _baseUniq-DRoq9q8B.js} +1 -1
- package/dist/assets/{arc-CzmkWEz_.js → arc-Cvi1SHNs.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-CYaOQmkg.js → architectureDiagram-Q4EWVU46-BlqmPh3g.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-Dall5YkT.js → blockDiagram-DXYQGD6D-Dyk7JSWO.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-DAk_9Ur1.js → c4Diagram-AHTNJAMY-C00cDbc4.js} +1 -1
- package/dist/assets/{channel-Ccyxypgy.js → channel-CdXT5cZK.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-CvbVyElK.js → chunk-4BX2VUAB-CSv6_viy.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-BRyhy_Du.js → chunk-4TB4RGXK-B7dfdEMX.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-a32qQl3r.js → chunk-55IACEB6-CzIsElZc.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-KDrHEMBT.js → chunk-EDXVE4YY-C_16c0SJ.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-BY0F4AhW.js → chunk-FMBD7UC4-BEsH6vuG.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-7lPaUOt9.js → chunk-OYMX7WX6-ByJtK0pa.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-C9UszWzd.js → chunk-QZHKN3VN-B2C0Ey4D.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-YG3JSfbH.js → chunk-YZCP3GAM-DYeerN90.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-0orWWSeB.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-0orWWSeB.js +1 -0
- package/dist/assets/clone-CqkWyivT.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-DIU72glS.js → cose-bilkent-S5V4N54A-nf2MgVIM.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-EouMsh-P.js → dagre-KV5264BT-QuXoeQ8Q.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-Cgr24o-m.js → diagram-5BDNPKRD-D7dXnuBC.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-DSNxAipm.js → diagram-G4DWMVQ6-CJkDxGbe.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-D8OP-yt5.js → diagram-MMDJMWI5-B0y-_dq3.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-BF-y_3p_.js → diagram-TYMM5635-D-sAYexO.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-kJejykzr.js → erDiagram-SMLLAGMA-45kKhs_p.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-B7mSPWGk.js → flowDiagram-DWJPFMVM-cHZi71BI.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-DN1NKD86.js → ganttDiagram-T4ZO3ILL-AOMWmRX8.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-gN8Ai_7H.js → gitGraphDiagram-UUTBAWPF-BbA6UT1O.js} +1 -1
- package/dist/assets/{graph-CSC5UcCt.js → graph-C2vUhsyb.js} +1 -1
- package/dist/assets/{index-DI6crI9z.js → index-BZPd8BxU.js} +1 -1
- package/dist/assets/{index-Cwi-o2jM.js → index-Bg-5xzbn.js} +1 -1
- package/dist/assets/{index-CLOW0OUz.js → index-Ca1pfEu9.js} +1 -1
- package/dist/assets/{index-Cv33V1Pr.js → index-DVyCY4lq.js} +1 -1
- package/dist/assets/index-Dy14VaBK.js +2 -0
- package/dist/assets/{index-DHgN3ufc.js → index-JyReQHe7.js} +1 -1
- package/dist/assets/{index-C5sBul9J.js → index-NJXIDr7-.js} +1 -1
- package/dist/assets/{index-bUhmgSxt.js → index-NnF6whsx.js} +1 -1
- package/dist/assets/index-f4nPU7KM.css +1 -0
- package/dist/assets/{infoDiagram-42DDH7IO-DuSgqRNf.js → infoDiagram-42DDH7IO-JZCrFgrr.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-B4EdK2CC.js → ishikawaDiagram-UXIWVN3A-CnQoRWsu.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-B5wbjiO8.js → journeyDiagram-VCZTEJTY-CHPiVGlE.js} +1 -1
- package/dist/assets/{jszip.min-Dx820onf.js → jszip.min-d8dJpMtW.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-1UxMJBgT.js → kanban-definition-6JOO6SKY--vaCznms.js} +1 -1
- package/dist/assets/{layout-BZ4cy_A3.js → layout-C6UBgcNI.js} +1 -1
- package/dist/assets/{linear-BKdLoimm.js → linear-g0XsIl_6.js} +1 -1
- package/dist/assets/{mermaid.core-DMom7_rg.js → mermaid.core-BAPdKagI.js} +2 -2
- package/dist/assets/{min-wRNE9X51.js → min-GUAWxzVx.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-BNmfJpA6.js → mindmap-definition-QFDTVHPH-D3NJObYN.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-vaH4-8Or.js → pieDiagram-DEJITSTG-B2qdCrvu.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-C2AY6FOB.js → quadrantDiagram-34T5L4WZ-y5VgO9qJ.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-aUdzXxVm.js → requirementDiagram-MS252O5E-M73JyLZ4.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-911CtS8z.js → sankeyDiagram-XADWPNL6-r4eNItTO.js} +1 -1
- package/dist/assets/seqResourceLoaders-BDbYWuli.js +2 -0
- package/dist/assets/seqResourceLoaders-DG5M9M0e.css +41 -0
- package/dist/assets/{sequenceDiagram-FGHM5R23-2R6stVjM.js → sequenceDiagram-FGHM5R23-CyaSnCUK.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-CbgRIOYj.js → stateDiagram-FHFEXIEX-I57EKIQ6.js} +1 -1
- package/dist/assets/{stateDiagram-v2-QKLJ7IA2-Xsvwai6m.js → stateDiagram-v2-QKLJ7IA2-CzouMKFP.js} +1 -1
- package/dist/assets/{timeline-definition-GMOUNBTQ-BhhXvDyp.js → timeline-definition-GMOUNBTQ-BsZ7rRxl.js} +1 -1
- package/dist/assets/{vendor-antd-DM2TzIqO.js → vendor-antd-Be06DOUh.js} +1 -1
- package/dist/assets/{vendor-codemirror-Bfrxv9Q7.js → vendor-codemirror-C9Tqo5sR.js} +1 -1
- package/dist/assets/{vendor-mdxeditor-CtZ5Rl88.js → vendor-mdxeditor-C927Ebjk.js} +2 -2
- package/dist/assets/{vendor-qrcode-55J239aW.js → vendor-qrcode-d7HbbJ8T.js} +1 -1
- package/dist/assets/{vendor-virtuoso-DY16TiQ-.js → vendor-virtuoso-TtNQfqSa.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-H7FLlPyO.js → vennDiagram-DHZGUBPP-exSnurzw.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-Dp35Fq7j.js → wardley-RL74JXVD-C3w7f85J.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-CcKrzQBH.js → wardleyDiagram-NUSXRM2D-BcHQWT7G.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-CwTqQPcT.js → xychartDiagram-5P7HB3ND-CjUkYNTb.js} +1 -1
- package/dist/index.html +7 -6
- package/findcc.js +52 -5
- package/package.json +5 -3
- package/server/lib/ask-bridge.js +45 -0
- package/server/lib/ask-constants.js +8 -0
- package/server/lib/ask-reaper.js +164 -0
- package/server/lib/auth.js +2 -1
- package/server/lib/model-system-prompts.js +253 -0
- package/server/lib/system-prompt-files.js +32 -5
- package/server/pty-manager.js +30 -4
- package/server/routes/ask-perm.js +84 -1
- package/server/routes/expert.js +84 -0
- package/server/server.js +74 -4
- package/dist/assets/App-BUcRXok_.js +0 -2
- package/dist/assets/App-Bla1Fr_X.css +0 -1
- package/dist/assets/Mobile-DolwHzoQ.css +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-CYXcQTEj.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-CYXcQTEj.js +0 -1
- package/dist/assets/clone-BRA0Q3Eo.js +0 -1
- package/dist/assets/index-BMKeFQ8D.js +0 -2
- package/dist/assets/index-Cy7Xfu81.css +0 -1
- package/dist/assets/seqResourceLoaders-BPRKtmaY.js +0 -2
- package/dist/assets/seqResourceLoaders-P_ytfji6.css +0 -41
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Ask / permission hook bridge routes (moved verbatim from server.js handleRequest).
|
|
2
|
-
import { loadAskStore, consumeIfFinal as askStoreConsumeIfFinal } from '../lib/ask-store.js';
|
|
2
|
+
import { loadAskStore, consumeIfFinal as askStoreConsumeIfFinal, markCancelled as askStoreMarkCancelled } from '../lib/ask-store.js';
|
|
3
3
|
import { runWaterfallHook, runParallelHook } from '../lib/plugin-loader.js';
|
|
4
4
|
import { sendEventToClients } from '../lib/log-watcher.js';
|
|
5
5
|
|
|
@@ -128,6 +128,9 @@ function askHook(req, res, parsedUrl, isLocal, deps) {
|
|
|
128
128
|
const _placeholderEntry = { questions, res, timer: null, createdAt: Date.now(), shortPoll: shortPollMode };
|
|
129
129
|
deps.pendingAskHooks.set(id, _placeholderEntry);
|
|
130
130
|
deps.persistAskEntry(id, _placeholderEntry);
|
|
131
|
+
// Seed waiter liveness for the reaper (ask-reaper.js). Optional chaining:
|
|
132
|
+
// older deps stubs and third-party callers may not provide the map.
|
|
133
|
+
if (shortPollMode) deps.askWaiterLastPoll?.set(id, Date.now());
|
|
131
134
|
|
|
132
135
|
// res.on('close') 提前注册:handler 用 entry.timer 守卫(占位期 timer:null → 仅 delete Map)
|
|
133
136
|
// Phase 3: short-poll 模式下 res 主动 end,close 不视为 client cancel;entry 由 24h timer 兜底清理。
|
|
@@ -237,6 +240,10 @@ async function askHookResult(req, res, parsedUrl, isLocal, deps) {
|
|
|
237
240
|
}
|
|
238
241
|
const qs = new URLSearchParams(m[2] || '');
|
|
239
242
|
const wait = Math.max(1000, Math.min(60000, parseInt(qs.get('wait') || '30000', 10)));
|
|
243
|
+
// Refresh waiter liveness on every poll arrival (see ask-reaper.js). Unconditional:
|
|
244
|
+
// even a poll that ends in 404/final proves the waiter process is alive; the reaper
|
|
245
|
+
// GCs records for ids that are no longer pending.
|
|
246
|
+
deps.askWaiterLastPoll?.set(id, Date.now());
|
|
240
247
|
|
|
241
248
|
// 1) disk 命中 answered/cancelled:浏览器答得早过 GET 到达 → 立即返并消费(一次性)
|
|
242
249
|
// 用 consumeIfFinal 单次 withLock 内判 status 决定是否 delete —— 旧设计的
|
|
@@ -289,6 +296,81 @@ async function askHookResult(req, res, parsedUrl, isLocal, deps) {
|
|
|
289
296
|
}
|
|
290
297
|
}
|
|
291
298
|
|
|
299
|
+
// POST /api/ask-hook/:id/cancel — best-effort cancel from a dying hook process
|
|
300
|
+
// (ask-bridge SIGTERM/SIGINT handler) or any client that wants to durably resolve
|
|
301
|
+
// a pending ask. Mirrors the WS ask-cancel handler in server.js for the hook path.
|
|
302
|
+
// Auth: standard request prelude (loopback-exempt), same as the sibling ask-hook routes.
|
|
303
|
+
function askHookCancel(req, res, parsedUrl, isLocal, deps) {
|
|
304
|
+
const m = parsedUrl.pathname.match(/^\/api\/ask-hook\/([^/?]+)\/cancel$/);
|
|
305
|
+
const id = m ? decodeURIComponent(m[1]) : '';
|
|
306
|
+
if (!id || id.length > 256 || !/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
307
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
308
|
+
res.end(JSON.stringify({ error: 'invalid id' }));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
let body = '';
|
|
312
|
+
let bodyTooLarge = false;
|
|
313
|
+
req.on('data', (chunk) => {
|
|
314
|
+
if (bodyTooLarge) return;
|
|
315
|
+
body += chunk;
|
|
316
|
+
if (body.length > 4096) {
|
|
317
|
+
// Respond here, not in 'end': req.destroy() never emits 'end' in real Node,
|
|
318
|
+
// so a deferred 413 would be dead code (mirrors permHook's in-data rejection).
|
|
319
|
+
bodyTooLarge = true;
|
|
320
|
+
try { if (!res.headersSent) { res.writeHead(413, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Request body too large' })); } } catch {}
|
|
321
|
+
req.destroy();
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
req.on('end', async () => {
|
|
325
|
+
if (bodyTooLarge) return;
|
|
326
|
+
let reason = 'hook process exited';
|
|
327
|
+
try {
|
|
328
|
+
const parsed = body ? JSON.parse(body) : {};
|
|
329
|
+
if (typeof parsed.reason === 'string' && parsed.reason) reason = parsed.reason.slice(0, 500);
|
|
330
|
+
} catch {}
|
|
331
|
+
// Respond fast: the caller may be a process that is about to die.
|
|
332
|
+
try { if (!res.headersSent) { res.writeHead(204); res.end(); } } catch {}
|
|
333
|
+
try {
|
|
334
|
+
let handled = false;
|
|
335
|
+
const entry = deps.pendingAskHooks.get(id);
|
|
336
|
+
if (entry) {
|
|
337
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
338
|
+
deps.pendingAskHooks.delete(id);
|
|
339
|
+
deps.askWaiterLastPoll?.delete(id);
|
|
340
|
+
if (entry.shortPoll) {
|
|
341
|
+
const wrote = await askStoreMarkCancelled(id, reason);
|
|
342
|
+
if (wrote) deps.notifyShortPollCancel?.(id, reason);
|
|
343
|
+
handled = wrote;
|
|
344
|
+
} else {
|
|
345
|
+
// Long-poll: end the hanging hook response so the bridge unblocks.
|
|
346
|
+
try {
|
|
347
|
+
if (entry.res && !entry.res.headersSent) {
|
|
348
|
+
entry.res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
349
|
+
entry.res.end(JSON.stringify({ cancelled: true, reason }));
|
|
350
|
+
}
|
|
351
|
+
} catch {}
|
|
352
|
+
deps.persistAskDelete(id);
|
|
353
|
+
handled = true;
|
|
354
|
+
}
|
|
355
|
+
} else {
|
|
356
|
+
// Disk-only (e.g. entry orphaned by a previous server process). Wake any
|
|
357
|
+
// hanging poll listeners too — parity with the WS ask-cancel handler.
|
|
358
|
+
handled = await askStoreMarkCancelled(id, reason);
|
|
359
|
+
if (handled) deps.notifyShortPollCancel?.(id, reason);
|
|
360
|
+
}
|
|
361
|
+
if (handled) {
|
|
362
|
+
if (deps.terminalWss) {
|
|
363
|
+
const cmsg = JSON.stringify({ type: 'ask-hook-cancelled', id, reason });
|
|
364
|
+
deps.terminalWss.clients.forEach((c) => {
|
|
365
|
+
if (c.readyState === 1) try { c.send(cmsg); } catch {}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
deps.notifyParentPending({ type: 'ask-hook-cancelled', id });
|
|
369
|
+
}
|
|
370
|
+
} catch {}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
292
374
|
// Permission hook bridge: receive tool permission request from perm-bridge.js, long-poll for user decision
|
|
293
375
|
function permHook(req, res, parsedUrl, isLocal, deps) {
|
|
294
376
|
let body = '';
|
|
@@ -447,6 +529,7 @@ export const askPermRoutes = [
|
|
|
447
529
|
{ method: 'GET', match: 'exact', path: '/api/pending-asks', handler: pendingAsks },
|
|
448
530
|
{ method: 'POST', match: 'exact', path: '/api/ask-hook', handler: askHook },
|
|
449
531
|
{ predicate: (url, method) => url.startsWith('/api/ask-hook/') && url.includes('/result') && method === 'GET', handler: askHookResult },
|
|
532
|
+
{ predicate: (url, method) => url.startsWith('/api/ask-hook/') && url.includes('/cancel') && method === 'POST', handler: askHookCancel },
|
|
450
533
|
{ method: 'POST', match: 'exact', path: '/api/perm-hook', handler: permHook },
|
|
451
534
|
{ method: 'POST', match: 'exact', path: '/api/stream-chunk', handler: streamChunk },
|
|
452
535
|
];
|
package/server/routes/expert.js
CHANGED
|
@@ -2,8 +2,17 @@
|
|
|
2
2
|
// (CC_SYSTEM.md / CC_APPEND_SYSTEM.md),对应偏好设置 → 专家设置 → 系统文本修改。
|
|
3
3
|
// 这两个文件由 pty-manager._spawnClaudeImpl 在启动 claude 时自动注入为
|
|
4
4
|
// --system-prompt-file / --append-system-prompt-file(见 server/lib/system-prompt-files.js)。
|
|
5
|
+
// 另有「按模型定制」条目(/api/expert/model-prompts):工作区 <ws>/system_prompt/ 与
|
|
6
|
+
// 全局 <LOG_DIR>/system_prompt/ 两套目录,见 server/lib/model-system-prompts.js。
|
|
5
7
|
// 鉴权沿用 dispatch 之前的全局鉴权(与 files-fs 写操作一致,不额外 gate isLocal)。
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
6
10
|
import { readWorkspaceSystemText, writeWorkspaceSystemText } from '../lib/system-prompt-files.js';
|
|
11
|
+
import {
|
|
12
|
+
MODEL_PROMPT_DIR, normalizeModelName, listModelPrompts,
|
|
13
|
+
writeModelPrompt, deleteModelPrompt,
|
|
14
|
+
} from '../lib/model-system-prompts.js';
|
|
15
|
+
import { LOG_DIR } from '../../findcc.js';
|
|
7
16
|
|
|
8
17
|
// 当前工作区目录全部由服务端解析(绝不接收客户端路径 → 无遍历面):
|
|
9
18
|
// 运行中/最近一次 claude 的 cwd > CCV_PROJECT_DIR > (仅非工作区模式才回退 process.cwd())
|
|
@@ -66,7 +75,82 @@ function postSystemText(req, res, parsedUrl, isLocal, deps) {
|
|
|
66
75
|
});
|
|
67
76
|
}
|
|
68
77
|
|
|
78
|
+
// 列出某目录全部模型条目并内联文本(条目数很小,一次返回免去 N 次跟进请求)。
|
|
79
|
+
// 直接读 listModelPrompts 给出的生效文件,避免 readModelPrompt 每条目重扫目录(N+1)。
|
|
80
|
+
function collectModelEntries(dir) {
|
|
81
|
+
if (!dir) return [];
|
|
82
|
+
return listModelPrompts(dir)
|
|
83
|
+
.map((e) => {
|
|
84
|
+
try {
|
|
85
|
+
return { name: e.name, mode: e.mode, text: readFileSync(join(dir, e.fileName), 'utf-8') };
|
|
86
|
+
} catch {
|
|
87
|
+
return null; // 列表后被并发删除等:跳过该条目
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
.filter(Boolean);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function getModelPrompts(req, res, parsedUrl, isLocal, deps) {
|
|
94
|
+
try {
|
|
95
|
+
const dir = await resolveDir(deps);
|
|
96
|
+
const globalDir = join(LOG_DIR, MODEL_PROMPT_DIR);
|
|
97
|
+
sendJson(res, 200, {
|
|
98
|
+
workspaceDir: dir || null,
|
|
99
|
+
workspaceActive: !!dir,
|
|
100
|
+
globalDir,
|
|
101
|
+
workspace: dir ? collectModelEntries(join(dir, MODEL_PROMPT_DIR)) : [],
|
|
102
|
+
global: collectModelEntries(globalDir),
|
|
103
|
+
});
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.error('[CC Viewer] expert model-prompts GET failed:', e.message);
|
|
106
|
+
sendJson(res, 500, { error: 'read_failed' });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function postModelPrompts(req, res, parsedUrl, isLocal, deps) {
|
|
111
|
+
let body = '';
|
|
112
|
+
let truncated = false;
|
|
113
|
+
req.on('data', (chunk) => {
|
|
114
|
+
body += chunk;
|
|
115
|
+
if (body.length > deps.MAX_POST_BODY) { truncated = true; req.destroy(); }
|
|
116
|
+
});
|
|
117
|
+
req.on('end', async () => {
|
|
118
|
+
if (truncated) return; // 超限已 destroy,socket 关闭,勿再解析/回包(对齐 postSystemText)
|
|
119
|
+
try {
|
|
120
|
+
const { scope, name, mode, text } = JSON.parse(body || '{}');
|
|
121
|
+
if (scope !== 'workspace' && scope !== 'global') {
|
|
122
|
+
sendJson(res, 400, { error: 'bad_scope' });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const canonical = normalizeModelName(name);
|
|
126
|
+
if (!canonical) { sendJson(res, 400, { error: 'bad_model_name' }); return; }
|
|
127
|
+
let targetDir;
|
|
128
|
+
if (scope === 'global') {
|
|
129
|
+
targetDir = join(LOG_DIR, MODEL_PROMPT_DIR);
|
|
130
|
+
} else {
|
|
131
|
+
const dir = await resolveDir(deps);
|
|
132
|
+
if (!dir) { sendJson(res, 400, { error: 'no_active_workspace' }); return; }
|
|
133
|
+
targetDir = join(dir, MODEL_PROMPT_DIR);
|
|
134
|
+
}
|
|
135
|
+
const raw = typeof text === 'string' ? text : '';
|
|
136
|
+
if (raw.trim().length === 0) {
|
|
137
|
+
// 空文本 = 删除条目(对齐 system-text 的「存空即禁用」约定;此时 mode 可缺省)。
|
|
138
|
+
deleteModelPrompt(targetDir, canonical);
|
|
139
|
+
sendJson(res, 200, { ok: true, name: canonical, scope, cleared: true });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const result = writeModelPrompt(targetDir, canonical, mode === 'override' ? 'override' : 'append', raw);
|
|
143
|
+
sendJson(res, 200, { ok: true, scope, ...result });
|
|
144
|
+
} catch (e) {
|
|
145
|
+
console.error('[CC Viewer] expert model-prompts POST failed:', e.message);
|
|
146
|
+
sendJson(res, 500, { error: 'write_failed' });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
69
151
|
export const expertRoutes = [
|
|
70
152
|
{ method: 'GET', match: 'exact', path: '/api/expert/system-text', handler: getSystemText },
|
|
71
153
|
{ method: 'POST', match: 'exact', path: '/api/expert/system-text', handler: postSystemText },
|
|
154
|
+
{ method: 'GET', match: 'exact', path: '/api/expert/model-prompts', handler: getModelPrompts },
|
|
155
|
+
{ method: 'POST', match: 'exact', path: '/api/expert/model-prompts', handler: postModelPrompts },
|
|
72
156
|
];
|
package/server/server.js
CHANGED
|
@@ -9,8 +9,9 @@ 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 { setEntry as askStoreSetEntry, deleteEntry as askStoreDeleteEntry, pruneStale as askStorePruneStale, markAnswered as askStoreMarkAnswered, markCancelled as askStoreMarkCancelled } from './lib/ask-store.js';
|
|
13
|
-
import { ASK_TIMEOUT_MS } from './lib/ask-constants.js';
|
|
12
|
+
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
|
+
import { ASK_TIMEOUT_MS, ASK_WAITER_REAP_INTERVAL_MS, ASK_WAITER_LIVENESS_MS } from './lib/ask-constants.js';
|
|
14
|
+
import { reapDeadAskWaiters, sweepOrphanedDiskAsks } from './lib/ask-reaper.js';
|
|
14
15
|
import { sdkApprovalCloseType } from './lib/sdk-adapter.js';
|
|
15
16
|
import { DIST_DIR, NODE_MODULES } from './_paths.js';
|
|
16
17
|
import { createDispatcher } from './routes/_dispatch.js';
|
|
@@ -74,7 +75,7 @@ function execWithStdin(cmd, args, input, options) {
|
|
|
74
75
|
}
|
|
75
76
|
import { LOG_FILE, _initPromise, _resumeState, _projectName, _logDir, streamingState, resetStreamingState, PROFILE_PATH, setLivePort, getImLiveText, resetImLiveText } from './interceptor.js';
|
|
76
77
|
import { recordInstance, listInstances } from './lib/instance-registry.js';
|
|
77
|
-
import { LOG_DIR, setLogDir, getClaudeConfigDir } from '../findcc.js';
|
|
78
|
+
import { LOG_DIR, setLogDir, getClaudeConfigDir, isBrowserOpenSuppressed } from '../findcc.js';
|
|
78
79
|
import { t, getLang, setLang } from './i18n.js';
|
|
79
80
|
import { loadAuthConfig, loadAuthState, saveAuthConfig, clearProjectOverride, generatePassword, decideAuth, parseCookies, renderLoginPage, localeFromAcceptLanguage } from './lib/auth.js';
|
|
80
81
|
import { checkAndUpdate } from './lib/updater.js';
|
|
@@ -195,6 +196,11 @@ function _persistAskDelete(id) {
|
|
|
195
196
|
// until either an answer/cancel arrives or wait ms elapses (then 204).
|
|
196
197
|
const shortPollListeners = new Map(); // id -> Set<{ res, tid, finished }>
|
|
197
198
|
|
|
199
|
+
// Waiter-liveness tracking for short-poll asks (see server/lib/ask-reaper.js).
|
|
200
|
+
// Memory-only BY DESIGN: persisting it would bump the ask-store SCHEMA_VERSION;
|
|
201
|
+
// the reaper's boot-time orphan sweep covers restarts instead.
|
|
202
|
+
const askWaiterLastPoll = new Map(); // id -> ms of last POST create / GET result poll
|
|
203
|
+
|
|
198
204
|
function _notifyShortPollAnswer(id, answers) {
|
|
199
205
|
const set = shortPollListeners.get(id);
|
|
200
206
|
if (!set) return;
|
|
@@ -493,6 +499,8 @@ const deps = {
|
|
|
493
499
|
pendingAskHooks,
|
|
494
500
|
pendingPermHooks,
|
|
495
501
|
shortPollListeners,
|
|
502
|
+
askWaiterLastPoll,
|
|
503
|
+
notifyShortPollCancel: _notifyShortPollCancel,
|
|
496
504
|
editorSessions,
|
|
497
505
|
gitRestoreLocks,
|
|
498
506
|
liveStreamLastSeq: _liveStreamLastSeq,
|
|
@@ -864,6 +872,56 @@ export async function startViewer() {
|
|
|
864
872
|
}, 60 * 60 * 1000);
|
|
865
873
|
_pruneAskStoreInterval.unref();
|
|
866
874
|
|
|
875
|
+
// Waiter-liveness reaper: resolves short-poll asks whose hook process (ask-bridge)
|
|
876
|
+
// died without notifying us (e.g. AskUserQuestion declined at the CLI → SIGTERM).
|
|
877
|
+
// Keys on waiter liveness, never on wall-clock ask age — the "GUI effectively
|
|
878
|
+
// no-timeout" contract is untouched. See server/lib/ask-reaper.js.
|
|
879
|
+
const _reaperDeps = {
|
|
880
|
+
pendingAskHooks,
|
|
881
|
+
shortPollListeners,
|
|
882
|
+
askWaiterLastPoll,
|
|
883
|
+
markCancelled: askStoreMarkCancelled,
|
|
884
|
+
loadAskStore: askStoreLoad,
|
|
885
|
+
notifyShortPollCancel: _notifyShortPollCancel,
|
|
886
|
+
// terminalWss is assigned later in startServer — resolve at call time via closure.
|
|
887
|
+
broadcastCancelled: (id, reason) => {
|
|
888
|
+
if (!terminalWss) return;
|
|
889
|
+
const cmsg = JSON.stringify({ type: 'ask-hook-cancelled', id, reason });
|
|
890
|
+
terminalWss.clients.forEach((c) => {
|
|
891
|
+
if (c.readyState === 1) try { c.send(cmsg); } catch {}
|
|
892
|
+
});
|
|
893
|
+
},
|
|
894
|
+
notifyParentPending: _notifyParentPending,
|
|
895
|
+
};
|
|
896
|
+
const _reaperState = { lastSweepAt: Date.now() };
|
|
897
|
+
_askReaperTimer = setInterval(() => {
|
|
898
|
+
reapDeadAskWaiters(_reaperDeps, _reaperState).catch(() => {});
|
|
899
|
+
}, ASK_WAITER_REAP_INTERVAL_MS);
|
|
900
|
+
_askReaperTimer.unref();
|
|
901
|
+
// One-shot boot sweep for disk-only orphans left by a previous server process.
|
|
902
|
+
// Delayed one liveness window so a bridge that survived our restart can re-poll
|
|
903
|
+
// and prove ownership first; skipped when another cc-viewer instance is running.
|
|
904
|
+
// Also skipped entirely under custom CCV_START_PORT/CCV_MAX_PORT: the lsof scan
|
|
905
|
+
// covers only OUR range, so an instance on a different custom range would be
|
|
906
|
+
// invisible and its live asks could be falsely swept. The memory-owned reaper
|
|
907
|
+
// and the ApprovalModal fallback UI still cover orphans in that setup.
|
|
908
|
+
const _customPortRange = !!(process.env.CCV_START_PORT || process.env.CCV_MAX_PORT);
|
|
909
|
+
const _bootTime = Date.now();
|
|
910
|
+
if (!_customPortRange) {
|
|
911
|
+
_askOrphanSweepTimer = setTimeout(() => {
|
|
912
|
+
sweepOrphanedDiskAsks({
|
|
913
|
+
..._reaperDeps,
|
|
914
|
+
bootTime: _bootTime,
|
|
915
|
+
ownPid: process.pid,
|
|
916
|
+
portRange: [START_PORT, MAX_PORT],
|
|
917
|
+
// Async on purpose: a wedged lsof would otherwise block the event loop
|
|
918
|
+
// (and every in-flight request) for up to the full timeout.
|
|
919
|
+
lsofImpl: async (cmd) => (await execAsync(cmd, { timeout: 2000, encoding: 'utf-8' })).stdout,
|
|
920
|
+
}).catch(() => {});
|
|
921
|
+
}, ASK_WAITER_LIVENESS_MS);
|
|
922
|
+
_askOrphanSweepTimer.unref();
|
|
923
|
+
}
|
|
924
|
+
|
|
867
925
|
// 通过插件 hook 获取 HTTPS 证书选项
|
|
868
926
|
let httpsOptions = null;
|
|
869
927
|
try {
|
|
@@ -946,11 +1004,12 @@ export async function startViewer() {
|
|
|
946
1004
|
}
|
|
947
1005
|
}
|
|
948
1006
|
// v2.0.69 之前的版本会清空控制台,自动打开浏览器确保用户能看到界面
|
|
1007
|
+
// L7 sandbox: suppressed under tests / CCV_NO_OPEN=1 (see findcc.js).
|
|
949
1008
|
try {
|
|
950
1009
|
const ccPkgPath = join(NODE_MODULES, '@anthropic-ai', 'claude-code', 'package.json');
|
|
951
1010
|
const ccVer = JSON.parse(readFileSync(ccPkgPath, 'utf-8')).version;
|
|
952
1011
|
const [maj, min, pat] = ccVer.split('.').map(Number);
|
|
953
|
-
if (maj < 2 || (maj === 2 && min === 0 && pat < 69)) {
|
|
1012
|
+
if ((maj < 2 || (maj === 2 && min === 0 && pat < 69)) && !isBrowserOpenSuppressed()) {
|
|
954
1013
|
const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open';
|
|
955
1014
|
execAsync(`${cmd} ${url}`, { timeout: 5000 }).catch(() => {});
|
|
956
1015
|
}
|
|
@@ -2108,6 +2167,9 @@ export function broadcastTurnEnd(sessionId = null, ts = Date.now()) {
|
|
|
2108
2167
|
// 流式状态 SSE 推送定时器:检测 streamingState 变化并广播给所有客户端。
|
|
2109
2168
|
// rising-edge → turn_end cancel(丢弃 pending,不 flush)由 _observeStreamingTick 统一处理。
|
|
2110
2169
|
let _streamingStatusTimer = null;
|
|
2170
|
+
// Waiter-liveness reaper timers (armed in startServer, cleared in the stop path).
|
|
2171
|
+
let _askReaperTimer = null;
|
|
2172
|
+
let _askOrphanSweepTimer = null;
|
|
2111
2173
|
// 启动后 30s 的更新检查 timer 句柄。必须可清理:
|
|
2112
2174
|
// - .unref() 防止它把事件循环 keep-alive 30s(测试进程靠 --test-force-exit 兜底是时序侥幸);
|
|
2113
2175
|
// - _doStop 里 clearTimeout 防止 stop/start 循环(Electron tab / 测试)泄漏多个 pending 检查。
|
|
@@ -2199,6 +2261,14 @@ async function _doStop() {
|
|
|
2199
2261
|
clearInterval(_streamingStatusTimer);
|
|
2200
2262
|
_streamingStatusTimer = null;
|
|
2201
2263
|
}
|
|
2264
|
+
if (_askReaperTimer) {
|
|
2265
|
+
clearInterval(_askReaperTimer);
|
|
2266
|
+
_askReaperTimer = null;
|
|
2267
|
+
}
|
|
2268
|
+
if (_askOrphanSweepTimer) {
|
|
2269
|
+
clearTimeout(_askOrphanSweepTimer);
|
|
2270
|
+
_askOrphanSweepTimer = null;
|
|
2271
|
+
}
|
|
2202
2272
|
if (_updateCheckTimer) {
|
|
2203
2273
|
clearTimeout(_updateCheckTimer);
|
|
2204
2274
|
_updateCheckTimer = null;
|