openclaw-vchat-plugin 0.0.7 → 0.0.8
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/dist/commands.d.ts +1 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +37 -62
- package/dist/commands.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -1
- package/dist/constants.js.map +1 -1
- package/dist/index.js +49 -8
- package/dist/index.js.map +1 -1
- package/dist/message-handler.d.ts.map +1 -1
- package/dist/message-handler.js +11 -4
- package/dist/message-handler.js.map +1 -1
- package/dist/relay-server.d.ts.map +1 -1
- package/dist/relay-server.js +159 -7
- package/dist/relay-server.js.bak-self-send-filter-20260310 +1028 -0
- package/dist/relay-server.js.map +1 -1
- package/dist/routes/config.routes.d.ts.map +1 -1
- package/dist/routes/config.routes.js +10 -0
- package/dist/routes/config.routes.js.map +1 -1
- package/dist/services/config.service.d.ts +8 -0
- package/dist/services/config.service.d.ts.map +1 -1
- package/dist/services/config.service.js +200 -2
- package/dist/services/config.service.js.map +1 -1
- package/package.json +1 -1
- package/src/commands.ts +38 -61
- package/src/constants.ts +4 -1
- package/src/index.ts +46 -8
- package/src/message-handler.ts +19 -5
- package/src/relay-server.ts +181 -7
- package/src/routes/config.routes.ts +10 -0
- package/src/services/config.service.ts +223 -3
package/src/relay-server.ts
CHANGED
|
@@ -4,6 +4,7 @@ import multer from 'multer';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import http from 'http';
|
|
7
|
+
import { execFileSync } from 'child_process';
|
|
7
8
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
8
9
|
import { randomUUID } from 'crypto';
|
|
9
10
|
import { SessionManager } from './session-manager';
|
|
@@ -26,6 +27,7 @@ interface GatewayWechatSession {
|
|
|
26
27
|
lastMessage: string;
|
|
27
28
|
lastMessageTime: number;
|
|
28
29
|
createdAt: number;
|
|
30
|
+
model?: string;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
type RequestMemoEntry<T> = {
|
|
@@ -69,6 +71,7 @@ function toBridgeSessionPayload(item: {
|
|
|
69
71
|
lastMessageTime?: number;
|
|
70
72
|
createdAt?: number;
|
|
71
73
|
agentId?: string;
|
|
74
|
+
model?: string;
|
|
72
75
|
}) {
|
|
73
76
|
const createdAt = Number(item.createdAt) || Date.now();
|
|
74
77
|
return {
|
|
@@ -78,6 +81,7 @@ function toBridgeSessionPayload(item: {
|
|
|
78
81
|
lastMessageTime: Number(item.lastMessageTime) || createdAt,
|
|
79
82
|
createdAt,
|
|
80
83
|
agentId: String(item.agentId || '').trim() || 'main',
|
|
84
|
+
model: String(item.model || '').trim(),
|
|
81
85
|
};
|
|
82
86
|
}
|
|
83
87
|
|
|
@@ -148,14 +152,75 @@ function isRenderableHistoryRole(roleRaw: string): boolean {
|
|
|
148
152
|
return roleRaw === 'user' || roleRaw === 'assistant' || roleRaw === 'system';
|
|
149
153
|
}
|
|
150
154
|
|
|
151
|
-
function
|
|
155
|
+
function getHistoryProvenance(row: any): any {
|
|
156
|
+
if (!row || typeof row !== 'object') return null;
|
|
157
|
+
return row?.provenance || row?.message?.provenance || null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function shouldHideHistoryRow(row: any, currentSessionKey?: string): boolean {
|
|
161
|
+
const provenance = getHistoryProvenance(row);
|
|
162
|
+
if (!provenance || typeof provenance !== 'object') return false;
|
|
163
|
+
|
|
164
|
+
const kind = String(provenance?.kind || '').trim().toLowerCase();
|
|
165
|
+
if (kind !== 'inter_session') return false;
|
|
166
|
+
|
|
167
|
+
const sourceTool = String(provenance?.sourceTool || '').trim().toLowerCase();
|
|
168
|
+
const sourceSessionKey = String(provenance?.sourceSessionKey || '').trim().toLowerCase();
|
|
169
|
+
const targetSessionKey = String(currentSessionKey || '').trim().toLowerCase();
|
|
170
|
+
|
|
171
|
+
// Hide self-routed relay messages. They are persisted by OpenClaw with user role,
|
|
172
|
+
// but should not be rendered as human-authored bubbles in the WeChat UI.
|
|
173
|
+
return sourceTool === 'sessions_send' && Boolean(targetSessionKey) && sourceSessionKey === targetSessionKey;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function normalizePreviewMessageText(raw: any, currentSessionKey?: string): string {
|
|
152
177
|
if (!raw || typeof raw !== 'object') return normalizeMessageText(raw);
|
|
153
178
|
const roleRaw = getHistoryRoleRaw(raw);
|
|
154
179
|
if (!isRenderableHistoryRole(roleRaw)) return '';
|
|
180
|
+
if (shouldHideHistoryRow(raw, currentSessionKey)) return '';
|
|
155
181
|
return normalizeMessageText(raw);
|
|
156
182
|
}
|
|
157
183
|
|
|
158
|
-
function
|
|
184
|
+
function isInternalSessionTitle(raw: any): boolean {
|
|
185
|
+
const title = String(raw || '').trim();
|
|
186
|
+
if (!title) return true;
|
|
187
|
+
|
|
188
|
+
const lower = title.toLowerCase();
|
|
189
|
+
if (
|
|
190
|
+
lower === 'openclaw-wechat-plugin'
|
|
191
|
+
|| lower === 'openclaw-vchat-plugin'
|
|
192
|
+
|| lower === 'openclaw-vchat'
|
|
193
|
+
|| lower === 'openclaw-tui'
|
|
194
|
+
) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
lower.startsWith('webchat:')
|
|
200
|
+
|| lower.startsWith('agent:')
|
|
201
|
+
|| lower.includes('wechat:direct:')
|
|
202
|
+
|| lower.includes('g-agent-')
|
|
203
|
+
|| /^[a-f0-9-]{24,}$/i.test(title)
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function pickVisibleSessionTitle(row: any, lastMessage: string): string {
|
|
208
|
+
const candidates = [row?.title, row?.displayName, row?.name];
|
|
209
|
+
for (const candidate of candidates) {
|
|
210
|
+
const title = String(candidate || '').trim();
|
|
211
|
+
if (!title || isInternalSessionTitle(title)) continue;
|
|
212
|
+
return title;
|
|
213
|
+
}
|
|
214
|
+
return String(lastMessage || '').trim() || '新对话';
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function normalizeHistoryMessages(
|
|
218
|
+
rawPayload: any,
|
|
219
|
+
sessionId: string,
|
|
220
|
+
limit: number,
|
|
221
|
+
before?: number,
|
|
222
|
+
currentSessionKey?: string
|
|
223
|
+
): Message[] {
|
|
159
224
|
const payload = rawPayload || {};
|
|
160
225
|
const rows = Array.isArray(payload.messages) ? payload.messages
|
|
161
226
|
: Array.isArray(payload.items) ? payload.items
|
|
@@ -164,6 +229,7 @@ function normalizeHistoryMessages(rawPayload: any, sessionId: string, limit: num
|
|
|
164
229
|
: [];
|
|
165
230
|
|
|
166
231
|
const mapped = rows.map((row: any) => {
|
|
232
|
+
if (shouldHideHistoryRow(row, currentSessionKey)) return null;
|
|
167
233
|
const roleRaw = getHistoryRoleRaw(row) || 'assistant';
|
|
168
234
|
if (!isRenderableHistoryRole(roleRaw)) return null;
|
|
169
235
|
const role = roleRaw === 'user' ? 'user' : roleRaw === 'system' ? 'system' : 'assistant';
|
|
@@ -192,12 +258,100 @@ function normalizeHistoryMessages(rawPayload: any, sessionId: string, limit: num
|
|
|
192
258
|
return filtered.slice(filtered.length - limit);
|
|
193
259
|
}
|
|
194
260
|
|
|
261
|
+
function readConfiguredPrimaryModel(gateway: GatewayClient): string {
|
|
262
|
+
try {
|
|
263
|
+
const raw = gateway.readConfig();
|
|
264
|
+
const agents = Array.isArray(raw?.agents?.list) ? raw.agents.list : [];
|
|
265
|
+
const mainAgent = agents.find((item: any) => String(item?.id || '').trim() === 'main');
|
|
266
|
+
const candidates = [
|
|
267
|
+
mainAgent?.model,
|
|
268
|
+
raw?.agents?.defaults?.model?.primary,
|
|
269
|
+
raw?.agents?.defaults?.primary,
|
|
270
|
+
raw?.defaults?.primary,
|
|
271
|
+
raw?.model,
|
|
272
|
+
];
|
|
273
|
+
for (const candidate of candidates) {
|
|
274
|
+
const value = String(candidate || '').trim();
|
|
275
|
+
if (value) return value;
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
// ignore
|
|
279
|
+
}
|
|
280
|
+
return '';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function readOpenClawVersion(): string {
|
|
284
|
+
try {
|
|
285
|
+
const value = execFileSync('openclaw', ['--version'], {
|
|
286
|
+
encoding: 'utf8',
|
|
287
|
+
timeout: 3000,
|
|
288
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
289
|
+
});
|
|
290
|
+
return String(value || '').trim();
|
|
291
|
+
} catch {
|
|
292
|
+
return '';
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function getOpenClawSummary(gateway: GatewayClient): Promise<{
|
|
297
|
+
online: boolean;
|
|
298
|
+
version: string;
|
|
299
|
+
model: string;
|
|
300
|
+
error?: string;
|
|
301
|
+
}> {
|
|
302
|
+
const version = readOpenClawVersion();
|
|
303
|
+
try {
|
|
304
|
+
const status = await gateway.call('status', {});
|
|
305
|
+
const recent = status?.sessions?.recent?.[0];
|
|
306
|
+
const model = String(
|
|
307
|
+
status?.sessions?.defaults?.model
|
|
308
|
+
|| recent?.model
|
|
309
|
+
|| readConfiguredPrimaryModel(gateway)
|
|
310
|
+
|| ''
|
|
311
|
+
).trim();
|
|
312
|
+
return {
|
|
313
|
+
online: true,
|
|
314
|
+
version,
|
|
315
|
+
model,
|
|
316
|
+
};
|
|
317
|
+
} catch (err: any) {
|
|
318
|
+
return {
|
|
319
|
+
online: false,
|
|
320
|
+
version,
|
|
321
|
+
model: readConfiguredPrimaryModel(gateway),
|
|
322
|
+
error: err?.message || 'gateway-unavailable',
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function buildGatewayRecentModelHints(statusPayload: any) {
|
|
328
|
+
const hints = new Map<string, string>();
|
|
329
|
+
const recent = Array.isArray(statusPayload?.sessions?.recent) ? statusPayload.sessions.recent : [];
|
|
330
|
+
|
|
331
|
+
for (const row of recent) {
|
|
332
|
+
const model = String(row?.model || '').trim();
|
|
333
|
+
if (!model) continue;
|
|
334
|
+
|
|
335
|
+
const key = String(row?.key || row?.sessionKey || '').trim().toLowerCase();
|
|
336
|
+
if (key) {
|
|
337
|
+
hints.set(key, model);
|
|
338
|
+
const parsed = parseWeChatDirectThreadSessionKey(key);
|
|
339
|
+
if (parsed) {
|
|
340
|
+
hints.set(`${parsed.userId}:${parsed.agentId}:${parsed.sessionId}`, model);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return hints;
|
|
346
|
+
}
|
|
347
|
+
|
|
195
348
|
async function listGatewayWechatSessions(gateway: GatewayClient, userId: string, agentId?: string): Promise<GatewayWechatSession[]> {
|
|
196
349
|
const targetUserId = sanitizeWeChatId(userId, 'unknown');
|
|
197
350
|
const targetAgentId = sanitizeWeChatId(agentId, '');
|
|
198
351
|
const response = await gateway.call('sessions.list', {});
|
|
199
352
|
const rawSessions = Array.isArray(response?.sessions) ? response.sessions : [];
|
|
200
353
|
const now = Date.now();
|
|
354
|
+
let recentModelHints: Map<string, string> | null = null;
|
|
201
355
|
|
|
202
356
|
const rows: GatewayWechatSession[] = [];
|
|
203
357
|
for (const row of rawSessions) {
|
|
@@ -208,19 +362,36 @@ async function listGatewayWechatSessions(gateway: GatewayClient, userId: string,
|
|
|
208
362
|
if (parsed.userId !== targetUserId) continue;
|
|
209
363
|
if (targetAgentId && parsed.agentId !== targetAgentId) continue;
|
|
210
364
|
|
|
211
|
-
const lastMessage = normalizePreviewMessageText(row?.lastMessage || row?.preview || row?.summary || '');
|
|
212
|
-
const title =
|
|
365
|
+
const lastMessage = normalizePreviewMessageText(row?.lastMessage || row?.preview || row?.summary || '', key);
|
|
366
|
+
const title = pickVisibleSessionTitle(row, lastMessage);
|
|
213
367
|
const createdAt = parseTimestamp(row?.createdAt) || pickTimestamp(row, now);
|
|
214
368
|
const lastMessageTime = pickTimestamp(row, createdAt || now);
|
|
369
|
+
const modelProvider = String(row?.modelProvider || '').trim();
|
|
370
|
+
const modelId = String(row?.model || '').trim();
|
|
371
|
+
let model = modelProvider && modelId ? `${modelProvider}/${modelId}` : modelId;
|
|
372
|
+
if (!model) {
|
|
373
|
+
if (!recentModelHints) {
|
|
374
|
+
try {
|
|
375
|
+
const status = await gateway.call('status', {});
|
|
376
|
+
recentModelHints = buildGatewayRecentModelHints(status);
|
|
377
|
+
} catch {
|
|
378
|
+
recentModelHints = new Map();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
model = recentModelHints.get(key)
|
|
382
|
+
|| recentModelHints.get(`${parsed.userId}:${parsed.agentId}:${parsed.sessionId}`)
|
|
383
|
+
|| '';
|
|
384
|
+
}
|
|
215
385
|
rows.push({
|
|
216
386
|
id: parsed.sessionId,
|
|
217
387
|
userId: targetUserId,
|
|
218
388
|
agentId: parsed.agentId,
|
|
219
389
|
gatewaySessionKey: key,
|
|
220
|
-
title
|
|
390
|
+
title,
|
|
221
391
|
lastMessage,
|
|
222
392
|
lastMessageTime,
|
|
223
393
|
createdAt: createdAt || lastMessageTime || now,
|
|
394
|
+
model,
|
|
224
395
|
});
|
|
225
396
|
}
|
|
226
397
|
|
|
@@ -270,7 +441,7 @@ async function callGatewayHistory(
|
|
|
270
441
|
for (const params of attempts) {
|
|
271
442
|
try {
|
|
272
443
|
const result = await gateway.call('chat.history', params);
|
|
273
|
-
const messages = normalizeHistoryMessages(result, sessionId, limit, before);
|
|
444
|
+
const messages = normalizeHistoryMessages(result, sessionId, limit, before, key);
|
|
274
445
|
const hasMore = Boolean(result?.hasMore) || messages.length === limit;
|
|
275
446
|
return { messages, hasMore };
|
|
276
447
|
} catch (err: any) {
|
|
@@ -387,12 +558,14 @@ export function createRelayServer(config: PluginConfig, gateway: GatewayClient)
|
|
|
387
558
|
const internal = express.Router();
|
|
388
559
|
internal.use(internalAuth);
|
|
389
560
|
|
|
390
|
-
internal.get('/bridge/capabilities', (_req: Request, res: Response) => {
|
|
561
|
+
internal.get('/bridge/capabilities', async (_req: Request, res: Response) => {
|
|
562
|
+
const openclaw = await getOpenClawSummary(gateway);
|
|
391
563
|
res.json({
|
|
392
564
|
protocolVersion: BRIDGE_PROTOCOL_VERSION,
|
|
393
565
|
pluginVersion: BRIDGE_PLUGIN_VERSION,
|
|
394
566
|
capabilities: Array.from(BRIDGE_CAPABILITIES),
|
|
395
567
|
bridgeRole: 'gateway-bridge',
|
|
568
|
+
openclaw,
|
|
396
569
|
runtime: {
|
|
397
570
|
persistMessagesEnabled: sessionManager.isPersistMessagesEnabled(),
|
|
398
571
|
localFallbackEnabled: Boolean(config.allowLocalFallback),
|
|
@@ -429,6 +602,7 @@ export function createRelayServer(config: PluginConfig, gateway: GatewayClient)
|
|
|
429
602
|
lastMessageTime: item.lastMessageTime || item.createdAt || Date.now(),
|
|
430
603
|
createdAt: local?.createdAt || item.createdAt,
|
|
431
604
|
agentId: item.agentId,
|
|
605
|
+
model: item.model,
|
|
432
606
|
});
|
|
433
607
|
}).sort((a, b) => b.lastMessageTime - a.lastMessageTime);
|
|
434
608
|
|
|
@@ -27,6 +27,16 @@ router.get('/providers', (req: Request, res: Response) => {
|
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
router.get('/models', (req: Request, res: Response) => {
|
|
31
|
+
try {
|
|
32
|
+
const models = configService.getConfiguredModels();
|
|
33
|
+
res.json({ models });
|
|
34
|
+
} catch (err: any) {
|
|
35
|
+
console.error('[ConfigRoutes] getConfiguredModels 错误:', err);
|
|
36
|
+
res.status(500).json({ error: err.message || '获取模型列表失败' });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
30
40
|
/**
|
|
31
41
|
* POST /api/config/providers
|
|
32
42
|
* Body: { preset?, providerId?, baseUrl?, apiKey, name?, api?, authHeader?, headers?, models? }
|
|
@@ -22,7 +22,9 @@ const DEFAULT_CONFIG_PATH = path.join(__dirname, '..', '..', 'default-config.jso
|
|
|
22
22
|
function readConfig(): any {
|
|
23
23
|
try {
|
|
24
24
|
const content = fs.readFileSync(OPENCLAW_CONFIG_PATH, 'utf-8');
|
|
25
|
-
|
|
25
|
+
const parsed = JSON.parse(content);
|
|
26
|
+
normalizeAgentList(parsed);
|
|
27
|
+
return parsed;
|
|
26
28
|
} catch {
|
|
27
29
|
return { models: { providers: {} } };
|
|
28
30
|
}
|
|
@@ -150,12 +152,139 @@ function ensureAgentDefaults(config: any): Record<string, any> {
|
|
|
150
152
|
return config.agents.defaults;
|
|
151
153
|
}
|
|
152
154
|
|
|
155
|
+
function normalizeAgentList(config: any): any[] {
|
|
156
|
+
if (!config.agents || typeof config.agents !== 'object') config.agents = {};
|
|
157
|
+
const raw = config.agents.list;
|
|
158
|
+
|
|
159
|
+
if (Array.isArray(raw)) {
|
|
160
|
+
config.agents.list = raw
|
|
161
|
+
.filter((item) => item && typeof item === 'object')
|
|
162
|
+
.map((item: any, index: number) => {
|
|
163
|
+
const entry = { ...item };
|
|
164
|
+
const id = String(entry.id || '').trim() || `agent_${index + 1}`;
|
|
165
|
+
entry.id = id;
|
|
166
|
+
return entry;
|
|
167
|
+
});
|
|
168
|
+
return config.agents.list;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const normalized: any[] = [];
|
|
172
|
+
if (raw && typeof raw === 'object') {
|
|
173
|
+
Object.entries(raw).forEach(([key, value], index) => {
|
|
174
|
+
if (!value || typeof value !== 'object') return;
|
|
175
|
+
const entry: any = { ...(value as Record<string, any>) };
|
|
176
|
+
entry.id = String(entry.id || key || '').trim() || `agent_${index + 1}`;
|
|
177
|
+
normalized.push(entry);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
config.agents.list = normalized;
|
|
182
|
+
return config.agents.list;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function getAgentModel(config: any, agentId = 'main'): string {
|
|
186
|
+
const list = normalizeAgentList(config);
|
|
187
|
+
const hit = list.find((item: any) => String(item?.id || '').trim() === agentId);
|
|
188
|
+
return String(hit?.model || '').trim();
|
|
189
|
+
}
|
|
190
|
+
|
|
153
191
|
function getConfiguredPrimaryModel(config: any): string {
|
|
154
|
-
return String(
|
|
192
|
+
return String(
|
|
193
|
+
getAgentModel(config, 'main')
|
|
194
|
+
|| config?.agents?.defaults?.model?.primary
|
|
195
|
+
|| config?.defaults?.primary
|
|
196
|
+
|| ''
|
|
197
|
+
).trim();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function buildModelRef(providerId: string, modelId: string): string {
|
|
201
|
+
const provider = String(providerId || '').trim();
|
|
202
|
+
const rawModelId = String(modelId || '').trim();
|
|
203
|
+
if (!provider || !rawModelId) return '';
|
|
204
|
+
if (rawModelId.toLowerCase().startsWith(`${provider.toLowerCase()}/`)) {
|
|
205
|
+
return rawModelId;
|
|
206
|
+
}
|
|
207
|
+
return `${provider}/${rawModelId}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function collectProviderModelRefs(providerId: string, providerEntry: any): string[] {
|
|
211
|
+
const models = Array.isArray(providerEntry?.models) ? providerEntry.models : [];
|
|
212
|
+
const refs = models
|
|
213
|
+
.map((model: any) => buildModelRef(providerId, String(model?.id || model?.name || model || '').trim()))
|
|
214
|
+
.filter(Boolean);
|
|
215
|
+
return Array.from(new Set(refs));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function collectAllowedModelRefs(config: any): string[] {
|
|
219
|
+
const defaults = ensureAgentDefaults(config);
|
|
220
|
+
const allowed = defaults?.models;
|
|
221
|
+
const refs = (allowed && typeof allowed === 'object' && !Array.isArray(allowed))
|
|
222
|
+
? Object.keys(allowed).map((key) => String(key || '').trim()).filter(Boolean)
|
|
223
|
+
: [];
|
|
224
|
+
|
|
225
|
+
if (refs.length > 0) return refs;
|
|
226
|
+
|
|
227
|
+
const primary = getConfiguredPrimaryModel(config);
|
|
228
|
+
return primary ? [primary] : [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function reconcileProviderAllowedModels(config: any, providerId: string, providerEntry?: any | null): void {
|
|
232
|
+
const defaults = ensureAgentDefaults(config);
|
|
233
|
+
const agentList = normalizeAgentList(config);
|
|
234
|
+
const currentAllowed = defaults?.models && typeof defaults.models === 'object' && !Array.isArray(defaults.models)
|
|
235
|
+
? defaults.models
|
|
236
|
+
: {};
|
|
237
|
+
|
|
238
|
+
const providerPrefix = `${String(providerId || '').trim().toLowerCase()}/`;
|
|
239
|
+
const nextAllowed: Record<string, any> = {};
|
|
240
|
+
|
|
241
|
+
Object.entries(currentAllowed).forEach(([ref, meta]) => {
|
|
242
|
+
const safeRef = String(ref || '').trim();
|
|
243
|
+
if (!safeRef) return;
|
|
244
|
+
if (providerPrefix && safeRef.toLowerCase().startsWith(providerPrefix)) return;
|
|
245
|
+
nextAllowed[safeRef] = meta && typeof meta === 'object' && !Array.isArray(meta) ? meta : {};
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
collectProviderModelRefs(providerId, providerEntry).forEach((ref) => {
|
|
249
|
+
nextAllowed[ref] = currentAllowed[ref] && typeof currentAllowed[ref] === 'object' && !Array.isArray(currentAllowed[ref])
|
|
250
|
+
? currentAllowed[ref]
|
|
251
|
+
: {};
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
defaults.models = nextAllowed;
|
|
255
|
+
|
|
256
|
+
if (!defaults.model || typeof defaults.model !== 'object') defaults.model = {};
|
|
257
|
+
|
|
258
|
+
const allowedRefs = Object.keys(nextAllowed);
|
|
259
|
+
const currentPrimary = String(defaults.model.primary || '').trim();
|
|
260
|
+
if (!currentPrimary || !nextAllowed[currentPrimary]) {
|
|
261
|
+
const mainAgent = agentList.find((item: any) => String(item?.id || '').trim() === 'main');
|
|
262
|
+
const currentMainModel = String(mainAgent?.model || '').trim();
|
|
263
|
+
if (currentMainModel && nextAllowed[currentMainModel]) {
|
|
264
|
+
defaults.model.primary = currentMainModel;
|
|
265
|
+
} else if (allowedRefs.length > 0) {
|
|
266
|
+
defaults.model.primary = allowedRefs[0];
|
|
267
|
+
} else {
|
|
268
|
+
delete defaults.model.primary;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let mainAgent = agentList.find((item: any) => String(item?.id || '').trim() === 'main');
|
|
273
|
+
const primary = String(defaults.model.primary || '').trim();
|
|
274
|
+
if (!mainAgent && primary) {
|
|
275
|
+
mainAgent = { id: 'main', model: primary };
|
|
276
|
+
agentList.unshift(mainAgent);
|
|
277
|
+
} else if (mainAgent) {
|
|
278
|
+
const currentMainModel = String(mainAgent.model || '').trim();
|
|
279
|
+
if ((!currentMainModel || !nextAllowed[currentMainModel]) && primary) {
|
|
280
|
+
mainAgent.model = primary;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
155
283
|
}
|
|
156
284
|
|
|
157
285
|
function applyDefaultModelFallback(config: any, modelRef: string): void {
|
|
158
286
|
const defaults = ensureAgentDefaults(config);
|
|
287
|
+
normalizeAgentList(config);
|
|
159
288
|
if (!defaults.model || typeof defaults.model !== 'object') defaults.model = {};
|
|
160
289
|
defaults.model.primary = modelRef;
|
|
161
290
|
if (!defaults.models || typeof defaults.models !== 'object' || Array.isArray(defaults.models)) {
|
|
@@ -166,7 +295,7 @@ function applyDefaultModelFallback(config: any, modelRef: string): void {
|
|
|
166
295
|
}
|
|
167
296
|
}
|
|
168
297
|
|
|
169
|
-
function setDefaultModelOfficial(config: any, modelRef: string): void {
|
|
298
|
+
export function setDefaultModelOfficial(config: any, modelRef: string): void {
|
|
170
299
|
// 等价于官方 `openclaw models set <provider/model>` 的最小配置结果。
|
|
171
300
|
// 真相源仍然是 OpenClaw 官方字段,不引入私有结构。
|
|
172
301
|
applyDefaultModelFallback(config, modelRef);
|
|
@@ -174,6 +303,58 @@ function setDefaultModelOfficial(config: any, modelRef: string): void {
|
|
|
174
303
|
console.log(`[ConfigService] 已自动设置默认模型: ${modelRef}`);
|
|
175
304
|
}
|
|
176
305
|
|
|
306
|
+
function resolveConfiguredModelRef(config: any, input: string): string {
|
|
307
|
+
const raw = String(input || '').trim();
|
|
308
|
+
if (!raw) throw new Error('请提供模型名称');
|
|
309
|
+
|
|
310
|
+
const providers = ensureProvidersObject(config);
|
|
311
|
+
const loweredInput = raw.toLowerCase();
|
|
312
|
+
const matches: string[] = [];
|
|
313
|
+
|
|
314
|
+
Object.entries(providers).forEach(([providerId, providerEntry]) => {
|
|
315
|
+
const models = Array.isArray((providerEntry as any)?.models) ? (providerEntry as any).models : [];
|
|
316
|
+
models.forEach((model: any) => {
|
|
317
|
+
const modelId = String(model?.id || model?.name || '').trim();
|
|
318
|
+
const modelName = String(model?.name || model?.id || '').trim();
|
|
319
|
+
if (!modelId) return;
|
|
320
|
+
const modelRef = `${providerId}/${modelId}`;
|
|
321
|
+
if (
|
|
322
|
+
modelRef.toLowerCase() === loweredInput
|
|
323
|
+
|| modelId.toLowerCase() === loweredInput
|
|
324
|
+
|| modelName.toLowerCase() === loweredInput
|
|
325
|
+
) {
|
|
326
|
+
matches.push(modelRef);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
if (matches.length === 1) return matches[0];
|
|
332
|
+
if (matches.length > 1) {
|
|
333
|
+
throw new Error(`模型名重复,请使用 provider/model:${matches.join('、')}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const available = Object.entries(providers).flatMap(([providerId, providerEntry]) => {
|
|
337
|
+
const models = Array.isArray((providerEntry as any)?.models) ? (providerEntry as any).models : [];
|
|
338
|
+
return models
|
|
339
|
+
.map((model: any) => String(model?.id || model?.name || '').trim())
|
|
340
|
+
.filter(Boolean)
|
|
341
|
+
.map((modelId: string) => `${providerId}/${modelId}`);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
if (available.length === 0) {
|
|
345
|
+
throw new Error('当前没有可用模型,请先配置模型提供商');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
throw new Error(`模型未配置:${raw}。当前可用:${available.join('、')}`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function setConfiguredModel(input: string): string {
|
|
352
|
+
const config = readConfig();
|
|
353
|
+
const modelRef = resolveConfiguredModelRef(config, input);
|
|
354
|
+
setDefaultModelOfficial(config, modelRef);
|
|
355
|
+
return modelRef;
|
|
356
|
+
}
|
|
357
|
+
|
|
177
358
|
function maybeBootstrapDefaultModel(config: any, providerId: string, providerEntry: any): void {
|
|
178
359
|
const existingPrimary = getConfiguredPrimaryModel(config);
|
|
179
360
|
if (existingPrimary) return;
|
|
@@ -203,6 +384,7 @@ function pickProviderDisplayName(providerId: string, entry: any): string {
|
|
|
203
384
|
*/
|
|
204
385
|
function writeConfig(config: any): void {
|
|
205
386
|
fs.mkdirSync(path.dirname(OPENCLAW_CONFIG_PATH), { recursive: true });
|
|
387
|
+
normalizeAgentList(config);
|
|
206
388
|
fs.writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
|
|
207
389
|
}
|
|
208
390
|
|
|
@@ -216,6 +398,41 @@ export function getProvidersMap(): Record<string, any> {
|
|
|
216
398
|
return JSON.parse(JSON.stringify(providersObj));
|
|
217
399
|
}
|
|
218
400
|
|
|
401
|
+
export function getConfiguredModels(): Array<{
|
|
402
|
+
value: string;
|
|
403
|
+
label: string;
|
|
404
|
+
providerId: string;
|
|
405
|
+
modelId: string;
|
|
406
|
+
}> {
|
|
407
|
+
const config = readConfig();
|
|
408
|
+
const providers = ensureProvidersObject(config);
|
|
409
|
+
const allowedRefs = collectAllowedModelRefs(config);
|
|
410
|
+
|
|
411
|
+
return allowedRefs
|
|
412
|
+
.map((ref) => {
|
|
413
|
+
const rawRef = String(ref || '').trim();
|
|
414
|
+
if (!rawRef) return null;
|
|
415
|
+
|
|
416
|
+
const slashIndex = rawRef.indexOf('/');
|
|
417
|
+
const providerId = slashIndex >= 0 ? rawRef.slice(0, slashIndex) : '';
|
|
418
|
+
const modelId = slashIndex >= 0 ? rawRef.slice(slashIndex + 1) : rawRef;
|
|
419
|
+
const providerModels = Array.isArray(providers?.[providerId]?.models) ? providers[providerId].models : [];
|
|
420
|
+
const hit = providerModels.find((model: any) => {
|
|
421
|
+
const candidate = String(model?.id || model?.name || model || '').trim();
|
|
422
|
+
return candidate === modelId || buildModelRef(providerId, candidate) === rawRef;
|
|
423
|
+
});
|
|
424
|
+
const label = String(hit?.name || hit?.id || modelId || rawRef).trim() || rawRef;
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
value: rawRef,
|
|
428
|
+
label,
|
|
429
|
+
providerId,
|
|
430
|
+
modelId,
|
|
431
|
+
};
|
|
432
|
+
})
|
|
433
|
+
.filter((item): item is { value: string; label: string; providerId: string; modelId: string } => Boolean(item));
|
|
434
|
+
}
|
|
435
|
+
|
|
219
436
|
/**
|
|
220
437
|
* 获取提供商列表(apiKey 脱敏,供 UI 展示)
|
|
221
438
|
*/
|
|
@@ -295,6 +512,7 @@ export function addProvider(options: {
|
|
|
295
512
|
}
|
|
296
513
|
|
|
297
514
|
providers[providerId] = providerEntry;
|
|
515
|
+
reconcileProviderAllowedModels(config, providerId, providerEntry);
|
|
298
516
|
writeConfig(config);
|
|
299
517
|
maybeBootstrapDefaultModel(config, providerId, providerEntry);
|
|
300
518
|
|
|
@@ -324,6 +542,7 @@ export function deleteProvider(providerId: string): void {
|
|
|
324
542
|
}
|
|
325
543
|
|
|
326
544
|
delete providers[providerId];
|
|
545
|
+
reconcileProviderAllowedModels(config, providerId, null);
|
|
327
546
|
writeConfig(config);
|
|
328
547
|
|
|
329
548
|
console.log(`[ConfigService] 删除提供商: ${providerId}`);
|
|
@@ -370,6 +589,7 @@ export function updateProvider(providerId: string, data: Partial<{
|
|
|
370
589
|
if (data.models !== undefined) next.models = formatModels(data.models);
|
|
371
590
|
|
|
372
591
|
providers[providerId] = next;
|
|
592
|
+
reconcileProviderAllowedModels(config, providerId, next);
|
|
373
593
|
writeConfig(config);
|
|
374
594
|
maybeBootstrapDefaultModel(config, providerId, next);
|
|
375
595
|
|