cc-viewer 1.7.11 → 1.7.12

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-gH_W4sVT.js"></script>
24
+ <script type="module" crossorigin src="./assets/index-QCUTFwkw.js"></script>
25
25
  <link rel="modulepreload" crossorigin href="./assets/vendor-antd-DADYo_zg.js">
26
26
  <link rel="modulepreload" crossorigin href="./assets/vendor-codemirror-tF6HNoR6.js">
27
27
  <link rel="modulepreload" crossorigin href="./assets/vendor-mdxeditor-CFAmRN3Y.js">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-viewer",
3
- "version": "1.7.11",
3
+ "version": "1.7.12",
4
4
  "description": "Claude Code logging, visualization, and management toolkit — launch a web viewer alongside Claude Code with full request/response tracing, proxy, and mobile support",
5
5
  "license": "MIT",
6
6
  "main": "server.js",
@@ -27,6 +27,29 @@ export function parseContextSizeSuffix(modelName) {
27
27
  return m[2].toLowerCase() === 'm' ? num * 1000000 : num * 1000;
28
28
  }
29
29
 
30
+ /**
31
+ * Resolve the model name to use for context-window classification (血条窗口判定专用).
32
+ *
33
+ * Precedence differs from getEffectiveModel (response-first) on one deliberate
34
+ * point: an EXPLICIT [Nk]/[Nm] suffix on the REQUEST model (`body.model`, which
35
+ * carries the user's hot-switch config / model selector intent) is authoritative
36
+ * and must NOT be overridden by the upstream response. Upstream APIs normalize
37
+ * the response `model` — e.g. hot-switching to `k3[1m]` makes Moonshot return
38
+ * `response.body.model: "k3"`, stripping the [1m] marker; a response-first read
39
+ * would then misclassify the window (bare k3 vs the configured 1M). So: request
40
+ * suffix wins; otherwise fall back to the response model, then the request name.
41
+ *
42
+ * @param {object|null|undefined} request log entry with body / response
43
+ * @returns {string|null}
44
+ */
45
+ export function getCalibrationModel(request) {
46
+ const reqModel = request?.body?.model;
47
+ if (typeof reqModel === 'string' && parseContextSizeSuffix(reqModel) != null) return reqModel;
48
+ const respModel = request?.response?.body?.model;
49
+ if (typeof respModel === 'string' && respModel) return respModel;
50
+ return (typeof reqModel === 'string' && reqModel) ? reqModel : null;
51
+ }
52
+
30
53
  // 模型家族 → 窗口档位表(有序,首条命中)。后缀解析在表外先行(见 getModelMaxTokens)。
