@bolloon/bolloon-agent 0.1.32 → 0.1.33
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/agents/pi-sdk.js +10 -1
- package/dist/llm/audio-config-store.js +199 -0
- package/dist/llm/config-store.js +20 -10
- package/dist/llm/pi-ai.js +2 -2
- package/dist/llm/video-config-store.js +31 -1
- package/dist/pi-ecosystem/index.js +1 -1
- package/dist/web/api-config.html +13 -1
- package/dist/web/client.js +375 -8
- package/dist/web/server.js +269 -5
- package/package.json +1 -1
- package/src/agents/pi-sdk.ts +9 -1
- package/src/llm/audio-config-store.ts +6 -1
- package/src/llm/config-store.ts +21 -11
- package/src/llm/pi-ai.ts +2 -2
- package/src/llm/video-config-store.ts +7 -1
- package/src/web/api-config.html +13 -1
- package/src/web/client.js +375 -8
- package/src/web/server.ts +228 -5
package/dist/web/server.js
CHANGED
|
@@ -11,6 +11,7 @@ import { initMinimax, getMinimax } from '../constraints/index.js';
|
|
|
11
11
|
import { createAgentSession } from '../agents/pi-sdk.js';
|
|
12
12
|
import { llmConfigStore } from '../llm/config-store.js';
|
|
13
13
|
import { videoConfigStore } from '../llm/video-config-store.js';
|
|
14
|
+
import { audioConfigStore } from '../llm/audio-config-store.js';
|
|
14
15
|
import { irohTransport } from '../network/iroh-transport.js';
|
|
15
16
|
import { createAgentDelegateApp } from './agent-delegate-server.js';
|
|
16
17
|
import { createIrohDelegateTransport } from './iroh-delegate-transport.js';
|
|
@@ -327,6 +328,114 @@ function isSharedWith(ch, peerPublicKey) {
|
|
|
327
328
|
const shared = Array.isArray(ch.shared_with_peers) ? ch.shared_with_peers : [];
|
|
328
329
|
return shared.includes(peerPublicKey);
|
|
329
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* v3 新增: 解析 LLM 回复里的 @-mentions, 把消息发到目标 channel.
|
|
333
|
+
*
|
|
334
|
+
* 语法: "@渠道名 消息内容" — 渠道名匹配 local channels by name, 或 remote channels by name.
|
|
335
|
+
* - 本地 channel: 直接 push 到 session
|
|
336
|
+
* - 远端 channel: 通过 P2P RPC 转发到对端
|
|
337
|
+
*
|
|
338
|
+
* 返回: 解析到的 mention 列表, 供 SSE 广播
|
|
339
|
+
*/
|
|
340
|
+
async function routeMentionsInReply(originChannelId, replyText, localChannels, remoteChannels) {
|
|
341
|
+
const results = [];
|
|
342
|
+
// 解析: 匹配 @渠道名 后面跟一段文字 (到下一个 @ 或 行尾)
|
|
343
|
+
// 渠道名: 中文/英文/数字/下划线/连字符, 1-30 字符
|
|
344
|
+
const regex = /@([一-龥A-Za-z0-9_\-]{1,30})\s+([^\n@]+?)(?=(?:\s*@[一-龥A-Za-z0-9_\-]{1,30}\s)|$)/g;
|
|
345
|
+
const matches = [...replyText.matchAll(regex)];
|
|
346
|
+
if (matches.length === 0)
|
|
347
|
+
return results;
|
|
348
|
+
// 找当前 channel 的 name (用于日志)
|
|
349
|
+
let originChannelName = originChannelId;
|
|
350
|
+
try {
|
|
351
|
+
const chs = await loadChannels();
|
|
352
|
+
const oc = chs.find(c => c.id === originChannelId);
|
|
353
|
+
if (oc)
|
|
354
|
+
originChannelName = oc.name;
|
|
355
|
+
}
|
|
356
|
+
catch { }
|
|
357
|
+
console.log(`[v3-cross] (${originChannelName}) 解析到 ${matches.length} 个 @-mention`);
|
|
358
|
+
for (const m of matches) {
|
|
359
|
+
const targetName = m[1].trim();
|
|
360
|
+
const text = m[2].trim();
|
|
361
|
+
if (!text)
|
|
362
|
+
continue;
|
|
363
|
+
// 优先本地 (本地 channel 不能有 ownerPublicKey)
|
|
364
|
+
const localTarget = localChannels.find(c => c.name === targetName);
|
|
365
|
+
const remoteTarget = !localTarget ? remoteChannels.find(c => c.name === targetName) : null;
|
|
366
|
+
if (localTarget) {
|
|
367
|
+
// 本地: 直接 push 到 session
|
|
368
|
+
try {
|
|
369
|
+
const existing = await loadSession(localTarget.id, 'default');
|
|
370
|
+
const session = existing || {
|
|
371
|
+
channelId: localTarget.id, sessionId: 'default', messages: [], lastUpdated: new Date().toISOString()
|
|
372
|
+
};
|
|
373
|
+
session.messages.push({
|
|
374
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
375
|
+
type: 'ai',
|
|
376
|
+
content: text,
|
|
377
|
+
timestamp: new Date().toISOString(),
|
|
378
|
+
source: 'ai-mention', // v3: 标记是其他 channel 的 AI @-mention 进来的
|
|
379
|
+
originChannelId, // 谁 @ 过来的
|
|
380
|
+
originChannelName // 渠道名 (方便显示)
|
|
381
|
+
});
|
|
382
|
+
session.lastUpdated = new Date().toISOString();
|
|
383
|
+
await saveSession(session);
|
|
384
|
+
console.log(`[v3-cross] (${originChannelName}) @${targetName} → 本地 channel ${localTarget.id}, 存了 ${text.length} chars`);
|
|
385
|
+
// 推 SSE 让本地 UI 知道有 AI 跨渠道消息
|
|
386
|
+
broadcast({
|
|
387
|
+
type: 'cross-mention-received',
|
|
388
|
+
originChannelId, originChannelName,
|
|
389
|
+
targetChannelId: localTarget.id, targetChannelName: localTarget.name,
|
|
390
|
+
text, source: 'ai-mention'
|
|
391
|
+
}, 'broadcast');
|
|
392
|
+
results.push({ targetName, targetId: localTarget.id, source: 'local', text, status: 'sent' });
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
console.error(`[v3-cross] @${targetName} 本地存失败:`, err.message);
|
|
396
|
+
results.push({ targetName, targetId: localTarget.id, source: 'local', text, status: 'failed' });
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
else if (remoteTarget) {
|
|
400
|
+
// 远端: 通过 P2P RPC 转发
|
|
401
|
+
const ownerPk = remoteTarget._ownerPublicKey;
|
|
402
|
+
if (!v3P2PRef) {
|
|
403
|
+
console.warn(`[v3-cross] P2PDirect 未启动, 跳过远端 @${targetName}`);
|
|
404
|
+
results.push({ targetName, targetId: remoteTarget.id, source: 'remote', text, status: 'failed' });
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
try {
|
|
408
|
+
const rpc = JSON.stringify({
|
|
409
|
+
v: 3, op: 'agent.cross.post',
|
|
410
|
+
payload: {
|
|
411
|
+
targetChannelId: remoteTarget.id,
|
|
412
|
+
targetChannelName: remoteTarget.name,
|
|
413
|
+
originChannelId,
|
|
414
|
+
originChannelName,
|
|
415
|
+
text,
|
|
416
|
+
fromPublicKey: v3P2PRef.getPublicKey()
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
const ok = v3P2PRef.sendTo(ownerPk, rpc);
|
|
420
|
+
if (ok) {
|
|
421
|
+
console.log(`[v3-cross] (${originChannelName}) @${targetName} → 远端 peer ${ownerPk.substring(0, 12)}... (channelId=${remoteTarget.id})`);
|
|
422
|
+
results.push({ targetName, targetId: remoteTarget.id, source: 'remote', text, status: 'sent' });
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
results.push({ targetName, targetId: remoteTarget.id, source: 'remote', text, status: 'failed' });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
console.error(`[v3-cross] @${targetName} 远端 RPC 失败:`, err.message);
|
|
430
|
+
results.push({ targetName, targetId: remoteTarget.id, source: 'remote', text, status: 'failed' });
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
console.warn(`[v3-cross] @${targetName} 找不到匹配 channel (本地 ${localChannels.length} 个, 远端 ${remoteChannels.length} 个)`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return results;
|
|
438
|
+
}
|
|
330
439
|
/**
|
|
331
440
|
* v3: 处理 Hyperswarm 通道收到的 v3 RPC 消息
|
|
332
441
|
* 设计: 用 HyperswarmCommunicator (DHT topic 自动发现) 取代 iroh 直接 connect
|
|
@@ -436,11 +545,13 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
436
545
|
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
437
546
|
type: 'user',
|
|
438
547
|
content: text,
|
|
439
|
-
timestamp: new Date().toISOString()
|
|
548
|
+
timestamp: new Date().toISOString(),
|
|
549
|
+
source: 'remote', // v3: 标记远端访客
|
|
550
|
+
fromPublicKey: senderKey // v3: 记录对方 publicKey
|
|
440
551
|
});
|
|
441
552
|
session.lastUpdated = new Date().toISOString();
|
|
442
553
|
await saveSession(session);
|
|
443
|
-
console.log(`[v3] (${channelId}) 存 user 消息 (${text.length} chars) 到 A 的 session`);
|
|
554
|
+
console.log(`[v3] (${channelId}) 存 user 消息 (${text.length} chars) 到 A 的 session (来自 ${senderKey.substring(0, 12)}...)`);
|
|
444
555
|
}
|
|
445
556
|
catch (saveErr) {
|
|
446
557
|
console.warn(`[v3] 存 user 消息失败 (不影响 chat):`, saveErr.message);
|
|
@@ -466,7 +577,30 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
466
577
|
// 2. 跑 LLM (复用 Phase 1 的 buildJudgmentHint — 注入 channel 的 judgment)
|
|
467
578
|
const { getMinimax } = await import('../constraints/index.js');
|
|
468
579
|
const llm = getMinimax();
|
|
469
|
-
|
|
580
|
+
// v3 新增: 在 prompt 头部标记"这是远端访客", 让 AI 知道对方不是自己 owner
|
|
581
|
+
const visitorHint = `[系统上下文] 消息来源: 远端访客 (P2P 连接, publicKey=${senderKey.substring(0, 12)}...). 对方不是你 owner, 是通过 P2P 网络访问你这个 channel 的合作者. 称呼对方时可用 "远端访客" / "朋友" / "合作者", 不要叫 "主人".\n\n`;
|
|
582
|
+
// v3 新增: 也注入 channel 目录给 LLM (B 的 channel 也可以 @-mention 其他)
|
|
583
|
+
let dirHint = '';
|
|
584
|
+
const localChannels = (await loadChannels()).filter(c => c.id !== channelId);
|
|
585
|
+
const remoteChannels = [];
|
|
586
|
+
for (const [peerPk, list] of remoteChannelCache.entries()) {
|
|
587
|
+
if (peerPk === senderKey)
|
|
588
|
+
continue; // 跳过发起方
|
|
589
|
+
for (const ch of list) {
|
|
590
|
+
remoteChannels.push({ ...ch, _ownerPublicKey: peerPk });
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (localChannels.length > 0 || remoteChannels.length > 0) {
|
|
594
|
+
dirHint += '[系统上下文] 可用渠道 (你可以用 @渠道名 消息内容 给它们发消息):\n';
|
|
595
|
+
for (const c of localChannels) {
|
|
596
|
+
dirHint += ` - [本地] @${c.name} (id=${c.id})\n`;
|
|
597
|
+
}
|
|
598
|
+
for (const c of remoteChannels) {
|
|
599
|
+
dirHint += ` - [远端, owner=${(c._ownerPublicKey || '').substring(0, 8)}…] @${c.name} (id=${c.id})\n`;
|
|
600
|
+
}
|
|
601
|
+
dirHint += '语法: 在回复中写 "@渠道名 我要说的话" 即可. 消息会持久化到目标 channel 的 session.\n\n';
|
|
602
|
+
}
|
|
603
|
+
const fullPrompt = `${visitorHint}${dirHint}${judgmentHint}${text}`;
|
|
470
604
|
let fullResponse = '';
|
|
471
605
|
// v3 新增: 流式 token 节流推给 B — 让 B 看到过程
|
|
472
606
|
let lastFlushAt = 0;
|
|
@@ -589,6 +723,53 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
589
723
|
}
|
|
590
724
|
return;
|
|
591
725
|
}
|
|
726
|
+
// v3 新增: 收到远端发来的 @-mention 跨渠道消息, 存到本地 target channel
|
|
727
|
+
if (op === 'agent.cross.post') {
|
|
728
|
+
const { targetChannelId, targetChannelName, originChannelId, originChannelName, text, fromPublicKey } = parsed.payload || {};
|
|
729
|
+
if (!targetChannelId || !text) {
|
|
730
|
+
console.warn(`[v3-cross] agent.cross.post 缺少 targetChannelId/text`);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
try {
|
|
734
|
+
// 找 channel — 必须存在于本节点
|
|
735
|
+
const channels = await loadChannels();
|
|
736
|
+
const ch = channels.find(c => c.id === targetChannelId);
|
|
737
|
+
if (!ch) {
|
|
738
|
+
console.warn(`[v3-cross] agent.cross.post: 本节点无 channel ${targetChannelId}, 忽略`);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
// 存到 session — 这是一条来自其他节点的 LLM @-mention
|
|
742
|
+
const existing = await loadSession(targetChannelId, 'default');
|
|
743
|
+
const session = existing || {
|
|
744
|
+
channelId: targetChannelId, sessionId: 'default', messages: [], lastUpdated: new Date().toISOString()
|
|
745
|
+
};
|
|
746
|
+
session.messages.push({
|
|
747
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
748
|
+
type: 'ai',
|
|
749
|
+
content: text,
|
|
750
|
+
timestamp: new Date().toISOString(),
|
|
751
|
+
source: 'ai-mention-remote', // v3: 来自其他节点的 AI @-mention
|
|
752
|
+
originChannelId, // 哪个 channel 触发的
|
|
753
|
+
originChannelName,
|
|
754
|
+
fromPublicKey // 哪个节点来的
|
|
755
|
+
});
|
|
756
|
+
session.lastUpdated = new Date().toISOString();
|
|
757
|
+
await saveSession(session);
|
|
758
|
+
console.log(`[v3-cross] 收到远端 @-mention: ${originChannelName} → 本地 ${targetChannelName} (${text.length} chars)`);
|
|
759
|
+
// 推 SSE 让本地 UI 知道有跨渠道消息到达
|
|
760
|
+
broadcast({
|
|
761
|
+
type: 'cross-mention-received',
|
|
762
|
+
originChannelId, originChannelName,
|
|
763
|
+
targetChannelId, targetChannelName: ch.name,
|
|
764
|
+
text, source: 'ai-mention-remote',
|
|
765
|
+
fromPublicKey
|
|
766
|
+
}, 'broadcast');
|
|
767
|
+
}
|
|
768
|
+
catch (err) {
|
|
769
|
+
console.error(`[v3-cross] 处理 agent.cross.post 失败:`, err.message);
|
|
770
|
+
}
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
592
773
|
console.log(`[v3] 收到未知 op: ${op}`);
|
|
593
774
|
}
|
|
594
775
|
async function buildJudgmentHint(channel, channelIdForLog) {
|
|
@@ -1059,6 +1240,8 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1059
1240
|
let contextHint = '';
|
|
1060
1241
|
if (realChannelDid)
|
|
1061
1242
|
contextHint += `[系统上下文] 当前频道名称: ${realChannelName}, 你的真实 DID: ${realChannelDid}\n`;
|
|
1243
|
+
// v3 新增: 标识发送方 — 让 AI 分清内部 owner vs 远端访客
|
|
1244
|
+
contextHint += `[系统上下文] 消息来源: 本地 (channel 内部 owner / 此机器上的用户). 称呼对方时用 "你" 或 "主人" 即可.\n`;
|
|
1062
1245
|
if (boundWalletAddress) {
|
|
1063
1246
|
contextHint += `[系统上下文] 已绑定的加密钱包地址: ${boundWalletAddress}。当用户授权或启用自动工具调用时, 可使用该地址发起链上操作。\n`;
|
|
1064
1247
|
}
|
|
@@ -1073,15 +1256,38 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1073
1256
|
const judgmentHint = await buildJudgmentHint(channelForJudgment, channelId);
|
|
1074
1257
|
if (judgmentHint)
|
|
1075
1258
|
contextHint += judgmentHint;
|
|
1259
|
+
// v3 新增: 注入"可用渠道"目录, 让 LLM 知道可以 @-mention 哪些 channel
|
|
1260
|
+
// - 本地 channels (除了自己)
|
|
1261
|
+
// - 远端 channels (remoteChannelCache 缓存的)
|
|
1262
|
+
const localChannels = (await loadChannels()).filter(c => c.id !== channelId);
|
|
1263
|
+
const remoteChannels = [];
|
|
1264
|
+
for (const [peerPk, list] of remoteChannelCache.entries()) {
|
|
1265
|
+
for (const ch of list) {
|
|
1266
|
+
remoteChannels.push({ ...ch, _ownerPublicKey: peerPk });
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
if (localChannels.length > 0 || remoteChannels.length > 0) {
|
|
1270
|
+
contextHint += '[系统上下文] 可用渠道 (你可以用 @渠道名 消息内容 给它们发消息):\n';
|
|
1271
|
+
for (const c of localChannels) {
|
|
1272
|
+
contextHint += ` - [本地] @${c.name} (id=${c.id})\n`;
|
|
1273
|
+
}
|
|
1274
|
+
for (const c of remoteChannels) {
|
|
1275
|
+
contextHint += ` - [远端, owner=${(c._ownerPublicKey || '').substring(0, 8)}…] @${c.name} (id=${c.id})\n`;
|
|
1276
|
+
}
|
|
1277
|
+
contextHint += '语法: 当你想给其他渠道发消息, 在回复中写 "@渠道名 我要说的话" 即可. 消息会持久化到目标 channel 的 session, 你之后能看到"自己"在那里说的话.\n\n';
|
|
1278
|
+
}
|
|
1076
1279
|
if (contextHint)
|
|
1077
1280
|
contextHint += '\n';
|
|
1078
1281
|
fullResponse = await agent.promptStream(contextHint + text, streamCallback);
|
|
1282
|
+
// v3 新增: 解析 LLM 回复里的 @-mentions, 转发到目标 channel
|
|
1283
|
+
await routeMentionsInReply(channelId, fullResponse, localChannels, remoteChannels);
|
|
1079
1284
|
broadcast({ type: 'ai', content: fullResponse }, channelId);
|
|
1080
1285
|
const existingSession = await loadSession(channelId, currentSessionId);
|
|
1081
1286
|
const session = existingSession || { channelId, sessionId: currentSessionId, messages: [], lastUpdated: new Date().toISOString() };
|
|
1082
1287
|
session.sessionId = currentSessionId;
|
|
1083
|
-
|
|
1084
|
-
session.messages.push({ id: crypto.randomUUID(), type: '
|
|
1288
|
+
// v3: 加 source 标记 (local = 内部 owner, remote = 远端访客)
|
|
1289
|
+
session.messages.push({ id: crypto.randomUUID(), type: 'user', content: text, timestamp: new Date().toISOString(), source: 'local' });
|
|
1290
|
+
session.messages.push({ id: crypto.randomUUID(), type: 'ai', content: fullResponse, timestamp: new Date().toISOString(), source: 'local' });
|
|
1085
1291
|
session.lastUpdated = new Date().toISOString();
|
|
1086
1292
|
await saveSession(session);
|
|
1087
1293
|
const channels = await loadChannels();
|
|
@@ -1953,6 +2159,11 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1953
2159
|
if (!provider || !config) {
|
|
1954
2160
|
return res.status(400).json({ error: 'provider and config required' });
|
|
1955
2161
|
}
|
|
2162
|
+
// 如果前端发的是掩码(***xxx),从当前配置里取真实 key
|
|
2163
|
+
const currentConfig = await llmConfigStore.getProvider(provider);
|
|
2164
|
+
if (currentConfig && config.apiKey && config.apiKey.startsWith('***')) {
|
|
2165
|
+
config.apiKey = currentConfig.apiKey;
|
|
2166
|
+
}
|
|
1956
2167
|
await llmConfigStore.updateProvider(provider, config);
|
|
1957
2168
|
// 如果是活跃供应商,重新初始化 Pi SDK
|
|
1958
2169
|
const currentActive = await llmConfigStore.getActiveProvider();
|
|
@@ -2065,6 +2276,59 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
2065
2276
|
res.status(500).json({ error: err.message });
|
|
2066
2277
|
}
|
|
2067
2278
|
});
|
|
2279
|
+
// ==================== 音频生成配置 (TTS / Music) ====================
|
|
2280
|
+
// 获取音频配置
|
|
2281
|
+
app.get('/api/audio-config', async (req, res) => {
|
|
2282
|
+
try {
|
|
2283
|
+
const config = await audioConfigStore.getConfig();
|
|
2284
|
+
const providerInfo = audioConfigStore.getAllProviderInfo();
|
|
2285
|
+
const masked = Object.fromEntries(Object.entries(config.providers).map(([key, val]) => [
|
|
2286
|
+
key,
|
|
2287
|
+
{ ...val, apiKey: val.apiKey ? '***' + val.apiKey.slice(-4) : '' }
|
|
2288
|
+
]));
|
|
2289
|
+
res.json({
|
|
2290
|
+
activeProvider: config.activeProvider,
|
|
2291
|
+
providers: masked,
|
|
2292
|
+
providerInfo
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
catch (err) {
|
|
2296
|
+
res.status(500).json({ error: err.message });
|
|
2297
|
+
}
|
|
2298
|
+
});
|
|
2299
|
+
// 更新音频供应商配置
|
|
2300
|
+
app.post('/api/audio-config', async (req, res) => {
|
|
2301
|
+
try {
|
|
2302
|
+
const { provider, config } = req.body;
|
|
2303
|
+
if (!provider || !config) {
|
|
2304
|
+
return res.status(400).json({ error: 'provider and config required' });
|
|
2305
|
+
}
|
|
2306
|
+
// 掩码回写真实 key
|
|
2307
|
+
const currentConfig = await audioConfigStore.getProvider(provider);
|
|
2308
|
+
if (currentConfig && config.apiKey && config.apiKey.startsWith('***')) {
|
|
2309
|
+
config.apiKey = currentConfig.apiKey;
|
|
2310
|
+
}
|
|
2311
|
+
await audioConfigStore.updateProvider(provider, config);
|
|
2312
|
+
res.json({ ok: true });
|
|
2313
|
+
}
|
|
2314
|
+
catch (err) {
|
|
2315
|
+
res.status(500).json({ error: err.message });
|
|
2316
|
+
}
|
|
2317
|
+
});
|
|
2318
|
+
// 测试音频供应商连接
|
|
2319
|
+
app.post('/api/audio-test', async (req, res) => {
|
|
2320
|
+
try {
|
|
2321
|
+
const { provider } = req.body;
|
|
2322
|
+
if (!provider) {
|
|
2323
|
+
return res.status(400).json({ error: 'provider required' });
|
|
2324
|
+
}
|
|
2325
|
+
const result = await audioConfigStore.testProvider(provider);
|
|
2326
|
+
res.json(result);
|
|
2327
|
+
}
|
|
2328
|
+
catch (err) {
|
|
2329
|
+
res.status(500).json({ error: err.message });
|
|
2330
|
+
}
|
|
2331
|
+
});
|
|
2068
2332
|
// 统一 AI 解析入口:CLI / 接收方节点 调这里完成 LLM + judgment + harness
|
|
2069
2333
|
// 入参: { text, mimeType, fileName, fromNodeId, source }
|
|
2070
2334
|
// 出参: { summary, qualityScore, judgmentId?, gateArtifact? }
|
package/package.json
CHANGED
package/src/agents/pi-sdk.ts
CHANGED
|
@@ -1178,6 +1178,7 @@ ${toolDefs}
|
|
|
1178
1178
|
const reply = response.reply.trim();
|
|
1179
1179
|
|
|
1180
1180
|
console.log(`[PiAgent] LLM 回复长度: ${reply.length}, 内容预览: "${reply.substring(0, 80)}..."`);
|
|
1181
|
+
console.log(`[PiAgent] LLM 完整回复:\n${reply}`);
|
|
1181
1182
|
|
|
1182
1183
|
// 通知前端:收到 LLM 回复
|
|
1183
1184
|
if (onStream) {
|
|
@@ -1450,7 +1451,14 @@ Workspace root folder: ${this.cwd}
|
|
|
1450
1451
|
const marker = '<final gen>';
|
|
1451
1452
|
const markerIndex = content.indexOf(marker);
|
|
1452
1453
|
if (markerIndex !== -1) {
|
|
1453
|
-
|
|
1454
|
+
const after = content.substring(markerIndex + marker.length).trim();
|
|
1455
|
+
// v3 修复: 如果 <final gen> 之后是空, fallback 用 marker 之前的内容 (去掉 marker)
|
|
1456
|
+
// 否则 LLM 写了 <final gen> 在末尾时, 用户看到空回复 + error
|
|
1457
|
+
if (after) {
|
|
1458
|
+
content = after;
|
|
1459
|
+
} else {
|
|
1460
|
+
content = content.substring(0, markerIndex).trim();
|
|
1461
|
+
}
|
|
1454
1462
|
}
|
|
1455
1463
|
// 移除任何 tool call 标记
|
|
1456
1464
|
let cleaned = content
|
|
@@ -222,9 +222,14 @@ class AudioConfigStore {
|
|
|
222
222
|
return { success: true, latency };
|
|
223
223
|
} else {
|
|
224
224
|
const errorText = await response.text().catch(() => 'Unknown error');
|
|
225
|
+
const hint = response.status === 401
|
|
226
|
+
? '(请确认是 MiniMax 的 API Key)'
|
|
227
|
+
: response.status === 404
|
|
228
|
+
? '(端点不存在 — 请检查 baseUrl)'
|
|
229
|
+
: '';
|
|
225
230
|
return {
|
|
226
231
|
success: false,
|
|
227
|
-
error: `HTTP ${response.status}: ${errorText.substring(0,
|
|
232
|
+
error: `HTTP ${response.status}: ${errorText.substring(0, 500)}${hint ? ' ' + hint : ''}`,
|
|
228
233
|
latency
|
|
229
234
|
};
|
|
230
235
|
}
|
package/src/llm/config-store.ts
CHANGED
|
@@ -77,7 +77,7 @@ export const DEFAULT_PROVIDER_CONFIGS: Record<ModelProvider, ProviderConfig> = {
|
|
|
77
77
|
enabled: false,
|
|
78
78
|
apiKey: '',
|
|
79
79
|
baseUrl: 'https://api.minimaxi.com/v1',
|
|
80
|
-
model: 'MiniMax-
|
|
80
|
+
model: 'MiniMax-M3',
|
|
81
81
|
temperature: 0.7,
|
|
82
82
|
maxTokens: 4096,
|
|
83
83
|
requiresApiKey: true
|
|
@@ -129,17 +129,22 @@ export const DEFAULT_PROVIDER_CONFIGS: Record<ModelProvider, ProviderConfig> = {
|
|
|
129
129
|
}
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
-
export const PROVIDER_INFO: Record<ModelProvider, { name: string; description: string; requiresApiKey: boolean }> = {
|
|
133
|
-
openai: { name: 'OpenAI', description: 'GPT-4, GPT-3.5 等模型', requiresApiKey: true },
|
|
134
|
-
anthropic: { name: 'Anthropic', description: 'Claude 3.5 系列模型', requiresApiKey: true },
|
|
132
|
+
export const PROVIDER_INFO: Record<ModelProvider, { name: string; description: string; requiresApiKey: boolean; models?: string[] }> = {
|
|
133
|
+
openai: { name: 'OpenAI', description: 'GPT-4, GPT-3.5 等模型', requiresApiKey: true, models: ['gpt-4', 'gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'] },
|
|
134
|
+
anthropic: { name: 'Anthropic', description: 'Claude 3.5 系列模型', requiresApiKey: true, models: ['claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229'] },
|
|
135
135
|
openrouter: { name: 'OpenRouter', description: '聚合多个 AI 供应商', requiresApiKey: true },
|
|
136
|
-
gemini: { name: 'Google Gemini', description: 'Gemini 系列模型', requiresApiKey: true },
|
|
136
|
+
gemini: { name: 'Google Gemini', description: 'Gemini 系列模型', requiresApiKey: true, models: ['gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-1.5-flash'] },
|
|
137
137
|
ollama: { name: 'Ollama', description: '本地 LLM 运行框架', requiresApiKey: false },
|
|
138
|
-
minimax: {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
138
|
+
minimax: {
|
|
139
|
+
name: 'MiniMax',
|
|
140
|
+
description: '国产大模型服务',
|
|
141
|
+
requiresApiKey: true,
|
|
142
|
+
models: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2', 'MiniMax-M2.1-highspeed', 'MiniMax-M2.7-highspeed']
|
|
143
|
+
},
|
|
144
|
+
deepseek: { name: 'DeepSeek', description: '深度求索大模型', requiresApiKey: true, models: ['deepseek-chat', 'deepseek-reasoner'] },
|
|
145
|
+
kimi: { name: 'Kimi (月之暗面)', description: 'Moonshot 长上下文模型', requiresApiKey: true, models: ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k'] },
|
|
146
|
+
glm: { name: 'GLM (智谱)', description: '智谱 ChatGLM 系列模型', requiresApiKey: true, models: ['glm-4-flash', 'glm-4', 'glm-4-plus', 'glm-4-air', 'glm-4-airx'] },
|
|
147
|
+
qwen: { name: 'Qwen (通义千问)', description: '阿里云通义千问系列', requiresApiKey: true, models: ['qwen-plus', 'qwen-max', 'qwen-turbo', 'qwen-long'] },
|
|
143
148
|
local: { name: '本地模型', description: '本地部署的模型服务', requiresApiKey: false }
|
|
144
149
|
};
|
|
145
150
|
|
|
@@ -323,7 +328,12 @@ class LLMConfigStore {
|
|
|
323
328
|
return { success: true, latency };
|
|
324
329
|
} else {
|
|
325
330
|
const errorText = await response.text().catch(() => 'Unknown error');
|
|
326
|
-
|
|
331
|
+
const hint = response.status === 401
|
|
332
|
+
? '(API Key 无效或不匹配该供应商 — 请检查是否复制完整、有无多余空格)'
|
|
333
|
+
: response.status === 404
|
|
334
|
+
? '(端点不存在 — 请检查 baseUrl)'
|
|
335
|
+
: '';
|
|
336
|
+
return { success: false, error: `HTTP ${response.status}: ${errorText.substring(0, 500)}${hint ? ' ' + hint : ''}`, latency };
|
|
327
337
|
}
|
|
328
338
|
} catch (error: any) {
|
|
329
339
|
return { success: false, error: error.message || 'Connection failed', latency: Date.now() - startTime };
|
package/src/llm/pi-ai.ts
CHANGED
|
@@ -176,7 +176,7 @@ export class PiAIModel {
|
|
|
176
176
|
ollama: this.config.model || 'llama3.2',
|
|
177
177
|
openrouter: this.config.model || 'anthropic/claude-3.5-sonnet',
|
|
178
178
|
gemini: this.config.model || 'gemini-2.0-flash',
|
|
179
|
-
minimax: this.config.model || process.env.MINIMAX_MODEL || 'MiniMax-
|
|
179
|
+
minimax: this.config.model || process.env.MINIMAX_MODEL || 'MiniMax-M3',
|
|
180
180
|
deepseek: this.config.model || process.env.DEEPSEEK_MODEL || 'deepseek-chat',
|
|
181
181
|
kimi: this.config.model || process.env.KIMI_MODEL || process.env.MOONSHOT_MODEL || 'moonshot-v1-8k',
|
|
182
182
|
glm: this.config.model || process.env.GLM_MODEL || process.env.ZHIPU_MODEL || 'glm-4-flash',
|
|
@@ -482,7 +482,7 @@ function detectModel(provider: ModelProvider): string {
|
|
|
482
482
|
ollama: 'llama3.2',
|
|
483
483
|
openrouter: 'anthropic/claude-3.5-sonnet',
|
|
484
484
|
gemini: 'gemini-2.0-flash',
|
|
485
|
-
minimax: 'MiniMax-
|
|
485
|
+
minimax: 'MiniMax-M3',
|
|
486
486
|
deepseek: 'deepseek-chat',
|
|
487
487
|
kimi: 'moonshot-v1-8k',
|
|
488
488
|
glm: 'glm-4-flash',
|
|
@@ -232,9 +232,15 @@ class VideoConfigStore {
|
|
|
232
232
|
return { success: true, latency };
|
|
233
233
|
} else {
|
|
234
234
|
const errorText = await response.text().catch(() => 'Unknown error');
|
|
235
|
+
// 401 通常是 key 错误,给出针对性提示
|
|
236
|
+
const hint = response.status === 401
|
|
237
|
+
? '(请确认是火山方舟 ARK 的 API Key,不是 MiniMax / 其他平台)'
|
|
238
|
+
: response.status === 404
|
|
239
|
+
? '(端点不存在 — 火山方舟可能没有 /models,请检查 baseUrl)'
|
|
240
|
+
: '';
|
|
235
241
|
return {
|
|
236
242
|
success: false,
|
|
237
|
-
error: `HTTP ${response.status}: ${errorText.substring(0,
|
|
243
|
+
error: `HTTP ${response.status}: ${errorText.substring(0, 500)}${hint ? ' ' + hint : ''}`,
|
|
238
244
|
latency
|
|
239
245
|
};
|
|
240
246
|
}
|
package/src/web/api-config.html
CHANGED
|
@@ -92,7 +92,8 @@
|
|
|
92
92
|
|
|
93
93
|
<div class="form-group">
|
|
94
94
|
<label>模型</label>
|
|
95
|
-
<input type="text" id="modelInput" placeholder="如 gpt-4">
|
|
95
|
+
<input type="text" id="modelInput" placeholder="如 gpt-4" list="modelSuggestList">
|
|
96
|
+
<datalist id="modelSuggestList"></datalist>
|
|
96
97
|
<p class="form-hint" id="modelHint"></p>
|
|
97
98
|
</div>
|
|
98
99
|
|
|
@@ -358,6 +359,17 @@
|
|
|
358
359
|
document.getElementById('baseUrlInput').value = p.baseUrl || '';
|
|
359
360
|
document.getElementById('modelInput').value = p.model || '';
|
|
360
361
|
|
|
362
|
+
// 填充模型下拉建议(仅 LLM 类型有 providerInfo.models)
|
|
363
|
+
const datalist = document.getElementById('modelSuggestList');
|
|
364
|
+
datalist.innerHTML = '';
|
|
365
|
+
if (type === 'llm' && i.models && Array.isArray(i.models)) {
|
|
366
|
+
for (const m of i.models) {
|
|
367
|
+
const opt = document.createElement('option');
|
|
368
|
+
opt.value = m;
|
|
369
|
+
datalist.appendChild(opt);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
361
373
|
// 视频/音频/LLM 专用字段显隐
|
|
362
374
|
const isVideo = type === 'video';
|
|
363
375
|
const isAudio = type === 'audio';
|