@emqo/claudebridge 0.7.0 → 0.9.0
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/config.yaml.example +12 -3
- package/dist/adapters/base.d.ts +1 -0
- package/dist/adapters/discord.d.ts +4 -0
- package/dist/adapters/discord.js +99 -23
- package/dist/adapters/telegram.d.ts +4 -0
- package/dist/adapters/telegram.js +138 -60
- package/dist/core/agent.d.ts +37 -0
- package/dist/core/agent.js +251 -232
- package/dist/core/config.d.ts +2 -70
- package/dist/core/config.js +9 -38
- package/dist/core/i18n.js +12 -4
- package/dist/core/keys.d.ts +2 -10
- package/dist/core/keys.js +7 -22
- package/dist/core/lock.d.ts +8 -4
- package/dist/core/lock.js +28 -17
- package/dist/core/logger.d.ts +11 -0
- package/dist/core/logger.js +24 -0
- package/dist/core/router.d.ts +25 -0
- package/dist/core/router.js +125 -0
- package/dist/core/schema.d.ts +166 -0
- package/dist/core/schema.js +85 -0
- package/dist/core/session.d.ts +50 -0
- package/dist/core/session.js +100 -0
- package/dist/core/store.d.ts +52 -15
- package/dist/core/store.js +105 -19
- package/dist/ctl.js +32 -30
- package/dist/index.js +42 -13
- package/dist/providers/base.d.ts +26 -0
- package/dist/providers/base.js +1 -0
- package/dist/providers/claude.d.ts +9 -0
- package/dist/providers/claude.js +53 -0
- package/dist/providers/codex.d.ts +9 -0
- package/dist/providers/codex.js +35 -0
- package/dist/providers/registry.d.ts +2 -0
- package/dist/providers/registry.js +12 -0
- package/dist/skills/bridge.d.ts +2 -0
- package/dist/skills/bridge.js +2 -0
- package/dist/webhook.js +7 -5
- package/package.json +8 -4
package/config.yaml.example
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# Leave endpoints empty to use claude CLI's own authentication
|
|
2
2
|
# 留空则直接使用 claude CLI 自身的认证配置
|
|
3
3
|
endpoints: []
|
|
4
|
-
# - name: "
|
|
5
|
-
#
|
|
6
|
-
# api_key: "sk-your-api-key"
|
|
4
|
+
# - name: "claude-main"
|
|
5
|
+
# provider: "claude"
|
|
7
6
|
# model: "claude-sonnet-4-20250514"
|
|
7
|
+
# - name: "codex"
|
|
8
|
+
# provider: "codex"
|
|
9
|
+
# model: "o3-mini"
|
|
8
10
|
|
|
9
11
|
locale: en # "zh" for Chinese
|
|
12
|
+
log_level: info # debug | info | warn | error
|
|
10
13
|
|
|
11
14
|
agent:
|
|
12
15
|
allowed_tools:
|
|
@@ -30,6 +33,12 @@ agent:
|
|
|
30
33
|
max_memories: 50
|
|
31
34
|
skill:
|
|
32
35
|
enabled: true
|
|
36
|
+
session:
|
|
37
|
+
enabled: true # Enable multi-session (concurrent conversations per user)
|
|
38
|
+
max_per_user: 3 # Max concurrent sub-sessions per user
|
|
39
|
+
idle_timeout_minutes: 30 # Auto-close idle sub-sessions after this time
|
|
40
|
+
classifier_budget: 0.05 # Max budget for routing classifier (Tier 3)
|
|
41
|
+
classifier_model: "" # Model for classifier (empty = use default)
|
|
33
42
|
|
|
34
43
|
workspace:
|
|
35
44
|
base_dir: "./workspaces"
|
package/dist/adapters/base.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface MessageContext {
|
|
|
7
7
|
export interface Adapter {
|
|
8
8
|
start(): Promise<void>;
|
|
9
9
|
stop(): void;
|
|
10
|
+
reloadConfig?(config: any, locale: string): void;
|
|
10
11
|
}
|
|
11
12
|
/** Split long text into chunks respecting newlines, with code-block-aware balancing */
|
|
12
13
|
export declare function chunkText(text: string, maxLen: number): string[];
|
|
@@ -14,8 +14,11 @@ export declare class DiscordAdapter implements Adapter {
|
|
|
14
14
|
private activeAutoTasks;
|
|
15
15
|
private maxParallel;
|
|
16
16
|
constructor(engine: AgentEngine, store: Store, config: DiscordConfig, locale?: string);
|
|
17
|
+
reloadConfig(config: DiscordConfig, locale: string): void;
|
|
17
18
|
private setup;
|
|
18
19
|
private handlePrompt;
|
|
20
|
+
/** Chunk text and send via edit + follow-up replies */
|
|
21
|
+
private sendChunkedResponse;
|
|
19
22
|
start(): Promise<void>;
|
|
20
23
|
stop(): void;
|
|
21
24
|
private checkReminders;
|
|
@@ -23,4 +26,5 @@ export declare class DiscordAdapter implements Adapter {
|
|
|
23
26
|
private runAutoTask;
|
|
24
27
|
private checkApprovals;
|
|
25
28
|
private handleStatusCommand;
|
|
29
|
+
private handleSessionsCommand;
|
|
26
30
|
}
|
package/dist/adapters/discord.js
CHANGED
|
@@ -4,6 +4,8 @@ import { join } from "path";
|
|
|
4
4
|
import { chunkText } from "./base.js";
|
|
5
5
|
import { reloadConfig } from "../core/config.js";
|
|
6
6
|
import { t } from "../core/i18n.js";
|
|
7
|
+
import { log as rootLog } from "../core/logger.js";
|
|
8
|
+
const log = rootLog.child("discord");
|
|
7
9
|
const EDIT_INTERVAL = 1500;
|
|
8
10
|
export class DiscordAdapter {
|
|
9
11
|
engine;
|
|
@@ -31,6 +33,11 @@ export class DiscordAdapter {
|
|
|
31
33
|
});
|
|
32
34
|
this.setup();
|
|
33
35
|
}
|
|
36
|
+
reloadConfig(config, locale) {
|
|
37
|
+
this.config = config;
|
|
38
|
+
this.locale = locale;
|
|
39
|
+
this.maxParallel = this.engine.getMaxParallel();
|
|
40
|
+
}
|
|
34
41
|
setup() {
|
|
35
42
|
this.client.on("messageCreate", async (msg) => {
|
|
36
43
|
if (msg.author.bot)
|
|
@@ -43,12 +50,17 @@ export class DiscordAdapter {
|
|
|
43
50
|
if (!this.engine.access.isAllowed(msg.author.id, groupId))
|
|
44
51
|
return;
|
|
45
52
|
const text = msg.content.replace(/<@!?\d+>/g, "").trim();
|
|
53
|
+
// Extract reply-to message ID for session routing
|
|
54
|
+
const replyToMsgId = msg.reference?.messageId || undefined;
|
|
46
55
|
// Management commands
|
|
47
56
|
if (text === "!help") {
|
|
48
57
|
await msg.reply(t(this.locale, "help").replaceAll("/", "!"));
|
|
49
58
|
return;
|
|
50
59
|
}
|
|
51
60
|
if (text === "!new") {
|
|
61
|
+
if (this.engine.isMultiSessionEnabled()) {
|
|
62
|
+
this.engine.getSessionManager().closeAll(msg.author.id);
|
|
63
|
+
}
|
|
52
64
|
this.store.clearSession(msg.author.id);
|
|
53
65
|
await msg.reply(t(this.locale, "session_cleared"));
|
|
54
66
|
return;
|
|
@@ -124,6 +136,10 @@ export class DiscordAdapter {
|
|
|
124
136
|
await this.handleStatusCommand(msg);
|
|
125
137
|
return;
|
|
126
138
|
}
|
|
139
|
+
if (text === "!sessions") {
|
|
140
|
+
await this.handleSessionsCommand(msg);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
127
143
|
// File upload handling
|
|
128
144
|
if (msg.attachments.size > 0) {
|
|
129
145
|
const ws = this.engine.getWorkDir(msg.author.id);
|
|
@@ -137,16 +153,55 @@ export class DiscordAdapter {
|
|
|
137
153
|
}
|
|
138
154
|
const names = [...msg.attachments.values()].map(a => a.name).join(", ");
|
|
139
155
|
const prompt = text || `Analyze the uploaded file(s): ${names}`;
|
|
140
|
-
await this.handlePrompt(msg, prompt);
|
|
156
|
+
await this.handlePrompt(msg, prompt, replyToMsgId);
|
|
141
157
|
return;
|
|
142
158
|
}
|
|
143
159
|
// Text message — send to Claude (skill system handles intents)
|
|
144
160
|
if (!text)
|
|
145
161
|
return;
|
|
146
|
-
await this.handlePrompt(msg, text);
|
|
162
|
+
await this.handlePrompt(msg, text, replyToMsgId);
|
|
147
163
|
});
|
|
148
164
|
}
|
|
149
|
-
async handlePrompt(msg, text) {
|
|
165
|
+
async handlePrompt(msg, text, replyToMsgId) {
|
|
166
|
+
// Multi-session mode: route and execute concurrently
|
|
167
|
+
if (this.engine.isMultiSessionEnabled()) {
|
|
168
|
+
const placeholder = await msg.reply(t(this.locale, "thinking"));
|
|
169
|
+
let lastEdit = 0;
|
|
170
|
+
let lastText = "";
|
|
171
|
+
try {
|
|
172
|
+
const res = await this.engine.handleUserMessage(msg.author.id, text, "discord", String(msg.channelId), replyToMsgId, async (_chunk, full) => {
|
|
173
|
+
const now = Date.now();
|
|
174
|
+
if (now - lastEdit < EDIT_INTERVAL)
|
|
175
|
+
return;
|
|
176
|
+
const preview = full.slice(-1900) + "\n\n...";
|
|
177
|
+
if (preview === lastText)
|
|
178
|
+
return;
|
|
179
|
+
lastText = preview;
|
|
180
|
+
lastEdit = now;
|
|
181
|
+
try {
|
|
182
|
+
await placeholder.edit(preview);
|
|
183
|
+
}
|
|
184
|
+
catch { }
|
|
185
|
+
});
|
|
186
|
+
// Track response message for reply-to routing
|
|
187
|
+
if (res.subSessionId) {
|
|
188
|
+
this.engine.getSessionManager().trackMessage(placeholder.id, String(msg.channelId), res.subSessionId);
|
|
189
|
+
}
|
|
190
|
+
// Add label prefix if multiple active sessions
|
|
191
|
+
const activeSessions = this.engine.getSessionManager().getActive(msg.author.id, "discord");
|
|
192
|
+
const labelPrefix = activeSessions.length > 1 && res.label ? `[${res.label.slice(0, 30)}]\n` : "";
|
|
193
|
+
await this.sendChunkedResponse(msg, placeholder, res.text, labelPrefix);
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
log.error("error", { error: err?.message });
|
|
197
|
+
try {
|
|
198
|
+
await placeholder.edit(`Error: ${err.message || "unknown"}`);
|
|
199
|
+
}
|
|
200
|
+
catch { }
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Legacy single-session mode
|
|
150
205
|
if (this.engine.isLocked(msg.author.id)) {
|
|
151
206
|
await msg.reply(t(this.locale, "still_processing"));
|
|
152
207
|
return;
|
|
@@ -169,30 +224,34 @@ export class DiscordAdapter {
|
|
|
169
224
|
}
|
|
170
225
|
catch { }
|
|
171
226
|
});
|
|
172
|
-
|
|
173
|
-
const chunks = chunkText(res.text, maxLen);
|
|
174
|
-
try {
|
|
175
|
-
await placeholder.edit(chunks[0]);
|
|
176
|
-
}
|
|
177
|
-
catch { }
|
|
178
|
-
for (let i = 1; i < chunks.length; i++) {
|
|
179
|
-
await msg.reply(chunks[i]);
|
|
180
|
-
}
|
|
227
|
+
await this.sendChunkedResponse(msg, placeholder, res.text);
|
|
181
228
|
}
|
|
182
229
|
catch (err) {
|
|
183
|
-
|
|
230
|
+
log.error("error", { error: err?.message });
|
|
184
231
|
try {
|
|
185
232
|
await placeholder.edit(`Error: ${err.message || "unknown"}`);
|
|
186
233
|
}
|
|
187
234
|
catch { }
|
|
188
235
|
}
|
|
189
236
|
}
|
|
237
|
+
/** Chunk text and send via edit + follow-up replies */
|
|
238
|
+
async sendChunkedResponse(msg, placeholder, text, labelPrefix = "") {
|
|
239
|
+
const maxLen = this.config.chunk_size || 1900;
|
|
240
|
+
const chunks = chunkText(labelPrefix + text, maxLen);
|
|
241
|
+
try {
|
|
242
|
+
await placeholder.edit(chunks[0]);
|
|
243
|
+
}
|
|
244
|
+
catch { }
|
|
245
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
246
|
+
await msg.reply(chunks[i]);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
190
249
|
async start() {
|
|
191
|
-
|
|
250
|
+
log.info("starting bot...");
|
|
192
251
|
await this.client.login(this.config.token);
|
|
193
|
-
|
|
252
|
+
log.info("logged in", { tag: this.client.user?.tag });
|
|
194
253
|
this.maxParallel = this.engine.getMaxParallel();
|
|
195
|
-
|
|
254
|
+
log.info("ready", { maxParallel: this.maxParallel, multiSession: this.engine.isMultiSessionEnabled() });
|
|
196
255
|
this.reminderTimer = setInterval(() => this.checkReminders(), 30000);
|
|
197
256
|
this.autoTimer = setInterval(() => this.processAutoTasks(), 60000);
|
|
198
257
|
this.approvalTimer = setInterval(() => this.checkApprovals(), 15000);
|
|
@@ -217,7 +276,7 @@ export class DiscordAdapter {
|
|
|
217
276
|
}
|
|
218
277
|
}
|
|
219
278
|
catch (e) {
|
|
220
|
-
|
|
279
|
+
log.error("reminder error", { error: e?.message });
|
|
221
280
|
}
|
|
222
281
|
}
|
|
223
282
|
async processAutoTasks() {
|
|
@@ -238,10 +297,9 @@ export class DiscordAdapter {
|
|
|
238
297
|
throw new Error("channel not found");
|
|
239
298
|
const channel = ch;
|
|
240
299
|
await channel.send(t(this.locale, "auto_starting", { id: task.id, desc: task.description }));
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
: await this.engine.runStream(task.user_id, task.description, "discord", task.chat_id, undefined, 0);
|
|
300
|
+
log.info("auto-task starting", { taskId: task.id, userId: task.user_id });
|
|
301
|
+
// Always use runParallel for auto-tasks: fresh session, no user session pollution
|
|
302
|
+
const res = await this.engine.runParallel(task.user_id, task.description, "discord", task.chat_id, undefined, 0);
|
|
245
303
|
if (res.timedOut) {
|
|
246
304
|
this.store.markTaskResult(task.id, "failed");
|
|
247
305
|
if (res.text)
|
|
@@ -274,7 +332,7 @@ export class DiscordAdapter {
|
|
|
274
332
|
}
|
|
275
333
|
catch (err) {
|
|
276
334
|
this.store.markTaskResult(task.id, "failed");
|
|
277
|
-
|
|
335
|
+
log.error("auto-task failed", { taskId: task.id, error: err?.message });
|
|
278
336
|
// Self-healing: auto-retry failed tasks (max 3 retries)
|
|
279
337
|
const retryMatch = task.description.match(/\[retry (\d+)\/3\]/);
|
|
280
338
|
const retryCount = retryMatch ? parseInt(retryMatch[1]) : 0;
|
|
@@ -306,7 +364,7 @@ export class DiscordAdapter {
|
|
|
306
364
|
}
|
|
307
365
|
}
|
|
308
366
|
catch (e) {
|
|
309
|
-
|
|
367
|
+
log.error("approval check error", { error: e?.message });
|
|
310
368
|
}
|
|
311
369
|
}
|
|
312
370
|
async handleStatusCommand(msg) {
|
|
@@ -333,4 +391,22 @@ export class DiscordAdapter {
|
|
|
333
391
|
const report = `${t(this.locale, "status_report")}\n${lines.join("\n")}\n\nSummary: ${summary}`;
|
|
334
392
|
await msg.reply(report);
|
|
335
393
|
}
|
|
394
|
+
async handleSessionsCommand(msg) {
|
|
395
|
+
if (!this.engine.isMultiSessionEnabled()) {
|
|
396
|
+
await msg.reply("Multi-session mode is disabled.");
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const sessions = this.engine.getSessionManager().getActive(msg.author.id, "discord");
|
|
400
|
+
if (!sessions.length) {
|
|
401
|
+
await msg.reply(t(this.locale, "no_sessions"));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const statusIcon = { active: "🟢", idle: "🟡", expired: "🔴", closed: "⚫" };
|
|
405
|
+
const lines = sessions.map(s => {
|
|
406
|
+
const ago = Math.round((Date.now() - s.lastActiveAt) / 60000);
|
|
407
|
+
const locked = this.engine.isSessionLocked(s.id) ? " [processing]" : "";
|
|
408
|
+
return `${statusIcon[s.status] || "⚪"} ${s.id.slice(0, 8)} "${s.label || "(no topic)"}" (${ago}min ago, ${s.messageCount} msgs, $${s.totalCost.toFixed(4)})${locked}`;
|
|
409
|
+
});
|
|
410
|
+
await msg.reply(`${t(this.locale, "sessions_list")}\n${lines.join("\n")}`);
|
|
411
|
+
}
|
|
336
412
|
}
|
|
@@ -17,6 +17,7 @@ export declare class TelegramAdapter implements Adapter {
|
|
|
17
17
|
private pages;
|
|
18
18
|
private static PAGE_TTL;
|
|
19
19
|
constructor(engine: AgentEngine, store: Store, config: TelegramConfig, locale?: string);
|
|
20
|
+
reloadConfig(config: TelegramConfig, locale: string): void;
|
|
20
21
|
private get api();
|
|
21
22
|
private call;
|
|
22
23
|
private reply;
|
|
@@ -25,6 +26,8 @@ export declare class TelegramAdapter implements Adapter {
|
|
|
25
26
|
private pageKeyboard;
|
|
26
27
|
private handlePageCallback;
|
|
27
28
|
private handlePrompt;
|
|
29
|
+
/** Format and send a response with MarkdownV2 + pagination support */
|
|
30
|
+
private sendFormattedResponse;
|
|
28
31
|
start(): Promise<void>;
|
|
29
32
|
stop(): void;
|
|
30
33
|
private registerCommands;
|
|
@@ -34,4 +37,5 @@ export declare class TelegramAdapter implements Adapter {
|
|
|
34
37
|
private checkApprovals;
|
|
35
38
|
private handleApprovalCallback;
|
|
36
39
|
private handleStatusCommand;
|
|
40
|
+
private handleSessionsCommand;
|
|
37
41
|
}
|
|
@@ -2,6 +2,8 @@ import { chunkText } from "./base.js";
|
|
|
2
2
|
import { reloadConfig } from "../core/config.js";
|
|
3
3
|
import { toTelegramMarkdown } from "../core/markdown.js";
|
|
4
4
|
import { t, getCommandDescriptions } from "../core/i18n.js";
|
|
5
|
+
import { log as rootLog } from "../core/logger.js";
|
|
6
|
+
const log = rootLog.child("telegram");
|
|
5
7
|
const EDIT_INTERVAL = 1500;
|
|
6
8
|
export class TelegramAdapter {
|
|
7
9
|
engine;
|
|
@@ -23,6 +25,11 @@ export class TelegramAdapter {
|
|
|
23
25
|
this.config = config;
|
|
24
26
|
this.locale = locale;
|
|
25
27
|
}
|
|
28
|
+
reloadConfig(config, locale) {
|
|
29
|
+
this.config = config;
|
|
30
|
+
this.locale = locale;
|
|
31
|
+
this.maxParallel = this.engine.getMaxParallel();
|
|
32
|
+
}
|
|
26
33
|
get api() {
|
|
27
34
|
return `https://api.telegram.org/bot${this.config.token}`;
|
|
28
35
|
}
|
|
@@ -40,7 +47,7 @@ export class TelegramAdapter {
|
|
|
40
47
|
clearTimeout(timer);
|
|
41
48
|
const json = await res.json();
|
|
42
49
|
if (!json.ok) {
|
|
43
|
-
|
|
50
|
+
log.error("API error", { method, description: json.description });
|
|
44
51
|
const err = new Error(json.description || `Telegram API error: ${method}`);
|
|
45
52
|
err.apiError = true;
|
|
46
53
|
throw err;
|
|
@@ -54,11 +61,12 @@ export class TelegramAdapter {
|
|
|
54
61
|
}
|
|
55
62
|
}
|
|
56
63
|
}
|
|
57
|
-
async reply(chatId, text, parseMode) {
|
|
64
|
+
async reply(chatId, text, parseMode, replyToMsgId) {
|
|
58
65
|
return this.call("sendMessage", {
|
|
59
66
|
chat_id: chatId,
|
|
60
67
|
text,
|
|
61
68
|
...(parseMode ? { parse_mode: parseMode } : {}),
|
|
69
|
+
...(replyToMsgId ? { reply_to_message_id: replyToMsgId } : {}),
|
|
62
70
|
});
|
|
63
71
|
}
|
|
64
72
|
async editMsg(chatId, msgId, text, parseMode) {
|
|
@@ -92,17 +100,24 @@ export class TelegramAdapter {
|
|
|
92
100
|
return;
|
|
93
101
|
const groupId = msg.chat.type !== "private" ? String(chatId) : undefined;
|
|
94
102
|
if (!this.engine.access.isAllowed(String(uid), groupId)) {
|
|
95
|
-
|
|
103
|
+
log.info("user not allowed", { uid });
|
|
96
104
|
return;
|
|
97
105
|
}
|
|
98
106
|
const text = (msg.text || "").trim();
|
|
99
|
-
|
|
107
|
+
log.debug("message", { uid, text: text.slice(0, 50) });
|
|
108
|
+
// Extract reply-to message ID for session routing
|
|
109
|
+
const replyToMsgId = msg.reply_to_message?.message_id
|
|
110
|
+
? String(msg.reply_to_message.message_id)
|
|
111
|
+
: undefined;
|
|
100
112
|
// Management commands
|
|
101
113
|
if (text === "/start" || text === "/help") {
|
|
102
114
|
await this.reply(chatId, t(this.locale, "help"));
|
|
103
115
|
return;
|
|
104
116
|
}
|
|
105
117
|
if (text === "/new") {
|
|
118
|
+
if (this.engine.isMultiSessionEnabled()) {
|
|
119
|
+
this.engine.getSessionManager().closeAll(String(uid));
|
|
120
|
+
}
|
|
106
121
|
this.store.clearSession(String(uid));
|
|
107
122
|
await this.reply(chatId, t(this.locale, "session_cleared"));
|
|
108
123
|
return;
|
|
@@ -152,6 +167,10 @@ export class TelegramAdapter {
|
|
|
152
167
|
await this.handleStatusCommand(chatId, String(uid));
|
|
153
168
|
return;
|
|
154
169
|
}
|
|
170
|
+
if (text === "/sessions") {
|
|
171
|
+
await this.handleSessionsCommand(chatId, String(uid));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
155
174
|
// File upload
|
|
156
175
|
if (msg.document || msg.photo) {
|
|
157
176
|
let fileId;
|
|
@@ -175,7 +194,7 @@ export class TelegramAdapter {
|
|
|
175
194
|
const ws = this.engine.getWorkDir(String(uid));
|
|
176
195
|
writeFileSync(join(ws, fileName), buf);
|
|
177
196
|
const prompt = msg.caption || `Analyze the uploaded file: ${fileName}`;
|
|
178
|
-
await this.handlePrompt(chatId, String(uid), prompt);
|
|
197
|
+
await this.handlePrompt(chatId, String(uid), prompt, replyToMsgId);
|
|
179
198
|
}
|
|
180
199
|
catch (e) {
|
|
181
200
|
await this.reply(chatId, t(this.locale, "upload_failed") + e.message);
|
|
@@ -184,7 +203,7 @@ export class TelegramAdapter {
|
|
|
184
203
|
}
|
|
185
204
|
// Text message — send to Claude (skill system handles intents)
|
|
186
205
|
if (text)
|
|
187
|
-
await this.handlePrompt(chatId, String(uid), text);
|
|
206
|
+
await this.handlePrompt(chatId, String(uid), text, replyToMsgId);
|
|
188
207
|
}
|
|
189
208
|
pageKeyboard(chatId, msgId, cur, total) {
|
|
190
209
|
const btns = [];
|
|
@@ -251,7 +270,39 @@ export class TelegramAdapter {
|
|
|
251
270
|
}
|
|
252
271
|
await answer();
|
|
253
272
|
}
|
|
254
|
-
async handlePrompt(chatId, uid, text) {
|
|
273
|
+
async handlePrompt(chatId, uid, text, replyToMsgId) {
|
|
274
|
+
// Multi-session mode: route and execute concurrently (no global lock check)
|
|
275
|
+
if (this.engine.isMultiSessionEnabled()) {
|
|
276
|
+
const placeholder = await this.reply(chatId, t(this.locale, "thinking"));
|
|
277
|
+
const msgId = placeholder.message_id;
|
|
278
|
+
let lastEdit = 0;
|
|
279
|
+
try {
|
|
280
|
+
log.info("running claude (multi-session)", { uid });
|
|
281
|
+
const res = await this.engine.handleUserMessage(uid, text, "telegram", String(chatId), replyToMsgId, async (_chunk, full) => {
|
|
282
|
+
const now = Date.now();
|
|
283
|
+
if (now - lastEdit < EDIT_INTERVAL)
|
|
284
|
+
return;
|
|
285
|
+
lastEdit = now;
|
|
286
|
+
const preview = full.slice(-3500) + "\n\n...";
|
|
287
|
+
await this.editMsg(chatId, msgId, preview);
|
|
288
|
+
});
|
|
289
|
+
log.info("claude done", { uid, session: res.subSessionId?.slice(0, 8), cost: res.cost?.toFixed(4) });
|
|
290
|
+
// Track response message → sub-session mapping for future reply-to routing
|
|
291
|
+
if (res.subSessionId) {
|
|
292
|
+
this.engine.getSessionManager().trackMessage(String(msgId), String(chatId), res.subSessionId);
|
|
293
|
+
}
|
|
294
|
+
// Check if user has multiple active sessions — add label prefix
|
|
295
|
+
const activeSessions = this.engine.getSessionManager().getActive(uid, "telegram");
|
|
296
|
+
const labelPrefix = activeSessions.length > 1 && res.label ? `[${res.label.slice(0, 30)}]\n` : "";
|
|
297
|
+
await this.sendFormattedResponse(chatId, msgId, res.text, labelPrefix);
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
log.error("claude error", { error: err?.message });
|
|
301
|
+
await this.editMsg(chatId, msgId, `Error: ${err.message || "unknown"}`);
|
|
302
|
+
}
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
// Legacy single-session mode (session.enabled: false)
|
|
255
306
|
if (this.engine.isLocked(uid)) {
|
|
256
307
|
await this.reply(chatId, t(this.locale, "still_processing"));
|
|
257
308
|
return;
|
|
@@ -260,7 +311,7 @@ export class TelegramAdapter {
|
|
|
260
311
|
const msgId = placeholder.message_id;
|
|
261
312
|
let lastEdit = 0;
|
|
262
313
|
try {
|
|
263
|
-
|
|
314
|
+
log.info("running claude", { uid });
|
|
264
315
|
const res = await this.engine.runStream(uid, text, "telegram", String(chatId), async (_chunk, full) => {
|
|
265
316
|
const now = Date.now();
|
|
266
317
|
if (now - lastEdit < EDIT_INTERVAL)
|
|
@@ -269,81 +320,91 @@ export class TelegramAdapter {
|
|
|
269
320
|
const preview = full.slice(-3500) + "\n\n...";
|
|
270
321
|
await this.editMsg(chatId, msgId, preview);
|
|
271
322
|
});
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
323
|
+
log.info("claude done", { uid, cost: res.cost?.toFixed(4) });
|
|
324
|
+
await this.sendFormattedResponse(chatId, msgId, res.text);
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
log.error("claude error", { error: err?.message });
|
|
328
|
+
await this.editMsg(chatId, msgId, `Error: ${err.message || "unknown"}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/** Format and send a response with MarkdownV2 + pagination support */
|
|
332
|
+
async sendFormattedResponse(chatId, msgId, text, labelPrefix = "") {
|
|
333
|
+
const maxLen = this.config.chunk_size || 4000;
|
|
334
|
+
const fullText = labelPrefix + text;
|
|
335
|
+
const md = toTelegramMarkdown(fullText);
|
|
336
|
+
const mdChunks = chunkText(md, maxLen);
|
|
337
|
+
const rawChunks = chunkText(fullText, maxLen);
|
|
338
|
+
if (mdChunks.length <= 1) {
|
|
339
|
+
try {
|
|
340
|
+
await this.editMsg(chatId, msgId, mdChunks[0], "MarkdownV2");
|
|
285
341
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
342
|
+
catch {
|
|
343
|
+
await this.editMsg(chatId, msgId, fullText);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
const key = `${chatId}:${msgId}`;
|
|
348
|
+
if (this.pages.size >= 50) {
|
|
349
|
+
const oldest = this.pages.keys().next().value;
|
|
350
|
+
this.pages.delete(oldest);
|
|
351
|
+
}
|
|
352
|
+
this.pages.set(key, { chunks: mdChunks, raw: rawChunks, ts: Date.now() });
|
|
353
|
+
setTimeout(() => this.pages.delete(key), TelegramAdapter.PAGE_TTL);
|
|
354
|
+
const keyboard = this.pageKeyboard(chatId, msgId, 0, mdChunks.length);
|
|
355
|
+
try {
|
|
356
|
+
await this.call("editMessageText", {
|
|
357
|
+
chat_id: chatId,
|
|
358
|
+
message_id: msgId,
|
|
359
|
+
text: mdChunks[0],
|
|
360
|
+
parse_mode: "MarkdownV2",
|
|
361
|
+
reply_markup: keyboard,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
292
365
|
try {
|
|
293
366
|
await this.call("editMessageText", {
|
|
294
367
|
chat_id: chatId,
|
|
295
368
|
message_id: msgId,
|
|
296
|
-
text:
|
|
297
|
-
parse_mode: "MarkdownV2",
|
|
369
|
+
text: rawChunks[0],
|
|
298
370
|
reply_markup: keyboard,
|
|
299
371
|
});
|
|
300
372
|
}
|
|
301
|
-
catch {
|
|
302
|
-
// MarkdownV2 failed, fallback to raw text
|
|
303
|
-
try {
|
|
304
|
-
await this.call("editMessageText", {
|
|
305
|
-
chat_id: chatId,
|
|
306
|
-
message_id: msgId,
|
|
307
|
-
text: rawChunks[0],
|
|
308
|
-
reply_markup: keyboard,
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
catch { }
|
|
312
|
-
}
|
|
373
|
+
catch { }
|
|
313
374
|
}
|
|
314
375
|
}
|
|
315
|
-
catch (err) {
|
|
316
|
-
console.error("[telegram] claude error:", err);
|
|
317
|
-
await this.editMsg(chatId, msgId, `Error: ${err.message || "unknown"}`);
|
|
318
|
-
}
|
|
319
376
|
}
|
|
320
377
|
async start() {
|
|
321
378
|
this.running = true;
|
|
322
379
|
this.maxParallel = this.engine.getMaxParallel();
|
|
323
|
-
|
|
380
|
+
log.info("starting long polling...", { maxParallel: this.maxParallel, multiSession: this.engine.isMultiSessionEnabled() });
|
|
324
381
|
this.reminderTimer = setInterval(() => this.checkReminders(), 30000);
|
|
325
382
|
this.autoTimer = setInterval(() => this.processAutoTasks(), 60000);
|
|
326
383
|
this.approvalTimer = setInterval(() => this.checkApprovals(), 15000);
|
|
327
384
|
await this.registerCommands();
|
|
385
|
+
let pollBackoff = 0;
|
|
328
386
|
while (this.running) {
|
|
329
387
|
try {
|
|
330
388
|
const ctrl = new AbortController();
|
|
331
|
-
const timer = setTimeout(() => ctrl.abort(),
|
|
332
|
-
const res = await fetch(`${this.api}/getUpdates?offset=${this.offset}&timeout=
|
|
389
|
+
const timer = setTimeout(() => ctrl.abort(), 30000);
|
|
390
|
+
const res = await fetch(`${this.api}/getUpdates?offset=${this.offset}&timeout=10`, { signal: ctrl.signal });
|
|
333
391
|
clearTimeout(timer);
|
|
334
392
|
const json = await res.json();
|
|
335
393
|
if (!json.ok) {
|
|
336
|
-
|
|
394
|
+
log.error("poll error", { response: json });
|
|
337
395
|
continue;
|
|
338
396
|
}
|
|
397
|
+
pollBackoff = 0; // reset on success
|
|
339
398
|
for (const update of json.result) {
|
|
340
399
|
this.offset = update.update_id + 1;
|
|
341
|
-
this.handleUpdate(update).catch(e =>
|
|
400
|
+
this.handleUpdate(update).catch(e => log.error("handler error", { error: e?.message }));
|
|
342
401
|
}
|
|
343
402
|
}
|
|
344
403
|
catch (err) {
|
|
345
|
-
|
|
346
|
-
|
|
404
|
+
pollBackoff = Math.min(pollBackoff + 1, 6);
|
|
405
|
+
const delay = Math.min(3000 * Math.pow(2, pollBackoff), 120000);
|
|
406
|
+
log.warn("poll error", { retryIn: delay / 1000, error: err.cause?.code || err.message || "unknown" });
|
|
407
|
+
await new Promise(r => setTimeout(r, delay));
|
|
347
408
|
}
|
|
348
409
|
}
|
|
349
410
|
}
|
|
@@ -359,10 +420,10 @@ export class TelegramAdapter {
|
|
|
359
420
|
async registerCommands() {
|
|
360
421
|
try {
|
|
361
422
|
await this.call("setMyCommands", { commands: getCommandDescriptions(this.locale) });
|
|
362
|
-
|
|
423
|
+
log.info("commands registered");
|
|
363
424
|
}
|
|
364
425
|
catch (e) {
|
|
365
|
-
|
|
426
|
+
log.error("failed to register commands", { error: e?.message });
|
|
366
427
|
}
|
|
367
428
|
}
|
|
368
429
|
async checkReminders() {
|
|
@@ -374,7 +435,7 @@ export class TelegramAdapter {
|
|
|
374
435
|
}
|
|
375
436
|
}
|
|
376
437
|
catch (e) {
|
|
377
|
-
|
|
438
|
+
log.error("reminder error", { error: e?.message });
|
|
378
439
|
}
|
|
379
440
|
}
|
|
380
441
|
async processAutoTasks() {
|
|
@@ -392,10 +453,9 @@ export class TelegramAdapter {
|
|
|
392
453
|
const chatId = Number(task.chat_id);
|
|
393
454
|
await this.reply(chatId, t(this.locale, "auto_starting", { id: task.id, desc: task.description }));
|
|
394
455
|
try {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
: await this.engine.runStream(task.user_id, task.description, "telegram", task.chat_id, undefined, 0);
|
|
456
|
+
log.info("auto-task starting", { taskId: task.id, userId: task.user_id });
|
|
457
|
+
// Always use runParallel for auto-tasks: fresh session, no user session pollution
|
|
458
|
+
const res = await this.engine.runParallel(task.user_id, task.description, "telegram", task.chat_id, undefined, 0);
|
|
399
459
|
if (res.timedOut) {
|
|
400
460
|
this.store.markTaskResult(task.id, "failed");
|
|
401
461
|
if (res.text)
|
|
@@ -470,7 +530,7 @@ export class TelegramAdapter {
|
|
|
470
530
|
}
|
|
471
531
|
}
|
|
472
532
|
catch (e) {
|
|
473
|
-
|
|
533
|
+
log.error("approval check error", { error: e?.message });
|
|
474
534
|
}
|
|
475
535
|
}
|
|
476
536
|
async handleApprovalCallback(cb) {
|
|
@@ -547,4 +607,22 @@ export class TelegramAdapter {
|
|
|
547
607
|
const report = `${t(this.locale, "status_report")}\n${lines.join("\n")}\n\nSummary: ${summary}`;
|
|
548
608
|
await this.reply(chatId, report);
|
|
549
609
|
}
|
|
610
|
+
async handleSessionsCommand(chatId, userId) {
|
|
611
|
+
if (!this.engine.isMultiSessionEnabled()) {
|
|
612
|
+
await this.reply(chatId, "Multi-session mode is disabled.");
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const sessions = this.engine.getSessionManager().getActive(userId, "telegram");
|
|
616
|
+
if (!sessions.length) {
|
|
617
|
+
await this.reply(chatId, t(this.locale, "no_sessions"));
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const statusIcon = { active: "🟢", idle: "🟡", expired: "🔴", closed: "⚫" };
|
|
621
|
+
const lines = sessions.map(s => {
|
|
622
|
+
const ago = Math.round((Date.now() - s.lastActiveAt) / 60000);
|
|
623
|
+
const locked = this.engine.isSessionLocked(s.id) ? " [processing]" : "";
|
|
624
|
+
return `${statusIcon[s.status] || "⚪"} ${s.id.slice(0, 8)} "${s.label || "(no topic)"}" (${ago}min ago, ${s.messageCount} msgs, $${s.totalCost.toFixed(4)})${locked}`;
|
|
625
|
+
});
|
|
626
|
+
await this.reply(chatId, `${t(this.locale, "sessions_list")}\n${lines.join("\n")}`);
|
|
627
|
+
}
|
|
550
628
|
}
|