@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.
@@ -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
+ }