cc-viewer 1.6.308 → 1.6.310

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/dist/index.html CHANGED
@@ -21,7 +21,7 @@
21
21
  // 整体显示大小已弃用 CSS zoom:Electron 改用 webFrame.setZoomFactor(首屏抢占见
22
22
  // electron/tab-content-preload.js),纯浏览器交由用户用浏览器自带快捷键缩放,故此处不再设 zoom。
23
23
  </script>
24
- <script type="module" crossorigin src="/assets/index-BD-SSlan.js"></script>
24
+ <script type="module" crossorigin src="/assets/index-DgGJTx-b.js"></script>
25
25
  <link rel="modulepreload" crossorigin href="/assets/vendor-antd-Bur5ZxWE.js">
26
26
  <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-Si44UqBp.js">
27
27
  <link rel="modulepreload" crossorigin href="/assets/vendor-mdxeditor-Cco3AQJS.js">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-viewer",
3
- "version": "1.6.308",
3
+ "version": "1.6.310",
4
4
  "description": "Claude Code Logger visualization management tool",
5
5
  "license": "MIT",
6
6
  "main": "server.js",
@@ -121,7 +121,8 @@
121
121
  },
122
122
  "overrides": {
123
123
  "axios": "^1.16.1",
124
- "qs": "^6.15.2"
124
+ "qs": "^6.15.2",
125
+ "node-gyp": "^12.1.0"
125
126
  },
