Skip to content

Commit e0f3d45

Browse files
committed
feat(config): add "config ui" local web UI to manage config profiles
启动绑定 127.0.0.1 的本地 HTTP server + 内嵌单页 WebUI,可视化查看/ 新建/切换/删除全部命名 profile 并编辑键值与凭证。 - core: readConfigProfiles / deleteConfigProfile 全量配置读写 API - commands/config/shared.ts: 抽出 VALID_KEYS/别名/校验,set.ts 复用 - commands/shared/local-server.ts: 抽出 listen/openInBrowser,login-console 复用 - config ui: token + Host 校验,--config 决定初始聚焦,密钥明文可编辑
1 parent ac4dbb9 commit e0f3d45

16 files changed

Lines changed: 833 additions & 111 deletions

File tree

packages/cli/src/commands.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
visionDescribe,
1717
configShow,
1818
configSet,
19+
configUi,
1920
update,
2021
appCall,
2122
appList,
@@ -103,6 +104,7 @@ export const commands: Record<string, AnyCommand> = {
103104
"vision describe": visionDescribe,
104105
"config show": configShow,
105106
"config set": configSet,
107+
"config ui": configUi,
106108
update,
107109
"app call": appCall,
108110
"app list": appList,

packages/commands/src/commands/auth/login-console.ts

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { execFile } from "node:child_process";
21
import { randomBytes } from "node:crypto";
32
import http from "node:http";
43

@@ -12,6 +11,7 @@ import {
1211
type Identity,
1312
type Settings,
1413
} from "bailian-cli-core";
14+
import { listenLocalServer, openInBrowser } from "../shared/local-server.ts";
1515

1616
/** 登录流程的能力面:身份(UA)、有效配置(timeout 等)、auth 域落盘。 */
1717
export interface LoginDeps {
@@ -361,32 +361,7 @@ async function extractCredentialsFromRequest(
361361
}
362362

363363
function listenServerOnFreeLocalPort(server: http.Server): Promise<number> {
364-
return new Promise((resolve, reject) => {
365-
const onErr = (e: Error) => reject(e);
366-
server.once("error", onErr);
367-
server.listen({ port: 0, host: "127.0.0.1", exclusive: true }, () => {
368-
server.off("error", onErr);
369-
const addr = server.address();
370-
if (!addr || typeof addr === "string") {
371-
reject(new Error("Expected TCP socket address"));
372-
return;
373-
}
374-
resolve(addr.port);
375-
});
376-
});
377-
}
378-
379-
function openInBrowser(url: string): Promise<void> {
380-
const platform = process.platform;
381-
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
382-
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
383-
384-
return new Promise((resolve, reject) => {
385-
execFile(cmd, args, { windowsHide: true }, (err) => {
386-
if (err) reject(err);
387-
else resolve();
388-
});
389-
});
364+
return listenLocalServer(server);
390365
}
391366

392367
const RETRY_DELAY_BASE_MS = 500;

packages/commands/src/commands/config/set.ts

Lines changed: 6 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,6 @@
1-
import {
2-
defineCommand,
3-
detectOutputFormat,
4-
maskToken,
5-
BailianError,
6-
ExitCode,
7-
type ConfigFile,
8-
} from "bailian-cli-core";
1+
import { defineCommand, detectOutputFormat, maskToken, type ConfigFile } from "bailian-cli-core";
92
import { emitResult } from "bailian-cli-runtime";
10-
11-
const VALID_KEYS = [
12-
"base_url",
13-
"output",
14-
"output_dir",
15-
"timeout",
16-
"api_key",
17-
"access_token",
18-
"access_key_id",
19-
"access_key_secret",
20-
"default_text_model",
21-
"default_video_model",
22-
"default_image_model",
23-
"default_speech_model",
24-
"default_omni_model",
25-
"workspace_id",
26-
];
27-
28-
// Keys whose values are secrets. Their stored value must never be echoed back in
29-
// cleartext (CI logs, pipes, shared terminals); show a masked form instead — the
30-
// same policy `config show` and `auth status` already follow.
31-
const SECRET_KEYS = new Set(["api_key", "access_token", "access_key_id", "access_key_secret"]);
32-
33-
// Allow hyphen-style keys (e.g. default-text-model → default_text_model)
34-
const KEY_ALIASES: Record<string, string> = {
35-
"base-url": "base_url",
36-
"output-dir": "output_dir",
37-
"api-key": "api_key",
38-
"access-token": "access_token",
39-
"access-key-id": "access_key_id",
40-
"access-key-secret": "access_key_secret",
41-
"default-text-model": "default_text_model",
42-
"default-video-model": "default_video_model",
43-
"default-image-model": "default_image_model",
44-
"default-speech-model": "default_speech_model",
45-
"default-omni-model": "default_omni_model",
46-
"workspace-id": "workspace_id",
47-
};
3+
import { SECRET_KEYS, resolveKey, validateAndCoerce } from "./shared.ts";
484

495
export default defineCommand({
506
description: "Set a config value",
@@ -55,7 +11,7 @@ export default defineCommand({
5511
type: "string",
5612
valueHint: "<key>",
5713
description:
58-
"Config key (base_url, output, output_dir, timeout, api_key, access_token, access_key_id, access_key_secret, default_*_model, workspace_id)",
14+
"Config key (base_url, output, output_dir, timeout, api_key, access_token, access_key_id, access_key_secret, security_token, default_*_model, workspace_id)",
5915
required: true,
6016
},
6117
value: { type: "string", valueHint: "<value>", description: "Value to set", required: true },
@@ -70,33 +26,9 @@ export default defineCommand({
7026
const key = flags.key;
7127
const value = flags.value;
7228

73-
// Resolve hyphen aliases to underscore keys
74-
const resolvedKey: string = KEY_ALIASES[key] || key;
75-
76-
if (!VALID_KEYS.includes(resolvedKey)) {
77-
throw new BailianError(
78-
`Invalid config key "${key}". Valid keys: ${VALID_KEYS.join(", ")}`,
79-
ExitCode.USAGE,
80-
);
81-
}
82-
83-
// Validate specific values
84-
if (resolvedKey === "output" && !["text", "json"].includes(value)) {
85-
throw new BailianError(
86-
`Invalid output format "${value}". Valid values: text, json`,
87-
ExitCode.USAGE,
88-
);
89-
}
90-
91-
if (resolvedKey === "timeout") {
92-
const num = Number(value);
93-
if (isNaN(num) || num <= 0) {
94-
throw new BailianError(
95-
`Invalid timeout "${value}". Must be a positive number.`,
96-
ExitCode.USAGE,
97-
);
98-
}
99-
}
29+
// Resolve hyphen aliases to underscore keys and validate/coerce the value.
30+
const resolvedKey: string = resolveKey(key);
31+
const coerced = validateAndCoerce(key, value);
10032

10133
const format = detectOutputFormat(settings.output);
10234

@@ -112,7 +44,6 @@ export default defineCommand({
11244
return;
11345
}
11446

115-
const coerced = resolvedKey === "timeout" ? Number(value) : value;
11647
await ctx.configStore().write({ [resolvedKey]: coerced } as Partial<ConfigFile>);
11748

11849
if (!settings.quiet) {
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { BailianError, ExitCode } from "bailian-cli-core";
2+
3+
/** Config keys that `config set` / `config ui` accept for read/write. */
4+
export const VALID_KEYS = [
5+
"base_url",
6+
"output",
7+
"output_dir",
8+
"timeout",
9+
"api_key",
10+
"access_token",
11+
"access_key_id",
12+
"access_key_secret",
13+
"security_token",
14+
"default_text_model",
15+
"default_video_model",
16+
"default_image_model",
17+
"default_speech_model",
18+
"default_omni_model",
19+
"workspace_id",
20+
] as const;
21+
22+
// Keys whose values are secrets. `config set` / `config show` mask these; the
23+
// web UI renders them as password fields (values are still sent in cleartext
24+
// over the token-gated localhost socket).
25+
export const SECRET_KEYS = new Set<string>([
26+
"api_key",
27+
"access_token",
28+
"access_key_id",
29+
"access_key_secret",
30+
"security_token",
31+
]);
32+
33+
// Allow hyphen-style keys (e.g. default-text-model → default_text_model).
34+
export const KEY_ALIASES: Record<string, string> = {
35+
"base-url": "base_url",
36+
"output-dir": "output_dir",
37+
"api-key": "api_key",
38+
"access-token": "access_token",
39+
"access-key-id": "access_key_id",
40+
"access-key-secret": "access_key_secret",
41+
"security-token": "security_token",
42+
"default-text-model": "default_text_model",
43+
"default-video-model": "default_video_model",
44+
"default-image-model": "default_image_model",
45+
"default-speech-model": "default_speech_model",
46+
"default-omni-model": "default_omni_model",
47+
"workspace-id": "workspace_id",
48+
};
49+
50+
/** Resolve a hyphen alias to its underscore config key. */
51+
export function resolveKey(key: string): string {
52+
return KEY_ALIASES[key] || key;
53+
}
54+
55+
/**
56+
* Validate a single config entry and coerce its value to the stored type.
57+
* Throws BailianError(USAGE) for unknown keys or invalid values.
58+
*/
59+
export function validateAndCoerce(key: string, value: string): string | number {
60+
const resolvedKey = resolveKey(key);
61+
62+
if (!(VALID_KEYS as readonly string[]).includes(resolvedKey)) {
63+
throw new BailianError(
64+
`Invalid config key "${key}". Valid keys: ${VALID_KEYS.join(", ")}`,
65+
ExitCode.USAGE,
66+
);
67+
}
68+
69+
if (resolvedKey === "output" && !["text", "json"].includes(value)) {
70+
throw new BailianError(
71+
`Invalid output format "${value}". Valid values: text, json`,
72+
ExitCode.USAGE,
73+
);
74+
}
75+
76+
if (resolvedKey === "timeout") {
77+
const num = Number(value);
78+
if (isNaN(num) || num <= 0) {
79+
throw new BailianError(
80+
`Invalid timeout "${value}". Must be a positive number.`,
81+
ExitCode.USAGE,
82+
);
83+
}
84+
return num;
85+
}
86+
87+
return value;
88+
}

0 commit comments

Comments
 (0)