@armadra/agent 0.6.0 → 0.6.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/CHANGELOG.md +26 -0
- package/CHANGELOG.zh-CN.md +20 -0
- package/dist/ai/providers/discovered-cache.d.ts +48 -0
- package/dist/ai/providers/discovered-cache.js +102 -0
- package/dist/ai/providers/model-visibility.d.ts +33 -0
- package/dist/ai/providers/model-visibility.js +70 -0
- package/dist/ai/providers/registry.d.ts +2 -0
- package/dist/ai/providers/registry.js +6 -1
- package/dist/auth/chatgpt/cli.d.ts +3 -1
- package/dist/auth/chatgpt/cli.js +31 -4
- package/dist/bundle/ama.cjs +1343 -640
- package/dist/cli/compose-providers.d.ts +1 -0
- package/dist/cli/compose-providers.js +6 -0
- package/dist/cli/subcommands/config-set.js +19 -11
- package/dist/cli/subcommands/models-discover.d.ts +2 -1
- package/dist/cli/subcommands/models-discover.js +17 -1
- package/dist/cli/subcommands/models-enable.d.ts +13 -0
- package/dist/cli/subcommands/models-enable.js +145 -0
- package/dist/cli/subcommands/models.d.ts +2 -0
- package/dist/cli/subcommands/models.js +10 -2
- package/dist/config/json-schema.js +1 -1
- package/dist/config/schema-w5.js +9 -2
- package/dist/config/settings-registry.d.ts +3 -0
- package/dist/config/settings-registry.js +1 -0
- package/dist/config/types-w5.d.ts +7 -0
- package/dist/config/types-w5.js +2 -0
- package/dist/i18n/catalog.d.ts +43 -3
- package/dist/i18n/messages/auth.d.ts +4 -2
- package/dist/i18n/messages/auth.js +14 -2
- package/dist/i18n/messages/cli-help.js +4 -0
- package/dist/i18n/messages/config-keys.d.ts +4 -0
- package/dist/i18n/messages/config-keys.js +6 -2
- package/dist/i18n/messages/config.d.ts +6 -0
- package/dist/i18n/messages/config.js +2 -0
- package/dist/i18n/messages/errors.d.ts +1 -1
- package/dist/i18n/messages/interactive-startup.d.ts +0 -4
- package/dist/i18n/messages/interactive-startup.js +0 -4
- package/dist/i18n/messages/interactive.d.ts +0 -4
- package/dist/i18n/messages/panels.d.ts +43 -0
- package/dist/i18n/messages/panels.js +43 -0
- package/dist/i18n/messages/settings.d.ts +7 -0
- package/dist/i18n/messages/settings.js +7 -0
- package/dist/i18n/messages/subcommands-config.d.ts +2 -0
- package/dist/i18n/messages/subcommands-config.js +2 -0
- package/dist/i18n/messages/subcommands-models.d.ts +34 -0
- package/dist/i18n/messages/subcommands-models.js +34 -0
- package/dist/i18n/messages/subcommands.d.ts +28 -0
- package/dist/i18n/messages/subcommands.js +3 -0
- package/dist/modes/interactive/commands.d.ts +3 -0
- package/dist/modes/interactive/commands.js +18 -11
- package/dist/modes/interactive/config-ui.js +1 -1
- package/dist/modes/interactive/interactive-mode.js +2 -0
- package/dist/modes/interactive/model-items.d.ts +45 -0
- package/dist/modes/interactive/model-items.js +134 -0
- package/dist/modes/interactive/model-picker.d.ts +57 -0
- package/dist/modes/interactive/model-picker.js +191 -0
- package/dist/modes/interactive/startup-ui.d.ts +3 -9
- package/dist/modes/interactive/startup-ui.js +8 -60
- package/dist/tui/components/select-list.d.ts +2 -0
- package/dist/tui/components/select-list.js +7 -0
- package/docs/en/providers.md +2 -0
- package/docs/en/tui.md +8 -1
- package/docs/providers.md +6 -0
- package/docs/tui.md +8 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,32 @@ English · [简体中文](CHANGELOG.zh-CN.md)
|
|
|
5
5
|
> This file is in English starting with 0.6.0. Release notes for 0.1 through 0.5.1 are in Chinese in
|
|
6
6
|
> [CHANGELOG.zh-CN.md](CHANGELOG.zh-CN.md). New entries go into both files.
|
|
7
7
|
|
|
8
|
+
## 0.6.1 (2026-10-03)
|
|
9
|
+
|
|
10
|
+
- **The model picker shows only configured models; ChatGPT models can be picked**: `/model` (and the startup picker and
|
|
11
|
+
`/config` model items) lists only providers with a key, a valid OAuth sign-in or a local server, one row per model on
|
|
12
|
+
multi-channel providers (other channels in the description; type `@` in the filter for `model@channel` rows); the current
|
|
13
|
+
model is pinned when it is not listed. `Tab` switches to "all" (unconfigured providers marked "no key configured", with an
|
|
14
|
+
`ama auth set` hint), `Space` adds / removes the highlighted model to / from the new user-level `models.enabled` list
|
|
15
|
+
(`provider/model[@channel]`, `provider/*`), which, once set, is all the picker shows; `ama models enable | disable` and
|
|
16
|
+
`ama models list --enabled` edit and show it. `ama auth login chatgpt` now fetches the models available to the account
|
|
17
|
+
(read-only, no usage) into `<dataDir>/models/discovered/chatgpt.json`, `ama models discover chatgpt` rewrites it and
|
|
18
|
+
logout deletes it; the registry merges the cache into providers with an empty model table, so ChatGPT models appear in
|
|
19
|
+
`/model` and `ama models list`. Note: with only a ChatGPT sign-in and no `defaultModel`, the first cached ChatGPT model can
|
|
20
|
+
now be chosen as the default model. Docs: docs/en/tui.md, docs/en/providers.md "ChatGPT login".
|
|
21
|
+
|
|
22
|
+
- **`ama auth login chatgpt` explains a refused sign-in**: when the OAuth callback carries `error=access_denied` (or another
|
|
23
|
+
error) the message now lists the likely causes (the authorization page was cancelled or plan usage was not checked; the
|
|
24
|
+
account / plan is not eligible — sharing is Plus / Pro only, Team / Enterprise workspaces may not offer it, the browser may be
|
|
25
|
+
signed in to another account; the region is not supported) and, for siwc, suggests the fallback
|
|
26
|
+
`ama auth login chatgpt --flavor codex`. Nothing is retried or switched automatically.
|
|
27
|
+
|
|
28
|
+
- **`ama config set` reports the written value when a higher layer overrides it**: with e.g. `AMA_LANG=zh`,
|
|
29
|
+
`ama config set ui.language en` used to print the effective value `zh` labelled "(user)". The first line is now the value just
|
|
30
|
+
written and its layer (`ui.language = en (written to user)`), and the second line names the override and the effective value
|
|
31
|
+
(`Still overridden by AMA_LANG (zh); the effective value is zh`). `ama config get` still shows the effective value and its
|
|
32
|
+
real source.
|
|
33
|
+
|
|
8
34
|
## 0.6.0 (2026-10-03)
|
|
9
35
|
|
|
10
36
|
Wave 6: the agent bar and sub-agent view, traces, memory, ChatGPT login, the `/config` settings panel, and a bilingual
|
package/CHANGELOG.zh-CN.md
CHANGED
|
@@ -4,6 +4,26 @@
|
|
|
4
4
|
|
|
5
5
|
> 从 0.6.0 起 [CHANGELOG.md](CHANGELOG.md) 为英文,本文件保留中文记录(0.1–0.5.1 的完整历史在此)。新条目两份都要加。
|
|
6
6
|
|
|
7
|
+
## 0.6.1(2026-10-03)
|
|
8
|
+
|
|
9
|
+
- **模型选择器只显示已配置的模型,ChatGPT 模型可选**:`/model`(以及启动选择器、`/config` 的模型项)只列有 key、OAuth 已登录或
|
|
10
|
+
本地服务的供应商;多渠道供应商每个模型一行(其它渠道写在说明里,筛选文本输入 `@` 列出 `模型@渠道` 行);当前模型不在列表里时
|
|
11
|
+
置顶。`Tab` 切到「全部」(没配置的供应商标「未配置 key」,选中时提示 `ama auth set`),`Space` 把高亮的模型加入 / 移出新的
|
|
12
|
+
用户级清单 `models.enabled`(`provider/model[@channel]`、`provider/*`),设置后选择器只列清单内的模型;`ama models enable |
|
|
13
|
+
disable` 与 `ama models list --enabled` 在命令行编辑和查看。`ama auth login chatgpt` 成功后拉取账户可用的模型(只读、不消耗
|
|
14
|
+
额度)缓存到 `<dataDir>/models/discovered/chatgpt.json`,`ama models discover chatgpt` 重写、logout 删除;注册表把缓存并入模型
|
|
15
|
+
表为空的供应商,ChatGPT 模型因此出现在 `/model` 与 `ama models list` 里。注意:只登录了 ChatGPT、没设 `defaultModel` 时,
|
|
16
|
+
缺省模型现在可能选中缓存里的第一个 ChatGPT 模型。文档:docs/tui.md、docs/providers.md「ChatGPT 登录」。
|
|
17
|
+
|
|
18
|
+
- **`ama auth login chatgpt` 授权被拒时说明原因**:OAuth 回调带 `error=access_denied`(或其它 error)时,提示改为列出可能原因
|
|
19
|
+
(在授权页取消或没勾选使用 ChatGPT 套餐额度;账户 / 套餐不符合——额度共享只对 Plus / Pro 开放,Team / Enterprise 工作空间可能
|
|
20
|
+
未开放,浏览器可能登录了别的账户;所在地区受限),siwc 下另提示可改用备用 `ama auth login chatgpt --flavor codex`。不自动重试、
|
|
21
|
+
不自动换 flavor。
|
|
22
|
+
|
|
23
|
+
- **`ama config set` 被更高优先级来源覆盖时,回执显示刚写入的值**:如 `AMA_LANG=zh` 下 `ama config set ui.language en`,原来
|
|
24
|
+
第一行显示覆盖后的生效值 `zh` 却标「(用户级)」。现在第一行是刚写入的值与写入层(`ui.language = en(已写入用户级)`),第二行
|
|
25
|
+
提示覆盖来源与生效值(`当前仍被 AMA_LANG(zh)覆盖,生效值为 zh`)。`ama config get` 仍显示生效值与真实来源。
|
|
26
|
+
|
|
7
27
|
## 0.6.0(2026-10-03)
|
|
8
28
|
|
|
9
29
|
第六波:Agent 栏与子 Agent 视图、轨迹、记忆(Memory)、ChatGPT 登录、`/config` 设置面板、中英双语界面。设计依据与决定表见
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 发现得到的模型列表缓存:`<dataDir>/models/discovered/<provider>.json`。
|
|
3
|
+
*
|
|
4
|
+
* 模型表为空、接受任意 slug 的供应商(`chatgpt` 的订阅后端)在选择器里原本一个模型都看不到。
|
|
5
|
+
* `ama auth login chatgpt` 成功后与 `ama models discover chatgpt` 把账户可用的 slug 写进这里;组装注册表时
|
|
6
|
+
* (`mergeDiscoveredModels`)并入**模型表仍为空**的供应商,元数据用 models.dev(快照 ⊕ 用户刷新)补全。
|
|
7
|
+
* `ama auth logout chatgpt` 删掉它。只存 slug、显示名、flavor 与时间戳——没有 token、账户 id。
|
|
8
|
+
*/
|
|
9
|
+
import type { Model, ProviderData } from "../types.js";
|
|
10
|
+
import type { ModelsDevIndex } from "./models-dev.js";
|
|
11
|
+
export declare const DISCOVERED_CACHE_VERSION = 1;
|
|
12
|
+
export interface DiscoveredCacheFile {
|
|
13
|
+
version: number;
|
|
14
|
+
provider: string;
|
|
15
|
+
/** ISO 时间。 */
|
|
16
|
+
fetchedAt: string;
|
|
17
|
+
/** ChatGPT 登录的 flavor(siwc / codex);其它供应商不写。 */
|
|
18
|
+
flavor?: string;
|
|
19
|
+
models: {
|
|
20
|
+
id: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
}[];
|
|
23
|
+
}
|
|
24
|
+
/** 缓存文件位置。 */
|
|
25
|
+
export declare function discoveredCachePath(dataDir: string, provider: string): string;
|
|
26
|
+
/** 读缓存;不存在、损坏或形状不对返回 undefined(不报错,选择器给提示行)。 */
|
|
27
|
+
export declare function readDiscoveredCache(dataDir: string, provider: string): DiscoveredCacheFile | undefined;
|
|
28
|
+
/** 写缓存(先写临时文件再改名);返回路径。 */
|
|
29
|
+
export declare function writeDiscoveredCache(dataDir: string, provider: string, input: {
|
|
30
|
+
models: readonly {
|
|
31
|
+
id: string;
|
|
32
|
+
name?: string | undefined;
|
|
33
|
+
}[];
|
|
34
|
+
flavor?: string | undefined;
|
|
35
|
+
}, now?: number): string;
|
|
36
|
+
/** 删缓存;删了返回 true。 */
|
|
37
|
+
export declare function clearDiscoveredCache(dataDir: string, provider: string): boolean;
|
|
38
|
+
/** 缓存里的模型 → 注册表模型(models.dev 补元数据,补不到用自定义缺省)。 */
|
|
39
|
+
export declare function discoveredModels(file: DiscoveredCacheFile, provider: ProviderData, modelsDev: () => ModelsDevIndex | undefined): Model[];
|
|
40
|
+
/** 注册表里能追加模型的最小接口(`ProviderRegistry.addModels`)。 */
|
|
41
|
+
export interface DiscoveredTarget {
|
|
42
|
+
list(): readonly ProviderData[];
|
|
43
|
+
addModels(providerId: string, models: readonly Model[], source?: "discovered"): void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 把缓存并入模型表为空的供应商(本地服务除外:它们每次启动现场探测)。返回并入了的供应商 id。
|
|
47
|
+
*/
|
|
48
|
+
export declare function mergeDiscoveredModels(registry: DiscoveredTarget, dataDir: string, modelsDev: () => ModelsDevIndex | undefined): string[];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 发现得到的模型列表缓存:`<dataDir>/models/discovered/<provider>.json`。
|
|
3
|
+
*
|
|
4
|
+
* 模型表为空、接受任意 slug 的供应商(`chatgpt` 的订阅后端)在选择器里原本一个模型都看不到。
|
|
5
|
+
* `ama auth login chatgpt` 成功后与 `ama models discover chatgpt` 把账户可用的 slug 写进这里;组装注册表时
|
|
6
|
+
* (`mergeDiscoveredModels`)并入**模型表仍为空**的供应商,元数据用 models.dev(快照 ⊕ 用户刷新)补全。
|
|
7
|
+
* `ama auth logout chatgpt` 删掉它。只存 slug、显示名、flavor 与时间戳——没有 token、账户 id。
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { withCustomDefaults } from "./catalog.js";
|
|
12
|
+
import { enrichEntry } from "./enrich.js";
|
|
13
|
+
export const DISCOVERED_CACHE_VERSION = 1;
|
|
14
|
+
/** 缓存文件位置。 */
|
|
15
|
+
export function discoveredCachePath(dataDir, provider) {
|
|
16
|
+
return join(dataDir, "models", "discovered", `${provider}.json`);
|
|
17
|
+
}
|
|
18
|
+
function isEntry(value) {
|
|
19
|
+
if (typeof value !== "object" || value === null)
|
|
20
|
+
return false;
|
|
21
|
+
const { id, name } = value;
|
|
22
|
+
return typeof id === "string" && id !== "" && (name === undefined || typeof name === "string");
|
|
23
|
+
}
|
|
24
|
+
/** 读缓存;不存在、损坏或形状不对返回 undefined(不报错,选择器给提示行)。 */
|
|
25
|
+
export function readDiscoveredCache(dataDir, provider) {
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = JSON.parse(readFileSync(discoveredCachePath(dataDir, provider), "utf8"));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
34
|
+
return undefined;
|
|
35
|
+
const file = parsed;
|
|
36
|
+
if (file.provider !== provider || !Array.isArray(file.models))
|
|
37
|
+
return undefined;
|
|
38
|
+
return {
|
|
39
|
+
version: typeof file.version === "number" ? file.version : DISCOVERED_CACHE_VERSION,
|
|
40
|
+
provider,
|
|
41
|
+
fetchedAt: typeof file.fetchedAt === "string" ? file.fetchedAt : "",
|
|
42
|
+
...(typeof file.flavor === "string" ? { flavor: file.flavor } : {}),
|
|
43
|
+
models: file.models
|
|
44
|
+
.filter(isEntry)
|
|
45
|
+
.map((m) => (m.name ? { id: m.id, name: m.name } : { id: m.id })),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** 写缓存(先写临时文件再改名);返回路径。 */
|
|
49
|
+
export function writeDiscoveredCache(dataDir, provider, input, now = Date.now()) {
|
|
50
|
+
const path = discoveredCachePath(dataDir, provider);
|
|
51
|
+
const file = {
|
|
52
|
+
version: DISCOVERED_CACHE_VERSION,
|
|
53
|
+
provider,
|
|
54
|
+
fetchedAt: new Date(now).toISOString(),
|
|
55
|
+
...(input.flavor !== undefined ? { flavor: input.flavor } : {}),
|
|
56
|
+
models: input.models.map((m) => (m.name ? { id: m.id, name: m.name } : { id: m.id })),
|
|
57
|
+
};
|
|
58
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
59
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
60
|
+
writeFileSync(tmp, `${JSON.stringify(file, null, 2)}\n`);
|
|
61
|
+
renameSync(tmp, path);
|
|
62
|
+
return path;
|
|
63
|
+
}
|
|
64
|
+
/** 删缓存;删了返回 true。 */
|
|
65
|
+
export function clearDiscoveredCache(dataDir, provider) {
|
|
66
|
+
const path = discoveredCachePath(dataDir, provider);
|
|
67
|
+
if (!existsSync(path))
|
|
68
|
+
return false;
|
|
69
|
+
rmSync(path, { force: true });
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
/** 缓存里的模型 → 注册表模型(models.dev 补元数据,补不到用自定义缺省)。 */
|
|
73
|
+
export function discoveredModels(file, provider, modelsDev) {
|
|
74
|
+
return file.models.map((m) => {
|
|
75
|
+
const { entry } = enrichEntry(m.name ? { id: m.id, name: m.name } : { id: m.id }, modelsDev);
|
|
76
|
+
const model = withCustomDefaults(entry, provider.id, provider.api);
|
|
77
|
+
// 只挂发现时登录的 flavor 对应的渠道(订阅后端的另一条渠道用不了,不在说明与 `@` 行里出现);
|
|
78
|
+
// 缓存没写 flavor 或渠道不存在时用供应商的缺省渠道
|
|
79
|
+
const names = (provider.channels ?? []).map((c) => c.name);
|
|
80
|
+
if (names.length > 0) {
|
|
81
|
+
const own = file.flavor !== undefined && names.includes(file.flavor) ? file.flavor : undefined;
|
|
82
|
+
model.channels = [own ?? provider.defaultChannel ?? names[0]];
|
|
83
|
+
}
|
|
84
|
+
return model;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 把缓存并入模型表为空的供应商(本地服务除外:它们每次启动现场探测)。返回并入了的供应商 id。
|
|
89
|
+
*/
|
|
90
|
+
export function mergeDiscoveredModels(registry, dataDir, modelsDev) {
|
|
91
|
+
const merged = [];
|
|
92
|
+
for (const provider of registry.list()) {
|
|
93
|
+
if (provider.models.length > 0 || !provider.requiresApiKey)
|
|
94
|
+
continue;
|
|
95
|
+
const file = readDiscoveredCache(dataDir, provider.id);
|
|
96
|
+
if (file === undefined || file.models.length === 0)
|
|
97
|
+
continue;
|
|
98
|
+
registry.addModels(provider.id, discoveredModels(file, provider, modelsDev), "discovered");
|
|
99
|
+
merged.push(provider.id);
|
|
100
|
+
}
|
|
101
|
+
return merged;
|
|
102
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型选择器的可见性规则(`/model`、启动选择器、`/config` 的模型项、`ama models list --enabled` 共用)。
|
|
3
|
+
*
|
|
4
|
+
* - 供应商的可用状态:本地(`requiresApiKey: false`)、有 key、OAuth 已登录、OAuth 需重新登录、没配置;
|
|
5
|
+
* 前三种算「已配置」,缺省只显示它们的模型。
|
|
6
|
+
* - `models.enabled`(用户级):`provider/model[@channel]` 或 `provider/*`;设置后只显示清单内的模型。
|
|
7
|
+
* 清单为空等同不设(写回时删掉这个键)。
|
|
8
|
+
*/
|
|
9
|
+
import type { ProviderData, ProviderRegistryApi } from "../types.js";
|
|
10
|
+
export type ProviderAccess = "local" | "key" | "oauth" | "needsLogin" | "none";
|
|
11
|
+
/** 解析 key 判断供应商的可用状态(OAuth 条目会按需刷新,与发请求前相同)。 */
|
|
12
|
+
export declare function providerAccess(registry: ProviderRegistryApi, provider: ProviderData): Promise<ProviderAccess>;
|
|
13
|
+
export declare function accessReady(access: ProviderAccess): boolean;
|
|
14
|
+
/** `provider/model@channel` → 三段(模型 id 里可以有 `/`)。 */
|
|
15
|
+
export declare function splitEnabledRef(ref: string): {
|
|
16
|
+
provider: string;
|
|
17
|
+
model: string;
|
|
18
|
+
channel?: string;
|
|
19
|
+
} | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* 清单是否包含该行:`exact` 是逐字列出(`provider/model`,渠道行要 `provider/model@channel`),
|
|
22
|
+
* `wildcard` 只经 `provider/*` 匹配上。
|
|
23
|
+
*/
|
|
24
|
+
export declare function enabledMatch(list: readonly string[], ref: string): {
|
|
25
|
+
kind: "exact" | "wildcard";
|
|
26
|
+
pattern: string;
|
|
27
|
+
} | undefined;
|
|
28
|
+
/** 追加(去重、保持顺序)。 */
|
|
29
|
+
export declare function addEnabled(list: readonly string[] | undefined, refs: readonly string[]): string[];
|
|
30
|
+
/** 移出;移空返回 undefined(删掉键,恢复「显示全部已配置」)。 */
|
|
31
|
+
export declare function removeEnabled(list: readonly string[] | undefined, refs: readonly string[]): string[] | undefined;
|
|
32
|
+
/** 配置里的清单:非数组或空数组视为未设置。 */
|
|
33
|
+
export declare function enabledList(value: unknown): readonly string[] | undefined;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模型选择器的可见性规则(`/model`、启动选择器、`/config` 的模型项、`ama models list --enabled` 共用)。
|
|
3
|
+
*
|
|
4
|
+
* - 供应商的可用状态:本地(`requiresApiKey: false`)、有 key、OAuth 已登录、OAuth 需重新登录、没配置;
|
|
5
|
+
* 前三种算「已配置」,缺省只显示它们的模型。
|
|
6
|
+
* - `models.enabled`(用户级):`provider/model[@channel]` 或 `provider/*`;设置后只显示清单内的模型。
|
|
7
|
+
* 清单为空等同不设(写回时删掉这个键)。
|
|
8
|
+
*/
|
|
9
|
+
import { liveToken } from "../../auth/oauth/live.js";
|
|
10
|
+
/** 解析 key 判断供应商的可用状态(OAuth 条目会按需刷新,与发请求前相同)。 */
|
|
11
|
+
export async function providerAccess(registry, provider) {
|
|
12
|
+
if (!provider.requiresApiKey)
|
|
13
|
+
return "local";
|
|
14
|
+
const key = await registry
|
|
15
|
+
.resolveApiKey(provider.id)
|
|
16
|
+
.catch(() => ({ apiKey: undefined, source: "none" }));
|
|
17
|
+
if (key.apiKey === undefined)
|
|
18
|
+
return "none";
|
|
19
|
+
if (key.source !== "oauth")
|
|
20
|
+
return "key";
|
|
21
|
+
return liveToken(key.apiKey)?.needsLogin === true ? "needsLogin" : "oauth";
|
|
22
|
+
}
|
|
23
|
+
export function accessReady(access) {
|
|
24
|
+
return access === "local" || access === "key" || access === "oauth";
|
|
25
|
+
}
|
|
26
|
+
/** `provider/model@channel` → 三段(模型 id 里可以有 `/`)。 */
|
|
27
|
+
export function splitEnabledRef(ref) {
|
|
28
|
+
const slash = ref.indexOf("/");
|
|
29
|
+
if (slash <= 0 || slash === ref.length - 1)
|
|
30
|
+
return undefined;
|
|
31
|
+
const provider = ref.slice(0, slash);
|
|
32
|
+
const rest = ref.slice(slash + 1);
|
|
33
|
+
const at = rest.lastIndexOf("@");
|
|
34
|
+
if (at > 0)
|
|
35
|
+
return { provider, model: rest.slice(0, at), channel: rest.slice(at + 1) };
|
|
36
|
+
return { provider, model: rest };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 清单是否包含该行:`exact` 是逐字列出(`provider/model`,渠道行要 `provider/model@channel`),
|
|
40
|
+
* `wildcard` 只经 `provider/*` 匹配上。
|
|
41
|
+
*/
|
|
42
|
+
export function enabledMatch(list, ref) {
|
|
43
|
+
if (list.includes(ref))
|
|
44
|
+
return { kind: "exact", pattern: ref };
|
|
45
|
+
const parts = splitEnabledRef(ref);
|
|
46
|
+
if (parts === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
const wildcard = `${parts.provider}/*`;
|
|
49
|
+
return list.includes(wildcard) ? { kind: "wildcard", pattern: wildcard } : undefined;
|
|
50
|
+
}
|
|
51
|
+
/** 追加(去重、保持顺序)。 */
|
|
52
|
+
export function addEnabled(list, refs) {
|
|
53
|
+
const out = [...(list ?? [])];
|
|
54
|
+
for (const ref of refs)
|
|
55
|
+
if (!out.includes(ref))
|
|
56
|
+
out.push(ref);
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
/** 移出;移空返回 undefined(删掉键,恢复「显示全部已配置」)。 */
|
|
60
|
+
export function removeEnabled(list, refs) {
|
|
61
|
+
const out = (list ?? []).filter((ref) => !refs.includes(ref));
|
|
62
|
+
return out.length > 0 ? out : undefined;
|
|
63
|
+
}
|
|
64
|
+
/** 配置里的清单:非数组或空数组视为未设置。 */
|
|
65
|
+
export function enabledList(value) {
|
|
66
|
+
if (!Array.isArray(value))
|
|
67
|
+
return undefined;
|
|
68
|
+
const list = value.filter((x) => typeof x === "string");
|
|
69
|
+
return list.length > 0 ? list : undefined;
|
|
70
|
+
}
|
|
@@ -63,6 +63,8 @@ export declare class ProviderRegistry implements ProviderRegistryApi {
|
|
|
63
63
|
private readonly envBaseUrls;
|
|
64
64
|
/** baseUrl 指向非官方主机的内置供应商:接受目录外的 model id。 */
|
|
65
65
|
private readonly relayed;
|
|
66
|
+
/** 模型表只来自发现缓存的供应商:表外的 id 照样合成(与空表相同)。 */
|
|
67
|
+
private readonly openTables;
|
|
66
68
|
/** config 里写了自己 `channels` 的供应商(内置渠道不因改 baseUrl 作废)。 */
|
|
67
69
|
private readonly userChannels;
|
|
68
70
|
/** config 里写了供应商级 `api` 的供应商(单渠道回落时目录模型跟它走)。 */
|
|
@@ -57,6 +57,8 @@ export class ProviderRegistry {
|
|
|
57
57
|
envBaseUrls = new Map();
|
|
58
58
|
/** baseUrl 指向非官方主机的内置供应商:接受目录外的 model id。 */
|
|
59
59
|
relayed = new Set();
|
|
60
|
+
/** 模型表只来自发现缓存的供应商:表外的 id 照样合成(与空表相同)。 */
|
|
61
|
+
openTables = new Set();
|
|
60
62
|
/** config 里写了自己 `channels` 的供应商(内置渠道不因改 baseUrl 作废)。 */
|
|
61
63
|
userChannels = new Set();
|
|
62
64
|
/** config 里写了供应商级 `api` 的供应商(单渠道回落时目录模型跟它走)。 */
|
|
@@ -344,6 +346,8 @@ export class ProviderRegistry {
|
|
|
344
346
|
const provider = this.providers.get(providerId);
|
|
345
347
|
if (!provider)
|
|
346
348
|
return;
|
|
349
|
+
if (source === "discovered" && provider.models.length === 0 && provider.requiresApiKey)
|
|
350
|
+
this.openTables.add(providerId);
|
|
347
351
|
for (const raw of models) {
|
|
348
352
|
this.raw.delete(`${providerId}/${raw.id}`);
|
|
349
353
|
const model = this.materialize(provider, { ...raw, provider: providerId });
|
|
@@ -408,7 +412,8 @@ export class ProviderRegistry {
|
|
|
408
412
|
const model = provider.models.find((m) => m.id === id);
|
|
409
413
|
if (model)
|
|
410
414
|
return { ok: true, model, provider };
|
|
411
|
-
const
|
|
415
|
+
const open = this.relayed.has(provider.id) || this.openTables.has(provider.id);
|
|
416
|
+
const relayed = provider.models.length === 0 || open;
|
|
412
417
|
if (synthesize && relayed && id.length > 0) {
|
|
413
418
|
const synthesized = this.synthesize(provider, id);
|
|
414
419
|
if (channel !== undefined && provider.channels?.some((c) => c.name === channel))
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* - login:flavor 取 `--flavor` > 用户级 `auth.chatgpt.flavor` > siwc;`--paste` / `--device`(只 codex)/ 缺省
|
|
5
5
|
* 浏览器;codex 首次在 TTY 下一次性确认「非官方、仅个人使用」(非 TTY 需 `--yes`),`acknowledgedAt` 入条目;
|
|
6
6
|
* 成功后在刷新锁下写 auth.json(0600)。
|
|
7
|
-
*
|
|
7
|
+
* 成功后查一次模型列表写发现缓存(`<dataDir>/models/discovered/chatgpt.json`,失败静默)。
|
|
8
|
+
* - logout:SIWC 先撤销 refresh token(失败照样删本地,提示到 ChatGPT 设置里断开);codex 只删本地;
|
|
9
|
+
* 同时删发现缓存。
|
|
8
10
|
* - status:flavor、计划、掩码邮箱、access token 剩余、needsLogin;codex 另查 `wham/usage` 显示配额。
|
|
9
11
|
*
|
|
10
12
|
* 输出里从不出现 token、code、账户 id;错误按码渲染(i18n),不回显响应原文。
|
package/dist/auth/chatgpt/cli.js
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* - login:flavor 取 `--flavor` > 用户级 `auth.chatgpt.flavor` > siwc;`--paste` / `--device`(只 codex)/ 缺省
|
|
5
5
|
* 浏览器;codex 首次在 TTY 下一次性确认「非官方、仅个人使用」(非 TTY 需 `--yes`),`acknowledgedAt` 入条目;
|
|
6
6
|
* 成功后在刷新锁下写 auth.json(0600)。
|
|
7
|
-
*
|
|
7
|
+
* 成功后查一次模型列表写发现缓存(`<dataDir>/models/discovered/chatgpt.json`,失败静默)。
|
|
8
|
+
* - logout:SIWC 先撤销 refresh token(失败照样删本地,提示到 ChatGPT 设置里断开);codex 只删本地;
|
|
9
|
+
* 同时删发现缓存。
|
|
8
10
|
* - status:flavor、计划、掩码邮箱、access token 剩余、needsLogin;codex 另查 `wham/usage` 显示配额。
|
|
9
11
|
*
|
|
10
12
|
* 输出里从不出现 token、code、账户 id;错误按码渲染(i18n),不回显响应原文。
|
|
@@ -19,7 +21,8 @@ import { UsageError } from "../../cli/args.js";
|
|
|
19
21
|
import { ExitCode } from "../../cli/exit-codes.js";
|
|
20
22
|
import { openBrowser } from "../oauth/browser.js";
|
|
21
23
|
import { readOAuthEntry, withRefreshLock, writeOAuthEntry } from "../oauth/token-store.js";
|
|
22
|
-
import {
|
|
24
|
+
import { clearDiscoveredCache, writeDiscoveredCache } from "../../ai/providers/discovered-cache.js";
|
|
25
|
+
import { fetchCodexUsage, listChatGptModels } from "./backend-client.js";
|
|
23
26
|
import { maskEmail } from "./claims.js";
|
|
24
27
|
import { loginChatGpt, revokeChatGpt } from "./login.js";
|
|
25
28
|
import { CHATGPT_PROVIDER_ID, chatgptBaseUrl } from "./presets.js";
|
|
@@ -55,7 +58,7 @@ export function loginErrorText(error, flavor) {
|
|
|
55
58
|
case "oauth_timeout":
|
|
56
59
|
return m.timeout;
|
|
57
60
|
case "oauth_denied":
|
|
58
|
-
return m.denied(detail.error ?? "error");
|
|
61
|
+
return m.denied(detail.error ?? "error", flavor === "siwc");
|
|
59
62
|
case "oauth_state_mismatch":
|
|
60
63
|
return m.stateMismatch;
|
|
61
64
|
case "oauth_no_code":
|
|
@@ -184,9 +187,32 @@ export async function runLogin(args, io, deps = {}) {
|
|
|
184
187
|
io.stdout(`${m.freePlanHint(entry.planType)}\n`);
|
|
185
188
|
if (flavor === "siwc")
|
|
186
189
|
io.stdout(`${m.siwcLimitHint}\n`);
|
|
187
|
-
io
|
|
190
|
+
const count = await cacheModels(io, entry, config, deps);
|
|
191
|
+
io.stdout(`${count === undefined ? m.modelHint : m.modelsCached(count)}\n`);
|
|
188
192
|
return ExitCode.Ok;
|
|
189
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* 登录后查一次账户可用的模型(只读的模型列表接口,不消耗额度)并写发现缓存,`/model` 由此列出;
|
|
196
|
+
* 任何失败静默(返回 undefined,调用方改提示 `ama models discover chatgpt`)。
|
|
197
|
+
*/
|
|
198
|
+
async function cacheModels(io, entry, config, deps) {
|
|
199
|
+
try {
|
|
200
|
+
const models = await listChatGptModels(deps.fetch ?? fetch, chatgptBaseUrl(entry.flavor, io.env), {
|
|
201
|
+
flavor: entry.flavor,
|
|
202
|
+
accessToken: entry.accessToken,
|
|
203
|
+
accountId: entry.accountId,
|
|
204
|
+
originator: config?.originator,
|
|
205
|
+
});
|
|
206
|
+
if (models.length === 0)
|
|
207
|
+
return undefined;
|
|
208
|
+
const dataDir = resolveDataDir({ env: io.env });
|
|
209
|
+
writeDiscoveredCache(dataDir, CHATGPT_PROVIDER_ID, { models, flavor: entry.flavor });
|
|
210
|
+
return models.length;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
190
216
|
export async function runLogout(args, io, deps = {}) {
|
|
191
217
|
const m = msg().auth.logout;
|
|
192
218
|
const provider = providerOf(args, "logout");
|
|
@@ -205,6 +231,7 @@ export async function runLogout(args, io, deps = {}) {
|
|
|
205
231
|
io.stderr(`ama: ${loginErrorText(error, entry.flavor)}\n`);
|
|
206
232
|
return ExitCode.RuntimeError;
|
|
207
233
|
}
|
|
234
|
+
clearDiscoveredCache(resolveDataDir({ env: io.env }), provider);
|
|
208
235
|
const text = revoked === undefined
|
|
209
236
|
? m.local(provider, args.path)
|
|
210
237
|
: revoked
|