lark-relay 0.4.13 → 0.4.14

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/bin/lark-relay.js CHANGED
@@ -218,7 +218,7 @@ async function cmdTake(argv) {
218
218
  }
219
219
 
220
220
  function cmdCursors(argv) {
221
- const CURSORS_FLAGS = ['prune', 'json', 'help']
221
+ const CURSORS_FLAGS = ['prune', 'force', 'json', 'help']
222
222
  const a = parseArgs(argv, { flags: CURSORS_FLAGS, keys: [] })
223
223
  if (a._unknown.length) return rejectUnknown('lark-relay cursors', argv, a._unknown, CURSORS_FLAGS)
224
224
  if (a.help) {
@@ -256,26 +256,36 @@ function cmdCursors(argv) {
256
256
 
257
257
  // --prune:位置参数是要删的身份;没给就只清 stale 的那些
258
258
  const names = a._
259
- const res = cursors.prune(names)
259
+ const res = cursors.prune(names, { force: !!a.force })
260
260
 
261
261
  // removed 的元素是 `name/app` —— 删的单位是 app,一个身份可能盯多个
262
262
  for (const n of res.removed) process.stdout.write(`已删 ${n}\n`)
263
263
  for (const n of res.missing) process.stderr.write(`没有这个游标身份:${n}\n`)
264
264
 
265
- // 在跑的一律不删 —— take 会立刻把游标写回来,删了是白删还让人以为清理生效了
265
+ // 在跑的不删 -- take 会立刻把游标写回来。只有显式点名时才报告(见 cursors.prune)
266
266
  for (const r of res.refused) {
267
267
  process.stderr.write(
268
268
  `跳过 ${r.name}:有 take 正在用它(pid ${r.pid})。\n` +
269
269
  ` 要清它先停掉那个 take(kill ${r.pid}),否则它会把游标立刻写回来。\n`,
270
270
  )
271
271
  }
272
- if (!res.removed.length && !res.missing.length && !res.refused.length) {
272
+
273
+ // 「○ 暂停」的游标还能续接,删了就丢消费位置 -- 要 --force 再确认一次。
274
+ // 2026-09-06 真实踩到:点名删一个刚退出 take 的 idle 游标,那段消息的消费位置没了
275
+ for (const f of res.needForce) {
276
+ process.stderr.write(
277
+ `⚠ ${f.name}/${f.app} 是「○ 暂停」,位置 ${f.day} 仍在扫描窗口内\n` +
278
+ ` 删了就丢续接凭据:重挂时会按 --since now 跳过这段已落盘的消息\n` +
279
+ ` 确认要删 -> lark-relay cursors --prune ${f.name} --force\n`,
280
+ )
281
+ }
282
+
283
+ if (!res.removed.length && !res.missing.length && !res.refused.length && !res.needForce.length) {
273
284
  process.stdout.write(names.length ? '没删任何东西\n' : '没有 stale 游标,无需清理\n')
274
285
  }
275
- // 退出码:点名的目标一个都没删成(全被拒/全不存在)才算失败 ——
276
- // 裸 prune 顺带跳过一个在跑的游标不是错误,那次清理是成功的
286
+ // 点名的目标一个都没删成才算失败
277
287
  if (res.missing.length) return 2
278
- if (res.refused.length && !res.removed.length && names.length) return 3
288
+ if ((res.refused.length || res.needForce.length) && !res.removed.length) return 3
279
289
  return 0
280
290
  }
281
291
 
package/lib/cursors.js CHANGED
@@ -115,10 +115,13 @@ function removeStaleLock(name) {
115
115
  /**
116
116
  * 清理游标。
117
117
  * @param {string[]} names 指定要删的身份(该身份下全部 app);空数组 = 只删 stale 的那些 app
118
- * @returns {{removed:string[], refused:Array<{name:string,pid:number}>, missing:string[]}}
119
- * removed 的元素形如 `name/app` -- 删的单位是 app,不是整个身份
118
+ * @param {{force?:boolean}} opts force 才允许删「○ 暂停」(窗口内)的游标
119
+ * @returns {{removed:string[], refused:Array<{name:string,pid:number}>, missing:string[],
120
+ * needForce:Array<{name:string,app:string,day:string}>}}
121
+ * removed 的元素形如 `name/app` -- 删的单位是 app,不是整个身份。
122
+ * refused 只在**显式点名**时填充:裸 prune 遇到在跑的游标是静默跳过,不算例外
120
123
  */
121
- function prune(names = []) {
124
+ function prune(names = [], opts = {}) {
122
125
  const all = listCursors()
123
126
  // 一个 name 可能有多个 app 行,按身份归并 -- live 判定是按身份的(锁以 name 为键)
124
127
  const byName = new Map()
@@ -130,6 +133,7 @@ function prune(names = []) {
130
133
  const removed = []
131
134
  const refused = []
132
135
  const missing = []
136
+ const needForce = []
133
137
 
134
138
  const targets = names.length ? names : [...byName.keys()]
135
139
 
@@ -139,23 +143,35 @@ function prune(names = []) {
139
143
  missing.push(name)
140
144
  continue
141
145
  }
142
- // live 一律不删,即使显式点名:take 还在跑,它会立刻把游标写回来 --
143
- // 删了是白删,还会让人以为清理没生效
144
146
  const live = rows.find((r) => r.state === 'live')
145
147
  if (live) {
146
- refused.push({ name, pid: live.pid })
148
+ // 显式点名时必须解释为什么没删(用户明确要求了);
149
+ // 而裸 prune 的意图是「清掉失效的」-- 在跑的游标本就不在这个视野里,
150
+ // 报告它是噪音(实测:3 个 take 在跑时,一次日常清理吐 3 段无关提示)
151
+ if (names.length) refused.push({ name, pid: live.pid })
147
152
  continue
148
153
  }
149
154
  // 显式点名 = 清该身份的全部 app(用户明确不盯了);
150
155
  // 裸 prune = 只清 stale 的那些 app,窗口内的留着续接
151
156
  const victims = names.length ? rows : rows.filter((r) => r.state === 'stale')
157
+ let deletedAny = false
152
158
  for (const r of victims) {
153
- if (removeCursorApp(name, r.app)) removed.push(`${name}/${r.app}`)
159
+ // ⚠️ 「○ 暂停」= 位置还在扫描窗口内,**删了就丢续接凭据**(重挂会按 --since now
160
+ // 跳过这段已落盘的消息)。stale 删掉无损,idle 不是 -- 故 idle 要 --force 兜一道。
161
+ // 2026-09-06 真实踩到:点名删一个刚退出 take 的 idle 游标,消费位置就此丢失
162
+ if (r.state === 'idle' && !opts.force) {
163
+ needForce.push({ name, app: r.app, day: r.day })
164
+ continue
165
+ }
166
+ if (removeCursorApp(name, r.app)) {
167
+ removed.push(`${name}/${r.app}`)
168
+ deletedAny = true
169
+ }
154
170
  }
155
- if (victims.length) removeStaleLock(name)
171
+ if (deletedAny) removeStaleLock(name)
156
172
  }
157
173
 
158
- return { removed, refused, missing }
174
+ return { removed, refused, missing, needForce }
159
175
  }
160
176
 
161
177
  module.exports = {
package/lib/help.js CHANGED
@@ -120,21 +120,22 @@ const CURSORS_HELP = `游标治理:看清哪些还在值守,清掉已失效的
120
120
 
121
121
  lark-relay cursors # 列出全部游标及状态
122
122
  lark-relay cursors --prune # 清掉全部 stale 的
123
- lark-relay cursors --prune <name>... # 清指定身份的全部 app(在跑的会跳过)
123
+ lark-relay cursors --prune <name>... # 清指定身份的全部 app
124
+ lark-relay cursors --prune <name> --force # 连「○ 暂停」的一起清
124
125
 
125
126
  三态:● 在跑 / ○ 暂停(窗口内,可续接) / ⚠ stale(超窗口,续接无意义)
126
127
 
128
+ **只有 stale 是无条件可清的**:
129
+ ● 在跑 不删。take 会立刻把游标写回来
130
+ ○ 暂停 要 --force。它还能续接,删了重挂就按 --since now 跳过这段消息
131
+ ⚠ stale 直接清。那段消息 gc 已回收,续接和 --since now 等价 -> 删它无损
132
+
127
133
  删的单位是 **app**,不是身份:一个 --name 可以同时盯多个 app,
128
- 裸 --prune 只清其中 stale 的那些,窗口内的留着续接(点名才整个身份清)。
134
+ 裸 --prune 只清其中 stale 的那些(在跑的静默跳过)。
129
135
  连带删该 app 的 dedup 记录(<app>.seen.json),不留会让重挂后头几条被误判重复。
130
136
 
131
- 为什么 stale 能放心删:消费侧只扫最近几天的目录,更早的日期 collect 的 gc
132
- 已整目录删掉。**续接一个超窗口的游标和 --since now 行为完全一样** --
133
- 删它不会丢任何还取得到的消息,留着只会让 status 虚报积压。
134
- 反过来,窗口内的「○ 暂停」不自动删 -- 那是「关了会话、过阵子接着盯」的续接凭据。
135
-
136
- 退出码:0 正常(含「跳过了在跑的那个」),2 参数错或点名的身份不存在,
137
- 3 点名的目标全在跑、一个都没清掉`
137
+ 退出码:0 正常,2 参数错或点名的身份不存在,
138
+ 3 点名的目标一个都没清掉(在跑,或需要 --force)`
138
139
 
139
140
  const GUIDE = `lark-relay -- Lark 事件中继站。两件事:收下来 / 我来取。
140
141
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lark-relay",
3
- "version": "0.4.13",
3
+ "version": "0.4.14",
4
4
  "description": "Lark event relay: collect events to disk, take a batch when you need it.",
5
5
  "keywords": [
6
6
  "lark",