llm-api-gateway-cli 1.0.4 → 1.0.6

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/lib/config.js CHANGED
@@ -11,6 +11,8 @@
11
11
  import { DEFAULT_BASE_URL, strArg } from './common.js';
12
12
  import { resolveSecretKey } from './secrets.js';
13
13
  import { resolveSettings, settingsFilePath } from './settings.js';
14
+ // CLI 侧语言:这四个 CLI 入口的报错与提示都从这里取词(默认中文)
15
+ import { pick } from './i18n.js';
14
16
 
15
17
  /**
16
18
  * 缺密钥时的统一提示(各 CLI 文案一致,避免用户按提示改了还是不通)。
@@ -24,6 +26,24 @@ export const KEY_HINT =
24
26
  + '② 或 gateway-agent config set key sk-xxx / 在任务页·聊天页「设置」里填(都存 credentials.json,仅本用户可读);'
25
27
  + '③ 或 --key sk-xxx、环境变量 SK / GATEWAY_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY,或在 .env 中写 GATEWAY_KEY=sk-xxx';
26
28
 
29
+ /**
30
+ * 英文版提示(只给 CLI 侧用)。
31
+ *
32
+ * 为什么不直接把 KEY_HINT 改成 `pick(...)`:它是**模块级常量**,import 时就求值了,
33
+ * 而语言是入口脚本 parseArgs 之后才 setLang 的 —— 那样永远只会拿到中文(见 lib/i18n.js 纪律 3)。
34
+ * 所以保留 KEY_HINT 这个常量的中文原样(既有测试与调用方都按值用它),
35
+ * 需要按语言取词的地方调 `keyHint()`。
36
+ */
37
+ export const KEY_HINT_EN =
38
+ 'Missing sk- key: (1) run gateway-agent setup (first-time setup: it asks once for the gateway URL and the key); '
39
+ + '(2) or gateway-agent config set key sk-xxx, or fill it in the task/chat page under "Settings" (both write credentials.json, readable by this user only); '
40
+ + '(3) or --key sk-xxx, env SK / GATEWAY_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY, or GATEWAY_KEY=sk-xxx in .env';
41
+
42
+ /** 按当前 CLI 语言取密钥提示(默认中文) */
43
+ export function keyHint() {
44
+ return pick(KEY_HINT, KEY_HINT_EN);
45
+ }
46
+
27
47
  /**
28
48
  * 数字参数:非法值(NaN / 布尔 / 空串)一律返回 undefined。
29
49
  * 之前直接 Number(v) 会把 --temperature abc 变成 NaN 塞给上游,报错信息很难看懂。
@@ -80,11 +100,11 @@ export function resolveConfig(args = {}, { defaultModel = '', defaultMaxTokens,
80
100
  };
81
101
  }
82
102
 
83
- /** 把异常压成一行可读中文(上游错误、HTTP 状态、普通异常都覆盖) */
103
+ /** 把异常压成一行可读文字(上游错误、HTTP 状态、普通异常都覆盖) */
84
104
  export function errorMessage(e) {
85
105
  const status = e?.status ?? e?.statusCode ?? '';
86
106
  const message = e?.error?.message || e?.message || String(e);
87
- return `[错误]${status ? ` HTTP ${status}` : ''}: ${message}`;
107
+ return pick(`[错误]${status ? ` HTTP ${status}` : ''}: ${message}`, `[error]${status ? ` HTTP ${status}` : ''}: ${message}`);
88
108
  }
89
109
 
90
110
  /**
package/lib/configcmd.js CHANGED
@@ -11,6 +11,9 @@
11
11
  * 密钥(`key`)走**另一条路**:它不进 config.json,而是写 `<数据根>/credentials.json`(仅属主可读),
12
12
  * 所以 `set key` 不是「被拒绝」,而是「写到另一个文件」。命令行的说法与 Web 设置面板完全一致 ——
13
13
  * 这也是「不用手写 .env 就能用」的落点。
14
+ *
15
+ * 输出语言(20260922):文案全部走 `pick(中文, 英文)`,默认中文一字不变;
16
+ * 语言由入口脚本 setLang 设好,这里只管取词。**pick 必须在函数里调用**(见 lib/i18n.js 纪律 3)。
14
17
  */
