@sleep2agi/commhub-server 0.5.0-preview.6 → 0.5.0-preview.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -0
- package/package.json +1 -1
- package/src/index.ts +20 -1
- package/src/tools.ts +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# @sleep2agi/commhub-server
|
|
2
|
+
|
|
3
|
+
AI Agent 通信中枢 — MCP Server + SSE Push + REST API。
|
|
4
|
+
|
|
5
|
+
## 快速启动
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 需要 Bun
|
|
9
|
+
bunx @sleep2agi/commhub-server
|
|
10
|
+
|
|
11
|
+
# 或指定端口 + auth
|
|
12
|
+
PORT=9200 COMMHUB_AUTH_TOKEN=your-secret bunx @sleep2agi/commhub-server
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
启动后:
|
|
16
|
+
- MCP: `http://0.0.0.0:9200/mcp` (Claude Code / Codex 连接)
|
|
17
|
+
- SSE: `http://0.0.0.0:9200/events/:alias` (Agent 实时推送)
|
|
18
|
+
- REST: `http://0.0.0.0:9200/api/*` (Dashboard / 监控)
|
|
19
|
+
- Health: `http://0.0.0.0:9200/health`
|
|
20
|
+
|
|
21
|
+
## MCP 工具 (17 个)
|
|
22
|
+
|
|
23
|
+
### Agent 端 (从 Agent 调用)
|
|
24
|
+
| 工具 | 说明 |
|
|
25
|
+
|------|------|
|
|
26
|
+
| `report_status` | 心跳 + 状态更新 (idle/working/blocked/error/offline) |
|
|
27
|
+
| `report_completion` | 任务完成汇报 |
|
|
28
|
+
| `get_inbox` | 拉取待办任务 |
|
|
29
|
+
| `ack_inbox` | 确认收到任务 |
|
|
30
|
+
|
|
31
|
+
### Hub 端 (从指挥室 / Dashboard 调用)
|
|
32
|
+
| 工具 | 说明 |
|
|
33
|
+
|------|------|
|
|
34
|
+
| `send_task` | 下发任务 (+ 可选 ttl_seconds) |
|
|
35
|
+
| `send_message` | 发消息 (不触发 Agent 处理) |
|
|
36
|
+
| `send_reply` | 回复任务 (replied/failed/cancelled + in_reply_to) |
|
|
37
|
+
| `send_ack` | 确认任务 (不入 inbox) |
|
|
38
|
+
| `retry_task` | 重试失败/过期/取消的任务 |
|
|
39
|
+
| `cancel_task` | 取消待处理任务 |
|
|
40
|
+
| `reassign_task` | 转移任务到另一个 Agent |
|
|
41
|
+
| `get_task` | 查询任务详情 |
|
|
42
|
+
| `get_all_status` | 全局状态面板 |
|
|
43
|
+
| `get_session_status` | 单 session 详情 |
|
|
44
|
+
| `broadcast` | 群发消息 |
|
|
45
|
+
|
|
46
|
+
## REST API
|
|
47
|
+
|
|
48
|
+
| 端点 | 方法 | 说明 |
|
|
49
|
+
|------|------|------|
|
|
50
|
+
| `/health` | GET | 健康检查 (无需 auth) |
|
|
51
|
+
| `/api/status` | GET | 所有 session |
|
|
52
|
+
| `/api/tasks` | GET | 任务列表 (支持 status/from_name/to_name/task_id/limit 过滤) |
|
|
53
|
+
| `/api/nodes` | GET | 节点持久化信息 |
|
|
54
|
+
| `/api/task_events` | GET | 任务审计日志 |
|
|
55
|
+
| `/api/messages` | GET | 消息列表 |
|
|
56
|
+
| `/api/completions` | GET | 完成记录 |
|
|
57
|
+
| `/mcp` | POST | MCP Streamable HTTP |
|
|
58
|
+
|
|
59
|
+
## 数据库 (6 表)
|
|
60
|
+
|
|
61
|
+
SQLite WAL 模式, 自动创建在 `~/.commhub/commhub.db`
|
|
62
|
+
|
|
63
|
+
| 表 | 说明 |
|
|
64
|
+
|---|------|
|
|
65
|
+
| `sessions` | 运行时 session (21 列, 含 node_id/session_id/channels) |
|
|
66
|
+
| `inbox` | 消息队列 (13 列, 含 in_reply_to/scope) |
|
|
67
|
+
| `tasks` | 任务生命周期 (17 列, 完整状态机) |
|
|
68
|
+
| `nodes` | 持久化节点身份 (11 列, 独立于 session) |
|
|
69
|
+
| `completions` | 完成记录 (7 列) |
|
|
70
|
+
| `task_events` | 审计日志 (7 列, 每次状态变化记录) |
|
|
71
|
+
|
|
72
|
+
任务状态机:
|
|
73
|
+
```
|
|
74
|
+
created → delivered → acked → running → replied
|
|
75
|
+
→ failed → retry → delivered
|
|
76
|
+
→ cancelled
|
|
77
|
+
delivered → expired (5min patrol)
|
|
78
|
+
delivered/acked/running → reassign → delivered (新agent)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 环境变量
|
|
82
|
+
|
|
83
|
+
| 变量 | 默认 | 说明 |
|
|
84
|
+
|------|------|------|
|
|
85
|
+
| `PORT` | 9200 | 监听端口 |
|
|
86
|
+
| `HOST` | 0.0.0.0 | 监听地址 |
|
|
87
|
+
| `COMMHUB_AUTH_TOKEN` | (无) | Bearer token 鉴权 |
|
|
88
|
+
| `COMMHUB_DB` | ~/.commhub/commhub.db | 数据库路径 |
|
|
89
|
+
|
|
90
|
+
## 鉴权
|
|
91
|
+
|
|
92
|
+
设置 `COMMHUB_AUTH_TOKEN` 后, 所有端点需要 Bearer token:
|
|
93
|
+
- Header: `Authorization: Bearer <token>`
|
|
94
|
+
- 或 Query: `?token=<token>`
|
|
95
|
+
- `/health` 不需要 auth
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -286,6 +286,24 @@ Bun.serve({
|
|
|
286
286
|
return withCors(req, Response.json({ ok: true, messages: rows }));
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
// ── REST: stats summary ──
|
|
290
|
+
if (url.pathname === "/api/stats") {
|
|
291
|
+
const taskStats = db.query<any, []>("SELECT status, COUNT(*) as count FROM tasks GROUP BY status").all();
|
|
292
|
+
const sessionStats = db.query<any, []>("SELECT status, COUNT(*) as count FROM sessions GROUP BY status").all();
|
|
293
|
+
const totalTasks = db.query<{ cnt: number }, []>("SELECT COUNT(*) as cnt FROM tasks").get();
|
|
294
|
+
const totalNodes = db.query<{ cnt: number }, []>("SELECT COUNT(*) as cnt FROM nodes").get();
|
|
295
|
+
const recentTasks = db.query<any, []>(
|
|
296
|
+
"SELECT task_id, from_name, to_name, status, created_at FROM tasks ORDER BY created_at DESC LIMIT 5"
|
|
297
|
+
).all();
|
|
298
|
+
return withCors(req, Response.json({
|
|
299
|
+
ok: true,
|
|
300
|
+
tasks: { total: totalTasks?.cnt || 0, by_status: taskStats },
|
|
301
|
+
sessions: { by_status: sessionStats },
|
|
302
|
+
nodes: { total: totalNodes?.cnt || 0 },
|
|
303
|
+
recent_tasks: recentTasks,
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
|
|
289
307
|
// ── REST: task events (V2 Sprint 2) ──
|
|
290
308
|
if (url.pathname === "/api/task_events") {
|
|
291
309
|
const taskId = url.searchParams.get("task_id");
|
|
@@ -330,7 +348,8 @@ Bun.serve({
|
|
|
330
348
|
params.push(limit);
|
|
331
349
|
|
|
332
350
|
const rows = db.query(sql).all(...params);
|
|
333
|
-
|
|
351
|
+
const stats = db.query<any, []>("SELECT status, COUNT(*) as count FROM tasks GROUP BY status").all();
|
|
352
|
+
return withCors(req, Response.json({ ok: true, tasks: rows, count: rows.length, stats }));
|
|
334
353
|
}
|
|
335
354
|
|
|
336
355
|
// ── REST: recent completions ──
|
package/src/tools.ts
CHANGED
|
@@ -565,6 +565,67 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
565
565
|
}
|
|
566
566
|
);
|
|
567
567
|
|
|
568
|
+
// ── V2: list_tasks (查询任务列表) ──
|
|
569
|
+
server.tool(
|
|
570
|
+
"list_tasks",
|
|
571
|
+
"List tasks with filters. Agents can query their own pending/running tasks.",
|
|
572
|
+
{
|
|
573
|
+
alias: z.string().max(200).optional().describe("Filter by to_name (target agent)"),
|
|
574
|
+
status: z.string().max(50).optional().describe("Filter by status"),
|
|
575
|
+
from_name: z.string().max(200).optional().describe("Filter by sender"),
|
|
576
|
+
limit: z.number().min(1).max(100).optional().default(20),
|
|
577
|
+
},
|
|
578
|
+
async ({ alias, status, from_name, limit }) => {
|
|
579
|
+
let sql = "SELECT task_id, from_name, to_name, priority, status, content, result, created_at, completed_at FROM tasks WHERE 1=1";
|
|
580
|
+
const params: any[] = [];
|
|
581
|
+
if (alias) { sql += ` AND to_name = ?${params.length + 1}`; params.push(alias); }
|
|
582
|
+
if (status) { sql += ` AND status = ?${params.length + 1}`; params.push(status); }
|
|
583
|
+
if (from_name) { sql += ` AND from_name = ?${params.length + 1}`; params.push(from_name); }
|
|
584
|
+
sql += ` ORDER BY created_at DESC LIMIT ?${params.length + 1}`;
|
|
585
|
+
params.push(limit);
|
|
586
|
+
const tasks = db.query(sql).all(...params);
|
|
587
|
+
|
|
588
|
+
// Stats
|
|
589
|
+
const stats = db.query<any, []>(
|
|
590
|
+
"SELECT status, COUNT(*) as count FROM tasks GROUP BY status"
|
|
591
|
+
).all();
|
|
592
|
+
|
|
593
|
+
return {
|
|
594
|
+
content: [{
|
|
595
|
+
type: "text" as const,
|
|
596
|
+
text: JSON.stringify({ ok: true, tasks, count: tasks.length, stats }),
|
|
597
|
+
}],
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
// ── V2: cancel_task (取消任务) ──
|
|
603
|
+
server.tool(
|
|
604
|
+
"cancel_task",
|
|
605
|
+
"Cancel a pending task. Works on delivered/acked/running tasks.",
|
|
606
|
+
{
|
|
607
|
+
task_id: z.string().min(1).max(200).describe("Task ID to cancel"),
|
|
608
|
+
reason: z.string().max(1000).optional().describe("Cancellation reason"),
|
|
609
|
+
from_session: z.string().max(200).optional().default("hub"),
|
|
610
|
+
},
|
|
611
|
+
async ({ task_id, reason, from_session }) => {
|
|
612
|
+
console.log(`[${ts()}] ${from_session} → cancel_task → ${task_id.slice(0, 8)}`);
|
|
613
|
+
const result = db.run(
|
|
614
|
+
`UPDATE tasks SET status = 'cancelled', result = ?1, completed_at = datetime('now')
|
|
615
|
+
WHERE task_id = ?2 AND status IN ('created', 'delivered', 'acked', 'running')`,
|
|
616
|
+
[reason || "cancelled by " + from_session, task_id]
|
|
617
|
+
);
|
|
618
|
+
// Also ack the inbox entry to prevent agent from picking it up
|
|
619
|
+
if (result.changes > 0) {
|
|
620
|
+
db.run("UPDATE inbox SET acked = 1 WHERE id = ?1 AND acked = 0", [task_id]);
|
|
621
|
+
logTaskEvent(task_id, null, "cancelled", from_session, reason || undefined);
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
content: [{ type: "text" as const, text: JSON.stringify({ ok: result.changes > 0, task_id, cancelled: result.changes > 0 }) }],
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
);
|
|
628
|
+
|
|
568
629
|
// ── V2: reassign_task (转移任务到另一个 agent) ──
|
|
569
630
|
server.tool(
|
|
570
631
|
"reassign_task",
|
|
@@ -584,6 +645,8 @@ export function registerTools(server: McpServer, clientIP?: string) {
|
|
|
584
645
|
const oldAlias = task.to_name;
|
|
585
646
|
try {
|
|
586
647
|
db.run("BEGIN IMMEDIATE");
|
|
648
|
+
// Ack old inbox to prevent original agent from picking it up
|
|
649
|
+
db.run("UPDATE inbox SET acked = 1 WHERE id = ?1 AND acked = 0", [task_id]);
|
|
587
650
|
db.run("UPDATE tasks SET to_name = ?1, status = 'delivered', started_at = NULL, delivered_at = datetime('now') WHERE task_id = ?2", [new_alias, task_id]);
|
|
588
651
|
const newInboxId = uuidv4();
|
|
589
652
|
db.run("INSERT INTO inbox (id, session_name, type, priority, content, from_session, requires_response) VALUES (?1, ?2, 'task', ?3, ?4, ?5, 'reply')",
|