lark-relay 0.4.2 → 0.4.4
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 +1 -1
- package/bin/lark-relay.js +6 -1
- package/lib/collect.js +1 -1
- package/lib/help.js +19 -7
- package/lib/status.js +37 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Lark 事件是**流式**的 -- 进程不在的时刻,消息**永久丢失**,不
|
|
|
34
34
|
## 命令
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
lark-relay collect # 底座:全部 profile 各起 consume -> 原子落盘(
|
|
37
|
+
lark-relay collect # 底座:全部 profile 各起 consume -> 原子落盘(常驻服务)
|
|
38
38
|
lark-relay take … # 场景1:阻塞等一批 -> 输出 -> 退出
|
|
39
39
|
lark-relay dispatch <task> # 场景2:常驻循环 = take + 唤起 claude
|
|
40
40
|
lark-relay status # 谁在跑 / 各 app 积压 / 各游标位置
|
package/bin/lark-relay.js
CHANGED
|
@@ -258,7 +258,12 @@ async function cmdDispatch(argv) {
|
|
|
258
258
|
if (a.list) return dispatch.cmdList()
|
|
259
259
|
const task = a._[0]
|
|
260
260
|
if (!task) {
|
|
261
|
-
|
|
261
|
+
// 空参 = 求教,和 take 一致(exit 0);带了 flag 却没任务名是写错,短错误
|
|
262
|
+
if (argv.length === 0) {
|
|
263
|
+
process.stdout.write(`${help.DISPATCH_HELP}\n`)
|
|
264
|
+
return 0
|
|
265
|
+
}
|
|
266
|
+
process.stderr.write('需要任务名($LR_ROOT/<任务名>/dispatch.yaml)。看现有任务:`lark-relay dispatch --list`\n')
|
|
262
267
|
return 2
|
|
263
268
|
}
|
|
264
269
|
return await dispatch.run(task, { once: !!a.once })
|
package/lib/collect.js
CHANGED
|
@@ -120,7 +120,7 @@ async function runCollect(opts) {
|
|
|
120
120
|
`collect 依赖 lark-cli 提供认证与事件总线。检查:\n` +
|
|
121
121
|
` which ${larkcli.CLI} # 装了吗、在 PATH 里吗\n` +
|
|
122
122
|
` ${larkcli.CLI} profile list # 能跑吗\n` +
|
|
123
|
-
|
|
123
|
+
`常驻服务(systemd 单元/launchd plist)的 PATH 要用 fnm default alias 的稳定路径,不能用会话级的 multishell 路径`,
|
|
124
124
|
)
|
|
125
125
|
e.userFacing = true
|
|
126
126
|
throw e
|
package/lib/help.js
CHANGED
|
@@ -53,7 +53,7 @@ function takeHelp(apps) {
|
|
|
53
53
|
const COLLECT_HELP = `把全部 lark-cli profile 的事件收下来,原子落盘。零参数、零配置。
|
|
54
54
|
|
|
55
55
|
lark-relay collect # 前台跑(调试)
|
|
56
|
-
|
|
56
|
+
常驻:Linux 用 systemd,macOS 用 launchd(单元模板见仓库 systemd//launchd 目录)
|
|
57
57
|
|
|
58
58
|
app 列表实时读 \`lark-cli profile list\`,新增 profile 自动纳入,无需改配置。
|
|
59
59
|
每 app 一个子进程;某个挂了单独重启,不影响其他。
|
|
@@ -64,6 +64,9 @@ tokenStatus 为 expired 的 profile 自动跳过并 warn(永久失败,重试是
|
|
|
64
64
|
8s 后才起 consumer,收到 0 条)。collect 独立常驻,才能让 take/dispatch 侧
|
|
65
65
|
崩了、claude 跑 30 分钟、会话关几小时,都不丢消息。
|
|
66
66
|
|
|
67
|
+
装完先 \`lark-relay status\` 确认 -- collect 行应为 running;不在跑看
|
|
68
|
+
服务日志(Linux \`journalctl -u lark-relay-collect\`;macOS \`log show --predicate 'process == "lark-relay"'\`)。
|
|
69
|
+
|
|
67
70
|
落盘:~/.lark-relay/store/<app>/<YYYY-MM-DD>/<纳秒>_<pid>_<seq>.json
|
|
68
71
|
回收由 collect 进程内每小时自查一次,删超期的整个日期目录(不另起 gc 单元/timer)。
|
|
69
72
|
|
|
@@ -78,15 +81,17 @@ const STATUS_HELP = `一眼看清全局:谁在跑、积压多少、游标在哪
|
|
|
78
81
|
lark-relay status --json # 机器可读
|
|
79
82
|
|
|
80
83
|
排障入口:某消费者「落后」很多 -> 它的 take/dispatch 没在跑或卡住;
|
|
81
|
-
「无消费者」= 白采,可考虑 --exclude
|
|
84
|
+
「无消费者」= 白采,可考虑 --exclude。
|
|
85
|
+
collect 行不是 running -> 看服务日志(systemd journal 或 launchd 统一日志),
|
|
86
|
+
collect 停摆的每一秒都在丢消息。`
|
|
82
87
|
|
|
83
88
|
const DISPATCH_HELP = `常驻:等消息 -> 唤起 claude 干活 -> 结果回群。无人在场也跑。
|
|
84
89
|
|
|
85
90
|
lark-relay dispatch --list # 列出所有 dispatch 任务及状态
|
|
86
|
-
lark-relay dispatch <task> --once # 前台跑一轮(
|
|
87
|
-
|
|
91
|
+
lark-relay dispatch <task> --once # 前台跑一轮(调试,不进常驻服务)
|
|
92
|
+
常驻:Linux 用 systemd,macOS 用 launchd(单元模板见仓库 systemd//launchd 目录)
|
|
88
93
|
|
|
89
|
-
--list 输出:任务名 / app / 群数 /
|
|
94
|
+
--list 输出:任务名 / app / 群数 / 常驻服务是否 active / 上次 ledger 时间
|
|
90
95
|
|
|
91
96
|
任务发现:扫 $LR_ROOT/*/dispatch.yaml -- 有文件即是任务,无需注册表。
|
|
92
97
|
LR_ROOT 在单元里指定(唯一的目录绑定,与代码无关)。
|
|
@@ -115,7 +120,13 @@ mode 三个场景(一个键定死全部行为,不用拼组合)
|
|
|
115
120
|
最佳实践
|
|
116
121
|
· 边界写 instructions,别指望 --add-dir 目录的 CLAUDE.md(启动不加载)
|
|
117
122
|
· 改完 yaml 先 --once 跑一轮验证,再 enable
|
|
118
|
-
·
|
|
123
|
+
· 高风险发布仍建议人工执行
|
|
124
|
+
|
|
125
|
+
退出码(--once)
|
|
126
|
+
0 跑了一轮,或没等到消息但正常结束
|
|
127
|
+
1 取批异常(看 stderr)
|
|
128
|
+
2 任务不存在或配置错
|
|
129
|
+
4 等消息超时(5min 没消息)`
|
|
119
130
|
|
|
120
131
|
const GUIDE = `lark-relay -- Lark 事件中继站。三个命令:收下来 / 我来取 / 派给别人。
|
|
121
132
|
|
|
@@ -136,6 +147,7 @@ const GUIDE = `lark-relay -- Lark 事件中继站。三个命令:收下来 / 我
|
|
|
136
147
|
前置:collect 必须在跑
|
|
137
148
|
lark-relay status # collect 行应为 running
|
|
138
149
|
进程不在的时刻消息永久丢失,不是延迟送达。
|
|
150
|
+
不在跑 -> 起常驻服务(Linux systemd / macOS launchd,模板见仓库;或 \`lark-relay collect\` 前台调试)
|
|
139
151
|
|
|
140
152
|
take 三步(照做)
|
|
141
153
|
1. 查群 ID lark-cli --profile <app> im +chat-search --as user --query "<群名>"
|
|
@@ -150,7 +162,7 @@ take 三步(照做)
|
|
|
150
162
|
建 dispatch 任务
|
|
151
163
|
1. 在任务目录写 dispatch.yaml(\`lark-relay dispatch --help\` 有模板)
|
|
152
164
|
2. lark-relay dispatch <task> --once # 前台验一轮
|
|
153
|
-
3.
|
|
165
|
+
3. 起常驻服务(Linux systemd / macOS launchd,模板见仓库)
|
|
154
166
|
|
|
155
167
|
常见错误
|
|
156
168
|
· 拿不到消息 -> 九成是 bot 不在群。验 bot 必须 --as user,
|
package/lib/status.js
CHANGED
|
@@ -39,6 +39,39 @@ function unitMainPid(unit) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// 服务管理器按平台分治:Linux systemd / macOS launchd。
|
|
43
|
+
// 单元名/label 是部署约定,与 systemd 单元、launchd plist 同源;
|
|
44
|
+
// LR_COLLECT_LABEL / LR_DISPATCH_LABEL_PREFIX 是改名部署的逃生舱。
|
|
45
|
+
const COLLECT_LABEL = process.env.LR_COLLECT_LABEL || 'com.adaex.lark-relay-collect'
|
|
46
|
+
const DISPATCH_LABEL_PREFIX = process.env.LR_DISPATCH_LABEL_PREFIX || 'com.adaex.lark-relay-dispatch'
|
|
47
|
+
|
|
48
|
+
function launchctlState(label) {
|
|
49
|
+
try {
|
|
50
|
+
const out = execFileSync('launchctl', ['print', `system/${label}`], { encoding: 'utf8' })
|
|
51
|
+
const state = (out.match(/^\s*state\s*=\s*(\S+)/m) || [])[1]
|
|
52
|
+
const pid = Number((out.match(/^\s*pid\s*=\s*(\d+)/m) || [])[1]) || null
|
|
53
|
+
return { state: state === 'running' ? 'active' : state || 'inactive', pid }
|
|
54
|
+
} catch {
|
|
55
|
+
return { state: 'inactive', pid: null }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// kind: 'collect' | 'dispatch';dispatch 带任务名。
|
|
60
|
+
function serviceState(kind, name) {
|
|
61
|
+
if (process.platform === 'darwin') {
|
|
62
|
+
const label = kind === 'collect' ? COLLECT_LABEL : `${DISPATCH_LABEL_PREFIX}.${name}`
|
|
63
|
+
return launchctlState(label)
|
|
64
|
+
}
|
|
65
|
+
const unit = kind === 'collect' ? 'lark-relay-collect.service' : `lark-relay-dispatch@${name}.service`
|
|
66
|
+
return { state: unitState(unit), pid: unitMainPid(unit) }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function collectHint() {
|
|
70
|
+
return process.platform === 'darwin'
|
|
71
|
+
? `sudo launchctl bootstrap system /Library/LaunchDaemons/${COLLECT_LABEL}.plist`
|
|
72
|
+
: 'systemctl enable --now lark-relay-collect'
|
|
73
|
+
}
|
|
74
|
+
|
|
42
75
|
// 消费者 = cursors/ 下的目录。每个记录它在各 app 上的游标位置。
|
|
43
76
|
function listConsumers() {
|
|
44
77
|
let names
|
|
@@ -75,9 +108,7 @@ function lagOf(app, cursor) {
|
|
|
75
108
|
}
|
|
76
109
|
|
|
77
110
|
async function buildStatus() {
|
|
78
|
-
const
|
|
79
|
-
const state = unitState(collectUnit)
|
|
80
|
-
const pid = unitMainPid(collectUnit)
|
|
111
|
+
const { state, pid } = serviceState('collect')
|
|
81
112
|
|
|
82
113
|
let profiles = []
|
|
83
114
|
try {
|
|
@@ -111,7 +142,6 @@ async function buildStatus() {
|
|
|
111
142
|
|
|
112
143
|
const tasks = []
|
|
113
144
|
for (const t of listTasks()) {
|
|
114
|
-
const unit = `lark-relay-dispatch@${t.name}.service`
|
|
115
145
|
let lastLedgerMs = null
|
|
116
146
|
for (const chat of t.config.chats || []) {
|
|
117
147
|
try {
|
|
@@ -123,12 +153,12 @@ async function buildStatus() {
|
|
|
123
153
|
name: t.name,
|
|
124
154
|
app: t.config.app,
|
|
125
155
|
chats: (t.config.chats || []).length,
|
|
126
|
-
state:
|
|
156
|
+
state: serviceState('dispatch', t.name).state,
|
|
127
157
|
lastLedgerMs,
|
|
128
158
|
})
|
|
129
159
|
}
|
|
130
160
|
|
|
131
|
-
return { collect: { state, pid, usable, expired }, apps: rows, tasks }
|
|
161
|
+
return { collect: { state, pid, usable, expired, hint: collectHint() }, apps: rows, tasks }
|
|
132
162
|
}
|
|
133
163
|
|
|
134
164
|
function formatStatus(s) {
|
|
@@ -145,7 +175,7 @@ function formatStatus(s) {
|
|
|
145
175
|
out.push(head)
|
|
146
176
|
if (s.collect.state !== 'active') {
|
|
147
177
|
out.push(' ⚠ collect 没在跑 —— 此刻的消息会永久丢失,不是延迟送达')
|
|
148
|
-
out.push(
|
|
178
|
+
out.push(` ${s.collect.hint}`)
|
|
149
179
|
}
|
|
150
180
|
out.push('')
|
|
151
181
|
|