@coclaw/openclaw-coclaw 0.25.0 → 0.26.0
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/index.js +5 -2
- package/package.json +5 -1
- package/src/model-default/handlers.js +27 -27
- package/src/model-default/index.js +11 -7
- package/src/model-default/resolve.js +41 -11
- package/src/provider-auth/handlers.js +87 -3
- package/src/provider-auth/index.js +41 -3
package/index.js
CHANGED
|
@@ -810,11 +810,14 @@ const plugin = {
|
|
|
810
810
|
loadSdk: () => import('openclaw/plugin-sdk/provider-auth'),
|
|
811
811
|
loadConfigMutation: () => import('openclaw/plugin-sdk/config-mutation'),
|
|
812
812
|
// provider-catalog-runtime 供通用 device-code 扫码登录(B1)拿 resolvePluginProviders,
|
|
813
|
-
// 驱动 provider 自带的 device_code 登录方法(codex/copilot 及以后任意 device_code provider
|
|
813
|
+
// 驱动 provider 自带的 device_code 登录方法(codex/copilot 及以后任意 device_code provider);
|
|
814
|
+
// 同时供 providerAuth.catalog 拿 setup 全集(mode:'setup')
|
|
814
815
|
loadProviderCatalogRuntime: () => import('openclaw/plugin-sdk/provider-catalog-runtime'),
|
|
816
|
+
// agent-runtime 供 providerAuth.catalog 的 hasCred 别名归一基座 id(resolveProviderIdForAuth)
|
|
817
|
+
loadAgentRuntime: () => import('openclaw/plugin-sdk/agent-runtime'),
|
|
815
818
|
});
|
|
816
819
|
|
|
817
|
-
// 模型默认配置 RPC(coclaw.model.set / list / listUsable
|
|
820
|
+
// 模型默认配置 RPC(coclaw.model.set / list / listAvailable + listUsable 过渡别名)。三个 SDK 子入口的字面量
|
|
818
821
|
// dynamic import 必须留在本入口源码——OpenClaw plugin loader 只扫入口源码
|
|
819
822
|
// 命中 `openclaw/plugin-sdk/*` 字面量并触发 jiti 重写;藏在子模块的字面量
|
|
820
823
|
// loader 看不到 → 原生 Node 解析必败。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coclaw/openclaw-coclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "OpenClaw plugin for remote chat over WebRTC. Run `openclaw coclaw enroll` after install.",
|
|
@@ -55,6 +55,10 @@
|
|
|
55
55
|
"verify": "pnpm check && pnpm test",
|
|
56
56
|
"link": "bash ./scripts/link.sh",
|
|
57
57
|
"unlink": "bash ./scripts/unlink.sh",
|
|
58
|
+
"wt:up": "bash ./scripts/worktree-gateway.sh up",
|
|
59
|
+
"wt:reload": "bash ./scripts/worktree-gateway.sh reload",
|
|
60
|
+
"wt:call": "bash ./scripts/worktree-gateway.sh call",
|
|
61
|
+
"wt:down": "bash ./scripts/worktree-gateway.sh down",
|
|
58
62
|
"install:npm": "bash ./scripts/install-npm.sh",
|
|
59
63
|
"uninstall:npm": "bash ./scripts/uninstall-npm.sh",
|
|
60
64
|
"release:pre": "bash ./scripts/prerelease.sh",
|
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* model-default/handlers.js —— coclaw.model.set / list /
|
|
2
|
+
* model-default/handlers.js —— coclaw.model.set / list / listAvailable 三个 RPC 的纯函数实现
|
|
3
|
+
* (listAvailable 即原 listUsable 改名;index.js 把 listUsable 留作过渡别名映到同一 handler)
|
|
3
4
|
*
|
|
4
5
|
* 设计要点(详见 docs/model-config-api.md § 3):
|
|
5
6
|
* - DI 注入 sdk(mutateConfigFile / loadModelCatalog / provider-auth 凭据探针 / resolveProviderIdForAuth)
|
|
6
7
|
* + loadConfig + resolveAgentDir,便于单测;产线注入在 ./index.js
|
|
7
8
|
* - **出参不加 status wrap**(gateway-method-design skill 约定):set → {};list → { default, agents };
|
|
8
|
-
*
|
|
9
|
+
* listAvailable → { byProvider }(configuredProviders 已迁出,UI 加 provider 排除改吃 catalog.hasCred)
|
|
9
10
|
* - 错误码只用 INVALID_ARGS / IO_FAILED,参考 provider-auth/handlers.js
|
|
10
11
|
* 既有 plugin 的 respondError 用 INTERNAL_ERROR 与本节契约不一致,所以本模块自带局部 helper
|
|
11
12
|
* - set 校验 fail-fast 顺序:params shape → 拒未知字段 → agentId → primary 类型 → primary 形态
|
|
12
13
|
* (纯字符串:含 '/'、'/' 不在端点;不依赖 cfg)→ loadConfig → 凭据门 → 存在性
|
|
13
14
|
* 形态校验**前置在 loadConfig 之前**,cfg 不可读时非法形态仍是 INVALID_ARGS 而非 IO_FAILED
|
|
14
15
|
* - 凭据门 + 选模型器枚举 + list 信号全部走统一别名感知原语(resolve.js),杜绝跨界面口径分叉(§ 3.2.1)
|
|
15
|
-
* - set 存在性 +
|
|
16
|
+
* - set 存在性 + listAvailable 枚举走同一目录源 loadModelCatalog({readOnly:false}):选得到 ⇒ 设得上(红线天然成立)。
|
|
17
|
+
* 用 readOnly:false(含 manifest 合并)才带进 openai-codex/* 等 manifest-only provider;readOnly:true 只读落盘,
|
|
18
|
+
* 这类从不落盘的 provider 缺失(oauth 已授权却选不出,本次回归根因)。
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
21
|
import { listAllPrimariesWithCredentials, computeProviderUsable, enumerateUsableModels } from './resolve.js';
|
|
19
22
|
import { writePrimary } from './persist.js';
|
|
20
23
|
|
|
21
24
|
const SET_ALLOWED_KEYS = new Set(['agentId', 'primary']);
|
|
22
|
-
const
|
|
25
|
+
const LISTAVAILABLE_ALLOWED_KEYS = new Set(['agentId']);
|
|
23
26
|
|
|
24
27
|
function respondInvalid(respond, message) {
|
|
25
28
|
respond(false, undefined, { code: 'INVALID_ARGS', message });
|
|
@@ -50,7 +53,7 @@ function parseProviderModel(primary) {
|
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
/**
|
|
53
|
-
* 构造 set /
|
|
56
|
+
* 构造 set / listAvailable 用的统一别名感知凭据 deps。
|
|
54
57
|
* @param {object} sdk
|
|
55
58
|
* @param {string} agentDir
|
|
56
59
|
* @returns {object}
|
|
@@ -66,15 +69,16 @@ function buildCredDeps(sdk, agentDir) {
|
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
/**
|
|
69
|
-
* cfg 相关的 primary 校验:凭据门 +
|
|
72
|
+
* cfg 相关的 primary 校验:凭据门 + 目录源存在性。
|
|
70
73
|
* 形态拆分由调用方完成(fail-fast 前置在 loadConfig 之前)。
|
|
71
74
|
*
|
|
72
75
|
* - 凭据门走统一原语 computeProviderUsable(取代旧 ledger-only isProviderAuthProfileConfigured):
|
|
73
76
|
* 覆盖 env + 账本 + 内联 + 别名套餐,修「内联/env/别名 provider 选得到设不上」,且继续拒幽灵
|
|
74
77
|
* (无任何源凭据的 openai/gpt-5.5 被门挡住)。cooldown 中凭据仍算已配置(沿用 isProviderApiKeyConfigured 立场)。
|
|
75
|
-
* -
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
+
* - 存在性走目录源 loadModelCatalog({readOnly:false})(与选模型器枚举同源 → 「选得到设不上」红线天然成立);
|
|
79
|
+
* 用 readOnly:false(含 manifest 合并)才有 openai-codex/* 这类 manifest-only provider(readOnly:true 只读落盘缺它们)。
|
|
80
|
+
* 它返回全量 manifest,但与凭据门联用并不过松——无凭据 provider 仍被门挡住;且非 buildModelsProviderData,无幽灵注入。
|
|
81
|
+
* 整体抛错由外层 catch 映射 IO_FAILED(set 是写操作,失败安全为先)。
|
|
78
82
|
*
|
|
79
83
|
* @returns {Promise<string|null>} 错误 message;null 表通过
|
|
80
84
|
*/
|
|
@@ -82,7 +86,7 @@ async function validateProviderCredAndCatalog({ provider, model, primary, cfg, s
|
|
|
82
86
|
if (!computeProviderUsable(primary, cfg, deps)) {
|
|
83
87
|
return `provider "${provider}" has no usable credential`;
|
|
84
88
|
}
|
|
85
|
-
const entries = await sdk.loadModelCatalog({ readOnly:
|
|
89
|
+
const entries = await sdk.loadModelCatalog({ readOnly: false });
|
|
86
90
|
const exists = Array.isArray(entries)
|
|
87
91
|
&& entries.some((e) => e && e.provider === provider && e.id === model);
|
|
88
92
|
if (!exists) {
|
|
@@ -92,19 +96,19 @@ async function validateProviderCredAndCatalog({ provider, model, primary, cfg, s
|
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
/**
|
|
95
|
-
* 构造 set / list /
|
|
99
|
+
* 构造 set / list / listAvailable 三个 handler。
|
|
96
100
|
*
|
|
97
101
|
* @param {object} opts
|
|
98
102
|
* @param {object} opts.sdk
|
|
99
103
|
* @param {Function} opts.sdk.mutateConfigFile - openclaw/plugin-sdk/config-mutation(set 写盘)
|
|
100
|
-
* @param {Function} opts.sdk.loadModelCatalog - openclaw/plugin-sdk/agent-runtime(set 存在性 +
|
|
104
|
+
* @param {Function} opts.sdk.loadModelCatalog - openclaw/plugin-sdk/agent-runtime(set 存在性 + listAvailable 枚举的目录源)
|
|
101
105
|
* @param {Function} opts.sdk.isProviderApiKeyConfigured - openclaw/plugin-sdk/provider-auth(env+账本凭据信号,别名感知)
|
|
102
106
|
* @param {Function} opts.sdk.hasConfiguredSecretInput - openclaw/plugin-sdk/provider-auth(内联 key 判定)
|
|
103
|
-
* @param {Function} opts.sdk.ensureAuthProfileStore - openclaw/plugin-sdk/provider-auth
|
|
107
|
+
* @param {Function} opts.sdk.ensureAuthProfileStore - openclaw/plugin-sdk/provider-auth(账本非空判定)
|
|
104
108
|
* @param {Function} opts.sdk.resolveProviderIdForAuth - openclaw/plugin-sdk/agent-runtime(别名归一)
|
|
105
109
|
* @param {Function} opts.loadConfig - 返回当前 cfg snapshot;缺失时返回 null
|
|
106
110
|
* @param {Function} opts.resolveAgentDir - 返回 agent /agent 子目录全路径(默认 main agent;agentId 贯穿)
|
|
107
|
-
* @returns {{ set: Function, list: Function,
|
|
111
|
+
* @returns {{ set: Function, list: Function, listAvailable: Function }}
|
|
108
112
|
*/
|
|
109
113
|
export function buildModelDefaultHandlers({ sdk, loadConfig, resolveAgentDir }) {
|
|
110
114
|
async function set({ params, respond }) {
|
|
@@ -204,14 +208,14 @@ export function buildModelDefaultHandlers({ sdk, loadConfig, resolveAgentDir })
|
|
|
204
208
|
}
|
|
205
209
|
}
|
|
206
210
|
|
|
207
|
-
async function
|
|
211
|
+
async function listAvailable({ params, respond }) {
|
|
208
212
|
try {
|
|
209
213
|
if (!params || typeof params !== 'object' || Array.isArray(params)) {
|
|
210
214
|
respondInvalid(respond, 'params must be an object');
|
|
211
215
|
return;
|
|
212
216
|
}
|
|
213
217
|
for (const key of Object.keys(params)) {
|
|
214
|
-
if (!
|
|
218
|
+
if (!LISTAVAILABLE_ALLOWED_KEYS.has(key)) {
|
|
215
219
|
respondInvalid(respond, `unknown field: ${key}`);
|
|
216
220
|
return;
|
|
217
221
|
}
|
|
@@ -233,16 +237,12 @@ export function buildModelDefaultHandlers({ sdk, loadConfig, resolveAgentDir })
|
|
|
233
237
|
// 故四个消费点在产线天然同 dir;测试可注入按 agentId 分目录的 resolver 钉住贯穿。
|
|
234
238
|
const deps = buildCredDeps(sdk, resolveAgentDir(agentId));
|
|
235
239
|
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
catch {
|
|
244
|
-
entries = [];
|
|
245
|
-
}
|
|
240
|
+
// 目录源 loadModelCatalog({readOnly:false}):含 manifest 合并,才有 openai-codex/* 这类 manifest-only provider
|
|
241
|
+
// (readOnly:true 只读落盘缺它们 → oauth 已授权却选不出,本次回归根因)。
|
|
242
|
+
// 抛错(罕见,如 runtime config 取不到)→ 走外层 catch 映射 IO_FAILED,不吞成空 entries:
|
|
243
|
+
// byProvider 同时驱动前端 primary 有效性(计算属性),把"清单没加载出来"伪装成"权威空清单"会让前端
|
|
244
|
+
// 误报主模型失效。如实暴露失败 → UI 维持 available=null="先不下结论",与"真空(无凭据)"区分开。
|
|
245
|
+
const entries = await sdk.loadModelCatalog({ readOnly: false });
|
|
246
246
|
|
|
247
247
|
respond(true, enumerateUsableModels(entries, cfg, deps));
|
|
248
248
|
}
|
|
@@ -251,5 +251,5 @@ export function buildModelDefaultHandlers({ sdk, loadConfig, resolveAgentDir })
|
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
return { set, list,
|
|
254
|
+
return { set, list, listAvailable };
|
|
255
255
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* model-default 注册入口 —— 把 coclaw.model.set / list /
|
|
2
|
+
* model-default 注册入口 —— 把 coclaw.model.set / list / listAvailable 接到 gateway
|
|
3
|
+
*(listAvailable 即原 listUsable 改名;listUsable 作过渡别名继续注册、映到同一 handler)。
|
|
3
4
|
*
|
|
4
5
|
* 设计(同 provider-auth/index.js):
|
|
5
6
|
* - 三个 SDK 子入口(config-mutation / provider-auth / agent-runtime)懒加载,
|
|
@@ -34,7 +35,7 @@ function defaultLoadProviderAuth() {
|
|
|
34
35
|
}
|
|
35
36
|
function defaultLoadAgentRuntime() {
|
|
36
37
|
// agent-runtime barrel 同时给:resolveProviderIdForAuth(别名归一)+ loadModelCatalog(干净目录,
|
|
37
|
-
// set 存在性 /
|
|
38
|
+
// set 存在性 / listAvailable 选模型器枚举同源)。barrel re-export provider-auth-aliases.js + model-catalog.js。
|
|
38
39
|
_agentRuntimeP ??= import('openclaw/plugin-sdk/agent-runtime');
|
|
39
40
|
return _agentRuntimeP;
|
|
40
41
|
}
|
|
@@ -49,7 +50,8 @@ export function __resetSdkCaches() {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
|
-
* 在 gateway api 上注册 `coclaw.model.set` / `coclaw.model.list` / `coclaw.model.
|
|
53
|
+
* 在 gateway api 上注册 `coclaw.model.set` / `coclaw.model.list` / `coclaw.model.listAvailable`
|
|
54
|
+
*(外加过渡别名 `coclaw.model.listUsable`,与 listAvailable 映同一 handler)。
|
|
53
55
|
*
|
|
54
56
|
* 仅 `register(api)` 的 `if (api.registrationMode === 'full')` 分支调;
|
|
55
57
|
* 其它 mode 注册副作用违规(参 plugins/openclaw/CLAUDE.md "Service / register 副作用边界")。
|
|
@@ -80,13 +82,13 @@ export function registerModelDefaultHandlers(api, opts = {}) {
|
|
|
80
82
|
]);
|
|
81
83
|
const sdk = {
|
|
82
84
|
mutateConfigFile: configMutation.mutateConfigFile,
|
|
83
|
-
// 干净目录(set 存在性 +
|
|
85
|
+
// 干净目录(set 存在性 + listAvailable 枚举同源):agent-runtime barrel re-export model-catalog.js
|
|
84
86
|
loadModelCatalog: agentRuntime.loadModelCatalog,
|
|
85
|
-
// 凭据信号(providerUsable / hasAnyUsableCredential /
|
|
87
|
+
// 凭据信号(providerUsable / hasAnyUsableCredential / 凭据门)
|
|
86
88
|
isProviderApiKeyConfigured: providerAuth.isProviderApiKeyConfigured,
|
|
87
89
|
hasConfiguredSecretInput: providerAuth.hasConfiguredSecretInput,
|
|
88
90
|
ensureAuthProfileStore: providerAuth.ensureAuthProfileStore,
|
|
89
|
-
// 别名归一(内联凭据信号 +
|
|
91
|
+
// 别名归一(内联凭据信号 + 选模型器枚举)
|
|
90
92
|
resolveProviderIdForAuth: agentRuntime.resolveProviderIdForAuth,
|
|
91
93
|
};
|
|
92
94
|
return buildModelDefaultHandlers({ sdk, loadConfig, resolveAgentDir });
|
|
@@ -114,5 +116,7 @@ export function registerModelDefaultHandlers(api, opts = {}) {
|
|
|
114
116
|
|
|
115
117
|
api.registerGatewayMethod('coclaw.model.set', wrap('set'));
|
|
116
118
|
api.registerGatewayMethod('coclaw.model.list', wrap('list'));
|
|
117
|
-
api.registerGatewayMethod('coclaw.model.
|
|
119
|
+
api.registerGatewayMethod('coclaw.model.listAvailable', wrap('listAvailable'));
|
|
120
|
+
// 过渡别名:旧 UI 仍调 listUsable,映到同一 handler(决策1:纯过渡,不加任何额外兜底)
|
|
121
|
+
api.registerGatewayMethod('coclaw.model.listUsable', wrap('listAvailable'));
|
|
118
122
|
}
|
|
@@ -123,21 +123,49 @@ function hasInlineKey(cfg, provider, deps) {
|
|
|
123
123
|
return false;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* 某 provider(裸名)在自管账本里有没有任一来源凭据(oauth / token / api-key)。
|
|
128
|
+
* 别名感知:查询名与各 profile 的 cred.provider 两侧都过 resolveProviderIdForAuth 归一后比较,
|
|
129
|
+
* 与 computeConfiguredProviders 的账本口径一致(不校验 type:匹配到任一 well-formed profile 即算)。
|
|
130
|
+
* 补 isProviderApiKeyConfigured 只认 api-key 的缺口:纯 OAuth provider(codex / copilot 等设备码家族)
|
|
131
|
+
* 只有 oauth 凭据、无 key,旧逻辑两路皆 false 会被全组丢出 byProvider(见 changeset / TODO 根成因)。
|
|
132
|
+
* 归一为空串的 provider 不匹配(与 computeConfiguredProviders 的丢弃空 id 一致),
|
|
133
|
+
* 避免 whitespace-only 查询名与 whitespace-only cred 同归一到 '' 的误命中。
|
|
134
|
+
* @param {string} provider - 裸 provider 名
|
|
135
|
+
* @param {object} deps - { agentDir, ensureAuthProfileStore, resolveProviderIdForAuth }
|
|
136
|
+
* @returns {boolean}
|
|
137
|
+
*/
|
|
138
|
+
function hasLedgerCred(provider, deps) {
|
|
139
|
+
const store = deps.ensureAuthProfileStore(deps.agentDir, { allowKeychainPrompt: false });
|
|
140
|
+
if (!store || !store.profiles || typeof store.profiles !== 'object') return false;
|
|
141
|
+
const targetId = deps.resolveProviderIdForAuth(provider);
|
|
142
|
+
if (!targetId) return false;
|
|
143
|
+
for (const cred of Object.values(store.profiles)) {
|
|
144
|
+
if (!cred || typeof cred.provider !== 'string' || cred.provider.length === 0) continue;
|
|
145
|
+
if (deps.resolveProviderIdForAuth(cred.provider) === targetId) return true;
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
126
150
|
/**
|
|
127
151
|
* 某 provider(裸名,无斜杠)有没有可用凭据 —— 统一别名感知原语。
|
|
128
|
-
* 判定 = isProviderApiKeyConfigured
|
|
129
|
-
* ∪ hasInlineKey(内联 key
|
|
130
|
-
*
|
|
131
|
-
*
|
|
152
|
+
* 判定 = isProviderApiKeyConfigured(env + 账本里的 api-key,别名归一其内部完成)
|
|
153
|
+
* ∪ hasInlineKey(内联 key,别名归一)
|
|
154
|
+
* ∪ hasLedgerCred(账本里的 oauth/token 等非 api-key 凭据,别名归一)。
|
|
155
|
+
* 覆盖 env + 内联 + 账本(api-key / oauth / token 全口径)+ 别名套餐;
|
|
156
|
+
* 统一漏 IAM/本地(hasAuthForModelProvider 未导出 plugin-sdk,接受)。
|
|
157
|
+
* 选模型器枚举 / model.set 门 / providerUsable 三个消费点同吃本原语;noKey 走姊妹原语
|
|
158
|
+
* computeHasAnyUsableCredential(同源探针 + 账本判定),口径与本原语对齐、不跨界面分叉。
|
|
132
159
|
* @param {string|null} provider - 裸 provider 名(如 'openai' / 'volcengine-plan')
|
|
133
160
|
* @param {object} cfg
|
|
134
|
-
* @param {object} deps - { agentDir, isProviderApiKeyConfigured, hasConfiguredSecretInput, resolveProviderIdForAuth }
|
|
161
|
+
* @param {object} deps - { agentDir, isProviderApiKeyConfigured, hasConfiguredSecretInput, ensureAuthProfileStore, resolveProviderIdForAuth }
|
|
135
162
|
* @returns {boolean}
|
|
136
163
|
*/
|
|
137
164
|
export function computeProviderUsableByName(provider, cfg, deps) {
|
|
138
165
|
if (typeof provider !== 'string' || provider.length === 0) return false;
|
|
139
166
|
if (deps.isProviderApiKeyConfigured({ provider, agentDir: deps.agentDir })) return true;
|
|
140
|
-
|
|
167
|
+
if (hasInlineKey(cfg, provider, deps)) return true;
|
|
168
|
+
return hasLedgerCred(provider, deps);
|
|
141
169
|
}
|
|
142
170
|
|
|
143
171
|
/**
|
|
@@ -244,8 +272,8 @@ export function computeConfiguredProviders(cfg, deps) {
|
|
|
244
272
|
}
|
|
245
273
|
|
|
246
274
|
/**
|
|
247
|
-
*
|
|
248
|
-
* catalogEntries
|
|
275
|
+
* 选模型器枚举(纯同步):把目录源按 entry.provider 分组,留 computeProviderUsableByName 为真的 provider。
|
|
276
|
+
* catalogEntries 由调用方传入(handler 调 loadModelCatalog({readOnly:false}) 后传进来;含 manifest 才有 openai-codex/* 这类 manifest-only provider),
|
|
249
277
|
* 本函数不自己 await loadModelCatalog;空 / 非数组 entries → 空 byProvider。
|
|
250
278
|
* 变体 provider(如 volcengine-plan)经 manifest 目录行进入 entries、再经基座 key 别名感知保留;
|
|
251
279
|
* 无凭据 provider 被丢(含幽灵——幽灵根本不在 loadModelCatalog 这个源里)。
|
|
@@ -254,10 +282,10 @@ export function computeConfiguredProviders(cfg, deps) {
|
|
|
254
282
|
* (image_generation 等是网关响应的另一类型;imageModel 注入只在 buildModelsProviderData 尾部、不在此源),
|
|
255
283
|
* 故无"纯图像/视频生成"条目混入;entry.input 是"输入"模态而非输出 kind,按它滤会误删多模态文本模型。
|
|
256
284
|
*
|
|
257
|
-
* @param {object[]} catalogEntries - loadModelCatalog({readOnly:
|
|
285
|
+
* @param {object[]} catalogEntries - loadModelCatalog({readOnly:false}) 的结果(ModelCatalogEntry[])
|
|
258
286
|
* @param {object} cfg
|
|
259
287
|
* @param {object} deps - { agentDir, isProviderApiKeyConfigured, hasConfiguredSecretInput, resolveProviderIdForAuth, ensureAuthProfileStore }
|
|
260
|
-
* @returns {{ byProvider: Record<string, string[]
|
|
288
|
+
* @returns {{ byProvider: Record<string, string[]> }}
|
|
261
289
|
*/
|
|
262
290
|
export function enumerateUsableModels(catalogEntries, cfg, deps) {
|
|
263
291
|
const grouped = new Map(); // provider -> Set<modelId>
|
|
@@ -279,7 +307,9 @@ export function enumerateUsableModels(catalogEntries, cfg, deps) {
|
|
|
279
307
|
byProvider[provider] = [...ids].sort();
|
|
280
308
|
}
|
|
281
309
|
}
|
|
282
|
-
|
|
310
|
+
// 只返回 byProvider;configuredProviders 已迁出(catalog 经 computeConfiguredProviders 单独算 hasCred,
|
|
311
|
+
// UI 加 provider 排除改吃 catalog.hasCred)。computeConfiguredProviders 仍具名导出供 catalog 复用。
|
|
312
|
+
return { byProvider };
|
|
283
313
|
}
|
|
284
314
|
|
|
285
315
|
/**
|
|
@@ -38,7 +38,7 @@ import { PORTAL_PROVIDER_ID, CONFIG_DEFAULT_BASE_URL, VALID_REGIONS } from './mi
|
|
|
38
38
|
import { getPortalModels } from './portal-model-catalog.js';
|
|
39
39
|
import { remoteLog } from '../remote-log.js';
|
|
40
40
|
import { getClawConfig } from '../claw-config.js';
|
|
41
|
-
import { listAllPrimaries, providerSegmentOf } from '../model-default/resolve.js';
|
|
41
|
+
import { listAllPrimaries, providerSegmentOf, computeConfiguredProviders } from '../model-default/resolve.js';
|
|
42
42
|
import { deepMergeInto } from '../utils/deep-merge.js';
|
|
43
43
|
import {
|
|
44
44
|
isVerificationNote,
|
|
@@ -50,6 +50,14 @@ import {
|
|
|
50
50
|
const VALID_CRED_TYPES = new Set(['api_key', 'oauth', 'token']);
|
|
51
51
|
const PORTAL_PROFILE_ID = `${PORTAL_PROVIDER_ID}:default`;
|
|
52
52
|
|
|
53
|
+
// catalog 出参的 authMethods 映射(一条规则零特判):只露这三 kind,token/custom 不进出参。
|
|
54
|
+
// kind 五值见上游 types.ts(oauth|api_key|token|device_code|custom)。
|
|
55
|
+
const KIND_TO_AUTH_METHOD = {
|
|
56
|
+
device_code: 'oauth-device-code',
|
|
57
|
+
oauth: 'oauth-login',
|
|
58
|
+
api_key: 'api-key',
|
|
59
|
+
};
|
|
60
|
+
|
|
53
61
|
function respondInvalid(respond, message) {
|
|
54
62
|
respond(false, undefined, { code: 'INVALID_ARGS', message });
|
|
55
63
|
}
|
|
@@ -84,7 +92,9 @@ function isNonEmptyString(v) {
|
|
|
84
92
|
* @param {Function} [opts.logRemote] - (text) → void,OAuth 终态诊断推送;默认模块级 remoteLog(测试注入 spy)
|
|
85
93
|
* @param {Function} [opts.resolveConfig] - () → OpenClaw runtime config 快照;通用 device-code 登录(B1)拿 config 用,默认 getClawConfig
|
|
86
94
|
* @param {Function} [opts.resolveProviders] - ({ config, providerRefs }) → ProviderPlugin[];B1 经它拿 provider 的 auth 方法(生产由入口注入,内部 activate:false),默认抛错
|
|
87
|
-
* @
|
|
95
|
+
* @param {Function} [opts.resolveSetupProviders] - ({ config }) → ProviderPlugin[];catalog 经它拿 setup 全集(mode:'setup', activate:false, cache:true,生产由入口注入),默认抛错
|
|
96
|
+
* @param {Function} [opts.loadProviderIdResolver] - () → Promise<resolveProviderIdForAuth>;catalog 算 hasCred 时别名归一基座 id(生产由入口惰性加载 agent-runtime),默认抛错
|
|
97
|
+
* @returns {{ setApiKey, list, remove, loginOauth, cancelOauth, catalog }}
|
|
88
98
|
*/
|
|
89
99
|
export function buildProviderAuthHandlers({
|
|
90
100
|
sdk,
|
|
@@ -96,6 +106,8 @@ export function buildProviderAuthHandlers({
|
|
|
96
106
|
logRemote = remoteLog,
|
|
97
107
|
resolveConfig = getClawConfig,
|
|
98
108
|
resolveProviders = () => { throw new Error('provider catalog runtime not injected'); },
|
|
109
|
+
resolveSetupProviders = () => { throw new Error('provider catalog runtime not injected'); },
|
|
110
|
+
loadProviderIdResolver = () => { throw new Error('agent runtime not injected'); },
|
|
99
111
|
}) {
|
|
100
112
|
// TODO: 将来若要支持"设默认模型 / 多账号顺序"等需要写 cfg 的操作,会撞上
|
|
101
113
|
// gateway 重启窗口的 UX 问题——参 docs/model-config-api.md § 3 / § 5(占位章节)。
|
|
@@ -553,7 +565,79 @@ export function buildProviderAuthHandlers({
|
|
|
553
565
|
}
|
|
554
566
|
}
|
|
555
567
|
|
|
556
|
-
|
|
568
|
+
// --- provider 目录(能力 1):枚举全集 provider + 各自认证方式 + 是否已配凭据 ---
|
|
569
|
+
|
|
570
|
+
// 无参;出参 { providers: [{ provider, authMethods, hasCred }] }(命名对象、不 wrap、不 undefined)。
|
|
571
|
+
// 数据源 = resolvePluginProviders(setup) 全集(含未配 provider);authMethods 一条规则零特判
|
|
572
|
+
// (device_code/oauth/api_key 三 kind 露出,token/custom 不露,authMethods 空则该 provider 不进出参);
|
|
573
|
+
// hasCred 复用 computeConfiguredProviders 三源(账本/内联/env)别名归一基座 id。
|
|
574
|
+
// 错误码:未知字段 → INVALID_ARGS(params:{} / undefined / null 都放行);解析/凭据探针/store 读抛错 → IO_FAILED。
|
|
575
|
+
async function catalog({ params, respond }) {
|
|
576
|
+
try {
|
|
577
|
+
// 无参方法:params 缺省(undefined / null)或空对象都放行;带任何字段即未知字段。
|
|
578
|
+
if (params !== undefined && params !== null) {
|
|
579
|
+
if (typeof params !== 'object' || Array.isArray(params)) {
|
|
580
|
+
respondInvalid(respond, 'params must be an object');
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const keys = Object.keys(params);
|
|
584
|
+
if (keys.length > 0) {
|
|
585
|
+
respondInvalid(respond, `unknown field: ${keys[0]}`);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const config = resolveConfig() ?? {};
|
|
591
|
+
// setup 全集(含未配):activate:false 零副作用、cache:true 复用进程内发现缓存。生产由入口注入。
|
|
592
|
+
const providers = await resolveSetupProviders({ config });
|
|
593
|
+
// 别名归一基座 id(hasCred 计算用):agent-runtime 惰性加载,失败走 IO_FAILED。
|
|
594
|
+
const resolveProviderIdForAuth = await loadProviderIdResolver();
|
|
595
|
+
// 三源 hasCred(账本/内联/env),全过 resolveProviderIdForAuth 归一基座 id。
|
|
596
|
+
const configuredSet = new Set(computeConfiguredProviders(config, {
|
|
597
|
+
agentDir: resolveAgentDir(),
|
|
598
|
+
isProviderApiKeyConfigured: sdk.isProviderApiKeyConfigured,
|
|
599
|
+
hasConfiguredSecretInput: sdk.hasConfiguredSecretInput,
|
|
600
|
+
ensureAuthProfileStore: sdk.ensureAuthProfileStore,
|
|
601
|
+
resolveProviderIdForAuth,
|
|
602
|
+
}));
|
|
603
|
+
|
|
604
|
+
const out = [];
|
|
605
|
+
for (const p of providers ?? []) {
|
|
606
|
+
const provider = p?.id;
|
|
607
|
+
if (typeof provider !== 'string' || provider.length === 0) continue;
|
|
608
|
+
const authMethods = mapAuthMethods(p.auth);
|
|
609
|
+
if (authMethods.length === 0) continue; // custom-only / token-only / 空 auth[] 自然排除
|
|
610
|
+
out.push({ provider, authMethods, hasCred: configuredSet.has(provider) });
|
|
611
|
+
}
|
|
612
|
+
respond(true, { providers: out });
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
respondIoFailed(respond, err);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return { setApiKey, list, remove, loginOauth, cancelOauth, catalog };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* 把 provider 的 auth[] 映射成对外的 authMethods(catalog 用,一条规则零特判):
|
|
624
|
+
* device_code→oauth-device-code、oauth→oauth-login、api_key→api-key;token/custom 不露。
|
|
625
|
+
* 保留 auth[] 出现顺序,按方法名去重(一 provider 可多入口、同 kind 多条只算一次)。
|
|
626
|
+
* @param {Array<{kind?:string}>} authArr - resolvePluginProviders 返回项的 auth 数组
|
|
627
|
+
* @returns {string[]}
|
|
628
|
+
*/
|
|
629
|
+
function mapAuthMethods(authArr) {
|
|
630
|
+
const out = [];
|
|
631
|
+
const seen = new Set();
|
|
632
|
+
if (!Array.isArray(authArr)) return out;
|
|
633
|
+
for (const a of authArr) {
|
|
634
|
+
const method = KIND_TO_AUTH_METHOD[a?.kind];
|
|
635
|
+
if (method && !seen.has(method)) {
|
|
636
|
+
seen.add(method);
|
|
637
|
+
out.push(method);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return out;
|
|
557
641
|
}
|
|
558
642
|
|
|
559
643
|
/**
|
|
@@ -25,6 +25,7 @@ import { mainAgentDir } from '../claw-paths.js';
|
|
|
25
25
|
let _sdkPromise;
|
|
26
26
|
let _configMutationPromise;
|
|
27
27
|
let _catalogRuntimePromise;
|
|
28
|
+
let _agentRuntimePromise;
|
|
28
29
|
|
|
29
30
|
// 默认 loader 仅作 fallback:生产路径必须由入口(plugins/openclaw/index.js)注入,
|
|
30
31
|
// 因为 OpenClaw plugin loader 只扫入口源码识别 `openclaw/plugin-sdk/*` 字面量并触发 jiti 重写;
|
|
@@ -45,6 +46,12 @@ function defaultLoadProviderCatalogRuntime() {
|
|
|
45
46
|
return _catalogRuntimePromise;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
function defaultLoadAgentRuntime() {
|
|
50
|
+
// agent-runtime barrel 提供 resolveProviderIdForAuth(别名归一基座 id)——catalog 算 hasCred 用。
|
|
51
|
+
_agentRuntimePromise ??= import('openclaw/plugin-sdk/agent-runtime');
|
|
52
|
+
return _agentRuntimePromise;
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
/**
|
|
49
56
|
* 测试辅助:清掉懒加载 SDK 缓存。
|
|
50
57
|
*/
|
|
@@ -52,6 +59,7 @@ export function __resetSdkCache() {
|
|
|
52
59
|
_sdkPromise = undefined;
|
|
53
60
|
_configMutationPromise = undefined;
|
|
54
61
|
_catalogRuntimePromise = undefined;
|
|
62
|
+
_agentRuntimePromise = undefined;
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
/**
|
|
@@ -65,7 +73,8 @@ export function __resetSdkCache() {
|
|
|
65
73
|
* @param {Function} [opts.resolveAgentDir] - 覆盖 agentDir 解析(默认 mainAgentDir)
|
|
66
74
|
* @param {Function} [opts.loadSdk] - 必传(生产由入口注入字面量 dynamic import);缺省回退仅为测试兜底
|
|
67
75
|
* @param {Function} [opts.loadConfigMutation] - 必传(同上,OAuth 写 cfg 用)
|
|
68
|
-
* @param {Function} [opts.loadProviderCatalogRuntime] - 必传(同上,通用 device-code 登录 B1
|
|
76
|
+
* @param {Function} [opts.loadProviderCatalogRuntime] - 必传(同上,通用 device-code 登录 B1 + catalog setup 全集拿 resolvePluginProviders 用)
|
|
77
|
+
* @param {Function} [opts.loadAgentRuntime] - 必传(同上,catalog 算 hasCred 拿 resolveProviderIdForAuth 用)
|
|
69
78
|
* @param {object} [opts.registry] - 覆盖 oauth-registry(默认模块级单例)
|
|
70
79
|
*/
|
|
71
80
|
export function registerProviderAuthHandlers(api, opts = {}) {
|
|
@@ -73,9 +82,10 @@ export function registerProviderAuthHandlers(api, opts = {}) {
|
|
|
73
82
|
const loadSdk = opts.loadSdk ?? defaultLoadSdk;
|
|
74
83
|
const loadConfigMutation = opts.loadConfigMutation ?? defaultLoadConfigMutation;
|
|
75
84
|
const loadProviderCatalogRuntime = opts.loadProviderCatalogRuntime ?? defaultLoadProviderCatalogRuntime;
|
|
85
|
+
const loadAgentRuntime = opts.loadAgentRuntime ?? defaultLoadAgentRuntime;
|
|
76
86
|
const registry = opts.registry ?? { registerLogin, getLogin, removeLogin };
|
|
77
87
|
|
|
78
|
-
// catalog-runtime 仅通用 device-code 登录(B1
|
|
88
|
+
// catalog-runtime 仅通用 device-code 登录(B1)与 catalog 才需要,独立惰性加载——不耦合进 getHandlers
|
|
79
89
|
// 的 Promise.all,避免 setApiKey / list / remove / minimax-oauth 因这个 SDK 子入口缺失而连带失败。
|
|
80
90
|
let catalogRuntimePromise;
|
|
81
91
|
const resolveProviders = async ({ config, providerRefs }) => {
|
|
@@ -89,6 +99,25 @@ export function registerProviderAuthHandlers(api, opts = {}) {
|
|
|
89
99
|
mode: 'runtime',
|
|
90
100
|
});
|
|
91
101
|
};
|
|
102
|
+
// catalog 用的 setup 全集解析(独立于上面的 runtime 闭包——B1 登录仍走 runtime):
|
|
103
|
+
// mode:'setup' 无条件返回全部 provider(含未配过的),activate:false 零副作用、cache:true 复用发现缓存。
|
|
104
|
+
const resolveSetupProviders = async ({ config }) => {
|
|
105
|
+
catalogRuntimePromise ??= loadProviderCatalogRuntime();
|
|
106
|
+
const catalogRuntime = await catalogRuntimePromise;
|
|
107
|
+
return catalogRuntime.resolvePluginProviders({
|
|
108
|
+
config,
|
|
109
|
+
activate: false,
|
|
110
|
+
cache: true,
|
|
111
|
+
mode: 'setup',
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
// agent-runtime 同样独立惰性加载(仅 catalog 的 hasCred 别名归一才需要),不耦合进 getHandlers。
|
|
115
|
+
let agentRuntimePromise;
|
|
116
|
+
const loadProviderIdResolver = async () => {
|
|
117
|
+
agentRuntimePromise ??= loadAgentRuntime();
|
|
118
|
+
const agentRuntime = await agentRuntimePromise;
|
|
119
|
+
return agentRuntime.resolveProviderIdForAuth;
|
|
120
|
+
};
|
|
92
121
|
|
|
93
122
|
let handlersPromise;
|
|
94
123
|
async function getHandlers() {
|
|
@@ -107,7 +136,15 @@ export function registerProviderAuthHandlers(api, opts = {}) {
|
|
|
107
136
|
generatePkce: providerAuthSdk.generatePkceVerifierChallenge,
|
|
108
137
|
toForm: providerAuthSdk.toFormUrlEncoded,
|
|
109
138
|
});
|
|
110
|
-
return buildProviderAuthHandlers({
|
|
139
|
+
return buildProviderAuthHandlers({
|
|
140
|
+
sdk,
|
|
141
|
+
resolveAgentDir,
|
|
142
|
+
oauth,
|
|
143
|
+
registry,
|
|
144
|
+
resolveProviders,
|
|
145
|
+
resolveSetupProviders,
|
|
146
|
+
loadProviderIdResolver,
|
|
147
|
+
});
|
|
111
148
|
})();
|
|
112
149
|
}
|
|
113
150
|
return handlersPromise;
|
|
@@ -136,4 +173,5 @@ export function registerProviderAuthHandlers(api, opts = {}) {
|
|
|
136
173
|
api.registerGatewayMethod('coclaw.providerAuth.remove', wrap('remove'));
|
|
137
174
|
api.registerGatewayMethod('coclaw.providerAuth.loginOauth', wrap('loginOauth'));
|
|
138
175
|
api.registerGatewayMethod('coclaw.providerAuth.cancelOauth', wrap('cancelOauth'));
|
|
176
|
+
api.registerGatewayMethod('coclaw.providerAuth.catalog', wrap('catalog'));
|
|
139
177
|
}
|