126
127
  "c8": {
127
128
  "include": [
@@ -0,0 +1,143 @@
1
+ // CLIENT-SAFE: no node deps. Imported by src/ — do not add fs/process/node: imports.
2
+ //
3
+ // 上下文窗口规则唯一事实源(前后端同源):
4
+ // - 服务端:server/lib/context-watcher.js、server/routes/events.js 直接 import
5
+ // - 前端:src/utils/helpers.js thin re-export(经 Vite 跨目录打包,先例见 tools-xml-formatter.js)
6
+ // 此前规则散落三处(前端 MODEL_CONTEXT_SIZES / _classifyContextSize、服务端 getContextSizeForModel)
7
+ // 且已漂移(服务端不认识 deepseek-v4),收编于此后任何档位变更只改这一个文件。
8
+ //
9
+ // 关键有意决策:裸 claude-sonnet-4-6(无 [1m] 后缀)按 200K 而非 API 规格表的 1M ——
10
+ // 与 Claude Code 选模型的默认行为一致([1m] 是用户显式 opt-in);若实际跑在 1M 模式,
11
+ // 由 [1m] 后缀规则或 adaptContextWindow 用量纠偏兜底,血条不会卡死在 100%。
12
+
13
+ // [Nk]/[Nm] 显式窗口后缀,如 claude-fable-5[1m]、claude-sonnet-4-6[200k]、[500k]。
14
+ // 显式 opt-in 优先级最高,胜过一切家族规则。
15
+ const SIZE_SUFFIX_RE = /\[(\d+)([km])\]/i;
16
+
17
+ /**
18
+ * 解析模型名里的 [Nk]/[Nm] 窗口后缀。
19
+ * @param {string} modelName
20
+ * @returns {number|null} 解析出的窗口 token 数,无后缀返回 null
21
+ */
22
+ export function parseContextSizeSuffix(modelName) {
23
+ if (!modelName || typeof modelName !== 'string') return null;
24
+ const m = modelName.match(SIZE_SUFFIX_RE);
25
+ if (!m) return null;
26
+ const num = parseInt(m[1], 10);
27
+ return m[2].toLowerCase() === 'm' ? num * 1000000 : num * 1000;
28
+ }
29
+
30
+ // 模型家族 → 窗口档位表(有序,首条命中)。后缀解析在表外先行(见 getModelMaxTokens)。
31
+ const MODEL_CONTEXT_SIZES = [
32
+ // haiku 全系 200K,显式置于一切 1M 默认之前(claude-haiku-4-5 等)
33
+ { match: /haiku/i, tokens: 200000 },
34
+ // 旧 Opus 修正:opus-4-0 / opus-4-1 / opus-4-5 实为 200K(opus-4-6 起才 1M)。
35
+ // (?!\d) 防误吞 opus-4-15 这类未来版本号;分隔符兼容连字符/点/空格。
36
+ { match: /opus[ -]?4[-. ][015](?!\d)/i, tokens: 200000 },
37
+ // claude-3-opus(3-opus / opus-3 两种写法)实为 200K
38
+ { match: /3[-.]opus|opus[-.]3/i, tokens: 200000 },
39
+ // 其余 Opus(4-6 起与未来版本)默认 1M
40
+ { match: /opus/i, tokens: 1000000 },
41
+ // mythons 默认 1M(置于 /claude/ 之前,避免被抢成 200K)
42
+ { match: /mythons/i, tokens: 1000000 },
43
+ // fable-5 家族(fable-5 / fable-5.x / fable-5-x)默认 1M,同样须排在 /claude/ 之前
44
+ { match: /fable[ -]5/i, tokens: 1000000 },
45
+ // 有意为之:裸 claude-sonnet-4-6(无 [1m] 后缀)维持 200K,与 Claude Code 选模型的
46
+ // 默认行为一致([1m] 是显式 opt-in);真 1M 场景靠后缀或 adaptContextWindow 纠偏兜底。
47
+ { match: /claude/i, tokens: 200000 },
48
+ { match: /gpt-4o|o1|o3|o4/i, tokens: 128000 },
49
+ { match: /gpt-4/i, tokens: 128000 },
50
+ { match: /gpt-3/i, tokens: 16000 },
51
+ // deepseek-v4 defaults to 1M; placed before generic /deepseek/ so the
52
+ // first-match-wins loop picks it up before falling through to 128K.
53
+ { match: /deepseek-v4/i, tokens: 1000000 },
54
+ { match: /deepseek/i, tokens: 128000 },
55
+ ];
56
+
57
+ /**
58
+ * 模型名 → 上下文窗口 token 数。后缀优先,其次家族档位表,默认 200K。
59
+ * @param {string|null|undefined} modelName
60
+ * @returns {number}
61
+ */
62
+ export function getModelMaxTokens(modelName) {
63
+ if (!modelName) return 200000;
64
+ const suffix = parseContextSizeSuffix(modelName);
65
+ if (suffix) return suffix;
66
+ for (const entry of MODEL_CONTEXT_SIZES) {
67
+ if (entry.match.test(modelName)) return entry.tokens;
68
+ }
69
+ return 200000;
70
+ }
71
+
72
+ /**
73
+ * 校准二分类:名字 → 1M/200K(血条 calibration 'auto' 路径专用)。
74
+ * 不变量:只返回 1000000 或 200000(resolveCalibrationTokens 依赖此不变量)。
75
+ * 裸 '1m' 子串(无方括号,如 deepseek-v3-1m)→ 1M 的宽松规则仅限本分类器,
76
+ * 刻意不进 getModelMaxTokens(后者面向精确档位)。128K/16K 档归入 200K 桶。
77
+ * @param {string} modelName
78
+ * @returns {1000000|200000}
79
+ */
80
+ export function classifyContextWindow(modelName) {
81
+ if (!modelName || typeof modelName !== 'string') return 200000;
82
+ if (modelName.toLowerCase().includes('1m')) return 1000000;
83
+ return getModelMaxTokens(modelName) >= 1000000 ? 1000000 : 200000;
84
+ }
85
+
86
+ /**
87
+ * 血条自适应纠偏:把"分类器判出的上下文窗口"按真实用量修正。
88
+ * 一个真正的 200K 模型,其输入上下文(input + cache_creation + cache_read)物理上不可能
89
+ * 超过 200K —— 超了 API 直接拒收。所以一旦真实输入用量越过 200K 还被判成 200K,必然是
90
+ * model 名识别错了(误判),此时自动升到 1M,免得血条卡死在 100%、百分比与真实进度脱节。
91
+ * 仅做 200K→1M 这一个方向的纠偏;其余判定(1M、各家 200K 真值等)一律原样返回。
92
+ * 注意:usedContextTokens 必须是"输入侧"用量(sumUsageInputTokens,不含 output_tokens),
93
+ * 否则大输出会误触发。
94
+ * @param {number} classifiedTokens classifyContextWindow / getModelMaxTokens 的结果
95
+ * @param {number} usedContextTokens 当前输入上下文实际用量(input + cache_creation + cache_read)
96
+ * @returns {number} 修正后的上下文窗口 token 数(1000000 或原值)
97
+ */
98
+ export function adaptContextWindow(classifiedTokens, usedContextTokens) {
99
+ if (classifiedTokens === 200000 && usedContextTokens > 200000) return 1000000;
100
+ return classifiedTokens;
101
+ }
102
+
103
+ /**
104
+ * cache_creation 兼容求和:flat 字段(cache_creation_input_tokens)存在(非 null/undefined,
105
+ * 0 也算存在)直接用;缺失时回落到新版嵌套对象 usage.cache_creation 的各 TTL 分桶求和
106
+ * (ephemeral_5m_input_tokens + ephemeral_1h_input_tokens,未来新增分桶自动计入)。
107
+ * @param {object|null|undefined} usage API usage 对象
108
+ * @returns {number}
109
+ */
110
+ export function sumCacheCreationTokens(usage) {
111
+ if (!usage) return 0;
112
+ if (usage.cache_creation_input_tokens != null) return usage.cache_creation_input_tokens || 0;
113
+ const nested = usage.cache_creation;
114
+ if (nested && typeof nested === 'object') {
115
+ let sum = 0;
116
+ for (const v of Object.values(nested)) {
117
+ if (typeof v === 'number' && Number.isFinite(v)) sum += v;
118
+ }
119
+ return sum;
120
+ }
121
+ return 0;
122
+ }
123
+
124
+ /**
125
+ * 输入侧上下文用量(不含 output_tokens)。用于自适应纠偏判定。
126
+ * @param {object|null|undefined} usage
127
+ * @returns {number}
128
+ */
129
+ export function sumUsageInputTokens(usage) {
130
+ if (!usage) return 0;
131
+ return (usage.input_tokens || 0) + sumCacheCreationTokens(usage) + (usage.cache_read_input_tokens || 0);
132
+ }
133
+
134
+ /**
135
+ * 血条分子统一口径:输入侧 + 末轮 output_tokens,对齐 Claude Code /context 的
136
+ * "当前上下文占用"语义(末轮回复已进入下一轮上下文)。
137
+ * @param {object|null|undefined} usage
138
+ * @returns {number}
139
+ */
140
+ export function sumUsageContextTokens(usage) {
141
+ if (!usage) return 0;
142
+ return sumUsageInputTokens(usage) + (usage.output_tokens || 0);
143
+ }
@@ -2,6 +2,7 @@ import { readFileSync, existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { getClaudeConfigDir } from '../../findcc.js';
5
+ import { getModelMaxTokens, adaptContextWindow, sumUsageInputTokens, sumUsageContextTokens } from './context-rules.js';
5
6
 
6
7
  export const CONTEXT_WINDOW_FILE = join(getClaudeConfigDir(), 'context-window.json');
7
8
  export const CLAUDE_SETTINGS_FILE = join(getClaudeConfigDir(), 'settings.json');
@@ -26,17 +27,10 @@ export function readModelContextSize() {
26
27
  const modelId = data?.model?.id || null;
27
28
  let contextSize = 200000;
28
29
  if (modelId) {
29
- const lower = modelId.toLowerCase();
30
- const sizeMatch = lower.match(/\[(\d+)([km])\]/);
31
- if (sizeMatch) {
32
- const num = parseInt(sizeMatch[1], 10);
33
- contextSize = sizeMatch[2] === 'm' ? num * 1000000 : num * 1000;
34
- } else if (/opus|mythons|fable[ -]5/i.test(lower)) {
35
- // Opus / mythons / fable-5 family models default to 1M context
36
- contextSize = 1000000;
37
- }
30
+ // [Nk]/[Nm] 后缀与家族档位统一走共享规则表(server/lib/context-rules.js)
31
+ contextSize = getModelMaxTokens(modelId);
38
32
  // Cache the base name → size mapping
39
- const base = lower.replace(/^claude-/i, '').replace(/\[.*\]/, '').trim();
33
+ const base = modelId.toLowerCase().replace(/^claude-/i, '').replace(/\[.*\]/, '').trim();
40
34
  _startupModelBase = base;
41
35
  _startupContextSize = contextSize;
42
36
  }
@@ -61,9 +55,9 @@ export function getContextSizeForModel(apiModelName) {
61
55
  if (_startupModelBase && base === _startupModelBase) {
62
56
  return _startupContextSize;
63
57
  }
64
- // Opus / mythons / fable-5 family always have 1M context; other unknown models default to 200K
65
- if (/opus|mythons|fable[ -]5/i.test(lower)) return 1000000;
66
- return 200000;
58
+ // 完整档位表见 server/lib/context-rules.js(与前端同源;含 haiku/旧 opus/3-opus 200K
59
+ // deepseek-v4 1M、gpt/deepseek 等三方档位,默认 200K)
60
+ return getModelMaxTokens(apiModelName);
67
61
  }
68
62
 
69
63
  /**
@@ -110,13 +104,12 @@ export function readClaudeProjectModel(cwd, filePath = CLAUDE_USER_CONFIG_FILE)
110
104
  */
111
105
  export function buildContextWindowEvent(usage, contextSize) {
112
106
  if (!usage) return null;
113
- const inputTokens = (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
107
+ // 分子口径与前端血条同源(context-rules):输入侧含嵌套 cache_creation 容错,total 含末轮 output
108
+ const inputTokens = sumUsageInputTokens(usage);
114
109
  const outputTokens = usage.output_tokens || 0;
115
- const totalTokens = inputTokens + outputTokens;
116
- // 自适应纠偏:真正的 200K 模型输入上下文(input+cache)不可能 > 200K(超了 API 拒收),
117
- // 一旦越过整窗还判成 200K,必是 model 名识别错 → 升 1M,使 used_percentage / size 不再失真。
118
- // 与 src/utils/helpers.js 的 adaptContextWindow 同一规则(此处服务端无法 import 前端模块,内联)。
119
- const effectiveSize = (contextSize === 200000 && inputTokens > 200000) ? 1000000 : contextSize;
110
+ const totalTokens = sumUsageContextTokens(usage);
111
+ // 自适应纠偏(共享规则,纠偏判定只用输入侧用量,详见 context-rules.adaptContextWindow)
112
+ const effectiveSize = adaptContextWindow(contextSize, inputTokens);
120
113
  const usedPct = Math.round((totalTokens / effectiveSize) * 100);
121
114
  return {
122
115
  total_input_tokens: inputTokens,
@@ -10,6 +10,7 @@ import { enrichRawIfNeeded } from '../lib/enrich-plan-input.js';
10
10
  import { validateLogPath } from '../lib/log-management.js';
11
11
  import { isMainAgentEntry, extractCachedContent } from '../lib/kv-cache-analyzer.js';
12
12
  import { CONTEXT_WINDOW_FILE, readModelContextSize, buildContextWindowEvent, getContextSizeForModel } from '../lib/context-watcher.js';
13
+ import { adaptContextWindow } from '../lib/context-rules.js';
13
14
 
14
15
  function turnEndNotify(req, res, parsedUrl, isLocal, deps) {
15
16
  if (!isLocal) {
@@ -295,9 +296,9 @@ async function events(req, res, parsedUrl, isLocal, deps) {
295
296
  const inputTokens = cw.total_input_tokens || 0;
296
297
  const outputTokens = cw.total_output_tokens || 0;
297
298
  const totalTokens = inputTokens + outputTokens;
298
- // 自适应纠偏:与 buildContextWindowEvent 同规则 —— 判 200K 但输入上下文用量已越窗,
299
- // 必是 model 名误判 → 升 1M,避免这条 fallback 与已纠偏的主路径产出不一致的血条。
300
- const effectiveSize = (contextSize === 200000 && inputTokens > 200000) ? 1000000 : contextSize;
299
+ // 自适应纠偏:与 buildContextWindowEvent 同源(context-rules.adaptContextWindow),
300
+ // 避免这条 fallback 与已纠偏的主路径产出不一致的血条。
301
+ const effectiveSize = adaptContextWindow(contextSize, inputTokens);
301
302
  const usedPct = effectiveSize > 0 ? Math.round((totalTokens / effectiveSize) * 100) : 0;
302
303
  const data = { ...cw, context_window_size: effectiveSize, used_percentage: usedPct, remaining_percentage: 100 - usedPct };
303
304
  res.write(`event: context_window\ndata: ${JSON.stringify(data)}\n\n`);