@yuanchilin/dsh-mailbox 0.0.1 → 0.0.2

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.
@@ -6,7 +6,7 @@
6
6
  # - layout=dirs (兼容旧双目录): 显式 dirs 映射 { id -> 目录 }
7
7
  # - 消息格式: { id, from, to, type, topic, payload, ts, reply_to }
8
8
  # - 路由: to=<id> 定向 / to=all 广播 (写一份, 各人自取)
9
- # - seen 去重: 每参与者独立 seen 文件 (默认 <outDir>/.seen.json)
9
+ # - seen 去重: 每参与者独立 seen 目录 (默认 <outDir>/.seen/, 每消息一个 .seen 标记, 原子写)
10
10
  #
11
11
  # 用法:
12
12
  # Import-Module <dir>/mailbox.psm1
@@ -32,7 +32,7 @@ function Get-MailboxConfig {
32
32
  participants = @() # layout=root 可选的显式参与者列表 (默认自动扫描)
33
33
  intervalSec = 2
34
34
  timeoutSec = 0
35
- seenFile = "" # 默认 <outDir>/.seen.json
35
+ seenFile = "" # 显式时用单文件兼容; 默认目录化 <outDir>/.seen/
36
36
  patchRoot = "" # 供示例补丁 handler 使用
37
37
  }
38
38
 
@@ -124,12 +124,38 @@ function Get-MailboxSeenFile {
124
124
  return Join-Path (Resolve-MailboxDirs $Cfg).Out ".seen.json"
125
125
  }
126
126
 
