@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 +21 -0
- package/README.md +69 -0
- package/bin/mailbox.mjs +183 -0
- package/cordis.patch.yml +12 -0
- package/lib/core.js +186 -0
- package/lib/index.js +199 -0
- package/lib/types/index.d.ts +44 -0
- package/package.json +60 -0
- package/skill/README.md +117 -0
- package/skill/SKILL.md +83 -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 +172 -0
- package/skill/mailbox.psm1 +270 -0
- package/skill/self-test.ps1 +105 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// mailbox.mjs — 通用跨会话文件信箱 CLI v1 (Node 版, 零依赖)
|
|
4
|
+
//
|
|
5
|
+
// 与 pwsh 版 (mailbox.ps1) 同协议、同配置、同命令, 可互换混用:
|
|
6
|
+
// node mailbox.mjs init --id agent-a --root D:/Downloads/Agent/.mailbox
|
|
7
|
+
// node mailbox.mjs send --to agent-b --topic hello --payload '{"x":1}'
|
|
8
|
+
// node mailbox.mjs recv --format table
|
|
9
|
+
// node mailbox.mjs wait --timeout 60
|
|
10
|
+
// node mailbox.mjs poll --interval 2 --handlers ./handlers.mjs
|
|
11
|
+
// node mailbox.mjs clean --ttl-hours 24
|
|
12
|
+
// node mailbox.mjs status
|
|
13
|
+
//
|
|
14
|
+
// 配置优先级: 参数 > 环境变量 (MAILBOX_CONFIG/ID/ROOT/INTERVAL/TIMEOUT) > 配置文件 > 默认
|
|
15
|
+
// 消息格式: { id, from, to, type, topic, payload, ts, reply_to }
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, rmSync } from "node:fs";
|
|
19
|
+
import { join, dirname, basename, resolve } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const DEFAULT_CONFIG = join(__dirname, "mailbox.config.json");
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// 参数解析: --key value / --flag
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
const args = { _: [] };
|
|
30
|
+
for (let i = 0; i < argv.length; i++) {
|
|
31
|
+
const a = argv[i];
|
|
32
|
+
if (a.startsWith("--")) {
|
|
33
|
+
const key = a.slice(2);
|
|
34
|
+
if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
|
|
35
|
+
args[key] = argv[++i];
|
|
36
|
+
} else {
|
|
37
|
+
args[key] = true;
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
args._.push(a);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return args;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// 配置解析
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
function getConfig(args) {
|
|
50
|
+
const cfg = {
|
|
51
|
+
identity: "",
|
|
52
|
+
layout: "root",
|
|
53
|
+
root: "",
|
|
54
|
+
dirs: {},
|
|
55
|
+
participants: [],
|
|
56
|
+
intervalSec: 2,
|
|
57
|
+
timeoutSec: 0,
|
|
58
|
+
seenFile: "",
|
|
59
|
+
patchRoot: "",
|
|
60
|
+
};
|
|
61
|
+
let configPath = args.config || process.env.MAILBOX_CONFIG || DEFAULT_CONFIG;
|
|
62
|
+
if (existsSync(configPath)) {
|
|
63
|
+
try {
|
|
64
|
+
Object.assign(cfg, JSON.parse(readFileSync(configPath, "utf-8")));
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.warn(`[mailbox] 读取配置失败: ${configPath} (${e.message})`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (process.env.MAILBOX_ID) cfg.identity = process.env.MAILBOX_ID;
|
|
70
|
+
if (process.env.MAILBOX_ROOT) { cfg.root = process.env.MAILBOX_ROOT; cfg.layout = "root"; }
|
|
71
|
+
if (process.env.MAILBOX_INTERVAL) cfg.intervalSec = Number(process.env.MAILBOX_INTERVAL);
|
|
72
|
+
if (process.env.MAILBOX_TIMEOUT) cfg.timeoutSec = Number(process.env.MAILBOX_TIMEOUT);
|
|
73
|
+
if (args.identity) cfg.identity = args.identity;
|
|
74
|
+
if (args.root) { cfg.root = args.root; cfg.layout = "root"; }
|
|
75
|
+
if (args.interval !== undefined) cfg.intervalSec = Number(args.interval);
|
|
76
|
+
if (args.timeout !== undefined) cfg.timeoutSec = Number(args.timeout);
|
|
77
|
+
return { cfg, configPath };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveDirs(cfg) {
|
|
81
|
+
if (cfg.layout === "dirs") {
|
|
82
|
+
if (!cfg.dirs || !cfg.dirs[cfg.identity]) {
|
|
83
|
+
throw new Error(`layout=dirs 但配置缺少 identity '${cfg.identity}' 的目录映射`);
|
|
84
|
+
}
|
|
85
|
+
const out = cfg.dirs[cfg.identity];
|
|
86
|
+
const inDirs = [...new Set(Object.entries(cfg.dirs).filter(([k]) => k !== cfg.identity).map(([, v]) => v))];
|
|
87
|
+
return { out, in: inDirs };
|
|
88
|
+
}
|
|
89
|
+
if (!cfg.root) throw new Error("layout=root 需要配置 root");
|
|
90
|
+
const out = join(cfg.root, cfg.identity);
|
|
91
|
+
let participants = Array.isArray(cfg.participants) && cfg.participants.length > 0 ? cfg.participants : [];
|
|
92
|
+
if (participants.length === 0 && existsSync(cfg.root)) {
|
|
93
|
+
participants = readdirSync(cfg.root, { withFileTypes: true })
|
|
94
|
+
.filter((d) => d.isDirectory())
|
|
95
|
+
.map((d) => d.name);
|
|
96
|
+
}
|
|
97
|
+
const inDirs = [...new Set(participants.filter((p) => p !== cfg.identity).map((p) => join(cfg.root, p)))];
|
|
98
|
+
return { out, in: inDirs };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function seenFileOf(cfg) {
|
|
102
|
+
if (cfg.seenFile) return cfg.seenFile;
|
|
103
|
+
return join(resolveDirs(cfg).out, ".seen.json");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function loadSeen(cfg) {
|
|
107
|
+
const f = seenFileOf(cfg);
|
|
108
|
+
if (!existsSync(f)) return [];
|
|
109
|
+
try {
|
|
110
|
+
// pwsh 旧版本可能把单元素 seen 写成裸字符串 "id", 归一化为数组
|
|
111
|
+
const v = JSON.parse(readFileSync(f, "utf-8"));
|
|
112
|
+
return Array.isArray(v) ? v : [v];
|
|
113
|
+
} catch { return []; }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function saveSeen(cfg, seen) {
|
|
117
|
+
const f = seenFileOf(cfg);
|
|
118
|
+
mkdirSync(dirname(f), { recursive: true });
|
|
119
|
+
writeFileSync(f, JSON.stringify([...new Set(seen)]));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// 命令实现
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
function cmdInit(cfg, configPath, args) {
|
|
128
|
+
const out = {
|
|
129
|
+
identity: cfg.identity,
|
|
130
|
+
layout: cfg.layout,
|
|
131
|
+
root: cfg.root,
|
|
132
|
+
dirs: cfg.dirs,
|
|
133
|
+
participants: cfg.participants,
|
|
134
|
+
intervalSec: cfg.intervalSec,
|
|
135
|
+
timeoutSec: cfg.timeoutSec,
|
|
136
|
+
seenFile: cfg.seenFile,
|
|
137
|
+
patchRoot: cfg.patchRoot,
|
|
138
|
+
};
|
|
139
|
+
writeFileSync(configPath, JSON.stringify(out, null, 2) + "\n", "utf-8");
|
|
140
|
+
console.log(`已生成配置: ${configPath}`);
|
|
141
|
+
console.log(JSON.stringify(out, null, 2));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function cmdSend(cfg, args) {
|
|
145
|
+
if (!args.to) throw new Error("send 需要 --to <id|all>");
|
|
146
|
+
let payload = {};
|
|
147
|
+
if (args.payload) {
|
|
148
|
+
try { payload = JSON.parse(args.payload); } catch { throw new Error(`payload 不是合法 JSON: ${args.payload}`); }
|
|
149
|
+
}
|
|
150
|
+
const dirs = resolveDirs(cfg);
|
|
151
|
+
mkdirSync(dirs.out, { recursive: true });
|
|
152
|
+
const id = `${ts()}${rand4()}-${rand4()}`;
|
|
153
|
+
const msg = {
|
|
154
|
+
id, from: cfg.identity, to: args.to,
|
|
155
|
+
type: args.type || "notify",
|
|
156
|
+
topic: args.topic || "",
|
|
157
|
+
payload,
|
|
158
|
+
ts: Date.now(),
|
|
159
|
+
reply_to: args.replyTo || "",
|
|
160
|
+
};
|
|
161
|
+
writeFileSync(join(dirs.out, `msg_${id}.json`), JSON.stringify(msg) + "\n", "utf-8");
|
|
162
|
+
console.log(`sent ${id} (${new Date().toTimeString().slice(0, 8)})`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function cmdRecv(cfg, args) {
|
|
166
|
+
const msgs = recvNew(cfg, true);
|
|
167
|
+
if (msgs.length === 0) { console.log("(无新消息)"); return; }
|
|
168
|
+
if (args.format === "json") {
|
|
169
|
+
for (const m of msgs) console.log(JSON.stringify(m));
|
|
170
|
+
} else {
|
|
171
|
+
for (const m of msgs) {
|
|
172
|
+
const p = m.payload && Object.keys(m.payload).length ? JSON.stringify(m.payload) : "";
|
|
173
|
+
console.log(`[${m.from} -> ${m.to}] ${m.type} topic=${m.topic} id=${m.id}`);
|
|
174
|
+
if (m.reply_to) console.log(` reply_to=${m.reply_to} ts=${m.ts}`);
|
|
175
|
+
if (p) console.log(` payload: ${p}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function recvNew(cfg, markSeen) {
|
|
181
|
+
const seen = loadSeen(cfg);
|
|
182
|
+
const dirs = resolveDirs(cfg);
|
|
183
|
+
const fresh = [];
|
|
184
|
+
for (const dir of dirs.in) {
|
|
185
|
+
if (!existsSync(dir)) continue;
|
|
186
|
+
for (const f of readdirSync(dir).filter((f) => f.startsWith("msg_") && f.endsWith(".json")).sort()) {
|
|
187
|
+
try {
|
|
188
|
+
const m = JSON.parse(readFileSync(join(dir, f), "utf-8"));
|
|
189
|
+
if ((m.to === cfg.identity || m.to === "all") && !seen.includes(m.id)) {
|
|
190
|
+
fresh.push(m);
|
|
191
|
+
if (markSeen) seen.push(m.id);
|
|
192
|
+
}
|
|
193
|
+
} catch { /* 跳过损坏消息 */ }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (markSeen) saveSeen(cfg, seen);
|
|
197
|
+
return fresh;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function cmdWait(cfg, args) {
|
|
201
|
+
const timeoutSec = cfg.timeoutSec;
|
|
202
|
+
const started = Date.now();
|
|
203
|
+
for (;;) {
|
|
204
|
+
const msgs = recvNew(cfg, true);
|
|
205
|
+
if (msgs.length > 0) {
|
|
206
|
+
console.log(`=== NEW MESSAGES: ${msgs.length} ===`);
|
|
207
|
+
for (const m of msgs) console.log(JSON.stringify(m));
|
|
208
|
+
console.log("=== WAKE-UP (exit 0) ===");
|
|
209
|
+
process.exit(0);
|
|
210
|
+
}
|
|
211
|
+
if (timeoutSec > 0 && (Date.now() - started) / 1000 >= timeoutSec) {
|
|
212
|
+
console.log(`TIMEOUT after ${timeoutSec}s, no new messages`);
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
await sleep(cfg.intervalSec * 1000);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function cmdPoll(cfg, args) {
|
|
220
|
+
let handler = null;
|
|
221
|
+
if (args.handlers) {
|
|
222
|
+
const p = resolve(args.handlers);
|
|
223
|
+
handler = await import("file://" + p.replace(/\\/g, "/"));
|
|
224
|
+
console.log(`已加载 handlers: ${p}`);
|
|
225
|
+
}
|
|
226
|
+
console.log(`poll 启动 (identity=${cfg.identity} 每 ${cfg.intervalSec}s). Ctrl+C 退出`);
|
|
227
|
+
for (;;) {
|
|
228
|
+
try {
|
|
229
|
+
for (const m of recvNew(cfg, true)) {
|
|
230
|
+
console.log(`[收到] from=${m.from} type=${m.type} topic=${m.topic} id=${m.id}`);
|
|
231
|
+
let handled = false;
|
|
232
|
+
if (handler && typeof handler.handle === "function") {
|
|
233
|
+
handled = await handler.handle(m, { cfg, send: (o) => sendFrom(cfg, { ...o, from: cfg.identity }) });
|
|
234
|
+
}
|
|
235
|
+
if (!handled) {
|
|
236
|
+
if (m.type === "request") {
|
|
237
|
+
sendFrom(cfg, { to: m.from, type: "response", topic: m.topic, payload: { echo: m.payload, from: cfg.identity }, replyTo: m.id });
|
|
238
|
+
console.log(` → 已回 response (reply_to=${m.id})`);
|
|
239
|
+
} else {
|
|
240
|
+
console.log(JSON.stringify(m));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
try { removeMsg(cfg, m.id, true); } catch { /* 权限不足则跳过 */ }
|
|
244
|
+
}
|
|
245
|
+
} catch (e) {
|
|
246
|
+
console.warn(`轮询异常: ${e.message}`);
|
|
247
|
+
}
|
|
248
|
+
await sleep(cfg.intervalSec * 1000);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function sendFrom(cfg, { to, type = "notify", topic = "", payload = {}, replyTo = "" }) {
|
|
253
|
+
const dirs = resolveDirs(cfg);
|
|
254
|
+
mkdirSync(dirs.out, { recursive: true });
|
|
255
|
+
const id = `${ts()}${rand4()}-${rand4()}`;
|
|
256
|
+
const msg = { id, from: cfg.identity, to, type, topic, payload, ts: Date.now(), reply_to: replyTo };
|
|
257
|
+
writeFileSync(join(dirs.out, `msg_${id}.json`), JSON.stringify(msg) + "\n", "utf-8");
|
|
258
|
+
return id;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function removeMsg(cfg, id, inbox) {
|
|
262
|
+
const dirs = resolveDirs(cfg);
|
|
263
|
+
const targets = inbox ? dirs.in : [dirs.out];
|
|
264
|
+
for (const dir of targets) {
|
|
265
|
+
if (!existsSync(dir)) continue;
|
|
266
|
+
for (const f of readdirSync(dir)) {
|
|
267
|
+
if (!f.startsWith("msg_") || !f.endsWith(".json")) continue;
|
|
268
|
+
try {
|
|
269
|
+
const m = JSON.parse(readFileSync(join(dir, f), "utf-8"));
|
|
270
|
+
if (m.id === id) { rmSync(join(dir, f), { force: true }); return; }
|
|
271
|
+
} catch { /* 跳过 */ }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function cmdClean(cfg, args) {
|
|
277
|
+
const ttlHours = args.ttlHours !== undefined ? Number(args.ttlHours) : 24;
|
|
278
|
+
const dryRun = !!args.dryRun;
|
|
279
|
+
const dirs = resolveDirs(cfg);
|
|
280
|
+
if (!existsSync(dirs.out)) { console.log(`clean: 0 条 (目录不存在)`); return; }
|
|
281
|
+
const cutoff = Date.now() - ttlHours * 3600 * 1000;
|
|
282
|
+
let removed = 0;
|
|
283
|
+
for (const f of readdirSync(dirs.out)) {
|
|
284
|
+
if (!f.startsWith("msg_") || !f.endsWith(".json")) continue;
|
|
285
|
+
const p = join(dirs.out, f);
|
|
286
|
+
try {
|
|
287
|
+
if (statSync(p).mtimeMs < cutoff) {
|
|
288
|
+
if (!dryRun) rmSync(p, { force: true });
|
|
289
|
+
removed++;
|
|
290
|
+
}
|
|
291
|
+
} catch { /* 跳过 */ }
|
|
292
|
+
}
|
|
293
|
+
console.log(`clean: ${dryRun ? "dry-run" : "已删除"} ${removed} 条过期消息 (TtlHours=${ttlHours})`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function cmdStatus(cfg) {
|
|
297
|
+
const dirs = resolveDirs(cfg);
|
|
298
|
+
const seen = loadSeen(cfg).length;
|
|
299
|
+
const outCount = existsSync(dirs.out) ? readdirSync(dirs.out).filter((f) => f.startsWith("msg_")).length : 0;
|
|
300
|
+
console.log(`身份: ${cfg.identity} layout=${cfg.layout}`);
|
|
301
|
+
console.log(`写: ${dirs.out} (消息 ${outCount})`);
|
|
302
|
+
console.log(`seen: ${seen} 条`);
|
|
303
|
+
for (const d of dirs.in) {
|
|
304
|
+
const n = existsSync(d) ? readdirSync(d).filter((f) => f.startsWith("msg_")).length : 0;
|
|
305
|
+
console.log(`读: ${d} (消息 ${n})`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// 工具
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
function ts() {
|
|
313
|
+
const d = new Date();
|
|
314
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
315
|
+
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
316
|
+
}
|
|
317
|
+
function rand4() {
|
|
318
|
+
return Math.random().toString(16).slice(2, 6).padEnd(4, "0");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
// 入口
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
const args = parseArgs(process.argv.slice(2));
|
|
325
|
+
const command = args._[0] || "status";
|
|
326
|
+
const { cfg, configPath } = getConfig(args);
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
switch (command) {
|
|
330
|
+
case "init": cmdInit(cfg, configPath, args); break;
|
|
331
|
+
case "send": cmdSend(cfg, args); break;
|
|
332
|
+
case "recv": cmdRecv(cfg, args); break;
|
|
333
|
+
case "wait": await cmdWait(cfg, args); break;
|
|
334
|
+
case "poll": await cmdPoll(cfg, args); break;
|
|
335
|
+
case "clean": cmdClean(cfg, args); break;
|
|
336
|
+
case "status": cmdStatus(cfg); break;
|
|
337
|
+
default:
|
|
338
|
+
console.error(`未知命令: ${command} (可用: init/send/recv/wait/poll/clean/status)`);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
} catch (e) {
|
|
342
|
+
console.error(`[mailbox] ${e.message}`);
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# ============================================================================
|
|
2
|
+
# mailbox.ps1 — 通用跨会话文件信箱 CLI v1
|
|
3
|
+
#
|
|
4
|
+
# 用法示例:
|
|
5
|
+
# .\mailbox.ps1 init -Id agent-a -Root D:/Downloads/Agent/.mailbox
|
|
6
|
+
# .\mailbox.ps1 send -To agent-b -Topic hello -Payload '{"x":1}'
|
|
7
|
+
# .\mailbox.ps1 recv -Format table
|
|
8
|
+
# .\mailbox.ps1 wait -Timeout 60 # 新消息即退出 (exit 0, 唤醒 agent)
|
|
9
|
+
# .\mailbox.ps1 poll -Interval 2 -Handlers .\examples\handlers.patch.ps1
|
|
10
|
+
# .\mailbox.ps1 clean -TtlHours 24
|
|
11
|
+
# .\mailbox.ps1 status
|
|
12
|
+
#
|
|
13
|
+
# 配置优先级: 参数 > 环境变量 (MAILBOX_CONFIG/ID/ROOT/INTERVAL/TIMEOUT) > 配置文件 > 默认
|
|
14
|
+
# ============================================================================
|
|
15
|
+
[CmdletBinding()]
|
|
16
|
+
param(
|
|
17
|
+
[Parameter(Position = 0)]
|
|
18
|
+
[ValidateSet("init","send","recv","wait","poll","clean","status")]
|
|
19
|
+
[string]$Command = "status",
|
|
20
|
+
|
|
21
|
+
[string]$Config = "",
|
|
22
|
+
[string]$Identity = "",
|
|
23
|
+
[string]$Root = "",
|
|
24
|
+
|
|
25
|
+
# send
|
|
26
|
+
[string]$To = "",
|
|
27
|
+
[ValidateSet("request","response","notify","reply")]
|
|
28
|
+
[string]$Type = "notify",
|
|
29
|
+
[string]$Topic = "",
|
|
30
|
+
[string]$Payload = "",
|
|
31
|
+
[string]$ReplyTo = "",
|
|
32
|
+
|
|
33
|
+
# recv
|
|
34
|
+
[ValidateSet("table","json")]
|
|
35
|
+
[string]$Format = "table",
|
|
36
|
+
|
|
37
|
+
# wait / poll
|
|
38
|
+
[int]$Timeout = -1,
|
|
39
|
+
[int]$Interval = -1,
|
|
40
|
+
[string]$Handlers = "",
|
|
41
|
+
|
|
42
|
+
# clean
|
|
43
|
+
[int]$TtlHours = 24,
|
|
44
|
+
[switch]$DryRun
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
$ErrorActionPreference = "Stop"
|
|
48
|
+
Import-Module (Join-Path $PSScriptRoot "mailbox.psm1") -Force
|
|
49
|
+
|
|
50
|
+
$cfg = Get-MailboxConfig -ConfigPath $Config
|
|
51
|
+
if ($Identity) { $cfg.identity = $Identity }
|
|
52
|
+
if ($Root) { $cfg.root = $Root; $cfg.layout = "root" }
|
|
53
|
+
if (-not $cfg.identity) { throw "未指定身份: 请用 -Identity / MAILBOX_ID / 配置文件 identity" }
|
|
54
|
+
if ($Interval -ge 0) { $cfg.intervalSec = $Interval }
|
|
55
|
+
if ($Timeout -ge 0) { $cfg.timeoutSec = $Timeout }
|
|
56
|
+
|
|
57
|
+
switch ($Command) {
|
|
58
|
+
"init" {
|
|
59
|
+
$out = [ordered]@{
|
|
60
|
+
identity = $cfg.identity
|
|
61
|
+
layout = "root"
|
|
62
|
+
root = $cfg.root
|
|
63
|
+
dirs = $cfg.dirs
|
|
64
|
+
participants = @()
|
|
65
|
+
intervalSec = $cfg.intervalSec
|
|
66
|
+
timeoutSec = $cfg.timeoutSec
|
|
67
|
+
seenFile = $cfg.seenFile
|
|
68
|
+
patchRoot = $cfg.patchRoot
|
|
69
|
+
}
|
|
70
|
+
$cfgPath = if ($Config) { $Config } else { Join-Path $PSScriptRoot "mailbox.config.json" }
|
|
71
|
+
$out | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $cfgPath -Encoding UTF8
|
|
72
|
+
Write-Host "已生成配置: $cfgPath"
|
|
73
|
+
Write-Host ($out | ConvertTo-Json -Depth 6)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
"send" {
|
|
77
|
+
if (-not $To) { throw "send 需要 -To <id|all>" }
|
|
78
|
+
$payloadObj = @{}
|
|
79
|
+
if ($Payload) {
|
|
80
|
+
try { $payloadObj = $Payload | ConvertFrom-Json } catch { throw "payload 不是合法 JSON: $Payload" }
|
|
81
|
+
}
|
|
82
|
+
$id = Send-Mailbox -Cfg $cfg -To $To -Type $Type -Topic $Topic -Payload $payloadObj -ReplyTo $ReplyTo
|
|
83
|
+
Write-Output "sent $id ($(Get-Date -Format 'HH:mm:ss'))"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
"recv" {
|
|
87
|
+
$msgs = @(Recv-Mailbox -Cfg $cfg)
|
|
88
|
+
if ($msgs.Count -eq 0) { Write-Output "(无新消息)"; break }
|
|
89
|
+
if ($Format -eq "json") {
|
|
90
|
+
$msgs | ForEach-Object { Write-Output ($_ | ConvertTo-Json -Depth 8 -Compress) }
|
|
91
|
+
} else {
|
|
92
|
+
$msgs | ForEach-Object {
|
|
93
|
+
$p = if ($_.payload) { ($_ | ConvertTo-Json -Depth 3 -Compress) } else { "" }
|
|
94
|
+
Write-Host "[$($_.from) -> $($_.to)] $($_.type) topic=$($_.topic) id=$($_.id)"
|
|
95
|
+
Write-Host " reply_to=$($_.reply_to) ts=$($_.ts)"
|
|
96
|
+
if ($p) { Write-Host " payload: $p" }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
"wait" {
|
|
102
|
+
# 事件唤醒: 检测到新消息 → 打印 → exit 0 (DSH 后台 job 完成即通知 agent)
|
|
103
|
+
$started = Get-Date
|
|
104
|
+
while ($true) {
|
|
105
|
+
$msgs = @(Recv-Mailbox -Cfg $cfg)
|
|
106
|
+
if ($msgs.Count -gt 0) {
|
|
107
|
+
Write-Host "=== NEW MESSAGES: $($msgs.Count) ==="
|
|
108
|
+
$msgs | ForEach-Object { Write-Host ($_ | ConvertTo-Json -Depth 8 -Compress) }
|
|
109
|
+
Write-Host "=== WAKE-UP (exit 0) ==="
|
|
110
|
+
exit 0
|
|
111
|
+
}
|
|
112
|
+
if ($cfg.timeoutSec -gt 0 -and ((Get-Date) - $started).TotalSeconds -ge $cfg.timeoutSec) {
|
|
113
|
+
Write-Host "TIMEOUT after $($cfg.timeoutSec)s, no new messages"
|
|
114
|
+
exit 0
|
|
115
|
+
}
|
|
116
|
+
Start-Sleep -Seconds $cfg.intervalSec
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
"poll" {
|
|
121
|
+
# 常驻轮询: 默认 request→echo response, notify/reply→打印
|
|
122
|
+
# -Handlers <ps1>: dot-source 后若定义 Handle-Message($Msg, $Ctx) 则调用
|
|
123
|
+
if ($Handlers) {
|
|
124
|
+
$handlersPath = if (Test-Path -LiteralPath $Handlers) { (Resolve-Path $Handlers).Path } else { $Handlers }
|
|
125
|
+
. $handlersPath
|
|
126
|
+
Write-Host "已加载 handlers: $handlersPath"
|
|
127
|
+
}
|
|
128
|
+
Write-Host "poll 启动 (identity=$($cfg.identity) 每 $($cfg.intervalSec)s). Ctrl+C 退出"
|
|
129
|
+
while ($true) {
|
|
130
|
+
try {
|
|
131
|
+
$msgs = @(Recv-Mailbox -Cfg $cfg)
|
|
132
|
+
foreach ($m in $msgs) {
|
|
133
|
+
Write-Host "[收到] from=$($m.from) type=$($m.type) topic=$($m.topic) id=$($m.id)"
|
|
134
|
+
$handled = $false
|
|
135
|
+
if ($Handlers -and (Get-Command Handle-Message -ErrorAction SilentlyContinue)) {
|
|
136
|
+
$handled = Handle-Message -Msg $m -Ctx @{ Cfg = $cfg }
|
|
137
|
+
}
|
|
138
|
+
if (-not $handled) {
|
|
139
|
+
if ($m.type -eq "request") {
|
|
140
|
+
# 默认: 自动回 response (echo)
|
|
141
|
+
Send-Mailbox -Cfg $cfg -To $m.from -Type "response" -Topic $m.topic -ReplyTo $m.id `
|
|
142
|
+
-Payload @{ echo = $m.payload; from = $cfg.identity }
|
|
143
|
+
Write-Host " → 已回 response (reply_to=$($m.id))"
|
|
144
|
+
} else {
|
|
145
|
+
Write-Host ($m | ConvertTo-Json -Depth 6 -Compress)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
try { Remove-MailboxMsg -Cfg $cfg -Id $m.id -InInbox } catch { }
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
Write-Host "轮询异常: $($_.Exception.Message)" -ForegroundColor Yellow
|
|
152
|
+
}
|
|
153
|
+
Start-Sleep -Seconds $cfg.intervalSec
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
"clean" {
|
|
158
|
+
$removed = Clear-MailboxTTL -Cfg $cfg -TtlHours $TtlHours -DryRun:$DryRun
|
|
159
|
+
$mode = if ($DryRun) { "dry-run" } else { "已删除" }
|
|
160
|
+
Write-Output "clean: $mode $removed 条过期消息 (TtlHours=$TtlHours, outDir=$((Resolve-MailboxDirs $cfg).Out))"
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
"status" {
|
|
164
|
+
$s = Get-MailboxStatus $cfg
|
|
165
|
+
Write-Host "身份: $($s.identity) layout=$($s.layout)"
|
|
166
|
+
Write-Host "写: $($s.outDir) (消息 $($s.outCount))"
|
|
167
|
+
Write-Host "seen: $($s.seen) 条"
|
|
168
|
+
foreach ($i in $s.inboxes) {
|
|
169
|
+
Write-Host "读: $($i.Dir) (消息 $($i.MsgCount))"
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|