cc-viewer 1.8.13 → 1.8.15
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 +6 -5
- package/cli.js +19 -0
- package/dist/assets/{App-R6odqLoE.js → App-DxDo_tCQ.js} +2 -2
- package/dist/assets/{MdxEditorPanel-CTqsETwc.js → MdxEditorPanel-Ddpu5ELR.js} +1 -1
- package/dist/assets/Mobile-DA0yMMgI.js +1 -0
- package/dist/assets/{ProxyStatsModal-CM2H8JYh.js → ProxyStatsModal-DKKfSpXx.js} +1 -1
- package/dist/assets/index-BCpkzaVk.js +2 -0
- package/dist/assets/index-Nvk8r3tO.css +1 -0
- package/dist/assets/{seqResourceLoaders-Xx6a8E03.css → seqResourceLoaders-B_FSQh3U.css} +1 -1
- package/dist/assets/seqResourceLoaders-BkzxH2jr.js +2 -0
- package/dist/index.html +2 -2
- package/node_modules/@ccv/core/src/context-rules.js +5 -0
- package/package.json +1 -1
- package/server/i18n.js +2 -2
- package/server/interceptor.js +176 -21
- package/server/lib/builtin-model-prompts.js +12 -1
- package/server/lib/create_system_prompt.js +94 -1
- package/server/lib/ensure-hooks.js +34 -3
- package/server/lib/interceptor-core.js +144 -0
- package/server/lib/launch-config.js +83 -3
- package/server/lib/model-system-prompts.js +6 -1
- package/server/lib/proxy/proxy-retry.js +57 -2
- package/server/lib/resume-watchdog.js +108 -0
- package/server/lib/session-id.js +33 -0
- package/server/lib/shell-hook-inspect.js +48 -0
- package/server/lib/system-prompt-files.js +14 -0
- package/server/lib/system-prompt-live.js +622 -0
- package/server/lib/task-bridge.js +10 -3
- package/server/lib/task-state.js +32 -0
- package/server/lib/v2/identity.js +3 -24
- package/server/lib/v2/session-select.js +11 -8
- package/server/lib/v2/v2-writer.js +13 -5
- package/server/proxy.js +32 -6
- package/server/routes/events.js +9 -3
- package/server/routes/preferences.js +6 -1
- package/server/routes/workspaces.js +3 -2
- package/server/server.js +35 -5
- package/server/system-prompt-templates/presets/GLM-5.2.md +2 -0
- package/server/system-prompt-templates/presets/GLM-5.3.md +2 -0
- package/server/system-prompt-templates/presets/{Qwen-3.7-Max.md → Qwen-3.md} +3 -1
- package/server/system-prompt-templates/presets/deepseek-v4-flash.md +2 -0
- package/server/system-prompt-templates/presets/deepseek-v4-pro.md +2 -0
- package/server/system-prompt-templates/presets/index.json +7 -7
- package/server/system-prompt-templates/presets/kimi-k2.7-code.md +3 -0
- package/server/system-prompt-templates/presets/kimi-k3.md +2 -0
- package/dist/assets/Mobile-o_QrQ_eI.js +0 -1
- package/dist/assets/index--yjDHxJD.js +0 -2
- package/dist/assets/index-D1yGak8I.css +0 -1
- package/dist/assets/seqResourceLoaders-BgeRQt8C.js +0 -2
|
@@ -399,6 +399,150 @@ export function replaceTopLevelModel(jsonStr, oldModel, newModel) {
|
|
|
399
399
|
return jsonStr.slice(0, idx) + replaced + jsonStr.slice(idx + needle.length);
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
+
// ─── replaceTopLevelSystem ────────────────────────────────────────────────────
|
|
403
|
+
// system 文本随主模型热切换(system-prompt-live.js)的 wire 层原语:在原始 JSON
|
|
404
|
+
// 字符串上定向替换顶层 "system" 成员的值,避免对巨型 wire body(-c checkpoint
|
|
405
|
+
// 可达数十 MB)做二次 JSON.parse + 全量 re-stringify。
|
|
406
|
+
//
|
|
407
|
+
// 与 replaceTopLevelModel 的三点差异:
|
|
408
|
+
// 1. needle 只有 key(值可能极大且未知),嵌套的同名成员(如工具 input_schema
|
|
409
|
+
// 里名为 "system" 的属性)会产生多候选 → 要求顶层候选唯一,否则返回 null
|
|
410
|
+
// 由调用方回退 parse/stringify。
|
|
411
|
+
// 2. 值的结束位置用「字符串/转义感知的有界扫描」定位(string 处理 \" 与 \\;
|
|
412
|
+
// array/object 做深度计数,跳过字符串内部)。扫描结束后校验下一个非空白字符
|
|
413
|
+
// 必须是 `,` 或 `}`(顶层对象内的合法后继),否则视为定位失败返回 null。
|
|
414
|
+
// 3. body 没有顶层 "system" 成员时(罕见,如某些 utility 调用),opts.allowPrepend
|
|
415
|
+
// 为 true 才前插;否则返回 null。前插的重复键风险与 injectOutputConfigEffort
|
|
416
|
+
// 同理——调用方必须先在解析后的 body 上确认 system 不存在。
|
|
417
|
+
//
|
|
418
|
+
// newSystemJson 是完整的新值 JSON(字符串或数组的序列化形态,调用方负责 JSON.stringify)。
|
|
419
|
+
// 返回替换后的字符串;无法唯一定位 / 入参非法 → null(调用方回退)。
|
|
420
|
+
|
|
421
|
+
// 从 jsonStr[i](值首字符,必须为 " [ { 或字面量)扫描到值结束,返回结束下标(exclusive),
|
|
422
|
+
// 无法定位 → -1。字符串处理 \" 与 \\ 转义;数组/对象做深度计数并跳过字符串内容;
|
|
423
|
+
// 字面量(true/false/null/number)直接扫到分隔符。system 值在真实 wire 上只会是
|
|
424
|
+
// string 或 array,其余形态防御性支持。
|
|
425
|
+
function _scanJsonValueEnd(jsonStr, i) {
|
|
426
|
+
const open = jsonStr[i];
|
|
427
|
+
if (open === '"') {
|
|
428
|
+
let p = i + 1;
|
|
429
|
+
while (p < jsonStr.length) {
|
|
430
|
+
const c = jsonStr[p];
|
|
431
|
+
if (c === '\\') { p += 2; continue; }
|
|
432
|
+
if (c === '"') return p + 1;
|
|
433
|
+
p++;
|
|
434
|
+
}
|
|
435
|
+
return -1;
|
|
436
|
+
}
|
|
437
|
+
if (open === '[' || open === '{') {
|
|
438
|
+
let depth = 0;
|
|
439
|
+
let p = i;
|
|
440
|
+
while (p < jsonStr.length) {
|
|
441
|
+
const c = jsonStr[p];
|
|
442
|
+
if (c === '"') {
|
|
443
|
+
const end = _scanJsonValueEnd(jsonStr, p);
|
|
444
|
+
if (end === -1) return -1;
|
|
445
|
+
p = end;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (c === '[' || c === '{') depth++;
|
|
449
|
+
else if (c === ']' || c === '}') {
|
|
450
|
+
depth--;
|
|
451
|
+
if (depth === 0) return p + 1;
|
|
452
|
+
if (depth < 0) return -1;
|
|
453
|
+
}
|
|
454
|
+
p++;
|
|
455
|
+
}
|
|
456
|
+
return -1;
|
|
457
|
+
}
|
|
458
|
+
// literal: true / false / null / number → 扫到 , } ] 或空白
|
|
459
|
+
const m = /^-?\d+(\.\d+)?([eE][+-]?\d+)?|^true|^false|^null/.exec(jsonStr.slice(i, i + 32));
|
|
460
|
+
return m ? i + m[0].length : -1;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function replaceTopLevelSystem(jsonStr, newSystemJson, opts = {}) {
|
|
464
|
+
if (typeof jsonStr !== 'string' || !jsonStr ||
|
|
465
|
+
typeof newSystemJson !== 'string' || !newSystemJson) return null;
|
|
466
|
+
// 防御:newSystemJson 必须是合法 JSON 值(调用方 Bug 直接把垃圾写上 wire 不可接受)。
|
|
467
|
+
try { JSON.parse(newSystemJson); } catch { return null; }
|
|
468
|
+
|
|
469
|
+
const needles = ['"system":', '"system" :'];
|
|
470
|
+
// 顶层必须是对象:顶层数组(如 [{...}])里的成员也可能通过 { 边界校验,
|
|
471
|
+
// 但那不是顶层 "system" —— 整体拒绝(调用方回退)。
|
|
472
|
+
const openIdx = jsonStr.search(/\S/);
|
|
473
|
+
if (openIdx === -1 || jsonStr[openIdx] !== '{') return null;
|
|
474
|
+
// 深度感知候选扫描(review 第二轮 P1-4):成员边界校验(前一非空白字符是 { 或 ,)
|
|
475
|
+
// 不足以区分「顶层 system」与「嵌套同名成员」—— tools[i].input_schema.properties.system
|
|
476
|
+
// 前面同样是 {。必须跟踪 JSON 容器深度,只接受 depth===1(顶层对象内)的候选,
|
|
477
|
+
// 否则前插/替换会把 persona 写进工具 schema(顶层 system 根本没设置)。
|
|
478
|
+
//
|
|
479
|
+
// 性能(review 第二轮二次验证 P1):逐字符深度扫描是 O(body),对巨型 -c checkpoint
|
|
480
|
+
// (可达数十 MB)比「回退 parse+stringify」还贵 —— 而绝大多数请求(无 system 的旁路
|
|
481
|
+
// 调用、或 system 在后的大 body)根本不需要全文扫描。先用 indexOf 预筛:body 完全
|
|
482
|
+
// 不含 "system" 字样(含字符串值内)时 candidates 恒为空,直接跳过扫描(O(needle)
|
|
483
|
+
// 原生加速);只有确实含 needle 才付出深度扫描成本。预筛命中不等于有候选(可能在
|
|
484
|
+
// 字符串值内),仍需扫描确认 —— 预筛只在「确定无 needle」时短路,语义不变。
|
|
485
|
+
const candidates = [];
|
|
486
|
+
const _hasNeedle = jsonStr.indexOf('"system"') !== -1; // 覆盖 "system": 与 "system" : 两 needle 的公共前缀
|
|
487
|
+
if (_hasNeedle) {
|
|
488
|
+
let depth = 0;
|
|
489
|
+
let p = openIdx;
|
|
490
|
+
while (p < jsonStr.length) {
|
|
491
|
+
const c = jsonStr[p];
|
|
492
|
+
if (c === '"') {
|
|
493
|
+
// 字符串内容整体跳过(含转义)—— 但先检查它是否是 depth===1 的 "system" 键
|
|
494
|
+
if (depth === 1) {
|
|
495
|
+
for (const needle of needles) {
|
|
496
|
+
if (jsonStr.startsWith(needle, p)) {
|
|
497
|
+
// 键边界:前一个非空白字符必须是 { 或 ,(顶层对象成员位置)
|
|
498
|
+
let q = p - 1;
|
|
499
|
+
while (q >= 0 && (jsonStr[q] === ' ' || jsonStr[q] === '\t' || jsonStr[q] === '\n' || jsonStr[q] === '\r')) q--;
|
|
500
|
+
if (q >= 0 && (jsonStr[q] === '{' || jsonStr[q] === ',')) {
|
|
501
|
+
candidates.push({ idx: p, needle });
|
|
502
|
+
}
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const end = _scanJsonValueEnd(jsonStr, p);
|
|
508
|
+
if (end === -1) return null;
|
|
509
|
+
p = end;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (c === '{' || c === '[') depth++;
|
|
513
|
+
else if (c === '}' || c === ']') depth--;
|
|
514
|
+
p++;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (candidates.length > 1) return null; // 嵌套同名成员 → 回退 parse/stringify
|
|
518
|
+
|
|
519
|
+
if (candidates.length === 0) {
|
|
520
|
+
// 无顶层 system 成员:只有显式允许才前插(utility 端点绝不能发明 system)。
|
|
521
|
+
if (opts.allowPrepend !== true) return null;
|
|
522
|
+
const i = jsonStr.indexOf('{');
|
|
523
|
+
if (i === -1) return null;
|
|
524
|
+
if (/\S/.test(jsonStr.slice(0, i))) return null; // 顶层必须是对象
|
|
525
|
+
const after = jsonStr.slice(i + 1);
|
|
526
|
+
const m = after.match(/^\s*(\S)/);
|
|
527
|
+
const needsComma = !!(m && m[1] !== '}');
|
|
528
|
+
const insert = `"system":${newSystemJson}` + (needsComma ? ',' : '');
|
|
529
|
+
return jsonStr.slice(0, i + 1) + insert + jsonStr.slice(i + 1);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const { idx, needle } = candidates[0];
|
|
533
|
+
// 值起始:跳过冒号后的空白
|
|
534
|
+
let vStart = idx + needle.length;
|
|
535
|
+
while (vStart < jsonStr.length && (jsonStr[vStart] === ' ' || jsonStr[vStart] === '\t' || jsonStr[vStart] === '\n' || jsonStr[vStart] === '\r')) vStart++;
|
|
536
|
+
if (vStart >= jsonStr.length) return null;
|
|
537
|
+
const vEnd = _scanJsonValueEnd(jsonStr, vStart);
|
|
538
|
+
if (vEnd === -1) return null;
|
|
539
|
+
// 值后边界:下一个非空白字符必须是 , 或 }(顶层对象内的合法后继)
|
|
540
|
+
let p = vEnd;
|
|
541
|
+
while (p < jsonStr.length && (jsonStr[p] === ' ' || jsonStr[p] === '\t' || jsonStr[p] === '\n' || jsonStr[p] === '\r')) p++;
|
|
542
|
+
if (p >= jsonStr.length || (jsonStr[p] !== ',' && jsonStr[p] !== '}')) return null;
|
|
543
|
+
return jsonStr.slice(0, vStart) + newSystemJson + jsonStr.slice(vEnd);
|
|
544
|
+
}
|
|
545
|
+
|
|
402
546
|
// proxy profile hot-switch 模型解析:按 request body 里 model 的家族名映射到 profile 的对应字段。
|
|
403
547
|
// 家族用**大小写不敏感子串**匹配(/opus/i 等),只认这几个已知家族单词——
|
|
404
548
|
// 因此 claude-opus-4-8、未来的 claude-opus-5 等任何版本都命中同一家族,版本升级无需重配。
|
|
@@ -21,13 +21,15 @@ import { randomBytes } from 'node:crypto';
|
|
|
21
21
|
import { LOG_DIR } from '../../findcc.js';
|
|
22
22
|
import { reportSwallowed } from '@ccv/core/error-report';
|
|
23
23
|
import {
|
|
24
|
-
buildSystemPromptFileArgs, hasArg, isNonEmptyFile,
|
|
24
|
+
buildSystemPromptFileArgs, hasArg, argValue, isNonEmptyFile,
|
|
25
25
|
SYSTEM_PROMPT_FILE, APPEND_SYSTEM_PROMPT_FILE,
|
|
26
26
|
} from './system-prompt-files.js';
|
|
27
27
|
import { renderSystemPromptFileArgs, renderedPromptDir } from './system-prompt-render.js';
|
|
28
|
+
import { createSystemPromptVariables } from './create_system_prompt.js';
|
|
28
29
|
import { appendPending, readSnapshot, resolveContinueTargetUuid } from './system-prompt-snapshots.js';
|
|
29
30
|
import { MODEL_PROMPT_DIR, listModelPrompts } from './model-system-prompts.js';
|
|
30
31
|
import { resolveSpawnModel } from './spawn-model-resolver.js';
|
|
32
|
+
import { setLaunchSystemPromptInfo } from './system-prompt-live.js';
|
|
31
33
|
|
|
32
34
|
// Opus 4.7 默认不再返回 thinking;为所有非显式覆写的调用加上 summarized。
|
|
33
35
|
// 纯函数:仅根据 args 决定是否注入;用户已显式传入 `--thinking-display` 时原样返回。
|
|
@@ -210,7 +212,17 @@ export function resolveLaunchSystemPrompt(p) {
|
|
|
210
212
|
resolvedModelId: null,
|
|
211
213
|
diagnostic: null,
|
|
212
214
|
};
|
|
213
|
-
if
|
|
215
|
+
// The variable set collected while rendering this launch's injected files (if any).
|
|
216
|
+
// Captured via the render seam below and published to the live layer so a later hot
|
|
217
|
+
// model switch re-renders with THIS launch's variables — never by spawning git in
|
|
218
|
+
// the fetch hook. Stays null when nothing was injected or no file had `${...}`.
|
|
219
|
+
let collectedVariables = null;
|
|
220
|
+
if (!spawnDir) {
|
|
221
|
+
// 复位 live 启动判定:早退不得把上一次 launch 的注入状态泄漏给这次
|
|
222
|
+
// (什么都没注入的 launch 必须关闭 live 改写门)。
|
|
223
|
+
try { setLaunchSystemPromptInfo(null); } catch (err) { reportSwallowed('launch-config.publishLiveInfo', err); }
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
214
226
|
|
|
215
227
|
// Continuation launches (-c/--continue/-r/--resume): pin the resumed conversation's
|
|
216
228
|
// original injection — never re-render variables. IM workers (insideLogDir) skip the
|
|
@@ -245,6 +257,12 @@ export function resolveLaunchSystemPrompt(p) {
|
|
|
245
257
|
|
|
246
258
|
if (pinned) {
|
|
247
259
|
out.sysPrompt = pinned;
|
|
260
|
+
// resolvedModelId 仅在 fresh 分支产出(modelReader 的真模型 id)。pinned 分支的
|
|
261
|
+
// snap.model 是「条目名」(OPUS/KIMI-K3 大写 stem;sentinel 为 null),不是 wire
|
|
262
|
+
// 模型 id —— 置 null,live seed 由 pinned:true 走「无条件 seed 自 pin 字节」,
|
|
263
|
+
// 保证 resume pin 不被强行覆盖(snap.model 与 _targetModel 用 === 永不命中,
|
|
264
|
+
// 误当模型 id 会让 seed 死掉、第 2 请求被重选覆盖 pin)。
|
|
265
|
+
out.resolvedModelId = null;
|
|
248
266
|
} else {
|
|
249
267
|
// launchSettings is this launch's own live configuration — launchers like cfuse
|
|
250
268
|
// deliver ANTHROPIC_MODEL exclusively inside it, so it must participate in model
|
|
@@ -274,14 +292,38 @@ export function resolveLaunchSystemPrompt(p) {
|
|
|
274
292
|
}
|
|
275
293
|
// Resolve `${...}` template variables in the injected files. Skipped entirely when
|
|
276
294
|
// suppression zeroed the args (a rejected binary never pays the render cost).
|
|
295
|
+
// The collected variable set is captured (below) and published to the live layer
|
|
296
|
+
// so a later hot model switch re-renders with THIS launch's variables — never by
|
|
297
|
+
// spawning git inside the fetch hook.
|
|
277
298
|
if (sysPrompt.args.length > 0) {
|
|
278
|
-
sysPrompt = renderSystemPromptFileArgs(sysPrompt, {
|
|
299
|
+
sysPrompt = renderSystemPromptFileArgs(sysPrompt, {
|
|
300
|
+
cwd: spawnDir,
|
|
301
|
+
modelId: resolvedModelId,
|
|
302
|
+
variablesFactory: (overrides, opts) => {
|
|
303
|
+
const variables = createSystemPromptVariables(overrides, opts);
|
|
304
|
+
collectedVariables = variables;
|
|
305
|
+
return variables;
|
|
306
|
+
},
|
|
307
|
+
});
|
|
279
308
|
} else {
|
|
280
309
|
sysPrompt = { ...sysPrompt, entries: [] };
|
|
281
310
|
}
|
|
282
311
|
out.sysPrompt = sysPrompt;
|
|
283
312
|
}
|
|
284
313
|
|
|
314
|
+
// The render seam above is lazy: it only fires when some injected file actually
|
|
315
|
+
// contains `${...}`, so a no-template launch collects nothing and the snapshot would
|
|
316
|
+
// stay null — in which case the live layer falls back to an empty variable skeleton
|
|
317
|
+
// and hot-switch re-renders of a built-in preset produce EMPTY `${memory.dir}` /
|
|
318
|
+
// `${os.*}` / `${environment.lang}` texts (a model told to write into a `` path).
|
|
319
|
+
// Collect unconditionally for live-eligible launches: this runs on the spawn path
|
|
320
|
+
// (git subprocess at launch is fine — the hook path is what must never spawn).
|
|
321
|
+
if (!collectedVariables && !insideLogDir && !suppressInjection && !(pinned && pinned.noRecord === true)) {
|
|
322
|
+
try {
|
|
323
|
+
collectedVariables = createSystemPromptVariables({ model: { name: out.resolvedModelId || '' } }, { cwd: spawnDir });
|
|
324
|
+
} catch (err) { reportSwallowed('launch-config.collectVariables', err); }
|
|
325
|
+
}
|
|
326
|
+
|
|
285
327
|
// Unified suppression (pin and fresh alike): nothing was injected → nothing can be bound.
|
|
286
328
|
if (suppressInjection) {
|
|
287
329
|
out.sysPrompt = { args: [], loaded: [], model: null, entries: [] };
|
|
@@ -299,6 +341,44 @@ export function resolveLaunchSystemPrompt(p) {
|
|
|
299
341
|
try { appendPending(spawnDir, rec, logDir); } catch (err) { reportSwallowed('launch-config.appendPending', err); }
|
|
300
342
|
}
|
|
301
343
|
out.pendingRec = rec;
|
|
344
|
+
|
|
345
|
+
// 发布启动判定给 live 层(system-prompt-live.js)。强行覆盖模式:以热切换选中的
|
|
346
|
+
// 模型为准,只要该模型有对应 system 文本就注入/替换(含启动未注入的情况)。
|
|
347
|
+
// manualSystemPrompt:用户手动传了 --system-prompt(任意文本)或 --system-prompt-file
|
|
348
|
+
// 且其值不是 ccv 本次注入的路径 —— 此类会话绝不被热切换覆盖(手动优先;用户说的
|
|
349
|
+
// 手动是字面 flag,不含 ccv 启动阶段写入的那份)。
|
|
350
|
+
const _injectedSysPath = argValue(out.sysPrompt.args, '--system-prompt-file');
|
|
351
|
+
// ccv 本次注入的 --append-system-prompt-file 路径(同款识别,排除「手动 vs 注入」误判)
|
|
352
|
+
const _injectedAppendPath = argValue(out.sysPrompt.args, '--append-system-prompt-file');
|
|
353
|
+
const _manualSys =
|
|
354
|
+
hasArg(extraArgs, '--system-prompt') ||
|
|
355
|
+
(() => { const v = argValue(extraArgs, '--system-prompt-file'); return v !== null && v !== _injectedSysPath; })();
|
|
356
|
+
// 手动优先同样覆盖 append 两族:用户手传 --append-system-prompt[-file] 时启动期按
|
|
357
|
+
// 「手动优先」跳过 ccv 注入(buildSystemPromptFileArgs:148),live 层不得在请求时回补
|
|
358
|
+
// —— 同一条规则启动期生效、live 层绕过是不一致的。任一手动 flag → 关闭 live 门。
|
|
359
|
+
const _manualAppendSys =
|
|
360
|
+
hasArg(extraArgs, '--append-system-prompt') ||
|
|
361
|
+
(() => { const v = argValue(extraArgs, '--append-system-prompt-file'); return v !== null && v !== _injectedAppendPath; })();
|
|
362
|
+
try {
|
|
363
|
+
setLaunchSystemPromptInfo({
|
|
364
|
+
workspaceDir: spawnDir,
|
|
365
|
+
resolvedModelId: out.resolvedModelId,
|
|
366
|
+
entries: effectiveEntries,
|
|
367
|
+
pinned: !!pinned,
|
|
368
|
+
// suppressInjection(二进制拒绝 flag 自愈 / IM 去注入重启)必须有对应 suppressed,
|
|
369
|
+
// 否则 live 门会被击穿、把启动期明确不要的注入在请求时回补。
|
|
370
|
+
suppressed: suppressInjection ? 'suppressInjection' : (out.sysPrompt.suppressed ?? null),
|
|
371
|
+
manualSystemPrompt: _manualSys || _manualAppendSys,
|
|
372
|
+
// IM worker(spawnDir 在 LOG_DIR 内)persona 绝不被全局模型条目替换 → 拒绝 live 覆盖。
|
|
373
|
+
// F2 resume(pinned.noRecord:目标已识别但无快照)语义是「本次启动不注入任何东西,
|
|
374
|
+
// 绝不改动既有上下文的 system」→ 同样拒绝 live 覆盖(否则 resume 会话被强行注入)。
|
|
375
|
+
allowLive: !insideLogDir && !(pinned && pinned.noRecord === true),
|
|
376
|
+
// 本次启动收集到的变量集(含 git/os/env/memory…;live 层只缓存其快照部分)。
|
|
377
|
+
// 热切换渲染复用它,绝不在 fetch hook 同步段跑 git。无注入时由上方兜底收集
|
|
378
|
+
// (否则 preset 的 ${memory.dir}/${os.*} 会渲染成空串)。
|
|
379
|
+
variableSnapshot: collectedVariables,
|
|
380
|
+
});
|
|
381
|
+
} catch (err) { reportSwallowed('launch-config.publishLiveInfo', err); }
|
|
302
382
|
return out;
|
|
303
383
|
}
|
|
304
384
|
|
|
@@ -253,12 +253,17 @@ export function deleteModelPrompt(dir, name) {
|
|
|
253
253
|
// resolved id IS the bare shorthand — no accidental widening.
|
|
254
254
|
const MODEL_ID_ALIASES = {
|
|
255
255
|
k3: ['k3', 'kimi-k3', 'kimi'],
|
|
256
|
+
// deepseek 简写同理:代理/CLI 常把 `deepseek-v4-flash` 简写成 `deepseek-flash`。
|
|
257
|
+
// DeepSeek shorthand: proxies/CLIs often shorten `deepseek-v4-flash` to `deepseek-flash`.
|
|
258
|
+
'deepseek-flash': ['deepseek-flash', 'deepseek-v4-flash'],
|
|
256
259
|
};
|
|
257
260
|
|
|
258
261
|
// 展开模型 id 的全部等价小写拼写(无别名时就是单元素数组)。
|
|
262
|
+
// 用 hasOwn 查表:普通对象下 `MODEL_ID_ALIASES['constructor']` 会取到 Object 构造器,
|
|
263
|
+
// 让 .some 调用炸掉(模型 id 来自外部输入,不能信任)。
|
|
259
264
|
// Expand a model id into every equivalent lowercase spelling.
|
|
260
265
|
function modelIdVariants(id) {
|
|
261
|
-
return MODEL_ID_ALIASES[id]
|
|
266
|
+
return Object.hasOwn(MODEL_ID_ALIASES, id) ? MODEL_ID_ALIASES[id] : [id];
|
|
262
267
|
}
|
|
263
268
|
|
|
264
269
|
// 供内置预设匹配复用的导出包装:入参任意大小写,先剥 `[1m]` 类方括号后缀
|
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
// - Streaming responses: only read status + headers to decide whether to retry; never retry after the body has
|
|
16
16
|
// started being sent (retry-before-first-byte strategy).
|
|
17
17
|
// - race/stagger use AbortController; cancelled requests must be released correctly.
|
|
18
|
-
import { resolveProfileModel } from '../interceptor-core.js';
|
|
18
|
+
import { resolveProfileModel, replaceTopLevelSystem } from '../interceptor-core.js';
|
|
19
19
|
import { readFileSync, existsSync } from 'node:fs';
|
|
20
20
|
import { reportSwallowed } from '@ccv/core/error-report';
|
|
21
|
+
import { liveSystemPromptEnabled, getLiveEntry, applyLiveSystem, knownInjectedTexts } from '../system-prompt-live.js';
|
|
22
|
+
import { parseUserId } from '../session-id.js';
|
|
21
23
|
|
|
22
24
|
// ── Configuration ─────────────────────────────────────────────────
|
|
23
25
|
|
|
@@ -458,6 +460,55 @@ function discardBody(response) {
|
|
|
458
460
|
|
|
459
461
|
// ── executeRequest ────────────────────────────────────────────────
|
|
460
462
|
|
|
463
|
+
/**
|
|
464
|
+
* Live system 应用(proxy 路径):模型替换之后执行。幂等 —— interceptor hook 对 trace
|
|
465
|
+
* 请求会再跑一次 applyLiveSystem,此时 body.system 已是目标形态,剥离已知注入后重组
|
|
466
|
+
* 得到相同值 → null(无变化),与 resolveProfileModel 的双跑幂等同理。
|
|
467
|
+
* 门:liveSystemPromptEnabled + role 为 main/未分类 + 非 utility + sessionId 可解析 +
|
|
468
|
+
* 缓存已有该 (session, model) 条目(proxy 同步段绝不新生成 —— 启动模型 seed 由 hook
|
|
469
|
+
* 先行,非启动模型的异步旁路生成也由 hook 负责;这里只消费既有缓存)。
|
|
470
|
+
*/
|
|
471
|
+
function applyLiveSystemPrompt(body, ctx) {
|
|
472
|
+
try {
|
|
473
|
+
if (!body || !ctx || ctx.isUtility === true) return body;
|
|
474
|
+
if (ctx.role && ctx.role !== 'main') return body;
|
|
475
|
+
if (!liveSystemPromptEnabled()) return body;
|
|
476
|
+
const projectKey = ctx.projectKey;
|
|
477
|
+
if (!projectKey) return body;
|
|
478
|
+
const s = typeof body === 'string' ? body : body.toString('utf-8');
|
|
479
|
+
const obj = JSON.parse(s);
|
|
480
|
+
if (!obj || typeof obj !== 'object') return body;
|
|
481
|
+
const sid = parseUserId(obj.metadata?.user_id)?.sessionId ?? null;
|
|
482
|
+
if (!sid) return body;
|
|
483
|
+
const model = typeof obj.model === 'string' ? obj.model : ''; // 模型替换已先行 → 生效模型
|
|
484
|
+
if (!model) return body;
|
|
485
|
+
const entry = getLiveEntry(projectKey, sid, model);
|
|
486
|
+
if (!entry) return body; // 缓存未就绪 → 放行(同进程 hook 的同步生成会补上;proxy 只消费不生成)
|
|
487
|
+
// role 门(对齐 hook,review P0-1/P1-4):已删除 isMainAgentRequest 死门 —— 它要求
|
|
488
|
+
// system 含 "You are Claude Code" 官方文案,而 override 主场景(--system-prompt-file
|
|
489
|
+
// 整段替换)的 base 是自定义 persona,永远判 false → proxy-only 路径(CCV_WORKSPACE_MODE /
|
|
490
|
+
// Electron)live 特性整体失效;且与下方「无 system 时合成目标形态」分支自相矛盾
|
|
491
|
+
// (isMainAgentRequest 要求 body.system 存在,该分支正为「无 system」而设)。
|
|
492
|
+
// 门由 ctx.role(proxy.js 在 live 启用时分类填入,未分类 → undefined → main 语义)
|
|
493
|
+
// + ctx.isUtility 承担。结构门:tools 非空 —— 无 tools 的旁路调用(标题生成/压缩探针,
|
|
494
|
+
// 本机语料 1323 条 tools=[])与 main 同形但绝不改写 system(旧 mainAgent 门隐式覆盖)。
|
|
495
|
+
if (!Array.isArray(obj.tools) || obj.tools.length === 0) return body;
|
|
496
|
+
// 强行覆盖:body 无 system(启动未注入)时用空字符串合成目标形态(前插由 allowPrepend 承担)。
|
|
497
|
+
const hasSystem = 'system' in obj && obj.system != null;
|
|
498
|
+
const newSystem = applyLiveSystem(hasSystem ? obj.system : '', entry, knownInjectedTexts(projectKey, sid));
|
|
499
|
+
if (newSystem === null) return body;
|
|
500
|
+
// 字节级替换(与 hook 同一原语):巨型 -c checkpoint 只改 system 成员、不整体重建,
|
|
501
|
+
// 且与 hook 输出字节一致(hook 二次 pass 必为 no-op)。needle 定位失败才回退对象级重建。
|
|
502
|
+
const replaced = replaceTopLevelSystem(s, JSON.stringify(newSystem), { allowPrepend: !hasSystem });
|
|
503
|
+
if (replaced !== null) return replaced;
|
|
504
|
+
obj.system = newSystem;
|
|
505
|
+
return JSON.stringify(obj);
|
|
506
|
+
} catch (err) {
|
|
507
|
+
reportSwallowed('proxyRetry.live-system', err);
|
|
508
|
+
return body;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
461
512
|
/**
|
|
462
513
|
* Executes a request with retries. Returns { response, attempts, retryCodes, durationMs, finalStatus, upstreamStatus, succeeded }.
|
|
463
514
|
*
|
|
@@ -465,7 +516,7 @@ function discardBody(response) {
|
|
|
465
516
|
* @param {string} params.url full upstream URL
|
|
466
517
|
* @param {object} params.fetchOptions { method, headers, body }
|
|
467
518
|
* @param {object} params.retryConfig retry config
|
|
468
|
-
* @param {object} params.ctx { dispatcher, profile } network proxy dispatcher + model replacement profile
|
|
519
|
+
* @param {object} params.ctx { dispatcher, profile, role, isUtility, projectKey, launchInfo } network proxy dispatcher + model replacement profile + live system context
|
|
469
520
|
* @returns {Promise<object>}
|
|
470
521
|
*/
|
|
471
522
|
export async function executeRequest({ url, fetchOptions, retryConfig, ctx }) {
|
|
@@ -483,6 +534,10 @@ export async function executeRequest({ url, fetchOptions, retryConfig, ctx }) {
|
|
|
483
534
|
if (finalBody && profile) {
|
|
484
535
|
finalBody = applyModelReplacement(finalBody, profile);
|
|
485
536
|
}
|
|
537
|
+
// Live system 改写(模型替换之后;幂等,hook 会再跑一次)
|
|
538
|
+
if (finalBody) {
|
|
539
|
+
finalBody = applyLiveSystemPrompt(finalBody, ctx);
|
|
540
|
+
}
|
|
486
541
|
const finalFetchOptions = { ...fetchOptions, body: finalBody };
|
|
487
542
|
|
|
488
543
|
const startTime = Date.now();
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Resume watchdog (L2): detect a `-c`/`-r` continuation that BYPASSED ccv (shell hook
|
|
2
|
+
// missing or claude invoked directly), so the panel can warn that the system-prompt
|
|
3
|
+
// injection was lost and the prompt-prefix cache will be fully rewritten.
|
|
4
|
+
//
|
|
5
|
+
// 裸续接检测(L2):发现「transcript 在写、但 ccv 没观测到对应 wire 流量」的会话,
|
|
6
|
+
// 经 SSE 提示用户(一次性,可关闭)。判定三重门,缺一不报:
|
|
7
|
+
// 1. 该会话 uuid 有 snapshot 记录(曾注入过 —— 从未注入的会话裸奔无损,不报);
|
|
8
|
+
// 2. 当前工作区仍配置着注入(injectionConfigured —— 用户已删掉注入配置则不报);
|
|
9
|
+
// 3. transcript 在快照写入之后又有可观增长,且 ccv 的 v2 会话目录里没有对应活动
|
|
10
|
+
// (有 = 请求经过了 ccv,不是裸奔)。
|
|
11
|
+
// Failure philosophy: every error degrades to "no report" — never throws, never
|
|
12
|
+
// affects the request or spawn paths.
|
|
13
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { reportSwallowed } from '@ccv/core/error-report';
|
|
16
|
+
import { LOG_DIR } from '../../findcc.js';
|
|
17
|
+
import { injectionConfigured } from './launch-config.js';
|
|
18
|
+
import { readSnapshotByKey, transcriptDirForCwd, projectKeyForCwd } from './system-prompt-snapshots.js';
|
|
19
|
+
|
|
20
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
21
|
+
// transcript 必须比快照记录新这么多才算「发生了新的续接」——文件系统 mtime 粒度 +
|
|
22
|
+
// 快照写入与 transcript flush 的既有竞态窗口内不误报。
|
|
23
|
+
const RESUME_MIN_GAP_MS = 5 * 1000;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 检测当前工作区最近被续接、但未经 ccv 的会话。
|
|
27
|
+
* Detect recently-resumed sessions of this workspace that bypassed ccv.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} p
|
|
30
|
+
* @param {string} p.cwd 当前工作区目录
|
|
31
|
+
* @param {string} [p.logDir]
|
|
32
|
+
* @param {(uuid: string) => boolean} p.hasWireActivity 该 uuid 是否已有 ccv wire 观测
|
|
33
|
+
* (由调用方提供 —— v2 sessions 目录存在该 uuid 的目录/journal;测试直接注入)
|
|
34
|
+
* @param {number} [p.now]
|
|
35
|
+
* @returns {{ uuid: string, transcriptPath: string }|null}
|
|
36
|
+
*/
|
|
37
|
+
export function detectBypassedResume({ cwd, logDir = LOG_DIR, hasWireActivity, now = Date.now() }) {
|
|
38
|
+
try {
|
|
39
|
+
if (!cwd || typeof hasWireActivity !== 'function') return null;
|
|
40
|
+
const projectKey = projectKeyForCwd(cwd);
|
|
41
|
+
if (!projectKey) return null;
|
|
42
|
+
// Gate 2: injection still configured (user removed the config → nothing lost).
|
|
43
|
+
if (!injectionConfigured(cwd, logDir)) return null;
|
|
44
|
+
const tDir = transcriptDirForCwd(cwd);
|
|
45
|
+
if (!tDir || !existsSync(tDir)) return null;
|
|
46
|
+
let latest = null;
|
|
47
|
+
for (const f of readdirSync(tDir)) {
|
|
48
|
+
if (!f.endsWith('.jsonl')) continue;
|
|
49
|
+
const uuid = f.slice(0, -'.jsonl'.length);
|
|
50
|
+
if (!UUID_RE.test(uuid)) continue;
|
|
51
|
+
// Gate 1: only sessions that ever had an injection (a snapshot record).
|
|
52
|
+
const snap = readSnapshotByKey(projectKey, uuid, logDir);
|
|
53
|
+
if (!snap) continue;
|
|
54
|
+
const full = join(tDir, uuid + '.jsonl');
|
|
55
|
+
let mtime;
|
|
56
|
+
try { mtime = statSync(full).mtimeMs; } catch { continue; }
|
|
57
|
+
// Gate 3a: transcript grew meaningfully AFTER the snapshot was recorded.
|
|
58
|
+
if (mtime - snap.createdAt < RESUME_MIN_GAP_MS) continue;
|
|
59
|
+
// Gate 3b: ccv observed no wire traffic for this uuid (a ccv-routed resume
|
|
60
|
+
// would have produced a v2 session dir).
|
|
61
|
+
if (hasWireActivity(uuid)) continue;
|
|
62
|
+
if (!latest || mtime > latest.mtime) latest = { uuid, transcriptPath: full, mtime };
|
|
63
|
+
}
|
|
64
|
+
return latest ? { uuid: latest.uuid, transcriptPath: latest.transcriptPath } : null;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
reportSwallowed('resume-watchdog.detect', err);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 当前工作区 v2 sessions 目录里是否存在该 uuid 的活动目录。 */
|
|
72
|
+
export function v2HasWireActivity(projectKey, uuid, logDir = LOG_DIR) {
|
|
73
|
+
try {
|
|
74
|
+
const sessDir = join(logDir, projectKey, 'sessions');
|
|
75
|
+
if (!existsSync(sessDir)) return false;
|
|
76
|
+
for (const d of readdirSync(sessDir)) {
|
|
77
|
+
if (d.includes(uuid)) return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
} catch {
|
|
81
|
+
return true; // 读失败按「有活动」处理 —— 宁可不报,不可误报
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 启动周期检测(默认 60s,仅当前工作区;命中后按 uuid 去重,每个 uuid 只报一次)。
|
|
87
|
+
* Start the periodic watcher. onHit({uuid, transcriptPath}) fires at most once per uuid.
|
|
88
|
+
* @returns {{ stop: () => void }}
|
|
89
|
+
*/
|
|
90
|
+
export function startResumeWatchdog({ cwd, onHit, intervalMs = 60_000, logDir = LOG_DIR } = {}) {
|
|
91
|
+
const reported = new Set();
|
|
92
|
+
const projectKey = projectKeyForCwd(cwd);
|
|
93
|
+
const tick = () => {
|
|
94
|
+
try {
|
|
95
|
+
const hit = detectBypassedResume({
|
|
96
|
+
cwd, logDir,
|
|
97
|
+
hasWireActivity: (uuid) => v2HasWireActivity(projectKey, uuid, logDir),
|
|
98
|
+
});
|
|
99
|
+
if (hit && !reported.has(hit.uuid)) {
|
|
100
|
+
reported.add(hit.uuid);
|
|
101
|
+
onHit?.(hit);
|
|
102
|
+
}
|
|
103
|
+
} catch (err) { reportSwallowed('resume-watchdog.tick', err); }
|
|
104
|
+
};
|
|
105
|
+
const timer = setInterval(tick, intervalMs);
|
|
106
|
+
timer.unref?.();
|
|
107
|
+
return { stop: () => clearInterval(timer), _reported: reported };
|
|
108
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Shared leaf: parse session_id out of body.metadata.user_id.
|
|
2
|
+
//
|
|
3
|
+
// Lifted from v2/identity.js so lib-root and lib/proxy modules can use it without
|
|
4
|
+
// an R3 cross-subsystem edge into v2/ (house pattern: isNonEmptyFile → file-api.js
|
|
5
|
+
// re-export shell). v2/identity.js re-exports from here; do not duplicate the logic.
|
|
6
|
+
//
|
|
7
|
+
// Encodings (WIRE_FORMAT_V2 spec §8):
|
|
8
|
+
// - 'json': '{"device_id":…,"account_uuid":…,"session_id":"<uuid>"}'
|
|
9
|
+
// - 'delimited': 'user_<hash>_account_<acct?>_session_<uuid>'
|
|
10
|
+
|
|
11
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse a raw metadata.user_id string into { sessionId, encoding } or null.
|
|
15
|
+
* @param {unknown} userIdRaw
|
|
16
|
+
* @returns {{ sessionId: string, encoding: 'json'|'delimited' } | null}
|
|
17
|
+
*/
|
|
18
|
+
export function parseUserId(userIdRaw) {
|
|
19
|
+
if (typeof userIdRaw !== 'string' || userIdRaw === '') return null;
|
|
20
|
+
try {
|
|
21
|
+
const obj = JSON.parse(userIdRaw);
|
|
22
|
+
if (obj && typeof obj.session_id === 'string' && obj.session_id !== '') {
|
|
23
|
+
return { sessionId: obj.session_id, encoding: 'json' };
|
|
24
|
+
}
|
|
25
|
+
return null; // valid JSON but no session_id — treat as unparseable
|
|
26
|
+
} catch { /* not JSON → try the delimited form */ }
|
|
27
|
+
const idx = userIdRaw.lastIndexOf('_session_');
|
|
28
|
+
if (idx >= 0) {
|
|
29
|
+
const tail = userIdRaw.slice(idx + '_session_'.length);
|
|
30
|
+
if (UUID_RE.test(tail)) return { sessionId: tail, encoding: 'delimited' };
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Shell-hook 状态只读探针(L1):cli.js 与 server 共用,避免 server 反向 import cli.js
|
|
2
|
+
// (cli.js 是 side-effectful 入口,import 即执行 dispatch)。
|
|
3
|
+
// Shell-hook read-only inspector, shared by cli.js and the server (which must never
|
|
4
|
+
// import the side-effectful cli.js entry).
|
|
5
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { resolve } from 'node:path';
|
|
8
|
+
|
|
9
|
+
export const SHELL_HOOK_START = '# >>> CC-Viewer Auto-Inject >>>';
|
|
10
|
+
export const SHELL_HOOK_END = '# <<< CC-Viewer Auto-Inject <<<';
|
|
11
|
+
|
|
12
|
+
// 候选 rc 文件全集(removeShellHook 原有清单)。install 只写 getShellConfigPath()
|
|
13
|
+
// 选中的那一个,inspect 扫全集 —— 用户可能换过 shell,旧 rc 里的 hook 同样算「已装」。
|
|
14
|
+
const RC_CANDIDATES = ['.zshrc', '.zprofile', '.bashrc', '.bash_profile', '.profile'];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 检查 shell hook 安装状态(只读,绝不写 rc —— 「用户删掉 = 不想装」必须被尊重)。
|
|
18
|
+
* Inspect shell-hook installation state. Read-only by contract.
|
|
19
|
+
*
|
|
20
|
+
* @param {(isNative: boolean) => string} buildShellHook 期望内容的构造器(cli.js 注入,
|
|
21
|
+
* 使「stale」判定与 install 用同一份模板;server 侧无模板时可传 null 跳过 stale 判定)
|
|
22
|
+
* @returns {{ installed: boolean, stale: boolean, path: string|null, corrupt: string|null }}
|
|
23
|
+
* - installed: 任一候选 rc 含 START 标记
|
|
24
|
+
* - stale: 标记块存在但与当前模板不一致(upgrade 后旧版 hook 残留)
|
|
25
|
+
* - corrupt: 含 START 但块不完整(END 损坏)的文件路径
|
|
26
|
+
*/
|
|
27
|
+
export function inspectShellHook(buildShellHook = null) {
|
|
28
|
+
const home = homedir();
|
|
29
|
+
let found = null;
|
|
30
|
+
let stale = false;
|
|
31
|
+
let corrupt = null;
|
|
32
|
+
for (const f of RC_CANDIDATES) {
|
|
33
|
+
const p = resolve(home, f);
|
|
34
|
+
try {
|
|
35
|
+
if (!existsSync(p)) continue;
|
|
36
|
+
const content = readFileSync(p, 'utf-8');
|
|
37
|
+
if (!content.includes(SHELL_HOOK_START)) continue;
|
|
38
|
+
const m = content.match(new RegExp(`${SHELL_HOOK_START}[\\s\\S]*?${SHELL_HOOK_END}`));
|
|
39
|
+
if (!m) { corrupt = p; continue; }
|
|
40
|
+
if (!found) found = p;
|
|
41
|
+
if (buildShellHook) {
|
|
42
|
+
// 两种模式(npm/native)任一匹配即不算 stale —— install 按部署形态二选一。
|
|
43
|
+
if (m[0] !== buildShellHook(false) && m[0] !== buildShellHook(true)) stale = true;
|
|
44
|
+
}
|
|
45
|
+
} catch { /* 读失败的 rc 不参与判定 */ }
|
|
46
|
+
}
|
|
47
|
+
return { installed: found !== null, stale, path: found, corrupt };
|
|
48
|
+
}
|
|
@@ -34,6 +34,20 @@ export function hasArg(args, ...names) {
|
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// 取出 args 里某个 flag 的值(`--x <v>` 取下一个 token;`--x=<v>` 取等号后)。无 → null。
|
|
38
|
+
// live 层用:判断「用户手动传了 --system-prompt-file」时要排除 ccv 启动阶段注入的那份
|
|
39
|
+
// (用户说的手动是字面 flag,不含 ccv 注入),需比对值。
|
|
40
|
+
export function argValue(args, name) {
|
|
41
|
+
if (!Array.isArray(args)) return null;
|
|
42
|
+
for (let i = 0; i < args.length; i++) {
|
|
43
|
+
const a = args[i];
|
|
44
|
+
if (typeof a !== 'string') continue;
|
|
45
|
+
if (a.startsWith(name + '=')) return a.slice(name.length + 1);
|
|
46
|
+
if (a === name && i + 1 < args.length && typeof args[i + 1] === 'string') return args[i + 1];
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
/**
|
|
38
52
|
* 启动 claude 前,按「启动目录」里的 sentinel 文件决定是否注入 system prompt 文件参数。
|
|
39
53
|
*
|