llm-api-gateway-cli 1.0.0 → 1.0.1
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.md +4 -1
- package/cli-agent.js +21 -4
- package/lib/common.js +24 -3
- package/lib/config.js +22 -5
- package/lib/hub.js +11 -3
- package/lib/secrets.js +25 -9
- package/lib/settings.js +65 -1
- package/package.json +1 -1
- package/public/manual.html +13 -0
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ npm install # 安装 openai 与 @anthropic-ai/sdk
|
|
|
37
37
|
|
|
38
38
|
装完就能在**任意目录**直接用 `gateway-agent`,密钥不需要手写 `.env`:起服务后在页面「设置」里填一次(或 `gateway-agent config set key sk-xxx`),存进 `~/.llm-api-gateway-cli/credentials.json`(**不进 `config.json`**);也仍然可以用环境变量 / `.env` / `--key`,那三层优先级更高。
|
|
39
39
|
|
|
40
|
-
> **⚠️ 装完还差一步:配密钥**(不用手写 `.env`)。`gateway-web` / `gateway-task` **没有密钥也能起**(页面照常打得开,启动横幅写「密钥 未配置」),在页面「设置 → 网关密钥」里填一次即可,保存后立即生效、不用重启;命令行也可 `gateway-agent config set key sk-xxx
|
|
40
|
+
> **⚠️ 装完还差一步:配密钥**(不用手写 `.env`)。`gateway-web` / `gateway-task` **没有密钥也能起**(页面照常打得开,启动横幅写「密钥 未配置」),在页面「设置 → 网关密钥」里填一次即可,保存后立即生效、不用重启;命令行也可 `gateway-agent config set key sk-xxx`。**四个命令行入口也读这个文件**,所以配一次 Web 与命令行都认。只有一次性命令行 agent(`gateway-agent -p "…"`)没有界面可交互,缺密钥会**直接报错退出**(横幅会先告诉你当前的网关/模型及其来源,以及「那是内置默认、不是配好了」)。想用环境变量 / `.env` 也行(优先级更高):`.env` 的加载顺序是**①安装目录 → ②当前工作目录**,npm 全局安装的包目录里只有 `.env.example`,所以要放在**你运行命令的那个目录**(服务在**启动那一刻**读)。网关不在默认地址(`http://127.0.0.1:9000`)或想固定模型,用 `gateway-agent config set baseUrl|model …`(也能在设置面板里改)。完整步骤见手册页第一节「安装与启动 → 首次配置」。
|
|
41
41
|
|
|
42
42
|
> 不想翻文档也行:起服务后打开 **[操作手册页 `/manual`](http://127.0.0.1:3100/manual)**,第一节就是「安装与启动」—— 三条路线、参数表、换镜像、**首次配置(界面 / 命令行 / 环境变量三种给密钥方式、密钥存哪、网关地址与模型)**、启动自检与卸载,命令都是可复制的。
|
|
43
43
|
|
|
@@ -761,11 +761,14 @@ npm run test:all # 先离线再联网
|
|
|
761
761
|
| `tests/task-session.test.mjs` | 任务会话(方案 C)与历史预算:工具结果跨轮活下来、纯工具轮不被丢、旧任务懒迁移(含**带 `args` 时还原成原生 `tool_calls`**)、同一任务并发 409、请求校验 400、旧格式兼容开关、删任务连带删会话、`/api/store` 汇报会话占用;`history.maxChars` 超预算时压正文留记录、改配置立刻生效、`readFileDigest` 清单注入与「已被外部修改」告警 |
|
|
762
762
|
| `tests/settings.test.mjs` | 配置落盘:18 个键的默认值与往返、四层优先级与来源列、越界值/未知键(含模糊建议)、`config.json` 拒收密钥(改名夹带也拒)、`unset` 回默认、坏文件降级与拒绝覆盖、UI 状态键与 `config` 子命令(`list/get/set/unset/path`,含「被环境变量盖住」的警告)、`GET/PUT /api/settings` |
|
|
763
763
|
| `tests/secrets.test.mjs` | 免 `.env` 的密钥配置:密钥文件路径(跟数据根走 / `LLM_GATEWAY_SECRET_FILE` 覆盖)、读写往返与覆盖、POSIX `0600` 与「权限被放松」告警、坏文件降级、优先级(`--key`/env > 文件)、`keyRow` 只回掩码、`config set/get/list/unset/path key` 的落点,以及**没有密钥也能起服务** + 接口回 409 `needsKey` + 界面 PUT 一次即生效/手工改文件刷新即生效 |
|
|
764
|
+
| `tests/config-source.test.mjs` | **配置来源与首次使用引导**:`loadDotEnv` 记下「哪个变量由哪个 `.env` 提供」且真环境变量不算、来源文案四层(内置默认 / 启动参数 / 环境变量 + `.env` 路径 / 配置文件路径)、`summarizeConfigSources` 的「全默认 + 无 config.json = 全新机器」判定、密钥来源说到具体别名、**四个 CLI 也认 `credentials.json`**(本轮修掉的缺陷),以及 CLI/hub 横幅必须带来源、缺密钥时仍退 1 的源码契约 |
|
|
764
765
|
|
|
765
766
|
## 配置:住在磁盘上,CLI / Web / REPL 同一套
|
|
766
767
|
|
|
767
768
|
**优先级一句话**:`--flag` > 环境变量 > `config.json` > 内置默认;密钥是唯一的例外 —— `--key` / 环境变量 / `.env` > `credentials.json`,而它**永远不进 `config.json`**。
|
|
768
769
|
|
|
770
|
+
**启动横幅会告诉你每个值「从哪来」**(`(内置默认)` / `(启动参数 --model)` / `(环境变量 GATEWAY_MODEL(来自 .env:E:\proj\.env))` / `(配置文件 …\config.json)`),命令行 agent 的横幅还多一行脱敏密钥。这条不是装饰:`.env` 的查找顺序是**①安装目录 → ②当前工作目录**,所以**在别的项目目录里跑会读到那个目录的 `.env`** —— 横幅里那句 `来自 .env:<路径>` 就是用来破「我明明没配过,怎么已经有值了 / 怎么已经有密钥了」这个高频误会的。反过来,全新机器第一次跑会看到两个值都标「内置默认」并多一句「这台机器还没有任何配置」+ 三条填法:**有值不等于配过了**。四个命令行入口与两个 Web 入口读同一套配置(含 `credentials.json`)。
|
|
771
|
+
|
|
769
772
|
配置文件的默认位置是 `~/.llm-api-gateway-cli/config.json`(Windows 也就是 `C:\Users\<你>\.llm-api-gateway-cli\config.json`;与任务、会话同一个数据根,整体想换地方用 `LLM_GATEWAY_DATA_DIR`),也可以用环境变量 `LLM_GATEWAY_CONFIG` 只把配置文件指到别处。文件不存在也没关系,第一次 `config set` 会自动建。
|
|
770
773
|
|
|
771
774
|
```bash
|
package/cli-agent.js
CHANGED
|
@@ -25,14 +25,15 @@ import path from 'node:path';
|
|
|
25
25
|
import readline from 'node:readline';
|
|
26
26
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
27
27
|
|
|
28
|
-
import { loadDotEnv, parseArgs, DEFAULT_BASE_URL } from './lib/common.js';
|
|
28
|
+
import { loadDotEnv, parseArgs, DEFAULT_BASE_URL, maskKey } from './lib/common.js';
|
|
29
29
|
import { resolveConfig, numArg, fail, KEY_HINT } from './lib/config.js';
|
|
30
30
|
import { checkWorkDir, setBashEnabled, availableTools } from './lib/tools.js';
|
|
31
31
|
import { createSession, restoreSession, runTurn, resumePendingTurn, countTurns } from './lib/runner.js';
|
|
32
32
|
import { createSessionStore, resolveSessionDir, SESSION_TTL_MS } from './lib/sessionstore.js';
|
|
33
33
|
import { COMMANDS, parseSlash, unescapeSlash, suggest, helpText, diffLines } from './lib/commands.js';
|
|
34
34
|
import { configCommand, configHelp } from './lib/configcmd.js';
|
|
35
|
-
import { settingsFilePath } from './lib/settings.js';
|
|
35
|
+
import { settingsFilePath, summarizeConfigSources } from './lib/settings.js';
|
|
36
|
+
import { secretSourceText } from './lib/secrets.js';
|
|
36
37
|
import { AGENT_LIMITS } from './lib/agent.js';
|
|
37
38
|
import { MEMORY_FILES, readMemory } from './lib/memory.js';
|
|
38
39
|
import { refreshMcpTools, mcpStatusText } from './lib/mcp.js';
|
|
@@ -364,10 +365,23 @@ async function main() {
|
|
|
364
365
|
// bash 默认关闭:命令级权限不该默默打开
|
|
365
366
|
setBashEnabled(Boolean(args['allow-bash']));
|
|
366
367
|
|
|
367
|
-
const
|
|
368
|
+
const bootCfg = resolveConfig(args, { defaultModel: DEFAULT_MODEL });
|
|
369
|
+
const { key, baseUrl, model, temperature, maxTokens } = bootCfg;
|
|
370
|
+
// 启动横幅必须能回答「这个值从哪来」:用户在新机器上第一次跑,看到 base/model 有值会以为
|
|
371
|
+
// 「已经配好了」,其实那可能只是**内置默认值**,也可能来自**当前目录的 .env**(别的项目的配置)。
|
|
372
|
+
const srcInfo = summarizeConfigSources({ env: process.env, args });
|
|
373
|
+
const srcOf = (k) => (srcInfo.rows[k] ? `(${srcInfo.rows[k].sourceText})` : '');
|
|
374
|
+
const keySrcOf = key ? secretSourceText(bootCfg.secret) : '';
|
|
368
375
|
// 模型轮次上限:与 Web 侧同名同义;非法值交给 resolveMaxSteps 回落到默认
|
|
369
376
|
const maxSteps = numArg(args['max-steps']);
|
|
370
377
|
if (!key) {
|
|
378
|
+
process.stderr.write(
|
|
379
|
+
`[gateway] 还没配密钥 · base=${baseUrl}${srcOf('baseUrl')} model=${model}${srcOf('model')}\n` +
|
|
380
|
+
(srcInfo.allDefault && !srcInfo.configExists
|
|
381
|
+
? ' 这台机器还没有任何配置(没找到 config.json / credentials.json / .env / 相关环境变量):\n' +
|
|
382
|
+
' 上面那两个值是**内置默认**,不是「已经配好了」——配一次之后就一直用它。\n'
|
|
383
|
+
: ''),
|
|
384
|
+
);
|
|
371
385
|
process.stderr.write(`[错误] ${KEY_HINT}\n`);
|
|
372
386
|
process.exit(1);
|
|
373
387
|
}
|
|
@@ -517,7 +531,10 @@ async function main() {
|
|
|
517
531
|
: '按上述原因未向网关发出请求'}(CLI 本次不携带 MCP 工具)`
|
|
518
532
|
: mcpStatusText();
|
|
519
533
|
process.stderr.write(
|
|
520
|
-
`[gateway] 原生 Agent · base=${baseUrl} model=${model}\n` +
|
|
534
|
+
`[gateway] 原生 Agent · base=${baseUrl}${srcOf('baseUrl')} model=${model}${srcOf('model')}\n` +
|
|
535
|
+
// 密钥这一行上轮只做在 Web 横幅里,CLI 的横幅反倒不说密钥从哪来 —— 而「我没配过密钥,
|
|
536
|
+
// 它怎么跑起来了」正是最需要一句解释的情形(多半是当前目录的 .env 给的)
|
|
537
|
+
` 密钥 ${maskKey(key)}(${keySrcOf})\n` +
|
|
521
538
|
` 工作目录 ${workDir}\n` +
|
|
522
539
|
` 工具 ${toolNames}${args['allow-bash'] ? ' [bash 已开启]' : ''}\n` +
|
|
523
540
|
` ${mcpLine}\n` +
|
package/lib/common.js
CHANGED
|
@@ -11,19 +11,40 @@ import path from 'node:path';
|
|
|
11
11
|
export const DEFAULT_BASE_URL = 'http://127.0.0.1:9000';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* 「这个环境变量是哪个 .env 给的」。
|
|
15
|
+
*
|
|
16
|
+
* 只记**真正被 .env 写入**的那些:真环境变量本来就存在时 .env 不参与(下面的
|
|
17
|
+
* `!(m[1] in process.env)` 分支),也就不该被标成「来自 .env」。
|
|
18
|
+
* 有了它,配置来源才能说清「环境变量 GATEWAY_MODEL(来自 .env:E:\proj\.env)」,
|
|
19
|
+
* 而不是笼统一句「环境变量」—— 用户排查「我没配过怎么会有值」时,差别就在这。
|
|
16
20
|
*/
|
|
21
|
+
const dotEnvOrigins = new Map();
|
|
22
|
+
|
|
23
|
+
/** 读取 .env(先脚本目录、再当前工作目录),不覆盖已存在的环境变量。
|
|
24
|
+
* 返回值是**本次调用**真正写入的来源表,便于调用方/测试确认「谁被写进来了」。 */
|
|
17
25
|
export function loadDotEnv(scriptDir) {
|
|
26
|
+
const files = [];
|
|
27
|
+
const origins = new Map();
|
|
18
28
|
for (const file of [path.join(scriptDir, '.env'), path.join(process.cwd(), '.env')]) {
|
|
19
29
|
if (!existsSync(file)) continue;
|
|
30
|
+
files.push(file);
|
|
20
31
|
for (const raw of readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
21
32
|
const m = raw.match(/^\s*([A-Za-z_][\w.-]*)\s*=\s*(.*)\s*$/);
|
|
22
33
|
if (!m) continue;
|
|
23
34
|
const value = m[2].replace(/^['"]|['"]$/g, '');
|
|
24
|
-
if (!(m[1] in process.env))
|
|
35
|
+
if (!(m[1] in process.env)) {
|
|
36
|
+
process.env[m[1]] = value;
|
|
37
|
+
dotEnvOrigins.set(m[1], file);
|
|
38
|
+
origins.set(m[1], file);
|
|
39
|
+
}
|
|
25
40
|
}
|
|
26
41
|
}
|
|
42
|
+
return { files, origins };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 某个环境变量的 .env 出处;不是 .env 给的(没设 / 真环境变量)返回 '' */
|
|
46
|
+
export function dotEnvOrigin(name) {
|
|
47
|
+
return name ? dotEnvOrigins.get(name) || '' : '';
|
|
27
48
|
}
|
|
28
49
|
|
|
29
50
|
/** 只接受真正的字符串参数值:值型选项缺值时会退化成布尔 true,必须挡掉 */
|
package/lib/config.js
CHANGED
|
@@ -8,12 +8,20 @@
|
|
|
8
8
|
* 所有 CLI 都走 resolveConfig,保证问「密钥从哪来」只有一种答案。
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { resolveBaseUrl, strArg } from './common.js';
|
|
12
|
+
import { resolveSecretKey } from './secrets.js';
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* 缺密钥时的统一提示(各 CLI 文案一致,避免用户按提示改了还是不通)。
|
|
16
|
+
*
|
|
17
|
+
* 三条路都必须是真的:界面 / `config set key` 写的是 `credentials.json`,而四个 CLI 现在
|
|
18
|
+
* 也读这个文件(下面的 resolveConfig 走 resolveSecretKey)—— 提示与行为对不上,
|
|
19
|
+
* 用户会按着提示配完发现还是报缺密钥。
|
|
20
|
+
*/
|
|
14
21
|
export const KEY_HINT =
|
|
15
|
-
'缺少 sk-
|
|
16
|
-
+ '
|
|
22
|
+
'缺少 sk- 密钥:① 在任务页/聊天页「设置」里填(存 credentials.json,仅本用户可读,命令行也认它);'
|
|
23
|
+
+ '② 或执行 gateway-agent config set key sk-xxx;'
|
|
24
|
+
+ '③ 或 --key sk-xxx、环境变量 SK / GATEWAY_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY,或在 .env 中写 GATEWAY_KEY=sk-xxx';
|
|
17
25
|
|
|
18
26
|
/**
|
|
19
27
|
* 数字参数:非法值(NaN / 布尔 / 空串)一律返回 undefined。
|
|
@@ -38,8 +46,17 @@ export function numArg(v) {
|
|
|
38
46
|
*/
|
|
39
47
|
export function resolveConfig(args = {}, { defaultModel = '', defaultMaxTokens, env = process.env } = {}) {
|
|
40
48
|
const maxTokens = numArg(args['max-tokens']) ?? defaultMaxTokens;
|
|
49
|
+
// 密钥走 lib/secrets.js:`--key` > 环境变量 / `.env` > credentials.json。
|
|
50
|
+
// 命令行必须认那个文件,否则「在界面里配一次就能用」对 CLI 入口是句空话
|
|
51
|
+
// (界面与 config set key 写的都是它)。来源字段一并带出来,横幅才能说清「谁给的」。
|
|
52
|
+
const secret = resolveSecretKey({ args, env });
|
|
41
53
|
return {
|
|
42
|
-
key:
|
|
54
|
+
key: secret.key,
|
|
55
|
+
keySource: secret.source,
|
|
56
|
+
keyFile: secret.file,
|
|
57
|
+
// 整个来源对象一并带出:横幅要 `secretSourceText(cfg.secret)` 才能说清
|
|
58
|
+
// 「环境变量 GATEWAY_KEY(来自 .env:…)」这类细节
|
|
59
|
+
secret,
|
|
43
60
|
baseUrl: resolveBaseUrl(args, env),
|
|
44
61
|
// 顺序:--model > GATEWAY_MODEL > 该入口的默认模型
|
|
45
62
|
model: strArg(args.model) || strArg(env.GATEWAY_MODEL) || defaultModel,
|
package/lib/hub.js
CHANGED
|
@@ -78,6 +78,8 @@ import {
|
|
|
78
78
|
isSecretKey,
|
|
79
79
|
parseSettingValue,
|
|
80
80
|
unknownKeyError,
|
|
81
|
+
settingSourceText,
|
|
82
|
+
summarizeConfigSources,
|
|
81
83
|
} from './settings.js';
|
|
82
84
|
import { commandRows } from './commands.js';
|
|
83
85
|
import { estimateCost, formatCost } from './pricing.js';
|
|
@@ -1456,11 +1458,17 @@ export function runHub(argv, { entry = 'web' } = {}) {
|
|
|
1456
1458
|
console.log('LLM API Gateway · 本地 Web UI');
|
|
1457
1459
|
console.log(` 聊天 http://${host}:${port}/`);
|
|
1458
1460
|
console.log(` 任务 http://${host}:${port}/task`);
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
+
// 网关 / 模型也要带来源:新机器上「有值」不等于「配过」——默认值、当前目录的 .env、
|
|
1462
|
+
// config.json 三者长得很像,不说清来源就只能靠猜(与 CLI 横幅同一套文案)
|
|
1463
|
+
console.log(` 网关 ${baseUrl}(${settingSourceText(settings, 'baseUrl')})`);
|
|
1464
|
+
console.log(` 模型 ${model}(${settingSourceText(settings, 'model')})`);
|
|
1461
1465
|
// 密钥一行要能回答「配没配、从哪来」——来源是环境时,credentials.json 不参与,这一点要说清
|
|
1462
|
-
console.log(` 密钥 ${key ? `${maskKey(key)}(${secretSourceText(secret
|
|
1466
|
+
console.log(` 密钥 ${key ? `${maskKey(key)}(${secretSourceText(secret)})` : '未配置'}`);
|
|
1463
1467
|
if (!key) {
|
|
1468
|
+
const srcInfo = summarizeConfigSources({ keys: ['baseUrl', 'model'], env: cfg.env, args });
|
|
1469
|
+
if (srcInfo.allDefault && !srcInfo.configExists) {
|
|
1470
|
+
console.log(' 这台机器还没有任何配置:上面的网关与模型是**内置默认值**,不是「已经配好了」');
|
|
1471
|
+
}
|
|
1464
1472
|
console.log(' 填法:页面「设置」里填(存 credentials.json,仅本用户可读)');
|
|
1465
1473
|
console.log(' 或 gateway-agent config set key sk-xxx;或环境变量 GATEWAY_KEY / --key');
|
|
1466
1474
|
}
|
package/lib/secrets.js
CHANGED
|
@@ -22,7 +22,7 @@ import os from 'node:os';
|
|
|
22
22
|
|
|
23
23
|
import { writeJsonAtomic } from './jsonstore.js';
|
|
24
24
|
import { defaultStoreRoot } from './taskstore.js';
|
|
25
|
-
import { resolveKey, strArg, maskKey } from './common.js';
|
|
25
|
+
import { resolveKey, strArg, maskKey, dotEnvOrigin } from './common.js';
|
|
26
26
|
|
|
27
27
|
/** 想单独把密钥文件放别处时用它(整体换数据根用 LLM_GATEWAY_DATA_DIR) */
|
|
28
28
|
export const SECRET_ENV = 'LLM_GATEWAY_SECRET_FILE';
|
|
@@ -142,11 +142,14 @@ export function clearSecret({ file = secretFilePath() } = {}) {
|
|
|
142
142
|
export function resolveSecretKey({ args = null, env = process.env, file = secretFilePath(env) } = {}) {
|
|
143
143
|
const external = resolveKey(args, env);
|
|
144
144
|
if (external) {
|
|
145
|
-
// 区分 --key 与其它:args.key
|
|
146
|
-
|
|
145
|
+
// 区分 --key 与其它:args.key 存在就是命令行显式给的。
|
|
146
|
+
// env 层再细分到「哪个别名」以及「是不是某个 .env 给的」——横幅要说清这个。
|
|
147
|
+
if (strArg(args?.key)) return { key: external, source: 'flag', envName: '', dotEnvFile: '', file, warning: '' };
|
|
148
|
+
const envName = KEY_ENV_NAMES.find((n) => strArg(env?.[n])) || '';
|
|
149
|
+
return { key: external, source: 'env', envName, dotEnvFile: envName ? dotEnvOrigin(envName) : '', file, warning: '' };
|
|
147
150
|
}
|
|
148
151
|
const read = readSecret({ file, env });
|
|
149
|
-
return { key: read.key, source: read.key ? 'file' : '', file, warning: read.warning };
|
|
152
|
+
return { key: read.key, source: read.key ? 'file' : '', envName: '', dotEnvFile: '', file, warning: read.warning };
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
export const SECRET_SOURCE_LABELS = {
|
|
@@ -155,9 +158,22 @@ export const SECRET_SOURCE_LABELS = {
|
|
|
155
158
|
file: '密钥文件',
|
|
156
159
|
};
|
|
157
160
|
|
|
158
|
-
/**
|
|
159
|
-
|
|
160
|
-
|
|
161
|
+
/** 认的密钥环境变量别名(顺序即优先级,与 lib/common.js 的 resolveKey 一致) */
|
|
162
|
+
const KEY_ENV_NAMES = ['SK', 'GATEWAY_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'];
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 人话来源:给启动横幅与 config list 用(`.env` 走的是 env 层,见 resolveSecretKey 注释)。
|
|
166
|
+
*
|
|
167
|
+
* 第二参可以传 `{ envName, dotEnvFile }`(或整个 resolveSecretKey 的返回值),
|
|
168
|
+
* 这样「环境变量」能细到「环境变量 GATEWAY_KEY(来自 .env:E:\proj\.env)」——
|
|
169
|
+
* 用户排查「我没配过哪来的密钥」时靠的就是这一句。
|
|
170
|
+
*/
|
|
171
|
+
export function secretSourceText(source, detail = {}) {
|
|
172
|
+
const s = typeof source === 'object' && source ? source.source : source;
|
|
173
|
+
const d = typeof source === 'object' && source ? source : detail || {};
|
|
174
|
+
const base = SECRET_SOURCE_LABELS[s] || '未配置';
|
|
175
|
+
if (s !== 'env' || !d.envName) return base;
|
|
176
|
+
return d.dotEnvFile ? `${base} ${d.envName}(来自 .env:${d.dotEnvFile})` : `${base} ${d.envName}`;
|
|
161
177
|
}
|
|
162
178
|
|
|
163
179
|
/**
|
|
@@ -190,8 +206,8 @@ export function keyRow({ env = process.env, file = secretFilePath(env), args = n
|
|
|
190
206
|
mask: maskKey(resolved.key),
|
|
191
207
|
file,
|
|
192
208
|
fileMode: perms.mode,
|
|
193
|
-
//
|
|
194
|
-
sourceText: secretSourceText(resolved
|
|
209
|
+
// 来源是人话,页面直接显示;命令行同源(带别名与 .env 出处,见 secretSourceText)
|
|
210
|
+
sourceText: secretSourceText(resolved),
|
|
195
211
|
warning: resolved.warning || perms.warning || '',
|
|
196
212
|
fromFile,
|
|
197
213
|
};
|
package/lib/settings.js
CHANGED
|
@@ -20,7 +20,7 @@ import path from 'node:path';
|
|
|
20
20
|
import os from 'node:os';
|
|
21
21
|
import { readFileSync, mkdirSync } from 'node:fs';
|
|
22
22
|
|
|
23
|
-
import { DEFAULT_BASE_URL } from './common.js';
|
|
23
|
+
import { DEFAULT_BASE_URL, dotEnvOrigin } from './common.js';
|
|
24
24
|
import { MODES, AGENT_LIMITS } from './agent.js';
|
|
25
25
|
import { TTL_DAYS, MAX_TASKS, defaultStoreRoot } from './taskstore.js';
|
|
26
26
|
import { TASK_SESSION_MAX_BYTES, HISTORY_MAX_CHARS, HISTORY_KEEP_RECENT_TURNS } from './tasksession.js';
|
|
@@ -399,6 +399,70 @@ export function resolveSettings({ file = settingsFilePath(), env = process.env,
|
|
|
399
399
|
return { values, sources, warnings, file: read.file, exists: read.exists };
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
+
/** 四层优先级的展示名 */
|
|
403
|
+
export const SOURCE_LABELS = {
|
|
404
|
+
flag: '启动参数',
|
|
405
|
+
env: '环境变量',
|
|
406
|
+
file: '配置文件',
|
|
407
|
+
default: '内置默认',
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* 把某一项的来源翻成**能直接打印**的一句话。
|
|
412
|
+
*
|
|
413
|
+
* 只写「环境变量」是不够的:用户最常卡住的正是「我没配过,这值哪来的」——
|
|
414
|
+
* 而 `GATEWAY_MODEL` 可能来自真环境变量,也可能来自**某个 .env 文件**
|
|
415
|
+
* (`.env` 的查找顺序是「安装目录 → 当前工作目录」,见 lib/common.js)。
|
|
416
|
+
* 所以这里把变量名与 .env 路径都写出来,一眼能看出是不是别的项目的配置漏进来了。
|
|
417
|
+
*
|
|
418
|
+
* 第三参 `originOf` 是「查 .env 出处」的函数(默认用全局记录的 `dotEnvOrigin`),
|
|
419
|
+
* 与 `resolveSettings({env, file})` 同一个思路:可注入,测试才能给出确定的环境。
|
|
420
|
+
*/
|
|
421
|
+
export function settingSourceText(resolved, key, originOf = dotEnvOrigin) {
|
|
422
|
+
const entry = SETTINGS_SCHEMA.find((e) => e.key === key) || {};
|
|
423
|
+
switch (resolved.sources[key]) {
|
|
424
|
+
case 'flag': {
|
|
425
|
+
// flag 里可能已经自带横线(`--base-url` / `-m/--model`),别再补一个变成 `----base-url`
|
|
426
|
+
const flag = entry.flag || key;
|
|
427
|
+
return `${SOURCE_LABELS.flag} ${flag.startsWith('-') ? flag : `--${flag}`}`;
|
|
428
|
+
}
|
|
429
|
+
case 'env': {
|
|
430
|
+
const name = entry.env || '';
|
|
431
|
+
const dot = name ? originOf(name) : '';
|
|
432
|
+
if (!name) return SOURCE_LABELS.env;
|
|
433
|
+
return dot ? `${SOURCE_LABELS.env} ${name}(来自 .env:${dot})` : `${SOURCE_LABELS.env} ${name}`;
|
|
434
|
+
}
|
|
435
|
+
case 'file':
|
|
436
|
+
return `${SOURCE_LABELS.file} ${resolved.file}`;
|
|
437
|
+
default:
|
|
438
|
+
return SOURCE_LABELS.default;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* 启动横幅与「首次使用」判定共用的摘要。
|
|
444
|
+
*
|
|
445
|
+
* `allDefault` 看的是**整个白名单**而不是传进来的几个键:只要任何一项被 flag / 环境变量 /
|
|
446
|
+
* config.json 给过值,这台机器就算「配置过」,不该再唠叨首次使用引导。
|
|
447
|
+
*/
|
|
448
|
+
export function summarizeConfigSources({ keys = ['baseUrl', 'model'], env = process.env, args = null, file = settingsFilePath() } = {}) {
|
|
449
|
+
const resolved = resolveSettings({ file, env, args });
|
|
450
|
+
const rows = {};
|
|
451
|
+
for (const key of keys) {
|
|
452
|
+
rows[key] = {
|
|
453
|
+
value: getByPath(resolved.values, key),
|
|
454
|
+
source: resolved.sources[key],
|
|
455
|
+
sourceText: settingSourceText(resolved, key),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
rows,
|
|
460
|
+
file: resolved.file,
|
|
461
|
+
configExists: resolved.exists,
|
|
462
|
+
allDefault: SETTINGS_SCHEMA.every((entry) => resolved.sources[entry.key] === 'default'),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
402
466
|
/**
|
|
403
467
|
* `config list` 与 Web 设置面板共用的行:键、当前值、来源、默认、说明,
|
|
404
468
|
* 外加**渲染所需的类型信息**(type / 枚举取值 / 范围)。
|
package/package.json
CHANGED
package/public/manual.html
CHANGED
|
@@ -178,6 +178,8 @@ gateway-agent config unset key # 清掉,回到「未配置」</code><
|
|
|
178
178
|
<code>OPENAI_API_KEY</code> / <code>ANTHROPIC_API_KEY</code> / <code>ANTHROPIC_AUTH_TOKEN</code>)>
|
|
179
179
|
<code>.env</code> 里写 <code>GATEWAY_KEY=sk-xxx</code> > <code>credentials.json</code>。
|
|
180
180
|
环境里给了密钥时,那个文件<b>不参与</b>(<code>config list</code> 的「来源」列会告诉你是哪一层在生效)。
|
|
181
|
+
<b>四个命令行入口(agent / openai / anthropic / claude-code)也读 <code>credentials.json</code></b>——
|
|
182
|
+
也就是说「路线一 / 二」配一次,Web 与命令行<b>都认</b>,不必为了命令行再手写 <code>.env</code>。
|
|
181
183
|
<code>.env</code> 的加载顺序是<b>①安装目录 → ②当前工作目录</b>;<b>npm 全局安装后包目录里只有
|
|
182
184
|
<code>.env.example</code></b>,所以要用 <code>.env</code> 就放在<b>你起服务的那个目录</b>:
|
|
183
185
|
<div class="code-block"><code># 在你干活的项目目录里(可选,不是必须)
|
|
@@ -217,6 +219,17 @@ gateway-agent config set mode auto # 新任务的默认审批模式</co
|
|
|
217
219
|
启动横幅会打印<b>网关 / 模型 / 密钥(脱敏 + 来源)/ 工具 / 存储 / 配置文件与密钥文件路径</b> ——
|
|
218
220
|
先对着这几行核一遍,比在页面上猜快:密钥那行写「未配置」时,同一段还有三条填法。
|
|
219
221
|
</li>
|
|
222
|
+
<li>
|
|
223
|
+
<b>每个值都带「来源」——看到有值不等于配过了</b>:横幅上写的是
|
|
224
|
+
<code>网关 http://127.0.0.1:9000(内置默认)</code>、<code>模型 qwen3:8b(内置默认)</code> 这样,
|
|
225
|
+
来源分四种:<b>内置默认</b> / <b>启动参数</b> / <b>环境变量(含 <code>.env</code>,会连文件路径一起写出来)</b> /
|
|
226
|
+
<b>配置文件 <code>config.json</code></b>。命令行 agent 的横幅同样带来源,并且有一行脱敏的密钥。
|
|
227
|
+
<div class="code-block"><code>[gateway] 原生 Agent · base=http://127.0.0.1:9000(内置默认) model=deepseek-v4-flash(环境变量 GATEWAY_MODEL(来自 .env:E:\proj\.env))
|
|
228
|
+
密钥 sk-abcd****wxyz(环境变量 GATEWAY_KEY(来自 .env:E:\proj\.env))</code></div>
|
|
229
|
+
这条信息专门用来破一个高频误会:<b>在别的项目目录里跑,会读到那个目录的 <code>.env</code></b>
|
|
230
|
+
(查找顺序是「安装目录 → 当前工作目录」),于是「我明明没配过,怎么已经有值了」。
|
|
231
|
+
全新机器第一次跑则是另一副样子:两个值都标「内置默认」,并多一句「这台机器还没有任何配置」+ 三条填法。
|
|
232
|
+
</li>
|
|
220
233
|
<li>打开 <code>http://127.0.0.1:3100/task</code> —— 你正在看的这一页就是它。</li>
|
|
221
234
|
<li>
|
|
222
235
|
<b>连不上网关时怎么判断</b>:<code>curl http://127.0.0.1:9000/v1/models</code>(Windows:<code>irm http://127.0.0.1:9000/v1/models</code>)
|