lark-relay 0.4.8 → 0.4.10
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/README.md +12 -9
- package/bin/lark-relay-collect.js +21 -6
- package/bin/lark-relay.js +31 -6
- package/lib/args.js +8 -4
- package/lib/collect.js +2 -2
- package/lib/filter.js +1 -1
- package/lib/help.js +18 -7
- package/lib/log.js +4 -5
- package/lib/status.js +5 -32
- package/lib/store.js +5 -2
- package/lib/utf8.js +1 -1
- package/package.json +4 -6
package/README.md
CHANGED
|
@@ -25,16 +25,13 @@ Lark 事件是**流式**的 -- 进程不在的时刻,消息**永久丢失**,不
|
|
|
25
25
|
停摆的每一秒都在丢消息。
|
|
26
26
|
|
|
27
27
|
分离到**可执行文件**这一层,不是两个子命令:采集是常驻服务,服务管理器
|
|
28
|
-
(
|
|
28
|
+
(launchd)展示的是可执行文件名,与 label 同名才认得出是谁。
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
| 控制权 | AI 手里(自己决定何时再取) |
|
|
36
|
-
| 谁能干 | 任何 agent(Claude Code / Codex / 手敲) |
|
|
37
|
-
| 配置 | 纯参数,无文件 |
|
|
30
|
+
`take` 的形态:
|
|
31
|
+
|
|
32
|
+
- **长命的是 AI 会话**(人设、上下文连续),**短命的是消息**(来一批处理一批)
|
|
33
|
+
- **AI 等消息**,不是消息推给 AI —— 控制权在 AI 手里,自己决定何时再取
|
|
34
|
+
- 任何 agent 都能干(Claude Code / Codex / 手敲),纯参数、无配置文件
|
|
38
35
|
|
|
39
36
|
## 命令
|
|
40
37
|
|
|
@@ -83,6 +80,12 @@ lark-relay take --app <app> --chats oc_xxx --render text
|
|
|
83
80
|
多消费者**共享 store、各持游标**,不做「处理完即删」-- 谁都不能替别人删。
|
|
84
81
|
盯同一 app 请用不同 `--name` 隔离游标。
|
|
85
82
|
|
|
83
|
+
**游标要自己收尾**:它是「关了会话、过阵子接着盯」的续接凭据,所以引擎**不会**
|
|
84
|
+
自动清理不活跃的游标 -- 自动清掉就等于下次重挂要么全量重放、要么跳过停机期的消息。
|
|
85
|
+
代价是不再用的游标会一直留在 `status` 里显示「落后 N 条」,而 store 只保留几天,
|
|
86
|
+
那个 N 会随过期删除而变小(**看着像追上了,其实是消息没了**)。
|
|
87
|
+
一次性监听、验完的排查、不再值守的任务,收尾时 `rm -rf ~/.lark-relay/cursors/<name>`。
|
|
88
|
+
|
|
86
89
|
## 设计取舍
|
|
87
90
|
|
|
88
91
|
- **原子落盘**:collect 读 stdout NDJSON 后自己 `写 .tmp -> rename`(同目录原子)。
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
// lark-relay-collect —— 事件采集底座,常驻服务专用入口。
|
|
5
5
|
//
|
|
6
6
|
// 为什么独立成一个可执行文件、不做 `lark-relay collect` 子命令:
|
|
7
|
-
// 这是个 LaunchDaemon
|
|
7
|
+
// 这是个 LaunchDaemon,服务管理器展示的是 ProgramArguments[0]。
|
|
8
8
|
// 挤在通用 CLI 里时 `launchctl print` 的 program 栏是 `lark-relay`、`ps` 的 COMM
|
|
9
9
|
// 是裸 `node` —— 看不出这是常驻服务。独立后 bin 名与 launchd label
|
|
10
|
-
// (com.adaex.lark-relay-collect)
|
|
10
|
+
// (com.adaex.lark-relay-collect)同名,好搜好认。
|
|
11
11
|
//
|
|
12
12
|
// 采集逻辑全在 lib/collect.js,本文件只做参数解析与进程身份。
|
|
13
13
|
|
|
@@ -17,8 +17,9 @@ require('../lib/utf8').ensureUtf8()
|
|
|
17
17
|
// 排障第一步常是 `ps | grep`,进程认不出来会先浪费一轮
|
|
18
18
|
process.title = 'lark-relay-collect'
|
|
19
19
|
|
|
20
|
-
const { parseArgs,
|
|
20
|
+
const { parseArgs, numStrict, list, rejectUnknown } = require('../lib/args')
|
|
21
21
|
const help = require('../lib/help')
|
|
22
|
+
const store = require('../lib/store')
|
|
22
23
|
|
|
23
24
|
const VERSION = require('../package.json').version
|
|
24
25
|
const PROG = 'lark-relay-collect'
|
|
@@ -63,19 +64,33 @@ async function main() {
|
|
|
63
64
|
return 2
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
// retain 必须落在消费侧的扫描窗口内,否则事件留在盘上却永远扫不到 =
|
|
68
|
+
// 静默丢消息(正是 store.js 那条注释警告的)。两个值在两个进程里,
|
|
69
|
+
// 只能在这里把不变量钉死:装完新版直接起不来,好过悄悄丢
|
|
70
|
+
const retain = numStrict(a.retain, 3)
|
|
71
|
+
if (retain === null || !(retain >= 1) || retain > store.SCAN_DAYS) {
|
|
72
|
+
process.stderr.write(
|
|
73
|
+
`--retain 需要 1~${store.SCAN_DAYS} 之间(消费侧扫描窗口 ${store.SCAN_DAYS} 天),当前:${a.retain}\n` +
|
|
74
|
+
`要保留更久得同时调大 LARK_RELAY_SCAN_DAYS,否则超窗口的事件留着也扫不到\n`,
|
|
75
|
+
)
|
|
76
|
+
return 2
|
|
77
|
+
}
|
|
78
|
+
|
|
66
79
|
const { runCollect } = require('../lib/collect')
|
|
67
80
|
return await runCollect({
|
|
68
81
|
exclude: list(a.exclude),
|
|
69
|
-
retain
|
|
82
|
+
retain,
|
|
70
83
|
})
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
main()
|
|
74
|
-
.then((code) =>
|
|
87
|
+
.then((code) => {
|
|
88
|
+
process.exitCode = code || 0
|
|
89
|
+
})
|
|
75
90
|
.catch((err) => {
|
|
76
91
|
// 注:runCollect 内部的错误走 lib/log.js 的 logLine(带时间戳,常驻服务要的)。
|
|
77
92
|
// 这里只兜住它之前/之外的意外,故是裸 stderr
|
|
78
93
|
if (err && err.userFacing) process.stderr.write(`${err.message}\n`)
|
|
79
94
|
else process.stderr.write(`${PROG}: ${(err && err.stack) || err}\n`)
|
|
80
|
-
process.
|
|
95
|
+
process.exitCode = 1
|
|
81
96
|
})
|
package/bin/lark-relay.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require('../lib/utf8').ensureUtf8()
|
|
5
5
|
|
|
6
|
-
const { parseArgs,
|
|
6
|
+
const { parseArgs, numStrict, suggest, rejectUnknown } = require('../lib/args')
|
|
7
7
|
const help = require('../lib/help')
|
|
8
8
|
|
|
9
9
|
const VERSION = require('../package.json').version
|
|
@@ -129,14 +129,33 @@ async function cmdTake(argv) {
|
|
|
129
129
|
return 3
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
// --since 只有 now|all 两个取值。必须白名单校验:曾经是「跟 'now' 比一次,
|
|
133
|
+
// 其余全当 all」,于是 `--since Now`(大写)、`--since 乱码` 都静默变成全量重放。
|
|
134
|
+
// 与本仓「拼错参数当场硬失败」的铁律一致(args.js 开头那条)
|
|
132
135
|
const since = a.since === true ? 'now' : a.since || 'now'
|
|
136
|
+
if (since !== 'now' && since !== 'all') {
|
|
137
|
+
process.stderr.write(`--since 只能是 now 或 all,当前:${since}\n`)
|
|
138
|
+
return 2
|
|
139
|
+
}
|
|
133
140
|
if (since === 'now' && store.readCursor(name, a.app) === null) {
|
|
134
141
|
store.seedCursorNow(name, a.app) // 默认不重放历史,消灭「预热游标」这一步
|
|
135
142
|
}
|
|
136
143
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
144
|
+
// 时长参数校验:负数/非数字过去会静默退回默认值或原样生效
|
|
145
|
+
// (实测 --debounce -5 被接受并打印「防抖=-5s」,--timeout abc 悄悄变成 12 小时)
|
|
146
|
+
const dur = (val, def, label) => {
|
|
147
|
+
const n = numStrict(val, def)
|
|
148
|
+
if (n === null || !(n >= 0)) {
|
|
149
|
+
const e = new Error(`--${label} 需要 >= 0 的秒数,当前:${val === true ? '(没给值)' : val}`)
|
|
150
|
+
e.userFacing = true
|
|
151
|
+
e.exitCode = 2
|
|
152
|
+
throw e
|
|
153
|
+
}
|
|
154
|
+
return n * 1000
|
|
155
|
+
}
|
|
156
|
+
const debounceMs = dur(a.debounce, 5, 'debounce')
|
|
157
|
+
const maxWaitMs = dur(a['max-wait'], 25, 'max-wait')
|
|
158
|
+
const timeoutMs = dur(a.timeout, 12 * 3600, 'timeout')
|
|
140
159
|
|
|
141
160
|
// 起来先打一行「监听中」-- 没这行就是没起来。
|
|
142
161
|
// 反馈实测:进程静默退出(参数错)被当成「在监听」,漏看 bot 第一条 Working
|
|
@@ -188,9 +207,15 @@ async function cmdStatus(argv) {
|
|
|
188
207
|
}
|
|
189
208
|
|
|
190
209
|
main()
|
|
191
|
-
.then((code) =>
|
|
210
|
+
.then((code) => {
|
|
211
|
+
// ⚠️ 不能用 process.exit() —— stdout 是管道时它会丢掉未 flush 的缓冲。
|
|
212
|
+
// 实测:400 条 NDJSON 重定向到文件得 676070 字节,同样数据接管道只收到
|
|
213
|
+
// 65536(管道缓冲上限)且切在 JSON 中途,而 stderr 照报「400 条」。
|
|
214
|
+
// 设 exitCode 让事件循环自然排空,退出码语义不变
|
|
215
|
+
process.exitCode = code || 0
|
|
216
|
+
})
|
|
192
217
|
.catch((err) => {
|
|
193
218
|
if (err && err.userFacing) process.stderr.write(`${err.message}\n`)
|
|
194
219
|
else process.stderr.write(`lark-relay: ${(err && err.stack) || err}\n`)
|
|
195
|
-
process.
|
|
220
|
+
process.exitCode = (err && err.exitCode) || 1
|
|
196
221
|
})
|
package/lib/args.js
CHANGED
|
@@ -49,10 +49,14 @@ function parseArgs(argv, spec = {}) {
|
|
|
49
49
|
return out
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
// 数值参数:省略 -> 默认值;给了但不是有效数字 -> null,由调用方硬失败。
|
|
53
|
+
// ⚠️ 不要退化成「非法值悄悄用默认值」—— 那样 `--timeout abc` 就成了
|
|
54
|
+
// 「跑起来但行为不对」,与本仓「拼错当场失败」的铁律相悖(实测它会静默变成 12 小时)
|
|
55
|
+
function numStrict(v, dflt) {
|
|
56
|
+
if (v === undefined) return dflt
|
|
57
|
+
if (v === true) return null // `--timeout` 后面没跟值
|
|
54
58
|
const n = Number(v)
|
|
55
|
-
return Number.isFinite(n) ? n :
|
|
59
|
+
return Number.isFinite(n) ? n : null
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
function list(v) {
|
|
@@ -160,4 +164,4 @@ function rewriteArgv(argv, unknown, allowed) {
|
|
|
160
164
|
return out
|
|
161
165
|
}
|
|
162
166
|
|
|
163
|
-
module.exports = { parseArgs,
|
|
167
|
+
module.exports = { parseArgs, numStrict, list, suggest, rejectUnknown, rewriteArgv }
|
package/lib/collect.js
CHANGED
|
@@ -117,7 +117,7 @@ async function runCollect(opts) {
|
|
|
117
117
|
ensureDir(paths.store)
|
|
118
118
|
ensureDir(paths.cursors)
|
|
119
119
|
// 日志接管要最早做 —— 下面的启动横幅、profile 报错都该带上时间戳。
|
|
120
|
-
// 非 launchd 场景(
|
|
120
|
+
// 非 launchd 场景(前台调试)是 no-op,logLine 退化成裸 stderr 直写
|
|
121
121
|
logInit()
|
|
122
122
|
|
|
123
123
|
const exclude = new Set(opts.exclude || [])
|
|
@@ -132,7 +132,7 @@ async function runCollect(opts) {
|
|
|
132
132
|
`collect 依赖 lark-cli 提供认证与事件总线。检查:\n` +
|
|
133
133
|
` which ${larkcli.CLI} # 装了吗、在 PATH 里吗\n` +
|
|
134
134
|
` ${larkcli.CLI} profile list # 能跑吗\n` +
|
|
135
|
-
`常驻服务(
|
|
135
|
+
`常驻服务(launchd plist)的 PATH 要用 fnm default alias 的稳定路径,不能用会话级的 multishell 路径`,
|
|
136
136
|
)
|
|
137
137
|
return 1
|
|
138
138
|
}
|
package/lib/filter.js
CHANGED
|
@@ -39,7 +39,7 @@ function applyFilter(events, expr) {
|
|
|
39
39
|
} catch (err) {
|
|
40
40
|
// 保守放行 + warn,不抛 —— 抛出去会让常驻消费者崩溃循环:
|
|
41
41
|
// 表达式对某类事件报错(如 `.mentions[].id` 遇到没有 mentions 的消息)时,
|
|
42
|
-
//
|
|
42
|
+
// 那条消息永远卡在队首,游标推不动,服务每 5s 重启一次,该任务彻底停摆。
|
|
43
43
|
// 放行的代价是模型多看几条本该滤掉的消息,远小于停摆
|
|
44
44
|
process.stderr.write(
|
|
45
45
|
`warn: --filter 求值失败,本批不过滤(${events.length} 条放行)。表达式:${expr}\n` +
|
package/lib/help.js
CHANGED
|
@@ -47,16 +47,22 @@ function takeHelp(apps) {
|
|
|
47
47
|
· 用 run_in_background 起,不要前台跑 -- 会阻塞会话最长 12h
|
|
48
48
|
· 一批处理完再起下一个;不需要自己写 while 循环
|
|
49
49
|
· 拿不到消息先查第二步:bot 不在群是最常见原因
|
|
50
|
-
· 多个消费者盯同一 app 用不同 --name
|
|
50
|
+
· 多个消费者盯同一 app 用不同 --name,游标互不干扰
|
|
51
|
+
· 不盯了就删游标:rm -rf ~/.lark-relay/cursors/<name>
|
|
52
|
+
游标是给「关了会话、过阵子接着盯」用的续接凭据,不是运行态缓存,
|
|
53
|
+
引擎不会自动清 -- 一次性的临时监听、验证完的排查、不再值守的任务,
|
|
54
|
+
收尾时自己删掉。留着只会在 status 里挂一行越积越多的「落后 N 条」,
|
|
55
|
+
而 store 只保留几天,那个 N 迟早会因为过期删除而变小 --
|
|
56
|
+
看着像追上了,其实是消息没了`
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
const COLLECT_HELP = `把全部 lark-cli profile 的事件收下来,原子落盘。零参数、零配置。
|
|
54
60
|
|
|
55
61
|
lark-relay-collect # 前台跑(调试)
|
|
56
|
-
常驻:
|
|
62
|
+
常驻:macOS 用 launchd(plist 模板见仓库 launchd/ 目录)
|
|
57
63
|
|
|
58
64
|
独立可执行文件而非 \`lark-relay\` 的子命令 —— 它是常驻服务,服务管理器展示的是
|
|
59
|
-
可执行文件名,与 launchd label(com.adaex.lark-relay-collect)
|
|
65
|
+
可执行文件名,与 launchd label(com.adaex.lark-relay-collect)同名才认得出。
|
|
60
66
|
|
|
61
67
|
app 列表实时读 \`lark-cli profile list\`,新增 profile 自动纳入,无需改配置。
|
|
62
68
|
每 app 一个子进程;某个挂了单独重启,不影响其他。
|
|
@@ -91,9 +97,11 @@ const STATUS_HELP = `一眼看清全局:谁在跑、积压多少、游标在哪
|
|
|
91
97
|
|
|
92
98
|
排障入口:某消费者「落后」很多 -> 它的 take 没在跑或卡住;
|
|
93
99
|
「无消费者」= 白采,可考虑 --exclude。
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
⚠️ 「落后」的那个消费者如果已经不打算再盯了,删掉它的游标:
|
|
101
|
+
\`rm -rf ~/.lark-relay/cursors/<name>\` -- 引擎不会自动清,
|
|
102
|
+
留着只会让这张表越来越长,且积压数会随 store 过期删除而虚假变小。
|
|
103
|
+
collect 行不是 running -> \`tail -f ~/.lark-relay/logs/collect.err.log\`
|
|
104
|
+
(launchd 不进统一日志),collect 停摆的每一秒都在丢消息。`
|
|
97
105
|
|
|
98
106
|
const GUIDE = `lark-relay -- Lark 事件中继站。两件事:收下来 / 我来取。
|
|
99
107
|
|
|
@@ -104,7 +112,7 @@ const GUIDE = `lark-relay -- Lark 事件中继站。两件事:收下来 / 我来
|
|
|
104
112
|
前置:采集必须在跑
|
|
105
113
|
lark-relay status # collect 行应为 running
|
|
106
114
|
进程不在的时刻消息永久丢失,不是延迟送达。
|
|
107
|
-
不在跑 -> 起常驻服务(
|
|
115
|
+
不在跑 -> 起常驻服务(macOS launchd,plist 模板见仓库;
|
|
108
116
|
或 \`lark-relay-collect\` 前台调试)
|
|
109
117
|
|
|
110
118
|
take 三步(照做)
|
|
@@ -116,6 +124,9 @@ take 三步(照做)
|
|
|
116
124
|
起来后 stderr 有一行「监听中」-- 没有就是没起来,看 stderr。
|
|
117
125
|
一批处理完再起一个。不要自己写 while 循环 -- 进程退出会通知你。
|
|
118
126
|
退出码:0=有一批,4=超时没消息(正常,再起一个),2=参数错,3=游标被占用。
|
|
127
|
+
4. 不盯了收尾 rm -rf ~/.lark-relay/cursors/<name>
|
|
128
|
+
游标只为「关了会话、过阵子接着盯」而存在,引擎不会自动清。
|
|
129
|
+
临时监听、验完的排查、不再值守的任务,收尾时自己删掉
|
|
119
130
|
|
|
120
131
|
常见错误
|
|
121
132
|
· 拿不到消息 -> 九成是 bot 不在群。验 bot 必须 --as user,
|
package/lib/log.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// CLI 的用户可见输出(take 的 NDJSON 载荷、status --json)绝不能过这里 --
|
|
5
5
|
// 加前缀会破坏下游解析契约。
|
|
6
6
|
//
|
|
7
|
-
// 为什么需要:
|
|
8
|
-
//
|
|
7
|
+
// 为什么需要:launchd 只有 StandardErrorPath 直写文件 -- 无时间戳、无轮转、
|
|
8
|
+
// 无分级(journald 那种免费给时间戳和轮转的东西这边没有)。而本项目的核心排查
|
|
9
9
|
// 问题恰是「**什么时候**断线、断了多久、那个窗口丢了几条」,时间戳是唯一依据。
|
|
10
10
|
const fs = require('fs')
|
|
11
11
|
const path = require('path')
|
|
@@ -13,11 +13,10 @@ const { paths } = require('./paths')
|
|
|
13
13
|
|
|
14
14
|
// 激活判据:stderr 是普通文件 = launchd 直写场景。
|
|
15
15
|
// 实测 fs.fstatSync(2):pipe -> isFile=false/isFIFO=true;重定向到文件 -> isFile=true。
|
|
16
|
-
//
|
|
16
|
+
// 这一个判据同时分开了两种场景:
|
|
17
17
|
// launchd 直写文件 -> 要时间戳、要轮转(本模块接管)
|
|
18
|
-
// systemd journald -> 不要(journald 自带时间戳,加了会重复;socket 不是 file)
|
|
19
18
|
// 前台调试 tty/pipe -> 不要(人眼看,时间戳是噪音)
|
|
20
|
-
//
|
|
19
|
+
// 所以不需要环境变量开关 -- 判据天然对齐。
|
|
21
20
|
function stderrIsFile() {
|
|
22
21
|
try {
|
|
23
22
|
return fs.fstatSync(2).isFile()
|
package/lib/status.js
CHANGED
|
@@ -17,29 +17,7 @@ function ago(ms) {
|
|
|
17
17
|
return `${Math.round(s / 86400)}d ago`
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const out = execFileSync('systemctl', ['is-active', unit], { encoding: 'utf8' }).trim()
|
|
23
|
-
return out
|
|
24
|
-
} catch (err) {
|
|
25
|
-
return (err.stdout || '').trim() || 'inactive'
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function unitMainPid(unit) {
|
|
30
|
-
try {
|
|
31
|
-
const out = execFileSync('systemctl', ['show', unit, '-p', 'MainPID', '--value'], {
|
|
32
|
-
encoding: 'utf8',
|
|
33
|
-
}).trim()
|
|
34
|
-
const n = Number(out)
|
|
35
|
-
return n > 0 ? n : null
|
|
36
|
-
} catch {
|
|
37
|
-
return null
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// 服务管理器按平台分治:Linux systemd / macOS launchd。
|
|
42
|
-
// 单元名/label 是部署约定,与 systemd 单元、launchd plist 同源;
|
|
20
|
+
// 服务管理器:launchd。label 是部署约定,与 launchd plist 同源;
|
|
43
21
|
// LR_COLLECT_LABEL 是改名部署的逃生舱。
|
|
44
22
|
const COLLECT_LABEL = process.env.LR_COLLECT_LABEL || 'com.adaex.lark-relay-collect'
|
|
45
23
|
|
|
@@ -55,9 +33,8 @@ function launchctlState(label) {
|
|
|
55
33
|
// 输出 `state = not running`,(\S+) 只吃到 "not",status 会显示成
|
|
56
34
|
// 「collect ○ not」。而「collect 没在跑」正是最要命的状态,偏偏这时不可读
|
|
57
35
|
//
|
|
58
|
-
// 只把 running 归一成 active(供上层判活),其余保留 launchd
|
|
59
|
-
//
|
|
60
|
-
// `not running`,术语一致比跨平台统一更有用
|
|
36
|
+
// 只把 running 归一成 active(供上层判活),其余保留 launchd 原话 ——
|
|
37
|
+
// 用户接着要跑 `launchctl print`,看到的就是 `not running`,术语一致才好排查
|
|
61
38
|
const state = (out.match(/^\s*state\s*=\s*(.+)$/m) || [])[1]
|
|
62
39
|
const pid = Number((out.match(/^\s*pid\s*=\s*(\d+)/m) || [])[1]) || null
|
|
63
40
|
const s = (state || '').trim()
|
|
@@ -68,15 +45,11 @@ function launchctlState(label) {
|
|
|
68
45
|
}
|
|
69
46
|
|
|
70
47
|
function serviceState() {
|
|
71
|
-
|
|
72
|
-
const unit = 'lark-relay-collect.service'
|
|
73
|
-
return { state: unitState(unit), pid: unitMainPid(unit) }
|
|
48
|
+
return launchctlState(COLLECT_LABEL)
|
|
74
49
|
}
|
|
75
50
|
|
|
76
51
|
function collectHint() {
|
|
77
|
-
return
|
|
78
|
-
? `sudo launchctl bootstrap system /Library/LaunchDaemons/${COLLECT_LABEL}.plist`
|
|
79
|
-
: 'systemctl enable --now lark-relay-collect'
|
|
52
|
+
return `sudo launchctl bootstrap system /Library/LaunchDaemons/${COLLECT_LABEL}.plist`
|
|
80
53
|
}
|
|
81
54
|
|
|
82
55
|
// 消费者 = cursors/ 下的目录。每个记录它在各 app 上的游标位置。
|
package/lib/store.js
CHANGED
|
@@ -8,7 +8,9 @@ const path = require('path')
|
|
|
8
8
|
const { paths, ensureDir, eventFilename, dayKey, dayKeyOffset } = require('./paths')
|
|
9
9
|
|
|
10
10
|
// scan 窗口天数。必须 >= store 保留天数,否则「保留着却扫不到」= 静默丢消息。
|
|
11
|
-
// 只扫窗口内是为了避免全目录排序(旧架构每轮全扫 4467 个文件)
|
|
11
|
+
// 只扫窗口内是为了避免全目录排序(旧架构每轮全扫 4467 个文件)。
|
|
12
|
+
// ⚠️ 与 collect 的 --retain 是两个进程里的两个值,没法在运行时互相读取,
|
|
13
|
+
// 故由 collect 启动时校验(见 bin/lark-relay-collect.js),这里导出给它用
|
|
12
14
|
const SCAN_DAYS = Number(process.env.LARK_RELAY_SCAN_DAYS || 4)
|
|
13
15
|
|
|
14
16
|
// 原子落盘:同目录 写 .tmp → rename。
|
|
@@ -68,7 +70,7 @@ function afterCursor(entry, cursor) {
|
|
|
68
70
|
return cmpName(entry.name, cName) > 0
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
// 只扫今天 + 昨天……
|
|
73
|
+
// 只扫今天 + 昨天…… 的窗口必须覆盖 store 保留天数,避免「保留 3 天却只扫 2 天」:
|
|
72
74
|
// 消费者停机超过 1 天后,盘上还在的事件会永久扫不到(游标停在更早的日期,
|
|
73
75
|
// 而窗口已经不覆盖它 → 静默丢失)。实测:游标停在 D-2、第三天启动即复现
|
|
74
76
|
function scanWindow(app, days = SCAN_DAYS) {
|
|
@@ -176,6 +178,7 @@ function gcStore(retainDays) {
|
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
module.exports = {
|
|
181
|
+
SCAN_DAYS,
|
|
179
182
|
writeEvent,
|
|
180
183
|
scanWindow,
|
|
181
184
|
readEvent,
|
package/lib/utf8.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lark-relay",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.10",
|
|
4
4
|
"description": "Lark event relay: collect events to disk, take a batch when you need it.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lark",
|
|
@@ -26,10 +26,8 @@
|
|
|
26
26
|
"README.md"
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
|
-
"test": "node --test test/*.test.js"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"type": "git",
|
|
33
|
-
"url": "git+https://github.com/adaex/lark-relay.git"
|
|
29
|
+
"test": "node --test test/*.test.js",
|
|
30
|
+
"prepublishOnly": "node --test test/*.test.js && npm run check-pack",
|
|
31
|
+
"check-pack": "node scripts/check-pack.js"
|
|
34
32
|
}
|
|
35
33
|
}
|