@yuanchilin/dsh-mailbox 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 yuanchilin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @yuanchilin/dsh-mailbox
2
+
3
+ DeepSeek Harness 跨会话文件信箱插件:通过共享文件系统在任意会话/agent 之间异步收发消息。
4
+
5
+ - **DSH 工具**:`mailbox_send` / `mailbox_recv` / `mailbox_status` / `mailbox_clean`
6
+ - **Node CLI**(零依赖):`npx @yuanchilin/dsh-mailbox send|recv|wait|poll|clean|status|init`
7
+ - **pwsh 模块**:随 `skill/` 打包,供需要 PowerShell 的会话使用
8
+ - **DSH skill**:`skill/SKILL.md`,安装后可复制到 `~/.dsh/skills/mailbox/` 供所有会话加载
9
+
10
+ ## 安装为 DSH 插件
11
+
12
+ ```sh
13
+ dsh plugin --profile web add @yuanchilin/dsh-mailbox
14
+ # 然后重启 dsh web
15
+ ```
16
+
17
+ 配置信箱(在 profile 的 `cordis.patch.yml` 中覆盖):
18
+
19
+ ```yaml
20
+ - id: mailbox
21
+ config:
22
+ identity: agent-a # 本会话身份
23
+ root: D:/Downloads/Agent/.mailbox # 共享根目录 (layout=root, 每人一个子目录)
24
+ ```
25
+
26
+ `layout=dirs`(旧 mcp/RP 双目录兼容):提供 `dirs: { "<id>": "<目录>" }`。
27
+
28
+ ## 使用
29
+
30
+ ```text
31
+ mailbox_send { to: "agent-b" | "all", type: "notify|request|response|reply",
32
+ topic: "hello", payload: {...}, replyTo: "<msg-id>" }
33
+ mailbox_recv {} → 返回新消息列表 (自动 seen 去重)
34
+ mailbox_status {} → 身份/目录/消息数
35
+ mailbox_clean { ttlHours: 24, dryRun: false }
36
+ ```
37
+
38
+ 长驻场景(事件唤醒 / 常驻轮询)不适合做成工具,用 CLI 挂后台 job:
39
+
40
+ ```bash
41
+ npx @yuanchilin/dsh-mailbox wait --timeout 600 # 新消息即 exit 0 (DSH 唤醒)
42
+ npx @yuanchilin/dsh-mailbox poll --interval 2 # 常驻, request 自动回 response
43
+ ```
44
+
45
+ ## 协议
46
+
47
+ - 消息文件:`msg_<id>.json`,内容 `{ id, from, to, type, topic, payload, ts, reply_to }`
48
+ - 路由:`to=<id>` 定向 / `to=all` 广播(写一份,各人自取)
49
+ - seen 去重:每参与者独立 `<outDir>/.seen.json`
50
+ - 配置优先级:CLI 参数 > 环境变量(`MAILBOX_CONFIG/ID/ROOT/INTERVAL/TIMEOUT`)> 配置文件 > 默认
51
+
52
+ ## 目录
53
+
54
+ ```
55
+ lib/core.js # 核心逻辑 (可独立 import)
56
+ lib/index.js # cordis 插件 (注册 4 个工具)
57
+ bin/mailbox.mjs # node CLI
58
+ cordis.patch.yml # bundle patch (dsh.bundle 声明)
59
+ skill/ # DSH skill (SKILL.md + pwsh 版工具)
60
+ test/ # node:test 测试
61
+ ```
62
+
63
+ ## 测试
64
+
65
+ ```sh
66
+ npm test
67
+ ```
68
+
69
+ MIT License
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ // ============================================================================
3
+ // @yuanchilin/dsh-mailbox — node CLI (薄封装, 复用 lib/core.js)
4
+ //
5
+ // node bin/mailbox.mjs send --to agent-b --topic hello --payload '{"x":1}'
6
+ // node bin/mailbox.mjs recv --format json
7
+ // node bin/mailbox.mjs wait --timeout 600 # 新消息即 exit 0 (唤醒)
8
+ // node bin/mailbox.mjs poll --interval 2 # 常驻 (request→echo)
9
+ // node bin/mailbox.mjs clean --ttl-hours 24 --dry-run
10
+ // node bin/mailbox.mjs status
11
+ // node bin/mailbox.mjs init --id agent-a --root D:/Downloads/Agent/.mailbox
12
+ //
13
+ // 配置优先级: 参数 > 环境变量 (MAILBOX_CONFIG/ID/ROOT/INTERVAL/TIMEOUT) > 配置文件 > 默认
14
+ // ============================================================================
15
+
16
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
17
+ import { join, dirname } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import * as core from "../lib/core.js";
20
+
21
+ const __dirname = dirname(fileURLToPath(import.meta.url));
22
+ const DEFAULT_CONFIG = join(__dirname, "..", "mailbox.config.json");
23
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
24
+
25
+ function parseArgs(argv) {
26
+ const args = { _: [] };
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const a = argv[i];
29
+ if (a.startsWith("--")) {
30
+ const key = a.slice(2);
31
+ if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) args[key] = argv[++i];
32
+ else args[key] = true;
33
+ } else {
34
+ args._.push(a);
35
+ }
36
+ }
37
+ return args;
38
+ }
39
+
40
+ function getConfig(args) {
41
+ let fileCfg = {};
42
+ const configPath = args.config || process.env.MAILBOX_CONFIG || DEFAULT_CONFIG;
43
+ if (existsSync(configPath)) {
44
+ try {
45
+ fileCfg = JSON.parse(readFileSync(configPath, "utf-8"));
46
+ } catch (e) {
47
+ console.warn(`[mailbox] 读取配置失败: ${configPath} (${e.message})`);
48
+ }
49
+ }
50
+ const cfg = core.resolveConfig({ ...fileCfg }, process.env);
51
+ if (args.identity) cfg.identity = args.identity;
52
+ if (args.root) { cfg.root = args.root; cfg.layout = "root"; }
53
+ if (args.interval !== undefined) cfg.intervalSec = Number(args.interval);
54
+ if (args.timeout !== undefined) cfg.timeoutSec = Number(args.timeout);
55
+ return { cfg, configPath };
56
+ }
57
+
58
+ const commands = {
59
+ init(args, cfg, configPath) {
60
+ const out = {
61
+ identity: cfg.identity,
62
+ layout: cfg.layout,
63
+ root: cfg.root,
64
+ dirs: cfg.dirs,
65
+ participants: cfg.participants,
66
+ intervalSec: cfg.intervalSec,
67
+ timeoutSec: cfg.timeoutSec,
68
+ seenFile: cfg.seenFile,
69
+ patchRoot: cfg.patchRoot,
70
+ };
71
+ writeFileSync(configPath, JSON.stringify(out, null, 2) + "\n", "utf-8");
72
+ console.log(`已生成配置: ${configPath}`);
73
+ },
74
+
75
+ send(args, cfg) {
76
+ if (!args.to) throw new Error("send 需要 --to <id|all>");
77
+ let payload = {};
78
+ if (args.payload) {
79
+ try { payload = JSON.parse(args.payload); } catch { throw new Error(`payload 不是合法 JSON: ${args.payload}`); }
80
+ }
81
+ core.assertUsable(cfg);
82
+ const id = core.sendMessage(cfg, {
83
+ to: args.to,
84
+ type: args.type || "notify",
85
+ topic: args.topic || "",
86
+ payload,
87
+ replyTo: args.replyTo || "",
88
+ });
89
+ console.log(`sent ${id} (${new Date().toTimeString().slice(0, 8)})`);
90
+ },
91
+
92
+ recv(args, cfg) {
93
+ core.assertUsable(cfg);
94
+ const msgs = core.recvNew(cfg, true);
95
+ if (msgs.length === 0) { console.log("(无新消息)"); return; }
96
+ if (args.format === "json") {
97
+ for (const m of msgs) console.log(JSON.stringify(m));
98
+ } else {
99
+ for (const m of msgs) {
100
+ const p = m.payload && Object.keys(m.payload).length ? ` payload=${JSON.stringify(m.payload)}` : "";
101
+ console.log(`[${m.from} -> ${m.to}] ${m.type} topic=${m.topic} id=${m.id}${m.reply_to ? ` reply_to=${m.reply_to}` : ""}${p}`);
102
+ }
103
+ }
104
+ },
105
+
106
+ async wait(args, cfg) {
107
+ core.assertUsable(cfg);
108
+ const started = Date.now();
109
+ for (;;) {
110
+ const msgs = core.recvNew(cfg, true);
111
+ if (msgs.length > 0) {
112
+ console.log(`=== NEW MESSAGES: ${msgs.length} ===`);
113
+ for (const m of msgs) console.log(JSON.stringify(m));
114
+ console.log("=== WAKE-UP (exit 0) ===");
115
+ process.exit(0);
116
+ }
117
+ if (cfg.timeoutSec > 0 && (Date.now() - started) / 1000 >= cfg.timeoutSec) {
118
+ console.log(`TIMEOUT after ${cfg.timeoutSec}s, no new messages`);
119
+ process.exit(0);
120
+ }
121
+ await sleep(cfg.intervalSec * 1000);
122
+ }
123
+ },
124
+
125
+ async poll(args, cfg) {
126
+ core.assertUsable(cfg);
127
+ console.log(`poll 启动 (identity=${cfg.identity} 每 ${cfg.intervalSec}s). Ctrl+C 退出`);
128
+ for (;;) {
129
+ try {
130
+ for (const m of core.recvNew(cfg, true)) {
131
+ console.log(`[收到] from=${m.from} type=${m.type} topic=${m.topic} id=${m.id}`);
132
+ if (m.type === "request") {
133
+ core.sendMessage(cfg, {
134
+ to: m.from,
135
+ type: "response",
136
+ topic: m.topic,
137
+ payload: { echo: m.payload, from: cfg.identity },
138
+ replyTo: m.id,
139
+ });
140
+ console.log(` → 已回 response (reply_to=${m.id})`);
141
+ } else {
142
+ console.log(JSON.stringify(m));
143
+ }
144
+ try { core.removeMessage(cfg, m.id, true); } catch { /* 权限不足则跳过 */ }
145
+ }
146
+ } catch (e) {
147
+ console.warn(`轮询异常: ${e.message}`);
148
+ }
149
+ await sleep(cfg.intervalSec * 1000);
150
+ }
151
+ },
152
+
153
+ clean(args, cfg) {
154
+ core.assertUsable(cfg);
155
+ const removed = core.cleanTTL(cfg, {
156
+ ttlHours: args.ttlHours !== undefined ? Number(args.ttlHours) : 24,
157
+ dryRun: !!args.dryRun,
158
+ });
159
+ console.log(`clean: ${args.dryRun ? "dry-run" : "已删除"} ${removed} 条过期消息`);
160
+ },
161
+
162
+ status(args, cfg) {
163
+ core.assertUsable(cfg);
164
+ const s = core.statusOf(cfg);
165
+ console.log(`身份: ${s.identity} layout=${s.layout}`);
166
+ console.log(`写: ${s.outDir} (消息 ${s.outCount})`);
167
+ console.log(`seen: ${s.seen} 条`);
168
+ for (const i of s.inboxes) console.log(`读: ${i.dir} (消息 ${i.msgCount})`);
169
+ },
170
+ };
171
+
172
+ const args = parseArgs(process.argv.slice(2));
173
+ const command = args._[0] || "status";
174
+ const { cfg, configPath } = getConfig(args);
175
+
176
+ try {
177
+ const fn = commands[command];
178
+ if (!fn) throw new Error(`未知命令: ${command} (可用: init/send/recv/wait/poll/clean/status)`);
179
+ await fn(args, cfg, configPath);
180
+ } catch (e) {
181
+ console.error(`[mailbox] ${e.message}`);
182
+ process.exit(1);
183
+ }
@@ -0,0 +1,12 @@
1
+ # The dsh-mailbox bundle patch: registers the mailbox plugin row over the
2
+ # profile root. Override identity/root in the profile's own cordis.patch.yml:
3
+ #
4
+ # - id: mailbox
5
+ # config:
6
+ # identity: agent-a
7
+ # root: D:/Downloads/Agent/.mailbox
8
+ #
9
+ # layout=dirs (legacy dual-directory): provide dirs: { "<id>": "<path>" }.
10
+ - insert:
11
+ - id: mailbox
12
+ name: '@yuanchilin/dsh-mailbox'
package/lib/core.js ADDED
@@ -0,0 +1,186 @@
1
+ // ============================================================================
2
+ // @yuanchilin/dsh-mailbox — core
3
+ //
4
+ // 跨会话文件信箱核心逻辑(与 mailbox.psm1 / mailbox.mjs 同协议):
5
+ // - N 参与者对等模型: 每人一个信箱目录, 各写各的, 互读对方的
6
+ // - layout=root: <root>/<id>/ 每人一子目录 (participants 留空自动扫描)
7
+ // - layout=dirs: dirs: { "<id>": "<目录>" } 显式映射 (旧双目录兼容)
8
+ // - 消息: { id, from, to, type, topic, payload, ts, reply_to }, 文件 msg_<id>.json
9
+ // - 路由: to=<id> 定向 / to=all 广播 (写一份, 各人自取)
10
+ // - seen 去重: 每参与者独立 seen 文件 (默认 <outDir>/.seen.json)
11
+ // ============================================================================
12
+
13
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, rmSync } from "node:fs";
14
+ import { join, dirname } from "node:path";
15
+
16
+ export const DEFAULTS = {
17
+ identity: "",
18
+ layout: "root", // "root" | "dirs"
19
+ root: "",
20
+ dirs: {},
21
+ participants: [],
22
+ intervalSec: 2,
23
+ timeoutSec: 0,
24
+ seenFile: "",
25
+ patchRoot: "",
26
+ };
27
+
28
+ /** 合并默认值(可选: 环境变量 > 显式覆盖)。插件侧直接传 Config 对象, CLI 侧先加载配置文件。 */
29
+ export function resolveConfig(partial = {}, env = {}) {
30
+ const cfg = { ...DEFAULTS, ...partial };
31
+ if (env.MAILBOX_ID) cfg.identity = env.MAILBOX_ID;
32
+ if (env.MAILBOX_ROOT) { cfg.root = env.MAILBOX_ROOT; cfg.layout = "root"; }
33
+ if (env.MAILBOX_INTERVAL) cfg.intervalSec = Number(env.MAILBOX_INTERVAL);
34
+ if (env.MAILBOX_TIMEOUT) cfg.timeoutSec = Number(env.MAILBOX_TIMEOUT);
35
+ if (!Array.isArray(cfg.participants)) cfg.participants = [];
36
+ return cfg;
37
+ }
38
+
39
+ export function resolveDirs(cfg) {
40
+ if (cfg.layout === "dirs") {
41
+ if (!cfg.dirs || !cfg.dirs[cfg.identity]) {
42
+ throw new Error(`layout=dirs 但配置缺少 identity '${cfg.identity}' 的目录映射`);
43
+ }
44
+ const out = cfg.dirs[cfg.identity];
45
+ const inDirs = [...new Set(Object.entries(cfg.dirs).filter(([k]) => k !== cfg.identity).map(([, v]) => v))];
46
+ return { out, in: inDirs };
47
+ }
48
+ if (!cfg.root) throw new Error("layout=root 需要配置 root");
49
+ const out = join(cfg.root, cfg.identity);
50
+ let participants = cfg.participants.filter(Boolean);
51
+ if (participants.length === 0 && existsSync(cfg.root)) {
52
+ participants = readdirSync(cfg.root, { withFileTypes: true })
53
+ .filter((d) => d.isDirectory())
54
+ .map((d) => d.name);
55
+ }
56
+ const inDirs = [...new Set(participants.filter((p) => p !== cfg.identity).map((p) => join(cfg.root, p)))];
57
+ return { out, in: inDirs };
58
+ }
59
+
60
+ export function seenFileOf(cfg) {
61
+ if (cfg.seenFile) return cfg.seenFile;
62
+ return join(resolveDirs(cfg).out, ".seen.json");
63
+ }
64
+
65
+ export function loadSeen(cfg) {
66
+ const f = seenFileOf(cfg);
67
+ if (!existsSync(f)) return [];
68
+ try {
69
+ // pwsh 旧版本可能把单元素 seen 写成裸字符串 "id", 归一化为数组
70
+ const v = JSON.parse(readFileSync(f, "utf-8"));
71
+ return Array.isArray(v) ? v : [v];
72
+ } catch {
73
+ return [];
74
+ }
75
+ }
76
+
77
+ export function saveSeen(cfg, seen) {
78
+ const f = seenFileOf(cfg);
79
+ mkdirSync(dirname(f), { recursive: true });
80
+ writeFileSync(f, JSON.stringify([...new Set(seen)]));
81
+ }
82
+
83
+ function newId() {
84
+ const d = new Date();
85
+ const p = (n) => String(n).padStart(2, "0");
86
+ const ts = `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
87
+ const rand4 = () => Math.random().toString(16).slice(2, 6).padEnd(4, "0");
88
+ return `${ts}-${rand4()}-${rand4()}`;
89
+ }
90
+
91
+ /** 发送: 写到自己的目录 (对方读你的目录)。返回消息 id。 */
92
+ export function sendMessage(cfg, { to, type = "notify", topic = "", payload = {}, replyTo = "" }) {
93
+ if (!to) throw new Error("send 需要 to (参与者 id 或 all)");
94
+ const dirs = resolveDirs(cfg);
95
+ mkdirSync(dirs.out, { recursive: true });
96
+ const id = newId();
97
+ const msg = { id, from: cfg.identity, to, type, topic, payload, ts: Date.now(), reply_to: replyTo };
98
+ writeFileSync(join(dirs.out, `msg_${id}.json`), JSON.stringify(msg) + "\n", "utf-8");
99
+ return id;
100
+ }
101
+
102
+ /** 接收: 扫描所有对方目录, 取 to=自己 或 to=all 且未 seen 的消息。markSeen 默认 true。 */
103
+ export function recvNew(cfg, markSeen = true) {
104
+ const seen = loadSeen(cfg);
105
+ const dirs = resolveDirs(cfg);
106
+ const fresh = [];
107
+ for (const dir of dirs.in) {
108
+ if (!existsSync(dir)) continue;
109
+ for (const f of readdirSync(dir).filter((f) => f.startsWith("msg_") && f.endsWith(".json")).sort()) {
110
+ try {
111
+ const m = JSON.parse(readFileSync(join(dir, f), "utf-8"));
112
+ if ((m.to === cfg.identity || m.to === "all") && !seen.includes(m.id)) {
113
+ fresh.push(m);
114
+ if (markSeen) seen.push(m.id);
115
+ }
116
+ } catch {
117
+ // 跳过损坏消息
118
+ }
119
+ }
120
+ }
121
+ if (markSeen) saveSeen(cfg, seen);
122
+ return fresh;
123
+ }
124
+
125
+ /** 按 id 删除消息: inbox=true 删对方目录(已处理), 否则删自己的目录(已发送)。 */
126
+ export function removeMessage(cfg, id, inbox = false) {
127
+ const dirs = resolveDirs(cfg);
128
+ const targets = inbox ? dirs.in : [dirs.out];
129
+ for (const dir of targets) {
130
+ if (!existsSync(dir)) continue;
131
+ for (const f of readdirSync(dir)) {
132
+ if (!f.startsWith("msg_") || !f.endsWith(".json")) continue;
133
+ try {
134
+ const m = JSON.parse(readFileSync(join(dir, f), "utf-8"));
135
+ if (m.id === id) {
136
+ rmSync(join(dir, f), { force: true });
137
+ return true;
138
+ }
139
+ } catch {
140
+ // 跳过
141
+ }
142
+ }
143
+ }
144
+ return false;
145
+ }
146
+
147
+ /** TTL 清理: 删除自己 OutDir 中超过 ttlHours 的已发送消息 (收方应已读过)。返回删除数。 */
148
+ export function cleanTTL(cfg, { ttlHours = 24, dryRun = false } = {}) {
149
+ const dirs = resolveDirs(cfg);
150
+ if (!existsSync(dirs.out)) return 0;
151
+ const cutoff = Date.now() - ttlHours * 3600 * 1000;
152
+ let removed = 0;
153
+ for (const f of readdirSync(dirs.out)) {
154
+ if (!f.startsWith("msg_") || !f.endsWith(".json")) continue;
155
+ const p = join(dirs.out, f);
156
+ try {
157
+ if (statSync(p).mtimeMs < cutoff) {
158
+ if (!dryRun) rmSync(p, { force: true });
159
+ removed++;
160
+ }
161
+ } catch {
162
+ // 跳过
163
+ }
164
+ }
165
+ return removed;
166
+ }
167
+
168
+ /** 状态: 身份/布局/目录/消息数/未读数。 */
169
+ export function statusOf(cfg) {
170
+ const dirs = resolveDirs(cfg);
171
+ const seen = loadSeen(cfg);
172
+ const outCount = existsSync(dirs.out)
173
+ ? readdirSync(dirs.out).filter((f) => f.startsWith("msg_")).length
174
+ : 0;
175
+ const inboxes = dirs.in.map((dir) => ({
176
+ dir,
177
+ msgCount: existsSync(dir) ? readdirSync(dir).filter((f) => f.startsWith("msg_")).length : 0,
178
+ }));
179
+ return { identity: cfg.identity, layout: cfg.layout, outDir: dirs.out, outCount, seen: seen.length, inboxes };
180
+ }
181
+
182
+ /** 校验配置是否可用于收发 (identity/目录已解析)。 */
183
+ export function assertUsable(cfg) {
184
+ if (!cfg.identity) throw new Error("mailbox 未配置 identity (在 cordis.patch.yml 的 mailbox 配置中设置)");
185
+ resolveDirs(cfg); // 抛错即不可用
186
+ }
package/lib/index.js ADDED
@@ -0,0 +1,199 @@
1
+ // ============================================================================
2
+ // @yuanchilin/dsh-mailbox — DeepSeek Harness cordis 插件
3
+ //
4
+ // 注册 4 个模型面向工具:
5
+ // mailbox_send 发送消息 (定向 to=<id> / 广播 to=all)
6
+ // mailbox_recv 读取新消息 (自动更新 seen 去重)
7
+ // mailbox_status 查看身份/目录/消息数
8
+ // mailbox_clean 按 TTL 清理自己发过的旧消息
9
+ //
10
+ // 配置 (cordis.patch.yml 的 mailbox 行 config, 或 profile patch 覆盖):
11
+ // identity, layout(root|dirs), root, dirs, participants,
12
+ // intervalSec, timeoutSec, seenFile, patchRoot
13
+ //
14
+ // 长驻场景 (mailbox_wait 事件唤醒 / mailbox_poll 常驻轮询) 不适合做成工具,
15
+ // 请用包内 CLI: npx mailbox wait|poll (或 pwsh 版 mailbox.ps1)。
16
+ // ============================================================================
17
+
18
+ import z from "@deepseek-ai/schemastery";
19
+ import { defineTool } from "@deepseek-ai/dsh-tools";
20
+ import * as core from "./core.js";
21
+
22
+ const name = "mailbox";
23
+ const inject = ["tools"];
24
+
25
+ /** schemastery 配置模式 (全部带默认值, 激活零配置; 收发前需配置 identity/root) */
26
+ const Config = z.object({
27
+ identity: z.string().default(""),
28
+ layout: z.string().default("root"),
29
+ root: z.string().default(""),
30
+ dirs: z.dict(z.string()).default({}),
31
+ participants: z.array(z.string()).default([]),
32
+ intervalSec: z.number().default(2),
33
+ timeoutSec: z.number().default(0),
34
+ seenFile: z.string().default(""),
35
+ patchRoot: z.string().default(""),
36
+ });
37
+
38
+ const text = (s) => [{ type: "text", text: s }];
39
+
40
+ function apply(ctx, config) {
41
+ const cfg = core.resolveConfig(config ?? {});
42
+
43
+ ctx.tools.register(defineTool({
44
+ name: "mailbox_send",
45
+ description: "通过共享文件系统信箱向其他会话/agent 发送一条异步消息。to=参与者 id 定向发送或 all 广播;消息类型 request/response/notify/reply;对方不在线也不丢消息(对方之后 recv 或 wait 即可收到)。配合 mailbox_recv / mailbox_wait 使用。",
46
+ parameters: {
47
+ to: { type: "string", required: true, description: "接收方参与者 id,或 all 广播" },
48
+ type: { type: "string", enum: ["request", "response", "notify", "reply"], required: false, description: "消息类型,默认 notify" },
49
+ topic: { type: "string", required: false, description: "消息主题,用于路由/归类(如 hello、patch_xxx)" },
50
+ payload: { type: "object", required: false, description: "任意 JSON 负载" },
51
+ replyTo: { type: "string", required: false, description: "应答目标消息 id(请求-响应模式)" },
52
+ },
53
+ output: {
54
+ schema: {
55
+ type: "object",
56
+ additionalProperties: false,
57
+ properties: {
58
+ ok: { type: "boolean", required: true },
59
+ id: { type: "string", required: true },
60
+ from: { type: "string", required: true },
61
+ to: { type: "string", required: true },
62
+ },
63
+ },
64
+ render: (_args, value) => text(`已发送 ${value.id} (${value.from} → ${value.to})`),
65
+ },
66
+ execute: async (args) => {
67
+ core.assertUsable(cfg);
68
+ const id = core.sendMessage(cfg, {
69
+ to: args.to,
70
+ type: args.type ?? "notify",
71
+ topic: args.topic ?? "",
72
+ payload: args.payload ?? {},
73
+ replyTo: args.replyTo ?? "",
74
+ });
75
+ return { ok: true, id, from: cfg.identity, to: args.to };
76
+ },
77
+ presentCall: (args) => ({
78
+ card: "generic",
79
+ title: `mailbox → ${args.to}`,
80
+ kind: "other",
81
+ rawInput: args,
82
+ }),
83
+ }));
84
+
85
+ ctx.tools.register(defineTool({
86
+ name: "mailbox_recv",
87
+ description: "读取信箱中发给本会话的新消息(自动记录 seen,重复调用不会重复返回)。返回消息列表:from/to/type/topic/payload/reply_to。无新消息时返回空列表。",
88
+ parameters: {
89
+ format: { type: "string", enum: ["table", "json"], required: false, description: "输出格式,默认 table" },
90
+ },
91
+ output: {
92
+ schema: {
93
+ type: "object",
94
+ additionalProperties: false,
95
+ properties: {
96
+ count: { type: "integer", required: true },
97
+ messages: {
98
+ type: "array",
99
+ required: true,
100
+ items: {
101
+ type: "object",
102
+ additionalProperties: true,
103
+ properties: {
104
+ id: { type: "string" },
105
+ from: { type: "string" },
106
+ to: { type: "string" },
107
+ type: { type: "string" },
108
+ topic: { type: "string" },
109
+ ts: { type: "integer" },
110
+ reply_to: { type: "string" },
111
+ payload: { type: "object" },
112
+ },
113
+ },
114
+ },
115
+ },
116
+ },
117
+ render: (_args, value) => {
118
+ if (value.count === 0) return text("(无新消息)");
119
+ const lines = value.messages.map((m) => {
120
+ const p = m.payload && Object.keys(m.payload).length ? ` payload=${JSON.stringify(m.payload)}` : "";
121
+ return `[${m.from} -> ${m.to}] ${m.type} topic=${m.topic} id=${m.id}${m.reply_to ? ` reply_to=${m.reply_to}` : ""}${p}`;
122
+ });
123
+ return text(`新消息 ${value.count} 条:\n${lines.join("\n")}`);
124
+ },
125
+ },
126
+ execute: async (args) => {
127
+ core.assertUsable(cfg);
128
+ const messages = core.recvNew(cfg, true);
129
+ return { count: messages.length, messages };
130
+ },
131
+ presentCall: () => ({ card: "generic", title: "mailbox recv", kind: "other" }),
132
+ }));
133
+
134
+ ctx.tools.register(defineTool({
135
+ name: "mailbox_status",
136
+ description: "查看信箱配置与状态:本会话身份、写入目录、已发送消息数、seen 记录数、各对方信箱的消息数。用于确认信箱是否配置好、对方是否在活跃。",
137
+ parameters: {},
138
+ output: {
139
+ schema: {
140
+ type: "object",
141
+ additionalProperties: true,
142
+ properties: {
143
+ identity: { type: "string" },
144
+ layout: { type: "string" },
145
+ outDir: { type: "string" },
146
+ outCount: { type: "integer" },
147
+ seen: { type: "integer" },
148
+ inboxes: { type: "array", items: { type: "object" } },
149
+ },
150
+ },
151
+ render: (_args, value) => {
152
+ const lines = [
153
+ `身份: ${value.identity} layout=${value.layout}`,
154
+ `写: ${value.outDir} (消息 ${value.outCount})`,
155
+ `seen: ${value.seen} 条`,
156
+ ...value.inboxes.map((i) => `读: ${i.dir} (消息 ${i.msgCount})`),
157
+ ];
158
+ return text(lines.join("\n"));
159
+ },
160
+ },
161
+ execute: async () => {
162
+ core.assertUsable(cfg);
163
+ return core.statusOf(cfg);
164
+ },
165
+ presentCall: () => ({ card: "generic", title: "mailbox status", kind: "other" }),
166
+ }));
167
+
168
+ ctx.tools.register(defineTool({
169
+ name: "mailbox_clean",
170
+ description: "按 TTL 清理本会话自己发过的旧消息(对方应已读过)。dryRun 只统计不删除。",
171
+ parameters: {
172
+ ttlHours: { type: "integer", required: false, description: "保留时长(小时),默认 24" },
173
+ dryRun: { type: "boolean", required: false, description: "只统计不删除,默认 false" },
174
+ },
175
+ output: {
176
+ schema: {
177
+ type: "object",
178
+ additionalProperties: false,
179
+ properties: {
180
+ removed: { type: "integer", required: true },
181
+ dryRun: { type: "boolean", required: true },
182
+ },
183
+ },
184
+ render: (_args, value) =>
185
+ text(`clean: ${value.dryRun ? "dry-run" : "已删除"} ${value.removed} 条过期消息`),
186
+ },
187
+ execute: async (args) => {
188
+ core.assertUsable(cfg);
189
+ const removed = core.cleanTTL(cfg, {
190
+ ttlHours: args.ttlHours ?? 24,
191
+ dryRun: args.dryRun ?? false,
192
+ });
193
+ return { removed, dryRun: args.dryRun ?? false };
194
+ },
195
+ presentCall: () => ({ card: "generic", title: "mailbox clean", kind: "other" }),
196
+ }));
197
+ }
198
+
199
+ export { Config, apply, inject, name };