31
54
  const MODEL_CONTEXT_SIZES = [
32
55
  // haiku 全系 200K,显式置于一切 1M 默认之前(claude-haiku-4-5 等)
@@ -48,6 +71,10 @@ const MODEL_CONTEXT_SIZES = [
48
71
  { match: /gpt-4o|o1|o3|o4/i, tokens: 128000 },
49
72
  { match: /gpt-4/i, tokens: 128000 },
50
73
  { match: /gpt-3/i, tokens: 16000 },
74
+ // Kimi 家族精确档:k2.x/k3 等带 kimi/moonshot 前缀的 → 256K;裸 'k3'(无前缀,
75
+ // 代理直连时的简写 model 名)→ 256K 精确档但 classifyContextWindow 不升 1M
76
+ // (见该函数的家族特判,裸 k3 归 200K 桶,超量由 adaptContextWindow 纠偏)。
77
+ { match: /kimi|moonshot|^k3$/i, tokens: 256000 },
51
78
  // deepseek-v4 defaults to 1M; placed before generic /deepseek/ so the
52
79
  // first-match-wins loop picks it up before falling through to 128K.
53
80
  { match: /deepseek-v4/i, tokens: 1000000 },
@@ -74,12 +101,19 @@ export function getModelMaxTokens(modelName) {
74
101
  * 不变量:只返回 1000000 或 200000(resolveCalibrationTokens 依赖此不变量)。
75
102
  * 裸 '1m' 子串(无方括号,如 deepseek-v3-1m)→ 1M 的宽松规则仅限本分类器,
76
103
  * 刻意不进 getModelMaxTokens(后者面向精确档位)。128K/16K 档归入 200K 桶。
104
+ * Kimi 家族特判:kimi/moonshot 前缀型号(k2.x/k3,真实窗口 256K)归 1M 桶 ——
105
+ * 避免会话中段从 200K 重标定到 256K/1M 的跳变;代价是相对真实 256K 上限
106
+ * 长期低估(约 4 倍刻度),可接受。裸 'k3' 同样归 1M:代理热切换到
107
+ * 'k3[1m]' 时上游会把响应 model 归一化成裸 'k3'(剥掉 [1m] 后缀),
108
+ * response-first 解析读到裸 'k3' 若归 200K 桶会与请求侧 1M 判定分裂,
109
+ * 血条分母错成 200K;且裸 'k3' 本就是 k3[1m] 的 1M 形态被剥后缀的产物。
77
110
  * @param {string} modelName
78
111
  * @returns {1000000|200000}
79
112
  */
80
113
  export function classifyContextWindow(modelName) {
81
114
  if (!modelName || typeof modelName !== 'string') return 200000;
82
115
  if (modelName.toLowerCase().includes('1m')) return 1000000;
116
+ if (/kimi|moonshot|^k3$/i.test(modelName)) return 1000000;
83
117
  return getModelMaxTokens(modelName) >= 1000000 ? 1000000 : 200000;
84
118
  }
85
119
 
@@ -88,7 +122,9 @@ export function classifyContextWindow(modelName) {
88
122
  * 一个真正的 200K 模型,其输入上下文(input + cache_creation + cache_read)物理上不可能
89
123
  * 超过 200K —— 超了 API 直接拒收。所以一旦真实输入用量越过 200K 还被判成 200K,必然是
90
124
  * model 名识别错了(误判),此时自动升到 1M,免得血条卡死在 100%、百分比与真实进度脱节。
91
- * 仅做 200K→1M 这一个方向的纠偏;其余判定(1M、各家 200K 真值等)一律原样返回。
125
+ * One-way upgrades only: 200K→1M and 256K→1M (the kimi exact tier used by the
126
+ * server-side SSE path); every other classification (1M, 128K/16K tiers, true
127
+ * 200K values) is returned unchanged — 128K is deliberately never promoted.
92
128
  * 注意:usedContextTokens 必须是"输入侧"用量(sumUsageInputTokens,不含 output_tokens),
93
129
  * 否则大输出会误触发。
94
130
  * @param {number} classifiedTokens classifyContextWindow / getModelMaxTokens 的结果
@@ -97,6 +133,7 @@ export function classifyContextWindow(modelName) {
97
133
  */
98
134
  export function adaptContextWindow(classifiedTokens, usedContextTokens) {
99
135
  if (classifiedTokens === 200000 && usedContextTokens > 200000) return 1000000;
136
+ if (classifiedTokens === 256000 && usedContextTokens > 256000) return 1000000;
100
137
  return classifiedTokens;
101
138
  }
102
139
 
@@ -2,7 +2,7 @@ import { readFileSync, existsSync, realpathSync } 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
+ import { getModelMaxTokens, adaptContextWindow, sumUsageInputTokens, sumUsageContextTokens, getCalibrationModel } from './context-rules.js';
6
6
 
7
7
  export const CONTEXT_WINDOW_FILE = join(getClaudeConfigDir(), 'context-window.json');
8
8
  export const CLAUDE_SETTINGS_FILE = join(getClaudeConfigDir(), 'settings.json');
@@ -43,10 +43,25 @@ export function readModelContextSize() {
43
43
  /**
44
44
  * Get context size for a given API model name (e.g. 'claude-opus-4-6-20250514').
45
45
  * Uses startup cache to avoid re-reading the file.
46
- * @param {string} apiModelName - model name from req.body.model
46
+ * Accepts either a bare model-name string (legacy path) or a full log entry.
47
+ * Entry input resolves the model via getCalibrationModel (context-rules.js):
48
+ * an explicit [Nk]/[Nm] suffix on the REQUEST model wins (the user's hot-switch
49
+ * config intent, e.g. k3[1m]); otherwise the upstream response.body.model is
50
+ * authoritative. The startup cache is request-side static info, stale after a
51
+ * hot-switch, so entry resolution skips it and goes straight to the family
52
+ * rules table. String input keeps legacy cache-first behavior unchanged.
53
+ * @param {string|object} modelOrEntry - model name, or log entry with body/response
47
54
  * @returns {number} context window size in tokens
48
55
  */
49
- export function getContextSizeForModel(apiModelName) {
56
+ export function getContextSizeForModel(modelOrEntry) {
57
+ const isEntry = modelOrEntry !== null && typeof modelOrEntry === 'object';
58
+ // Entry input: calibration-aware resolution (request [Nk]/[Nm] suffix wins,
59
+ // else response model). Authoritative over the stale startup cache.
60
+ if (isEntry) {
61
+ const model = getCalibrationModel(modelOrEntry);
62
+ return model ? getModelMaxTokens(model) : (_startupContextSize || 200000);
63
+ }
64
+ const apiModelName = modelOrEntry;
50
65
  if (!apiModelName) return _startupContextSize || 200000;
51
66
  const lower = apiModelName.toLowerCase();
52
67
  // Extract base: 'claude-opus-4-6-20250514' → 'opus-4-6'
@@ -56,7 +71,7 @@ export function getContextSizeForModel(apiModelName) {
56
71
  return _startupContextSize;
57
72
  }
58
73
  // 完整档位表见 server/lib/context-rules.js(与前端同源;含 haiku/旧 opus/3-opus 200K、
59
- // deepseek-v4 1M、gpt/deepseek 等三方档位,默认 200K)
74
+ // deepseek-v4 1M、kimi/moonshot 256K、gpt/deepseek 等三方档位,默认 200K)
60
75
  return getModelMaxTokens(apiModelName);
61
76
  }
62
77
 
@@ -156,7 +156,7 @@ export function processWatchedEntry(parsed, ctx) {
156
156
  if (cached) sendEventToClients(clients, 'kv_cache_content', cached);
157
157
  const usage = parsed.response?.body?.usage;
158
158
  if (usage) {
159
- const contextSize = getContextSizeForModel(parsed.body?.model);
159
+ const contextSize = getContextSizeForModel(parsed);
160
160
  const cwData = buildContextWindowEvent(usage, contextSize);
161
161
  if (cwData) sendEventToClients(clients, 'context_window', cwData);
162
162
  }
@@ -320,7 +320,7 @@ async function events(req, res, parsedUrl, isLocal, deps) {
320
320
  if (!latestContextWindow) {
321
321
  const usage = entry.response?.body?.usage;
322
322
  if (usage) {
323
- const contextSize = getContextSizeForModel(entry.body?.model);
323
+ const contextSize = getContextSizeForModel(entry);
324
324
  const cw = buildContextWindowEvent(usage, contextSize);
325
325
  if (cw) latestContextWindow = cw;
326
326
  }
@@ -7,6 +7,10 @@
7
7
  * server-reported model in `response.body.model` (authoritative under proxy
8
8
  * hot-switch) over the client-supplied `body.model`. Returns null when both
9
9
  * are missing — callers should fall back to a sensible default.
10
+ *
11
+ * KEEP IN SYNC: server/lib/context-watcher.js getContextSizeForModel reuses
12
+ * this precedence for its entry path — changing the priority here must be
13
+ * mirrored there (and vice versa).
10
14
  */
11
15
  export function getEffectiveModel(request) {
12
16
  return request?.response?.body?.model || request?.body?.model || null;
@@ -20,8 +20,9 @@ export {
20
20
  sumCacheCreationTokens,
21
21
  sumUsageInputTokens,
22
22
  sumUsageContextTokens,
23
+ getCalibrationModel,
23
24
  } from '../../server/lib/context-rules.js';
24
- import { classifyContextWindow, adaptContextWindow } from '../../server/lib/context-rules.js';
25
+ import { classifyContextWindow, adaptContextWindow, getCalibrationModel } from '../../server/lib/context-rules.js';
25
26
 
26
27
  // getEffectiveModel moved to ./effectiveModel.js (pure, node-testable — sessionMerge/sessionManager
27
28
  // import it without helpers' Vite-only svg imports); re-exported here to keep import paths stable.
@@ -57,7 +58,9 @@ const CALIBRATION_TOKEN_MAP = {
57
58
  export function resolveCalibrationTokens(calibrationModel, lastMainAgent, projectModelHint = null) {
58
59
  const direct = CALIBRATION_TOKEN_MAP[calibrationModel];
59
60
  if (direct) return direct;
60
- const lastModel = lastMainAgent ? getEffectiveModel(lastMainAgent) : null;
61
+ // 校准用 getCalibrationModel:请求名带显式 [Nk]/[Nm] 后缀时优先(用户热切换配置的
62
+ // 1M 意图),不被上游响应归一化(如 k3[1m]→裸 k3)覆盖;其余回退 response-first。
63
+ const lastModel = lastMainAgent ? getCalibrationModel(lastMainAgent) : null;
61
64
  // 优先用真实 mainAgent 信号;haiku 一律视为 init ping 噪声,跳过
62
65
  if (typeof lastModel === 'string' && lastModel && !/haiku/i.test(lastModel)) {
63
66
  return classifyContextWindow(lastModel);
@@ -334,7 +337,7 @@ const MODEL_PROVIDERS = [
334
337
  match: /kimi|moonshot|^k3$/i,
335
338
  name: 'Kimi',
336
339
  color: 'var(--bg-model-avatar)',
337
- svg: '<svg t="1771495664798" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6649" width="200" height="200"><path d="M731.062857 590.262857v318.317714h-148.589714V443.977143a146.285714 146.285714 0 0 1-146.285714 146.285714l-214.491429-0.036571v318.354285H73.142857V165.814857h148.553143v275.858286h180.882286l119.771428-275.858286h167.753143l-67.84 156.269714a255.524571 255.524571 0 0 1-106.678857 119.588572h66.925714a148.553143 148.553143 0 0 1 148.516572 148.589714z m120.758857-473.965714a99.035429 99.035429 0 0 1 0 198.070857h-99.035428V215.332571a99.035429 99.035429 0 0 1 99.035428-99.035428z" fill="currentColor" p-id="6650"></path></svg>',
340
+ svg: '<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M932.096 0a82.048 82.048 0 1 1 0 164.096h-72.352a9.568 9.568 0 0 1-9.664-9.632V82.016A82.048 82.048 0 0 1 932.096 0z" fill="#1783FF"></path><path d="M472.064 477.856l309.76-307.2c5.888-5.792 2.56-17.472-4.96-17.472h-166.72a7.008 7.008 0 0 0-5.056 2.112L271.456 486.24c-5.12 5.12-12.8 0.576-12.8-7.68V162.976c0-5.376-3.584-9.792-7.936-9.792H135.936c-4.352 0-7.936 4.416-7.936 9.792V843.52c0 5.44 3.584 9.824 7.936 9.824h114.784c4.352 0 7.936-4.288 7.936-9.824v-138.656c0-2.912 1.024-5.728 2.88-7.616l103.424-102.656a6.72 6.72 0 0 1 8.8-0.928l276.64 203.616a328.64 328.64 0 0 0 147.296 54.688c4.608 0.512 8.544-4 8.544-9.824V711.68c0-4.96-2.912-9.056-7.008-9.632a214.528 214.528 0 0 1-86.432-34.496l-239.456-173.472c-5.024-3.328-5.632-11.936-1.184-16.224h-0.096z" fill="currentColor"></path></svg>',
338
341
  },
339
342
  {
340
343
  match: /glm|chatglm/i,