127
+ # 目录化 seen 的存储目录; 显式 seenFile 时返回 "" (走单文件兼容)
128
+ function Get-MailboxSeenDir {
129
+ param($Cfg)
130
+ if ($Cfg.seenFile -ne "") { return "" }
131
+ return Join-Path (Resolve-MailboxDirs $Cfg).Out ".seen"
132
+ }
133
+
127
134
  function Get-MailboxSeen {
128
135
  param($Cfg)
129
- $f = Get-MailboxSeenFile $Cfg
136
+ $dir = Get-MailboxSeenDir $Cfg
137
+ if ($dir -eq "") {
138
+ $f = Get-MailboxSeenFile $Cfg
139
+ $seen = @()
140
+ if (Test-Path -LiteralPath $f) {
141
+ try { $seen = @((Get-Content -LiteralPath $f -Raw | ConvertFrom-Json)) } catch { $seen = @() }
142
+ }
143
+ return ,$seen
144
+ }
145
+ # 目录模式: 先迁移旧单文件 .seen.json (幂等)
146
+ $legacy = Get-MailboxSeenFile $Cfg
147
+ if ((Test-Path -LiteralPath $legacy) -and -not (Test-Path -LiteralPath $dir)) {
148
+ try {
149
+ $old = @((Get-Content -LiteralPath $legacy -Raw | ConvertFrom-Json))
150
+ New-Item -ItemType Directory -Force -Path $dir | Out-Null
151
+ foreach ($id in $old) { if ($id) { Set-Content -LiteralPath (Join-Path $dir "$id.seen") -Value "" -Encoding UTF8 } }
152
+ Remove-Item -LiteralPath $legacy -Force -ErrorAction SilentlyContinue
153
+ } catch { }
154
+ }
130
155
  $seen = @()
131
- if (Test-Path -LiteralPath $f) {
132
- try { $seen = @((Get-Content -LiteralPath $f -Raw | ConvertFrom-Json)) } catch { $seen = @() }
156
+ if (Test-Path -LiteralPath $dir) {
157
+ $seen = @(Get-ChildItem -LiteralPath $dir -Filter "*.seen" -ErrorAction SilentlyContinue |
158
+ ForEach-Object { $_.BaseName })
133
159
  }
134
160
  # 逗号运算符: 防止单元素数组在函数输出边界被解包成裸字符串 (PowerShell 经典坑)
135
161
  return ,$seen
@@ -137,12 +163,61 @@ function Get-MailboxSeen {
137
163
 
138
164
  function Save-MailboxSeen {
139
165
  param($Cfg, [string[]]$Seen)
140
- $f = Get-MailboxSeenFile $Cfg
141
- $dir = Split-Path $f -Parent
142
- if ($dir -and -not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
143
- # 参数形式 (-InputObject) 保证单元素也输出数组 JSON ["a"], 避免管道解包成 "a"
144
- $unique = @($Seen | Select-Object -Unique)
145
- ConvertTo-Json -InputObject $unique | Set-Content -LiteralPath $f -Encoding UTF8
166
+ $dir = Get-MailboxSeenDir $Cfg
167
+ if ($dir -eq "") {
168
+ $f = Get-MailboxSeenFile $Cfg
169
+ $d = Split-Path $f -Parent
170
+ if ($d -and -not (Test-Path -LiteralPath $d)) { New-Item -ItemType Directory -Force -Path $d | Out-Null }
171
+ # 参数形式 (-InputObject) 保证单元素也输出数组 JSON ["a"], 避免管道解包成 "a"
172
+ $unique = @($Seen | Select-Object -Unique)
173
+ ConvertTo-Json -InputObject $unique | Set-Content -LiteralPath $f -Encoding UTF8
174
+ return
175
+ }
176
+ # 目录模式: 逐条原子标记 (并发安全, 不互不覆盖)
177
+ foreach ($id in ($Seen | Select-Object -Unique)) { if ($id) { Add-MailboxSeenMark $Cfg $id } }
178
+ }
179
+
180
+ # 目录化 seen: 每条已读消息一个 `.seen` 标记文件, 临时文件 + Move 原子提交
181
+ function Add-MailboxSeenMark {
182
+ param($Cfg, [string]$Id)
183
+ if (-not $Id) { return }
184
+ $dir = Get-MailboxSeenDir $Cfg
185
+ if ($dir -eq "") {
186
+ # 单文件兼容: 读改写
187
+ $seen = Get-MailboxSeen $Cfg
188
+ if ($seen -notcontains $Id) { Save-MailboxSeen $Cfg @($seen + $Id) }
189
+ return
190
+ }
191
+ if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
192
+ $mark = Join-Path $dir ("$Id.seen")
193
+ if (Test-Path -LiteralPath $mark) { return }
194
+ $tmp = Join-Path $dir (".tmp-$Id")
195
+ Set-Content -LiteralPath $tmp -Value "" -Encoding UTF8
196
+ Move-Item -LiteralPath $tmp -Destination $mark -Force
197
+ }
198
+
199
+ # ---------------------------------------------------------------------------
200
+ # 回执协议 (ack): request 被消费自动回 delivered; 处理完回 done/error (幂等防重)
201
+ # ---------------------------------------------------------------------------
202
+ function Add-MailboxAck {
203
+ param($Cfg, $Request, [string]$Status, $Extra = @{})
204
+ if (-not $Request -or -not $Request.id -or -not $Request.from) { return $false }
205
+ if ($Request.from -eq $Cfg.identity) { return $false }
206
+ if ($Status -notin @("delivered", "processing", "done", "error")) { return $false }
207
+ # 幂等: 自己发件目录已有同 requestId+status 的回执则跳过
208
+ $dirs = Resolve-MailboxDirs $Cfg
209
+ if (Test-Path -LiteralPath $dirs.Out) {
210
+ $existing = Get-ChildItem -LiteralPath $dirs.Out -Filter "msg_*.json" -ErrorAction SilentlyContinue |
211
+ ForEach-Object {
212
+ try { Get-Content -LiteralPath $_.FullName -Raw | ConvertFrom-Json } catch { $null }
213
+ } |
214
+ Where-Object { $_ -and $_.reply_to -eq $Request.id -and $_.payload.status -eq $Status }
215
+ if ($existing) { return $false }
216
+ }
217
+ $payload = @{ status = $Status; requestId = $Request.id }
218
+ foreach ($k in $Extra.Keys) { $payload[$k] = $Extra[$k] }
219
+ $id = Send-Mailbox -To $Request.from -Type reply -Topic "status" -Payload $payload -ReplyTo $Request.id -Cfg $Cfg
220
+ return ($id -ne $null -and $id -ne "")
146
221
  }
147
222
 
148
223
  # ---------------------------------------------------------------------------
@@ -183,7 +258,8 @@ function Send-Mailbox {
183
258
  function Recv-Mailbox {
184
259
  param(
185
260
  $Cfg,
186
- [switch]$KeepSeen # 置位时只读不更新 seen (wait 场景由调用方决定)
261
+ [switch]$KeepSeen, # 置位时只读不更新 seen (wait 场景由调用方决定)
262
+ [switch]$NoAck # 置位时不自动回执 delivered (纯只读监控)
187
263
  )
188
264
 
189
265
  # Get-MailboxSeen 已用逗号保证返回扁平数组, 这里不要再 @() 包装 (会变成嵌套数组)
@@ -198,12 +274,15 @@ function Recv-Mailbox {
198
274
  $m = Get-Content -LiteralPath $_.FullName -Raw | ConvertFrom-Json
199
275
  if (($m.to -eq $Cfg.identity -or $m.to -eq "all") -and ($seen -notcontains $m.id)) {
200
276
  $new += $m
201
- if (-not $KeepSeen) { $seen += $m.id }
277
+ if (-not $KeepSeen) { Add-MailboxSeenMark $Cfg $m.id }
202
278
  }
203
279
  } catch { }
204
280
  }
205
281
  }
206
- if (-not $KeepSeen) { Save-MailboxSeen $Cfg $seen }
282
+ # 回执协议: request 型消息自动回 delivered (幂等防重)
283
+ if (-not $KeepSeen -and -not $NoAck) {
284
+ foreach ($m in $new) { if ($m.type -eq "request") { Add-MailboxAck $Cfg $m "delivered" | Out-Null } }
285
+ }
207
286
  return $new
208
287
  }
209
288