lark-relay 0.4.3 → 0.4.5

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 CHANGED
@@ -34,7 +34,7 @@ Lark 事件是**流式**的 -- 进程不在的时刻,消息**永久丢失**,不
34
34
  ## 命令
35
35
 
36
36
  ```bash
37
- lark-relay collect # 底座:全部 profile 各起 consume -> 原子落盘(systemd 常驻)
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/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
- `systemd 单元里 PATH 要用 fnm default alias 的稳定路径,不能用会话级的 multishell 路径`,
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
- systemctl enable --now lark-relay-collect # 常驻(推荐)
56
+ 常驻:Linux systemd,macOS launchd(单元模板见仓库 systemd//launchd 目录)
57
57
 
58
58
  app 列表实时读 \`lark-cli profile list\`,新增 profile 自动纳入,无需改配置。
59
59
  每 app 一个子进程;某个挂了单独重启,不影响其他。
@@ -65,7 +65,7 @@ tokenStatus 为 expired 的 profile 自动跳过并 warn(永久失败,重试是
65
65
  崩了、claude 跑 30 分钟、会话关几小时,都不丢消息。
66
66
 
67
67
  装完先 \`lark-relay status\` 确认 -- collect 行应为 running;不在跑看
68
- \`systemctl status lark-relay-collect\` 的日志。
68
+ 服务日志(Linux \`journalctl -u lark-relay-collect\`;macOS \`log show --predicate 'process == "lark-relay"'\`)。
69
69
 
70
70
  落盘:~/.lark-relay/store/<app>/<YYYY-MM-DD>/<纳秒>_<pid>_<seq>.json
71
71
  回收由 collect 进程内每小时自查一次,删超期的整个日期目录(不另起 gc 单元/timer)。
@@ -82,16 +82,16 @@ const STATUS_HELP = `一眼看清全局:谁在跑、积压多少、游标在哪
82
82
 
83
83
  排障入口:某消费者「落后」很多 -> 它的 take/dispatch 没在跑或卡住;
84
84
  「无消费者」= 白采,可考虑 --exclude。
85
- collect 行不是 running -> \`systemctl status lark-relay-collect\` 看日志,
85
+ collect 行不是 running -> 看服务日志(systemd journal launchd 统一日志),
86
86
  collect 停摆的每一秒都在丢消息。`
87
87
 
88
88
  const DISPATCH_HELP = `常驻:等消息 -> 唤起 claude 干活 -> 结果回群。无人在场也跑。
89
89
 
90
90
  lark-relay dispatch --list # 列出所有 dispatch 任务及状态
91
- lark-relay dispatch <task> --once # 前台跑一轮(调试,不进 systemd)
92
- systemctl enable --now lark-relay-dispatch@<task>
91
+ lark-relay dispatch <task> --once # 前台跑一轮(调试,不进常驻服务)
92
+ 常驻:Linux systemd,macOS 用 launchd(单元模板见仓库 systemd//launchd 目录)
93
93
 
94
- --list 输出:任务名 / app / 群数 / systemd 单元是否 active / 上次 ledger 时间
94
+ --list 输出:任务名 / app / 群数 / 常驻服务是否 active / 上次 ledger 时间
95
95
 
96
96
  任务发现:扫 $LR_ROOT/*/dispatch.yaml -- 有文件即是任务,无需注册表。
97
97
  LR_ROOT 在单元里指定(唯一的目录绑定,与代码无关)。
@@ -147,7 +147,7 @@ const GUIDE = `lark-relay -- Lark 事件中继站。三个命令:收下来 / 我
147
147
  前置:collect 必须在跑
148
148
  lark-relay status # collect 行应为 running
149
149
  进程不在的时刻消息永久丢失,不是延迟送达。
