Skip to content

Commit a8f45e9

Browse files
committed
fix(config): preserve unmanaged fields when saving profiles
1 parent 84805f2 commit a8f45e9

3 files changed

Lines changed: 61 additions & 2 deletions

File tree

docs/agents/config-profile-change.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
- `config list` 标识所有 Profile 与当前激活项。
4545
- `config show``auth status` 只输出本次最终选择的 `config``config_file`,不重复携带激活状态。
4646
- `config ui` 从持久化元数据读取激活项,提供显式激活操作,并在删除激活项后刷新为 `default`
47+
- `config ui` 保存时只替换 UI 管理的字段;Profile 中未展示但仍属于 `ConfigFile` 的合法字段必须保留,不能因打开并保存 UI 而丢失。
4748
- 同步 E2E topic routes、Skill setup 和自动生成 reference。
4849

4950
## 6. 最小测试矩阵
@@ -57,6 +58,7 @@
5758
- 登录、退出、`config set` 分别覆盖“当前激活项”和“显式不存在名称成功后创建”。
5859
- Console token 自动刷新不从其他 Profile 借用 AK/SK,也不把新 token 写入其他 Profile。
5960
- `config list/show/use/ui``auth status` 和依赖默认模型的消费命令覆盖对应 E2E。
61+
- `config ui` 覆盖保存时保留未管理字段,并继续允许空值清除 UI 管理字段。
6062

6163
## 7. 完成检查
6264

packages/commands/src/commands/config/ui.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
BailianError,
88
ExitCode,
99
normalizeConfigName,
10+
readConfigFile,
1011
writeConfigFile,
1112
deleteConfigProfile,
1213
type ConfigStore,
@@ -72,6 +73,19 @@ function buildProfilePatch(data: Record<string, unknown>): Record<string, string
7273
return cleaned;
7374
}
7475

76+
/** Preserve valid Config fields that the UI does not expose or manage. */
77+
function mergeUnmanagedProfileFields(
78+
existing: Record<string, unknown>,
79+
managedPatch: Record<string, string | number>,
80+
): Record<string, unknown> {
81+
const managedKeys = new Set<string>(VALID_KEYS);
82+
const merged: Record<string, unknown> = {};
83+
for (const [key, value] of Object.entries(existing)) {
84+
if (!managedKeys.has(key)) merged[key] = value;
85+
}
86+
return { ...merged, ...managedPatch };
87+
}
88+
7589
/**
7690
* Build the config-UI http server. Exported for tests. The handler enforces:
7791
* - Host header must be a loopback name (anti DNS-rebinding).
@@ -158,8 +172,10 @@ export function createConfigUiServer(token: string, configStore: ConfigStore): h
158172
sendJson(res, 400, { error: errMessage(err) });
159173
return;
160174
}
161-
await writeConfigFile(cleaned, normalized);
162-
sendJson(res, 200, { saved: cleaned });
175+
const existing = readConfigFile(normalized) as Record<string, unknown>;
176+
const saved = mergeUnmanagedProfileFields(existing, cleaned);
177+
await writeConfigFile(saved, normalized);
178+
sendJson(res, 200, { saved });
163179
return;
164180
}
165181

packages/commands/tests/config-ui.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,47 @@ test("POST /api/profile 写命名 profile(timeout 强制为 number),空串
128128
});
129129
});
130130

131+
test("POST /api/profile 保留 UI 未管理字段,同时替换 UI 管理字段", async () => {
132+
await withServer(async (port) => {
133+
await writeConfigFile(
134+
{
135+
api_key: "sk-old",
136+
output: "json",
137+
console_site: "international",
138+
console_region: "ap-southeast-1",
139+
console_switch_agent: 42,
140+
telemetry: false,
141+
},
142+
"stage",
143+
);
144+
145+
const save = await httpJson(port, "POST", `/api/profile?token=${TOKEN}`, {
146+
body: { name: "stage", data: { api_key: "sk-new" } },
147+
});
148+
expect(save.status).toBe(200);
149+
150+
const profile = readConfigFile("stage");
151+
expect(profile).toMatchObject({
152+
api_key: "sk-new",
153+
console_site: "international",
154+
console_region: "ap-southeast-1",
155+
console_switch_agent: 42,
156+
telemetry: false,
157+
});
158+
expect(profile.output).toBeUndefined();
159+
160+
const rawConfig = JSON.parse(readFileSync(getConfigPath(), "utf8"));
161+
expect(rawConfig.stage).toMatchObject({
162+
api_key: "sk-new",
163+
console_site: "international",
164+
console_region: "ap-southeast-1",
165+
console_switch_agent: 42,
166+
telemetry: false,
167+
});
168+
expect(rawConfig.stage.output).toBeUndefined();
169+
});
170+
});
171+
131172
test("New profile 立即保存空 Profile,其他配置读取可以看到", async () => {
132173
await withServer(async (port) => {
133174
const create = await httpJson(port, "POST", `/api/profile?token=${TOKEN}`, {

0 commit comments

Comments
 (0)