cc-viewer 1.6.330 → 1.6.331

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.
Files changed (58) hide show
  1. package/README.md +7 -8
  2. package/cli.js +6 -6
  3. package/concepts/ar/GlobalSettings.md +5 -2
  4. package/concepts/ar/ProxySwitch.md +15 -5
  5. package/concepts/da/GlobalSettings.md +5 -2
  6. package/concepts/da/ProxySwitch.md +15 -5
  7. package/concepts/de/GlobalSettings.md +5 -2
  8. package/concepts/de/ProxySwitch.md +15 -5
  9. package/concepts/en/GlobalSettings.md +7 -2
  10. package/concepts/en/ProxySwitch.md +15 -5
  11. package/concepts/es/GlobalSettings.md +5 -2
  12. package/concepts/es/ProxySwitch.md +15 -5
  13. package/concepts/fr/GlobalSettings.md +5 -2
  14. package/concepts/fr/ProxySwitch.md +15 -5
  15. package/concepts/it/GlobalSettings.md +5 -2
  16. package/concepts/it/ProxySwitch.md +15 -5
  17. package/concepts/ja/GlobalSettings.md +5 -2
  18. package/concepts/ja/ProxySwitch.md +15 -5
  19. package/concepts/ko/GlobalSettings.md +5 -2
  20. package/concepts/ko/ProxySwitch.md +15 -5
  21. package/concepts/no/GlobalSettings.md +5 -2
  22. package/concepts/no/ProxySwitch.md +15 -5
  23. package/concepts/pl/GlobalSettings.md +5 -2
  24. package/concepts/pl/ProxySwitch.md +15 -5
  25. package/concepts/pt-BR/GlobalSettings.md +5 -2
  26. package/concepts/pt-BR/ProxySwitch.md +34 -24
  27. package/concepts/ru/GlobalSettings.md +5 -2
  28. package/concepts/ru/ProxySwitch.md +15 -5
  29. package/concepts/th/GlobalSettings.md +5 -2
  30. package/concepts/th/ProxySwitch.md +15 -5
  31. package/concepts/tr/GlobalSettings.md +5 -2
  32. package/concepts/tr/ProxySwitch.md +34 -24
  33. package/concepts/uk/GlobalSettings.md +5 -2
  34. package/concepts/uk/ProxySwitch.md +15 -5
  35. package/concepts/zh/GlobalSettings.md +5 -2
  36. package/concepts/zh/ProxySwitch.md +15 -5
  37. package/concepts/zh-TW/GlobalSettings.md +7 -4
  38. package/concepts/zh-TW/ProxySwitch.md +15 -5
  39. package/dist/assets/App-CCQARuiH.js +2 -0
  40. package/dist/assets/{MdxEditorPanel-BmO4cIBT.js → MdxEditorPanel-BjJTwpmV.js} +1 -1
  41. package/dist/assets/{Mobile-ZXwho8zA.js → Mobile-BsJPIcqy.js} +1 -1
  42. package/dist/assets/{index-D4Pum3K_.js → index-ReSZCrNR.js} +2 -2
  43. package/dist/assets/seqResourceLoaders-K-REW6io.js +2 -0
  44. package/dist/assets/{seqResourceLoaders-BFMsIN74.css → seqResourceLoaders-P_ytfji6.css} +1 -1
  45. package/dist/index.html +1 -1
  46. package/dist/voice-packs/default/pack.json +1 -1
  47. package/findcc.js +50 -40
  48. package/interceptor.js +1 -1
  49. package/package.json +8 -4
  50. package/plugins/README.md +8 -0
  51. package/server/_paths.js +24 -22
  52. package/server/interceptor.js +39 -25
  53. package/server/lib/interceptor-core.js +107 -0
  54. package/server/routes/preferences.js +14 -1
  55. package/server.js +1 -0
  56. package/ultraAgents/README.md +49 -40
  57. package/dist/assets/App-DPdFPyOI.js +0 -2
  58. package/dist/assets/seqResourceLoaders-DXQFWqYa.js +0 -2
package/server/_paths.js CHANGED
@@ -1,41 +1,43 @@
1
- // 集中所有「相对于仓库根 / server / node_modules」的物理路径常量,
2
- // 避免 __dirname 跨深度 join('..','..',...) 写错。新代码直接 import 这里;
3
- // 老代码渐进迁移。命名 `_paths.js`:下划线前缀 = server 启动期常量集,
4
- // 与业务模块 (lib/*.js) 目录排序上自然区隔。
1
+ // Centralized physical path constants relative to repo root / server root / node_modules,
2
+ // to avoid error-prone cross-depth join('..','..',...) chains. New code should import from here;
3
+ // legacy code migrates gradually. Named `_paths.js`: underscore prefix = server startup
4
+ // constants, visually separated from business modules (lib/*.js) in directory listings.
5
5
  //