150
- 不在跑 -> systemctl enable --now lark-relay-collect(或 \`lark-relay collect\` 前台调试)
150
+ 不在跑 -> 起常驻服务(Linux systemd / macOS launchd,模板见仓库;或 \`lark-relay collect\` 前台调试)
151
151
 
152
152
  take 三步(照做)
153
153
  1. 查群 ID lark-cli --profile <app> im +chat-search --as user --query "<群名>"
@@ -162,7 +162,7 @@ take 三步(照做)
162
162
  建 dispatch 任务
163
163
  1. 在任务目录写 dispatch.yaml(\`lark-relay dispatch --help\` 有模板)
164
164
  2. lark-relay dispatch <task> --once # 前台验一轮
165
- 3. systemctl enable --now lark-relay-dispatch@<task>
165
+ 3. 起常驻服务(Linux systemd / macOS launchd,模板见仓库)
166
166
 
167
167
  常见错误
168
168
  · 拿不到消息 -> 九成是 bot 不在群。验 bot 必须 --as user,
package/lib/status.js CHANGED
@@ -39,6 +39,44 @@ 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
+ // stderr 必须 ignore:launchctl 对不存在的服务把 "Bad request" 写到 stderr,
51
+ // 实测 execFileSync 失败时这串会泄漏到终端,污染 status 输出
52
+ const out = execFileSync('launchctl', ['print', `system/${label}`], {
53
+ encoding: 'utf8',
54
+ stdio: ['ignore', 'pipe', 'ignore'],
55
+ })
56
+ const state = (out.match(/^\s*state\s*=\s*(\S+)/m) || [])[1]
57
+ const pid = Number((out.match(/^\s*pid\s*=\s*(\d+)/m) || [])[1]) || null
58
+ return { state: state === 'running' ? 'active' : state || 'inactive', pid }
59
+ } catch {
60
+ return { state: 'inactive', pid: null }
61
+ }
62
+ }
63
+
64
+ // kind: 'collect' | 'dispatch';dispatch 带任务名。
65
+ function serviceState(kind, name) {
66
+ if (process.platform === 'darwin') {
67
+ const label = kind === 'collect' ? COLLECT_LABEL : `${DISPATCH_LABEL_PREFIX}.${name}`
68
+ return launchctlState(label)
69
+ }
70
+ const unit = kind === 'collect' ? 'lark-relay-collect.service' : `lark-relay-dispatch@${name}.service`
71
+ return { state: unitState(unit), pid: unitMainPid(unit) }
72
+ }
73
+
74
+ function collectHint() {
75
+ return process.platform === 'darwin'
76
+ ? `sudo launchctl bootstrap system /Library/LaunchDaemons/${COLLECT_LABEL}.plist`
77
+ : 'systemctl enable --now lark-relay-collect'
78
+ }
79
+
42
80
  // 消费者 = cursors/ 下的目录。每个记录它在各 app 上的游标位置。
43
81
  function listConsumers() {
44
82
  let names
@@ -75,9 +113,7 @@ function lagOf(app, cursor) {
75
113
  }
76
114
 
77
115
  async function buildStatus() {
78
- const collectUnit = 'lark-relay-collect.service'
79
- const state = unitState(collectUnit)
80
- const pid = unitMainPid(collectUnit)
116
+ const { state, pid } = serviceState('collect')
81
117
 
82
118
  let profiles = []
83
119
  try {
@@ -111,7 +147,6 @@ async function buildStatus() {
111
147
 
112
148
  const tasks = []
113
149
  for (const t of listTasks()) {
114
- const unit = `lark-relay-dispatch@${t.name}.service`
115
150
  let lastLedgerMs = null
116
151
  for (const chat of t.config.chats || []) {
117
152
  try {
@@ -123,12 +158,12 @@ async function buildStatus() {
123
158
  name: t.name,
124
159
  app: t.config.app,
125
160
  chats: (t.config.chats || []).length,
126
- state: unitState(unit),
161
+ state: serviceState('dispatch', t.name).state,
127
162
  lastLedgerMs,
128
163
  })
129
164
  }
130
165
 
131
- return { collect: { state, pid, usable, expired }, apps: rows, tasks }
166
+ return { collect: { state, pid, usable, expired, hint: collectHint() }, apps: rows, tasks }
132
167
  }
133
168
 
134
169
  function formatStatus(s) {
@@ -145,7 +180,7 @@ function formatStatus(s) {
145
180
  out.push(head)
146
181
  if (s.collect.state !== 'active') {
147
182
  out.push(' ⚠ collect 没在跑 —— 此刻的消息会永久丢失,不是延迟送达')
148
- out.push(' systemctl enable --now lark-relay-collect')
183
+ out.push(` ${s.collect.hint}`)
149
184
  }
150
185
  out.push('')
151
186
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lark-relay",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Lark event relay: collect events to disk, take a batch when you need it, or dispatch work to an AI agent.",
5
5
  "keywords": [
6
6
  "lark",