@coclaw/openclaw-coclaw 0.21.5 → 0.22.2
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 +128 -39
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -4
- package/src/channel-plugin.js +1 -1
- package/src/chat-history-manager/manager.js +208 -22
- package/src/claw-paths.js +14 -0
- package/src/cli-registrar.js +84 -4
- package/src/common/gateway-notify.js +4 -4
- package/src/common/messages.js +42 -4
- package/src/model-default/handlers.js +177 -0
- package/src/model-default/index.js +106 -0
- package/src/model-default/persist.js +115 -0
- package/src/model-default/resolve.js +88 -0
- package/src/provider-auth/handlers.js +180 -0
- package/src/provider-auth/index.js +78 -0
- package/src/realtime-bridge.js +88 -4
- package/src/session-manager/manager.js +49 -14
- package/src/topic-manager/manager.js +12 -2
- package/src/webrtc/webrtc-peer.js +14 -4
package/src/cli-registrar.js
CHANGED
|
@@ -3,6 +3,7 @@ import { callGatewayMethod } from './common/gateway-notify.js';
|
|
|
3
3
|
import {
|
|
4
4
|
notBound, bindOk, unbindOk,
|
|
5
5
|
claimCodeCreated,
|
|
6
|
+
apiKeySetOk, authListEntries, authListEmpty, authRemoveOk,
|
|
6
7
|
} from './common/messages.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -113,7 +114,7 @@ export function registerCoclawCli({ program, logger: _logger }, deps = {}) {
|
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
const data = result.
|
|
117
|
+
const data = result.payload;
|
|
117
118
|
console.log(bindOk(data));
|
|
118
119
|
}
|
|
119
120
|
/* c8 ignore next 4 -- callGatewayMethod 不会抛异常,纯防御 */
|
|
@@ -139,8 +140,7 @@ export function registerCoclawCli({ program, logger: _logger }, deps = {}) {
|
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
// RPC 成功:输出认领码信息
|
|
142
|
-
|
|
143
|
-
const data = result.status;
|
|
143
|
+
const data = result.payload;
|
|
144
144
|
if (data?.code && data?.appUrl) {
|
|
145
145
|
console.log(claimCodeCreated({
|
|
146
146
|
code: data.code,
|
|
@@ -179,7 +179,7 @@ export function registerCoclawCli({ program, logger: _logger }, deps = {}) {
|
|
|
179
179
|
return;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
const data = result.
|
|
182
|
+
const data = result.payload;
|
|
183
183
|
console.log(unbindOk(data));
|
|
184
184
|
}
|
|
185
185
|
/* c8 ignore next 4 -- callGatewayMethod 不会抛异常,纯防御 */
|
|
@@ -188,4 +188,84 @@ export function registerCoclawCli({ program, logger: _logger }, deps = {}) {
|
|
|
188
188
|
process.exitCode = 1;
|
|
189
189
|
}
|
|
190
190
|
});
|
|
191
|
+
|
|
192
|
+
// 开发期辅助:provider-auth 三个 RPC 的瘦 CLI。
|
|
193
|
+
// 与 bind/unbind 一致——参数解析后调 gateway RPC,不重复业务逻辑。
|
|
194
|
+
const auth = coclaw
|
|
195
|
+
.command('auth')
|
|
196
|
+
.description('Manage provider auth credentials (developer helper)');
|
|
197
|
+
|
|
198
|
+
auth
|
|
199
|
+
.command('set-api-key <provider>')
|
|
200
|
+
.description('Store an API key for a provider')
|
|
201
|
+
.requiredOption('--key <key>', 'API key value (plaintext)')
|
|
202
|
+
.option('--profile-id <id>', 'Override profileId (default: <provider>:default)')
|
|
203
|
+
.action(async (provider, opts) => {
|
|
204
|
+
try {
|
|
205
|
+
const params = { provider, apiKey: opts.key };
|
|
206
|
+
if (opts.profileId) params.profileId = opts.profileId;
|
|
207
|
+
const result = await callWithRetry('coclaw.providerAuth.setApiKey', deps, {
|
|
208
|
+
params, timeoutMs: RPC_TIMEOUT_MS,
|
|
209
|
+
});
|
|
210
|
+
if (!result.ok) {
|
|
211
|
+
handleRpcError(result, 'set-api-key failed');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const data = result.payload;
|
|
215
|
+
console.log(apiKeySetOk({ provider, profileId: data?.profileId ?? `${provider}:default` }));
|
|
216
|
+
}
|
|
217
|
+
/* c8 ignore next 4 -- callGatewayMethod 不会抛异常,纯防御 */
|
|
218
|
+
catch (err) {
|
|
219
|
+
console.error(`Error: ${resolveErrorMessage(err)}`);
|
|
220
|
+
process.exitCode = 1;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
auth
|
|
225
|
+
.command('list')
|
|
226
|
+
.description('List stored auth profiles')
|
|
227
|
+
.option('--provider <provider>', 'Filter by provider id')
|
|
228
|
+
.action(async (opts) => {
|
|
229
|
+
try {
|
|
230
|
+
const rpcOpts = { timeoutMs: RPC_TIMEOUT_MS };
|
|
231
|
+
if (opts.provider) rpcOpts.params = { provider: opts.provider };
|
|
232
|
+
const result = await callWithRetry('coclaw.providerAuth.list', deps, rpcOpts);
|
|
233
|
+
if (!result.ok) {
|
|
234
|
+
handleRpcError(result, 'list failed');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const profiles = result.payload?.profiles ?? [];
|
|
238
|
+
if (profiles.length === 0) {
|
|
239
|
+
console.log(authListEmpty(opts.provider));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
console.log(authListEntries(profiles));
|
|
243
|
+
}
|
|
244
|
+
/* c8 ignore next 4 -- callGatewayMethod 不会抛异常,纯防御 */
|
|
245
|
+
catch (err) {
|
|
246
|
+
console.error(`Error: ${resolveErrorMessage(err)}`);
|
|
247
|
+
process.exitCode = 1;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
auth
|
|
252
|
+
.command('remove <provider>')
|
|
253
|
+
.description('Remove all stored auth profiles for a provider')
|
|
254
|
+
.action(async (provider) => {
|
|
255
|
+
try {
|
|
256
|
+
const result = await callWithRetry('coclaw.providerAuth.remove', deps, {
|
|
257
|
+
params: { provider }, timeoutMs: RPC_TIMEOUT_MS,
|
|
258
|
+
});
|
|
259
|
+
if (!result.ok) {
|
|
260
|
+
handleRpcError(result, 'remove failed');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
console.log(authRemoveOk(provider));
|
|
264
|
+
}
|
|
265
|
+
/* c8 ignore next 4 -- callGatewayMethod 不会抛异常,纯防御 */
|
|
266
|
+
catch (err) {
|
|
267
|
+
console.error(`Error: ${resolveErrorMessage(err)}`);
|
|
268
|
+
process.exitCode = 1;
|
|
269
|
+
}
|
|
270
|
+
});
|
|
191
271
|
}
|
|
@@ -46,7 +46,7 @@ export function escapeJsonForCmd(json) {
|
|
|
46
46
|
* @param {Function} [spawnFn] - 可注入的 spawn 函数(测试用)
|
|
47
47
|
* @param {object} [opts] - 可选配置(测试用)
|
|
48
48
|
* @param {number} [opts.timeoutMs] - 总超时毫秒数
|
|
49
|
-
* @returns {Promise<{ ok: boolean,
|
|
49
|
+
* @returns {Promise<{ ok: boolean, payload?: unknown, error?: string, message?: string }>}
|
|
50
50
|
*/
|
|
51
51
|
export function callGatewayMethod(method, spawnFn, opts) {
|
|
52
52
|
/* c8 ignore next -- ?? fallback */
|
|
@@ -95,9 +95,9 @@ export function callGatewayMethod(method, spawnFn, opts) {
|
|
|
95
95
|
if (!trimmed) return { ok: false, error: 'empty_output' };
|
|
96
96
|
try {
|
|
97
97
|
const parsed = JSON.parse(trimmed);
|
|
98
|
-
// openclaw gateway call --json
|
|
99
|
-
//
|
|
100
|
-
return { ok: true,
|
|
98
|
+
// openclaw gateway call --json 直接把 handler 的 wire payload 打到 stdout
|
|
99
|
+
// 整体 payload 原样透出,调用方自行读取业务字段(不再抠 .status 一层)
|
|
100
|
+
return { ok: true, payload: parsed };
|
|
101
101
|
} catch {
|
|
102
102
|
// 非 JSON 输出也视为成功(openclaw 非 --json 模式的兜底)
|
|
103
103
|
return { ok: true };
|
package/src/common/messages.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// bind/unbind CLI 及 command 的用户提示文案(统一出口)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// data 容忍 undefined / 缺字段,避免 helper "非 JSON 兜底"分支返回无 payload 时抛 destructure TypeError
|
|
4
|
+
export function bindOk(data) {
|
|
5
|
+
const { clawId = 'unknown', rebound, previousClawId } = data ?? {};
|
|
4
6
|
const action = rebound ? 're-bound' : 'bound';
|
|
5
7
|
const prev = previousClawId
|
|
6
8
|
? ` (previous Claw ${previousClawId} was auto-unbound)`
|
|
@@ -8,9 +10,9 @@ export function bindOk({ clawId, rebound, previousClawId }) {
|
|
|
8
10
|
return `OK. Claw (${clawId}) ${action} to CoClaw.${prev}`;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
export function unbindOk(
|
|
12
|
-
const
|
|
13
|
-
return `OK. Claw (${
|
|
13
|
+
export function unbindOk(data) {
|
|
14
|
+
const { clawId = 'unknown' } = data ?? {};
|
|
15
|
+
return `OK. Claw (${clawId}) unbound from CoClaw.`;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export function notBound() {
|
|
@@ -26,3 +28,39 @@ export function claimCodeCreated({ code, appUrl, expiresMinutes }) {
|
|
|
26
28
|
"If you don't have a CoClaw account yet, you can register on that page.",
|
|
27
29
|
].join('\n');
|
|
28
30
|
}
|
|
31
|
+
|
|
32
|
+
// provider-auth CLI 输出(auth set-api-key / list / remove)
|
|
33
|
+
|
|
34
|
+
export function apiKeySetOk({ provider, profileId }) {
|
|
35
|
+
return `OK. API key for "${provider}" stored (profileId=${profileId}).`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function authListEmpty(provider) {
|
|
39
|
+
return provider
|
|
40
|
+
? `No auth profiles found for provider "${provider}".`
|
|
41
|
+
: 'No auth profiles found.';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 把 list RPC 返回的 profiles 数组渲染成多行文本。
|
|
46
|
+
* 每行格式:`<profileId> <type> <preview-or-meta>`
|
|
47
|
+
* 调用方负责处理空数组(用 authListEmpty)。
|
|
48
|
+
*/
|
|
49
|
+
export function authListEntries(profiles) {
|
|
50
|
+
const lines = profiles.map((p) => {
|
|
51
|
+
const meta = [];
|
|
52
|
+
if (p.keyPreview) meta.push(p.keyPreview);
|
|
53
|
+
if (p.email) meta.push(p.email);
|
|
54
|
+
if (p.displayName) meta.push(p.displayName);
|
|
55
|
+
if (typeof p.expiresAt === 'number') {
|
|
56
|
+
meta.push(`expires=${new Date(p.expiresAt).toISOString()}`);
|
|
57
|
+
}
|
|
58
|
+
const metaStr = meta.length > 0 ? ` ${meta.join(' ')}` : '';
|
|
59
|
+
return `${p.profileId} ${p.type}${metaStr}`;
|
|
60
|
+
});
|
|
61
|
+
return lines.join('\n');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function authRemoveOk(provider) {
|
|
65
|
+
return `OK. Removed all auth profiles for "${provider}".`;
|
|
66
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* model-default/handlers.js —— coclaw.model.set / list 两个 RPC 的纯函数实现
|
|
3
|
+
*
|
|
4
|
+
* 设计要点(详见 docs/model-config-api.md § 3):
|
|
5
|
+
* - DI 注入 sdk(mutateConfigFile / buildModelsProviderData / isProviderAuthProfileConfigured)
|
|
6
|
+
* + loadConfig + resolveAgentDir,便于单测;产线注入在 ./index.js
|
|
7
|
+
* - **出参不加 status wrap**(gateway-method-design skill 新约定):set → {};list → { default, agents }
|
|
8
|
+
* - 错误码只用 INVALID_ARGS / IO_FAILED,参考 provider-auth/handlers.js
|
|
9
|
+
* 既有 plugin 的 respondError 用 INTERNAL_ERROR 与本节契约不一致,所以本模块自带局部 helper
|
|
10
|
+
* - set 校验 fail-fast 顺序:params shape → 拒未知字段 → agentId → primary 类型 → primary 形态
|
|
11
|
+
* (纯字符串:含 '/'、'/' 不在端点;不依赖 cfg)→ loadConfig → provider 凭据 → catalog
|
|
12
|
+
* 形态校验**前置在 loadConfig 之前**,cfg 不可读时非法形态仍是 INVALID_ARGS 而非 IO_FAILED
|
|
13
|
+
* - catalog 校验用 `view: 'all'`:picker 可见性过滤会误判某些合法 provider 不存在(subagent 调研结论)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { listAllPrimaries } from './resolve.js';
|
|
17
|
+
import { writePrimary } from './persist.js';
|
|
18
|
+
|
|
19
|
+
const ALLOWED_KEYS = new Set(['agentId', 'primary']);
|
|
20
|
+
|
|
21
|
+
function respondInvalid(respond, message) {
|
|
22
|
+
respond(false, undefined, { code: 'INVALID_ARGS', message });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function respondIoFailed(respond, err) {
|
|
26
|
+
respond(false, undefined, {
|
|
27
|
+
code: 'IO_FAILED',
|
|
28
|
+
message: String(err?.message ?? err),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isNonEmptyString(v) {
|
|
33
|
+
return typeof v === 'string' && v.trim().length > 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 纯字符串形态拆分:要求 primary 含 '/',且 '/' 不在端点。
|
|
38
|
+
* @returns {{ provider: string, model: string }|null}
|
|
39
|
+
*/
|
|
40
|
+
function parseProviderModel(primary) {
|
|
41
|
+
const slashIdx = primary.indexOf('/');
|
|
42
|
+
if (slashIdx <= 0 || slashIdx === primary.length - 1) return null;
|
|
43
|
+
return {
|
|
44
|
+
provider: primary.slice(0, slashIdx),
|
|
45
|
+
model: primary.slice(slashIdx + 1),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* cfg 相关的 primary 校验:provider 凭据 + catalog 存在性。
|
|
51
|
+
* 形态拆分由调用方完成(fail-fast 前置在 loadConfig 之前)。
|
|
52
|
+
*
|
|
53
|
+
* @returns {Promise<string|null>} 错误 message;null 表通过
|
|
54
|
+
*/
|
|
55
|
+
async function validateProviderCredAndCatalog({ provider, model, primary, cfg, sdk, agentDir }) {
|
|
56
|
+
// 凭据校验:isProviderAuthProfileConfigured 内部就是 listUsableProviderAuthProfileIds().length > 0
|
|
57
|
+
// cooldown profile 仍算"已配置"(cooldown 是临时态,上游 fallback 主循环会主动跳)
|
|
58
|
+
const hasCred = sdk.isProviderAuthProfileConfigured({ provider, cfg, agentDir });
|
|
59
|
+
if (!hasCred) {
|
|
60
|
+
return `provider "${provider}" has no usable auth profile`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// catalog 校验:view: 'all' 绕开 picker 可见性过滤(picker 过滤掉的 provider 不影响合法性)
|
|
64
|
+
const data = await sdk.buildModelsProviderData(cfg, undefined, { view: 'all' });
|
|
65
|
+
const modelSet = data?.byProvider?.get(provider);
|
|
66
|
+
if (!modelSet || !modelSet.has(model)) {
|
|
67
|
+
return `model "${primary}" not found in catalog`;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 构造 set + list 两个 handler。
|
|
74
|
+
*
|
|
75
|
+
* @param {object} opts
|
|
76
|
+
* @param {object} opts.sdk
|
|
77
|
+
* @param {Function} opts.sdk.mutateConfigFile - openclaw/plugin-sdk/config-mutation
|
|
78
|
+
* @param {Function} opts.sdk.buildModelsProviderData - openclaw/plugin-sdk/models-provider-runtime
|
|
79
|
+
* @param {Function} opts.sdk.isProviderAuthProfileConfigured - openclaw/plugin-sdk/provider-auth
|
|
80
|
+
* @param {Function} opts.loadConfig - 返回当前 cfg snapshot;缺失时返回 null
|
|
81
|
+
* @param {Function} opts.resolveAgentDir - 返回 main agent /agent 子目录全路径
|
|
82
|
+
* @returns {{ set: Function, list: Function }}
|
|
83
|
+
*/
|
|
84
|
+
export function buildModelDefaultHandlers({ sdk, loadConfig, resolveAgentDir }) {
|
|
85
|
+
async function set({ params, respond }) {
|
|
86
|
+
try {
|
|
87
|
+
if (!params || typeof params !== 'object' || Array.isArray(params)) {
|
|
88
|
+
respondInvalid(respond, 'params must be an object');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
for (const key of Object.keys(params)) {
|
|
92
|
+
if (!ALLOWED_KEYS.has(key)) {
|
|
93
|
+
respondInvalid(respond, `unknown field: ${key}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const { agentId } = params;
|
|
99
|
+
if (agentId !== undefined && !isNonEmptyString(agentId)) {
|
|
100
|
+
respondInvalid(respond, 'agentId must be a non-empty string when provided');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!Object.hasOwn(params, 'primary')) {
|
|
105
|
+
respondInvalid(respond, 'primary is required');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const primary = params.primary;
|
|
109
|
+
if (primary !== null && typeof primary !== 'string') {
|
|
110
|
+
respondInvalid(respond, 'primary must be a string or null');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (primary !== null && primary.length === 0) {
|
|
114
|
+
respondInvalid(respond, 'primary must be a non-empty string or null');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (primary !== null) {
|
|
119
|
+
// 形态校验前置:纯字符串检查,不依赖 cfg → cfg 不可读时也能给 INVALID_ARGS
|
|
120
|
+
const parts = parseProviderModel(primary);
|
|
121
|
+
if (!parts) {
|
|
122
|
+
respondInvalid(respond, 'primary must look like "<provider>/<model>"');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const cfg = loadConfig();
|
|
126
|
+
if (!cfg) {
|
|
127
|
+
respondIoFailed(respond, new Error('runtime config not available'));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const validationError = await validateProviderCredAndCatalog({
|
|
131
|
+
...parts,
|
|
132
|
+
primary,
|
|
133
|
+
cfg,
|
|
134
|
+
sdk,
|
|
135
|
+
agentDir: resolveAgentDir(),
|
|
136
|
+
});
|
|
137
|
+
if (validationError) {
|
|
138
|
+
respondInvalid(respond, validationError);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 单独 catch 写盘错误为 IO_FAILED;外层 catch 兜更上层异常(DI 调用本身崩之类)
|
|
144
|
+
try {
|
|
145
|
+
await writePrimary(
|
|
146
|
+
{ agentId, primary },
|
|
147
|
+
{ mutateConfigFile: sdk.mutateConfigFile },
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
respondIoFailed(respond, err);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
respond(true, {});
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
respondIoFailed(respond, err);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function list({ respond }) {
|
|
163
|
+
try {
|
|
164
|
+
const cfg = loadConfig();
|
|
165
|
+
if (!cfg) {
|
|
166
|
+
respondIoFailed(respond, new Error('runtime config not available'));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
respond(true, listAllPrimaries(cfg));
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
respondIoFailed(respond, err);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { set, list };
|
|
177
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* model-default 注册入口 —— 把 coclaw.model.set / list 接到 gateway。
|
|
3
|
+
*
|
|
4
|
+
* 设计(同 provider-auth/index.js):
|
|
5
|
+
* - 三个 SDK 子入口(config-mutation / models-provider-runtime / provider-auth)懒加载,
|
|
6
|
+
* 首次 RPC 调用才解析;失败硬编码 IO_FAILED 透 message
|
|
7
|
+
* - mainAgentDir 走 claw-paths.js;loadConfig 走 claw-config.js
|
|
8
|
+
* - opts 主要给单测用:可注入 fake sdk 工厂 / fake agentDir resolver / fake loadConfig
|
|
9
|
+
*
|
|
10
|
+
* 生产路径上 loadXxx 必须由入口(plugins/openclaw/index.js)显式注入字面量
|
|
11
|
+
* dynamic import —— 因 OpenClaw plugin loader 只扫入口源码识别
|
|
12
|
+
* `openclaw/plugin-sdk/*` 字面量并触发 jiti 重写;本子模块里的 import 字面量 loader
|
|
13
|
+
* 看不到 → 原生 Node 解析必败(详见 provider-auth/index.js 同款说明)。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { buildModelDefaultHandlers } from './handlers.js';
|
|
17
|
+
import { mainAgentDir } from '../claw-paths.js';
|
|
18
|
+
import { getClawConfig } from '../claw-config.js';
|
|
19
|
+
|
|
20
|
+
let _configMutationP;
|
|
21
|
+
let _modelsP;
|
|
22
|
+
let _providerAuthP;
|
|
23
|
+
|
|
24
|
+
function defaultLoadConfigMutation() {
|
|
25
|
+
_configMutationP ??= import('openclaw/plugin-sdk/config-mutation');
|
|
26
|
+
return _configMutationP;
|
|
27
|
+
}
|
|
28
|
+
function defaultLoadModelsProviderRuntime() {
|
|
29
|
+
_modelsP ??= import('openclaw/plugin-sdk/models-provider-runtime');
|
|
30
|
+
return _modelsP;
|
|
31
|
+
}
|
|
32
|
+
function defaultLoadProviderAuth() {
|
|
33
|
+
_providerAuthP ??= import('openclaw/plugin-sdk/provider-auth');
|
|
34
|
+
return _providerAuthP;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 测试辅助:清掉懒加载 SDK 缓存。
|
|
39
|
+
*/
|
|
40
|
+
export function __resetSdkCaches() {
|
|
41
|
+
_configMutationP = undefined;
|
|
42
|
+
_modelsP = undefined;
|
|
43
|
+
_providerAuthP = undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 在 gateway api 上注册 `coclaw.model.set` / `coclaw.model.list`。
|
|
48
|
+
*
|
|
49
|
+
* 仅 `register(api)` 的 `if (api.registrationMode === 'full')` 分支调;
|
|
50
|
+
* 其它 mode 注册副作用违规(参 plugins/openclaw/CLAUDE.md "Service / register 副作用边界")。
|
|
51
|
+
*
|
|
52
|
+
* @param {object} api - OpenClaw 注入的 plugin api
|
|
53
|
+
* @param {object} [opts]
|
|
54
|
+
* @param {Function} [opts.resolveAgentDir] - 覆盖 agentDir 解析(默认 mainAgentDir)
|
|
55
|
+
* @param {Function} [opts.loadConfig] - 覆盖 cfg 读取(默认 getClawConfig)
|
|
56
|
+
* @param {Function} [opts.loadConfigMutation] - 必传(生产由入口注入字面量 dynamic import)
|
|
57
|
+
* @param {Function} [opts.loadModelsProviderRuntime] - 必传(同上)
|
|
58
|
+
* @param {Function} [opts.loadProviderAuth] - 必传(同上)
|
|
59
|
+
*/
|
|
60
|
+
export function registerModelDefaultHandlers(api, opts = {}) {
|
|
61
|
+
const resolveAgentDir = opts.resolveAgentDir ?? mainAgentDir;
|
|
62
|
+
const loadConfig = opts.loadConfig ?? getClawConfig;
|
|
63
|
+
const loadConfigMutation = opts.loadConfigMutation ?? defaultLoadConfigMutation;
|
|
64
|
+
const loadModelsProviderRuntime = opts.loadModelsProviderRuntime ?? defaultLoadModelsProviderRuntime;
|
|
65
|
+
const loadProviderAuth = opts.loadProviderAuth ?? defaultLoadProviderAuth;
|
|
66
|
+
|
|
67
|
+
let handlersPromise;
|
|
68
|
+
async function getHandlers() {
|
|
69
|
+
if (!handlersPromise) {
|
|
70
|
+
handlersPromise = (async () => {
|
|
71
|
+
const [configMutation, modelsRuntime, providerAuth] = await Promise.all([
|
|
72
|
+
loadConfigMutation(),
|
|
73
|
+
loadModelsProviderRuntime(),
|
|
74
|
+
loadProviderAuth(),
|
|
75
|
+
]);
|
|
76
|
+
const sdk = {
|
|
77
|
+
mutateConfigFile: configMutation.mutateConfigFile,
|
|
78
|
+
buildModelsProviderData: modelsRuntime.buildModelsProviderData,
|
|
79
|
+
isProviderAuthProfileConfigured: providerAuth.isProviderAuthProfileConfigured,
|
|
80
|
+
};
|
|
81
|
+
return buildModelDefaultHandlers({ sdk, loadConfig, resolveAgentDir });
|
|
82
|
+
})();
|
|
83
|
+
}
|
|
84
|
+
return handlersPromise;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function wrap(methodName) {
|
|
88
|
+
return async (ctx) => {
|
|
89
|
+
let handlers;
|
|
90
|
+
try {
|
|
91
|
+
handlers = await getHandlers();
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
ctx.respond(false, undefined, {
|
|
95
|
+
code: 'IO_FAILED',
|
|
96
|
+
message: String(err?.message ?? err),
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
await handlers[methodName](ctx);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
api.registerGatewayMethod('coclaw.model.set', wrap('set'));
|
|
105
|
+
api.registerGatewayMethod('coclaw.model.list', wrap('list'));
|
|
106
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* model-default/persist.js —— 把 default/per-agent primary 字段级写回 cfg
|
|
3
|
+
*
|
|
4
|
+
* 关键约束(钉死自 D1 dump v9 + zod-schema.agent-model.ts AgentModelSchema):
|
|
5
|
+
* - 走 mutateConfigFile —— mutator 拿到的是 structuredClone 出的 deep draft,字段级修改安全;
|
|
6
|
+
* 但**整体重写 model 字段会丢失 fallbacks / timeoutMs 等兄弟字段**,必须按现有形态分支处理
|
|
7
|
+
* - model 字段有三态:string 简写(等价 { primary })/ object / 缺省
|
|
8
|
+
* - object 形态:保留 fallbacks / timeoutMs,只动 primary 一项
|
|
9
|
+
* - string 形态:升级成 { primary }(原 string 本来就只有 primary 这一项语义,无损)
|
|
10
|
+
* - 缺省形态:直接置 { primary }
|
|
11
|
+
* - clear primary 时:object 上 delete primary;若 object 删掉 primary 后没剩字段,删整个 model;
|
|
12
|
+
* string 形态直接删 model;entry / defaults 容器留空壳,不主动从 list 删(dump v9 § "删除某 scope 覆盖")
|
|
13
|
+
* - last-writer-wins,不传 baseHash(mutate.ts assertBaseHashMatches 在 undefined 时直接通过)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 把 default 或某 agent 的 primary 写回 cfg。
|
|
18
|
+
*
|
|
19
|
+
* @param {object} args
|
|
20
|
+
* @param {string} [args.agentId] - 缺省 = default scope;传非空 string = per-agent scope
|
|
21
|
+
* @param {string|null} args.primary - 非空 string 为设;null 为清
|
|
22
|
+
* @param {object} deps
|
|
23
|
+
* @param {Function} deps.mutateConfigFile - openclaw/plugin-sdk/config-mutation 的 mutateConfigFile
|
|
24
|
+
*/
|
|
25
|
+
export async function writePrimary(args, deps) {
|
|
26
|
+
const { agentId, primary } = args;
|
|
27
|
+
const { mutateConfigFile } = deps;
|
|
28
|
+
await mutateConfigFile({
|
|
29
|
+
mutate(draft) {
|
|
30
|
+
ensureAgents(draft);
|
|
31
|
+
if (agentId === undefined || agentId === null) {
|
|
32
|
+
applyDefaultScope(draft, primary);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
applyAgentScope(draft, agentId, primary);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ensureAgents(draft) {
|
|
41
|
+
const a = draft.agents;
|
|
42
|
+
if (!a || typeof a !== 'object' || Array.isArray(a)) {
|
|
43
|
+
draft.agents = {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function applyDefaultScope(draft, primary) {
|
|
48
|
+
if (primary === null) {
|
|
49
|
+
clearOnContainer(draft.agents.defaults);
|
|
50
|
+
// defaults 容器即便清空也保留,避免影响 cfg 中其它 defaults.* 字段(这里没改它们)
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const d = draft.agents.defaults;
|
|
54
|
+
if (!d || typeof d !== 'object' || Array.isArray(d)) {
|
|
55
|
+
draft.agents.defaults = {};
|
|
56
|
+
}
|
|
57
|
+
setOnContainer(draft.agents.defaults, primary);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function applyAgentScope(draft, agentId, primary) {
|
|
61
|
+
if (!Array.isArray(draft.agents.list)) {
|
|
62
|
+
draft.agents.list = [];
|
|
63
|
+
}
|
|
64
|
+
const idx = draft.agents.list.findIndex((e) => e?.id === agentId);
|
|
65
|
+
if (primary === null) {
|
|
66
|
+
if (idx === -1) return; // 没该 entry,无操作
|
|
67
|
+
clearOnContainer(draft.agents.list[idx]);
|
|
68
|
+
// dump v9:entry 整个就只剩这一项时留空壳,不主动从 list 删
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (idx === -1) {
|
|
72
|
+
// AgentEntrySchema 内部所有子 schema 都 optional 或外层 optional,
|
|
73
|
+
// { id, model: { primary } } 是合法最小 entry(核源 zod-schema.agent-runtime.ts:889)
|
|
74
|
+
draft.agents.list.push({ id: agentId, model: { primary } });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
setOnContainer(draft.agents.list[idx], primary);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 在容器(defaults 对象 或 list[i] entry)上字段级设置 model.primary。
|
|
82
|
+
* 容器存在性已由调用方保证。
|
|
83
|
+
*/
|
|
84
|
+
function setOnContainer(container, primary) {
|
|
85
|
+
const cur = container.model;
|
|
86
|
+
if (cur && typeof cur === 'object') {
|
|
87
|
+
// 保留 fallbacks / timeoutMs 等兄弟字段
|
|
88
|
+
cur.primary = primary;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// string / undefined / null:原状态没有可保留的兄弟字段,直接置 { primary }
|
|
92
|
+
container.model = { primary };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 在容器上字段级清除 model.primary。
|
|
97
|
+
* 容器不存在 / model 不存在 → 无操作。
|
|
98
|
+
*/
|
|
99
|
+
function clearOnContainer(container) {
|
|
100
|
+
if (!container || typeof container !== 'object') return;
|
|
101
|
+
const cur = container.model;
|
|
102
|
+
if (cur === undefined || cur === null) return;
|
|
103
|
+
if (typeof cur === 'string') {
|
|
104
|
+
// string 形态整个就只表达 primary,删 model 即清
|
|
105
|
+
delete container.model;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (typeof cur === 'object') {
|
|
109
|
+
delete cur.primary;
|
|
110
|
+
// primary 是 model object 唯一字段时整体删,避免留 {} 空壳
|
|
111
|
+
if (Object.keys(cur).length === 0) {
|
|
112
|
+
delete container.model;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* model-default/resolve.js —— 从 cfg 读 default + per-agent primary 的纯函数
|
|
3
|
+
*
|
|
4
|
+
* model 字段三态(见上游 zod-schema.agent-model.ts AgentModelSchema):
|
|
5
|
+
* - string:modelId 简写形态,等价于 { primary: <string> }
|
|
6
|
+
* - object:{ primary?, fallbacks?, timeoutMs? },primary 可缺省(schema 标了 optional)
|
|
7
|
+
* - 缺省 / null:未设
|
|
8
|
+
*
|
|
9
|
+
* D1 只读 primary 字段;fallbacks / timeoutMs 在 list RPC 出参里不暴露
|
|
10
|
+
* (设计 dump v9 § "RPC method"、docs/model-config-api.md § 3)。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const MAIN_AGENT_ID = 'main';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 从 model 字段(string|object|null|undefined)中提取 primary。
|
|
17
|
+
* @param {unknown} modelField - cfg.agents.defaults.model 或 cfg.agents.list[i].model
|
|
18
|
+
* @returns {string|null}
|
|
19
|
+
*/
|
|
20
|
+
export function readPrimaryFromModel(modelField) {
|
|
21
|
+
if (typeof modelField === 'string') {
|
|
22
|
+
return modelField.length > 0 ? modelField : null;
|
|
23
|
+
}
|
|
24
|
+
if (modelField && typeof modelField === 'object') {
|
|
25
|
+
const p = modelField.primary;
|
|
26
|
+
return typeof p === 'string' && p.length > 0 ? p : null;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 读 default scope 的 primary(cfg.agents.defaults.model)
|
|
33
|
+
* @param {object} cfg
|
|
34
|
+
* @returns {string|null}
|
|
35
|
+
*/
|
|
36
|
+
export function readDefaultPrimary(cfg) {
|
|
37
|
+
return readPrimaryFromModel(cfg?.agents?.defaults?.model);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 读某 agent 的 primary(cfg.agents.list[i].model)。
|
|
42
|
+
* 未找到该 agentId 也返回 null(与"未设"不区分)。
|
|
43
|
+
* @param {object} cfg
|
|
44
|
+
* @param {string} agentId
|
|
45
|
+
* @returns {string|null}
|
|
46
|
+
*/
|
|
47
|
+
export function readAgentPrimary(cfg, agentId) {
|
|
48
|
+
const entry = findAgentEntry(cfg, agentId);
|
|
49
|
+
return entry ? readPrimaryFromModel(entry.model) : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 在 cfg.agents.list 里按 id 找 entry。
|
|
54
|
+
* @returns {object|null}
|
|
55
|
+
*/
|
|
56
|
+
export function findAgentEntry(cfg, agentId) {
|
|
57
|
+
const list = cfg?.agents?.list;
|
|
58
|
+
if (!Array.isArray(list)) return null;
|
|
59
|
+
return list.find((e) => e?.id === agentId) ?? null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 装配 coclaw.model.list 出参(D1 契约见 docs/model-config-api.md § 3)。
|
|
64
|
+
* agents 来源:cfg.agents.list 所有 entry + 永远包含 main(心智模型 § 3.5)。
|
|
65
|
+
* @param {object} cfg
|
|
66
|
+
* @returns {{
|
|
67
|
+
* default: { primary: string|null },
|
|
68
|
+
* agents: Record<string, { primary: string|null }>,
|
|
69
|
+
* }}
|
|
70
|
+
*/
|
|
71
|
+
export function listAllPrimaries(cfg) {
|
|
72
|
+
const out = {
|
|
73
|
+
default: { primary: readDefaultPrimary(cfg) },
|
|
74
|
+
agents: {},
|
|
75
|
+
};
|
|
76
|
+
const list = cfg?.agents?.list;
|
|
77
|
+
if (Array.isArray(list)) {
|
|
78
|
+
for (const entry of list) {
|
|
79
|
+
if (!entry || typeof entry.id !== 'string' || entry.id.length === 0) continue;
|
|
80
|
+
out.agents[entry.id] = { primary: readPrimaryFromModel(entry.model) };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// main agent 始终存在(心智模型 § 3.5):cfg list 没显式 main entry 时补一条 primary=null
|
|
84
|
+
if (!Object.hasOwn(out.agents, MAIN_AGENT_ID)) {
|
|
85
|
+
out.agents[MAIN_AGENT_ID] = { primary: null };
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|