6
- // ⚠️ 物理位置敏感:本文件路径 = 所有常量的锚点(HERE = dirname(本文件))。
7
- // `git mv server/_paths.js <其它路径>` 会让 PACKAGE_ROOT / NODE_MODULES / DIST_DIR /
8
- // 等全部偏移,但不会有静态错误 —— 改动文件位置后必须人工核对所有 import 方的解析结果。
6
+ // ⚠️ Position-sensitive: this file's own path is the anchor for every constant
7
+ // (HERE = dirname(this file)). `git mv server/_paths.js <elsewhere>` will silently shift
8
+ // PACKAGE_ROOT / NODE_MODULES / DIST_DIR / etc. — no static error, so every import site
9
+ // must be manually verified after moving this file.
9
10
  //
10
- // NODE_MODULES 假设:cc-viewer 物理位于 `<node_modules>/cc-viewer/`(生产 npm install)。
11
- // `npm link` / git clone dev 场景下计算到错的位置;上层 (findcc.js)
12
- // `getGlobalNodeModulesDir()` 兜底,但本常量本身不感知这些边界。
11
+ // NODE_MODULES assumption: cc-viewer physically lives at `<node_modules>/cc-viewer/`
12
+ // (production npm install). Under `npm link` / git clone dev setups this resolves to the
13
+ // wrong location; upstream consumers (findcc.js) use `getGlobalNodeModulesDir()` as a
14
+ // fallback, but this constant itself is not aware of those edge cases.
13
15
  import { fileURLToPath } from 'node:url';
14
16
  import { dirname, join, resolve } from 'node:path';
15
17
 
16
18
  const HERE = dirname(fileURLToPath(import.meta.url));
17
19
 
18
- /** server/ 目录绝对路径 */
20
+ /** Absolute path of server/ directory */
19
21
  export const SERVER_DIR = HERE;
20
- /** cc-viewer 包根(npm install = `<node_modules>/cc-viewer`) */
22
+ /** cc-viewer package root (at npm install time = `<node_modules>/cc-viewer`) */
21
23
  export const PACKAGE_ROOT = resolve(HERE, '..');
22
- /** cc-viewer 同级 node_modules/(npm install 假设;npm link 失效) */
24
+ /** cc-viewer's sibling node_modules/ (npm install assumption; broken under npm link) */
23
25
  export const NODE_MODULES = resolve(HERE, '..', '..');
24
- /** server/lib/ 子目录 */
26
+ /** server/lib/ subdirectory */
25
27
  export const SERVER_LIB = join(HERE, 'lib');
26
- /** Vite build 产物(dist/index.html + assets/*) */
28
+ /** Vite build output (dist/index.html + assets/*) */
27
29
  export const DIST_DIR = join(PACKAGE_ROOT, 'dist');
28
- /** dev 资源(public/voice-packs/* 等,会被 vite 拷到 dist/) */
30
+ /** Dev resources (public/voice-packs/* etc., copied to dist/ by vite) */
29
31
  export const PUBLIC_DIR = join(PACKAGE_ROOT, 'public');
30
- /** 多语言概念文档(GlobalSettings.md / Tool-*.md 等) */
32
+ /** Multi-language concept docs (GlobalSettings.md / Tool-*.md etc.) */
31
33
  export const CONCEPTS_DIR = join(PACKAGE_ROOT, 'concepts');
32
- /** Bundled plugin 目录(plugin-loader 启动时扫这里) */
34
+ /** Bundled plugin directory (scanned by plugin-loader at startup) */
33
35
  export const PLUGINS_DIR = join(PACKAGE_ROOT, 'plugins');
34
- /** 随包发布的 ultraplan 预设专家目录(ultraAgents/*.json,/api/ultra-agents 扫这里) */
36
+ /** Bundled ultraplan preset expert directory (ultraAgents/*.json, served by /api/ultra-agents) */
35
37
  export const ULTRA_AGENTS_DIR = join(PACKAGE_ROOT, 'ultraAgents');
36
- /** 随包发布的 IM 内置默认技能源(server/imSkills/<lang>/<skill>/SKILL.md + scripts/*.mjs */
38
+ /** Bundled IM default skill source (server/imSkills/<lang>/<skill>/SKILL.md + scripts/*.mjs) */
37
39
  export const IM_SKILLS_DIR = join(SERVER_DIR, 'imSkills');
