llm-api-gateway-cli 1.0.5 → 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/README.en.md +116 -2
- package/README.md +101 -2
- package/cli-agent.js +186 -49
- package/cli-anthropic.js +57 -13
- package/cli-claude-code.js +59 -12
- package/cli-openai.js +48 -11
- package/lib/commands.js +397 -70
- package/lib/config.js +22 -2
- package/lib/configcmd.js +146 -45
- package/lib/hub.js +435 -43
- package/lib/i18n.js +80 -0
- package/lib/launcher.js +78 -24
- package/lib/mcp.js +16 -6
- package/lib/mcpadmin.js +331 -0
- package/lib/setup.js +101 -36
- package/package.json +1 -1
- package/public/manual.html +107 -3
- package/public/mcp.css +155 -0
- package/public/mcp.html +135 -0
- package/public/mcp.js +327 -0
- package/public/sessions.css +198 -0
- package/public/sessions.html +89 -0
- package/public/sessions.js +281 -0
- package/public/shell.html +8 -0
- package/public/shell.js +20 -4
- package/public/task.css +5 -0
- package/public/task.html +14 -1
package/lib/setup.js
CHANGED
|
@@ -13,11 +13,15 @@
|
|
|
13
13
|
*
|
|
14
14
|
* 「默认值」的处理是这套东西的关键:默认值**只在提问时以 `[默认]` 形式给出**,
|
|
15
15
|
* 用户回车才采用、也可以直接覆盖 —— 不做「悄悄用默认值跑起来」这件事。
|
|
16
|
+
*
|
|
17
|
+
* 输出语言(20260922):文案全部走 `pick(中文, 英文)`,默认中文一字不变;
|
|
18
|
+
* 语言由入口脚本 setLang 设好,这里只管取词。**pick 必须在函数里调用**(见 lib/i18n.js 纪律 3)。
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
21
|
import { strArg } from './common.js';
|
|
19
22
|
import { settingsFilePath, writeSettings, resolveSettings } from './settings.js';
|
|
20
23
|
import { secretFilePath, writeSecret, resolveSecretKey, KEY_SHAPE, SECRET_FILE_NAME } from './secrets.js';
|
|
24
|
+
import { pick } from './i18n.js';
|
|
21
25
|
|
|
22
26
|
/** 地址校验:与 settings 白名单同一口径(lib/settings.js:65) */
|
|
23
27
|
export const HTTP_URL_RE = /^https?:\/\/[^\s]+$/i;
|
|
@@ -25,17 +29,20 @@ export const HTTP_URL_RE = /^https?:\/\/[^\s]+$/i;
|
|
|
25
29
|
/** 校验网关地址;通过返回 '',否则返回一句人话 */
|
|
26
30
|
export function validateBaseUrl(v) {
|
|
27
31
|
const s = strArg(v);
|
|
28
|
-
if (!s) return '网关地址不能为空';
|
|
29
|
-
if (!HTTP_URL_RE.test(s)) return '网关地址需要以 http:// 或 https:// 开头';
|
|
32
|
+
if (!s) return pick('网关地址不能为空', 'Gateway URL must not be empty');
|
|
33
|
+
if (!HTTP_URL_RE.test(s)) return pick('网关地址需要以 http:// 或 https:// 开头', 'Gateway URL must start with http:// or https://');
|
|
30
34
|
return '';
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
/** 校验密钥:与 lib/secrets.js 的 KEY_SHAPE 同一口径 */
|
|
34
38
|
export function validateKey(v) {
|
|
35
39
|
const s = strArg(v);
|
|
36
|
-
if (!s) return '网关密钥不能为空';
|
|
40
|
+
if (!s) return pick('网关密钥不能为空', 'Gateway key must not be empty');
|
|
37
41
|
if (!KEY_SHAPE.test(s)) {
|
|
38
|
-
return
|
|
42
|
+
return pick(
|
|
43
|
+
`密钥形状不对:需要以 sk- 开头且长度足够(收到 ${s.length} 个字符,前缀 ${s.slice(0, 3)})`,
|
|
44
|
+
`Bad key shape: it must start with sk- and be long enough (got ${s.length} characters, prefix ${s.slice(0, 3)})`,
|
|
45
|
+
);
|
|
39
46
|
}
|
|
40
47
|
return '';
|
|
41
48
|
}
|
|
@@ -60,18 +67,45 @@ export function maskKeyish(key) {
|
|
|
60
67
|
return `${s.slice(0, 6)}****${s.slice(-4)}`;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
/**
|
|
70
|
+
/** 向导正文(纯文本,便于测试逐字断言);只有默认地址那一行要插值,所以整段在函数里取词 */
|
|
64
71
|
export function setupIntro({ baseUrlDefault }) {
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
return pick(
|
|
73
|
+
[
|
|
74
|
+
'LLM API Gateway · 首次配置',
|
|
75
|
+
'',
|
|
76
|
+
'这一步问两件事,都只写在本机(不会上传、不会进 git):',
|
|
77
|
+
` 1) 网关地址:你部署的 LLM API Gateway 的地址(默认 ${baseUrlDefault})`,
|
|
78
|
+
' 2) 网关密钥:网关后台发放的 sk- 密钥',
|
|
79
|
+
'',
|
|
80
|
+
].join('\n'),
|
|
81
|
+
[
|
|
82
|
+
'LLM API Gateway · First-time setup',
|
|
83
|
+
'',
|
|
84
|
+
'This asks two things, and both stay on this machine (nothing is uploaded, nothing goes into git):',
|
|
85
|
+
` 1) Gateway URL: the address of your LLM API Gateway deployment (default ${baseUrlDefault})`,
|
|
86
|
+
' 2) Gateway key: the sk- key issued by the gateway console',
|
|
87
|
+
'',
|
|
88
|
+
].join('\n'),
|
|
89
|
+
);
|
|
73
90
|
}
|
|
74
91
|
|
|
92
|
+
/** 向导收尾提示:静态两块模板,函数里 pick(见 lib/i18n.js 纪律 3) */
|
|
93
|
+
const SETUP_NEXT_ZH =
|
|
94
|
+
'\n接下来:\n' +
|
|
95
|
+
' gateway-agent 命令行对话(现在直接可用)\n' +
|
|
96
|
+
' gateway-task 本机 Web UI 与任务页 → http://127.0.0.1:3100\n' +
|
|
97
|
+
' gateway-agent config list 看所有配置项与它们的「来源」\n' +
|
|
98
|
+
'改网关地址 / 模型:gateway-agent config set baseUrl|model …,或在任务页「设置 → 其他配置」里改。\n' +
|
|
99
|
+
'密钥明文只在网关后台能重新生成;这里只回掩码。\n';
|
|
100
|
+
|
|
101
|
+
const SETUP_NEXT_EN =
|
|
102
|
+
'\nNext:\n' +
|
|
103
|
+
' gateway-agent command-line chat (ready to use now)\n' +
|
|
104
|
+
' gateway-task local Web UI and task page → http://127.0.0.1:3100\n' +
|
|
105
|
+
' gateway-agent config list inspect every setting and where it comes from\n' +
|
|
106
|
+
'Change the gateway URL / model: gateway-agent config set baseUrl|model …, or use "Settings → Other" on the task page.\n' +
|
|
107
|
+
'The plaintext key can only be regenerated in the gateway console; only a mask is shown here.\n';
|
|
108
|
+
|
|
75
109
|
/**
|
|
76
110
|
* 跑一次设置向导。
|
|
77
111
|
*
|
|
@@ -104,7 +138,10 @@ export async function runSetup({
|
|
|
104
138
|
return {
|
|
105
139
|
ok: false,
|
|
106
140
|
code: 1,
|
|
107
|
-
error:
|
|
141
|
+
error: pick(
|
|
142
|
+
'当前不是交互终端:请用 `gateway-agent setup --base-url <地址> --key sk-xxx` 一次配完。',
|
|
143
|
+
'Not an interactive terminal: run `gateway-agent setup --base-url <url> --key sk-xxx` to configure everything in one go.',
|
|
144
|
+
),
|
|
108
145
|
};
|
|
109
146
|
}
|
|
110
147
|
|
|
@@ -114,7 +151,7 @@ export async function runSetup({
|
|
|
114
151
|
let baseUrl = givenBase;
|
|
115
152
|
if (!baseUrl) {
|
|
116
153
|
for (let i = 0; i < attempts; i++) {
|
|
117
|
-
const answer = await ask('网关地址', { default: currentBase });
|
|
154
|
+
const answer = await ask(pick('网关地址', 'Gateway URL'), { default: currentBase });
|
|
118
155
|
const value = strArg(answer) || currentBase; // 回车 = 采用方括号里的默认值
|
|
119
156
|
const err = validateBaseUrl(value);
|
|
120
157
|
if (!err) {
|
|
@@ -123,14 +160,20 @@ export async function runSetup({
|
|
|
123
160
|
}
|
|
124
161
|
log(` ✗ ${err}\n`);
|
|
125
162
|
}
|
|
126
|
-
if (!baseUrl)
|
|
163
|
+
if (!baseUrl) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
code: 1,
|
|
167
|
+
error: pick('网关地址没填对,已取消(没有写入任何文件)。', 'Gateway URL was not valid; cancelled (no file was written).'),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
127
170
|
}
|
|
128
171
|
|
|
129
172
|
/* ---------- 2) 网关密钥 ---------- */
|
|
130
173
|
let key = givenKey;
|
|
131
174
|
if (!key) {
|
|
132
175
|
for (let i = 0; i < attempts; i++) {
|
|
133
|
-
const answer = await ask('网关密钥', { mask: true });
|
|
176
|
+
const answer = await ask(pick('网关密钥', 'Gateway key'), { mask: true });
|
|
134
177
|
const err = validateKey(answer);
|
|
135
178
|
if (!err) {
|
|
136
179
|
key = strArg(answer);
|
|
@@ -138,7 +181,13 @@ export async function runSetup({
|
|
|
138
181
|
}
|
|
139
182
|
log(` ✗ ${err}\n`);
|
|
140
183
|
}
|
|
141
|
-
if (!key)
|
|
184
|
+
if (!key) {
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
code: 1,
|
|
188
|
+
error: pick('网关密钥没填对,已取消(没有写入任何文件)。', 'Gateway key was not valid; cancelled (no file was written).'),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
142
191
|
}
|
|
143
192
|
|
|
144
193
|
/* ---------- 3) 落盘:复用既有实现,原子写与权限都在里面 ---------- */
|
|
@@ -148,30 +197,28 @@ export async function runSetup({
|
|
|
148
197
|
const res = writeSettings({ baseUrl: baseUrl.replace(/\/+$/, '') }, { file });
|
|
149
198
|
wrote = writeSecret(key, { file: secretFile });
|
|
150
199
|
log(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
200
|
+
pick(
|
|
201
|
+
'\n已写入:\n' +
|
|
202
|
+
` 网关地址 ${baseUrl.replace(/\/+$/, '')}\n → ${res.file}\n` +
|
|
203
|
+
` 网关密钥 ${maskKeyish(key)}\n → ${wrote.file}${wrote.mode ? `(权限 ${wrote.mode.toString(8)})` : '(Windows:靠用户目录 ACL)'}\n`,
|
|
204
|
+
'\nWritten:\n' +
|
|
205
|
+
` Gateway URL ${baseUrl.replace(/\/+$/, '')}\n → ${res.file}\n` +
|
|
206
|
+
` Gateway key ${maskKeyish(key)}\n → ${wrote.file}${wrote.mode ? ` (mode ${wrote.mode.toString(8)})` : ' (Windows: relies on the user-directory ACL)'}\n`,
|
|
207
|
+
) + (wrote.warning ? `${wrote.warning}\n` : ''),
|
|
155
208
|
);
|
|
156
209
|
} catch (e) {
|
|
157
|
-
return { ok: false, code: 1, error:
|
|
210
|
+
return { ok: false, code: 1, error: `${pick('写入配置失败:', 'Failed to write the configuration: ')}${e.message}` };
|
|
158
211
|
}
|
|
159
212
|
|
|
160
|
-
log(
|
|
161
|
-
'\n接下来:\n' +
|
|
162
|
-
' gateway-agent 命令行对话(现在直接可用)\n' +
|
|
163
|
-
' gateway-task 本机 Web UI 与任务页 → http://127.0.0.1:3100\n' +
|
|
164
|
-
' gateway-agent config list 看所有配置项与它们的「来源」\n' +
|
|
165
|
-
'改网关地址 / 模型:gateway-agent config set baseUrl|model …,或在任务页「设置 → 其他配置」里改。\n' +
|
|
166
|
-
'密钥明文只在网关后台能重新生成;这里只回掩码。\n',
|
|
167
|
-
);
|
|
213
|
+
log(pick(SETUP_NEXT_ZH, SETUP_NEXT_EN));
|
|
168
214
|
|
|
169
215
|
return { ok: true, code: 0, baseUrl: baseUrl.replace(/\/+$/, ''), key, file: resolveSettings({ file, env }).file, secretFile: wrote.file, secretName: SECRET_FILE_NAME };
|
|
170
216
|
}
|
|
171
217
|
|
|
172
|
-
/** `setup --help`
|
|
218
|
+
/** `setup --help` 的正文;两块模板里有运行时路径,所以在函数里取词(关键仍是 pick 在函数内求值) */
|
|
173
219
|
export function setupHelp() {
|
|
174
|
-
return
|
|
220
|
+
return pick(
|
|
221
|
+
`用法:gateway-agent setup [--base-url <地址>] [--key sk-xxx]
|
|
175
222
|
|
|
176
223
|
首次配置引导:问一次「网关地址」和「网关密钥」,写进本机的两个文件 ——
|
|
177
224
|
网关地址 → ${settingsFilePath()}
|
|
@@ -186,7 +233,24 @@ export function setupHelp() {
|
|
|
186
233
|
网页「设置」面板(任务页「其他配置」里地址与密钥都有)
|
|
187
234
|
gateway-agent config set baseUrl <地址> / config set key sk-xxx
|
|
188
235
|
环境变量 GATEWAY_BASE_URL / SK / GATEWAY_KEY,或当前目录的 .env
|
|
189
|
-
|
|
236
|
+
`,
|
|
237
|
+
`Usage: gateway-agent setup [--base-url <url>] [--key sk-xxx]
|
|
238
|
+
|
|
239
|
+
First-time setup: asks once for the gateway URL and the gateway key, then writes them to two local files —
|
|
240
|
+
Gateway URL → ${settingsFilePath()}
|
|
241
|
+
Gateway key → ${secretFilePath()} (not stored in config.json)
|
|
242
|
+
|
|
243
|
+
--base-url <url> give it directly, no question asked (must start with http:// or https://)
|
|
244
|
+
--key sk-xxx give it directly, no question asked
|
|
245
|
+
both given → nothing is asked, good for scripts / CI
|
|
246
|
+
neither given → interactive Q&A (defaults are shown in [brackets]; Enter accepts them)
|
|
247
|
+
|
|
248
|
+
Equivalent ways (any one of them; they read and write the same configuration):
|
|
249
|
+
the Web "Settings" panel (the task page has both URL and key under "Other settings")
|
|
250
|
+
gateway-agent config set baseUrl <url> / config set key sk-xxx
|
|
251
|
+
the GATEWAY_BASE_URL / SK / GATEWAY_KEY environment variables, or a .env in the current directory
|
|
252
|
+
`,
|
|
253
|
+
);
|
|
190
254
|
}
|
|
191
255
|
|
|
192
256
|
/**
|
|
@@ -254,7 +318,8 @@ export function createTerminalAsk({ input = process.stdin, output = process.stdo
|
|
|
254
318
|
|
|
255
319
|
return (label, { default: def = '', mask = false } = {}) => {
|
|
256
320
|
const suffix = def ? ` [${def}]` : '';
|
|
257
|
-
const hint = mask ? '(sk- 开头,输入不回显)' : '';
|
|
258
|
-
|
|
321
|
+
const hint = mask ? pick('(sk- 开头,输入不回显)', ' (starts with sk-, input is hidden)') : '';
|
|
322
|
+
// 行尾标点也要跟着语言走:中文是全角「:」,英文是半角「: 」
|
|
323
|
+
return readLine(pick(`${label}${suffix}${hint}:`, `${label}${suffix}${hint}: `), mask);
|
|
259
324
|
};
|
|
260
325
|
}
|
package/package.json
CHANGED
package/public/manual.html
CHANGED
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
<a href="#s-modes">三种模式</a>
|
|
71
71
|
<a href="#s-workdir">工作目录与边界</a>
|
|
72
72
|
<a href="#s-howto">指令怎么用</a>
|
|
73
|
+
<a href="#s-lang">输出语言</a>
|
|
73
74
|
<a href="#s-commands">指令一览</a>
|
|
74
75
|
<a href="#s-faq">常见问题</a>
|
|
75
76
|
</nav>
|
|
@@ -360,6 +361,7 @@ gateway-agent config set mode auto # 新任务的默认审批模式</co
|
|
|
360
361
|
<li>模型的一切文件操作都被限制在你选定<b>工作目录</b>之内;换目录用顶栏的目录芯片,或敲 <code>/cwd <路径></code>。</li>
|
|
361
362
|
<li><code>/files [子路径]</code> 列目录、<code>/init</code> 在工作目录里生成 <code>AGENTS.md</code> 项目约定骨架、<code>/instructions</code> 看当前注入给模型的项目记忆(<code>AGENTS.md</code> / <code>CLAUDE.md</code>,超上限截断)。</li>
|
|
362
363
|
<li>任务记录(含工具读过的文件正文)由服务端直写磁盘,换浏览器、清缓存都不会丢;存储位置在设置面板里能看到,也可以用启动参数 <code>--store <目录></code> 换地方。</li>
|
|
364
|
+
<li><b>命令行里的对话不在这一页</b>:侧边栏列的是<b>任务</b>(<code><数据根>/tasks/</code>);命令行 <code>gateway-agent -i</code> 的会话是<b>另一份存储</b>(<code><数据根>/sessions/<uuid>.json</code>),网页侧不读它 —— 所以终端里聊的内容不会出现在任务列表里,反过来网页任务也不在 <code>--resume</code> 的候选里。想在网页里<b>看</b>它:<a href="/sessions">/sessions</a>(<b>只读</b>回放;首页 Tab 栏的「<b>命令行</b>」就是它,同页切换不开新标签;单独打开任务页时侧栏也有入口);要接着聊就在终端:<code>gateway-agent -i --continue</code> / <code>--resume <id></code>,或 REPL 里 <code>/resume</code>(详见下一节与 FAQ)。</li>
|
|
363
365
|
<li>侧边栏<b>按工作目录分组</b>:同一个目录下可以挂多条任务,分组行右侧的 <code>+</code> 直接在该目录下再开一条。鼠标移到某条任务上,行尾的 <code>+</code> 可以<b>派生一条子任务</b>(继承父任务的工作目录,第二行以 <code>↳</code> 缩进显示);删父任务不会连坐,子任务会升一层。</li>
|
|
364
366
|
<li>存储目录若正好落在工作目录之内,页面会提醒你:模型能读到任务历史。要避免就把 <code>--store</code> 指到工作目录之外。</li>
|
|
365
367
|
<li><code>bash</code> 工具默认不开(模型跑不了测试);要用请在启动服务时加 <code>--allow-bash</code>。设置面板的「可用工具」一行会写清当前有哪些。</li>
|
|
@@ -376,11 +378,92 @@ gateway-agent config set mode auto # 新任务的默认审批模式</co
|
|
|
376
378
|
<li>指令带「能力」标记:<code>local</code> 本页即可;<code>api</code> 要用服务端接口;<code>fs</code> 需要服务端放行本机文件访问(服务没绑定在本机地址时会不可用,页面会说明原因)。</li>
|
|
377
379
|
<li><code>/help</code> 列任务页全部指令,<code>/help <指令名></code> 看单条用法(别名、能力、示例)。</li>
|
|
378
380
|
<li><code>/model</code> 改的就是设置面板里的「模型」,这一项<b>只能从网关 <code>/v1/models</code> 拉到的列表里选</b>(设置面板里是个下拉框,打不进字)。不带参数敲 <code>/model</code> 会列出可选模型;想改的模型不在列表里,先点设置面板的「刷新模型」,或检查网关这一侧绑定的上游。</li>
|
|
381
|
+
<li><b>命令行里的 <code>/mcp</code> 还能管绑定与自助登记</b>(终端 REPL 里敲):<code>/mcp</code> 看「已绑 / 可自助加 / 我自建」三段;<code>/mcp bind +3 -5</code> 或 <code>/mcp bind =2,7</code> 改绑;<code>/mcp mode off|inject|loop</code> 只改模式、不动绑定;<code>/mcp add <名字> <URL> [--header "k: v"]</code> 登记自己的 http/https 公网 MCP 服务器(网关探测一次并自动绑定);<code>/mcp rm <名字|id></code> 删掉自己登记的(先确认)。
|
|
382
|
+
能不能自助登记<b>由网关说了算</b>:没开放时只打印开启办法(网关 <code>.env</code> 设 <code>MCP_ALLOW_SELF_SERVERS=true</code> 并重启),不发那次注定 403 的请求;地址里的 <code>?key=</code> 这类凭据与 <code>--header</code> 的值<b>只显示掩码</b>(明文只发给网关)。
|
|
383
|
+
<b>不想敲终端就用网页</b>:首页 Tab 栏的「<b>MCP</b>」(同页切换,不开新标签),或直接开 <a href="/mcp">/mcp</a>;单独打开任务页时侧栏也有入口 —— 同一件事的图形入口,范围同样只是<b>你这把密钥</b>,而且<b>密钥不进浏览器</b>(页面只打本机接口,由服务代发)。全局服务器与统计仍归网关的管理员页面(<code>http://localhost:9000/#/mcp</code>);「开不开自助登记」是网关 <code>.env</code> 的部署期开关,页面上没有这个按钮。命令与用法在下面的指令表里也有(由代码生成)。</li>
|
|
384
|
+
<li><b>命令行可以切成英文</b>:<code>--lang en</code>(或环境变量 <code>LLM_GATEWAY_LANG=en</code>),
|
|
385
|
+
覆盖 <code>--help</code> / 横幅 / 报错 / REPL 提示 / <code>config</code> / <code>setup</code>;
|
|
386
|
+
<b>不给就是中文</b>,本页与网页界面始终中文 —— 详见下一节「命令行输出语言」。</li>
|
|
379
387
|
</ul>
|
|
380
388
|
</section>
|
|
381
389
|
|
|
390
|
+
<section id="s-lang">
|
|
391
|
+
<h2>七、命令行输出语言(<code>--lang en</code>)</h2>
|
|
392
|
+
<p>
|
|
393
|
+
命令行侧可以切成英文,<b>默认仍然是中文</b>(不给语言时与加这个开关之前逐字一致)。
|
|
394
|
+
两种给法等价,<code>--lang</code> 优先:
|
|
395
|
+
</p>
|
|
396
|
+
<div class="code-block"><code>gateway-agent --help --lang en # 英文帮助
|
|
397
|
+
gateway-agent -i --lang en # 英文 REPL:提示语、横幅、写入批准闸门都变英文
|
|
398
|
+
LLM_GATEWAY_LANG=en gateway-agent -p "…" # 环境变量写法,与上面等价
|
|
399
|
+
node server.js --help --lang en # 起服务的方式也认(只影响这一行帮助)
|
|
400
|
+
# Windows PowerShell:$env:LLM_GATEWAY_LANG='en'; gateway-agent -i</code></div>
|
|
401
|
+
<div class="manual-table-wrap">
|
|
402
|
+
<table class="manual-table">
|
|
403
|
+
<thead>
|
|
404
|
+
<tr><th>给法</th><th>优先级</th><th>说明</th></tr>
|
|
405
|
+
</thead>
|
|
406
|
+
<tbody>
|
|
407
|
+
<tr>
|
|
408
|
+
<td class="cmd">--lang en / --lang zh</td>
|
|
409
|
+
<td>最高</td>
|
|
410
|
+
<td><code>zh-CN</code> / <code>en-US</code> / <code>english</code> / <code>cn</code> 这类写法都认</td>
|
|
411
|
+
</tr>
|
|
412
|
+
<tr>
|
|
413
|
+
<td class="cmd">LLM_GATEWAY_LANG=en</td>
|
|
414
|
+
<td>次之</td>
|
|
415
|
+
<td>认不出的值<b>不报错</b>,直接退回默认中文 —— 语言不该拦住任何一次运行</td>
|
|
416
|
+
</tr>
|
|
417
|
+
<tr>
|
|
418
|
+
<td class="cmd">都不给</td>
|
|
419
|
+
<td>默认</td>
|
|
420
|
+
<td><code>zh</code></td>
|
|
421
|
+
</tr>
|
|
422
|
+
</tbody>
|
|
423
|
+
</table>
|
|
424
|
+
</div>
|
|
425
|
+
<div class="manual-table-wrap">
|
|
426
|
+
<table class="manual-table">
|
|
427
|
+
<thead>
|
|
428
|
+
<tr><th>会跟着变</th><th>不会变(始终中文)</th></tr>
|
|
429
|
+
</thead>
|
|
430
|
+
<tbody>
|
|
431
|
+
<tr>
|
|
432
|
+
<td>四个 CLI 入口与 <code>server.js</code> 的 <code>--help</code></td>
|
|
433
|
+
<td><b>Web 界面</b>:聊天页 / 任务页 / 设置面板</td>
|
|
434
|
+
</tr>
|
|
435
|
+
<tr>
|
|
436
|
+
<td>启动横幅(网关 / 模型 / 密钥来源那几行)</td>
|
|
437
|
+
<td><b>这一页手册</b>(包括下面的指令表格)</td>
|
|
438
|
+
</tr>
|
|
439
|
+
<tr>
|
|
440
|
+
<td>各种报错、缺密钥提示</td>
|
|
441
|
+
<td><code>GET /api/commands</code> 下发给网页的指令清单</td>
|
|
442
|
+
</tr>
|
|
443
|
+
<tr>
|
|
444
|
+
<td>REPL 提示(<code>/help</code> 的指令表、退出语)</td>
|
|
445
|
+
<td>发给模型的系统提示词与工具描述(那是<b>模型行为语言</b>,不是界面语言)</td>
|
|
446
|
+
</tr>
|
|
447
|
+
<tr>
|
|
448
|
+
<td><code>config list|get|set|unset|path</code> 与 <code>setup</code> 的输出</td>
|
|
449
|
+
<td>接口下发的报错文案(Web 与集成方的契约)</td>
|
|
450
|
+
</tr>
|
|
451
|
+
</tbody>
|
|
452
|
+
</table>
|
|
453
|
+
</div>
|
|
454
|
+
<p class="manual-note">
|
|
455
|
+
<b>为什么这一页不受影响</b>:语言是<b>进程级</b>开关(<code>lib/i18n.js</code>,零依赖),只在各入口脚本里
|
|
456
|
+
<code>setLang(resolveLang(args, env))</code> 设置一次,其余模块只管 <code>pick(中文, English)</code>;
|
|
457
|
+
手册页的指令表格由 <code>commandRows()</code> 生成,而它的<b>默认语言就是 <code>zh</code></b> ——
|
|
458
|
+
所以哪怕你用 <code>node server.js --lang en</code> 起服务,这一页与接口下发的清单仍是中文,不会半中半英。
|
|
459
|
+
缺英文时一律回落中文(不返回空串 / <code>undefined</code>),这是 <code>pick()</code> 的约定。
|
|
460
|
+
这个开关由 <code>tests/i18n.test.mjs</code> 盯着(含真起子进程跑五个入口的 <code>--help</code>,
|
|
461
|
+
英文输出里出现中文或全角字符就算失败)。
|
|
462
|
+
</p>
|
|
463
|
+
</section>
|
|
464
|
+
|
|
382
465
|
<section id="s-commands">
|
|
383
|
-
<h2
|
|
466
|
+
<h2>八、指令一览(自动生成)</h2>
|
|
384
467
|
<p class="manual-note">
|
|
385
468
|
三端对照在前,各端清单在后。<code>命令行 REPL</code> 是终端里的 <code>gateway-agent</code>;
|
|
386
469
|
<code>任务页</code> 是本服务的 <code>/task</code>;<code>本机聊天页</code> 是本服务的 <code>/</code>。
|
|
@@ -389,8 +472,26 @@ gateway-agent config set mode auto # 新任务的默认审批模式</co
|
|
|
389
472
|
</section>
|
|
390
473
|
|
|
391
474
|
<section id="s-faq">
|
|
392
|
-
<h2
|
|
475
|
+
<h2>九、常见问题</h2>
|
|
393
476
|
<dl class="manual-faq">
|
|
477
|
+
<dt>刚才在命令行聊的那条,网页任务列表里怎么没有?</dt>
|
|
478
|
+
<dd>
|
|
479
|
+
这是设计如此,不是丢了:网页侧边栏只列 <code><数据根>/tasks/</code> 里的<b>任务</b>(模式五),
|
|
480
|
+
命令行 REPL 的会话在<b>另一个目录</b> <code><数据根>/sessions/<uuid>.json</code>(模式六,
|
|
481
|
+
默认数据根是 <code>~/.llm-api-gateway-cli</code>),网页侧<b>没有任何接口读它</b> ——
|
|
482
|
+
两者的记录字段本来就不同(任务有标题、更新时间、回复索引;会话只有消息与用量),
|
|
483
|
+
所以两边各列各的,<b>不会互相出现</b>。要接着那条聊:
|
|
484
|
+
<code>gateway-agent -i --continue</code>(最近一条)或 <code>gateway-agent -i --resume <uuid></code>,
|
|
485
|
+
REPL 里敲 <code>/resume</code> 会列出最近几条;在哪个目录启动都行 ——
|
|
486
|
+
恢复时优先沿用会话里记下的工作目录(<code>-C</code> 才覆盖)。
|
|
487
|
+
想直接看文件:<code>dir %USERPROFILE%\.llm-api-gateway-cli\sessions</code>
|
|
488
|
+
(POSIX:<code>ls ~/.llm-api-gateway-cli/sessions</code>),一条会话一个 <code>.json</code>。
|
|
489
|
+
命令行切成英文(<code>--lang en</code>)与这件事无关。
|
|
490
|
+
<b>想在网页里看它</b>:打开 <a href="/sessions">/sessions</a>(<b>首页顶栏的「命令行」</b>、任务页顶栏与侧栏也都能进)——
|
|
491
|
+
那是<b>只读回放</b>:列出 <code><数据根>/sessions/</code> 里的会话,点开能看全文,
|
|
492
|
+
但页面上没有输入框、改不了任何东西(要接着聊仍是上面那条终端命令)。
|
|
493
|
+
</dd>
|
|
494
|
+
|
|
394
495
|
<dt><code>/files</code> 说"未放行"?</dt>
|
|
395
496
|
<dd>目录浏览、项目记忆、任务记录这类接口会回传本机路径与文件正文,服务只在本机监听(或显式加了 <code>--allow-remote-fs</code>)时才放行。用 <code>--host 0.0.0.0</code> 从别的机器访问时,它们是关着的 —— 这是有意的安全边界。</dd>
|
|
396
497
|
|
|
@@ -429,7 +530,10 @@ gateway-agent config set mode auto # 新任务的默认审批模式</co
|
|
|
429
530
|
<dt>装的时候说「没有 Node」或「Node 版本太低」?</dt>
|
|
430
531
|
<dd>安装器<b>不代装</b>运行时:它只报错并告诉你需要什么(<code>node -v</code> 要 ≥ 18),不动你的系统。装好 Node 再重跑同一条命令即可;中途失败也不会留半成品(临时文件必删)。</dd>
|
|
431
532
|
|
|
432
|
-
<dt
|
|
533
|
+
<dt><code>--lang en</code> 给了,网页界面怎么还是中文?</dt>
|
|
534
|
+
<dd>这是有意的:语言开关只作用于<b>命令行侧</b>(<code>--help</code> / 横幅 / 报错 / REPL 提示 / <code>config</code> / <code>setup</code> / 写入批准闸门),网页界面、这一页手册以及 <code>GET /api/commands</code> 下发的指令清单始终是中文;发往模型的系统提示词与工具描述也不跟着变。给法、优先级与完整边界见第七节「命令行输出语言」。另外注意 <code>node server.js --lang en</code> 只影响它自己那行 <code>--help</code>,不影响它起出来的页面。</dd>
|
|
535
|
+
|
|
536
|
+
<dt>不想装,只想在仓库里直接用?</dt>
|
|
433
537
|
<dd>在项目目录 <code>npm install</code> 之后,<code>node cli-agent.js</code>(终端 Agent)、<code>node server.js</code>(本机 Web UI 与任务页 <code>http://127.0.0.1:3100</code>)直接就能跑;<code>npm link</code> 只是把同一份东西做成任意目录可用的全局命令。</dd>
|
|
434
538
|
|
|
435
539
|
<dt>装完了,但敲 <code>gateway-agent</code> 说找不到命令?</dt>
|
package/public/mcp.css
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 网页 MCP 面板(/mcp)样式
|
|
3
|
+
*
|
|
4
|
+
* 与 sessions.css 同一纪律:只取 styles.css 里的变量,不定义新变量、不写裸颜色 ——
|
|
5
|
+
* theme.test.mjs 的「无裸十六进制颜色 / 无裸 rgba / 变量都有定义」三条对本文件同样成立。
|
|
6
|
+
* 骨架复用 styles.css 的 .app / .sidebar / .main / .topbar / .messages。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
.mcp-doc {
|
|
10
|
+
padding: 4px 0 40px;
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
gap: 12px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.mcp-card {
|
|
17
|
+
border: 1px solid var(--border);
|
|
18
|
+
border-radius: var(--radius);
|
|
19
|
+
background: var(--bg-soft);
|
|
20
|
+
padding: 12px 14px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.mcp-card > h2 {
|
|
24
|
+
margin: 0 0 10px;
|
|
25
|
+
font-size: 13.5px;
|
|
26
|
+
color: var(--text);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.mcp-row {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: 8px;
|
|
33
|
+
flex-wrap: wrap;
|
|
34
|
+
font-size: 12.5px;
|
|
35
|
+
color: var(--text-dim);
|
|
36
|
+
line-height: 1.9;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.mcp-row code {
|
|
40
|
+
font-family: var(--mono);
|
|
41
|
+
color: var(--text);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.mcp-item {
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: 8px;
|
|
48
|
+
padding: 5px 0;
|
|
49
|
+
font-size: 12.5px;
|
|
50
|
+
color: var(--text);
|
|
51
|
+
border-bottom: 1px dashed var(--border);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.mcp-item:last-child { border-bottom: none; }
|
|
55
|
+
|
|
56
|
+
.mcp-item .name {
|
|
57
|
+
font-family: var(--mono);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.mcp-item .meta {
|
|
61
|
+
color: var(--text-dim);
|
|
62
|
+
font-size: 11.5px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.mcp-item .spacer { flex: 1; }
|
|
66
|
+
|
|
67
|
+
.mcp-tag {
|
|
68
|
+
padding: 0 5px;
|
|
69
|
+
border-radius: 999px;
|
|
70
|
+
font-size: 10px;
|
|
71
|
+
background: var(--accent-soft);
|
|
72
|
+
color: var(--accent);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.mcp-tag.warn { background: var(--warn-soft-strong); color: var(--warn); }
|
|
76
|
+
.mcp-tag.off { background: var(--bg-code); color: var(--text-faint); }
|
|
77
|
+
|
|
78
|
+
.mcp-field {
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
gap: 4px;
|
|
82
|
+
margin-bottom: 8px;
|
|
83
|
+
font-size: 12px;
|
|
84
|
+
color: var(--text-dim);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.mcp-field input,
|
|
88
|
+
.mcp-field textarea,
|
|
89
|
+
.mcp-select {
|
|
90
|
+
width: 100%;
|
|
91
|
+
box-sizing: border-box;
|
|
92
|
+
padding: 6px 8px;
|
|
93
|
+
border-radius: var(--radius-sm);
|
|
94
|
+
border: 1px solid var(--border);
|
|
95
|
+
background: var(--bg-raised);
|
|
96
|
+
color: var(--text);
|
|
97
|
+
font-size: 12.5px;
|
|
98
|
+
font-family: inherit;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.mcp-select { width: auto; }
|
|
102
|
+
|
|
103
|
+
.mcp-field textarea { font-family: var(--mono); resize: vertical; }
|
|
104
|
+
|
|
105
|
+
.mcp-actions {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
gap: 8px;
|
|
109
|
+
margin-top: 6px;
|
|
110
|
+
flex-wrap: wrap;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.mcp-banner {
|
|
114
|
+
margin: 0;
|
|
115
|
+
padding: 10px 16px;
|
|
116
|
+
border-bottom: 1px solid var(--border);
|
|
117
|
+
background: var(--bg-soft);
|
|
118
|
+
color: var(--text-dim);
|
|
119
|
+
font-size: 12.5px;
|
|
120
|
+
line-height: 1.7;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.mcp-banner.danger {
|
|
124
|
+
background: var(--danger-soft);
|
|
125
|
+
border-bottom-color: var(--danger-border);
|
|
126
|
+
color: var(--danger);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.mcp-banner.ok {
|
|
130
|
+
background: var(--ok-soft);
|
|
131
|
+
border-bottom-color: var(--border);
|
|
132
|
+
color: var(--ok);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.mcp-note {
|
|
136
|
+
margin: 6px 0 0;
|
|
137
|
+
font-size: 11.5px;
|
|
138
|
+
color: var(--text-faint);
|
|
139
|
+
line-height: 1.7;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.mcp-tool {
|
|
143
|
+
display: block;
|
|
144
|
+
width: 100%;
|
|
145
|
+
text-align: left;
|
|
146
|
+
background: none;
|
|
147
|
+
border: none;
|
|
148
|
+
color: var(--text-dim);
|
|
149
|
+
font-size: 11.5px;
|
|
150
|
+
padding: 4px 6px;
|
|
151
|
+
border-radius: var(--radius-sm);
|
|
152
|
+
cursor: default;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.mcp-tool b { color: var(--text); font-weight: 600; }
|