@next-open-ai/openbot 0.3.2 → 0.6.6
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/README.md +1 -2
- package/apps/desktop/renderer/dist/assets/index-BNuvb6Ay.css +10 -0
- package/apps/desktop/renderer/dist/assets/index-DvQjslfT.js +89 -0
- package/apps/desktop/renderer/dist/index.html +2 -2
- package/dist/core/agent/agent-manager.d.ts +15 -7
- package/dist/core/agent/agent-manager.js +52 -21
- package/dist/core/agent/run.js +2 -2
- package/dist/core/config/desktop-config.d.ts +13 -0
- package/dist/core/config/desktop-config.js +19 -1
- package/dist/core/session-current-agent.d.ts +34 -0
- package/dist/core/session-current-agent.js +32 -0
- package/dist/core/tools/create-agent-tool.d.ts +6 -0
- package/dist/core/tools/create-agent-tool.js +97 -0
- package/dist/core/tools/index.d.ts +3 -0
- package/dist/core/tools/index.js +3 -0
- package/dist/core/tools/list-agents-tool.d.ts +5 -0
- package/dist/core/tools/list-agents-tool.js +45 -0
- package/dist/core/tools/switch-agent-tool.d.ts +6 -0
- package/dist/core/tools/switch-agent-tool.js +54 -0
- package/dist/gateway/channel/adapters/feishu.d.ts +11 -0
- package/dist/gateway/channel/adapters/feishu.js +218 -0
- package/dist/gateway/channel/channel-core.d.ts +9 -0
- package/dist/gateway/channel/channel-core.js +127 -0
- package/dist/gateway/channel/registry.d.ts +16 -0
- package/dist/gateway/channel/registry.js +54 -0
- package/dist/gateway/channel/run-agent.d.ts +26 -0
- package/dist/gateway/channel/run-agent.js +137 -0
- package/dist/gateway/channel/session-persistence.d.ts +36 -0
- package/dist/gateway/channel/session-persistence.js +46 -0
- package/dist/gateway/channel/types.d.ts +70 -0
- package/dist/gateway/channel/types.js +4 -0
- package/dist/gateway/channel-handler.d.ts +3 -4
- package/dist/gateway/channel-handler.js +8 -2
- package/dist/gateway/methods/agent-chat.js +30 -12
- package/dist/gateway/methods/run-scheduled-task.js +4 -2
- package/dist/gateway/server.js +50 -1
- package/dist/server/agent-config/agent-config.controller.d.ts +6 -1
- package/dist/server/agent-config/agent-config.service.d.ts +12 -1
- package/dist/server/agent-config/agent-config.service.js +10 -3
- package/dist/server/agents/agents.controller.d.ts +10 -0
- package/dist/server/agents/agents.controller.js +35 -1
- package/dist/server/agents/agents.gateway.js +18 -4
- package/dist/server/agents/agents.service.d.ts +4 -0
- package/dist/server/agents/agents.service.js +17 -1
- package/dist/server/config/config.controller.d.ts +2 -0
- package/dist/server/config/config.service.d.ts +3 -0
- package/dist/server/config/config.service.js +3 -1
- package/dist/server/saved-items/saved-items.controller.d.ts +32 -1
- package/dist/server/saved-items/saved-items.controller.js +154 -3
- package/dist/server/saved-items/saved-items.module.js +3 -1
- package/dist/server/workspace/workspace.service.d.ts +11 -0
- package/dist/server/workspace/workspace.service.js +40 -1
- package/package.json +2 -1
- package/apps/desktop/renderer/dist/assets/index-DKtaRFW4.js +0 -89
- package/apps/desktop/renderer/dist/assets/index-QHuqXpWQ.css +0 -10
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 飞书通道适配器:使用 @larksuiteoapi/node-sdk WebSocket 长连接模式接收消息,API 发送回复。
|
|
3
|
+
*/
|
|
4
|
+
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
5
|
+
import { dispatchMessage } from "../registry.js";
|
|
6
|
+
function parseFeishuTextContent(content) {
|
|
7
|
+
try {
|
|
8
|
+
const obj = JSON.parse(content);
|
|
9
|
+
if (obj && typeof obj.text === "string")
|
|
10
|
+
return obj.text;
|
|
11
|
+
return String(content);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return String(content);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** 已处理的飞书 message_id 缓存,避免同一条消息被重复触发导致多条回复与顺序错乱。TTL 5 分钟。 */
|
|
18
|
+
const PROCESSED_MESSAGE_IDS = new Map();
|
|
19
|
+
const PROCESSED_TTL_MS = 5 * 60 * 1000;
|
|
20
|
+
function isMessageAlreadyProcessed(messageId) {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
if (PROCESSED_MESSAGE_IDS.has(messageId)) {
|
|
23
|
+
const ts = PROCESSED_MESSAGE_IDS.get(messageId);
|
|
24
|
+
if (now - ts < PROCESSED_TTL_MS)
|
|
25
|
+
return true;
|
|
26
|
+
PROCESSED_MESSAGE_IDS.delete(messageId);
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function markMessageProcessed(messageId) {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
PROCESSED_MESSAGE_IDS.set(messageId, now);
|
|
33
|
+
if (PROCESSED_MESSAGE_IDS.size > 5000) {
|
|
34
|
+
for (const [id, ts] of PROCESSED_MESSAGE_IDS.entries()) {
|
|
35
|
+
if (now - ts > PROCESSED_TTL_MS)
|
|
36
|
+
PROCESSED_MESSAGE_IDS.delete(id);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 飞书 WebSocket 入站:SDK WSClient + EventDispatcher,收到 im.message.receive_v1 后转 UnifiedMessage。
|
|
42
|
+
* 按 message_id 去重,同一条用户消息只处理一次,避免重复回复与顺序错乱。
|
|
43
|
+
*/
|
|
44
|
+
class FeishuWSInbound {
|
|
45
|
+
config;
|
|
46
|
+
wsClient = null;
|
|
47
|
+
messageHandler = null;
|
|
48
|
+
constructor(config) {
|
|
49
|
+
this.config = config;
|
|
50
|
+
}
|
|
51
|
+
setMessageHandler(handler) {
|
|
52
|
+
this.messageHandler = handler;
|
|
53
|
+
}
|
|
54
|
+
async start() {
|
|
55
|
+
if (this.wsClient)
|
|
56
|
+
return;
|
|
57
|
+
const { appId, appSecret } = this.config;
|
|
58
|
+
if (!appId?.trim() || !appSecret?.trim()) {
|
|
59
|
+
console.warn("[Feishu] appId/appSecret missing, skip WS start");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const eventDispatcher = new Lark.EventDispatcher({}).register({
|
|
63
|
+
// 已读回执事件,无需处理,注册空 handler 避免 SDK 打 no handle 的 warn
|
|
64
|
+
"im.message.message_read_v1": async () => { },
|
|
65
|
+
"im.message.receive_v1": async (data) => {
|
|
66
|
+
const msg = data?.message;
|
|
67
|
+
if (!msg?.chat_id)
|
|
68
|
+
return;
|
|
69
|
+
const messageId = msg?.message_id;
|
|
70
|
+
if (messageId && isMessageAlreadyProcessed(messageId)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (messageId)
|
|
74
|
+
markMessageProcessed(messageId);
|
|
75
|
+
const content = msg.content ? parseFeishuTextContent(msg.content) : "";
|
|
76
|
+
if (!content.trim())
|
|
77
|
+
return;
|
|
78
|
+
const sender = data?.sender?.sender_id?.open_id ?? data?.sender?.sender_id?.user_id ?? "";
|
|
79
|
+
const unified = {
|
|
80
|
+
channelId: "feishu",
|
|
81
|
+
threadId: msg.chat_id,
|
|
82
|
+
userId: sender || "unknown",
|
|
83
|
+
userName: data?.sender?.sender_id?.name,
|
|
84
|
+
messageText: content,
|
|
85
|
+
replyTarget: "default",
|
|
86
|
+
messageId,
|
|
87
|
+
raw: data,
|
|
88
|
+
};
|
|
89
|
+
if (this.messageHandler) {
|
|
90
|
+
await this.messageHandler(unified);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
await dispatchMessage(unified);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
this.wsClient = new Lark.WSClient({
|
|
98
|
+
appId,
|
|
99
|
+
appSecret,
|
|
100
|
+
loggerLevel: Lark.LoggerLevel.warn,
|
|
101
|
+
});
|
|
102
|
+
await this.wsClient.start({ eventDispatcher });
|
|
103
|
+
console.log("[Feishu] WS client started");
|
|
104
|
+
}
|
|
105
|
+
async stop() {
|
|
106
|
+
if (this.wsClient) {
|
|
107
|
+
try {
|
|
108
|
+
await this.wsClient.stop?.();
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
console.warn("[Feishu] WS stop error", e);
|
|
112
|
+
}
|
|
113
|
+
this.wsClient = null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 飞书 API 出站:用 Client 调发送消息接口。
|
|
119
|
+
*/
|
|
120
|
+
class FeishuApiOutbound {
|
|
121
|
+
client;
|
|
122
|
+
constructor(config) {
|
|
123
|
+
this.client = new Lark.Client({
|
|
124
|
+
appId: config.appId,
|
|
125
|
+
appSecret: config.appSecret,
|
|
126
|
+
appType: Lark.AppType.SelfBuild,
|
|
127
|
+
domain: Lark.Domain.Feishu,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async send(targetId, reply) {
|
|
131
|
+
if (!targetId || targetId === "default") {
|
|
132
|
+
console.error("[Feishu] send skipped: invalid receive_id (missing or 'default'), check threadId from event");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const text = reply.text?.trim() || "(无内容)";
|
|
136
|
+
try {
|
|
137
|
+
await this.client.im.v1.message.create({
|
|
138
|
+
params: { receive_id_type: "chat_id" },
|
|
139
|
+
data: {
|
|
140
|
+
receive_id: targetId,
|
|
141
|
+
content: JSON.stringify({ text }),
|
|
142
|
+
msg_type: "text",
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
console.error("[Feishu] send message failed:", e);
|
|
148
|
+
throw e;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async sendStream(targetId) {
|
|
152
|
+
if (!targetId || targetId === "default") {
|
|
153
|
+
throw new Error("[Feishu] sendStream: invalid receive_id");
|
|
154
|
+
}
|
|
155
|
+
const initialCard = {
|
|
156
|
+
config: { wide_screen_mode: true },
|
|
157
|
+
header: { title: { tag: "plain_text", content: "🤔 思考中..." } },
|
|
158
|
+
elements: [{ tag: "div", text: { tag: "plain_text", content: "正在生成回答,请稍候..." } }],
|
|
159
|
+
};
|
|
160
|
+
const createRes = await this.client.im.v1.message.create({
|
|
161
|
+
params: { receive_id_type: "chat_id" },
|
|
162
|
+
data: {
|
|
163
|
+
receive_id: targetId,
|
|
164
|
+
msg_type: "interactive",
|
|
165
|
+
content: JSON.stringify(initialCard),
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
const messageId = createRes.data?.message_id;
|
|
169
|
+
if (!messageId) {
|
|
170
|
+
throw new Error("[Feishu] sendStream: create card did not return message_id");
|
|
171
|
+
}
|
|
172
|
+
const patch = async (content, title, showCursor) => {
|
|
173
|
+
const card = {
|
|
174
|
+
config: { wide_screen_mode: true },
|
|
175
|
+
header: { title: { tag: "plain_text", content: title } },
|
|
176
|
+
elements: [{ tag: "markdown", content: content + (showCursor ? " ▌" : "") }],
|
|
177
|
+
};
|
|
178
|
+
await this.client.im.v1.message.patch({
|
|
179
|
+
path: { message_id: messageId },
|
|
180
|
+
data: { content: JSON.stringify(card) },
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
return {
|
|
184
|
+
onChunk: async (accumulated) => {
|
|
185
|
+
try {
|
|
186
|
+
await patch(accumulated || " ", "🤖 回答中...", true);
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
console.error("[Feishu] stream patch failed:", e);
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
onDone: async (accumulated) => {
|
|
193
|
+
try {
|
|
194
|
+
await patch(accumulated || "(无内容)", "✅ 回答完成", false);
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
console.error("[Feishu] stream final patch failed:", e);
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 飞书通道:WebSocket 入站 + API 出站,使用官方 Node SDK。
|
|
205
|
+
*/
|
|
206
|
+
export function createFeishuChannel(config) {
|
|
207
|
+
const inbound = new FeishuWSInbound(config);
|
|
208
|
+
const outbound = new FeishuApiOutbound(config);
|
|
209
|
+
// 入站回调统一走 registry 分发(会带上 channelId,找到本 channel 再处理)
|
|
210
|
+
inbound.setMessageHandler((msg) => dispatchMessage(msg));
|
|
211
|
+
return {
|
|
212
|
+
id: "feishu",
|
|
213
|
+
name: "飞书",
|
|
214
|
+
defaultAgentId: config.defaultAgentId ?? "default",
|
|
215
|
+
getInbounds: () => [inbound],
|
|
216
|
+
getOutbounds: () => [outbound],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通道核心:收 UnifiedMessage → 会话映射 → 调 Agent → 选 Outbound → 发 UnifiedReply。
|
|
3
|
+
*/
|
|
4
|
+
import type { UnifiedMessage, IChannel } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* 处理一条入站消息:映射 session、跑 Agent、选出站并发送回复。
|
|
7
|
+
* 若 outbound 支持 sendStream,则用流式(先发卡片再逐次更新);否则一次性 send。
|
|
8
|
+
*/
|
|
9
|
+
export declare function handleChannelMessage(channel: IChannel, msg: UnifiedMessage): Promise<void>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { runAgentAndCollectReply, runAgentAndStreamReply } from "./run-agent.js";
|
|
2
|
+
import { getChannelSessionPersistence, persistChannelUserMessage, persistChannelAssistantMessage } from "./session-persistence.js";
|
|
3
|
+
const STREAM_THROTTLE_MS = 280;
|
|
4
|
+
function toSessionId(channelId, threadId) {
|
|
5
|
+
return `channel:${channelId}:${threadId}`;
|
|
6
|
+
}
|
|
7
|
+
/** 节流:在间隔内只执行最后一次 */
|
|
8
|
+
function throttle(fn, ms) {
|
|
9
|
+
let timer = null;
|
|
10
|
+
let lastRun = 0;
|
|
11
|
+
const run = () => {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
if (timer)
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
const elapsed = now - lastRun;
|
|
16
|
+
if (elapsed >= ms || lastRun === 0) {
|
|
17
|
+
lastRun = now;
|
|
18
|
+
fn();
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
timer = setTimeout(() => {
|
|
22
|
+
timer = null;
|
|
23
|
+
lastRun = Date.now();
|
|
24
|
+
fn();
|
|
25
|
+
}, ms - elapsed);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const flush = () => {
|
|
29
|
+
if (timer)
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
timer = null;
|
|
32
|
+
lastRun = Date.now();
|
|
33
|
+
fn();
|
|
34
|
+
};
|
|
35
|
+
const cancel = () => {
|
|
36
|
+
if (timer)
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
timer = null;
|
|
39
|
+
};
|
|
40
|
+
return { run, flush, cancel };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 处理一条入站消息:映射 session、跑 Agent、选出站并发送回复。
|
|
44
|
+
* 若 outbound 支持 sendStream,则用流式(先发卡片再逐次更新);否则一次性 send。
|
|
45
|
+
*/
|
|
46
|
+
export async function handleChannelMessage(channel, msg) {
|
|
47
|
+
const sessionId = toSessionId(msg.channelId, msg.threadId);
|
|
48
|
+
const defaultAgentId = channel.defaultAgentId ?? "default";
|
|
49
|
+
// 当前 agent:已存(DB)> 通道默认,保证对话中切换 agent 后下次仍用新 agent
|
|
50
|
+
const persistence = getChannelSessionPersistence();
|
|
51
|
+
const currentAgentId = persistence?.getSession(sessionId)?.agentId ?? defaultAgentId;
|
|
52
|
+
const outbound = channel.getOutboundForMessage
|
|
53
|
+
? channel.getOutboundForMessage(msg)
|
|
54
|
+
: channel.getOutbounds()[0];
|
|
55
|
+
if (!outbound) {
|
|
56
|
+
console.warn("[ChannelCore] no outbound for message", msg.channelId, msg.threadId);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const threadId = msg.threadId;
|
|
60
|
+
if (!threadId || threadId === "default") {
|
|
61
|
+
console.warn("[ChannelCore] invalid threadId, skip reply");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// 与 Web/Desktop 统一:通道会话入库并追加用户消息(用 currentAgentId)
|
|
65
|
+
await persistChannelUserMessage(sessionId, {
|
|
66
|
+
agentId: currentAgentId,
|
|
67
|
+
title: msg.messageText?.trim().slice(0, 50) || undefined,
|
|
68
|
+
messageText: msg.messageText?.trim() || "",
|
|
69
|
+
});
|
|
70
|
+
const useStream = typeof outbound.sendStream === "function";
|
|
71
|
+
if (useStream) {
|
|
72
|
+
let sink;
|
|
73
|
+
try {
|
|
74
|
+
sink = await outbound.sendStream(threadId);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.error("[ChannelCore] sendStream init failed:", err);
|
|
78
|
+
const reply = { text: "发信失败,请稍后再试。" };
|
|
79
|
+
await outbound.send(threadId, reply);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
let accumulated = "";
|
|
83
|
+
let donePromise = null;
|
|
84
|
+
const throttled = throttle(() => {
|
|
85
|
+
if (sink.onChunk)
|
|
86
|
+
void Promise.resolve(sink.onChunk(accumulated)).catch((e) => console.error("[ChannelCore] stream onChunk error:", e));
|
|
87
|
+
}, STREAM_THROTTLE_MS);
|
|
88
|
+
try {
|
|
89
|
+
await runAgentAndStreamReply({ sessionId, message: msg.messageText, agentId: currentAgentId }, {
|
|
90
|
+
onChunk(delta) {
|
|
91
|
+
accumulated += delta;
|
|
92
|
+
throttled.run();
|
|
93
|
+
},
|
|
94
|
+
onDone() {
|
|
95
|
+
throttled.cancel();
|
|
96
|
+
const final = accumulated.trim() || "(无文本回复)";
|
|
97
|
+
persistChannelAssistantMessage(sessionId, final);
|
|
98
|
+
donePromise = Promise.resolve(sink.onDone(final)).catch((e) => console.error("[ChannelCore] stream onDone error:", e));
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
if (donePromise)
|
|
102
|
+
await donePromise;
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
console.error("[ChannelCore] runAgent failed:", err);
|
|
106
|
+
throttled.cancel();
|
|
107
|
+
const fallback = accumulated.trim() || "处理时出错,请稍后再试。";
|
|
108
|
+
await Promise.resolve(sink.onDone(fallback)).catch(() => { });
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
let replyText;
|
|
113
|
+
try {
|
|
114
|
+
replyText = await runAgentAndCollectReply({
|
|
115
|
+
sessionId,
|
|
116
|
+
message: msg.messageText,
|
|
117
|
+
agentId: currentAgentId,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
console.error("[ChannelCore] runAgent failed:", err);
|
|
122
|
+
replyText = "处理时出错,请稍后再试。";
|
|
123
|
+
}
|
|
124
|
+
persistChannelAssistantMessage(sessionId, replyText);
|
|
125
|
+
const reply = { text: replyText };
|
|
126
|
+
await outbound.send(threadId, reply);
|
|
127
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通道注册表:按 channelId 注册与获取通道,供入站回调分发。
|
|
3
|
+
*/
|
|
4
|
+
import type { IChannel, UnifiedMessage } from "./types.js";
|
|
5
|
+
export declare function registerChannel(channel: IChannel): void;
|
|
6
|
+
export declare function unregisterChannel(channelId: string): void;
|
|
7
|
+
export declare function getChannel(channelId: string): IChannel | undefined;
|
|
8
|
+
export declare function listChannels(): IChannel[];
|
|
9
|
+
/**
|
|
10
|
+
* 入站收到统一消息时调用:按 channelId 找到通道并交给核心处理。
|
|
11
|
+
*/
|
|
12
|
+
export declare function dispatchMessage(msg: UnifiedMessage): Promise<void>;
|
|
13
|
+
/** 启动所有已注册通道的入站传输 */
|
|
14
|
+
export declare function startAllChannels(): Promise<void>;
|
|
15
|
+
/** 停止所有已注册通道的入站传输 */
|
|
16
|
+
export declare function stopAllChannels(): Promise<void>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { handleChannelMessage } from "./channel-core.js";
|
|
2
|
+
const channels = new Map();
|
|
3
|
+
export function registerChannel(channel) {
|
|
4
|
+
if (channels.has(channel.id)) {
|
|
5
|
+
console.warn(`[ChannelRegistry] overwriting channel ${channel.id}`);
|
|
6
|
+
}
|
|
7
|
+
channels.set(channel.id, channel);
|
|
8
|
+
}
|
|
9
|
+
export function unregisterChannel(channelId) {
|
|
10
|
+
channels.delete(channelId);
|
|
11
|
+
}
|
|
12
|
+
export function getChannel(channelId) {
|
|
13
|
+
return channels.get(channelId);
|
|
14
|
+
}
|
|
15
|
+
export function listChannels() {
|
|
16
|
+
return Array.from(channels.values());
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 入站收到统一消息时调用:按 channelId 找到通道并交给核心处理。
|
|
20
|
+
*/
|
|
21
|
+
export async function dispatchMessage(msg) {
|
|
22
|
+
const channel = channels.get(msg.channelId);
|
|
23
|
+
if (!channel) {
|
|
24
|
+
console.warn("[ChannelRegistry] no channel for", msg.channelId);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
await handleChannelMessage(channel, msg);
|
|
28
|
+
}
|
|
29
|
+
/** 启动所有已注册通道的入站传输 */
|
|
30
|
+
export async function startAllChannels() {
|
|
31
|
+
for (const ch of channels.values()) {
|
|
32
|
+
for (const inbound of ch.getInbounds()) {
|
|
33
|
+
try {
|
|
34
|
+
await inbound.start();
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
console.warn("[ChannelRegistry] start inbound failed for", ch.id, e);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** 停止所有已注册通道的入站传输 */
|
|
43
|
+
export async function stopAllChannels() {
|
|
44
|
+
for (const ch of channels.values()) {
|
|
45
|
+
for (const inbound of ch.getInbounds()) {
|
|
46
|
+
try {
|
|
47
|
+
await inbound.stop();
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
console.warn("[ChannelRegistry] stop inbound failed for", ch.id, e);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface RunAgentForChannelOptions {
|
|
2
|
+
/** 会话 ID,建议 channel:platform:threadId */
|
|
3
|
+
sessionId: string;
|
|
4
|
+
/** 用户消息正文 */
|
|
5
|
+
message: string;
|
|
6
|
+
/** 使用的 agentId,缺省 default */
|
|
7
|
+
agentId?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface RunAgentStreamCallbacks {
|
|
10
|
+
/** 每收到一段助手文本 delta 时调用 */
|
|
11
|
+
onChunk(delta: string): void;
|
|
12
|
+
/** turn_end 时可选调用,供通道按需处理(如本小轮结束、tool 结果已出) */
|
|
13
|
+
onTurnEnd?: () => void;
|
|
14
|
+
/** agent_end 时调用,表示整轮对话真正结束 */
|
|
15
|
+
onDone(): void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 使用现有 agentManager 跑一轮对话,以流式回调方式推送助手回复(onChunk/onDone)。
|
|
19
|
+
* onDone 在 agent_end 时调用,保证「对话真正结束」语义(多轮 tool+文本合并为一条回复)。
|
|
20
|
+
*/
|
|
21
|
+
export declare function runAgentAndStreamReply(options: RunAgentForChannelOptions, callbacks: RunAgentStreamCallbacks): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* 使用现有 agentManager 跑一轮对话,收集助手完整回复文本后返回。
|
|
24
|
+
* 不依赖 WebSocket 客户端,供通道核心调用。
|
|
25
|
+
*/
|
|
26
|
+
export declare function runAgentAndCollectReply(options: RunAgentForChannelOptions): Promise<string>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通道用:跑 Agent 并收集完整回复文本(无 WebSocket 客户端)。
|
|
3
|
+
*/
|
|
4
|
+
import { agentManager } from "../../core/agent/agent-manager.js";
|
|
5
|
+
import { getDesktopConfig, loadDesktopAgentConfig } from "../../core/config/desktop-config.js";
|
|
6
|
+
import { getExperienceContextForUserMessage } from "../../core/memory/index.js";
|
|
7
|
+
/**
|
|
8
|
+
* 使用现有 agentManager 跑一轮对话,以流式回调方式推送助手回复(onChunk/onDone)。
|
|
9
|
+
* onDone 在 agent_end 时调用,保证「对话真正结束」语义(多轮 tool+文本合并为一条回复)。
|
|
10
|
+
*/
|
|
11
|
+
export async function runAgentAndStreamReply(options, callbacks) {
|
|
12
|
+
const { sessionId, message, agentId: optionAgentId } = options;
|
|
13
|
+
const sessionAgentId = optionAgentId ?? "default";
|
|
14
|
+
let workspace = "default";
|
|
15
|
+
let provider;
|
|
16
|
+
let modelId;
|
|
17
|
+
let apiKey;
|
|
18
|
+
const agentConfig = await loadDesktopAgentConfig(sessionAgentId);
|
|
19
|
+
if (agentConfig) {
|
|
20
|
+
if (agentConfig.workspace)
|
|
21
|
+
workspace = agentConfig.workspace;
|
|
22
|
+
provider = agentConfig.provider;
|
|
23
|
+
modelId = agentConfig.model;
|
|
24
|
+
if (agentConfig.apiKey)
|
|
25
|
+
apiKey = agentConfig.apiKey;
|
|
26
|
+
}
|
|
27
|
+
const { maxAgentSessions } = getDesktopConfig();
|
|
28
|
+
const session = await agentManager.getOrCreateSession(sessionId, {
|
|
29
|
+
agentId: sessionAgentId,
|
|
30
|
+
workspace,
|
|
31
|
+
provider,
|
|
32
|
+
modelId,
|
|
33
|
+
apiKey,
|
|
34
|
+
maxSessions: maxAgentSessions,
|
|
35
|
+
targetAgentId: sessionAgentId,
|
|
36
|
+
mcpServers: agentConfig?.mcpServers,
|
|
37
|
+
systemPrompt: agentConfig?.systemPrompt,
|
|
38
|
+
});
|
|
39
|
+
let resolveDone;
|
|
40
|
+
const donePromise = new Promise((r) => {
|
|
41
|
+
resolveDone = r;
|
|
42
|
+
});
|
|
43
|
+
const unsubscribe = session.subscribe((event) => {
|
|
44
|
+
if (event.type === "message_update") {
|
|
45
|
+
const update = event;
|
|
46
|
+
if (update.assistantMessageEvent?.type === "text_delta" && update.assistantMessageEvent?.delta) {
|
|
47
|
+
callbacks.onChunk(update.assistantMessageEvent.delta);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (event.type === "turn_end") {
|
|
51
|
+
callbacks.onTurnEnd?.();
|
|
52
|
+
}
|
|
53
|
+
else if (event.type === "agent_end") {
|
|
54
|
+
callbacks.onDone();
|
|
55
|
+
resolveDone();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
try {
|
|
59
|
+
const experienceBlock = await getExperienceContextForUserMessage();
|
|
60
|
+
const userMessageToSend = experienceBlock.trim().length > 0
|
|
61
|
+
? `${experienceBlock}\n\n用户问题:\n${message}`
|
|
62
|
+
: message;
|
|
63
|
+
await session.sendUserMessage(userMessageToSend, { deliverAs: "followUp" });
|
|
64
|
+
await Promise.race([
|
|
65
|
+
donePromise,
|
|
66
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("Channel agent reply timeout")), 120_000)),
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
unsubscribe();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 使用现有 agentManager 跑一轮对话,收集助手完整回复文本后返回。
|
|
75
|
+
* 不依赖 WebSocket 客户端,供通道核心调用。
|
|
76
|
+
*/
|
|
77
|
+
export async function runAgentAndCollectReply(options) {
|
|
78
|
+
const { sessionId, message, agentId: optionAgentId } = options;
|
|
79
|
+
const sessionAgentId = optionAgentId ?? "default";
|
|
80
|
+
let workspace = "default";
|
|
81
|
+
let provider;
|
|
82
|
+
let modelId;
|
|
83
|
+
let apiKey;
|
|
84
|
+
const agentConfig = await loadDesktopAgentConfig(sessionAgentId);
|
|
85
|
+
if (agentConfig) {
|
|
86
|
+
if (agentConfig.workspace)
|
|
87
|
+
workspace = agentConfig.workspace;
|
|
88
|
+
provider = agentConfig.provider;
|
|
89
|
+
modelId = agentConfig.model;
|
|
90
|
+
if (agentConfig.apiKey)
|
|
91
|
+
apiKey = agentConfig.apiKey;
|
|
92
|
+
}
|
|
93
|
+
const { maxAgentSessions } = getDesktopConfig();
|
|
94
|
+
const session = await agentManager.getOrCreateSession(sessionId, {
|
|
95
|
+
agentId: sessionAgentId,
|
|
96
|
+
workspace,
|
|
97
|
+
provider,
|
|
98
|
+
modelId,
|
|
99
|
+
apiKey,
|
|
100
|
+
maxSessions: maxAgentSessions,
|
|
101
|
+
targetAgentId: sessionAgentId,
|
|
102
|
+
mcpServers: agentConfig?.mcpServers,
|
|
103
|
+
systemPrompt: agentConfig?.systemPrompt,
|
|
104
|
+
});
|
|
105
|
+
const chunks = [];
|
|
106
|
+
let resolveDone;
|
|
107
|
+
const donePromise = new Promise((r) => {
|
|
108
|
+
resolveDone = r;
|
|
109
|
+
});
|
|
110
|
+
const unsubscribe = session.subscribe((event) => {
|
|
111
|
+
if (event.type === "message_update") {
|
|
112
|
+
const update = event;
|
|
113
|
+
if (update.assistantMessageEvent?.type === "text_delta" && update.assistantMessageEvent?.delta) {
|
|
114
|
+
chunks.push(update.assistantMessageEvent.delta);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (event.type === "agent_end") {
|
|
118
|
+
resolveDone();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
try {
|
|
122
|
+
const experienceBlock = await getExperienceContextForUserMessage();
|
|
123
|
+
const userMessageToSend = experienceBlock.trim().length > 0
|
|
124
|
+
? `${experienceBlock}\n\n用户问题:\n${message}`
|
|
125
|
+
: message;
|
|
126
|
+
await session.sendUserMessage(userMessageToSend, { deliverAs: "followUp" });
|
|
127
|
+
// 等待 agent_end(整轮对话真正结束)或超时
|
|
128
|
+
await Promise.race([
|
|
129
|
+
donePromise,
|
|
130
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error("Channel agent reply timeout")), 120_000)),
|
|
131
|
+
]);
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
unsubscribe();
|
|
135
|
+
}
|
|
136
|
+
return chunks.join("").trim() || "(无文本回复)";
|
|
137
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通道会话持久化:与 Web/Desktop 统一使用同一套 session + chat_messages 入库。
|
|
3
|
+
* 由 Gateway 启动时注入 AgentsService,通道在处理消息前后调用 ensureSession / appendMessage。
|
|
4
|
+
*/
|
|
5
|
+
export type SessionType = "chat" | "scheduled" | "system";
|
|
6
|
+
export interface IChannelSessionPersistence {
|
|
7
|
+
getOrCreateSession(sessionId: string, options?: {
|
|
8
|
+
agentId?: string;
|
|
9
|
+
workspace?: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
type?: SessionType;
|
|
12
|
+
}): Promise<unknown>;
|
|
13
|
+
getSession(sessionId: string): {
|
|
14
|
+
agentId?: string;
|
|
15
|
+
} | undefined;
|
|
16
|
+
appendMessage(sessionId: string, role: "user" | "assistant", content: string, options?: {
|
|
17
|
+
toolCalls?: unknown[];
|
|
18
|
+
contentParts?: unknown[];
|
|
19
|
+
}): void;
|
|
20
|
+
}
|
|
21
|
+
export declare function setChannelSessionPersistence(service: IChannelSessionPersistence | null): void;
|
|
22
|
+
export declare function getChannelSessionPersistence(): IChannelSessionPersistence | null;
|
|
23
|
+
/**
|
|
24
|
+
* 确保通道会话已入库(getOrCreateSession),并追加一条用户消息。
|
|
25
|
+
* 在跑 Agent 前调用;若未注入持久化则静默跳过。
|
|
26
|
+
*/
|
|
27
|
+
export declare function persistChannelUserMessage(sessionId: string, options: {
|
|
28
|
+
agentId: string;
|
|
29
|
+
title?: string;
|
|
30
|
+
messageText: string;
|
|
31
|
+
}): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* 通道本轮助手回复结束时,将助手消息入库。
|
|
34
|
+
* 在 onDone 时调用;若未注入持久化则静默跳过。
|
|
35
|
+
*/
|
|
36
|
+
export declare function persistChannelAssistantMessage(sessionId: string, content: string): void;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通道会话持久化:与 Web/Desktop 统一使用同一套 session + chat_messages 入库。
|
|
3
|
+
* 由 Gateway 启动时注入 AgentsService,通道在处理消息前后调用 ensureSession / appendMessage。
|
|
4
|
+
*/
|
|
5
|
+
let persistence = null;
|
|
6
|
+
export function setChannelSessionPersistence(service) {
|
|
7
|
+
persistence = service;
|
|
8
|
+
}
|
|
9
|
+
export function getChannelSessionPersistence() {
|
|
10
|
+
return persistence;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 确保通道会话已入库(getOrCreateSession),并追加一条用户消息。
|
|
14
|
+
* 在跑 Agent 前调用;若未注入持久化则静默跳过。
|
|
15
|
+
*/
|
|
16
|
+
export async function persistChannelUserMessage(sessionId, options) {
|
|
17
|
+
const p = getChannelSessionPersistence();
|
|
18
|
+
if (!p)
|
|
19
|
+
return;
|
|
20
|
+
try {
|
|
21
|
+
await p.getOrCreateSession(sessionId, {
|
|
22
|
+
agentId: options.agentId,
|
|
23
|
+
title: options.title ?? (options.messageText.slice(0, 50) || undefined),
|
|
24
|
+
type: "chat",
|
|
25
|
+
});
|
|
26
|
+
p.appendMessage(sessionId, "user", options.messageText);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
console.warn("[ChannelSessionPersistence] persist user message failed:", e);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 通道本轮助手回复结束时,将助手消息入库。
|
|
34
|
+
* 在 onDone 时调用;若未注入持久化则静默跳过。
|
|
35
|
+
*/
|
|
36
|
+
export function persistChannelAssistantMessage(sessionId, content) {
|
|
37
|
+
const p = getChannelSessionPersistence();
|
|
38
|
+
if (!p)
|
|
39
|
+
return;
|
|
40
|
+
try {
|
|
41
|
+
p.appendMessage(sessionId, "assistant", content);
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
console.warn("[ChannelSessionPersistence] persist assistant message failed:", e);
|
|
45
|
+
}
|
|
46
|
+
}
|