@emqo/claudebridge 0.8.0 → 0.9.1
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/discord.d.ts +3 -0
- package/dist/adapters/discord.js +92 -20
- package/dist/adapters/telegram.d.ts +3 -0
- package/dist/adapters/telegram.js +124 -59
- package/dist/core/agent.d.ts +37 -0
- package/dist/core/agent.js +246 -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 +5 -22
- package/dist/core/lock.d.ts +8 -4
- package/dist/core/lock.js +25 -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 +95 -17
- package/dist/ctl.js +13 -1
- package/dist/index.js +30 -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"
|
|
@@ -17,6 +17,8 @@ export declare class DiscordAdapter implements Adapter {
|
|
|
17
17
|
reloadConfig(config: DiscordConfig, locale: string): void;
|
|
18
18
|
private setup;
|
|
19
19
|
private handlePrompt;
|
|
20
|
+
/** Chunk text and send via edit + follow-up replies */
|
|
21
|
+
private sendChunkedResponse;
|
|
20
22
|
start(): Promise<void>;
|
|
21
23
|
stop(): void;
|
|
22
24
|
private checkReminders;
|
|
@@ -24,4 +26,5 @@ export declare class DiscordAdapter implements Adapter {
|
|
|
24
26
|
private runAutoTask;
|
|
25
27
|
private checkApprovals;
|
|
26
28
|
private handleStatusCommand;
|
|
29
|
+
private handleSessionsCommand;
|
|
27
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;
|
|
@@ -48,12 +50,17 @@ export class DiscordAdapter {
|
|
|
48
50
|
if (!this.engine.access.isAllowed(msg.author.id, groupId))
|
|
49
51
|
return;
|
|
50
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;
|
|
51
55
|
// Management commands
|
|
52
56
|
if (text === "!help") {
|
|
53
57
|
await msg.reply(t(this.locale, "help").replaceAll("/", "!"));
|
|
54
58
|
return;
|
|
55
59
|
}
|
|
56
60
|
if (text === "!new") {
|
|
61
|
+
if (this.engine.isMultiSessionEnabled()) {
|
|
62
|
+
this.engine.getSessionManager().closeAll(msg.author.id);
|
|
63
|
+
}
|
|
57
64
|
this.store.clearSession(msg.author.id);
|
|
58
65
|
await msg.reply(t(this.locale, "session_cleared"));
|
|
59
66
|
return;
|
|
@@ -129,6 +136,10 @@ export class DiscordAdapter {
|
|
|
129
136
|
await this.handleStatusCommand(msg);
|
|
130
137
|
return;
|
|
131
138
|
}
|
|
139
|
+
if (text === "!sessions") {
|
|
140
|
+
await this.handleSessionsCommand(msg);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
132
143
|
// File upload handling
|
|
133
144
|
if (msg.attachments.size > 0) {
|
|
134
145
|
const ws = this.engine.getWorkDir(msg.author.id);
|
|
@@ -142,16 +153,55 @@ export class DiscordAdapter {
|
|
|
142
153
|
}
|
|
143
154
|
const names = [...msg.attachments.values()].map(a => a.name).join(", ");
|
|
144
155
|
const prompt = text || `Analyze the uploaded file(s): ${names}`;
|
|
145
|
-
await this.handlePrompt(msg, prompt);
|
|
156
|
+
await this.handlePrompt(msg, prompt, replyToMsgId);
|
|
146
157
|
return;
|
|
147
158
|
}
|
|
148
159
|
// Text message — send to Claude (skill system handles intents)
|
|
149
160
|
if (!text)
|
|
150
161
|
return;
|
|
151
|
-
await this.handlePrompt(msg, text);
|
|
162
|
+
await this.handlePrompt(msg, text, replyToMsgId);
|
|
152
163
|
});
|
|
153
164
|
}
|
|
154
|
-
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
|
|
155
205
|
if (this.engine.isLocked(msg.author.id)) {
|
|
156
206
|
await msg.reply(t(this.locale, "still_processing"));
|
|
157
207
|
return;
|
|
@@ -174,30 +224,34 @@ export class DiscordAdapter {
|
|
|
174
224
|
}
|
|
175
225
|
catch { }
|
|
176
226
|
});
|
|
177
|
-
|
|
178
|
-
const chunks = chunkText(res.text, maxLen);
|
|
179
|
-
try {
|
|
180
|
-
await placeholder.edit(chunks[0]);
|
|
181
|
-
}
|
|
182
|
-
catch { }
|
|
183
|
-
for (let i = 1; i < chunks.length; i++) {
|
|
184
|
-
await msg.reply(chunks[i]);
|
|
185
|
-
}
|
|
227
|
+
await this.sendChunkedResponse(msg, placeholder, res.text);
|
|
186
228
|
}
|
|
187
229
|
catch (err) {
|
|
188
|
-
|
|
230
|
+
log.error("error", { error: err?.message });
|
|
189
231
|
try {
|
|
190
232
|
await placeholder.edit(`Error: ${err.message || "unknown"}`);
|
|
191
233
|
}
|
|
192
234
|
catch { }
|
|
193
235
|
}
|
|
194
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
|
+
}
|
|
195
249
|
async start() {
|
|
196
|
-
|
|
250
|
+
log.info("starting bot...");
|
|
197
251
|
await this.client.login(this.config.token);
|
|
198
|
-
|
|
252
|
+
log.info("logged in", { tag: this.client.user?.tag });
|
|
199
253
|
this.maxParallel = this.engine.getMaxParallel();
|
|
200
|
-
|
|
254
|
+
log.info("ready", { maxParallel: this.maxParallel, multiSession: this.engine.isMultiSessionEnabled() });
|
|
201
255
|
this.reminderTimer = setInterval(() => this.checkReminders(), 30000);
|
|
202
256
|
this.autoTimer = setInterval(() => this.processAutoTasks(), 60000);
|
|
203
257
|
this.approvalTimer = setInterval(() => this.checkApprovals(), 15000);
|
|
@@ -222,7 +276,7 @@ export class DiscordAdapter {
|
|
|
222
276
|
}
|
|
223
277
|
}
|
|
224
278
|
catch (e) {
|
|
225
|
-
|
|
279
|
+
log.error("reminder error", { error: e?.message });
|
|
226
280
|
}
|
|
227
281
|
}
|
|
228
282
|
async processAutoTasks() {
|
|
@@ -243,7 +297,7 @@ export class DiscordAdapter {
|
|
|
243
297
|
throw new Error("channel not found");
|
|
244
298
|
const channel = ch;
|
|
245
299
|
await channel.send(t(this.locale, "auto_starting", { id: task.id, desc: task.description }));
|
|
246
|
-
|
|
300
|
+
log.info("auto-task starting", { taskId: task.id, userId: task.user_id });
|
|
247
301
|
// Always use runParallel for auto-tasks: fresh session, no user session pollution
|
|
248
302
|
const res = await this.engine.runParallel(task.user_id, task.description, "discord", task.chat_id, undefined, 0);
|
|
249
303
|
if (res.timedOut) {
|
|
@@ -278,7 +332,7 @@ export class DiscordAdapter {
|
|
|
278
332
|
}
|
|
279
333
|
catch (err) {
|
|
280
334
|
this.store.markTaskResult(task.id, "failed");
|
|
281
|
-
|
|
335
|
+
log.error("auto-task failed", { taskId: task.id, error: err?.message });
|
|
282
336
|
// Self-healing: auto-retry failed tasks (max 3 retries)
|
|
283
337
|
const retryMatch = task.description.match(/\[retry (\d+)\/3\]/);
|
|
284
338
|
const retryCount = retryMatch ? parseInt(retryMatch[1]) : 0;
|
|
@@ -310,7 +364,7 @@ export class DiscordAdapter {
|
|
|
310
364
|
}
|
|
311
365
|
}
|
|
312
366
|
catch (e) {
|
|
313
|
-
|
|
367
|
+
log.error("approval check error", { error: e?.message });
|
|
314
368
|
}
|
|
315
369
|
}
|
|
316
370
|
async handleStatusCommand(msg) {
|
|
@@ -337,4 +391,22 @@ export class DiscordAdapter {
|
|
|
337
391
|
const report = `${t(this.locale, "status_report")}\n${lines.join("\n")}\n\nSummary: ${summary}`;
|
|
338
392
|
await msg.reply(report);
|
|
339
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
|
+
}
|
|
340
412
|
}
|
|
@@ -26,6 +26,8 @@ export declare class TelegramAdapter implements Adapter {
|
|
|
26
26
|
private pageKeyboard;
|
|
27
27
|
private handlePageCallback;
|
|
28
28
|
private handlePrompt;
|
|
29
|
+
/** Format and send a response with MarkdownV2 + pagination support */
|
|
30
|
+
private sendFormattedResponse;
|
|
29
31
|
start(): Promise<void>;
|
|
30
32
|
stop(): void;
|
|
31
33
|
private registerCommands;
|
|
@@ -35,4 +37,5 @@ export declare class TelegramAdapter implements Adapter {
|
|
|
35
37
|
private checkApprovals;
|
|
36
38
|
private handleApprovalCallback;
|
|
37
39
|
private handleStatusCommand;
|
|
40
|
+
private handleSessionsCommand;
|
|
38
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;
|
|
@@ -45,7 +47,7 @@ export class TelegramAdapter {
|
|
|
45
47
|
clearTimeout(timer);
|
|
46
48
|
const json = await res.json();
|
|
47
49
|
if (!json.ok) {
|
|
48
|
-
|
|
50
|
+
log.error("API error", { method, description: json.description });
|
|
49
51
|
const err = new Error(json.description || `Telegram API error: ${method}`);
|
|
50
52
|
err.apiError = true;
|
|
51
53
|
throw err;
|
|
@@ -59,11 +61,12 @@ export class TelegramAdapter {
|
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
|
-
async reply(chatId, text, parseMode) {
|
|
64
|
+
async reply(chatId, text, parseMode, replyToMsgId) {
|
|
63
65
|
return this.call("sendMessage", {
|
|
64
66
|
chat_id: chatId,
|
|
65
67
|
text,
|
|
66
68
|
...(parseMode ? { parse_mode: parseMode } : {}),
|
|
69
|
+
...(replyToMsgId ? { reply_to_message_id: replyToMsgId } : {}),
|
|
67
70
|
});
|
|
68
71
|
}
|
|
69
72
|
async editMsg(chatId, msgId, text, parseMode) {
|
|
@@ -97,17 +100,24 @@ export class TelegramAdapter {
|
|
|
97
100
|
return;
|
|
98
101
|
const groupId = msg.chat.type !== "private" ? String(chatId) : undefined;
|
|
99
102
|
if (!this.engine.access.isAllowed(String(uid), groupId)) {
|
|
100
|
-
|
|
103
|
+
log.info("user not allowed", { uid });
|
|
101
104
|
return;
|
|
102
105
|
}
|
|
103
106
|
const text = (msg.text || "").trim();
|
|
104
|
-
|
|
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;
|
|
105
112
|
// Management commands
|
|
106
113
|
if (text === "/start" || text === "/help") {
|
|
107
114
|
await this.reply(chatId, t(this.locale, "help"));
|
|
108
115
|
return;
|
|
109
116
|
}
|
|
110
117
|
if (text === "/new") {
|
|
118
|
+
if (this.engine.isMultiSessionEnabled()) {
|
|
119
|
+
this.engine.getSessionManager().closeAll(String(uid));
|
|
120
|
+
}
|
|
111
121
|
this.store.clearSession(String(uid));
|
|
112
122
|
await this.reply(chatId, t(this.locale, "session_cleared"));
|
|
113
123
|
return;
|
|
@@ -157,6 +167,10 @@ export class TelegramAdapter {
|
|
|
157
167
|
await this.handleStatusCommand(chatId, String(uid));
|
|
158
168
|
return;
|
|
159
169
|
}
|
|
170
|
+
if (text === "/sessions") {
|
|
171
|
+
await this.handleSessionsCommand(chatId, String(uid));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
160
174
|
// File upload
|
|
161
175
|
if (msg.document || msg.photo) {
|
|
162
176
|
let fileId;
|
|
@@ -180,7 +194,7 @@ export class TelegramAdapter {
|
|
|
180
194
|
const ws = this.engine.getWorkDir(String(uid));
|
|
181
195
|
writeFileSync(join(ws, fileName), buf);
|
|
182
196
|
const prompt = msg.caption || `Analyze the uploaded file: ${fileName}`;
|
|
183
|
-
await this.handlePrompt(chatId, String(uid), prompt);
|
|
197
|
+
await this.handlePrompt(chatId, String(uid), prompt, replyToMsgId);
|
|
184
198
|
}
|
|
185
199
|
catch (e) {
|
|
186
200
|
await this.reply(chatId, t(this.locale, "upload_failed") + e.message);
|
|
@@ -189,7 +203,7 @@ export class TelegramAdapter {
|
|
|
189
203
|
}
|
|
190
204
|
// Text message — send to Claude (skill system handles intents)
|
|
191
205
|
if (text)
|
|
192
|
-
await this.handlePrompt(chatId, String(uid), text);
|
|
206
|
+
await this.handlePrompt(chatId, String(uid), text, replyToMsgId);
|
|
193
207
|
}
|
|
194
208
|
pageKeyboard(chatId, msgId, cur, total) {
|
|
195
209
|
const btns = [];
|
|
@@ -256,7 +270,39 @@ export class TelegramAdapter {
|
|
|
256
270
|
}
|
|
257
271
|
await answer();
|
|
258
272
|
}
|
|
259
|
-
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)
|
|
260
306
|
if (this.engine.isLocked(uid)) {
|
|
261
307
|
await this.reply(chatId, t(this.locale, "still_processing"));
|
|
262
308
|
return;
|
|
@@ -265,7 +311,7 @@ export class TelegramAdapter {
|
|
|
265
311
|
const msgId = placeholder.message_id;
|
|
266
312
|
let lastEdit = 0;
|
|
267
313
|
try {
|
|
268
|
-
|
|
314
|
+
log.info("running claude", { uid });
|
|
269
315
|
const res = await this.engine.runStream(uid, text, "telegram", String(chatId), async (_chunk, full) => {
|
|
270
316
|
const now = Date.now();
|
|
271
317
|
if (now - lastEdit < EDIT_INTERVAL)
|
|
@@ -274,63 +320,64 @@ export class TelegramAdapter {
|
|
|
274
320
|
const preview = full.slice(-3500) + "\n\n...";
|
|
275
321
|
await this.editMsg(chatId, msgId, preview);
|
|
276
322
|
});
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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");
|
|
290
341
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
this.pages.
|
|
300
|
-
|
|
301
|
-
|
|
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 {
|
|
302
365
|
try {
|
|
303
366
|
await this.call("editMessageText", {
|
|
304
367
|
chat_id: chatId,
|
|
305
368
|
message_id: msgId,
|
|
306
|
-
text:
|
|
307
|
-
parse_mode: "MarkdownV2",
|
|
369
|
+
text: rawChunks[0],
|
|
308
370
|
reply_markup: keyboard,
|
|
309
371
|
});
|
|
310
372
|
}
|
|
311
|
-
catch {
|
|
312
|
-
// MarkdownV2 failed, fallback to raw text
|
|
313
|
-
try {
|
|
314
|
-
await this.call("editMessageText", {
|
|
315
|
-
chat_id: chatId,
|
|
316
|
-
message_id: msgId,
|
|
317
|
-
text: rawChunks[0],
|
|
318
|
-
reply_markup: keyboard,
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
catch { }
|
|
322
|
-
}
|
|
373
|
+
catch { }
|
|
323
374
|
}
|
|
324
375
|
}
|
|
325
|
-
catch (err) {
|
|
326
|
-
console.error("[telegram] claude error:", err);
|
|
327
|
-
await this.editMsg(chatId, msgId, `Error: ${err.message || "unknown"}`);
|
|
328
|
-
}
|
|
329
376
|
}
|
|
330
377
|
async start() {
|
|
331
378
|
this.running = true;
|
|
332
379
|
this.maxParallel = this.engine.getMaxParallel();
|
|
333
|
-
|
|
380
|
+
log.info("starting long polling...", { maxParallel: this.maxParallel, multiSession: this.engine.isMultiSessionEnabled() });
|
|
334
381
|
this.reminderTimer = setInterval(() => this.checkReminders(), 30000);
|
|
335
382
|
this.autoTimer = setInterval(() => this.processAutoTasks(), 60000);
|
|
336
383
|
this.approvalTimer = setInterval(() => this.checkApprovals(), 15000);
|
|
@@ -344,19 +391,19 @@ export class TelegramAdapter {
|
|
|
344
391
|
clearTimeout(timer);
|
|
345
392
|
const json = await res.json();
|
|
346
393
|
if (!json.ok) {
|
|
347
|
-
|
|
394
|
+
log.error("poll error", { response: json });
|
|
348
395
|
continue;
|
|
349
396
|
}
|
|
350
397
|
pollBackoff = 0; // reset on success
|
|
351
398
|
for (const update of json.result) {
|
|
352
399
|
this.offset = update.update_id + 1;
|
|
353
|
-
this.handleUpdate(update).catch(e =>
|
|
400
|
+
this.handleUpdate(update).catch(e => log.error("handler error", { error: e?.message }));
|
|
354
401
|
}
|
|
355
402
|
}
|
|
356
403
|
catch (err) {
|
|
357
404
|
pollBackoff = Math.min(pollBackoff + 1, 6);
|
|
358
405
|
const delay = Math.min(3000 * Math.pow(2, pollBackoff), 120000);
|
|
359
|
-
|
|
406
|
+
log.warn("poll error", { retryIn: delay / 1000, error: err.cause?.code || err.message || "unknown" });
|
|
360
407
|
await new Promise(r => setTimeout(r, delay));
|
|
361
408
|
}
|
|
362
409
|
}
|
|
@@ -373,10 +420,10 @@ export class TelegramAdapter {
|
|
|
373
420
|
async registerCommands() {
|
|
374
421
|
try {
|
|
375
422
|
await this.call("setMyCommands", { commands: getCommandDescriptions(this.locale) });
|
|
376
|
-
|
|
423
|
+
log.info("commands registered");
|
|
377
424
|
}
|
|
378
425
|
catch (e) {
|
|
379
|
-
|
|
426
|
+
log.error("failed to register commands", { error: e?.message });
|
|
380
427
|
}
|
|
381
428
|
}
|
|
382
429
|
async checkReminders() {
|
|
@@ -388,7 +435,7 @@ export class TelegramAdapter {
|
|
|
388
435
|
}
|
|
389
436
|
}
|
|
390
437
|
catch (e) {
|
|
391
|
-
|
|
438
|
+
log.error("reminder error", { error: e?.message });
|
|
392
439
|
}
|
|
393
440
|
}
|
|
394
441
|
async processAutoTasks() {
|
|
@@ -406,7 +453,7 @@ export class TelegramAdapter {
|
|
|
406
453
|
const chatId = Number(task.chat_id);
|
|
407
454
|
await this.reply(chatId, t(this.locale, "auto_starting", { id: task.id, desc: task.description }));
|
|
408
455
|
try {
|
|
409
|
-
|
|
456
|
+
log.info("auto-task starting", { taskId: task.id, userId: task.user_id });
|
|
410
457
|
// Always use runParallel for auto-tasks: fresh session, no user session pollution
|
|
411
458
|
const res = await this.engine.runParallel(task.user_id, task.description, "telegram", task.chat_id, undefined, 0);
|
|
412
459
|
if (res.timedOut) {
|
|
@@ -483,7 +530,7 @@ export class TelegramAdapter {
|
|
|
483
530
|
}
|
|
484
531
|
}
|
|
485
532
|
catch (e) {
|
|
486
|
-
|
|
533
|
+
log.error("approval check error", { error: e?.message });
|
|
487
534
|
}
|
|
488
535
|
}
|
|
489
536
|
async handleApprovalCallback(cb) {
|
|
@@ -560,4 +607,22 @@ export class TelegramAdapter {
|
|
|
560
607
|
const report = `${t(this.locale, "status_report")}\n${lines.join("\n")}\n\nSummary: ${summary}`;
|
|
561
608
|
await this.reply(chatId, report);
|
|
562
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
|
+
}
|
|
563
628
|
}
|
package/dist/core/agent.d.ts
CHANGED
|
@@ -2,11 +2,15 @@ import { Config } from "./config.js";
|
|
|
2
2
|
import { Store } from "./store.js";
|
|
3
3
|
import { AccessControl } from "./permissions.js";
|
|
4
4
|
import { EndpointRotator } from "./keys.js";
|
|
5
|
+
import { SessionManager } from "./session.js";
|
|
6
|
+
import { SessionRouter } from "./router.js";
|
|
5
7
|
export interface AgentResponse {
|
|
6
8
|
text: string;
|
|
7
9
|
sessionId: string;
|
|
8
10
|
cost?: number;
|
|
9
11
|
timedOut?: boolean;
|
|
12
|
+
subSessionId?: string;
|
|
13
|
+
label?: string;
|
|
10
14
|
}
|
|
11
15
|
export type StreamCallback = (chunk: string, full: string) => void | Promise<void>;
|
|
12
16
|
export declare class AgentEngine {
|
|
@@ -14,6 +18,9 @@ export declare class AgentEngine {
|
|
|
14
18
|
private store;
|
|
15
19
|
private lock;
|
|
16
20
|
private rotator;
|
|
21
|
+
private sessionMgr;
|
|
22
|
+
private router;
|
|
23
|
+
private sessionExpiryTimer?;
|
|
17
24
|
access: AccessControl;
|
|
18
25
|
constructor(config: Config, store: Store);
|
|
19
26
|
reloadConfig(config: Config): void;
|
|
@@ -24,12 +31,42 @@ export declare class AgentEngine {
|
|
|
24
31
|
getRotator(): EndpointRotator;
|
|
25
32
|
getEndpointCount(): number;
|
|
26
33
|
getMaxParallel(): number;
|
|
34
|
+
getSessionManager(): SessionManager;
|
|
35
|
+
getRouter(): SessionRouter;
|
|
27
36
|
getWorkDir(userId: string): string;
|
|
37
|
+
/** @deprecated Use isSessionLocked() for multi-session mode */
|
|
28
38
|
isLocked(userId: string): boolean;
|
|
39
|
+
isSessionLocked(subSessionId: string): boolean;
|
|
40
|
+
isMultiSessionEnabled(): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Main entry point for user messages in multi-session mode.
|
|
43
|
+
* Routes to the correct sub-session and executes concurrently.
|
|
44
|
+
*/
|
|
45
|
+
handleUserMessage(userId: string, prompt: string, platform: string, chatId: string, replyToMsgId?: string, onChunk?: StreamCallback, overrideTimeoutMs?: number): Promise<AgentResponse>;
|
|
46
|
+
/**
|
|
47
|
+
* Execute a prompt within a specific sub-session.
|
|
48
|
+
* Acquires per-session lock, resumes claude session via -r flag.
|
|
49
|
+
*/
|
|
50
|
+
private _executeSubSession;
|
|
51
|
+
/**
|
|
52
|
+
* Core execution: spawn claude CLI with session resume for a sub-session.
|
|
53
|
+
* Thin wrapper around _spawnAgent with sub-session persistence.
|
|
54
|
+
*/
|
|
55
|
+
private _executeWithSession;
|
|
56
|
+
/** Build the append system prompt (memories + skill doc) */
|
|
57
|
+
private _buildAppendPrompt;
|
|
58
|
+
/**
|
|
59
|
+
* Unified core: spawn provider CLI, parse stream, handle timeout.
|
|
60
|
+
* All execute methods delegate here.
|
|
61
|
+
*/
|
|
62
|
+
private _spawnAgent;
|
|
63
|
+
/** @deprecated Use handleUserMessage() for multi-session mode */
|
|
29
64
|
runStream(userId: string, prompt: string, platform: string, chatId: string, onChunk?: StreamCallback, overrideTimeoutMs?: number): Promise<AgentResponse>;
|
|
30
65
|
runParallel(userId: string, prompt: string, platform: string, chatId: string, onChunk?: StreamCallback, overrideTimeoutMs?: number): Promise<AgentResponse>;
|
|
31
66
|
private _executeWithRetry;
|
|
67
|
+
/** Legacy single-session execution. Thin wrapper around _spawnAgent with store persistence. */
|
|
32
68
|
private _execute;
|
|
69
|
+
/** Parallel execution without session resume. Thin wrapper around _spawnAgent. */
|
|
33
70
|
private _executeNoSession;
|
|
34
71
|
private _autoSummarize;
|
|
35
72
|
}
|