fix(env-sync): 同步 provider_config.json(UI 分组/顺序/模型级配置)
问题:只同步 v2/config.json 时,只在 v2/provider_config.json 里存在的 provider 分组到对面就是缺失的 —— 表现为「provider 数量对,但 UI 分组少几个」。 实测:9 个分组里有 4 个(GINKA API / zxcbug / zxcbug-p / d1)纯规则型, 密钥只存在 provider_config.json 的 config.access.apiKey 里,旧实现完全没收集。 修复: - sync-core:新增 V2_PROVIDER_CONFIG、extract/sanitize/collectProviderConfigSecrets、 providerConfigKeyManifest;密钥命名空间 PROVIDER_CONFIG/<providerId>/<key> - export:导出 v2.provider-config.json(脱敏),密钥并入加密束,manifest 记摘要与哈希 - import:按 providerId 合并规则、回填密钥空位、按 (providerId,modelId) 合并模型级配置、 providerOrder 以快照为准并保留本机特有分组;重复导入幂等 - import:provider_config 路径补「护住本机已有密钥 N 个」提示(原来只在 config.json 侧有) - 测试:单元 6 项 + E2E 2 项(分组完整同步/强制覆盖),全量 72 项通过
This commit is contained in:
@@ -25,6 +25,8 @@ export const REPO_DIR = path.join(WORK, "repo");
|
||||
export const BACKUP_ROOT = path.join(WORK, "backup");
|
||||
export const V2_CONFIG = path.join(ZCODE_HOME, "v2", "config.json");
|
||||
export const V2_SETTING = path.join(ZCODE_HOME, "v2", "setting.json");
|
||||
/** UI 的分组/排序/模型级配置(providerOrder、group、personalModelIds、contextWindow 等) */
|
||||
export const V2_PROVIDER_CONFIG = path.join(ZCODE_HOME, "v2", "provider_config.json");
|
||||
/** 模型同步默认关闭,显式开启才同步(ZCODE_SYNC_WITH_MODELS=1) */
|
||||
export const WITH_MODELS = process.env.ZCODE_SYNC_WITH_MODELS === "1";
|
||||
/** 模型选中态(当前用哪个provider/model)默认不覆盖本机,显式开启才应用 */
|
||||
@@ -318,6 +320,69 @@ export function parseModelRel(rel) {
|
||||
return { provider: rest.slice(0, i), key: rest.slice(i + 1) };
|
||||
}
|
||||
|
||||
/* ---------- provider_config.json:UI 分组/顺序/模型级配置 ----------
|
||||
* config.json 只管「provider 能不能调」;UI 上看到的 9 个分组、拖拽顺序、
|
||||
* 每条 provider 属于哪个 group、模型级参数(contextWindow 等)全在 provider_config.json。
|
||||
* 早期版本只同步 config.json,于是只在 provider_config 里存在的分组整个丢失。
|
||||
*/
|
||||
export const PROVIDER_CONFIG_PREFIX = "PROVIDER_CONFIG/";
|
||||
export function isProviderConfigEntry(rel) { return String(rel).startsWith(PROVIDER_CONFIG_PREFIX); }
|
||||
/** PROVIDER_CONFIG/<providerId>/<key> → { provider, key } */
|
||||
export function parseProviderConfigRel(rel) {
|
||||
const rest = String(rel).slice(PROVIDER_CONFIG_PREFIX.length);
|
||||
const i = rest.indexOf("/");
|
||||
if (i <= 0 || i === rest.length - 1) throw new Error(`非法 provider_config 密钥路径:${rel}`);
|
||||
return { provider: rest.slice(0, i), key: rest.slice(i + 1) };
|
||||
}
|
||||
/** providerRules 里的真密钥(内存中处理,调用方不得打印) */
|
||||
export function extractProviderConfigSecrets(pcfg) {
|
||||
const out = [];
|
||||
for (const r of pcfg?.config?.providerConfigRules?.providerRules ?? []) {
|
||||
const id = r?.providerId;
|
||||
if (!id) continue;
|
||||
for (const [k, v] of Object.entries(r?.config?.access ?? {})) {
|
||||
if (typeof v === "string" && MODEL_KEY_RE.test(k) && v.length >= 8) out.push({ provider: id, key: k, value: v });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
/** 全量收集,供泄漏扫描比对 */
|
||||
export function collectProviderConfigSecrets(pcfg) {
|
||||
const vals = new Set();
|
||||
const walk = (node) => {
|
||||
if (Array.isArray(node)) return node.forEach(walk);
|
||||
if (node && typeof node === "object") {
|
||||
for (const [k, v] of Object.entries(node)) {
|
||||
if (typeof v === "string" && MODEL_KEY_RE.test(k) && v.length >= 8) vals.add(v);
|
||||
else walk(v);
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(pcfg?.config?.providerConfigRules?.providerRules ?? []);
|
||||
return [...vals];
|
||||
}
|
||||
/** 脱敏:providerRules 里的密钥换成占位符;返回哪些 provider 带密钥(只记有无) */
|
||||
export function sanitizeProviderConfig(pcfg) {
|
||||
const clean = JSON.parse(JSON.stringify(pcfg ?? {}));
|
||||
const withSecrets = [];
|
||||
for (const r of clean?.config?.providerConfigRules?.providerRules ?? []) {
|
||||
const id = r?.providerId;
|
||||
let hit = false;
|
||||
for (const [k, v] of Object.entries(r?.config?.access ?? {})) {
|
||||
if (typeof v === "string" && MODEL_KEY_RE.test(k) && v.length >= 8) { r.config.access[k] = MODEL_SECRET_REF; hit = true; }
|
||||
}
|
||||
if (hit && id) withSecrets.push(id);
|
||||
}
|
||||
return { config: clean, withSecrets };
|
||||
}
|
||||
/** provider_config 密钥清单(只记 provider/key/sha) */
|
||||
export function providerConfigKeyManifest(keys) {
|
||||
return (keys ?? []).map(({ provider, key, value }) => ({
|
||||
rel: `${PROVIDER_CONFIG_PREFIX}${provider}/${key}`,
|
||||
sha256: crypto.createHash("sha256").update(String(value), "utf8").digest("hex"),
|
||||
}));
|
||||
}
|
||||
|
||||
/* ---------- 脱敏扫描:明文区不得出现密钥值 ---------- */
|
||||
/** 递归收集 v2 provider 里的敏感值(内存中比对,绝不打印) */
|
||||
export function collectModelSecrets(v2cfg) {
|
||||
|
||||
Reference in New Issue
Block a user