38
- /** 随包发布的 IM 人格预置模板(server/imPreset/<lang>.md{platform}/{id} 运行时替换) */
40
+ /** Bundled IM persona preset templates (server/imPreset/<lang>.md, {platform}/{id} substituted at runtime) */
39
41
  export const IM_PRESET_DIR = join(SERVER_DIR, 'imPreset');
40
- /** cc-viewer 自己的 package.jsonupdater/server.js version */
42
+ /** cc-viewer's own package.json (updater/server.js reads version) */
41
43
  export const PACKAGE_JSON = join(PACKAGE_ROOT, 'package.json');
@@ -16,7 +16,7 @@ import { homedir } from 'node:os';
16
16
  import { fileURLToPath, pathToFileURL } from 'node:url';
17
17
  import { dirname, join, basename } from 'node:path';
18
18
  import { LOG_DIR } from '../findcc.js';
19
- import { assembleStreamMessage, createStreamAssembler, cleanupTempFiles, findRecentLog, claimUntaggedLog, logFilePrefix, isAnthropicApiPath, isMainAgentRequest, rotateLogFile, fingerprintMsg, replaceTopLevelModel } from './lib/interceptor-core.js';
19
+ import { assembleStreamMessage, createStreamAssembler, cleanupTempFiles, findRecentLog, claimUntaggedLog, logFilePrefix, isAnthropicApiPath, isMainAgentRequest, rotateLogFile, fingerprintMsg, replaceTopLevelModel, injectOutputConfigEffort, resolveProfileModel } from './lib/interceptor-core.js';
20
20
 
21
21
 
22
22
 
@@ -77,7 +77,7 @@ export let _cachedHaikuModel = null;
77
77
  // 切换 active 只影响当前 ccv 进程的 workspace,不污染其他实例。
78
78
  // profile.json 存放在 LOG_DIR 下,受 --log-dir / CCV_LOG_DIR 影响
79
79
  const PROFILE_PATH = join(LOG_DIR, 'profile.json');
80
- let _activeProfile = null; // { id, name, baseURL?, apiKey?, models?, activeModel? }
80
+ let _activeProfile = null; // { id, name, baseURL?, apiKey?, effort?, ANTHROPIC_MODEL?, ANTHROPIC_DEFAULT_OPUS_MODEL?, ANTHROPIC_DEFAULT_SONNET_MODEL?, ANTHROPIC_DEFAULT_HAIKU_MODEL?, activeModel?(legacy) }
81
81
 
82
82
  // 启动时捕获的原始配置(首次 API 请求时记录,不可变)
83
83
  let _defaultConfig = null; // { origin, authType, model }
@@ -838,32 +838,46 @@ export function setupInterceptor() {
838
838
  }
839
839
  }
840
840
  }