15
18
 
16
19
  import {
@@ -24,10 +27,19 @@ import {
24
27
  SETTINGS_SCHEMA,
25
28
  } from './settings.js';
26
29
  import { clearSecret, keyRow, secretFilePath, secretSourceText, writeSecret } from './secrets.js';
30
+ import { pick } from './i18n.js';
27
31
 
28
- const SOURCE_LABEL = { flag: '命令行', env: '环境变量', file: '配置文件', default: '内置默认' };
32
+ /** 来源标签:中英各留一份,取词在函数里做 —— 顶层 pick 会在 setLang 之前求值,永远拿到中文 */
33
+ const SOURCE_LABEL_ZH = { flag: '命令行', env: '环境变量', file: '配置文件', default: '内置默认' };
34
+ const SOURCE_LABEL_EN = { flag: 'command line', env: 'environment variable', file: 'config file', default: 'built-in default' };
29
35
  /** 密钥的来源只有三层(它不在 config.json 里),所以单独一套标签 */
30
- const SECRET_SOURCE_LABEL = { flag: '命令行 --key', env: '环境变量', file: '密钥文件', default: '未配置' };
36
+ const SECRET_SOURCE_LABEL_ZH = { flag: '命令行 --key', env: '环境变量', file: '密钥文件', default: '未配置' };
37
+ const SECRET_SOURCE_LABEL_EN = { flag: 'command line --key', env: 'environment variable', file: 'secret file', default: 'not configured' };
38
+
39
+ /** 取标签;认不出的来源原样回显(与旧行为一致)。
40
+ * 注意 pick() 只认字符串:整张表传进去会因为 en 不是字符串而**静默回落中文**,所以按 key 逐个取词。 */
41
+ const sourceLabel = (s) => pick(SOURCE_LABEL_ZH[s], SOURCE_LABEL_EN[s]) || s;
42
+ const secretSourceLabel = (s) => pick(SECRET_SOURCE_LABEL_ZH[s], SECRET_SOURCE_LABEL_EN[s]) || s;
31
43
 
32
44
  /** CLI 传进来的值是字符串;这里粗切一下,`set system 你是助手 请简短` 的剩余部分都算值 */
33
45
  export function splitConfigArg(arg) {
@@ -37,9 +49,9 @@ export function splitConfigArg(arg) {
37
49
  }
38
50
 
39
51
  const show = (v) => {
40
- if (v === null || v === undefined) return '(未设)';
41
- if (Array.isArray(v)) return v.length ? `${v.length} 项` : '(空)';
42
- if (typeof v === 'string') return v.length > 40 ? `${v.slice(0, 39)}…` : v || '(空串)';
52
+ if (v === null || v === undefined) return pick('(未设)', '(not set)');
53
+ if (Array.isArray(v)) return v.length ? pick(`${v.length} 项`, `${v.length} item(s)`) : pick('(空)', '(empty)');
54
+ if (typeof v === 'string') return v.length > 40 ? `${v.slice(0, 39)}…` : v || pick('(空串)', '(empty string)');
43
55
  return String(v);
44
56
  };
45
57
 
@@ -47,8 +59,18 @@ const show = (v) => {
47
59
  function overrideWarning(resolved, key) {
48
60
  const source = resolved.sources[key];
49
61
  const entry = SETTINGS_SCHEMA.find((e) => e.key === key);
50
- if (source === 'flag') return `注意:当前生效值来自命令行参数(${entry?.flag || key}),它优先于配置文件,你写的值现在不会生效。`;
51
- if (source === 'env') return `注意:当前生效值来自环境变量 ${entry?.env},它优先于配置文件,你写的值现在不会生效。`;
62
+ if (source === 'flag') {
63
+ return pick(
64
+ `注意:当前生效值来自命令行参数(${entry?.flag || key}),它优先于配置文件,你写的值现在不会生效。`,
65
+ `Note: the effective value comes from the command-line option (${entry?.flag || key}), which overrides the config file — what you just wrote will not take effect.`,
66
+ );
67
+ }
68
+ if (source === 'env') {
69
+ return pick(
70
+ `注意:当前生效值来自环境变量 ${entry?.env},它优先于配置文件,你写的值现在不会生效。`,
71
+ `Note: the effective value comes from environment variable ${entry?.env}, which overrides the config file — what you just wrote will not take effect.`,
72
+ );
73
+ }
52
74
  return '';
53
75
  }
54
76
 
@@ -69,8 +91,12 @@ export function configCommand(argv = [], { env = process.env, file = settingsFil
69
91
  return {
70
92
  code: 0,
71
93
  out:
72
- `${r.file}\n${r.exists ? '(已存在)' : '(还没有这份文件;set 一次就会创建)'}\n` +
73
- `${s.file}\n${s.hasKey ? `(已存在,${s.mask},来自${secretSourceText(s.source)})` : '(还没有;config set key sk-xxx 或页面设置里填)'}\n`,
94
+ `${r.file}\n${r.exists ? pick('(已存在)', '(exists)') : pick('(还没有这份文件;set 一次就会创建)', '(no such file yet; one `config set` creates it)')}\n` +
95
+ `${s.file}\n${
96
+ s.hasKey
97
+ ? pick(`(已存在,${s.mask},来自${secretSourceText(s.source)})`, `(exists, ${s.mask}, from ${secretSourceText(s.source)})`)
98
+ : pick('(还没有;config set key sk-xxx 或页面设置里填)', '(not set yet; run `config set key sk-xxx` or fill it in the settings page)')
99
+ }\n`,
74
100
  err: '',
75
101
  };
76
102
  }
@@ -80,10 +106,16 @@ export function configCommand(argv = [], { env = process.env, file = settingsFil
80
106
  // 密钥行排在最前:它是「能不能用」的前提,而它不在 config.json 里,单独标出来免得用户去文件里翻
81
107
  const secret = secretRow();
82
108
  const rows = [
83
- ['配置项', '当前值', '来源'],
84
- ['key', secret.hasKey ? `${secret.mask}(存 ${secret.file})` : '(未配置)', SECRET_SOURCE_LABEL[secret.source] || secret.source],
109
+ [pick('配置项', 'Setting'), pick('当前值', 'Current value'), pick('来源', 'Source')],
110
+ [
111
+ 'key',
112
+ secret.hasKey
113
+ ? pick(`${secret.mask}(存 ${secret.file})`, `${secret.mask} (in ${secret.file})`)
114
+ : pick('(未配置)', '(not configured)'),
115
+ secretSourceLabel(secret.source),
116
+ ],
85
117
  ...SETTINGS_SCHEMA.map((e) => {
86
- const row = [e.key, show(getByPath(r.values, e.key)), SOURCE_LABEL[r.sources[e.key]] || r.sources[e.key]];
118
+ const row = [e.key, show(getByPath(r.values, e.key)), sourceLabel(r.sources[e.key])];
87
119
  if (e.restart) row[0] = `${e.key} *`;
88
120
  return row;
89
121
  }),
@@ -91,32 +123,45 @@ export function configCommand(argv = [], { env = process.env, file = settingsFil
91
123
  const w = [0, 1, 2].map((i) => Math.max(...rows.map((row) => [...row[i]].length)));
92
124
  const body = rows.map((row, i) => row.map((c, ci) => pad(c, w[ci])).join(' ').trimEnd()).join('\n');
93
125
  const warns = [...r.warnings, ...[secret.warning].filter(Boolean)];
94
- const warn = warns.length ? `\n\n[警告]\n${warns.map((x) => ` ${x}`).join('\n')}` : '';
126
+ const warn = warns.length ? `${pick('\n\n[警告]\n', '\n\n[Warnings]\n')}${warns.map((x) => ` ${x}`).join('\n')}` : '';
95
127
  return {
96
128
  code: 0,
97
129
  out:
98
130
  `${body}\n\n` +
99
- `来源:命令行 > 环境变量 > 配置文件 > 内置默认。带 * 的改了要重启服务才生效。\n` +
100
- `配置文件:${r.file}${r.exists ? '' : '(还没有,用 config set 创建)'}\n` +
101
- `密钥不入配置文件:key 存在 ${secret.file}(仅本用户可读),环境变量 / --key 给的密钥优先于它。\n` +
102
- `改某项:gateway-agent config set <键> <值>;改回来:config unset <键>${warn}\n`,
131
+ pick(
132
+ `来源:命令行 > 环境变量 > 配置文件 > 内置默认。带 * 的改了要重启服务才生效。\n` +
133
+ `配置文件:${r.file}${r.exists ? '' : '(还没有,用 config set 创建)'}\n` +
134
+ `密钥不入配置文件:key 存在 ${secret.file}(仅本用户可读),环境变量 / --key 给的密钥优先于它。\n` +
135
+ `改某项:gateway-agent config set <键> <值>;改回来:config unset <键>`,
136
+ `Source: command line > environment variable > config file > built-in default. Rows marked * only take effect after a service restart.\n` +
137
+ `Config file: ${r.file}${r.exists ? '' : ' (none yet; create it with config set)'}\n` +
138
+ `The secret is not stored in the config file: key lives in ${secret.file} (readable by this user only), and a key from the environment / --key takes precedence over it.\n` +
139
+ `Change a setting: gateway-agent config set <key> <value>; revert: config unset <key>`,
140
+ ) +
141
+ `${warn}\n`,
103
142
  err: '',
104
143
  };
105
144
  }
106
145
 
107
146
  if (action === 'get') {
108
- if (!key) return { code: 1, out: '', err: '用法:gateway-agent config get <键>\n' };
147
+ if (!key) return { code: 1, out: '', err: pick('用法:gateway-agent config get <键>\n', 'Usage: gateway-agent config get <key>\n') };
109
148
  if (key === 'key') {
110
149
  const s = secretRow();
111
150
  const lines = [
112
151
  // 明文永不回显:命令行输出经常被贴进 issue / 日志
113
- `key = ${s.hasKey ? s.mask : '(未配置)'}`,
114
- ` 来源 ${SECRET_SOURCE_LABEL[s.source] || s.source}`,
115
- ` 存放 ${s.file}${s.fileMode ? `(权限 ${s.fileMode.toString(8)})` : ''}`,
116
- ' 说明 密钥不写进 config.json;这里只回掩码,明文请回网关后台重新生成',
117
- ' 环境变量 SK / GATEWAY_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY',
118
- ' 命令行 --key',
119
- s.warning ? ` 警告 ${s.warning}` : '',
152
+ `key = ${s.hasKey ? s.mask : pick('(未配置)', '(not configured)')}`,
153
+ pick(` 来源 ${secretSourceLabel(s.source)}`, ` Source ${secretSourceLabel(s.source)}`),
154
+ pick(
155
+ ` 存放 ${s.file}${s.fileMode ? `(权限 ${s.fileMode.toString(8)})` : ''}`,
156
+ ` Location ${s.file}${s.fileMode ? ` (mode ${s.fileMode.toString(8)})` : ''}`,
157
+ ),
158
+ pick(
159
+ ' 说明 密钥不写进 config.json;这里只回掩码,明文请回网关后台重新生成',
160
+ ' Note the key is never written to config.json; only a mask is shown here — regenerate the plaintext in the gateway console',
161
+ ),
162
+ pick(' 环境变量 SK / GATEWAY_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY', ' Env SK / GATEWAY_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY'),
163
+ pick(' 命令行 --key', ' Flag --key'),
164
+ s.warning ? pick(` 警告 ${s.warning}`, ` Warning ${s.warning}`) : '',
120
165
  ].filter(Boolean);
121
166
  return { code: 0, out: `${lines.join('\n')}\n`, err: '' };
122
167
  }
@@ -125,18 +170,27 @@ export function configCommand(argv = [], { env = process.env, file = settingsFil
125
170
  const r = resolved();
126
171
  const lines = [
127
172
  `${key} = ${JSON.stringify(getByPath(r.values, key))}`,
128
- ` 来源 ${SOURCE_LABEL[r.sources[key]] || r.sources[key]}`,
129
- ` 内置默认 ${JSON.stringify(defaultValueOf(entry))}`,
130
- ` 说明 ${entry.desc}`,
131
- entry.env ? ` 环境变量 ${entry.env}` : '',
132
- entry.flag ? ` 命令行 ${entry.flag}` : '',
133
- entry.restart ? ' 注意 改这项需要重启服务才生效' : '',
173
+ pick(` 来源 ${sourceLabel(r.sources[key])}`, ` Source ${sourceLabel(r.sources[key])}`),
174
+ pick(` 内置默认 ${JSON.stringify(defaultValueOf(entry))}`, ` Default ${JSON.stringify(defaultValueOf(entry))}`),
175
+ pick(` 说明 ${entry.desc}`, ` Note ${entry.desc}`),
176
+ entry.env ? pick(` 环境变量 ${entry.env}`, ` Env ${entry.env}`) : '',
177
+ entry.flag ? pick(` 命令行 ${entry.flag}`, ` Flag ${entry.flag}`) : '',
178
+ entry.restart ? pick(' 注意 改这项需要重启服务才生效', ' Note changing this takes effect only after a service restart') : '',
134
179
  ].filter(Boolean);
135
180
  return { code: 0, out: `${lines.join('\n')}\n`, err: '' };
136
181
  }
137
182
 
138
183
  if (action === 'set') {
139
- if (!key || rest.length === 0) return { code: 1, out: '', err: '用法:gateway-agent config set <键> <值>(字符串带空格时整段都要写上)\n' };
184
+ if (!key || rest.length === 0) {
185
+ return {
186
+ code: 1,
187
+ out: '',
188
+ err: pick(
189
+ '用法:gateway-agent config set <键> <值>(字符串带空格时整段都要写上)\n',
190
+ 'Usage: gateway-agent config set <key> <value> (pass the whole string when the value contains spaces)\n',
191
+ ),
192
+ };
193
+ }
140
194
  const value = rest.join(' ');
141
195
  // 密钥:写 credentials.json,不碰 config.json(红线没变,只是换了落点)
142
196
  if (key === 'key') {
@@ -144,12 +198,17 @@ export function configCommand(argv = [], { env = process.env, file = settingsFil
144
198
  const w = writeSecret(value, { file: secretFile });
145
199
  const s = secretRow();
146
200
  const override = s.source === 'env' || s.source === 'flag'
147
- ? '\n注意:当前进程的密钥来自环境变量 / --key,它优先于这个文件,你写的值现在不会生效。'
201
+ ? pick(
202
+ '\n注意:当前进程的密钥来自环境变量 / --key,它优先于这个文件,你写的值现在不会生效。',
203
+ '\nNote: this process takes the key from the environment / --key, which overrides this file — what you just wrote will not take effect.',
204
+ )
148
205
  : '';
149
- const perm = w.mode ? `(权限 ${w.mode.toString(8)})` : '(Windows:靠用户目录 ACL)';
150
206
  return {
151
207
  code: 0,
152
- out: `已写入 ${w.file}${perm}:key = ${s.mask}${w.warning ? `\n${w.warning}` : ''}${override}\n`,
208
+ out: pick(
209
+ `已写入 ${w.file}${w.mode ? `(权限 ${w.mode.toString(8)})` : '(Windows:靠用户目录 ACL)'}:key = ${s.mask}${w.warning ? `\n${w.warning}` : ''}${override}\n`,
210
+ `Written to ${w.file}${w.mode ? ` (mode ${w.mode.toString(8)})` : ' (Windows: relies on the user-directory ACL)'}: key = ${s.mask}${w.warning ? `\n${w.warning}` : ''}${override}\n`,
211
+ ),
153
212
  err: '',
154
213
  };
155
214
  } catch (e) {
@@ -161,11 +220,11 @@ export function configCommand(argv = [], { env = process.env, file = settingsFil
161
220
  const r = resolved();
162
221
  const warn = overrideWarning(r, key);
163
222
  const entry = SETTINGS_SCHEMA.find((e) => e.key === key);
164
- const restart = entry?.restart ? '\n(这项要重启服务才生效)' : '';
223
+ const restart = entry?.restart ? pick('\n(这项要重启服务才生效)', '\n(this only takes effect after a service restart)') : '';
165
224
  const shown = show(getByPath(r.values, key));
166
225
  return {
167
226
  code: 0,
168
- out: `已写入 ${res.file}:${key} = ${shown}${restart}${warn ? `\n${warn}` : ''}\n`,
227
+ out: `${pick(`已写入 ${res.file}:${key} = ${shown}`, `Wrote ${key} = ${shown} to ${res.file}`)}${restart}${warn ? `\n${warn}` : ''}\n`,
169
228
  err: '',
170
229
  };
171
230
  } catch (e) {
@@ -174,14 +233,19 @@ export function configCommand(argv = [], { env = process.env, file = settingsFil
174
233
  }
175
234
 
176
235
  if (action === 'unset' || action === 'rm') {
177
- if (!key) return { code: 1, out: '', err: '用法:gateway-agent config unset <键>\n' };
236
+ if (!key) return { code: 1, out: '', err: pick('用法:gateway-agent config unset <键>\n', 'Usage: gateway-agent config unset <key>\n') };
178
237
  if (key === 'key') {
179
238
  const cleared = clearSecret({ file: secretFile });
180
239
  const s = secretRow();
181
- const still = s.hasKey ? `当前生效的仍是${SECRET_SOURCE_LABEL[s.source] || s.source}的密钥:${s.mask}` : '现在没有可用的密钥了。';
240
+ const still = s.hasKey
241
+ ? pick(`当前生效的仍是${secretSourceLabel(s.source)}的密钥:${s.mask}`, `the effective key still comes from ${secretSourceLabel(s.source)}: ${s.mask}`)
242
+ : pick('现在没有可用的密钥了。', 'there is no usable key now.');
182
243
  return {
183
244
  code: 0,
184
- out: `${cleared.removed ? `已删掉 ${cleared.file},` : `本来就没有 ${cleared.file},`}${still}\n`,
245
+ out: pick(
246
+ `${cleared.removed ? `已删掉 ${cleared.file},` : `本来就没有 ${cleared.file},`}${still}\n`,
247
+ `${cleared.removed ? `Removed ${cleared.file}; ` : `there was no ${cleared.file}; `}${still}\n`,
248
+ ),
185
249
  err: '',
186
250
  };
187
251
  }
@@ -191,15 +255,31 @@ export function configCommand(argv = [], { env = process.env, file = settingsFil
191
255
  const now = show(getByPath(r.values, key));
192
256
  const warn = overrideWarning(r, key);
193
257
  if (!res.removed.length) {
194
- return { code: 0, out: `${key} 本来就没在配置文件里,现在用的仍是${SOURCE_LABEL[r.sources[key]] || r.sources[key]}的值:${now}${warn ? `\n${warn}` : ''}\n`, err: '' };
258
+ return {
259
+ code: 0,
260
+ out:
261
+ pick(
262
+ `${key} 本来就没在配置文件里,现在用的仍是${sourceLabel(r.sources[key])}的值:${now}`,
263
+ `${key} was not in the config file; the value in effect is still the one from ${sourceLabel(r.sources[key])}: ${now}`,
264
+ ) + `${warn ? `\n${warn}` : ''}\n`,
265
+ err: '',
266
+ };
195
267
  }
196
- return { code: 0, out: `已从 ${res.file} 删掉 ${key},现在生效的是:${now}${warn ? `\n${warn}` : ''}\n`, err: '' };
268
+ return {
269
+ code: 0,
270
+ out:
271
+ pick(
272
+ `已从 ${res.file} 删掉 ${key},现在生效的是:${now}`,
273
+ `Removed ${key} from ${res.file}; the value now in effect is: ${now}`,
274
+ ) + `${warn ? `\n${warn}` : ''}\n`,
275
+ err: '',
276
+ };
197
277
  } catch (e) {
198
278
  return { code: 1, out: '', err: `${e.message}\n` };
199
279
  }
200
280
  }
201
281
 
202
- return { code: 1, out: '', err: `未知子命令 ${action}\n\n${configHelp()}` };
282
+ return { code: 1, out: '', err: `${pick(`未知子命令 ${action}`, `Unknown subcommand ${action}`)}\n\n${configHelp()}` };
203
283
  }
204
284
 
205
285
  /** 中文对齐:按显示宽度补齐(中文算两格),否则表格会歪 */
@@ -209,8 +289,8 @@ function pad(text, width) {
209
289
  return s + ' '.repeat(Math.max(0, width - w));
210
290
  }
211
291
 
212
- export function configHelp() {
213
- return `用法:gateway-agent config <子命令>
292
+ /** config help 正文:两块模板,函数里 pick(见 lib/i18n.js 纪律 3) */
293
+ const CONFIG_HELP_ZH = `用法:gateway-agent config <子命令>
214
294
 
215
295
  list 列出所有配置项、当前值与来源(默认)
216
296
  get <键> 查看某一项(含默认值、说明、对应的环境变量与命令行选项)
@@ -227,4 +307,25 @@ Web 界面「设置」里的密钥输入框写的是同一个文件 —— 命
227
307
 
228
308
  REPL 里同一套:/config、/config set <键> <值>、/config get <键>、/config unset <键>
229
309
  `;
310
+
311
+ const CONFIG_HELP_EN = `Usage: gateway-agent config <subcommand>
312
+
313
+ list list every setting with its current value and source (default)
314
+ get <key> show one setting (default value, description, matching env var and CLI option)
315
+ set <key> <value> write to the config file (atomic write; bad input fails loudly and lists valid keys)
316
+ unset <key> remove a setting and fall back to the built-in default
317
+ path print the paths of the config file and the secret file
318
+
319
+ The config file path can be overridden with the LLM_GATEWAY_CONFIG environment variable.
320
+ Priority: command line > environment variable > config file > built-in default — the "Source" column of list tells you which layer the effective value comes from.
321
+
322
+ The secret (key) takes another path: config set key sk-xxx writes <data root>/credentials.json (readable by this user only)
323
+ and is **never written to config.json**; a key from the command line / environment / .env takes precedence over it. get key returns only a mask, never the plaintext.
324
+ The key input in the Web "Settings" panel writes the same file — the CLI and the UI never store two copies.
325
+
326
+ The REPL uses the same set: /config, /config set <key> <value>, /config get <key>, /config unset <key>
327
+ `;
328
+
329
+ export function configHelp() {
330
+ return pick(CONFIG_HELP_ZH, CONFIG_HELP_EN);
230
331
  }