@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 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)。三个 SDK 子入口的字面量
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.25.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 / listUsable 三个 RPC 的纯函数实现
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
- * listUsable → { byProvider, configuredProviders }
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 存在性 + listUsable 枚举走同一干净目录 loadModelCatalog({readOnly:true}):选得到 ⇒ 设得上(红线天然成立)
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 LISTUSABLE_ALLOWED_KEYS = new Set(['agentId']);
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 / listUsable 用的统一别名感知凭据 deps。
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
- * - 存在性走干净目录 loadModelCatalog({readOnly:true})(与选模型器枚举同源 → 「选得到设不上」红线天然成立);
76
- * 去掉旧 buildModelsProviderData view:'all'(~1076 模型/~10s、鉴权盲、过松)。
77
- * loadModelCatalog readOnly 自带「持久化失败→静态 manifest」兜底;整体抛错由外层 catch 映射 IO_FAILED(set 是写操作,失败安全为先)。
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: true });
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 / listUsable 三个 handler。
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 存在性 + listUsable 枚举的干净目录)
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(账本非空 / configuredProviders)
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, listUsable: 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 listUsable({ params, respond }) {
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 (!LISTUSABLE_ALLOWED_KEYS.has(key)) {
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
- // 干净目录 loadModelCatalog({readOnly:true}) 自带「持久化失败→静态 manifest」兜底(model-catalog.ts)。
237
- // 仍整体抛错(罕见,如 runtime config 取不到)→ 兜空 entries:byProvider 退化为空,
238
- // configuredProviders 不依赖目录仍可算 「不空白」,UI provider 排除照常工作(§ 3.2.1 降级)。
239
- let entries;
240
- try {
241
- entries = await sdk.loadModelCatalog({ readOnly: true });
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, listUsable };
254
+ return { set, list, listAvailable };
255
255
  }
@@ -1,5 +1,6 @@
1
1
  /**
2
- * model-default 注册入口 —— 把 coclaw.model.set / list / listUsable 接到 gateway
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 存在性 / listUsable 选模型器枚举同源)。barrel re-export provider-auth-aliases.js + model-catalog.js。
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.listUsable`。
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 存在性 + listUsable 枚举同源):agent-runtime barrel re-export model-catalog.js
85
+ // 干净目录(set 存在性 + listAvailable 枚举同源):agent-runtime barrel re-export model-catalog.js
84
86
  loadModelCatalog: agentRuntime.loadModelCatalog,
85
- // 凭据信号(providerUsable / hasAnyUsableCredential / 凭据门 / configuredProviders)
87
+ // 凭据信号(providerUsable / hasAnyUsableCredential / 凭据门)
86
88
  isProviderApiKeyConfigured: providerAuth.isProviderApiKeyConfigured,
87
89
  hasConfiguredSecretInput: providerAuth.hasConfiguredSecretInput,
88
90
  ensureAuthProfileStore: providerAuth.ensureAuthProfileStore,
89
- // 别名归一(内联凭据信号 + 选模型器枚举 + configuredProviders)
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.listUsable', wrap('listUsable'));
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(覆盖 env + 自管账本,别名归一其内部完成)
129
- * ∪ hasInlineKey(内联 key,别名归一)。
130
- * 覆盖 env + 内联 + 账本 + 别名套餐;统一漏 IAM/本地(hasAuthForModelProvider 未导出 plugin-sdk,接受)。
131
- * 选模型器枚举 / model.set / providerUsable / noKey 四个消费点同吃这一个原语,杜绝跨界面口径分叉。
152
+ * 判定 = isProviderApiKeyConfiguredenv + 账本里的 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
- return hasInlineKey(cfg, provider, deps);
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
- * 选模型器枚举(纯同步):把干净目录按 entry.provider 分组,留 computeProviderUsableByName 为真的 provider。
248
- * catalogEntries 由调用方传入(子任务 2 的 handler 调 loadModelCatalog({readOnly:true}) 后传进来),
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:true}) 的结果(ModelCatalogEntry[])
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[]>, configuredProviders: 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
- return { byProvider, configuredProviders: computeConfiguredProviders(cfg, deps) };
310
+ // 只返回 byProviderconfiguredProviders 已迁出(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
- * @returns {{ setApiKey, list, remove, loginOauth, cancelOauth }}
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
- return { setApiKey, list, remove, loginOauth, cancelOauth };
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 resolvePluginProviders 用)
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)才需要,独立惰性加载——不耦合进 getHandlers
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({ sdk, resolveAgentDir, oauth, registry, resolveProviders });
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
  }