841
- // 3. Model 替换 —— 避免对整条 wire body(-c 重启后全量 checkpoint 可达数十 MB)
842
- // 二次 JSON.parse + 全量 re-stringify:用 L557 已解析的 body 读旧值,对原始
843
- // 字符串做有界定向替换(唯一非歧义匹配才生效);定位失败回退旧 parse 路径,
844
- // 最坏退化为现状。注意不能复用 requestEntry.body 重建 wire —— delta 路径
845
- // (上方 needsCheckpoint=false 分支)已把它的 messages 改成增量切片。
846
- if (_activeProfile.activeModel && _fetchOpts?.body) {
847
- const _oldModel = (body && typeof body === 'object') ? body.model : undefined;
848
- if (_oldModel === _activeProfile.activeModel) {
849
- // 已是目标 model,跳过(旧路径会原样 re-stringify,对上游等价)
841
+ // 3. Model 替换 —— body.model 家族名解析目标(opus/sonnet/haiku 各自字段,
842
+ // fable/mythos/未识别 ANTHROPIC_MODEL;旧数据回退 activeModel 整体替换)。
843
+ // 避免对整条 wire body(-c 重启后全量 checkpoint 可达数十 MB)二次 JSON.parse +
844
+ // 全量 re-stringify:用 requestEntry.body 读旧值(函数级作用域;行 606 body
845
+ // if 块级变量,此处已越界——历史 BUG),对原始字符串做有界定向替换(唯一非歧义匹配
846
+ // 才生效);定位失败回退旧 parse 路径。注意只读 .model:delta 路径只改 .messages,
847
+ // 不能整体复用 requestEntry.body 重建 wire。
848
+ const _rb = requestEntry.body;
849
+ const _oldModel = (_rb && typeof _rb === 'object' && typeof _rb.model === 'string') ? _rb.model : undefined;
850
+ const _targetModel = _oldModel ? resolveProfileModel(_oldModel, _activeProfile) : null;
851
+ if (_targetModel && _fetchOpts?.body) {
852
+ const _replaced = (typeof _fetchOpts.body === 'string')
853
+ ? replaceTopLevelModel(_fetchOpts.body, _oldModel, _targetModel)
854
+ : null;
855
+ if (_replaced !== null) {
856
+ _fetchOpts = { ..._fetchOpts, body: _replaced };
850
857
  } else {
851
- const _replaced = (typeof _fetchOpts.body === 'string' && typeof _oldModel === 'string')
852
- ? replaceTopLevelModel(_fetchOpts.body, _oldModel, _activeProfile.activeModel)
853
- : null;
854
- if (_replaced !== null) {
855
- _fetchOpts = { ..._fetchOpts, body: _replaced };
856
- } else {
857
- try {
858
- const _b = JSON.parse(_fetchOpts.body);
859
- if (_b.model) {
860
- _b.model = _activeProfile.activeModel;
861
- _fetchOpts = { ..._fetchOpts, body: JSON.stringify(_b) };
862
- }
863
- } catch { }
864
- }
858
+ try {
859
+ const _b = JSON.parse(_fetchOpts.body);
860
+ if (_b.model) {
861
+ _b.model = _targetModel;
862
+ _fetchOpts = { ..._fetchOpts, body: JSON.stringify(_b) };
863
+ }
864
+ } catch { }
865
865
  }
866
866
  }
867
+ // 4. Effort 注入 —— 强制 output_config.effort(对应 CLAUDE_CODE_EFFORT_LEVEL)。
868
+ // 在 model 替换之后执行,操作 _fetchOpts.body 的最新形态;仅当 profile 显式设置了
869
+ // 合法 effort 才注入。排除 count_tokens / heartbeat:这些端点可能拒绝未知字段(400),
870
+ // 且 output_config 对它们无意义。hasOutputConfig 优先用 requestEntry.body(函数级作用域,
871
+ // 行 606 的 body 在此已越界)判定;解析失败退化为截断字符串时,回退到对 wire 串扫描
872
+ // "output_config"(避免误走前插路径产生重复键 → JSON "后者胜" 把注入的 effort 丢掉)。
873
+ if (_activeProfile.effort && typeof _fetchOpts?.body === 'string' &&
874
+ !requestEntry.isCountTokens && !requestEntry.isHeartbeat) {
875
+ const _hasOutputConfig = (_rb && typeof _rb === 'object')
876
+ ? (!!_rb.output_config && typeof _rb.output_config === 'object')
877
+ : _fetchOpts.body.includes('"output_config"');
878
+ const _injected = injectOutputConfigEffort(_fetchOpts.body, _activeProfile.effort, _hasOutputConfig);
879
+ if (_injected !== null) _fetchOpts = { ..._fetchOpts, body: _injected };
880
+ }
867
881
  // 记录 proxy 信息到日志条目
868
882
  requestEntry.proxyProfile = _activeProfile.name;
869
883
  requestEntry.proxyUrl = _fetchUrl;
@@ -504,3 +504,110 @@ export function replaceTopLevelModel(jsonStr, oldModel, newModel) {
504
504
  const replaced = needle.slice(0, needle.length - oldVal.length) + JSON.stringify(newModel);
505
505
  return jsonStr.slice(0, idx) + replaced + jsonStr.slice(idx + needle.length);
506
506
  }
507
+
508
+ // proxy profile hot-switch 模型解析:按 request body 里 model 的家族名映射到 profile 的对应字段。
509
+ // 家族用**大小写不敏感子串**匹配(/opus/i 等),只认这几个已知家族单词——
510
+ // 因此 claude-opus-4-8、未来的 claude-opus-5 等任何版本都命中同一家族,版本升级无需重配。
511
+ // 字段直接沿用 Claude Code 的环境变量名以保持一致:
512
+ // ANTHROPIC_MODEL —— 主模型;body.model 含 "fable" / "mythos" 时映射到它
513
+ // ANTHROPIC_DEFAULT_OPUS_MODEL —— body.model 含 "opus"
514
+ // ANTHROPIC_DEFAULT_SONNET_MODEL —— 含 "sonnet"
515
+ // ANTHROPIC_DEFAULT_HAIKU_MODEL —— 含 "haiku"
516
+ // 家族字段留空 = 该家族不改写(透传原始 model)。
517
+ // 未识别家族(既不是 opus/sonnet/haiku 也不是 fable/mythos)不做兜底替换,原样透传。
518
+ // 兼容旧数据:profile 未设任何新字段但有 activeModel(老结构)时,回退为旧的整体替换语义。
519
+ // 返回目标模型字符串;无需改写(无目标 / 目标同旧值 / 入参非法 / 未识别家族)时返回 null。
520
+ export function resolveProfileModel(oldModel, profile) {
521
+ if (typeof oldModel !== 'string' || !oldModel || !profile || typeof profile !== 'object') return null;
522
+ const opus = typeof profile.ANTHROPIC_DEFAULT_OPUS_MODEL === 'string' ? profile.ANTHROPIC_DEFAULT_OPUS_MODEL.trim() : '';
523
+ const sonnet = typeof profile.ANTHROPIC_DEFAULT_SONNET_MODEL === 'string' ? profile.ANTHROPIC_DEFAULT_SONNET_MODEL.trim() : '';
524
+ const haiku = typeof profile.ANTHROPIC_DEFAULT_HAIKU_MODEL === 'string' ? profile.ANTHROPIC_DEFAULT_HAIKU_MODEL.trim() : '';
525
+ const primary = typeof profile.ANTHROPIC_MODEL === 'string' ? profile.ANTHROPIC_MODEL.trim() : '';
526
+ const hasNew = !!(primary || opus || sonnet || haiku);
527
+
528
+ let target = '';
529
+ if (hasNew) {
530
+ if (/opus/i.test(oldModel)) target = opus;
531
+ else if (/sonnet/i.test(oldModel)) target = sonnet;
532
+ else if (/haiku/i.test(oldModel)) target = haiku;
533
+ else if (/fable/i.test(oldModel) || /mythos/i.test(oldModel)) target = primary; // 显式家族 → 主模型
534
+ // 未识别家族:target 保持 '',下方返回 null(不替换、原样透传)
535
+ } else if (typeof profile.activeModel === 'string') {
536
+ target = profile.activeModel.trim(); // 旧数据整体替换语义
537
+ }
538
+
539
+ if (!target || target === oldModel) return null;
540
+ return target;
541
+ }
542
+
543
+ // 旧配置迁移:老 profile 用 { models:[], activeModel } 做整体替换(不分家族,所有请求都换成
544
+ // activeModel)。新方案改用 ANTHROPIC_MODEL + 三个家族字段。为**忠实保留**旧的整体替换语义,
545
+ // 把 activeModel 填入全部四个模型字段(仅填空缺项,不覆盖用户已设值),这样 opus/sonnet/haiku/
546
+ // fable 各家族都仍命中同一模型;用户之后可在 UI 里按需拆分。丢弃遗留的 models / activeModel。
547
+ // 幂等:无遗留字段时原样返回、changed=false。纯函数,不落盘;调用方决定是否持久化。
548
+ export function migrateProxyProfile(p) {
549
+ if (!p || typeof p !== 'object') return { profile: p, changed: false };
550
+ const hasLegacy = ('activeModel' in p) || ('models' in p);
551
+ if (!hasLegacy) return { profile: p, changed: false };
552
+ const { models: _drop1, activeModel, ...rest } = p;
553
+ const out = { ...rest };
554
+ const am = typeof activeModel === 'string' ? activeModel.trim() : '';
555
+ if (am) {
556
+ // 保留整体替换:四个字段都回填 activeModel(已有值不动)
557
+ for (const k of ['ANTHROPIC_MODEL', 'ANTHROPIC_DEFAULT_OPUS_MODEL', 'ANTHROPIC_DEFAULT_SONNET_MODEL', 'ANTHROPIC_DEFAULT_HAIKU_MODEL']) {
558
+ if (!out[k]) out[k] = am;
559
+ }
560
+ }
561
+ return { profile: out, changed: true };
562
+ }
563
+
564
+ // 迁移整份 profiles 列表;返回 { profiles, changed }(任一 profile 变更即 changed=true)。
565
+ export function migrateProxyProfileList(profiles) {
566
+ if (!Array.isArray(profiles)) return { profiles, changed: false };
567
+ let changed = false;
568
+ const migrated = profiles.map(p => {
569
+ const r = migrateProxyProfile(p);
570
+ if (r.changed) changed = true;
571
+ return r.profile;
572
+ });
573
+ return { profiles: migrated, changed };
574
+ }
575
+
576
+ // proxy profile hot-switch 支持强制 output_config.effort(对应 CLAUDE_CODE_EFFORT_LEVEL)。
577
+ // 与 replaceTopLevelModel 同源思路:常见路径(body 里没有 output_config)走定向前插,
578
+ // 避免对多 MB wire body 做 JSON.parse + 全量 re-stringify(-c 重启后 checkpoint 可达数十 MB)。
579
+ // - 前插:在顶层对象开括号 `{` 之后插入 `"output_config":{"effort":"<v>"}`(后跟逗号,除非对象为空)。
580
+ // 顶层永远以 `{` 开头,插入后仍是合法 JSON;且 JSON 重复键"后者胜",但此路径仅在
581
+ // 调用方确认 body 无 output_config 时启用,不会产生重复键。
582
+ // - 合并:body 已有 output_config(罕见,如 CLI 传了 --effort)时回退整体 parse/stringify,
583
+ // 把 effort 并入既有对象。此路径 O(n) 但极少触发,可接受。
584
+ // effort 值域受限(low/medium/high/xhigh/max),非法值 → null(调用方跳过注入)。
585
+ const _VALID_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
586
+ export function injectOutputConfigEffort(jsonStr, effort, hasOutputConfig) {
587
+ if (typeof jsonStr !== 'string' || !jsonStr) return null;
588
+ if (typeof effort !== 'string' || !_VALID_EFFORTS.has(effort)) return null;
589
+ if (hasOutputConfig) {
590
+ // 已有 output_config:整体 parse/stringify 合并,保留既有子字段
591
+ try {
592
+ const obj = JSON.parse(jsonStr);
593
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return null;
594
+ if (!obj.output_config || typeof obj.output_config !== 'object' || Array.isArray(obj.output_config)) {
595
+ obj.output_config = {};
596
+ }
597
+ obj.output_config.effort = effort;
598
+ return JSON.stringify(obj);
599
+ } catch { return null; }
600
+ }
601
+ // 无 output_config:定向前插
602
+ const i = jsonStr.indexOf('{');
603
+ if (i === -1) return null;
604
+ // 顶层必须是对象:`{` 之前只允许空白,排除顶层数组(如 `[{...}]`)等把首个 `{` 误当顶层的情形。
605
+ // 与合并路径的 Array.isArray 守卫对称。真实 /v1/messages body 恒为顶层对象。
606
+ if (/\S/.test(jsonStr.slice(0, i))) return null;
607
+ // 探测开括号后的首个非空白字符:若是 `}`(空对象)则不追加逗号,避免尾逗号非法 JSON
608
+ const after = jsonStr.slice(i + 1);
609
+ const m = after.match(/^\s*(\S)/);
610
+ const needsComma = !!(m && m[1] !== '}');
611
+ const insert = `"output_config":{"effort":${JSON.stringify(effort)}}` + (needsComma ? ',' : '');
612
+ return jsonStr.slice(0, i + 1) + insert + jsonStr.slice(i + 1);
613
+ }
@@ -4,6 +4,7 @@ import { join, dirname } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
5
  import { LOG_DIR, setLogDir, getClaudeConfigDir } from '../../findcc.js';
6
6
  import { PROFILE_PATH, _defaultConfig, getActiveProfileId, setActiveProfileForWorkspace, _loadProxyProfile } from '../interceptor.js';
7
+ import { migrateProxyProfileList } from '../lib/interceptor-core.js';
7
8
  import { setLang } from '../i18n.js';
8
9
  import { reconcileVoicePackPrefs as vpReconcile } from '../lib/voice-pack-manager.js';
9
10
  import { readClaudeProjectModel } from '../lib/context-watcher.js';
@@ -219,7 +220,19 @@ function claudeSettingsPost(req, res, parsedUrl, isLocal, deps) {
219
220
 
220
221
  function proxyProfilesGet(req, res, parsedUrl, isLocal, deps) {
221
222
  try {
222
- const data = existsSync(PROFILE_PATH) ? JSON.parse(readFileSync(PROFILE_PATH, 'utf-8')) : deps.defaultProxyProfiles;
223
+ let data = existsSync(PROFILE_PATH) ? JSON.parse(readFileSync(PROFILE_PATH, 'utf-8')) : deps.defaultProxyProfiles;
224
+ // 旧配置一次性迁移(models/activeModel → ANTHROPIC_MODEL);有变更则回写磁盘并刷新 active profile。
225
+ if (Array.isArray(data.profiles)) {
226
+ const { profiles: migrated, changed } = migrateProxyProfileList(data.profiles);
227
+ if (changed) {
228
+ data = { ...data, profiles: migrated };
229
+ try {
230
+ mkdirSync(dirname(PROFILE_PATH), { recursive: true });
231
+ writeFileSync(PROFILE_PATH, JSON.stringify(data, null, 2), { mode: 0o600 });
232
+ _loadProxyProfile();
233
+ } catch { /* 迁移落盘失败不阻塞 GET;下次仍会尝试 */ }
234
+ }
235
+ }
223
236
  // 用 interceptor.getActiveProfileId() 返回 effective active(workspace > profile.json.active > 'max')
224
237
  const effectiveActive = getActiveProfileId();
225
238
  // 本机(127.0.0.1)= admin:下发明文 profile.apiKey 供本人在编辑表单(👁 折叠)里查阅/复制;已授权
package/server.js CHANGED
@@ -2,4 +2,5 @@
2
2
  // working without forcing every external consumer to know the physical layout.
3
3
  // Removing this file requires updating package.json + any user scripts that
4
4
  // run `node server.js`. Implementation lives at server/server.js.
5
+ // See also: interceptor.js (same pattern for the interceptor module).
5
6
  export * from './server/server.js';
@@ -1,62 +1,71 @@
1
- # ultraAgents — 预设专家 (Preset ultraplan experts)
1
+ # ultraAgents — Preset Experts
2
2
 
3
- 本目录随 cc-viewer 包发布。其中每个 `*.json` 文件定义一个「预设专家」,会出现在
4
- **UltraPlan → 自定义专家编辑器 「载入模版」** 弹窗中,供用户选中后将名称与内容
5
- 一键载入编辑面板(再按需改写并另存为自己的自定义专家)。
3
+ This directory ships with the cc-viewer package. Each `*.json` file defines a
4
+ "preset expert" that appears in the **UltraPlan → Custom Expert Editor
5
+ "Load Template"** modal. Users can select a preset to load its name and content
6
+ into the editor with one click, then customize and save as their own expert.
6
7
 
7
- > 加载入口:服务端 `GET /api/ultra-agents`(只读本目录、无参数),实现见
8
- > `server/lib/ultra-agents-api.js`。本目录中的非 `*.json` 文件(如本 README)会被忽略。
8
+ > Loading entry point: server-side `GET /api/ultra-agents` (read-only, no parameters),
9
+ > implemented in `server/lib/ultra-agents-api.js`. Non-`*.json` files in this directory
10
+ > (like this README) are ignored.
9
11
 
10
- ## JSON 格式
12
+ ## JSON Format
11
13
 
12
14
  ```jsonc
13
15
  {
14
- "id": "code-expert", // 必填。唯一标识,仅 [A-Za-z0-9._-]、长度 ≤200、不以 . 开头。
15
- // 仅作去重键( id 取文件名排序靠前者),不参与拼路径。
16
- "version": 1, // 可选。前向兼容标记,当前加载器忽略(不读不校验)
17
- "title": { "zh": "代码专家", "en": "Code Expert" }, // 必填。专家名称,内联本地化,见下。
18
- "description": { "zh": "资深工程师…", "en": "Senior engineer…" }, // 可选。一句话描述,内联本地化。
19
- "content": "<system-reminder>\n…\n</system-reminder>" // 必填。单语言正文,见下「content」。
16
+ "id": "code-expert", // Required. Unique identifier: [A-Za-z0-9._-] only, ≤200 chars,
17
+ // must not start with '.'. Used as a dedup key (first by filename
18
+ // sort wins when ids collide); not involved in path construction.
19
+ "version": 1, // Optional. Forward-compat marker, ignored by current loader.
20
+ "title": { "zh": "代码专家", "en": "Code Expert" }, // Required. Expert name, inline localized (see below).
21
+ "description": { "zh": "资深工程师…", "en": "Senior engineer…" }, // Optional. One-liner description, inline localized.
22
+ "content": "<system-reminder>\n…\n</system-reminder>" // Required. Single-language body (see "content" below).
20
23
  }
21
24
  ```
22
25
 
23
- ### title / description:在 JSON 协议层内联本地化
26
+ ### title / description: inline localization in the JSON protocol layer
24
27
 
25
- `title` `description` 两个字段都支持两种写法,**本地化在 JSON 协议层内联完成**(不依赖外部 i18n):
28
+ Both `title` and `description` support two forms, with **localization done inline in the JSON
29
+ protocol layer** (no external i18n dependency):
26
30
 
27
- - **纯字符串**:所有语言都用同一份文本。
28
- - **本地化对象** `{ "zh": "…", "en": "…", "zh-TW": "…" }`:前端按当前界面语言解析,
29
- 回退顺序为 **精确语言去区域的主语言(`zh-TW`→`zh`、`pt-BR`→`pt`)→ `en` → `zh`
30
- 首个非空值**(解析逻辑见 `src/utils/resolveLocalized.js`)。
31
+ - **Plain string**: same text for all languages.
32
+ - **Localization object** `{ "zh": "…", "en": "…", "zh-TW": "…" }`: the frontend resolves based on
33
+ the current UI language, with fallback order **exact language region-stripped primary language
34
+ (`zh-TW`→`zh`, `pt-BR`→`pt`) `en` `zh` → first non-empty value** (resolution logic in
35
+ `src/utils/resolveLocalized.js`).
31
36
 
32
- 因此一个文件即可覆盖任意多种语言;未列出的界面语言走上述回退。
37
+ A single file can cover any number of languages; unsupported UI languages follow the fallback chain above.
33
38
 
34
- ### content:单语言
39
+ ### content: single-language
35
40
 
36
- `content` 是**单语言字符串**(不做本地化)。载入并保存后,发送给 Claude Code 时被当作 ultraplan
37
- 的作用域指令;若 `content`(trim 后)以 `<system-reminder>` 开头,则原样使用、**不会被二次包裹**
38
- (见 `buildCustomTemplate`,`src/utils/ultraplanTemplates.js`)。因此预设 `content` 建议以
39
- `<system-reminder>`、`[SCOPED INSTRUCTION]` 风格书写。
41
+ `content` is a **single-language string** (not localized). After loading and saving, it is sent
42
+ to Claude Code as the ultraplan scoped instruction. If `content` (after trimming) starts with
43
+ `<system-reminder>`, it is used as-is and **will not be double-wrapped** (see `buildCustomTemplate`
44
+ in `src/utils/ultraplanTemplates.js`). Preset `content` should therefore be written in the
45
+ `<system-reminder>`, `[SCOPED INSTRUCTION]` style.
40
46
 
41
- > 内置 `code-expert` / `research-expert` `content` **直接取自**
42
- > `src/utils/ultraplanTemplates.js` `ULTRAPLAN_VARIANTS.codeExpert` / `researchExpert`,
43
- > 并由 `test/ultra-agents-api.test.js` 钉死逐字节一致——改正文请改那个源文件后重新生成本目录 JSON,
44
- > 不要在此手写另一份。
47
+ > The `content` of the built-in `code-expert` / `research-expert` presets is **taken directly from**
48
+ > `src/utils/ultraplanTemplates.js`'s `ULTRAPLAN_VARIANTS.codeExpert` / `researchExpert`,
49
+ > and is pinned byte-for-byte by `test/ultra-agents-api.test.js` to change the body, edit that
50
+ > source file and regenerate the JSON in this directory; do not hand-write a second copy here.
45
51
 
46
- ## 校验与限制
52
+ ## Validation and Limits
47
53
 
48
- 加载时对每个文件做防御性校验,不合法者仅 `console.warn` 跳过、不影响其它文件:
54
+ Each file undergoes defensive validation at load time. Invalid files are skipped with
55
+ `console.warn` only — other files are unaffected:
49
56
 
50
- - 必须是合法 JSON 的**普通对象**(非数组/标量)。
51
- - `id` 通过上述规则;`title`、`content` 为「非空字符串」或「至少含一个非空字符串值的对象」。
52
- - 单文件 **≤ 256KB**,超出跳过;有效专家 **≤ 100** 个,超出忽略。
53
- - `description` 缺失或非法时按空串处理。
57
+ - Must be valid JSON representing a **plain object** (not an array/scalar).
58
+ - `id` must pass the rules above; `title` and `content` must be a non-empty string or an object
59
+ with at least one non-empty string value.
60
+ - Single file **≤ 256KB**, skipped if exceeded; valid experts **≤ 100**, excess ignored.
61
+ - Missing or invalid `description` is treated as an empty string.
54
62
 
55
- ## 现有 demo
63
+ ## Existing Demos
56
64
 
57
- | 文件 | 专家 | `title` / `description` | `content` 来源 |
65
+ | File | Expert | `title` / `description` | `content` source |
58
66
  | --- | --- | --- | --- |
59
- | `code-expert.json` | 代码专家 / Code Expert | 内联本地化( 18 语言) | `ULTRAPLAN_VARIANTS.codeExpert` |
60
- | `research-expert.json` | 调研专家 / Research Expert | 内联本地化( 18 语言) | `ULTRAPLAN_VARIANTS.researchExpert` |
67
+ | `code-expert.json` | Code Expert / 代码专家 | Inline localized (all 18 languages) | `ULTRAPLAN_VARIANTS.codeExpert` |
68
+ | `research-expert.json` | Research Expert / 调研专家 | Inline localized (all 18 languages) | `ULTRAPLAN_VARIANTS.researchExpert` |
61
69
 
62
- 新增预设:在本目录放一个新的 `*.json`(建议文件名与 `id` 一致),重启/刷新即可在弹窗中看到。
70
+ To add a new preset: drop a new `*.json` file in this directory (file name should match `id`),
71
+ and restart/refresh to see it in the modal.