@next-open-ai/openbot 0.3.2 → 0.6.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/README.md +46 -7
- package/apps/desktop/renderer/dist/assets/index-LCp1YPVA.css +10 -0
- package/apps/desktop/renderer/dist/assets/index-l5fpDsHs.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 +24 -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/dingtalk.d.ts +11 -0
- package/dist/gateway/channel/adapters/dingtalk.js +190 -0
- package/dist/gateway/channel/adapters/feishu.d.ts +11 -0
- package/dist/gateway/channel/adapters/feishu.js +218 -0
- package/dist/gateway/channel/adapters/telegram.d.ts +14 -0
- package/dist/gateway/channel/adapters/telegram.js +197 -0
- package/dist/gateway/channel/channel-core.d.ts +9 -0
- package/dist/gateway/channel/channel-core.js +135 -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 +74 -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 +84 -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 +3 -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,135 @@
|
|
|
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
|
+
onTurnEnd() {
|
|
95
|
+
throttled.flush();
|
|
96
|
+
if (sink.onTurnEnd)
|
|
97
|
+
void Promise.resolve(sink.onTurnEnd(accumulated)).catch((e) => console.error("[ChannelCore] stream onTurnEnd error:", e));
|
|
98
|
+
},
|
|
99
|
+
onDone() {
|
|
100
|
+
throttled.cancel();
|
|
101
|
+
const final = accumulated.trim() || "(无文本回复)";
|
|
102
|
+
persistChannelAssistantMessage(sessionId, final);
|
|
103
|
+
donePromise = Promise.resolve(sink.onDone(final)).catch((e) => console.error("[ChannelCore] stream onDone error:", e));
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
if (donePromise)
|
|
107
|
+
await donePromise;
|
|
108
|
+
await msg.ack?.(undefined);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
console.error("[ChannelCore] runAgent failed:", err);
|
|
112
|
+
throttled.cancel();
|
|
113
|
+
const fallback = accumulated.trim() || "处理时出错,请稍后再试。";
|
|
114
|
+
await Promise.resolve(sink.onDone(fallback)).catch(() => { });
|
|
115
|
+
await msg.ack?.(undefined);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
let replyText;
|
|
120
|
+
try {
|
|
121
|
+
replyText = await runAgentAndCollectReply({
|
|
122
|
+
sessionId,
|
|
123
|
+
message: msg.messageText,
|
|
124
|
+
agentId: currentAgentId,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
console.error("[ChannelCore] runAgent failed:", err);
|
|
129
|
+
replyText = "处理时出错,请稍后再试。";
|
|
130
|
+
}
|
|
131
|
+
persistChannelAssistantMessage(sessionId, replyText);
|
|
132
|
+
const reply = { text: replyText };
|
|
133
|
+
const sendResult = await outbound.send(threadId, reply);
|
|
134
|
+
await msg.ack?.(sendResult);
|
|
135
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通道模块统一类型:与传输方式(webhook / 长连接)解耦。
|
|
3
|
+
*/
|
|
4
|
+
/** 统一入站消息:所有入站解析后的共同结构 */
|
|
5
|
+
export interface UnifiedMessage {
|
|
6
|
+
/** 通道标识,如 feishu / telegram */
|
|
7
|
+
channelId: string;
|
|
8
|
+
/** 平台内会话/群组/单聊标识,回复时用 */
|
|
9
|
+
threadId: string;
|
|
10
|
+
/** 平台内用户标识 */
|
|
11
|
+
userId: string;
|
|
12
|
+
/** 用户显示名(可选) */
|
|
13
|
+
userName?: string;
|
|
14
|
+
/** 消息正文 */
|
|
15
|
+
messageText: string;
|
|
16
|
+
/** 附件(可选) */
|
|
17
|
+
attachments?: {
|
|
18
|
+
type: string;
|
|
19
|
+
url?: string;
|
|
20
|
+
name?: string;
|
|
21
|
+
}[];
|
|
22
|
+
/** 原始 payload(排查用) */
|
|
23
|
+
raw?: unknown;
|
|
24
|
+
/** 回复应交给哪个出站,如 "default" 或 connectionId */
|
|
25
|
+
replyTarget?: string;
|
|
26
|
+
/** 平台消息 ID(可选,用于回复引用等) */
|
|
27
|
+
messageId?: string;
|
|
28
|
+
/** 通道可选:回复发送完成后调用的 ack(如钉钉 Stream 需回调响应防重试),参数为 send 返回值 */
|
|
29
|
+
ack?: (sendResult?: unknown) => void | Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
/** 统一出站回复 */
|
|
32
|
+
export interface UnifiedReply {
|
|
33
|
+
text: string;
|
|
34
|
+
attachments?: {
|
|
35
|
+
type: string;
|
|
36
|
+
url?: string;
|
|
37
|
+
name?: string;
|
|
38
|
+
}[];
|
|
39
|
+
}
|
|
40
|
+
/** 入站传输:从外部收数据 → 产出 UnifiedMessage */
|
|
41
|
+
export interface IInboundTransport {
|
|
42
|
+
start(): Promise<void>;
|
|
43
|
+
stop(): Promise<void>;
|
|
44
|
+
/** 设置收到消息时的回调 */
|
|
45
|
+
setMessageHandler(handler: (msg: UnifiedMessage) => void | Promise<void>): void;
|
|
46
|
+
}
|
|
47
|
+
/** 流式出站:先发一条占位消息,再由调用方按累积内容多次更新。返回的 sink 由 channel-core 在 onChunk / onTurnEnd / onDone 时调用。 */
|
|
48
|
+
export interface StreamSink {
|
|
49
|
+
/** 更新当前已累积的全文(节流后调用),通道可用来做占位或实时更新 */
|
|
50
|
+
onChunk(accumulated: string): void | Promise<void>;
|
|
51
|
+
/** 可选:agent 每轮结束(turn_end)时调用,通道可在此发一条消息(如钉钉按轮发) */
|
|
52
|
+
onTurnEnd?(accumulated: string): void | Promise<void>;
|
|
53
|
+
/** 流结束,做最终一次更新 */
|
|
54
|
+
onDone(accumulated: string): void | Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
/** 出站传输:将 UnifiedReply 发到外部;返回值可传给 UnifiedMessage.ack */
|
|
57
|
+
export interface IOutboundTransport {
|
|
58
|
+
send(targetId: string, reply: UnifiedReply): Promise<unknown>;
|
|
59
|
+
/** 可选:该出站是否还能往 targetId 发(如连接是否有效) */
|
|
60
|
+
canSend?(targetId: string): boolean;
|
|
61
|
+
/** 可选:流式发送。先创建占位消息,返回 sink 供调用方按累积内容更新(如飞书 create + patch)。 */
|
|
62
|
+
sendStream?(targetId: string): Promise<StreamSink>;
|
|
63
|
+
}
|
|
64
|
+
/** 通道:身份 + 入站/出站列表 + 回复路由 */
|
|
65
|
+
export interface IChannel {
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
/** 默认 agentId,用于会话 */
|
|
69
|
+
defaultAgentId?: string;
|
|
70
|
+
getInbounds(): IInboundTransport[];
|
|
71
|
+
getOutbounds(): IOutboundTransport[];
|
|
72
|
+
/** 根据消息或 threadId 选择出站;默认返回第一个 outbound */
|
|
73
|
+
getOutboundForMessage?(msg: UnifiedMessage): IOutboundTransport | undefined;
|
|
74
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 通道模块 HTTP
|
|
3
|
-
* 后续:接收外部 webhook、回调,校验后转内部事件;当前返回 501。
|
|
2
|
+
* 通道模块 HTTP 处理:GET 列出已配置通道;具体入站由各通道(如飞书 WebSocket)自行处理。
|
|
4
3
|
*/
|
|
5
|
-
import type { Request, Response } from
|
|
6
|
-
export declare function handleChannel(
|
|
4
|
+
import type { Request, Response } from "express";
|
|
5
|
+
export declare function handleChannel(req: Request, res: Response): void;
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { listChannels } from "./channel/registry.js";
|
|
2
|
+
export function handleChannel(req, res) {
|
|
3
|
+
if (req.method === "GET") {
|
|
4
|
+
const channels = listChannels().map((c) => ({ id: c.id, name: c.name }));
|
|
5
|
+
res.status(200).setHeader("Content-Type", "application/json").end(JSON.stringify({ ok: true, channels }));
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
res.status(404).setHeader("Content-Type", "application/json").end(JSON.stringify({ ok: false, message: "Not found" }));
|
|
3
9
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { agentManager } from "../../core/agent/agent-manager.js";
|
|
2
|
+
import { getSessionCurrentAgentResolver, getSessionCurrentAgentUpdater } from "../../core/session-current-agent.js";
|
|
2
3
|
import { getExperienceContextForUserMessage } from "../../core/memory/index.js";
|
|
3
4
|
import { send, createEvent } from "../utils.js";
|
|
4
5
|
import { connectedClients } from "../clients.js";
|
|
5
6
|
import { getDesktopConfig, loadDesktopAgentConfig } from "../../core/config/desktop-config.js";
|
|
7
|
+
const COMPOSITE_KEY_SEP = "::";
|
|
6
8
|
/**
|
|
7
9
|
* Broadcast message to all clients subscribed to a session
|
|
8
10
|
*/
|
|
@@ -31,14 +33,21 @@ export async function handleAgentChat(client, params) {
|
|
|
31
33
|
}
|
|
32
34
|
async function handleAgentChatInner(client, targetSessionId, message, params) {
|
|
33
35
|
const { targetAgentId } = params;
|
|
34
|
-
// 客户端在 connect 或 agent.chat 传入 agentId/sessionType,Gateway 不再请求 Nest
|
|
35
|
-
const sessionAgentId = params.agentId ?? client.agentId ?? "default";
|
|
36
36
|
const sessionType = params.sessionType ?? client.sessionType ?? "chat";
|
|
37
|
+
// 当前 agent:请求传入 > 已存(DB)> 连接/默认
|
|
38
|
+
const resolveCurrentAgent = getSessionCurrentAgentResolver();
|
|
39
|
+
const storedAgentId = resolveCurrentAgent?.(targetSessionId);
|
|
40
|
+
const clientAgentId = params.agentId ?? client.agentId ?? "default";
|
|
41
|
+
let currentAgentId = params.agentId ?? storedAgentId ?? clientAgentId ?? "default";
|
|
42
|
+
if (params.agentId) {
|
|
43
|
+
getSessionCurrentAgentUpdater()?.(targetSessionId, params.agentId);
|
|
44
|
+
currentAgentId = params.agentId;
|
|
45
|
+
}
|
|
37
46
|
let workspace = "default";
|
|
38
47
|
let provider;
|
|
39
48
|
let modelId;
|
|
40
49
|
let apiKey;
|
|
41
|
-
const agentConfig = await loadDesktopAgentConfig(
|
|
50
|
+
const agentConfig = await loadDesktopAgentConfig(currentAgentId);
|
|
42
51
|
if (agentConfig) {
|
|
43
52
|
if (agentConfig.workspace)
|
|
44
53
|
workspace = agentConfig.workspace;
|
|
@@ -47,17 +56,16 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
|
|
|
47
56
|
if (agentConfig.apiKey)
|
|
48
57
|
apiKey = agentConfig.apiKey;
|
|
49
58
|
}
|
|
50
|
-
// system / scheduled:每次对话前先删再建,对话结束马上关闭,节省资源
|
|
51
59
|
const isEphemeralSession = sessionType === "system" || sessionType === "scheduled";
|
|
52
60
|
if (isEphemeralSession) {
|
|
53
|
-
agentManager.deleteSession(targetSessionId);
|
|
61
|
+
agentManager.deleteSession(targetSessionId + COMPOSITE_KEY_SEP + currentAgentId);
|
|
54
62
|
}
|
|
55
|
-
|
|
56
|
-
const effectiveTargetAgentId = sessionType === "system" ? targetAgentId : sessionAgentId;
|
|
63
|
+
const effectiveTargetAgentId = sessionType === "system" ? targetAgentId : currentAgentId;
|
|
57
64
|
const { maxAgentSessions } = getDesktopConfig();
|
|
58
65
|
let session;
|
|
59
66
|
try {
|
|
60
67
|
session = await agentManager.getOrCreateSession(targetSessionId, {
|
|
68
|
+
agentId: currentAgentId,
|
|
61
69
|
workspace,
|
|
62
70
|
provider,
|
|
63
71
|
modelId,
|
|
@@ -65,6 +73,7 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
|
|
|
65
73
|
maxSessions: maxAgentSessions,
|
|
66
74
|
targetAgentId: effectiveTargetAgentId,
|
|
67
75
|
mcpServers: agentConfig?.mcpServers,
|
|
76
|
+
systemPrompt: agentConfig?.systemPrompt,
|
|
68
77
|
});
|
|
69
78
|
}
|
|
70
79
|
catch (err) {
|
|
@@ -75,7 +84,7 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
|
|
|
75
84
|
}
|
|
76
85
|
throw err;
|
|
77
86
|
}
|
|
78
|
-
//
|
|
87
|
+
// 向各通道广播:turn_end(本小轮结束)、agent_end(整轮对话结束),并保留 message_complete / conversation_end 兼容。各端按需处理。
|
|
79
88
|
const unsubscribe = session.subscribe((event) => {
|
|
80
89
|
// console.log(`Agent event received: ${event.type}`); // Reduce noise
|
|
81
90
|
let wsMessage = null;
|
|
@@ -112,17 +121,26 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
|
|
|
112
121
|
const usagePayload = promptTokens > 0 || completionTokens > 0
|
|
113
122
|
? { promptTokens, completionTokens }
|
|
114
123
|
: undefined;
|
|
115
|
-
|
|
124
|
+
const turnPayload = {
|
|
116
125
|
sessionId: targetSessionId,
|
|
117
126
|
content: "",
|
|
118
127
|
...(usagePayload && { usage: usagePayload }),
|
|
119
|
-
}
|
|
128
|
+
};
|
|
129
|
+
broadcastToSession(targetSessionId, createEvent("turn_end", turnPayload));
|
|
130
|
+
broadcastToSession(targetSessionId, createEvent("message_complete", turnPayload));
|
|
131
|
+
wsMessage = null;
|
|
132
|
+
}
|
|
133
|
+
else if (event.type === "agent_end") {
|
|
134
|
+
const agentPayload = { sessionId: targetSessionId };
|
|
135
|
+
broadcastToSession(targetSessionId, createEvent("agent_end", agentPayload));
|
|
136
|
+
broadcastToSession(targetSessionId, createEvent("conversation_end", agentPayload));
|
|
137
|
+
wsMessage = null;
|
|
120
138
|
}
|
|
121
139
|
if (wsMessage) {
|
|
122
140
|
broadcastToSession(targetSessionId, wsMessage);
|
|
123
141
|
}
|
|
124
|
-
if (event.type === "
|
|
125
|
-
agentManager.deleteSession(targetSessionId);
|
|
142
|
+
if (event.type === "agent_end" && isEphemeralSession) {
|
|
143
|
+
agentManager.deleteSession(targetSessionId + COMPOSITE_KEY_SEP + currentAgentId);
|
|
126
144
|
}
|
|
127
145
|
});
|
|
128
146
|
try {
|