@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,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 钉钉通道适配器:使用 dingtalk-stream SDK Stream 模式接收机器人消息,通过 sessionWebhook 回传回复。
|
|
3
|
+
*/
|
|
4
|
+
import { DWClient, EventAck, TOPIC_ROBOT } from "dingtalk-stream";
|
|
5
|
+
import { dispatchMessage } from "../registry.js";
|
|
6
|
+
/** conversationId -> sessionWebhook,用于回复时 POST;收到新消息时更新 */
|
|
7
|
+
const sessionWebhookByConversation = new Map();
|
|
8
|
+
/**
|
|
9
|
+
* 钉钉 Stream 入站:DWClient + registerCallbackListener(TOPIC_ROBOT),解析 RobotMessage 转 UnifiedMessage。
|
|
10
|
+
* 回复发送完成后需调用 ack(socketCallBackResponse)避免钉钉重试。
|
|
11
|
+
*/
|
|
12
|
+
class DingTalkStreamInbound {
|
|
13
|
+
client;
|
|
14
|
+
messageHandler = null;
|
|
15
|
+
constructor(client) {
|
|
16
|
+
this.client = client;
|
|
17
|
+
}
|
|
18
|
+
setMessageHandler(handler) {
|
|
19
|
+
this.messageHandler = handler;
|
|
20
|
+
}
|
|
21
|
+
async start() {
|
|
22
|
+
this.client.registerCallbackListener(TOPIC_ROBOT, async (res) => {
|
|
23
|
+
try {
|
|
24
|
+
const data = JSON.parse(res.data);
|
|
25
|
+
const conversationId = data.conversationId;
|
|
26
|
+
const sessionWebhook = data.sessionWebhook;
|
|
27
|
+
if (sessionWebhook) {
|
|
28
|
+
sessionWebhookByConversation.set(conversationId, sessionWebhook);
|
|
29
|
+
}
|
|
30
|
+
const textContent = data.msgtype === "text" && data.text?.content ? data.text.content.trim() : "";
|
|
31
|
+
if (!textContent)
|
|
32
|
+
return;
|
|
33
|
+
const messageId = res.headers?.messageId ?? "";
|
|
34
|
+
const unified = {
|
|
35
|
+
channelId: "dingtalk",
|
|
36
|
+
threadId: conversationId,
|
|
37
|
+
userId: data.senderStaffId ?? data.senderId ?? "unknown",
|
|
38
|
+
userName: data.senderNick,
|
|
39
|
+
messageText: textContent,
|
|
40
|
+
replyTarget: "default",
|
|
41
|
+
messageId,
|
|
42
|
+
raw: data,
|
|
43
|
+
ack: (sendResult) => {
|
|
44
|
+
if (messageId) {
|
|
45
|
+
this.client.socketCallBackResponse(messageId, sendResult ?? {});
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
if (this.messageHandler) {
|
|
50
|
+
await this.messageHandler(unified);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
await dispatchMessage(unified);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
console.error("[DingTalk] onBotMessage error:", e);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
this.client.registerAllEventListener(() => ({ status: EventAck.SUCCESS }));
|
|
61
|
+
await this.client.connect();
|
|
62
|
+
console.log("[DingTalk] Stream client connected");
|
|
63
|
+
}
|
|
64
|
+
async stop() {
|
|
65
|
+
try {
|
|
66
|
+
this.client.disconnect();
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
console.warn("[DingTalk] disconnect error", e);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 钉钉出站:通过 sessionWebhook POST 发送文本回复(需 access_token)。
|
|
75
|
+
* 支持 sendStream:按 agent 每轮(turn_end)发一条消息,不拆解最终回答。
|
|
76
|
+
*/
|
|
77
|
+
class DingTalkWebhookOutbound {
|
|
78
|
+
client;
|
|
79
|
+
constructor(client) {
|
|
80
|
+
this.client = client;
|
|
81
|
+
}
|
|
82
|
+
async postOne(sessionWebhook, text) {
|
|
83
|
+
const accessToken = await this.client.getAccessToken();
|
|
84
|
+
const body = JSON.stringify({
|
|
85
|
+
msgtype: "text",
|
|
86
|
+
text: { content: text },
|
|
87
|
+
});
|
|
88
|
+
const res = await fetch(sessionWebhook, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: {
|
|
91
|
+
"Content-Type": "application/json",
|
|
92
|
+
"x-acs-dingtalk-access-token": accessToken,
|
|
93
|
+
},
|
|
94
|
+
body,
|
|
95
|
+
});
|
|
96
|
+
return res.json().catch(() => ({}));
|
|
97
|
+
}
|
|
98
|
+
async send(targetId, reply) {
|
|
99
|
+
const sessionWebhook = sessionWebhookByConversation.get(targetId);
|
|
100
|
+
if (!sessionWebhook) {
|
|
101
|
+
console.error("[DingTalk] send skipped: no sessionWebhook for conversationId", targetId);
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const text = reply.text?.trim() || "(无内容)";
|
|
105
|
+
try {
|
|
106
|
+
return await this.postOne(sessionWebhook, text);
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
console.error("[DingTalk] send message failed:", e);
|
|
110
|
+
throw e;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async sendStream(targetId) {
|
|
114
|
+
const sessionWebhook = sessionWebhookByConversation.get(targetId);
|
|
115
|
+
if (!sessionWebhook) {
|
|
116
|
+
throw new Error("[DingTalk] sendStream: no sessionWebhook for conversationId " + targetId);
|
|
117
|
+
}
|
|
118
|
+
/** 已发送的字符右边界,[lastSentIndex, accumulated.length) 为未发送 */
|
|
119
|
+
let lastSentIndex = 0;
|
|
120
|
+
/** 串行锁:onTurnEnd 与 onDone 可能不 await,必须顺序执行发送,避免重复发同一段 */
|
|
121
|
+
let sendLock = Promise.resolve();
|
|
122
|
+
const sendOne = (text) => {
|
|
123
|
+
if (!text.trim())
|
|
124
|
+
return Promise.resolve();
|
|
125
|
+
return this.postOne(sessionWebhook, text.trim()).then(() => { }, (e) => {
|
|
126
|
+
console.error("[DingTalk] sendStream post failed:", e);
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
/** 仅发送尚未发送的区间,并推进 lastSentIndex;在锁内执行 */
|
|
130
|
+
const flushPending = (accumulated) => {
|
|
131
|
+
const pending = accumulated.slice(lastSentIndex).trim();
|
|
132
|
+
if (!pending)
|
|
133
|
+
return Promise.resolve();
|
|
134
|
+
return sendOne(pending).then(() => {
|
|
135
|
+
lastSentIndex = accumulated.length;
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
return {
|
|
139
|
+
onChunk: async () => {
|
|
140
|
+
// 钉钉按轮发,不按 chunk 发
|
|
141
|
+
},
|
|
142
|
+
onTurnEnd: async (accumulated) => {
|
|
143
|
+
sendLock = sendLock.then(() => flushPending(accumulated));
|
|
144
|
+
await sendLock;
|
|
145
|
+
},
|
|
146
|
+
onDone: async (accumulated) => {
|
|
147
|
+
const final = accumulated.trim() || "(无内容)";
|
|
148
|
+
sendLock = sendLock.then(() => {
|
|
149
|
+
const remaining = final.slice(lastSentIndex).trim();
|
|
150
|
+
if (remaining) {
|
|
151
|
+
return sendOne(remaining).then(() => {
|
|
152
|
+
lastSentIndex = final.length;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
if (lastSentIndex === 0) {
|
|
156
|
+
return sendOne(final).then(() => {
|
|
157
|
+
lastSentIndex = final.length;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return Promise.resolve();
|
|
161
|
+
});
|
|
162
|
+
await sendLock;
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* 钉钉通道:Stream 入站 + Webhook 出站,共用一个 DWClient。
|
|
169
|
+
*/
|
|
170
|
+
export function createDingTalkChannel(config) {
|
|
171
|
+
const { clientId, clientSecret } = config;
|
|
172
|
+
if (!clientId?.trim() || !clientSecret?.trim()) {
|
|
173
|
+
throw new Error("[DingTalk] clientId and clientSecret are required");
|
|
174
|
+
}
|
|
175
|
+
const client = new DWClient({
|
|
176
|
+
clientId: clientId.trim(),
|
|
177
|
+
clientSecret: clientSecret.trim(),
|
|
178
|
+
debug: false,
|
|
179
|
+
});
|
|
180
|
+
const inbound = new DingTalkStreamInbound(client);
|
|
181
|
+
const outbound = new DingTalkWebhookOutbound(client);
|
|
182
|
+
inbound.setMessageHandler((msg) => dispatchMessage(msg));
|
|
183
|
+
return {
|
|
184
|
+
id: "dingtalk",
|
|
185
|
+
name: "钉钉",
|
|
186
|
+
defaultAgentId: config.defaultAgentId ?? "default",
|
|
187
|
+
getInbounds: () => [inbound],
|
|
188
|
+
getOutbounds: () => [outbound],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { IChannel } from "../types.js";
|
|
2
|
+
export interface FeishuChannelConfig {
|
|
3
|
+
appId: string;
|
|
4
|
+
appSecret: string;
|
|
5
|
+
/** 默认绑定的 agentId */
|
|
6
|
+
defaultAgentId?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 飞书通道:WebSocket 入站 + API 出站,使用官方 Node SDK。
|
|
10
|
+
*/
|
|
11
|
+
export declare function createFeishuChannel(config: FeishuChannelConfig): IChannel;
|
|
@@ -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,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram 通道适配器:长轮询(getUpdates)接收消息,sendMessage / editMessageText 发送与流式更新。
|
|
3
|
+
* 桌面端无公网 URL 时用 long polling 是官方推荐方式;支持流式输出(先发占位再 editMessageText 更新)。
|
|
4
|
+
*/
|
|
5
|
+
import type { IChannel } from "../types.js";
|
|
6
|
+
export interface TelegramChannelConfig {
|
|
7
|
+
botToken: string;
|
|
8
|
+
/** 默认绑定的 agentId */
|
|
9
|
+
defaultAgentId?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Telegram 通道:长轮询入站 + API 出站,支持流式(editMessageText)。
|
|
13
|
+
*/
|
|
14
|
+
export declare function createTelegramChannel(config: TelegramChannelConfig): IChannel;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { dispatchMessage } from "../registry.js";
|
|
2
|
+
const TELEGRAM_API_BASE = "https://api.telegram.org/bot";
|
|
3
|
+
/** getUpdates 长轮询等待秒数(1–50) */
|
|
4
|
+
const LONG_POLL_TIMEOUT = 25;
|
|
5
|
+
/** 单条消息/编辑最大长度 */
|
|
6
|
+
const MAX_MESSAGE_LENGTH = 4096;
|
|
7
|
+
function apiUrl(token, method) {
|
|
8
|
+
return `${TELEGRAM_API_BASE}${encodeURIComponent(token)}/${method}`;
|
|
9
|
+
}
|
|
10
|
+
async function telegramGet(token, method, params = {}) {
|
|
11
|
+
const q = new URLSearchParams();
|
|
12
|
+
for (const [k, v] of Object.entries(params))
|
|
13
|
+
q.set(k, String(v));
|
|
14
|
+
const url = `${apiUrl(token, method)}?${q.toString()}`;
|
|
15
|
+
const res = await fetch(url, { method: "GET" });
|
|
16
|
+
return res.json();
|
|
17
|
+
}
|
|
18
|
+
async function telegramPost(token, method, body) {
|
|
19
|
+
const res = await fetch(apiUrl(token, method), {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: { "Content-Type": "application/json" },
|
|
22
|
+
body: JSON.stringify(body),
|
|
23
|
+
});
|
|
24
|
+
return res.json();
|
|
25
|
+
}
|
|
26
|
+
function truncateForTelegram(text) {
|
|
27
|
+
if (text.length <= MAX_MESSAGE_LENGTH)
|
|
28
|
+
return text;
|
|
29
|
+
return text.slice(0, MAX_MESSAGE_LENGTH - 3) + "...";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 入站:长轮询 getUpdates,收到 message/edited_message 后转 UnifiedMessage 并分发。
|
|
33
|
+
*/
|
|
34
|
+
class TelegramLongPollInbound {
|
|
35
|
+
config;
|
|
36
|
+
messageHandler = null;
|
|
37
|
+
stopped = false;
|
|
38
|
+
lastOffset = 0;
|
|
39
|
+
constructor(config) {
|
|
40
|
+
this.config = config;
|
|
41
|
+
}
|
|
42
|
+
setMessageHandler(handler) {
|
|
43
|
+
this.messageHandler = handler;
|
|
44
|
+
}
|
|
45
|
+
async start() {
|
|
46
|
+
const token = this.config.botToken?.trim();
|
|
47
|
+
if (!token) {
|
|
48
|
+
console.warn("[Telegram] botToken missing, skip long poll start");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.stopped = false;
|
|
52
|
+
this.lastOffset = 0;
|
|
53
|
+
const poll = async () => {
|
|
54
|
+
while (!this.stopped) {
|
|
55
|
+
try {
|
|
56
|
+
const resp = await telegramGet(token, "getUpdates", {
|
|
57
|
+
offset: this.lastOffset,
|
|
58
|
+
timeout: LONG_POLL_TIMEOUT,
|
|
59
|
+
});
|
|
60
|
+
if (!resp.ok || !Array.isArray(resp.result)) {
|
|
61
|
+
if (resp.description)
|
|
62
|
+
console.warn("[Telegram] getUpdates error:", resp.description);
|
|
63
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
for (const update of resp.result) {
|
|
67
|
+
if (update.update_id >= this.lastOffset)
|
|
68
|
+
this.lastOffset = update.update_id + 1;
|
|
69
|
+
const msg = update.message ?? update.edited_message;
|
|
70
|
+
if (!msg?.text?.trim() || !msg.chat?.id)
|
|
71
|
+
continue;
|
|
72
|
+
const from = msg.from;
|
|
73
|
+
const userId = from?.id != null ? String(from.id) : "unknown";
|
|
74
|
+
const userName = [from?.first_name, from?.last_name].filter(Boolean).join(" ").trim() || from?.username;
|
|
75
|
+
const unified = {
|
|
76
|
+
channelId: "telegram",
|
|
77
|
+
threadId: String(msg.chat.id),
|
|
78
|
+
userId,
|
|
79
|
+
userName: userName || undefined,
|
|
80
|
+
messageText: msg.text.trim(),
|
|
81
|
+
replyTarget: "default",
|
|
82
|
+
messageId: String(msg.message_id),
|
|
83
|
+
raw: msg,
|
|
84
|
+
};
|
|
85
|
+
if (this.messageHandler) {
|
|
86
|
+
await this.messageHandler(unified);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
await dispatchMessage(unified);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
if (!this.stopped)
|
|
95
|
+
console.error("[Telegram] long poll error:", e);
|
|
96
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
void poll();
|
|
101
|
+
console.log("[Telegram] long poll started");
|
|
102
|
+
}
|
|
103
|
+
async stop() {
|
|
104
|
+
this.stopped = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 出站:sendMessage 一次性发送;sendStream 先发占位再 editMessageText 流式更新(真流式)。
|
|
109
|
+
*/
|
|
110
|
+
class TelegramApiOutbound {
|
|
111
|
+
config;
|
|
112
|
+
constructor(config) {
|
|
113
|
+
this.config = config;
|
|
114
|
+
}
|
|
115
|
+
getToken() {
|
|
116
|
+
const t = this.config.botToken?.trim();
|
|
117
|
+
if (!t)
|
|
118
|
+
throw new Error("[Telegram] botToken required");
|
|
119
|
+
return t;
|
|
120
|
+
}
|
|
121
|
+
async send(targetId, reply) {
|
|
122
|
+
const text = reply.text?.trim() || "(无内容)";
|
|
123
|
+
const resp = await telegramPost(this.getToken(), "sendMessage", {
|
|
124
|
+
chat_id: targetId,
|
|
125
|
+
text: truncateForTelegram(text),
|
|
126
|
+
});
|
|
127
|
+
if (!resp.ok)
|
|
128
|
+
throw new Error(resp.description ?? "Telegram sendMessage failed");
|
|
129
|
+
return resp.result;
|
|
130
|
+
}
|
|
131
|
+
async sendStream(targetId) {
|
|
132
|
+
const token = this.getToken();
|
|
133
|
+
const placeholder = "…";
|
|
134
|
+
const createResp = await telegramPost(token, "sendMessage", {
|
|
135
|
+
chat_id: targetId,
|
|
136
|
+
text: placeholder,
|
|
137
|
+
});
|
|
138
|
+
if (!createResp.ok || createResp.result?.message_id == null) {
|
|
139
|
+
throw new Error(createResp.description ?? "Telegram sendMessage (stream placeholder) failed");
|
|
140
|
+
}
|
|
141
|
+
const messageId = createResp.result.message_id;
|
|
142
|
+
const chatId = targetId;
|
|
143
|
+
const edit = async (content) => {
|
|
144
|
+
const body = truncateForTelegram(content || " ");
|
|
145
|
+
const r = await telegramPost(token, "editMessageText", {
|
|
146
|
+
chat_id: chatId,
|
|
147
|
+
message_id: messageId,
|
|
148
|
+
text: body,
|
|
149
|
+
});
|
|
150
|
+
if (!r.ok && r.description?.includes("message is not modified"))
|
|
151
|
+
return;
|
|
152
|
+
if (!r.ok)
|
|
153
|
+
console.error("[Telegram] editMessageText failed:", r.description);
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
onChunk: async (accumulated) => {
|
|
157
|
+
try {
|
|
158
|
+
await edit(accumulated + " ▌");
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
console.error("[Telegram] stream edit failed:", e);
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
onTurnEnd: async (accumulated) => {
|
|
165
|
+
try {
|
|
166
|
+
await edit(accumulated || " ");
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
console.error("[Telegram] stream onTurnEnd edit failed:", e);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
onDone: async (accumulated) => {
|
|
173
|
+
try {
|
|
174
|
+
await edit(accumulated.trim() || "(无内容)");
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
console.error("[Telegram] stream onDone edit failed:", e);
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Telegram 通道:长轮询入站 + API 出站,支持流式(editMessageText)。
|
|
185
|
+
*/
|
|
186
|
+
export function createTelegramChannel(config) {
|
|
187
|
+
const inbound = new TelegramLongPollInbound(config);
|
|
188
|
+
const outbound = new TelegramApiOutbound(config);
|
|
189
|
+
inbound.setMessageHandler((msg) => dispatchMessage(msg));
|
|
190
|
+
return {
|
|
191
|
+
id: "telegram",
|
|
192
|
+
name: "Telegram",
|
|
193
|
+
defaultAgentId: config.defaultAgentId ?? "default",
|
|
194
|
+
getInbounds: () => [inbound],
|
|
195
|
+
getOutbounds: () => [outbound],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -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>;
|