@yuanchilin/dsh-mailbox 0.0.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/LICENSE +21 -0
- package/README.md +89 -0
- package/bin/mailbox.mjs +225 -0
- package/cordis.patch.yml +12 -0
- package/lib/client.js +110 -0
- package/lib/command.js +98 -0
- package/lib/core.js +441 -0
- package/lib/directory-parse.js +37 -0
- package/lib/index.js +319 -0
- package/lib/types/index.d.ts +123 -0
- package/lib/watcher.js +129 -0
- package/package.json +81 -0
- package/skill/README.md +117 -0
- package/skill/SKILL.md +96 -0
- package/skill/examples/handlers.patch.ps1 +154 -0
- package/skill/legacy-mcp.config.json +14 -0
- package/skill/mailbox.config.json +11 -0
- package/skill/mailbox.mjs +344 -0
- package/skill/mailbox.ps1 +218 -0
- package/skill/mailbox.psm1 +279 -0
- package/skill/self-test.ps1 +105 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// @yuanchilin/dsh-mailbox — DeepSeek Harness cordis 插件
|
|
3
|
+
//
|
|
4
|
+
// 注册 6 个模型面向工具 + 1 个宿主命令:
|
|
5
|
+
// mailbox_send 发送消息 (to=identity / 别名 / sessionId / all)
|
|
6
|
+
// mailbox_recv 读取新消息 (自动 seen 去重)
|
|
7
|
+
// mailbox_status 身份/目录/消息数 + 会话目录与在线状态
|
|
8
|
+
// mailbox_sessions 会话目录: 找"要对话的会话" (身份/别名/在线/未读)
|
|
9
|
+
// mailbox_alias 给本会话设置唯一别名 (便于他人定向发送)
|
|
10
|
+
// mailbox_clean 按 TTL 清理自己发过的旧消息
|
|
11
|
+
// /mailbox 宿主命令: /mailbox <目标|别名|all> <消息> (聊天框直发)
|
|
12
|
+
//
|
|
13
|
+
// 身份模型 (v1.1):
|
|
14
|
+
// - 默认按 <工作区名>-<会话短id> 自动派生, 同一工作区多个会话互不冲突,
|
|
15
|
+
// 工具执行时从 exec.agent.session 取 id / header.cwd / 标题。
|
|
16
|
+
// - 显式 config.identity 仍是最高优先级 (固定身份 / 旧配置兼容)。
|
|
17
|
+
// - 每次工具调用写注册表心跳 (<root>/_sessions/<sessionId>.json),
|
|
18
|
+
// 供 mailbox_sessions / status 展示在线状态与未读数。
|
|
19
|
+
//
|
|
20
|
+
// 配置 (cordis.patch.yml 的 mailbox 行 config, 或 profile patch 覆盖):
|
|
21
|
+
// identity(可选,留空自动), root(必填,共享目录), layout(root|dirs),
|
|
22
|
+
// dirs, participants, intervalSec, timeoutSec, seenFile, patchRoot,
|
|
23
|
+
// presenceWindowSec(在线判定窗口,默认 300)
|
|
24
|
+
//
|
|
25
|
+
// 长驻场景 (mailbox_wait 事件唤醒 / mailbox_poll 常驻轮询) 不适合做成工具,
|
|
26
|
+
// 请用包内 CLI: npx mailbox wait|poll (或 pwsh 版 mailbox.ps1), CLI 会写心跳。
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
import z from "@deepseek-ai/schemastery";
|
|
30
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
31
|
+
import * as core from "./core.js";
|
|
32
|
+
import { registerMailboxCommand } from "./command.js";
|
|
33
|
+
import { startMailboxWatcher } from "./watcher.js";
|
|
34
|
+
|
|
35
|
+
const name = "mailbox";
|
|
36
|
+
const inject = ["tools", "commands"];
|
|
37
|
+
|
|
38
|
+
/** schemastery 配置模式 (全部带默认值, 激活零配置; 收发前需配置 root) */
|
|
39
|
+
const Config = z.object({
|
|
40
|
+
identity: z.string().default(""),
|
|
41
|
+
layout: z.string().default("root"),
|
|
42
|
+
root: z.string().default(""),
|
|
43
|
+
dirs: z.dict(z.string()).default({}),
|
|
44
|
+
participants: z.array(z.string()).default([]),
|
|
45
|
+
intervalSec: z.number().default(2),
|
|
46
|
+
timeoutSec: z.number().default(0),
|
|
47
|
+
seenFile: z.string().default(""),
|
|
48
|
+
patchRoot: z.string().default(""),
|
|
49
|
+
presenceWindowSec: z.number().default(300),
|
|
50
|
+
watcher: z.boolean().default(true),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const text = (s) => [{ type: "text", text: s }];
|
|
54
|
+
|
|
55
|
+
/** 会话上下文: 每个工具调用都先解析身份 + 写心跳, 然后带着有效身份执行。 */
|
|
56
|
+
function withSession(cfg, exec, fn) {
|
|
57
|
+
const session = exec?.agent?.session;
|
|
58
|
+
const eff = core.effectiveConfig(cfg, session);
|
|
59
|
+
core.touchRegistry(cfg, session);
|
|
60
|
+
return fn(eff, session);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function apply(ctx, config) {
|
|
64
|
+
const cfg = core.resolveConfig(config ?? {});
|
|
65
|
+
|
|
66
|
+
// 宿主命令: /mailbox <目标|别名|all> <消息> (聊天输入框直接使用)
|
|
67
|
+
registerMailboxCommand(ctx, cfg);
|
|
68
|
+
|
|
69
|
+
// 内建 watcher: 新消息 → 为对应会话 agent 创建完成 job → DSH 唤醒。
|
|
70
|
+
// 随插件启动, dsh 重启自动复活 (根治 CLI wait job 重启即死的痛点)。
|
|
71
|
+
startMailboxWatcher(ctx, cfg);
|
|
72
|
+
|
|
73
|
+
ctx.tools.register(defineTool({
|
|
74
|
+
name: "mailbox_send",
|
|
75
|
+
description: "通过共享文件系统信箱向其他会话/agent 发送一条异步消息。to=参与者 identity / 别名 / 完整 sessionId 定向发送,或 all 广播;消息类型 request/response/notify/reply;对方不在线也不丢消息(对方之后 recv 或 CLI wait/poll 即可收到)。可用 mailbox_sessions 查看有哪些会话及其身份/别名。",
|
|
76
|
+
parameters: {
|
|
77
|
+
to: { type: "string", required: true, description: "接收方:identity(如 dsh-mailbox-17cbcfa0)/ 别名(如 rp)/ 完整 sessionId / all 广播" },
|
|
78
|
+
type: { type: "string", enum: ["request", "response", "notify", "reply"], description: "消息类型,默认 notify" },
|
|
79
|
+
topic: { type: "string", description: "消息主题,用于路由/归类(如 hello、patch_xxx)" },
|
|
80
|
+
payload: { type: "object", additionalProperties: true, description: "任意 JSON 负载" },
|
|
81
|
+
replyTo: { type: "string", description: "应答目标消息 id(请求-响应模式)" },
|
|
82
|
+
},
|
|
83
|
+
output: {
|
|
84
|
+
schema: {
|
|
85
|
+
type: "object",
|
|
86
|
+
additionalProperties: false,
|
|
87
|
+
properties: {
|
|
88
|
+
ok: { type: "boolean", required: true },
|
|
89
|
+
id: { type: "string", required: true },
|
|
90
|
+
from: { type: "string", required: true },
|
|
91
|
+
to: { type: "string", required: true },
|
|
92
|
+
toAlias: { type: "string" },
|
|
93
|
+
targetOnline: { oneOf: [{ type: "boolean" }, { type: "null" }] },
|
|
94
|
+
sessionsHint: { type: "string" },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
render: (_args, value) => {
|
|
98
|
+
const alias = value.toAlias ? ` (alias=${value.toAlias})` : "";
|
|
99
|
+
const online = value.targetOnline === null ? "" : value.targetOnline ? " 目标在线" : " 目标离线";
|
|
100
|
+
const hint = value.sessionsHint ? `\n\n${value.sessionsHint}` : "";
|
|
101
|
+
return text(`已发送 ${value.id} (${value.from} → ${value.to}${alias})${online}${hint}`);
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
execute: async (args, exec) => withSession(cfg, exec, (eff) => {
|
|
105
|
+
const target = core.resolveTarget(eff, args.to);
|
|
106
|
+
const id = core.sendMessage(eff, {
|
|
107
|
+
to: args.to,
|
|
108
|
+
type: args.type ?? "notify",
|
|
109
|
+
topic: args.topic ?? "",
|
|
110
|
+
payload: args.payload ?? {},
|
|
111
|
+
replyTo: args.replyTo ?? "",
|
|
112
|
+
});
|
|
113
|
+
const rec = core.listSessions(eff).find((s) => s.identity === target);
|
|
114
|
+
const known = core.isKnownTarget(eff, target);
|
|
115
|
+
return {
|
|
116
|
+
ok: true,
|
|
117
|
+
id,
|
|
118
|
+
from: eff.identity,
|
|
119
|
+
to: target,
|
|
120
|
+
toAlias: args.to !== target ? args.to : "",
|
|
121
|
+
targetOnline: rec ? core.isOnline(rec, eff.presenceWindowSec) : null,
|
|
122
|
+
sessionsHint: known ? "" : core.unknownTargetHint(eff, target),
|
|
123
|
+
};
|
|
124
|
+
}),
|
|
125
|
+
presentCall: (args) => ({
|
|
126
|
+
card: "generic",
|
|
127
|
+
title: `mailbox → ${args.to}`,
|
|
128
|
+
kind: "other",
|
|
129
|
+
rawInput: args,
|
|
130
|
+
}),
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
ctx.tools.register(defineTool({
|
|
134
|
+
name: "mailbox_recv",
|
|
135
|
+
description: "读取信箱中发给本会话的新消息(自动记录 seen,重复调用不会重复返回)。返回消息列表:from/to/type/topic/payload/reply_to。无新消息时返回空列表。长驻等待请用 CLI:npx mailbox wait。",
|
|
136
|
+
parameters: {
|
|
137
|
+
format: { type: "string", enum: ["table", "json"], description: "输出格式,默认 table" },
|
|
138
|
+
},
|
|
139
|
+
output: {
|
|
140
|
+
schema: {
|
|
141
|
+
type: "object",
|
|
142
|
+
additionalProperties: false,
|
|
143
|
+
properties: {
|
|
144
|
+
count: { type: "integer", required: true },
|
|
145
|
+
messages: {
|
|
146
|
+
type: "array",
|
|
147
|
+
required: true,
|
|
148
|
+
items: {
|
|
149
|
+
type: "object",
|
|
150
|
+
additionalProperties: true,
|
|
151
|
+
properties: {
|
|
152
|
+
id: { type: "string" },
|
|
153
|
+
from: { type: "string" },
|
|
154
|
+
to: { type: "string" },
|
|
155
|
+
type: { type: "string" },
|
|
156
|
+
topic: { type: "string" },
|
|
157
|
+
ts: { type: "integer" },
|
|
158
|
+
reply_to: { type: "string" },
|
|
159
|
+
payload: { type: "object", additionalProperties: true },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
render: (_args, value) => {
|
|
166
|
+
if (value.count === 0) return text("(无新消息)");
|
|
167
|
+
const lines = value.messages.map((m) => {
|
|
168
|
+
const p = m.payload && Object.keys(m.payload).length ? ` payload=${JSON.stringify(m.payload)}` : "";
|
|
169
|
+
return `[${m.from} -> ${m.to}] ${m.type} topic=${m.topic} id=${m.id}${m.reply_to ? ` reply_to=${m.reply_to}` : ""}${p}`;
|
|
170
|
+
});
|
|
171
|
+
return text(`新消息 ${value.count} 条:\n${lines.join("\n")}`);
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
execute: async (args, exec) => withSession(cfg, exec, (eff) => {
|
|
175
|
+
const messages = core.recvNew(eff, true);
|
|
176
|
+
return { count: messages.length, messages };
|
|
177
|
+
}),
|
|
178
|
+
presentCall: () => ({ card: "generic", title: "mailbox recv", kind: "other" }),
|
|
179
|
+
}));
|
|
180
|
+
|
|
181
|
+
ctx.tools.register(defineTool({
|
|
182
|
+
name: "mailbox_status",
|
|
183
|
+
description: "查看信箱配置与状态:本会话身份、写入目录、已发送消息数、seen 记录数、各对方信箱的消息数与未读,以及会话目录(各已注册会话的身份/别名/在线状态)。用于确认信箱是否配置好、对方是否在线。",
|
|
184
|
+
parameters: {},
|
|
185
|
+
output: {
|
|
186
|
+
schema: {
|
|
187
|
+
type: "object",
|
|
188
|
+
additionalProperties: true,
|
|
189
|
+
properties: {
|
|
190
|
+
identity: { type: "string" },
|
|
191
|
+
layout: { type: "string" },
|
|
192
|
+
outDir: { type: "string" },
|
|
193
|
+
outCount: { type: "integer" },
|
|
194
|
+
seen: { type: "integer" },
|
|
195
|
+
inboxes: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
196
|
+
sessions: { type: "array", items: { type: "object", additionalProperties: true } },
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
render: (_args, value) => {
|
|
200
|
+
const lines = [
|
|
201
|
+
`身份: ${value.identity} layout=${value.layout}`,
|
|
202
|
+
`写: ${value.outDir} (消息 ${value.outCount})`,
|
|
203
|
+
`seen: ${value.seen} 条`,
|
|
204
|
+
...value.inboxes.map((i) => `读: ${i.name || i.dir} (消息 ${i.msgCount}, 未读 ${i.unread ?? 0})`),
|
|
205
|
+
];
|
|
206
|
+
const sessions = value.sessions || [];
|
|
207
|
+
if (sessions.length > 0) {
|
|
208
|
+
lines.push("会话目录:");
|
|
209
|
+
for (const s of sessions) {
|
|
210
|
+
const on = s.online ? "●在线" : "○离线";
|
|
211
|
+
lines.push(` ${on} ${s.identity}${s.alias ? ` (${s.alias})` : ""} ${s.workspace || "?"}${s.title ? ` «${s.title}»` : ""}${s.unread ? ` 未读${s.unread}` : ""}`);
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
lines.push("会话目录: (暂无注册会话, 各会话调用一次 mailbox 工具即登记)");
|
|
215
|
+
}
|
|
216
|
+
return text(lines.join("\n"));
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
execute: async (_args, exec) => withSession(cfg, exec, (eff) => core.statusOf(eff)),
|
|
220
|
+
presentCall: () => ({ card: "generic", title: "mailbox status", kind: "other" }),
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
ctx.tools.register(defineTool({
|
|
224
|
+
name: "mailbox_sessions",
|
|
225
|
+
description: "会话目录:列出所有已注册会话(identity/别名/工作区/标题/在线状态/发给我未读条数),按最近活跃排序。用于快速找到要对话的会话:先看目录确定对方的 identity 或别名,再 mailbox_send 定向发送。",
|
|
226
|
+
parameters: {},
|
|
227
|
+
output: {
|
|
228
|
+
schema: {
|
|
229
|
+
type: "object",
|
|
230
|
+
additionalProperties: false,
|
|
231
|
+
properties: {
|
|
232
|
+
count: { type: "integer", required: true },
|
|
233
|
+
sessions: {
|
|
234
|
+
type: "array",
|
|
235
|
+
required: true,
|
|
236
|
+
items: { type: "object", additionalProperties: true },
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
render: (_args, value) => {
|
|
241
|
+
if (value.count === 0) return text("(暂无注册会话: 各会话调用一次 mailbox 工具即自动登记)");
|
|
242
|
+
const lines = value.sessions.map((s) => {
|
|
243
|
+
const on = s.online ? "●在线" : "○离线";
|
|
244
|
+
return `${on} ${s.identity}${s.alias ? ` (alias=${s.alias})` : ""} ${s.workspace || "?"}${s.title ? ` «${s.title}»` : ""}${s.unread ? ` 发给我的未读:${s.unread}` : ""}`;
|
|
245
|
+
});
|
|
246
|
+
return text(`会话 ${value.count} 个:\n${lines.join("\n")}`);
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
execute: async (_args, exec) => withSession(cfg, exec, (eff) => {
|
|
250
|
+
const sessions = core.listSessions(eff).map((s) => ({
|
|
251
|
+
...s,
|
|
252
|
+
online: core.isOnline(s, eff.presenceWindowSec),
|
|
253
|
+
unread: core.unreadFrom(eff, s.identity),
|
|
254
|
+
}));
|
|
255
|
+
return { count: sessions.length, sessions };
|
|
256
|
+
}),
|
|
257
|
+
presentCall: () => ({ card: "generic", title: "mailbox sessions", kind: "other" }),
|
|
258
|
+
}));
|
|
259
|
+
|
|
260
|
+
ctx.tools.register(defineTool({
|
|
261
|
+
name: "mailbox_alias",
|
|
262
|
+
description: "给本会话设置一个简短唯一别名(全库检查,已被其他会话占用会拒绝),便于其他会话用 alias 定向发消息,而不是记长 identity。alias 仅允许字母数字 . _ -(≤32 字符)。",
|
|
263
|
+
parameters: {
|
|
264
|
+
alias: { type: "string", required: true, description: "别名,例如 rp / mcp / builder(全库唯一)" },
|
|
265
|
+
},
|
|
266
|
+
output: {
|
|
267
|
+
schema: {
|
|
268
|
+
type: "object",
|
|
269
|
+
additionalProperties: false,
|
|
270
|
+
properties: {
|
|
271
|
+
alias: { type: "string", required: true },
|
|
272
|
+
identity: { type: "string", required: true },
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
render: (_args, value) => text(`别名已设置: ${value.identity} → "${value.alias}"`),
|
|
276
|
+
},
|
|
277
|
+
execute: async (args, exec) => withSession(cfg, exec, (eff, session) => {
|
|
278
|
+
const rec = core.setAlias(eff, session, args.alias);
|
|
279
|
+
return { alias: rec.alias, identity: rec.identity };
|
|
280
|
+
}),
|
|
281
|
+
presentCall: (args) => ({
|
|
282
|
+
card: "generic",
|
|
283
|
+
title: `mailbox alias → ${args.alias}`,
|
|
284
|
+
kind: "other",
|
|
285
|
+
rawInput: args,
|
|
286
|
+
}),
|
|
287
|
+
}));
|
|
288
|
+
|
|
289
|
+
ctx.tools.register(defineTool({
|
|
290
|
+
name: "mailbox_clean",
|
|
291
|
+
description: "按 TTL 清理本会话自己发过的旧消息(对方应已读过)。dryRun 只统计不删除。",
|
|
292
|
+
parameters: {
|
|
293
|
+
ttlHours: { type: "integer", description: "保留时长(小时),默认 24" },
|
|
294
|
+
dryRun: { type: "boolean", description: "只统计不删除,默认 false" },
|
|
295
|
+
},
|
|
296
|
+
output: {
|
|
297
|
+
schema: {
|
|
298
|
+
type: "object",
|
|
299
|
+
additionalProperties: false,
|
|
300
|
+
properties: {
|
|
301
|
+
removed: { type: "integer", required: true },
|
|
302
|
+
dryRun: { type: "boolean", required: true },
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
render: (_args, value) =>
|
|
306
|
+
text(`clean: ${value.dryRun ? "dry-run" : "已删除"} ${value.removed} 条过期消息`),
|
|
307
|
+
},
|
|
308
|
+
execute: async (args, exec) => withSession(cfg, exec, (eff) => {
|
|
309
|
+
const removed = core.cleanTTL(eff, {
|
|
310
|
+
ttlHours: args.ttlHours ?? 24,
|
|
311
|
+
dryRun: args.dryRun ?? false,
|
|
312
|
+
});
|
|
313
|
+
return { removed, dryRun: args.dryRun ?? false };
|
|
314
|
+
}),
|
|
315
|
+
presentCall: () => ({ card: "generic", title: "mailbox clean", kind: "other" }),
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export { Config, apply, inject, name };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// 最小类型声明 (core / command / watcher / directory-parse 导出的形状)
|
|
2
|
+
export interface MailboxConfig {
|
|
3
|
+
identity: string;
|
|
4
|
+
layout: "root" | "dirs";
|
|
5
|
+
root: string;
|
|
6
|
+
dirs: Record<string, string>;
|
|
7
|
+
participants: string[];
|
|
8
|
+
intervalSec: number;
|
|
9
|
+
timeoutSec: number;
|
|
10
|
+
seenFile: string;
|
|
11
|
+
patchRoot: string;
|
|
12
|
+
presenceWindowSec: number;
|
|
13
|
+
watcher: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface MailboxMessage {
|
|
17
|
+
id: string;
|
|
18
|
+
from: string;
|
|
19
|
+
to: string;
|
|
20
|
+
type: "request" | "response" | "notify" | "reply";
|
|
21
|
+
topic: string;
|
|
22
|
+
payload: unknown;
|
|
23
|
+
ts: number;
|
|
24
|
+
reply_to: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MailboxInbox {
|
|
28
|
+
dir: string;
|
|
29
|
+
name: string;
|
|
30
|
+
msgCount: number;
|
|
31
|
+
unread: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface MailboxSession {
|
|
35
|
+
sessionId: string;
|
|
36
|
+
identity: string;
|
|
37
|
+
alias: string;
|
|
38
|
+
workspace: string;
|
|
39
|
+
title: string;
|
|
40
|
+
firstSeen: number;
|
|
41
|
+
lastSeen: number;
|
|
42
|
+
online?: boolean;
|
|
43
|
+
unread?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface MailboxStatus {
|
|
47
|
+
identity: string;
|
|
48
|
+
layout: string;
|
|
49
|
+
outDir: string;
|
|
50
|
+
outCount: number;
|
|
51
|
+
seen: number;
|
|
52
|
+
inboxes: MailboxInbox[];
|
|
53
|
+
sessions: MailboxSession[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** DSH Session 对象的最小形状 (工具执行时 exec.agent.session)。 */
|
|
57
|
+
export interface DshSessionLike {
|
|
58
|
+
id: string;
|
|
59
|
+
header?: { cwd?: string };
|
|
60
|
+
events?: Array<{ type?: string; data?: { title?: string } }>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resolveConfig(partial?: Partial<MailboxConfig>, env?: NodeJS.ProcessEnv): MailboxConfig;
|
|
64
|
+
export function resolveDirs(cfg: MailboxConfig): { out: string; in: string[] };
|
|
65
|
+
export function seenFileOf(cfg: MailboxConfig): string;
|
|
66
|
+
export function loadSeen(cfg: MailboxConfig): string[];
|
|
67
|
+
export function saveSeen(cfg: MailboxConfig, seen: string[]): void;
|
|
68
|
+
export function sendMessage(cfg: MailboxConfig, msg: Partial<Pick<MailboxMessage, "to" | "type" | "topic" | "payload" | "reply_to">>): string;
|
|
69
|
+
export function recvNew(cfg: MailboxConfig, markSeen?: boolean): MailboxMessage[];
|
|
70
|
+
export function removeMessage(cfg: MailboxConfig, id: string, inbox?: boolean): boolean;
|
|
71
|
+
export function cleanTTL(cfg: MailboxConfig, opts?: { ttlHours?: number; dryRun?: boolean }): number;
|
|
72
|
+
export function statusOf(cfg: MailboxConfig): MailboxStatus;
|
|
73
|
+
export function assertUsable(cfg: MailboxConfig): void;
|
|
74
|
+
|
|
75
|
+
// ---- 会话身份 + 注册表 ----
|
|
76
|
+
export function sessionCtx(session?: DshSessionLike | null): { sessionId: string; workspace: string; title: string } | undefined;
|
|
77
|
+
export function deriveIdentity(ctx?: { sessionId: string; workspace: string; title: string }): string;
|
|
78
|
+
export function effectiveIdentity(cfg: MailboxConfig, session?: DshSessionLike | null): string;
|
|
79
|
+
export function effectiveConfig(cfg: MailboxConfig, session?: DshSessionLike | null): MailboxConfig;
|
|
80
|
+
export function registryDir(cfg: MailboxConfig): string;
|
|
81
|
+
export function touchRegistry(cfg: MailboxConfig, session?: DshSessionLike | null, opts?: { alias?: string }): MailboxSession | undefined;
|
|
82
|
+
export function touchRegistryCli(cfg: MailboxConfig, opts?: { workspace?: string }): MailboxSession | undefined;
|
|
83
|
+
export function listSessions(cfg: MailboxConfig): MailboxSession[];
|
|
84
|
+
export function isOnline(rec?: MailboxSession | null, windowSec?: number): boolean;
|
|
85
|
+
export function setAlias(cfg: MailboxConfig, session: DshSessionLike, alias: string): MailboxSession;
|
|
86
|
+
export function resolveTarget(cfg: MailboxConfig, to: string): string;
|
|
87
|
+
export function isKnownTarget(cfg: MailboxConfig, target: string): boolean;
|
|
88
|
+
export function sessionDirectoryText(cfg: MailboxConfig): string;
|
|
89
|
+
export function unknownTargetHint(cfg: MailboxConfig, target: string): string;
|
|
90
|
+
export function unreadInDir(cfg: MailboxConfig, dir: string): number;
|
|
91
|
+
export function unreadFrom(cfg: MailboxConfig, identity: string): number;
|
|
92
|
+
|
|
93
|
+
// ---- 宿主 /mailbox 命令 ----
|
|
94
|
+
export interface MailboxCommandResult {
|
|
95
|
+
kind: "success" | "error";
|
|
96
|
+
text: string;
|
|
97
|
+
}
|
|
98
|
+
export interface MailboxInvocation {
|
|
99
|
+
agent?: { session?: DshSessionLike };
|
|
100
|
+
rawInput: string;
|
|
101
|
+
}
|
|
102
|
+
export const MAILBOX_USAGE: string;
|
|
103
|
+
export function parseMailboxCommand(rawInput: string):
|
|
104
|
+
| { kind: "usage" }
|
|
105
|
+
| { kind: "list" }
|
|
106
|
+
| { kind: "recv" }
|
|
107
|
+
| { kind: "no-message"; to: string }
|
|
108
|
+
| { kind: "send"; to: string; message: string };
|
|
109
|
+
export function renderDirectory(cfg: MailboxConfig): string;
|
|
110
|
+
export function executeMailboxCommand(ctx: unknown, cfg: MailboxConfig, invocation: MailboxInvocation): MailboxCommandResult;
|
|
111
|
+
export function registerMailboxCommand(ctx: { commands: { register(definition: unknown): unknown } }, cfg: MailboxConfig): void;
|
|
112
|
+
|
|
113
|
+
// ---- 内建 watcher ----
|
|
114
|
+
export function scanMailboxRoot(root: string, known: Set<string>, live: Map<string, unknown>): Map<string, MailboxMessage[]>;
|
|
115
|
+
export function startMailboxWatcher(ctx: unknown, cfg: MailboxConfig): (() => void) | undefined;
|
|
116
|
+
|
|
117
|
+
// ---- 目录文本解析 (客户端补全) ----
|
|
118
|
+
export interface DirectoryOption {
|
|
119
|
+
id: string;
|
|
120
|
+
label: string;
|
|
121
|
+
detail: string;
|
|
122
|
+
}
|
|
123
|
+
export function parseDirectory(text: string): DirectoryOption[];
|
package/lib/watcher.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// @yuanchilin/dsh-mailbox — 插件内建 watcher (根治"重启杀监听")
|
|
3
|
+
//
|
|
4
|
+
// 背景: 用 CLI `wait` 挂后台 job 的监听属于会话进程, dsh 一重启就死。
|
|
5
|
+
// 方案: 监听做进插件本身 —— 插件每次启动 apply() 都会自动重启 watcher,
|
|
6
|
+
// 不依赖会话进程。轮询 <root>/ 各参与者目录, 发现发给"在线会话身份"
|
|
7
|
+
// 的新消息, 为该会话的 agent 创建一个立即完成的后台 job —— DSH 的
|
|
8
|
+
// 后台 job 完成通知会唤醒该 agent (与 CLI wait 完成唤醒同机制)。
|
|
9
|
+
//
|
|
10
|
+
// 去重: 收件人 seen 文件里已有的消息不重复唤醒 (已处理不打扰);
|
|
11
|
+
// 本进程内 known 集合保证同一文件只唤醒一次。
|
|
12
|
+
//
|
|
13
|
+
// 安全: jobs/agents 服务缺失时静默降级 (不影响其他 profile 启动);
|
|
14
|
+
// 任何轮询异常吞掉下轮重试, 绝不抛向启动流程。
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
import { readdirSync, readFileSync, existsSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import * as core from "./core.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 纯检测逻辑: 扫描 root 下参与者目录 (跳过 _/. 前缀), 找出 live 身份未 seen
|
|
23
|
+
* 的新消息。known (Set<绝对路径>) 就地更新; 返回 Map<identity, msg[]>。
|
|
24
|
+
*/
|
|
25
|
+
export function scanMailboxRoot(root, known, live) {
|
|
26
|
+
const fresh = new Map();
|
|
27
|
+
if (!existsSync(root)) return fresh;
|
|
28
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
29
|
+
if (!entry.isDirectory() || entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
|
|
30
|
+
const dirPath = join(root, entry.name);
|
|
31
|
+
let files;
|
|
32
|
+
try {
|
|
33
|
+
files = readdirSync(dirPath);
|
|
34
|
+
} catch {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
for (const f of files) {
|
|
38
|
+
if (!f.startsWith("msg_") || !f.endsWith(".json")) continue;
|
|
39
|
+
const filePath = join(dirPath, f);
|
|
40
|
+
if (known.has(filePath)) continue;
|
|
41
|
+
known.add(filePath);
|
|
42
|
+
let m;
|
|
43
|
+
try {
|
|
44
|
+
m = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
45
|
+
} catch {
|
|
46
|
+
continue; // 损坏消息跳过
|
|
47
|
+
}
|
|
48
|
+
for (const identity of live.keys()) {
|
|
49
|
+
if (m.to !== identity && m.to !== "all") continue;
|
|
50
|
+
// 自收消息 (from=自己): recv 只扫别人目录, 永远读不到 → 不唤醒 (否则每次重启重复响铃)
|
|
51
|
+
if (m.from === identity) continue;
|
|
52
|
+
// 收件人已处理 (seen 含该 id) 则不重复唤醒
|
|
53
|
+
const seenPath = join(root, identity, ".seen.json");
|
|
54
|
+
if (existsSync(seenPath)) {
|
|
55
|
+
try {
|
|
56
|
+
const v = JSON.parse(readFileSync(seenPath, "utf-8"));
|
|
57
|
+
const seen = Array.isArray(v) ? v : [v];
|
|
58
|
+
if (seen.includes(m.id)) continue;
|
|
59
|
+
} catch {
|
|
60
|
+
// seen 损坏则视为未处理, 照常唤醒
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!fresh.has(identity)) fresh.set(identity, []);
|
|
64
|
+
fresh.get(identity).push(m);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return fresh;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 启动内建 watcher。返回停止函数 (或 undefined = 未启用/环境不支持)。
|
|
73
|
+
* jobs/agents 为惰性获取: 缺失即静默不启用 (web 面两者都存在)。
|
|
74
|
+
*/
|
|
75
|
+
export function startMailboxWatcher(ctx, cfg) {
|
|
76
|
+
if (cfg.watcher === false || cfg.layout !== "root" || !cfg.root) return undefined;
|
|
77
|
+
const jobs = ctx.get("jobs");
|
|
78
|
+
const agents = ctx.get("agents");
|
|
79
|
+
if (!jobs || typeof jobs.start !== "function") return undefined;
|
|
80
|
+
if (!agents || typeof agents.list !== "function") return undefined;
|
|
81
|
+
|
|
82
|
+
const known = new Set();
|
|
83
|
+
|
|
84
|
+
const tick = () => {
|
|
85
|
+
try {
|
|
86
|
+
const live = new Map(); // identity → agent
|
|
87
|
+
for (const agent of agents.list()) {
|
|
88
|
+
try {
|
|
89
|
+
const identity = core.effectiveIdentity(cfg, agent.session);
|
|
90
|
+
if (identity) live.set(identity, agent);
|
|
91
|
+
} catch {
|
|
92
|
+
// 该 agent 无会话/身份, 跳过
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (live.size === 0) return;
|
|
96
|
+
const fresh = scanMailboxRoot(cfg.root, known, live);
|
|
97
|
+
for (const [identity, messages] of fresh) {
|
|
98
|
+
const agent = live.get(identity);
|
|
99
|
+
for (const m of messages) {
|
|
100
|
+
try {
|
|
101
|
+
jobs.start({
|
|
102
|
+
kind: "mailbox",
|
|
103
|
+
label: `mailbox 新消息: [${m.from} -> ${m.to}] ${m.topic || "(无主题)"}`,
|
|
104
|
+
owner: agent,
|
|
105
|
+
run: () => ({
|
|
106
|
+
cancel: () => {},
|
|
107
|
+
done: Promise.resolve({
|
|
108
|
+
status: "completed",
|
|
109
|
+
detail: `[${m.from} -> ${m.to}] ${m.topic || ""}`,
|
|
110
|
+
output: `收到新消息 id=${m.id} from=${m.from} topic=${m.topic || ""}\n调用 mailbox_recv 读取处理。`,
|
|
111
|
+
}),
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
} catch {
|
|
115
|
+
// 单个唤醒失败不影响其他 (如 kind 不被 jobs 实现接受)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// 轮询异常静默, 下轮重试
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
tick(); // 启动即扫一遍 (重启后立刻发现遗留未读)
|
|
125
|
+
const handle = setInterval(tick, (cfg.intervalSec || 2) * 1000);
|
|
126
|
+
const stop = () => clearInterval(handle);
|
|
127
|
+
if (typeof ctx.effect === "function") ctx.effect(() => stop);
|
|
128
|
+
return stop;
|
|
129
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yuanchilin/dsh-mailbox",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "DeepSeek Harness cross-session file mailbox: send/recv/status tools over a shared filesystem mailbox (N participants, directed & broadcast routing, seen dedup, TTL cleanup, session directory & presence), a /mailbox slash command with popup completion, a plugin-native wake watcher, a zero-dependency node CLI and a packaged DSH skill.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/types/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": {
|
|
14
|
+
"default": "./lib/client.js"
|
|
15
|
+
},
|
|
16
|
+
"./src/*": "./src/*",
|
|
17
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"mailbox": "bin/mailbox.mjs"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"lib/index.js",
|
|
25
|
+
"lib/core.js",
|
|
26
|
+
"lib/command.js",
|
|
27
|
+
"lib/watcher.js",
|
|
28
|
+
"lib/directory-parse.js",
|
|
29
|
+
"lib/client.js",
|
|
30
|
+
"lib/types/index.d.ts",
|
|
31
|
+
"bin/",
|
|
32
|
+
"cordis.patch.yml",
|
|
33
|
+
"skill/",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "node scripts/build.mjs",
|
|
39
|
+
"watch:client": "tsdown --watch",
|
|
40
|
+
"test": "node --test --test-isolation=none \"test/*.test.js\""
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"deepseek-harness",
|
|
44
|
+
"dsh",
|
|
45
|
+
"mailbox",
|
|
46
|
+
"cross-session",
|
|
47
|
+
"messaging",
|
|
48
|
+
"plugin",
|
|
49
|
+
"client"
|
|
50
|
+
],
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"dsh": {
|
|
53
|
+
"bundle": {
|
|
54
|
+
"patch": "./cordis.patch.yml"
|
|
55
|
+
},
|
|
56
|
+
"client": {
|
|
57
|
+
"inject": [
|
|
58
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-commands",
|
|
60
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
61
|
+
],
|
|
62
|
+
"platform": "web"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
70
|
+
"tsdown": "^0.22.2",
|
|
71
|
+
"typescript": "^6.0.3"
|
|
72
|
+
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
75
|
+
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
|
76
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6"
|
|
77
|
+
},
|
|
78
|
+
"engines": {
|
|
79
|
+
"node": ">=22.0.0"
|
|
80
|
+
}
|
|
81
|
+
}
|