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.
- package/README.md +7 -8
- package/cli.js +6 -6
- package/concepts/ar/GlobalSettings.md +5 -2
- package/concepts/ar/ProxySwitch.md +15 -5
- package/concepts/da/GlobalSettings.md +5 -2
- package/concepts/da/ProxySwitch.md +15 -5
- package/concepts/de/GlobalSettings.md +5 -2
- package/concepts/de/ProxySwitch.md +15 -5
- package/concepts/en/GlobalSettings.md +7 -2
- package/concepts/en/ProxySwitch.md +15 -5
- package/concepts/es/GlobalSettings.md +5 -2
- package/concepts/es/ProxySwitch.md +15 -5
- package/concepts/fr/GlobalSettings.md +5 -2
- package/concepts/fr/ProxySwitch.md +15 -5
- package/concepts/it/GlobalSettings.md +5 -2
- package/concepts/it/ProxySwitch.md +15 -5
- package/concepts/ja/GlobalSettings.md +5 -2
- package/concepts/ja/ProxySwitch.md +15 -5
- package/concepts/ko/GlobalSettings.md +5 -2
- package/concepts/ko/ProxySwitch.md +15 -5
- package/concepts/no/GlobalSettings.md +5 -2
- package/concepts/no/ProxySwitch.md +15 -5
- package/concepts/pl/GlobalSettings.md +5 -2
- package/concepts/pl/ProxySwitch.md +15 -5
- package/concepts/pt-BR/GlobalSettings.md +5 -2
- package/concepts/pt-BR/ProxySwitch.md +34 -24
- package/concepts/ru/GlobalSettings.md +5 -2
- package/concepts/ru/ProxySwitch.md +15 -5
- package/concepts/th/GlobalSettings.md +5 -2
- package/concepts/th/ProxySwitch.md +15 -5
- package/concepts/tr/GlobalSettings.md +5 -2
- package/concepts/tr/ProxySwitch.md +34 -24
- package/concepts/uk/GlobalSettings.md +5 -2
- package/concepts/uk/ProxySwitch.md +15 -5
- package/concepts/zh/GlobalSettings.md +5 -2
- package/concepts/zh/ProxySwitch.md +15 -5
- package/concepts/zh-TW/GlobalSettings.md +7 -4
- package/concepts/zh-TW/ProxySwitch.md +15 -5
- package/dist/assets/App-CCQARuiH.js +2 -0
- package/dist/assets/{MdxEditorPanel-BmO4cIBT.js → MdxEditorPanel-BjJTwpmV.js} +1 -1
- package/dist/assets/{Mobile-ZXwho8zA.js → Mobile-BsJPIcqy.js} +1 -1
- package/dist/assets/{index-D4Pum3K_.js → index-ReSZCrNR.js} +2 -2
- package/dist/assets/seqResourceLoaders-K-REW6io.js +2 -0
- package/dist/assets/{seqResourceLoaders-BFMsIN74.css → seqResourceLoaders-P_ytfji6.css} +1 -1
- package/dist/index.html +1 -1
- package/dist/voice-packs/default/pack.json +1 -1
- package/findcc.js +50 -40
- package/interceptor.js +1 -1
- package/package.json +8 -4
- package/plugins/README.md +8 -0
- package/server/_paths.js +24 -22
- package/server/interceptor.js +39 -25
- package/server/lib/interceptor-core.js +107 -0
- package/server/routes/preferences.js +14 -1
- package/server.js +1 -0
- package/ultraAgents/README.md +49 -40
- package/dist/assets/App-DPdFPyOI.js +0 -2
- package/dist/assets/seqResourceLoaders-DXQFWqYa.js +0 -2
package/server/_paths.js
CHANGED
|
@@ -1,41 +1,43 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
-
// ⚠️
|
|
7
|
-
// `git mv server/_paths.js
|
|
8
|
-
//
|
|
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
|
|
11
|
-
// `npm link` / git clone dev
|
|
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
|
|
22
|
+
/** cc-viewer package root (at npm install time = `<node_modules>/cc-viewer`) */
|
|
21
23
|
export const PACKAGE_ROOT = resolve(HERE, '..');
|
|
22
|
-
/** cc-viewer
|
|
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
|
|
28
|
+
/** Vite build output (dist/index.html + assets/*) */
|
|
27
29
|
export const DIST_DIR = join(PACKAGE_ROOT, 'dist');
|
|
28
|
-
/**
|
|
30
|
+
/** Dev resources (public/voice-packs/* etc., copied to dist/ by vite) */
|
|
29
31
|
export const PUBLIC_DIR = join(PACKAGE_ROOT, 'public');
|
|
30
|
-
/**
|
|
32
|
+
/** Multi-language concept docs (GlobalSettings.md / Tool-*.md etc.) */
|
|
31
33
|
export const CONCEPTS_DIR = join(PACKAGE_ROOT, 'concepts');
|
|
32
|
-
/** Bundled plugin
|
|
34
|
+
/** Bundled plugin directory (scanned by plugin-loader at startup) */
|
|
33
35
|
export const PLUGINS_DIR = join(PACKAGE_ROOT, 'plugins');
|
|
34
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
42
|
+
/** cc-viewer's own package.json (updater/server.js reads version) */
|
|
41
43
|
export const PACKAGE_JSON = join(PACKAGE_ROOT, 'package.json');
|
package/server/interceptor.js
CHANGED
|
@@ -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?,
|
|
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 替换 ——
|
|
842
|
-
//
|
|
843
|
-
//
|
|
844
|
-
//
|
|
845
|
-
//
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
-
|
|
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';
|
package/ultraAgents/README.md
CHANGED
|
@@ -1,62 +1,71 @@
|
|
|
1
|
-
# ultraAgents —
|
|
1
|
+
# ultraAgents — Preset Experts
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
>
|
|
8
|
-
> `server/lib/ultra-agents-api.js
|
|
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", //
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
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
|
|
26
|
+
### title / description: inline localization in the JSON protocol layer
|
|
24
27
|
|
|
25
|
-
`title`
|
|
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
|
-
-
|
|
29
|
-
|
|
30
|
-
→
|
|
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`
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
>
|
|
42
|
-
> `src/utils/ultraplanTemplates.js`
|
|
43
|
-
>
|
|
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
|
-
|
|
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
|
-
-
|
|
51
|
-
- `id`
|
|
52
|
-
|
|
53
|
-
-
|
|
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
|
-
##
|
|
63
|
+
## Existing Demos
|
|
56
64
|
|
|
57
|
-
|
|
|
65
|
+
| File | Expert | `title` / `description` | `content` source |
|
|
58
66
|
| --- | --- | --- | --- |
|
|
59
|
-
| `code-expert.json` |
|
|
60
|
-
| `research-expert.json` |
|
|
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
|
-
|
|
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.
|