@bolloon/bolloon-agent 0.1.28 → 0.1.30
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/web/client.js +191 -22
- package/dist/web/index.html +1 -0
- package/dist/web/server.js +277 -8
- package/package.json +1 -1
- package/src/llm/config-store.ts +61 -1
- package/src/llm/pi-ai.ts +5 -1
- package/src/web/client.js +191 -22
- package/src/web/index.html +1 -0
- package/src/web/server.ts +288 -8
package/dist/web/server.js
CHANGED
|
@@ -14,10 +14,32 @@ import { irohTransport } from '../network/iroh-transport.js';
|
|
|
14
14
|
import { createAgentDelegateApp } from './agent-delegate-server.js';
|
|
15
15
|
import { createIrohDelegateTransport } from './iroh-delegate-transport.js';
|
|
16
16
|
import { verifyMessage, isAddress, getAddress } from 'viem';
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
// 前端资源路径: 兼容 src 运行 + dist 运行 + npm 全局安装
|
|
18
|
+
// - src 跑 (tsx): __dirname = .../src/web → .../dist/web
|
|
19
|
+
// - dist 跑 (npm): __dirname = .../dist/web → 自身就是 web 根
|
|
20
|
+
// - 环境变量覆盖: BOLLOON_WEB_ROOT=xxx
|
|
21
|
+
// ESM scope 没有 __dirname, 这里自己声明
|
|
22
|
+
const __filename_local = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname_local = dirname(__filename_local);
|
|
24
|
+
let _baseDirname = __dirname_local;
|
|
25
|
+
function resolveWebRoot() {
|
|
26
|
+
if (process.env.BOLLOON_WEB_ROOT && fsSync.existsSync(process.env.BOLLOON_WEB_ROOT)) {
|
|
27
|
+
return process.env.BOLLOON_WEB_ROOT;
|
|
28
|
+
}
|
|
29
|
+
const d = _baseDirname;
|
|
30
|
+
const candidates = [
|
|
31
|
+
path.join(d), // dist/web
|
|
32
|
+
path.join(d, '..', '..', 'dist', 'web'), // src/web → dist/web
|
|
33
|
+
path.join(d, '..', 'web'), // dist/ → web/ 兄弟
|
|
34
|
+
];
|
|
35
|
+
for (const c of candidates) {
|
|
36
|
+
if (fsSync.existsSync(path.join(c, 'index.html')))
|
|
37
|
+
return c;
|
|
38
|
+
}
|
|
39
|
+
return candidates[1];
|
|
40
|
+
}
|
|
41
|
+
const webRoot = resolveWebRoot();
|
|
42
|
+
console.log(`[web] webRoot = ${webRoot}`);
|
|
21
43
|
const SHARED_SESSION_PATH = path.join(process.env.HOME || '/tmp', '.bolloon', 'sessions');
|
|
22
44
|
const SESSION_CACHE_PATH = path.join(SHARED_SESSION_PATH, 'cache');
|
|
23
45
|
const CHANNELS_PATH = path.join(SHARED_SESSION_PATH, 'channels.json');
|
|
@@ -259,6 +281,8 @@ let sseClients = new Set();
|
|
|
259
281
|
let remoteChannelCache = new Map();
|
|
260
282
|
// v3: P2PDirect 引用 (Hyperswarm 薄包装) - 模块级, 因为 web server 闭包里不可用
|
|
261
283
|
let v3P2PRef = null;
|
|
284
|
+
// v3: 等待中的 history RPC (B 端 chat-history endpoint 用) — rpcId → { resolve, reject }
|
|
285
|
+
const v3PendingHistoryGets = new Map();
|
|
262
286
|
let channelSessions = new Map(); // key: channelId
|
|
263
287
|
let sessionMessages = new Map(); // key: channelId + sessionId
|
|
264
288
|
/**
|
|
@@ -297,6 +321,11 @@ function sanitizeChannelForPeer(ch, peerPublicKey) {
|
|
|
297
321
|
// 🔒 不返回: bound_judgment_ids, walletAddress, walletBinding, autoInvokeTools, sessions, shared_with_peers
|
|
298
322
|
};
|
|
299
323
|
}
|
|
324
|
+
/** v3 新增: 判断 channel 是否分享给 peerPublicKey */
|
|
325
|
+
function isSharedWith(ch, peerPublicKey) {
|
|
326
|
+
const shared = Array.isArray(ch.shared_with_peers) ? ch.shared_with_peers : [];
|
|
327
|
+
return shared.includes(peerPublicKey);
|
|
328
|
+
}
|
|
300
329
|
/**
|
|
301
330
|
* v3: 处理 Hyperswarm 通道收到的 v3 RPC 消息
|
|
302
331
|
* 设计: 用 HyperswarmCommunicator (DHT topic 自动发现) 取代 iroh 直接 connect
|
|
@@ -382,7 +411,8 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
382
411
|
console.warn(`[v3] agent.chat.send 缺少 channelId/text`);
|
|
383
412
|
return;
|
|
384
413
|
}
|
|
385
|
-
|
|
414
|
+
const senderKey = fromPublicKey || peerKey;
|
|
415
|
+
console.log(`[v3] 收到 ${senderKey.substring(0, 12)}... 对 channel ${channelId} 的 chat: "${text.substring(0, 40)}..."`);
|
|
386
416
|
try {
|
|
387
417
|
// 1. 找到 channel
|
|
388
418
|
const channels = await loadChannels();
|
|
@@ -395,20 +425,84 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
395
425
|
await comm.sendToConnection(conn.id, reply);
|
|
396
426
|
return;
|
|
397
427
|
}
|
|
398
|
-
//
|
|
428
|
+
// v3 新增: 持久化 B 的 user 消息到 A 的 session — 让历史可拉
|
|
429
|
+
try {
|
|
430
|
+
const existing = await loadSession(channelId, 'default');
|
|
431
|
+
const session = existing || {
|
|
432
|
+
channelId, sessionId: 'default', messages: [], lastUpdated: new Date().toISOString()
|
|
433
|
+
};
|
|
434
|
+
session.messages.push({
|
|
435
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
436
|
+
type: 'user',
|
|
437
|
+
content: text,
|
|
438
|
+
timestamp: new Date().toISOString()
|
|
439
|
+
});
|
|
440
|
+
session.lastUpdated = new Date().toISOString();
|
|
441
|
+
await saveSession(session);
|
|
442
|
+
console.log(`[v3] (${channelId}) 存 user 消息 (${text.length} chars) 到 A 的 session`);
|
|
443
|
+
}
|
|
444
|
+
catch (saveErr) {
|
|
445
|
+
console.warn(`[v3] 存 user 消息失败 (不影响 chat):`, saveErr.message);
|
|
446
|
+
}
|
|
447
|
+
// v3 新增: 告诉 B "我开始想了, 用了哪些 judgment" — 让 B 看到决策依据
|
|
399
448
|
const judgmentHint = await buildJudgmentHint(ch, channelId);
|
|
449
|
+
const usedJudgments = await extractJudgmentsFromHint(ch);
|
|
450
|
+
try {
|
|
451
|
+
const thinkingStart = JSON.stringify({
|
|
452
|
+
v: 3, op: 'agent.chat.thinking',
|
|
453
|
+
payload: {
|
|
454
|
+
channelId,
|
|
455
|
+
phase: 'start',
|
|
456
|
+
fromPublicKey: v3P2PRef?.getPublicKey() || '',
|
|
457
|
+
hint: judgmentHint,
|
|
458
|
+
usedJudgments,
|
|
459
|
+
userText: text
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
await comm.sendToConnection(conn.id, thinkingStart);
|
|
463
|
+
}
|
|
464
|
+
catch { }
|
|
465
|
+
// 2. 跑 LLM (复用 Phase 1 的 buildJudgmentHint — 注入 channel 的 judgment)
|
|
400
466
|
const { getMinimax } = await import('../constraints/index.js');
|
|
401
467
|
const llm = getMinimax();
|
|
402
468
|
const fullPrompt = `${judgmentHint}${text}`;
|
|
403
469
|
let fullResponse = '';
|
|
470
|
+
// v3 新增: 流式 token 节流推给 B — 让 B 看到过程
|
|
471
|
+
let lastFlushAt = 0;
|
|
404
472
|
const streamCallback = (event) => {
|
|
405
|
-
// 流式 token, 不广播给 B (避免半成品噪音), 只记 A 自己的日志
|
|
406
473
|
if (event.type === 'token') {
|
|
407
474
|
fullResponse += event.content;
|
|
475
|
+
if (fullResponse.length - lastFlushAt >= 20) {
|
|
476
|
+
lastFlushAt = fullResponse.length;
|
|
477
|
+
const msg = JSON.stringify({
|
|
478
|
+
v: 3, op: 'agent.chat.thinking',
|
|
479
|
+
payload: { channelId, phase: 'token', partial: fullResponse, fromPublicKey: v3P2PRef?.getPublicKey() || '' }
|
|
480
|
+
});
|
|
481
|
+
comm.sendToConnection(conn.id, msg).catch(() => { });
|
|
482
|
+
}
|
|
408
483
|
}
|
|
409
484
|
};
|
|
410
485
|
const agent = await getAgentForChannel(channelId, ch.did || '', ch.name, ch.didDocRef);
|
|
411
486
|
fullResponse = await agent.promptStream(fullPrompt, streamCallback);
|
|
487
|
+
// v3 新增: 存 A 的 assistant 消息到 session — B 拉历史时能看到完整对话
|
|
488
|
+
try {
|
|
489
|
+
const existing = await loadSession(channelId, 'default');
|
|
490
|
+
const session = existing || {
|
|
491
|
+
channelId, sessionId: 'default', messages: [], lastUpdated: new Date().toISOString()
|
|
492
|
+
};
|
|
493
|
+
session.messages.push({
|
|
494
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
495
|
+
type: 'ai',
|
|
496
|
+
content: fullResponse,
|
|
497
|
+
timestamp: new Date().toISOString()
|
|
498
|
+
});
|
|
499
|
+
session.lastUpdated = new Date().toISOString();
|
|
500
|
+
await saveSession(session);
|
|
501
|
+
console.log(`[v3] (${channelId}) 存 assistant 回复 (${fullResponse.length} chars) 到 A 的 session`);
|
|
502
|
+
}
|
|
503
|
+
catch (saveErr) {
|
|
504
|
+
console.warn(`[v3] 存 assistant 消息失败 (不影响):`, saveErr.message);
|
|
505
|
+
}
|
|
412
506
|
// 3. 把完整回复发给 B
|
|
413
507
|
const reply = JSON.stringify({
|
|
414
508
|
v: 3, op: 'agent.chat.reply',
|
|
@@ -419,7 +513,7 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
419
513
|
}
|
|
420
514
|
});
|
|
421
515
|
await comm.sendToConnection(conn.id, reply);
|
|
422
|
-
console.log(`[v3] 回 chat.reply 给 ${
|
|
516
|
+
console.log(`[v3] 回 chat.reply 给 ${senderKey.substring(0, 12)}... (${fullResponse.length} chars)`);
|
|
423
517
|
}
|
|
424
518
|
catch (err) {
|
|
425
519
|
console.error(`[v3] agent.chat.send 处理失败:`, err.message);
|
|
@@ -434,6 +528,66 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
434
528
|
}
|
|
435
529
|
return;
|
|
436
530
|
}
|
|
531
|
+
if (op === 'agent.history.get') {
|
|
532
|
+
// v3 新增: B 拉 A 的 channel 历史 (含所有 message + judgment hint)
|
|
533
|
+
// 共享过滤: 只返回 B 可见的 channel + 包含的 judgment
|
|
534
|
+
const { channelId, rpcId, fromPublicKey } = parsed.payload || {};
|
|
535
|
+
if (!channelId || !rpcId) {
|
|
536
|
+
console.warn(`[v3] agent.history.get 缺少 channelId/rpcId`);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
const channels = await loadChannels();
|
|
541
|
+
const ch = channels.find(c => c.id === channelId);
|
|
542
|
+
if (!ch) {
|
|
543
|
+
const err = JSON.stringify({
|
|
544
|
+
v: 3, op: 'agent.history.get.reply',
|
|
545
|
+
payload: { rpcId, error: 'channel not found', messages: [], judgments: { bound: [], candidates: [] } }
|
|
546
|
+
});
|
|
547
|
+
await comm.sendToConnection(conn.id, err);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
// 共享过滤: 必须 peerKey 在 shared_with_peers 里 (避免泄露未分享的 channel)
|
|
551
|
+
const peerKey = fromPublicKey;
|
|
552
|
+
if (!peerKey || !isSharedWith(ch, peerKey)) {
|
|
553
|
+
const err = JSON.stringify({
|
|
554
|
+
v: 3, op: 'agent.history.get.reply',
|
|
555
|
+
payload: { rpcId, error: 'channel not shared with you', messages: [], judgments: { bound: [], candidates: [] } }
|
|
556
|
+
});
|
|
557
|
+
await comm.sendToConnection(conn.id, err);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
// 加载 A 端 session
|
|
561
|
+
const session = await loadSession(channelId, 'default');
|
|
562
|
+
// 加载 channel 用到的 judgment
|
|
563
|
+
const judgments = await extractJudgmentsFromHint(ch);
|
|
564
|
+
const reply = JSON.stringify({
|
|
565
|
+
v: 3, op: 'agent.history.get.reply',
|
|
566
|
+
payload: {
|
|
567
|
+
rpcId,
|
|
568
|
+
channelId,
|
|
569
|
+
messages: session?.messages || [],
|
|
570
|
+
lastUpdated: session?.lastUpdated,
|
|
571
|
+
judgments,
|
|
572
|
+
channelName: ch.name
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
await comm.sendToConnection(conn.id, reply);
|
|
576
|
+
console.log(`[v3] 回 history.reply 给 ${peerKey.substring(0, 12)}... (channelId=${channelId}, ${session?.messages?.length || 0} messages)`);
|
|
577
|
+
}
|
|
578
|
+
catch (err) {
|
|
579
|
+
console.error(`[v3] agent.history.get 处理失败:`, err.message);
|
|
580
|
+
try {
|
|
581
|
+
const errMsg = JSON.stringify({
|
|
582
|
+
v: 3, op: 'agent.history.get.reply',
|
|
583
|
+
payload: { rpcId, error: err.message, messages: [], judgments: { bound: [], candidates: [] } }
|
|
584
|
+
});
|
|
585
|
+
await comm.sendToConnection(conn.id, errMsg);
|
|
586
|
+
}
|
|
587
|
+
catch { }
|
|
588
|
+
}
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
437
591
|
console.log(`[v3] 收到未知 op: ${op}`);
|
|
438
592
|
}
|
|
439
593
|
async function buildJudgmentHint(channel, channelIdForLog) {
|
|
@@ -477,6 +631,38 @@ async function buildJudgmentHint(channel, channelIdForLog) {
|
|
|
477
631
|
return '';
|
|
478
632
|
}
|
|
479
633
|
}
|
|
634
|
+
/**
|
|
635
|
+
* v3 新增: 把 channel 当前用到的 judgment 提取成结构化数据, 给 B 端 UI 显示.
|
|
636
|
+
* 返回 { bound: [...], candidates: [...] } — bound 是硬绑定, candidates 是参考池.
|
|
637
|
+
*/
|
|
638
|
+
async function extractJudgmentsFromHint(channel) {
|
|
639
|
+
try {
|
|
640
|
+
const { loadAllJudgments, initializeValueStore } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
641
|
+
await initializeValueStore();
|
|
642
|
+
const allJudgments = await loadAllJudgments();
|
|
643
|
+
if (allJudgments.length === 0)
|
|
644
|
+
return { bound: [], candidates: [] };
|
|
645
|
+
const boundIds = new Set(channel && Array.isArray(channel.bound_judgment_ids) ? channel.bound_judgment_ids : []);
|
|
646
|
+
const summarize = (j) => ({
|
|
647
|
+
id: j.id,
|
|
648
|
+
decision: (j.decision || '').toString().slice(0, 200),
|
|
649
|
+
reasons: Array.isArray(j.reasons) ? j.reasons : [],
|
|
650
|
+
domain: j.domain,
|
|
651
|
+
stakes: j.stakes
|
|
652
|
+
});
|
|
653
|
+
const bound = allJudgments
|
|
654
|
+
.filter((j) => j.id !== undefined && boundIds.has(j.id))
|
|
655
|
+
.map(summarize);
|
|
656
|
+
const candidates = allJudgments
|
|
657
|
+
.filter((j) => j.id !== undefined && !boundIds.has(j.id))
|
|
658
|
+
.map(summarize);
|
|
659
|
+
return { bound, candidates };
|
|
660
|
+
}
|
|
661
|
+
catch (err) {
|
|
662
|
+
console.warn(`[v3] extractJudgmentsFromHint 失败:`, err.message);
|
|
663
|
+
return { bound: [], candidates: [] };
|
|
664
|
+
}
|
|
665
|
+
}
|
|
480
666
|
async function getAgentForChannel(channelId, channelDid, channelName, channelDidDoc) {
|
|
481
667
|
// 获取当前 channel 的 currentSessionId
|
|
482
668
|
const channels = await loadChannels();
|
|
@@ -604,6 +790,45 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
604
790
|
}, 'p2p-global');
|
|
605
791
|
return;
|
|
606
792
|
}
|
|
793
|
+
// v3 新增: B 端收到 A 的 thinking (开始 + 流式 token)
|
|
794
|
+
if (parsed.op === 'agent.chat.thinking') {
|
|
795
|
+
const phase = parsed.payload?.phase;
|
|
796
|
+
if (phase === 'start') {
|
|
797
|
+
console.log(`[v3] 收到来自 ${evt.fromPublicKey.substring(0, 12)}... 的 thinking start (judgments: bound=${(parsed.payload?.usedJudgments?.bound || []).length}, candidates=${(parsed.payload?.usedJudgments?.candidates || []).length})`);
|
|
798
|
+
}
|
|
799
|
+
broadcast({
|
|
800
|
+
type: 'remote-chat-thinking',
|
|
801
|
+
fromPublicKey: evt.fromPublicKey,
|
|
802
|
+
channelId: parsed.payload?.channelId,
|
|
803
|
+
phase: parsed.payload?.phase,
|
|
804
|
+
partial: parsed.payload?.partial,
|
|
805
|
+
hint: parsed.payload?.hint,
|
|
806
|
+
usedJudgments: parsed.payload?.usedJudgments,
|
|
807
|
+
userText: parsed.payload?.userText
|
|
808
|
+
}, 'p2p-global');
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
// v3 新增: B 端收到 A 的 history reply → resolve pending promise
|
|
812
|
+
if (parsed.op === 'agent.history.get.reply') {
|
|
813
|
+
const rpcId = parsed.payload?.rpcId;
|
|
814
|
+
if (rpcId && v3PendingHistoryGets.has(rpcId)) {
|
|
815
|
+
const pending = v3PendingHistoryGets.get(rpcId);
|
|
816
|
+
v3PendingHistoryGets.delete(rpcId);
|
|
817
|
+
if (parsed.payload?.error) {
|
|
818
|
+
pending.reject(new Error(parsed.payload.error));
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
pending.resolve({
|
|
822
|
+
channelId: parsed.payload.channelId,
|
|
823
|
+
messages: parsed.payload.messages || [],
|
|
824
|
+
lastUpdated: parsed.payload.lastUpdated,
|
|
825
|
+
judgments: parsed.payload.judgments || { bound: [], candidates: [] },
|
|
826
|
+
channelName: parsed.payload.channelName
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
607
832
|
const commShim = {
|
|
608
833
|
sendToConnection: (_id, data) => {
|
|
609
834
|
v3P2PRef.sendTo(evt.fromPublicKey, data);
|
|
@@ -2004,6 +2229,50 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
2004
2229
|
res.status(500).json({ error: err.message });
|
|
2005
2230
|
}
|
|
2006
2231
|
});
|
|
2232
|
+
// v3 新增: B 拉 A 的 channel 历史 + 用了哪些 judgment
|
|
2233
|
+
// GET /api/remote-channels/chat-history?targetPublicKey=...&channelId=...
|
|
2234
|
+
// 实现: B → POST 给 A 一个 agent.history.get RPC → A 把 session 返回 → B 渲染
|
|
2235
|
+
app.get('/api/remote-channels/chat-history', async (req, res) => {
|
|
2236
|
+
try {
|
|
2237
|
+
if (!v3P2PRef) {
|
|
2238
|
+
return res.status(503).json({ error: 'P2PDirect not started' });
|
|
2239
|
+
}
|
|
2240
|
+
const targetPublicKey = String(req.query.targetPublicKey || '');
|
|
2241
|
+
const channelId = String(req.query.channelId || '');
|
|
2242
|
+
if (!targetPublicKey || !channelId) {
|
|
2243
|
+
return res.status(400).json({ error: 'targetPublicKey, channelId required' });
|
|
2244
|
+
}
|
|
2245
|
+
// 通过 RPC 拉 A 的 session — A 端收到后异步回复
|
|
2246
|
+
const fromPk = v3P2PRef.getPublicKey();
|
|
2247
|
+
const rpcId = `hist-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2248
|
+
const msg = JSON.stringify({
|
|
2249
|
+
v: 3,
|
|
2250
|
+
op: 'agent.history.get',
|
|
2251
|
+
payload: { rpcId, channelId, fromPublicKey: fromPk }
|
|
2252
|
+
});
|
|
2253
|
+
const ok = v3P2PRef.sendTo(targetPublicKey, msg);
|
|
2254
|
+
if (!ok) {
|
|
2255
|
+
return res.status(502).json({ error: 'peer not connected' });
|
|
2256
|
+
}
|
|
2257
|
+
// 等待 A 异步回复 (15s timeout) — 用一个 Promise 等
|
|
2258
|
+
const result = await new Promise((resolve, reject) => {
|
|
2259
|
+
const timer = setTimeout(() => {
|
|
2260
|
+
v3PendingHistoryGets.delete(rpcId);
|
|
2261
|
+
reject(new Error('A 端 15s 内未回复, 可能未分享该 channel'));
|
|
2262
|
+
}, 15000);
|
|
2263
|
+
v3PendingHistoryGets.set(rpcId, {
|
|
2264
|
+
resolve: (data) => { clearTimeout(timer); resolve(data); },
|
|
2265
|
+
reject: (err) => { clearTimeout(timer); reject(err); }
|
|
2266
|
+
});
|
|
2267
|
+
});
|
|
2268
|
+
console.log(`[v3] chat-history 从 ${targetPublicKey.substring(0, 12)}... 拉到 ${(result.messages || []).length} 条`);
|
|
2269
|
+
res.json(result);
|
|
2270
|
+
}
|
|
2271
|
+
catch (err) {
|
|
2272
|
+
console.error('[v3] chat-history 失败:', err.message);
|
|
2273
|
+
res.status(504).json({ error: err.message });
|
|
2274
|
+
}
|
|
2275
|
+
});
|
|
2007
2276
|
// 获取已连接的节点
|
|
2008
2277
|
app.get('/api/peers', async (_req, res) => {
|
|
2009
2278
|
try {
|
package/package.json
CHANGED
package/src/llm/config-store.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import * as fs from 'fs/promises';
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
|
|
9
|
-
export type ModelProvider = 'openai' | 'anthropic' | 'ollama' | 'openrouter' | 'gemini' | 'minimax' | 'local';
|
|
9
|
+
export type ModelProvider = 'openai' | 'anthropic' | 'ollama' | 'openrouter' | 'gemini' | 'minimax' | 'deepseek' | 'kimi' | 'glm' | 'qwen' | 'local';
|
|
10
10
|
|
|
11
11
|
export interface ProviderConfig {
|
|
12
12
|
enabled: boolean;
|
|
@@ -82,6 +82,42 @@ export const DEFAULT_PROVIDER_CONFIGS: Record<ModelProvider, ProviderConfig> = {
|
|
|
82
82
|
maxTokens: 4096,
|
|
83
83
|
requiresApiKey: true
|
|
84
84
|
},
|
|
85
|
+
deepseek: {
|
|
86
|
+
enabled: false,
|
|
87
|
+
apiKey: '',
|
|
88
|
+
baseUrl: 'https://api.deepseek.com/v1',
|
|
89
|
+
model: 'deepseek-chat',
|
|
90
|
+
temperature: 0.7,
|
|
91
|
+
maxTokens: 4096,
|
|
92
|
+
requiresApiKey: true
|
|
93
|
+
},
|
|
94
|
+
kimi: {
|
|
95
|
+
enabled: false,
|
|
96
|
+
apiKey: '',
|
|
97
|
+
baseUrl: 'https://api.moonshot.cn/v1',
|
|
98
|
+
model: 'moonshot-v1-8k',
|
|
99
|
+
temperature: 0.7,
|
|
100
|
+
maxTokens: 4096,
|
|
101
|
+
requiresApiKey: true
|
|
102
|
+
},
|
|
103
|
+
glm: {
|
|
104
|
+
enabled: false,
|
|
105
|
+
apiKey: '',
|
|
106
|
+
baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
|
|
107
|
+
model: 'glm-4-flash',
|
|
108
|
+
temperature: 0.7,
|
|
109
|
+
maxTokens: 4096,
|
|
110
|
+
requiresApiKey: true
|
|
111
|
+
},
|
|
112
|
+
qwen: {
|
|
113
|
+
enabled: false,
|
|
114
|
+
apiKey: '',
|
|
115
|
+
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
116
|
+
model: 'qwen-plus',
|
|
117
|
+
temperature: 0.7,
|
|
118
|
+
maxTokens: 4096,
|
|
119
|
+
requiresApiKey: true
|
|
120
|
+
},
|
|
85
121
|
local: {
|
|
86
122
|
enabled: false,
|
|
87
123
|
apiKey: '',
|
|
@@ -100,6 +136,10 @@ export const PROVIDER_INFO: Record<ModelProvider, { name: string; description: s
|
|
|
100
136
|
gemini: { name: 'Google Gemini', description: 'Gemini 系列模型', requiresApiKey: true },
|
|
101
137
|
ollama: { name: 'Ollama', description: '本地 LLM 运行框架', requiresApiKey: false },
|
|
102
138
|
minimax: { name: 'MiniMax', description: '国产大模型服务', requiresApiKey: true },
|
|
139
|
+
deepseek: { name: 'DeepSeek', description: '深度求索大模型', requiresApiKey: true },
|
|
140
|
+
kimi: { name: 'Kimi (月之暗面)', description: 'Moonshot 长上下文模型', requiresApiKey: true },
|
|
141
|
+
glm: { name: 'GLM (智谱)', description: '智谱 ChatGLM 系列模型', requiresApiKey: true },
|
|
142
|
+
qwen: { name: 'Qwen (通义千问)', description: '阿里云通义千问系列', requiresApiKey: true },
|
|
103
143
|
local: { name: '本地模型', description: '本地部署的模型服务', requiresApiKey: false }
|
|
104
144
|
};
|
|
105
145
|
|
|
@@ -121,6 +161,18 @@ function getDefaultConfig(): LLMConfig {
|
|
|
121
161
|
if (process.env.MINIMAX_API_KEY) {
|
|
122
162
|
envConfigs.minimax = { ...DEFAULT_PROVIDER_CONFIGS.minimax, enabled: true, apiKey: process.env.MINIMAX_API_KEY };
|
|
123
163
|
}
|
|
164
|
+
if (process.env.DEEPSEEK_API_KEY) {
|
|
165
|
+
envConfigs.deepseek = { ...DEFAULT_PROVIDER_CONFIGS.deepseek, enabled: true, apiKey: process.env.DEEPSEEK_API_KEY };
|
|
166
|
+
}
|
|
167
|
+
if (process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY) {
|
|
168
|
+
envConfigs.kimi = { ...DEFAULT_PROVIDER_CONFIGS.kimi, enabled: true, apiKey: process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY || '' };
|
|
169
|
+
}
|
|
170
|
+
if (process.env.GLM_API_KEY || process.env.ZHIPU_API_KEY) {
|
|
171
|
+
envConfigs.glm = { ...DEFAULT_PROVIDER_CONFIGS.glm, enabled: true, apiKey: process.env.GLM_API_KEY || process.env.ZHIPU_API_KEY || '' };
|
|
172
|
+
}
|
|
173
|
+
if (process.env.QWEN_API_KEY || process.env.DASHSCOPE_API_KEY) {
|
|
174
|
+
envConfigs.qwen = { ...DEFAULT_PROVIDER_CONFIGS.qwen, enabled: true, apiKey: process.env.QWEN_API_KEY || process.env.DASHSCOPE_API_KEY || '' };
|
|
175
|
+
}
|
|
124
176
|
if (process.env.OLLAMA_BASE_URL) {
|
|
125
177
|
envConfigs.ollama = { ...DEFAULT_PROVIDER_CONFIGS.ollama, enabled: true, baseUrl: process.env.OLLAMA_BASE_URL };
|
|
126
178
|
}
|
|
@@ -131,6 +183,10 @@ function getDefaultConfig(): LLMConfig {
|
|
|
131
183
|
else if (process.env.OPENROUTER_API_KEY) activeProvider = 'openrouter';
|
|
132
184
|
else if (process.env.GEMINI_API_KEY) activeProvider = 'gemini';
|
|
133
185
|
else if (process.env.MINIMAX_API_KEY) activeProvider = 'minimax';
|
|
186
|
+
else if (process.env.DEEPSEEK_API_KEY) activeProvider = 'deepseek';
|
|
187
|
+
else if (process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY) activeProvider = 'kimi';
|
|
188
|
+
else if (process.env.GLM_API_KEY || process.env.ZHIPU_API_KEY) activeProvider = 'glm';
|
|
189
|
+
else if (process.env.QWEN_API_KEY || process.env.DASHSCOPE_API_KEY) activeProvider = 'qwen';
|
|
134
190
|
else if (process.env.OLLAMA_BASE_URL) activeProvider = 'ollama';
|
|
135
191
|
|
|
136
192
|
const providers = { ...DEFAULT_PROVIDER_CONFIGS };
|
|
@@ -281,6 +337,10 @@ class LLMConfigStore {
|
|
|
281
337
|
case 'openai':
|
|
282
338
|
case 'openrouter':
|
|
283
339
|
case 'minimax':
|
|
340
|
+
case 'deepseek':
|
|
341
|
+
case 'kimi':
|
|
342
|
+
case 'glm':
|
|
343
|
+
case 'qwen':
|
|
284
344
|
if (config.apiKey) headers['Authorization'] = `Bearer ${config.apiKey}`;
|
|
285
345
|
break;
|
|
286
346
|
case 'anthropic':
|
package/src/llm/pi-ai.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
|
|
4
|
-
export type ModelProvider = 'openai' | 'anthropic' | 'ollama' | 'openrouter' | 'gemini' | 'minimax' | 'local';
|
|
4
|
+
export type ModelProvider = 'openai' | 'anthropic' | 'ollama' | 'openrouter' | 'gemini' | 'minimax' | 'deepseek' | 'kimi' | 'glm' | 'qwen' | 'local';
|
|
5
5
|
|
|
6
6
|
export interface ModelConfig {
|
|
7
7
|
provider: ModelProvider;
|
|
@@ -105,6 +105,10 @@ export class PiAIModel {
|
|
|
105
105
|
switch (this.provider) {
|
|
106
106
|
case 'openai':
|
|
107
107
|
case 'minimax':
|
|
108
|
+
case 'deepseek':
|
|
109
|
+
case 'kimi':
|
|
110
|
+
case 'glm':
|
|
111
|
+
case 'qwen':
|
|
108
112
|
return this.callOpenAI(messages, temperature, maxTokens);
|
|
109
113
|
case 'anthropic':
|
|
110
114
|
return this.callAnthropic(messages, temperature, maxTokens);
|