@emqo/claudebridge 0.9.1 → 0.10.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/README.md +35 -9
- package/config.yaml.example +1 -1
- package/dist/adapters/discord.d.ts +2 -0
- package/dist/adapters/discord.js +32 -1
- package/dist/adapters/telegram.d.ts +2 -0
- package/dist/adapters/telegram.js +45 -0
- package/dist/core/agent.d.ts +4 -3
- package/dist/core/agent.js +50 -7
- package/dist/core/router.d.ts +13 -9
- package/dist/core/router.js +46 -24
- package/dist/core/schema.d.ts +3 -3
- package/dist/core/schema.js +1 -1
- package/dist/core/session.d.ts +10 -0
- package/dist/core/session.js +11 -0
- package/dist/core/store.d.ts +18 -0
- package/dist/core/store.js +36 -1
- package/dist/ctl.js +19 -1
- package/dist/skills/bridge.js +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,14 @@ Instead of hardcoded commands, ClaudeBridge injects a **skill document** into Cl
|
|
|
38
38
|
- **Parallel Execution**: Multiple `claude` instances running simultaneously (`max_parallel` config)
|
|
39
39
|
- **Observability**: `/status` command shows task queue, chain progress, and execution stats
|
|
40
40
|
|
|
41
|
+
### v0.10.0: Dispatcher Architecture (Master-Worker Sessions)
|
|
42
|
+
|
|
43
|
+
- **Dispatcher (Master Session)**: Every user has a single dispatcher that receives all messages and routes them to the correct sub-session. Users never interact with sub-sessions directly
|
|
44
|
+
- **Intelligent Routing**: Fast path ($0) for 0-1 active sessions; Claude-powered classification with user memories + session summaries for 2+ sessions
|
|
45
|
+
- **Session Summaries**: Each sub-session maintains an auto-generated summary, giving the dispatcher context about what each conversation is doing
|
|
46
|
+
- **Memory-Aware Dispatch**: Dispatcher sees user memories + all active session summaries when classifying, enabling accurate routing even for ambiguous messages
|
|
47
|
+
- **Concurrent Sub-Sessions**: Multiple sub-sessions execute in parallel with per-session locks
|
|
48
|
+
|
|
41
49
|
## Quick Start
|
|
42
50
|
|
|
43
51
|
### Global Install (npm)
|
|
@@ -91,6 +99,11 @@ agent:
|
|
|
91
99
|
max_memories: 50
|
|
92
100
|
skill:
|
|
93
101
|
enabled: true
|
|
102
|
+
session:
|
|
103
|
+
enabled: true
|
|
104
|
+
max_per_user: 3
|
|
105
|
+
idle_timeout_minutes: 30
|
|
106
|
+
dispatcher_budget: 0.05
|
|
94
107
|
|
|
95
108
|
workspace:
|
|
96
109
|
base_dir: "./workspaces"
|
|
@@ -231,11 +244,13 @@ src/
|
|
|
231
244
|
ctl.ts claudebridge-ctl: memory/task/reminder/auto ops via SQLite
|
|
232
245
|
webhook.ts HTTP server + GitHub webhooks + cron scheduler
|
|
233
246
|
core/
|
|
234
|
-
agent.ts Claude CLI subprocess spawner
|
|
247
|
+
agent.ts Claude CLI subprocess spawner + session summary sync
|
|
235
248
|
config.ts YAML config with env fallback
|
|
236
249
|
keys.ts Endpoint round-robin with cooldown
|
|
237
|
-
lock.ts Per-user concurrency mutex (Redis or in-memory)
|
|
250
|
+
lock.ts Per-user/per-session concurrency mutex (Redis or in-memory)
|
|
238
251
|
store.ts SQLite (WAL): sessions, usage, history, memories, tasks
|
|
252
|
+
router.ts Dispatcher: message routing with memories + session summaries
|
|
253
|
+
session.ts Sub-session lifecycle management
|
|
239
254
|
permissions.ts Whitelist access control
|
|
240
255
|
markdown.ts Markdown → Telegram MarkdownV2
|
|
241
256
|
i18n.ts Internationalization (en/zh)
|
|
@@ -250,13 +265,16 @@ src/
|
|
|
250
265
|
### Data Flow
|
|
251
266
|
|
|
252
267
|
```
|
|
253
|
-
User message → Adapter → Access check
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
268
|
+
User message → Adapter → Access check
|
|
269
|
+
↓
|
|
270
|
+
Dispatcher (master session)
|
|
271
|
+
├─ Fast path: 0-1 sessions → direct route ($0)
|
|
272
|
+
└─ Classify: 2+ sessions → Claude call (memories + summaries)
|
|
273
|
+
↓
|
|
274
|
+
Sub-session execution (per-session lock)
|
|
275
|
+
├─ Inject memories + skill doc → spawn claude CLI
|
|
276
|
+
├─ Stream response back to adapter
|
|
277
|
+
└─ Post: save history, sync summary, auto-summarize to shared memory
|
|
260
278
|
```
|
|
261
279
|
|
|
262
280
|
## Prerequisites
|
|
@@ -305,6 +323,14 @@ MIT
|
|
|
305
323
|
- **并行执行**:多个 `claude` 实例同时运行(`max_parallel` 配置)
|
|
306
324
|
- **可观测性**:`/status` 命令显示任务队列、链路进度和执行统计
|
|
307
325
|
|
|
326
|
+
### v0.10.0:Dispatcher 架构(主从会话)
|
|
327
|
+
|
|
328
|
+
- **Dispatcher(主会话)**:每个用户有一个 Dispatcher 接收所有消息并路由到正确的子会话,用户无需感知子会话的存在
|
|
329
|
+
- **智能路由**:0-1 个活跃会话走快速路径($0);2+ 个会话时 Claude 分类器携带用户记忆 + 会话摘要进行判断
|
|
330
|
+
- **会话摘要**:每个子会话自动生成摘要,让 Dispatcher 了解每个对话在做什么
|
|
331
|
+
- **记忆感知分发**:Dispatcher 分类时可见用户记忆 + 所有活跃会话摘要,即使模糊消息也能准确路由
|
|
332
|
+
- **并发子会话**:多个子会话并行执行,per-session 锁保证安全
|
|
333
|
+
|
|
308
334
|
## 快速开始
|
|
309
335
|
|
|
310
336
|
### 全局安装(npm)
|
package/config.yaml.example
CHANGED
|
@@ -37,7 +37,7 @@ agent:
|
|
|
37
37
|
enabled: true # Enable multi-session (concurrent conversations per user)
|
|
38
38
|
max_per_user: 3 # Max concurrent sub-sessions per user
|
|
39
39
|
idle_timeout_minutes: 30 # Auto-close idle sub-sessions after this time
|
|
40
|
-
|
|
40
|
+
dispatcher_budget: 0.05 # Max budget for dispatcher classifier
|
|
41
41
|
classifier_model: "" # Model for classifier (empty = use default)
|
|
42
42
|
|
|
43
43
|
workspace:
|
|
@@ -11,6 +11,7 @@ export declare class DiscordAdapter implements Adapter {
|
|
|
11
11
|
private reminderTimer?;
|
|
12
12
|
private autoTimer?;
|
|
13
13
|
private approvalTimer?;
|
|
14
|
+
private fileSendTimer?;
|
|
14
15
|
private activeAutoTasks;
|
|
15
16
|
private maxParallel;
|
|
16
17
|
constructor(engine: AgentEngine, store: Store, config: DiscordConfig, locale?: string);
|
|
@@ -27,4 +28,5 @@ export declare class DiscordAdapter implements Adapter {
|
|
|
27
28
|
private checkApprovals;
|
|
28
29
|
private handleStatusCommand;
|
|
29
30
|
private handleSessionsCommand;
|
|
31
|
+
private checkFileSends;
|
|
30
32
|
}
|
package/dist/adapters/discord.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Client, GatewayIntentBits } from "discord.js";
|
|
2
|
-
import { writeFileSync } from "fs";
|
|
2
|
+
import { writeFileSync, existsSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { chunkText } from "./base.js";
|
|
5
5
|
import { reloadConfig } from "../core/config.js";
|
|
@@ -16,6 +16,7 @@ export class DiscordAdapter {
|
|
|
16
16
|
reminderTimer;
|
|
17
17
|
autoTimer;
|
|
18
18
|
approvalTimer;
|
|
19
|
+
fileSendTimer;
|
|
19
20
|
activeAutoTasks = 0;
|
|
20
21
|
maxParallel = 1;
|
|
21
22
|
constructor(engine, store, config, locale = "en") {
|
|
@@ -255,6 +256,7 @@ export class DiscordAdapter {
|
|
|
255
256
|
this.reminderTimer = setInterval(() => this.checkReminders(), 30000);
|
|
256
257
|
this.autoTimer = setInterval(() => this.processAutoTasks(), 60000);
|
|
257
258
|
this.approvalTimer = setInterval(() => this.checkApprovals(), 15000);
|
|
259
|
+
this.fileSendTimer = setInterval(() => this.checkFileSends(), 5000);
|
|
258
260
|
}
|
|
259
261
|
stop() {
|
|
260
262
|
if (this.reminderTimer)
|
|
@@ -263,6 +265,8 @@ export class DiscordAdapter {
|
|
|
263
265
|
clearInterval(this.autoTimer);
|
|
264
266
|
if (this.approvalTimer)
|
|
265
267
|
clearInterval(this.approvalTimer);
|
|
268
|
+
if (this.fileSendTimer)
|
|
269
|
+
clearInterval(this.fileSendTimer);
|
|
266
270
|
this.client.destroy();
|
|
267
271
|
}
|
|
268
272
|
async checkReminders() {
|
|
@@ -409,4 +413,31 @@ export class DiscordAdapter {
|
|
|
409
413
|
});
|
|
410
414
|
await msg.reply(`${t(this.locale, "sessions_list")}\n${lines.join("\n")}`);
|
|
411
415
|
}
|
|
416
|
+
async checkFileSends() {
|
|
417
|
+
try {
|
|
418
|
+
const pending = this.store.getPendingFileSends("discord");
|
|
419
|
+
for (const f of pending) {
|
|
420
|
+
if (!existsSync(f.file_path)) {
|
|
421
|
+
this.store.markFileFailed(f.id);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
const ch = await this.client.channels.fetch(f.chat_id);
|
|
426
|
+
if (!ch?.isTextBased() || !("send" in ch)) {
|
|
427
|
+
this.store.markFileFailed(f.id);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
await ch.send({ content: f.caption || undefined, files: [f.file_path] });
|
|
431
|
+
this.store.markFileSent(f.id);
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
log.error("file send error", { id: f.id, error: err?.message });
|
|
435
|
+
this.store.markFileFailed(f.id);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
catch (e) {
|
|
440
|
+
log.error("checkFileSends error", { error: e?.message });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
412
443
|
}
|
|
@@ -12,6 +12,7 @@ export declare class TelegramAdapter implements Adapter {
|
|
|
12
12
|
private reminderTimer?;
|
|
13
13
|
private autoTimer?;
|
|
14
14
|
private approvalTimer?;
|
|
15
|
+
private fileSendTimer?;
|
|
15
16
|
private activeAutoTasks;
|
|
16
17
|
private maxParallel;
|
|
17
18
|
private pages;
|
|
@@ -38,4 +39,5 @@ export declare class TelegramAdapter implements Adapter {
|
|
|
38
39
|
private handleApprovalCallback;
|
|
39
40
|
private handleStatusCommand;
|
|
40
41
|
private handleSessionsCommand;
|
|
42
|
+
private checkFileSends;
|
|
41
43
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { chunkText } from "./base.js";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { basename, extname } from "path";
|
|
2
4
|
import { reloadConfig } from "../core/config.js";
|
|
3
5
|
import { toTelegramMarkdown } from "../core/markdown.js";
|
|
4
6
|
import { t, getCommandDescriptions } from "../core/i18n.js";
|
|
@@ -15,6 +17,7 @@ export class TelegramAdapter {
|
|
|
15
17
|
reminderTimer;
|
|
16
18
|
autoTimer;
|
|
17
19
|
approvalTimer;
|
|
20
|
+
fileSendTimer;
|
|
18
21
|
activeAutoTasks = 0;
|
|
19
22
|
maxParallel = 1;
|
|
20
23
|
pages = new Map();
|
|
@@ -381,6 +384,7 @@ export class TelegramAdapter {
|
|
|
381
384
|
this.reminderTimer = setInterval(() => this.checkReminders(), 30000);
|
|
382
385
|
this.autoTimer = setInterval(() => this.processAutoTasks(), 60000);
|
|
383
386
|
this.approvalTimer = setInterval(() => this.checkApprovals(), 15000);
|
|
387
|
+
this.fileSendTimer = setInterval(() => this.checkFileSends(), 5000);
|
|
384
388
|
await this.registerCommands();
|
|
385
389
|
let pollBackoff = 0;
|
|
386
390
|
while (this.running) {
|
|
@@ -416,6 +420,8 @@ export class TelegramAdapter {
|
|
|
416
420
|
clearInterval(this.autoTimer);
|
|
417
421
|
if (this.approvalTimer)
|
|
418
422
|
clearInterval(this.approvalTimer);
|
|
423
|
+
if (this.fileSendTimer)
|
|
424
|
+
clearInterval(this.fileSendTimer);
|
|
419
425
|
}
|
|
420
426
|
async registerCommands() {
|
|
421
427
|
try {
|
|
@@ -625,4 +631,43 @@ export class TelegramAdapter {
|
|
|
625
631
|
});
|
|
626
632
|
await this.reply(chatId, `${t(this.locale, "sessions_list")}\n${lines.join("\n")}`);
|
|
627
633
|
}
|
|
634
|
+
async checkFileSends() {
|
|
635
|
+
try {
|
|
636
|
+
const pending = this.store.getPendingFileSends("telegram");
|
|
637
|
+
for (const f of pending) {
|
|
638
|
+
if (!existsSync(f.file_path)) {
|
|
639
|
+
this.store.markFileFailed(f.id);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
const chatId = Number(f.chat_id);
|
|
643
|
+
const ext = extname(f.file_path).toLowerCase();
|
|
644
|
+
const isPhoto = [".jpg", ".jpeg", ".png", ".gif"].includes(ext);
|
|
645
|
+
const method = isPhoto ? "sendPhoto" : "sendDocument";
|
|
646
|
+
const fieldName = isPhoto ? "photo" : "document";
|
|
647
|
+
try {
|
|
648
|
+
const form = new FormData();
|
|
649
|
+
form.append("chat_id", String(chatId));
|
|
650
|
+
const blob = new Blob([await import("fs").then(fs => fs.readFileSync(f.file_path))]);
|
|
651
|
+
form.append(fieldName, blob, basename(f.file_path));
|
|
652
|
+
if (f.caption)
|
|
653
|
+
form.append("caption", f.caption);
|
|
654
|
+
const res = await fetch(`${this.api}/${method}`, { method: "POST", body: form });
|
|
655
|
+
const json = await res.json();
|
|
656
|
+
if (json.ok)
|
|
657
|
+
this.store.markFileSent(f.id);
|
|
658
|
+
else {
|
|
659
|
+
log.error("file send API error", { id: f.id, desc: json.description });
|
|
660
|
+
this.store.markFileFailed(f.id);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
catch (err) {
|
|
664
|
+
log.error("file send error", { id: f.id, error: err?.message });
|
|
665
|
+
this.store.markFileFailed(f.id);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
catch (e) {
|
|
670
|
+
log.error("checkFileSends error", { error: e?.message });
|
|
671
|
+
}
|
|
672
|
+
}
|
|
628
673
|
}
|
package/dist/core/agent.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Store } from "./store.js";
|
|
|
3
3
|
import { AccessControl } from "./permissions.js";
|
|
4
4
|
import { EndpointRotator } from "./keys.js";
|
|
5
5
|
import { SessionManager } from "./session.js";
|
|
6
|
-
import {
|
|
6
|
+
import { Dispatcher } from "./router.js";
|
|
7
7
|
export interface AgentResponse {
|
|
8
8
|
text: string;
|
|
9
9
|
sessionId: string;
|
|
@@ -19,7 +19,7 @@ export declare class AgentEngine {
|
|
|
19
19
|
private lock;
|
|
20
20
|
private rotator;
|
|
21
21
|
private sessionMgr;
|
|
22
|
-
private
|
|
22
|
+
private dispatcher;
|
|
23
23
|
private sessionExpiryTimer?;
|
|
24
24
|
access: AccessControl;
|
|
25
25
|
constructor(config: Config, store: Store);
|
|
@@ -32,7 +32,7 @@ export declare class AgentEngine {
|
|
|
32
32
|
getEndpointCount(): number;
|
|
33
33
|
getMaxParallel(): number;
|
|
34
34
|
getSessionManager(): SessionManager;
|
|
35
|
-
|
|
35
|
+
getDispatcher(): Dispatcher;
|
|
36
36
|
getWorkDir(userId: string): string;
|
|
37
37
|
/** @deprecated Use isSessionLocked() for multi-session mode */
|
|
38
38
|
isLocked(userId: string): boolean;
|
|
@@ -68,5 +68,6 @@ export declare class AgentEngine {
|
|
|
68
68
|
private _execute;
|
|
69
69
|
/** Parallel execution without session resume. Thin wrapper around _spawnAgent. */
|
|
70
70
|
private _executeNoSession;
|
|
71
|
+
private _syncSessionSummary;
|
|
71
72
|
private _autoSummarize;
|
|
72
73
|
}
|
package/dist/core/agent.js
CHANGED
|
@@ -6,7 +6,7 @@ import { AccessControl } from "./permissions.js";
|
|
|
6
6
|
import { EndpointRotator } from "./keys.js";
|
|
7
7
|
import { generateSkillDoc } from "../skills/bridge.js";
|
|
8
8
|
import { SessionManager } from "./session.js";
|
|
9
|
-
import {
|
|
9
|
+
import { Dispatcher } from "./router.js";
|
|
10
10
|
import { log as rootLog } from "./logger.js";
|
|
11
11
|
import { getProvider } from "../providers/registry.js";
|
|
12
12
|
const log = rootLog.child("agent");
|
|
@@ -16,7 +16,7 @@ export class AgentEngine {
|
|
|
16
16
|
lock;
|
|
17
17
|
rotator;
|
|
18
18
|
sessionMgr;
|
|
19
|
-
|
|
19
|
+
dispatcher;
|
|
20
20
|
sessionExpiryTimer;
|
|
21
21
|
access;
|
|
22
22
|
constructor(config, store) {
|
|
@@ -26,7 +26,7 @@ export class AgentEngine {
|
|
|
26
26
|
this.access = new AccessControl(config.access.allowed_users, config.access.allowed_groups);
|
|
27
27
|
this.rotator = new EndpointRotator(config.endpoints);
|
|
28
28
|
this.sessionMgr = new SessionManager(store, config.agent.session);
|
|
29
|
-
this.
|
|
29
|
+
this.dispatcher = new Dispatcher(this.sessionMgr, this.rotator, config.agent.session, store);
|
|
30
30
|
// Periodic idle session expiry (every 5 min)
|
|
31
31
|
this.sessionExpiryTimer = setInterval(() => {
|
|
32
32
|
this.sessionMgr.expireIdle();
|
|
@@ -53,8 +53,8 @@ export class AgentEngine {
|
|
|
53
53
|
getSessionManager() {
|
|
54
54
|
return this.sessionMgr;
|
|
55
55
|
}
|
|
56
|
-
|
|
57
|
-
return this.
|
|
56
|
+
getDispatcher() {
|
|
57
|
+
return this.dispatcher;
|
|
58
58
|
}
|
|
59
59
|
getWorkDir(userId) {
|
|
60
60
|
if (!this.config.workspace.isolation) {
|
|
@@ -80,8 +80,8 @@ export class AgentEngine {
|
|
|
80
80
|
* Routes to the correct sub-session and executes concurrently.
|
|
81
81
|
*/
|
|
82
82
|
async handleUserMessage(userId, prompt, platform, chatId, replyToMsgId, onChunk, overrideTimeoutMs) {
|
|
83
|
-
// 1.
|
|
84
|
-
const decision = await this.
|
|
83
|
+
// 1. Dispatch
|
|
84
|
+
const decision = await this.dispatcher.dispatch(userId, platform, chatId, prompt, replyToMsgId);
|
|
85
85
|
// 2. Create or get sub-session
|
|
86
86
|
let subSession;
|
|
87
87
|
if (decision.action === "create") {
|
|
@@ -121,6 +121,8 @@ export class AgentEngine {
|
|
|
121
121
|
// 6. Auto-summarize
|
|
122
122
|
if (this.config.agent.memory?.auto_summary)
|
|
123
123
|
this._autoSummarize(userId, prompt, res.text);
|
|
124
|
+
// 7. Sync sub-session summary for dispatcher context
|
|
125
|
+
this._syncSessionSummary(subSession, prompt, res.text);
|
|
124
126
|
return { ...res, subSessionId: subSession.id, label: subSession.label };
|
|
125
127
|
}
|
|
126
128
|
/**
|
|
@@ -336,6 +338,47 @@ export class AgentEngine {
|
|
|
336
338
|
logLabel: "parallel", verbose: false,
|
|
337
339
|
});
|
|
338
340
|
}
|
|
341
|
+
_syncSessionSummary(subSession, prompt, response) {
|
|
342
|
+
const ep = this.rotator.count
|
|
343
|
+
? this.rotator.next()
|
|
344
|
+
: { name: "default", provider: "claude", model: "" };
|
|
345
|
+
const summaryPrompt = `Summarize this conversation exchange in 1-2 sentences for a dispatcher that routes messages. Focus on the topic/task being discussed.\n\nUser: ${prompt.slice(0, 300)}\nAssistant: ${response.slice(0, 500)}`;
|
|
346
|
+
const args = ["-p", summaryPrompt, "--output-format", "stream-json", "--max-turns", "1", "--max-budget-usd", "0.02"];
|
|
347
|
+
if (ep.model)
|
|
348
|
+
args.push("--model", ep.model);
|
|
349
|
+
const env = { ...process.env };
|
|
350
|
+
const child = spawn("claude", args, { env, stdio: ["pipe", "pipe", "pipe"] });
|
|
351
|
+
child.stdin.end();
|
|
352
|
+
const killTimer = setTimeout(() => { try {
|
|
353
|
+
child.kill("SIGTERM");
|
|
354
|
+
}
|
|
355
|
+
catch { } }, 30000);
|
|
356
|
+
let result = "";
|
|
357
|
+
let buffer = "";
|
|
358
|
+
child.stdout.on("data", (data) => {
|
|
359
|
+
buffer += data.toString();
|
|
360
|
+
const lines = buffer.split("\n");
|
|
361
|
+
buffer = lines.pop() || "";
|
|
362
|
+
for (const line of lines) {
|
|
363
|
+
if (!line.trim())
|
|
364
|
+
continue;
|
|
365
|
+
try {
|
|
366
|
+
const msg = JSON.parse(line);
|
|
367
|
+
if (msg.type === "result" && msg.result)
|
|
368
|
+
result = msg.result;
|
|
369
|
+
}
|
|
370
|
+
catch { }
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
child.on("close", () => {
|
|
374
|
+
clearTimeout(killTimer);
|
|
375
|
+
if (result && result.length > 0) {
|
|
376
|
+
this.sessionMgr.updateSummary(subSession.id, result.trim().slice(0, 200));
|
|
377
|
+
log.info("session summary synced", { sessionId: subSession.id.slice(0, 8) });
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
child.on("error", (err) => { log.warn("session summary error", { error: err.message }); });
|
|
381
|
+
}
|
|
339
382
|
_autoSummarize(userId, prompt, response) {
|
|
340
383
|
const ep = this.rotator.count
|
|
341
384
|
? this.rotator.next()
|
package/dist/core/router.d.ts
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import { SessionManager } from "./session.js";
|
|
2
2
|
import { EndpointRotator } from "./keys.js";
|
|
3
3
|
import { SessionConfig } from "./config.js";
|
|
4
|
+
import { Store } from "./store.js";
|
|
4
5
|
export interface RouterDecision {
|
|
5
6
|
action: "route" | "create";
|
|
6
7
|
subSessionId?: string;
|
|
7
8
|
label?: string;
|
|
8
9
|
}
|
|
9
|
-
export declare class
|
|
10
|
+
export declare class Dispatcher {
|
|
10
11
|
private sessionMgr;
|
|
11
12
|
private rotator;
|
|
12
13
|
private config;
|
|
13
|
-
|
|
14
|
+
private store;
|
|
15
|
+
constructor(sessionMgr: SessionManager, rotator: EndpointRotator, config: SessionConfig, store: Store);
|
|
14
16
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* Dispatch user message:
|
|
18
|
+
* Fast path: reply-to → direct route ($0)
|
|
19
|
+
* Fast path: 0 active → create ($0)
|
|
20
|
+
* Fast path: 1 active → route ($0)
|
|
21
|
+
* Classify: 2+ active → Claude classifier with memories + summaries
|
|
19
22
|
*/
|
|
20
|
-
|
|
21
|
-
/**
|
|
23
|
+
dispatch(userId: string, platform: string, chatId: string, messageText: string, replyToMsgId?: string): Promise<RouterDecision>;
|
|
24
|
+
/** Classify with user memories + sub-session summaries for context */
|
|
22
25
|
private _classify;
|
|
23
|
-
/** Spawn claude CLI for single-turn classification
|
|
26
|
+
/** Spawn claude CLI for single-turn classification */
|
|
24
27
|
private _callClassifier;
|
|
25
28
|
}
|
|
29
|
+
export { Dispatcher as SessionRouter };
|
package/dist/core/router.js
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import { spawn } from "child_process";
|
|
2
2
|
import { log as rootLog } from "./logger.js";
|
|
3
|
-
const log = rootLog.child("
|
|
4
|
-
export class
|
|
3
|
+
const log = rootLog.child("dispatcher");
|
|
4
|
+
export class Dispatcher {
|
|
5
5
|
sessionMgr;
|
|
6
6
|
rotator;
|
|
7
7
|
config;
|
|
8
|
-
|
|
8
|
+
store;
|
|
9
|
+
constructor(sessionMgr, rotator, config, store) {
|
|
9
10
|
this.sessionMgr = sessionMgr;
|
|
10
11
|
this.rotator = rotator;
|
|
11
12
|
this.config = config;
|
|
13
|
+
this.store = store;
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Dispatch user message:
|
|
17
|
+
* Fast path: reply-to → direct route ($0)
|
|
18
|
+
* Fast path: 0 active → create ($0)
|
|
19
|
+
* Fast path: 1 active → route ($0)
|
|
20
|
+
* Classify: 2+ active → Claude classifier with memories + summaries
|
|
18
21
|
*/
|
|
19
|
-
async
|
|
20
|
-
//
|
|
22
|
+
async dispatch(userId, platform, chatId, messageText, replyToMsgId) {
|
|
23
|
+
// Fast path: reply-to routing
|
|
21
24
|
if (replyToMsgId) {
|
|
22
25
|
const sessId = this.sessionMgr.getSessionByMessage(replyToMsgId, chatId);
|
|
23
26
|
if (sessId) {
|
|
@@ -26,9 +29,8 @@ export class SessionRouter {
|
|
|
26
29
|
return { action: "route", subSessionId: sessId };
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
|
-
// reply-to pointed to closed/expired session — fall through to Tier 2/3
|
|
30
32
|
}
|
|
31
|
-
//
|
|
33
|
+
// Fast path: 0-1 active sessions
|
|
32
34
|
const active = this.sessionMgr.getActive(userId, platform);
|
|
33
35
|
if (active.length === 0) {
|
|
34
36
|
return { action: "create", label: messageText.slice(0, 50) };
|
|
@@ -36,49 +38,67 @@ export class SessionRouter {
|
|
|
36
38
|
if (active.length === 1) {
|
|
37
39
|
return { action: "route", subSessionId: active[0].id };
|
|
38
40
|
}
|
|
39
|
-
//
|
|
41
|
+
// 2+ sessions: classify with memories + summaries
|
|
40
42
|
return await this._classify(userId, platform, messageText, active);
|
|
41
43
|
}
|
|
42
|
-
/**
|
|
44
|
+
/** Classify with user memories + sub-session summaries for context */
|
|
43
45
|
async _classify(userId, platform, text, sessions) {
|
|
44
46
|
try {
|
|
47
|
+
// Gather context
|
|
48
|
+
const memories = this.store.getMemories(userId);
|
|
49
|
+
const summaries = this.sessionMgr.getSummaries(userId, platform);
|
|
45
50
|
const sessionList = sessions
|
|
46
51
|
.map(s => {
|
|
47
52
|
const ago = Math.round((Date.now() - s.lastActiveAt) / 60000);
|
|
48
|
-
|
|
53
|
+
const sum = summaries.find(x => x.id === s.id);
|
|
54
|
+
const summaryText = sum?.summary ? ` | Summary: ${sum.summary}` : "";
|
|
55
|
+
return `[${s.id.slice(0, 8)}] "${s.label || "(no topic)"}" (${ago}min ago${summaryText})`;
|
|
49
56
|
})
|
|
50
57
|
.join("\n");
|
|
51
|
-
const
|
|
58
|
+
const memoryBlock = memories.length
|
|
59
|
+
? `\nUser context:\n${memories.slice(0, 10).map(m => `- ${m.content}`).join("\n")}\n`
|
|
60
|
+
: "";
|
|
61
|
+
const prompt = `You are a message dispatcher. Route the user's message to the correct conversation, or decide to create a new one, or handle a management request.
|
|
62
|
+
${memoryBlock}
|
|
63
|
+
Active conversations:
|
|
64
|
+
${sessionList}
|
|
65
|
+
|
|
66
|
+
User message: "${text.slice(0, 300)}"
|
|
67
|
+
|
|
68
|
+
Reply with ONLY one of:
|
|
69
|
+
- An 8-char session ID to route to
|
|
70
|
+
- "new" to create a new conversation
|
|
71
|
+
|
|
72
|
+
No explanation.`;
|
|
52
73
|
const result = await this._callClassifier(prompt);
|
|
53
|
-
const cleaned = result.trim()
|
|
54
|
-
if (cleaned === "new") {
|
|
74
|
+
const cleaned = result.trim();
|
|
75
|
+
if (cleaned.toLowerCase() === "new") {
|
|
55
76
|
return { action: "create", label: text.slice(0, 50) };
|
|
56
77
|
}
|
|
57
78
|
// Match against active sessions (first 8 chars of ID)
|
|
58
|
-
const match = sessions.find(s => s.id.slice(0, 8) === cleaned);
|
|
79
|
+
const match = sessions.find(s => s.id.slice(0, 8) === cleaned.toLowerCase());
|
|
59
80
|
if (match) {
|
|
60
81
|
return { action: "route", subSessionId: match.id };
|
|
61
82
|
}
|
|
62
|
-
// Fallback:
|
|
83
|
+
// Fallback: route to most recently active
|
|
63
84
|
log.warn("classifier returned unexpected, falling back", { result: cleaned });
|
|
64
85
|
return { action: "route", subSessionId: sessions[0].id };
|
|
65
86
|
}
|
|
66
87
|
catch (err) {
|
|
67
|
-
// Classifier failed — fallback: create new session
|
|
68
88
|
log.warn("classifier error, creating new session", { error: err.message });
|
|
69
89
|
return { action: "create", label: text.slice(0, 50) };
|
|
70
90
|
}
|
|
71
91
|
}
|
|
72
|
-
/** Spawn claude CLI for single-turn classification
|
|
92
|
+
/** Spawn claude CLI for single-turn classification */
|
|
73
93
|
_callClassifier(prompt) {
|
|
74
94
|
return new Promise((resolve, reject) => {
|
|
95
|
+
const budget = this.config.dispatcher_budget ?? this.config.classifier_budget ?? 0.05;
|
|
75
96
|
const args = ["-p", prompt, "--output-format", "stream-json", "--max-turns", "1"];
|
|
76
|
-
if (
|
|
77
|
-
args.push("--max-budget-usd", String(
|
|
97
|
+
if (budget)
|
|
98
|
+
args.push("--max-budget-usd", String(budget));
|
|
78
99
|
if (this.config.classifier_model)
|
|
79
100
|
args.push("--model", this.config.classifier_model);
|
|
80
101
|
const env = { ...process.env };
|
|
81
|
-
// Use the first available endpoint for the classifier model
|
|
82
102
|
if (this.rotator.count) {
|
|
83
103
|
const ep = this.rotator.next();
|
|
84
104
|
if (!this.config.classifier_model && ep.model)
|
|
@@ -123,3 +143,5 @@ export class SessionRouter {
|
|
|
123
143
|
});
|
|
124
144
|
}
|
|
125
145
|
}
|
|
146
|
+
// Backward compatibility alias
|
|
147
|
+
export { Dispatcher as SessionRouter };
|
package/dist/core/schema.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ declare const SessionConfigSchema: z.ZodObject<{
|
|
|
16
16
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
17
17
|
max_per_user: z.ZodDefault<z.ZodNumber>;
|
|
18
18
|
idle_timeout_minutes: z.ZodDefault<z.ZodNumber>;
|
|
19
|
-
|
|
19
|
+
dispatcher_budget: z.ZodDefault<z.ZodNumber>;
|
|
20
20
|
classifier_model: z.ZodDefault<z.ZodString>;
|
|
21
21
|
}, z.core.$strip>;
|
|
22
22
|
declare const AgentConfigSchema: z.ZodObject<{
|
|
@@ -40,7 +40,7 @@ declare const AgentConfigSchema: z.ZodObject<{
|
|
|
40
40
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
41
41
|
max_per_user: z.ZodDefault<z.ZodNumber>;
|
|
42
42
|
idle_timeout_minutes: z.ZodDefault<z.ZodNumber>;
|
|
43
|
-
|
|
43
|
+
dispatcher_budget: z.ZodDefault<z.ZodNumber>;
|
|
44
44
|
classifier_model: z.ZodDefault<z.ZodString>;
|
|
45
45
|
}, z.core.$strip>>;
|
|
46
46
|
}, z.core.$strip>;
|
|
@@ -108,7 +108,7 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
108
108
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
109
109
|
max_per_user: z.ZodDefault<z.ZodNumber>;
|
|
110
110
|
idle_timeout_minutes: z.ZodDefault<z.ZodNumber>;
|
|
111
|
-
|
|
111
|
+
dispatcher_budget: z.ZodDefault<z.ZodNumber>;
|
|
112
112
|
classifier_model: z.ZodDefault<z.ZodString>;
|
|
113
113
|
}, z.core.$strip>>;
|
|
114
114
|
}, z.core.$strip>>;
|
package/dist/core/schema.js
CHANGED
|
@@ -16,7 +16,7 @@ const SessionConfigSchema = z.object({
|
|
|
16
16
|
enabled: z.boolean().default(true),
|
|
17
17
|
max_per_user: z.number().int().positive().default(3),
|
|
18
18
|
idle_timeout_minutes: z.number().positive().default(30),
|
|
19
|
-
|
|
19
|
+
dispatcher_budget: z.number().nonnegative().default(0.05),
|
|
20
20
|
classifier_model: z.string().default(""),
|
|
21
21
|
});
|
|
22
22
|
const AgentConfigSchema = z.object({
|
package/dist/core/session.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface SubSession {
|
|
|
7
7
|
chatId: string;
|
|
8
8
|
claudeSessionId: string | null;
|
|
9
9
|
label: string;
|
|
10
|
+
summary: string;
|
|
10
11
|
status: "active" | "idle" | "expired" | "closed";
|
|
11
12
|
createdAt: number;
|
|
12
13
|
lastActiveAt: number;
|
|
@@ -47,4 +48,13 @@ export declare class SessionManager {
|
|
|
47
48
|
isUsable(session: SubSession): boolean;
|
|
48
49
|
/** Get all sub-sessions for a user (all statuses) */
|
|
49
50
|
getAll(userId: string): SubSession[];
|
|
51
|
+
/** Update the summary of a sub-session */
|
|
52
|
+
updateSummary(sessionId: string, summary: string): void;
|
|
53
|
+
/** Get summaries of active sub-sessions for dispatcher context */
|
|
54
|
+
getSummaries(userId: string, platform: string): {
|
|
55
|
+
id: string;
|
|
56
|
+
label: string;
|
|
57
|
+
summary: string;
|
|
58
|
+
lastActiveAt: number;
|
|
59
|
+
}[];
|
|
50
60
|
}
|
package/dist/core/session.js
CHANGED
|
@@ -10,6 +10,7 @@ function toSubSession(row) {
|
|
|
10
10
|
chatId: row.chat_id,
|
|
11
11
|
claudeSessionId: row.claude_session_id ?? null,
|
|
12
12
|
label: row.label,
|
|
13
|
+
summary: row.summary ?? "",
|
|
13
14
|
status: row.status,
|
|
14
15
|
createdAt: row.created_at,
|
|
15
16
|
lastActiveAt: row.last_active_at,
|
|
@@ -97,4 +98,14 @@ export class SessionManager {
|
|
|
97
98
|
getAll(userId) {
|
|
98
99
|
return this.store.getAllSubSessions(userId).map(toSubSession);
|
|
99
100
|
}
|
|
101
|
+
/** Update the summary of a sub-session */
|
|
102
|
+
updateSummary(sessionId, summary) {
|
|
103
|
+
this.store.updateSubSessionSummary(sessionId, summary);
|
|
104
|
+
}
|
|
105
|
+
/** Get summaries of active sub-sessions for dispatcher context */
|
|
106
|
+
getSummaries(userId, platform) {
|
|
107
|
+
return this.store.getSubSessionSummaries(userId, platform).map(r => ({
|
|
108
|
+
id: r.id, label: r.label, summary: r.summary, lastActiveAt: r.last_active_at,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
100
111
|
}
|
package/dist/core/store.d.ts
CHANGED
|
@@ -145,4 +145,22 @@ export declare class Store {
|
|
|
145
145
|
message_count: number;
|
|
146
146
|
total_cost: number;
|
|
147
147
|
}[];
|
|
148
|
+
updateSubSessionSummary(id: string, summary: string): void;
|
|
149
|
+
getSubSessionSummaries(userId: string, platform: string): {
|
|
150
|
+
id: string;
|
|
151
|
+
label: string;
|
|
152
|
+
summary: string;
|
|
153
|
+
last_active_at: number;
|
|
154
|
+
}[];
|
|
155
|
+
addFileSend(userId: string, platform: string, chatId: string, filePath: string, caption: string): number;
|
|
156
|
+
getPendingFileSends(platform: string): {
|
|
157
|
+
id: number;
|
|
158
|
+
user_id: string;
|
|
159
|
+
platform: string;
|
|
160
|
+
chat_id: string;
|
|
161
|
+
file_path: string;
|
|
162
|
+
caption: string;
|
|
163
|
+
}[];
|
|
164
|
+
markFileSent(id: number): void;
|
|
165
|
+
markFileFailed(id: number): void;
|
|
148
166
|
}
|
package/dist/core/store.js
CHANGED
|
@@ -72,6 +72,17 @@ export class Store {
|
|
|
72
72
|
message_count INTEGER NOT NULL DEFAULT 0,
|
|
73
73
|
total_cost REAL NOT NULL DEFAULT 0
|
|
74
74
|
);
|
|
75
|
+
CREATE TABLE IF NOT EXISTS file_sends (
|
|
76
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
77
|
+
user_id TEXT NOT NULL,
|
|
78
|
+
platform TEXT NOT NULL,
|
|
79
|
+
chat_id TEXT NOT NULL,
|
|
80
|
+
file_path TEXT NOT NULL,
|
|
81
|
+
caption TEXT NOT NULL DEFAULT '',
|
|
82
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
83
|
+
created_at INTEGER NOT NULL
|
|
84
|
+
);
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_file_sends_status ON file_sends(platform, status);
|
|
75
86
|
CREATE INDEX IF NOT EXISTS idx_subsess_user ON sub_sessions(user_id, platform, status);
|
|
76
87
|
CREATE TABLE IF NOT EXISTS sub_session_messages (
|
|
77
88
|
platform_msg_id TEXT NOT NULL,
|
|
@@ -81,7 +92,7 @@ export class Store {
|
|
|
81
92
|
PRIMARY KEY (platform_msg_id, chat_id)
|
|
82
93
|
);
|
|
83
94
|
`);
|
|
84
|
-
// Schema migration: add parent_id, result, and
|
|
95
|
+
// Schema migration: add parent_id, result, scheduled_at, and sub_session summary columns
|
|
85
96
|
try {
|
|
86
97
|
this.db.exec("ALTER TABLE tasks ADD COLUMN parent_id INTEGER");
|
|
87
98
|
}
|
|
@@ -94,6 +105,10 @@ export class Store {
|
|
|
94
105
|
this.db.exec("ALTER TABLE tasks ADD COLUMN scheduled_at INTEGER");
|
|
95
106
|
}
|
|
96
107
|
catch { }
|
|
108
|
+
try {
|
|
109
|
+
this.db.exec("ALTER TABLE sub_sessions ADD COLUMN summary TEXT DEFAULT ''");
|
|
110
|
+
}
|
|
111
|
+
catch { }
|
|
97
112
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_id)");
|
|
98
113
|
// Startup recovery: reset orphaned 'running' tasks back to 'auto' so they get re-executed
|
|
99
114
|
const orphaned = this.db.prepare("SELECT id, description FROM tasks WHERE status = 'running'").all();
|
|
@@ -309,4 +324,24 @@ export class Store {
|
|
|
309
324
|
getAllSubSessions(userId) {
|
|
310
325
|
return this.db.prepare("SELECT * FROM sub_sessions WHERE user_id = ? ORDER BY last_active_at DESC").all(userId);
|
|
311
326
|
}
|
|
327
|
+
updateSubSessionSummary(id, summary) {
|
|
328
|
+
this.db.prepare("UPDATE sub_sessions SET summary = ? WHERE id = ?").run(summary, id);
|
|
329
|
+
}
|
|
330
|
+
getSubSessionSummaries(userId, platform) {
|
|
331
|
+
return this.db.prepare("SELECT id, label, summary, last_active_at FROM sub_sessions WHERE user_id = ? AND platform = ? AND status IN ('active','idle') ORDER BY last_active_at DESC").all(userId, platform);
|
|
332
|
+
}
|
|
333
|
+
// --- file_sends ---
|
|
334
|
+
addFileSend(userId, platform, chatId, filePath, caption) {
|
|
335
|
+
const r = this.db.prepare("INSERT INTO file_sends (user_id, platform, chat_id, file_path, caption, status, created_at) VALUES (?, ?, ?, ?, ?, 'pending', ?)").run(userId, platform, chatId, filePath, caption, Date.now());
|
|
336
|
+
return Number(r.lastInsertRowid);
|
|
337
|
+
}
|
|
338
|
+
getPendingFileSends(platform) {
|
|
339
|
+
return this.db.prepare("SELECT id, user_id, platform, chat_id, file_path, caption FROM file_sends WHERE platform = ? AND status = 'pending'").all(platform);
|
|
340
|
+
}
|
|
341
|
+
markFileSent(id) {
|
|
342
|
+
this.db.prepare("UPDATE file_sends SET status = 'sent' WHERE id = ?").run(id);
|
|
343
|
+
}
|
|
344
|
+
markFileFailed(id) {
|
|
345
|
+
this.db.prepare("UPDATE file_sends SET status = 'failed' WHERE id = ?").run(id);
|
|
346
|
+
}
|
|
312
347
|
}
|
package/dist/ctl.js
CHANGED
|
@@ -166,6 +166,24 @@ else if (category === "auto") {
|
|
|
166
166
|
fail("Usage: auto <add|add-approval|result|list|cancel|clear> ...");
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
|
+
else if (category === "file") {
|
|
170
|
+
if (action === "send") {
|
|
171
|
+
const [userId, platform, chatId, filePath, ...captionParts] = rest;
|
|
172
|
+
if (!userId || !platform || !chatId || !filePath)
|
|
173
|
+
fail("Usage: file send <user_id> <platform> <chat_id> <file_path> [caption...]");
|
|
174
|
+
const { resolve } = await import("path");
|
|
175
|
+
const { existsSync } = await import("fs");
|
|
176
|
+
const resolved = resolve(filePath);
|
|
177
|
+
if (!existsSync(resolved))
|
|
178
|
+
fail(`File not found: ${resolved}`);
|
|
179
|
+
const caption = captionParts.join(" ");
|
|
180
|
+
const r = db.prepare("INSERT INTO file_sends (user_id, platform, chat_id, file_path, caption, status, created_at) VALUES (?, ?, ?, ?, ?, 'pending', ?)").run(userId, platform, chatId, resolved, caption, Date.now());
|
|
181
|
+
output({ ok: true, id: Number(r.lastInsertRowid), message: `File queued for sending: ${resolved}` });
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
fail("Usage: file <send> ...");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
169
187
|
else if (category === "session") {
|
|
170
188
|
if (action === "list") {
|
|
171
189
|
const [userId] = rest;
|
|
@@ -179,6 +197,6 @@ else if (category === "session") {
|
|
|
179
197
|
}
|
|
180
198
|
}
|
|
181
199
|
else {
|
|
182
|
-
fail("Usage: claudebridge-ctl <memory|task|reminder|auto|session> <action> [args...]");
|
|
200
|
+
fail("Usage: claudebridge-ctl <memory|task|reminder|auto|file|session> <action> [args...]");
|
|
183
201
|
}
|
|
184
202
|
db.close();
|
package/dist/skills/bridge.js
CHANGED
|
@@ -36,6 +36,12 @@ export function generateSkillDoc(ctx) {
|
|
|
36
36
|
`- 取消自动任务: \`${ctl} auto cancel <任务ID>\``,
|
|
37
37
|
`- 清除已完成任务: \`${ctl} auto clear ${ctx.userId}\``,
|
|
38
38
|
``,
|
|
39
|
+
`### 文件发送`,
|
|
40
|
+
`- 发送文件: \`${ctl} file send ${ctx.userId} ${ctx.platform} ${ctx.chatId} <文件路径> "说明"\``,
|
|
41
|
+
`- filePath 为 workspace 中的文件路径(相对或绝对)`,
|
|
42
|
+
`- 支持图片(jpg/png/gif)和文档(pdf/csv/txt/zip 等)`,
|
|
43
|
+
`- 用户要求生成文件时:先创建文件,再调用此命令发送`,
|
|
44
|
+
``,
|
|
39
45
|
`### 使用指南`,
|
|
40
46
|
`- 用户要你记住某事 → 使用 memory add`,
|
|
41
47
|
`- 用户问你记住了什么 → 使用 memory list`,
|
|
@@ -146,6 +152,12 @@ export function generateSkillDoc(ctx) {
|
|
|
146
152
|
`- Cancel an auto task: \`${ctl} auto cancel <task_id>\``,
|
|
147
153
|
`- Clear completed tasks: \`${ctl} auto clear ${ctx.userId}\``,
|
|
148
154
|
``,
|
|
155
|
+
`### File Sending`,
|
|
156
|
+
`- Send a file: \`${ctl} file send ${ctx.userId} ${ctx.platform} ${ctx.chatId} <filePath> "caption"\``,
|
|
157
|
+
`- filePath can be relative (to workspace) or absolute`,
|
|
158
|
+
`- Supports images (jpg/png/gif) and documents (pdf/csv/txt/zip etc.)`,
|
|
159
|
+
`- When user asks to generate a file: create it first, then call this command to send`,
|
|
160
|
+
``,
|
|
149
161
|
`### Guidelines`,
|
|
150
162
|
`- User wants you to remember something → use memory add`,
|
|
151
163
|
`- User asks what you remember → use memory list`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emqo/claudebridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Bridge claude CLI to chat platforms (Telegram, Discord) with scheduled auto-tasks, autonomous project management, HITL approval, conditional branching, webhook triggers, parallel execution, and observability",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|