lark-relay 0.4.6 → 0.4.7
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 +14 -10
- package/bin/lark-relay-collect.js +58 -0
- package/bin/lark-relay.js +30 -86
- package/lib/args.js +61 -1
- package/lib/collect.js +1 -1
- package/lib/help.js +11 -6
- package/lib/utf8.js +15 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# lark-relay
|
|
2
2
|
|
|
3
|
-
Lark(飞书)
|
|
3
|
+
Lark(飞书)事件中继站。两件事:**收下来** / **我来取**。
|
|
4
4
|
|
|
5
5
|
依赖 [`lark-cli`](https://www.npmjs.com/package/@lark-base-open/lark-cli) 提供认证与
|
|
6
|
-
|
|
6
|
+
事件总线;本包只负责缓冲与批次,零 npm 依赖。
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
|
-
npm i -g lark-relay
|
|
10
|
-
lark-relay guide
|
|
9
|
+
npm i -g lark-relay # 装出两个命令:lark-relay-collect(常驻)/ lark-relay(在场)
|
|
10
|
+
lark-relay guide # 装完先读这个
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
## 为什么需要它
|
|
@@ -16,14 +16,17 @@ Lark 事件是**流式**的 -- 进程不在的时刻,消息**永久丢失**,不
|
|
|
16
16
|
(实测:消息发出 8 秒后才起 consumer,收到 0 条)。
|
|
17
17
|
|
|
18
18
|
所以要有一个常驻进程只管把事件收下来落盘,消费侧崩了、AI 跑了半小时、
|
|
19
|
-
会话关了几小时,都不丢消息。这就是 `collect`。
|
|
19
|
+
会话关了几小时,都不丢消息。这就是 `lark-relay-collect`。
|
|
20
20
|
|
|
21
21
|
## 两段分离
|
|
22
22
|
|
|
23
|
-
采集与消费必须分离:`collect` 只管把事件收下来落盘,`take`
|
|
24
|
-
|
|
23
|
+
采集与消费必须分离:`lark-relay-collect` 只管把事件收下来落盘,`lark-relay take`
|
|
24
|
+
在你需要时取走一批。消费侧崩了、AI 跑了半小时、会话关了几小时,采集都不能停 --
|
|
25
25
|
停摆的每一秒都在丢消息。
|
|
26
26
|
|
|
27
|
+
分离到**可执行文件**这一层,不是两个子命令:采集是常驻服务,服务管理器
|
|
28
|
+
(systemd / launchd)展示的是可执行文件名,与单元名同名才认得出是谁。
|
|
29
|
+
|
|
27
30
|
| | |
|
|
28
31
|
| --- | --- |
|
|
29
32
|
| 长命的 | AI 会话(人设、上下文连续) |
|
|
@@ -36,13 +39,14 @@ Lark 事件是**流式**的 -- 进程不在的时刻,消息**永久丢失**,不
|
|
|
36
39
|
## 命令
|
|
37
40
|
|
|
38
41
|
```bash
|
|
39
|
-
lark-relay
|
|
42
|
+
lark-relay-collect # 底座:全部 profile 各起 consume -> 原子落盘(常驻服务)
|
|
43
|
+
|
|
40
44
|
lark-relay take … # 阻塞等一批 -> 输出 -> 退出
|
|
41
45
|
lark-relay status # 谁在跑 / 各 app 积压 / 各游标位置
|
|
42
46
|
lark-relay guide # 一页用法
|
|
43
47
|
```
|
|
44
48
|
|
|
45
|
-
### collect
|
|
49
|
+
### lark-relay-collect
|
|
46
50
|
|
|
47
51
|
零参数、零配置。app 列表实时读 `lark-cli profile list`,新增 profile 自动纳入。
|
|
48
52
|
未授权或授权过期的 profile 自动跳过并 warn(永久失败,重试是死循环)。
|
|
@@ -50,7 +54,7 @@ lark-relay guide # 一页用法
|
|
|
50
54
|
每 app 一个子进程,某个挂了单独重启不影响其他。回收在本进程内每小时自查,
|
|
51
55
|
删超期的整个日期目录 -- 不另起 gc 单元/timer。
|
|
52
56
|
|
|
53
|
-
### take
|
|
57
|
+
### lark-relay take
|
|
54
58
|
|
|
55
59
|
只有一种形态:**阻塞等 -> 有一批就输出 -> 退出**。循环交给 harness --
|
|
56
60
|
Claude Code 用 `run_in_background` 起,进程退出会主动通知 AI,这才是事件驱动。
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// lark-relay-collect —— 事件采集底座,常驻服务专用入口。
|
|
5
|
+
//
|
|
6
|
+
// 为什么独立成一个可执行文件、不做 `lark-relay collect` 子命令:
|
|
7
|
+
// 这是个 LaunchDaemon / systemd 单元,服务管理器展示的是 ProgramArguments[0]。
|
|
8
|
+
// 挤在通用 CLI 里时 `launchctl print` 的 program 栏是 `lark-relay`、`ps` 的 COMM
|
|
9
|
+
// 是裸 `node` —— 看不出这是常驻服务。独立后 bin 名与 launchd label
|
|
10
|
+
// (com.adaex.lark-relay-collect)、systemd 单元名三处同名,好搜好认。
|
|
11
|
+
//
|
|
12
|
+
// 采集逻辑全在 lib/collect.js,本文件只做参数解析与进程身份。
|
|
13
|
+
|
|
14
|
+
require('../lib/utf8').ensureUtf8()
|
|
15
|
+
|
|
16
|
+
// ps/top 里显示成自己的名字,不再是裸 node(实测 macOS 与 Linux 都生效)。
|
|
17
|
+
// 排障第一步常是 `ps | grep`,进程认不出来会先浪费一轮
|
|
18
|
+
process.title = 'lark-relay-collect'
|
|
19
|
+
|
|
20
|
+
const { parseArgs, num, list, rejectUnknown } = require('../lib/args')
|
|
21
|
+
const help = require('../lib/help')
|
|
22
|
+
|
|
23
|
+
const VERSION = require('../package.json').version
|
|
24
|
+
const PROG = 'lark-relay-collect'
|
|
25
|
+
const KEYS = ['exclude', 'retain']
|
|
26
|
+
|
|
27
|
+
async function main() {
|
|
28
|
+
const argv = process.argv.slice(2)
|
|
29
|
+
|
|
30
|
+
// --version 要有:deploy-launchd.sh 用它打印「全局命令版本」作部署前后对照
|
|
31
|
+
if (argv[0] === '--version' || argv[0] === '-v' || argv[0] === 'version') {
|
|
32
|
+
process.stdout.write(`${VERSION}\n`)
|
|
33
|
+
return 0
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const a = parseArgs(argv, { flags: ['help'], keys: KEYS })
|
|
37
|
+
if (a._unknown.length) return rejectUnknown(PROG, argv, a._unknown, KEYS)
|
|
38
|
+
if (a.help || argv[0] === 'help') {
|
|
39
|
+
process.stdout.write(`${help.COLLECT_HELP}\n`)
|
|
40
|
+
return 0
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { runCollect } = require('../lib/collect')
|
|
44
|
+
return await runCollect({
|
|
45
|
+
exclude: list(a.exclude),
|
|
46
|
+
retain: num(a.retain, 3),
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
main()
|
|
51
|
+
.then((code) => process.exit(code || 0))
|
|
52
|
+
.catch((err) => {
|
|
53
|
+
// 注:runCollect 内部的错误走 lib/log.js 的 logLine(带时间戳,常驻服务要的)。
|
|
54
|
+
// 这里只兜住它之前/之外的意外,故是裸 stderr
|
|
55
|
+
if (err && err.userFacing) process.stderr.write(`${err.message}\n`)
|
|
56
|
+
else process.stderr.write(`${PROG}: ${(err && err.stack) || err}\n`)
|
|
57
|
+
process.exit(1)
|
|
58
|
+
})
|
package/bin/lark-relay.js
CHANGED
|
@@ -1,88 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
// 按字节切片,但子进程(lark-cli / claude)会继承 locale,中文仍会被切坏。
|
|
6
|
-
if (!/utf-?8/i.test(process.env.LC_ALL || process.env.LC_CTYPE || process.env.LANG || '')) {
|
|
7
|
-
process.env.LANG = 'C.UTF-8'
|
|
8
|
-
process.env.LC_ALL = 'C.UTF-8'
|
|
9
|
-
}
|
|
4
|
+
require('../lib/utf8').ensureUtf8()
|
|
10
5
|
|
|
11
|
-
const { parseArgs, num,
|
|
6
|
+
const { parseArgs, num, suggest, rejectUnknown } = require('../lib/args')
|
|
12
7
|
const help = require('../lib/help')
|
|
13
8
|
|
|
14
9
|
const VERSION = require('../package.json').version
|
|
15
10
|
|
|
16
|
-
//
|
|
11
|
+
// 带值参数白名单(布尔开关在 parseArgs 的 flags 里)。
|
|
17
12
|
// 不在白名单的 --xxx 一律硬报错 —— 拼错参数静默吞掉是实测事故:
|
|
18
13
|
// --chat-id 拼错 -> 打印用法退出,看着像「正常退出但没消息」,漏看 bot 第一条 Working
|
|
19
14
|
const TAKE_KEYS = ['app', 'chats', 'name', 'filter', 'debounce', 'max-wait', 'timeout', 'render', 'since']
|
|
20
|
-
const COLLECT_KEYS = ['exclude', 'retain']
|
|
21
|
-
|
|
22
|
-
// 未知参数 -> stderr 短错误 + 候选提示 + 改写后的命令,exit 2。
|
|
23
|
-
// 不打印完整用法 -- 那看着像 --help 成功,会被当成「正常退出但没消息」
|
|
24
|
-
function rejectUnknown(cmd, argv, unknown, allowed) {
|
|
25
|
-
const names = [...new Set(unknown)]
|
|
26
|
-
const shown = names.map((k) => `--${k}`).join(', ')
|
|
27
|
-
let hint = ''
|
|
28
|
-
for (const k of names) {
|
|
29
|
-
const s = suggest(k, allowed)
|
|
30
|
-
if (s) {
|
|
31
|
-
hint = `\n你是不是想写 --${s}?`
|
|
32
|
-
break
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
const rewrite = rewriteArgv(argv, names, allowed)
|
|
36
|
-
const tail = rewrite.length
|
|
37
|
-
? `\n你是不是想跑这个?\n lark-relay ${cmd} ${rewrite.join(' ')}`
|
|
38
|
-
: ''
|
|
39
|
-
process.stderr.write(`未知参数:${shown}${hint}${tail}\n跑 \`lark-relay ${cmd}\` 看完整用法\n`)
|
|
40
|
-
return 2
|
|
41
|
-
}
|
|
42
15
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const out = []
|
|
52
|
-
for (let i = 0; i < argv.length; i++) {
|
|
53
|
-
const a = argv[i]
|
|
54
|
-
if (a.startsWith('--') && a !== '--') {
|
|
55
|
-
const eq = a.indexOf('=')
|
|
56
|
-
const k = eq !== -1 ? a.slice(2, eq) : a.slice(2)
|
|
57
|
-
if (map.has(k)) {
|
|
58
|
-
out.push(eq !== -1 ? `--${map.get(k)}=${a.slice(eq + 1)}` : `--${map.get(k)}`)
|
|
59
|
-
if (eq === -1) {
|
|
60
|
-
const next = argv[i + 1]
|
|
61
|
-
if (next !== undefined && !next.startsWith('--')) {
|
|
62
|
-
out.push(next)
|
|
63
|
-
i++
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
continue
|
|
67
|
-
}
|
|
68
|
-
if (eq === -1) {
|
|
69
|
-
const next = argv[i + 1]
|
|
70
|
-
if (next !== undefined && !next.startsWith('--')) i++
|
|
71
|
-
}
|
|
72
|
-
continue
|
|
73
|
-
}
|
|
74
|
-
out.push(a)
|
|
75
|
-
}
|
|
76
|
-
return out
|
|
77
|
-
}
|
|
16
|
+
const COMMANDS = ['take', 'status', 'guide']
|
|
17
|
+
|
|
18
|
+
// collect 搬家提示。能力是**搬走了不是删了**,所以不能只说「未知命令」
|
|
19
|
+
const MOVED_HINT =
|
|
20
|
+
`collect 已拆成独立命令 —— 它是常驻服务,不该和在场取用挤在一个入口:\n` +
|
|
21
|
+
` lark-relay-collect # 前台跑(调试)\n` +
|
|
22
|
+
` lark-relay-collect --help # 参数与常驻部署\n` +
|
|
23
|
+
`同一个包就带,不用另外装。\n`
|
|
78
24
|
|
|
79
|
-
const USAGE = `lark-relay ${VERSION} —— Lark 事件中继站
|
|
25
|
+
const USAGE = `lark-relay ${VERSION} —— Lark 事件中继站(在场取用)
|
|
80
26
|
|
|
81
|
-
lark-relay collect 底座:全部 profile 各起 consume → 原子落盘(常驻服务)
|
|
82
27
|
lark-relay take … 在场取用:阻塞等一批 → 输出 → 退出
|
|
83
28
|
lark-relay status 谁在跑 / 各 app 积压 / 各游标位置
|
|
84
29
|
lark-relay guide 一页用法(装完先读这个)
|
|
85
30
|
|
|
31
|
+
采集底座是独立命令(常驻服务):lark-relay-collect
|
|
32
|
+
|
|
86
33
|
第一次用 → lark-relay guide
|
|
87
34
|
各命令详情 → lark-relay <命令> --help`
|
|
88
35
|
|
|
@@ -104,37 +51,34 @@ async function main() {
|
|
|
104
51
|
case 'guide':
|
|
105
52
|
process.stdout.write(`${help.GUIDE}\n`)
|
|
106
53
|
return 0
|
|
107
|
-
case 'collect':
|
|
108
|
-
return await cmdCollect(rest)
|
|
109
54
|
case 'take':
|
|
110
55
|
return await cmdTake(rest)
|
|
111
56
|
case 'status':
|
|
112
57
|
return await cmdStatus(rest)
|
|
58
|
+
// collect 拆成了独立可执行文件,这里专门拦一下告诉人改敲什么。
|
|
59
|
+
// 不能落到 default 分支:suggest('collect', ['take','status','guide']) 必然返回
|
|
60
|
+
// null(编辑距离都 >2、前缀 coll 也不匹配),只会打出干巴巴的「未知命令:collect」--
|
|
61
|
+
// 那会让人以为采集功能被删了。**能力是搬家,不是消失,提示必须给出新名字**
|
|
62
|
+
case 'collect':
|
|
63
|
+
process.stderr.write(MOVED_HINT)
|
|
64
|
+
return 2
|
|
113
65
|
default: {
|
|
114
|
-
|
|
66
|
+
// 拼错 collect(collectt / colect)同样走搬家提示 —— 意图明显是采集,
|
|
67
|
+
// 给「未知命令」等于让人以为功能没了。判据:编辑距离 <=2 且不像任何现有命令
|
|
68
|
+
if (cmd !== 'collect' && suggest(cmd, ['collect']) === 'collect' && !suggest(cmd, COMMANDS)) {
|
|
69
|
+
process.stderr.write(`未知命令:${cmd}。${MOVED_HINT}`)
|
|
70
|
+
return 2
|
|
71
|
+
}
|
|
72
|
+
const s = suggest(cmd, COMMANDS)
|
|
115
73
|
process.stderr.write(`未知命令:${cmd}${s ? `。你是不是想写 ${s}?` : ''}\n\n${USAGE}\n`)
|
|
116
74
|
return 2
|
|
117
75
|
}
|
|
118
76
|
}
|
|
119
77
|
}
|
|
120
78
|
|
|
121
|
-
async function cmdCollect(argv) {
|
|
122
|
-
const a = parseArgs(argv, { flags: ['help'], keys: COLLECT_KEYS })
|
|
123
|
-
if (a._unknown.length) return rejectUnknown('collect', argv, a._unknown, COLLECT_KEYS)
|
|
124
|
-
if (a.help) {
|
|
125
|
-
process.stdout.write(`${help.COLLECT_HELP}\n`)
|
|
126
|
-
return 0
|
|
127
|
-
}
|
|
128
|
-
const { runCollect } = require('../lib/collect')
|
|
129
|
-
return await runCollect({
|
|
130
|
-
exclude: list(a.exclude),
|
|
131
|
-
retain: num(a.retain, 3),
|
|
132
|
-
})
|
|
133
|
-
}
|
|
134
|
-
|
|
135
79
|
async function cmdTake(argv) {
|
|
136
80
|
const a = parseArgs(argv, { flags: ['help'], keys: TAKE_KEYS })
|
|
137
|
-
if (a._unknown.length) return rejectUnknown('take', argv, a._unknown, TAKE_KEYS)
|
|
81
|
+
if (a._unknown.length) return rejectUnknown('lark-relay take', argv, a._unknown, TAKE_KEYS)
|
|
138
82
|
const larkcli = require('../lib/larkcli')
|
|
139
83
|
|
|
140
84
|
// 空参数输出完整照做指导,而非报错 -- AI 的真实行为是「先空参数跑一下看看」。
|
|
@@ -231,7 +175,7 @@ async function cmdTake(argv) {
|
|
|
231
175
|
|
|
232
176
|
async function cmdStatus(argv) {
|
|
233
177
|
const a = parseArgs(argv, { flags: ['json', 'help'], keys: [] })
|
|
234
|
-
if (a._unknown.length) return rejectUnknown('status', argv, a._unknown, ['json'])
|
|
178
|
+
if (a._unknown.length) return rejectUnknown('lark-relay status', argv, a._unknown, ['json'])
|
|
235
179
|
if (a.help) {
|
|
236
180
|
process.stdout.write(`${help.STATUS_HELP}\n`)
|
|
237
181
|
return 0
|
package/lib/args.js
CHANGED
|
@@ -100,4 +100,64 @@ function suggest(unknown, allowed) {
|
|
|
100
100
|
return null
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
// 未知参数 -> stderr 短错误 + 候选提示 + 改写后的命令,exit 2。
|
|
104
|
+
// 不打印完整用法 -- 那看着像 --help 成功,会被当成「正常退出但没消息」
|
|
105
|
+
//
|
|
106
|
+
// ⚠️ `prog` 必须由调用方传入,不能写死 'lark-relay':两个 bin 共用这段,
|
|
107
|
+
// 而提示里那行「你是不是想跑这个」是要照抄执行的 —— 打出一个不存在的命令
|
|
108
|
+
// (如已拆走的 `lark-relay collect`)会把人引向死路。
|
|
109
|
+
// lark-relay 传 `lark-relay <子命令>`,lark-relay-collect 传自己的名字。
|
|
110
|
+
function rejectUnknown(prog, argv, unknown, allowed, out = process.stderr) {
|
|
111
|
+
const names = [...new Set(unknown)]
|
|
112
|
+
const shown = names.map((k) => `--${k}`).join(', ')
|
|
113
|
+
let hint = ''
|
|
114
|
+
for (const k of names) {
|
|
115
|
+
const s = suggest(k, allowed)
|
|
116
|
+
if (s) {
|
|
117
|
+
hint = `\n你是不是想写 --${s}?`
|
|
118
|
+
break
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const rewrite = rewriteArgv(argv, names, allowed)
|
|
122
|
+
const tail = rewrite.length ? `\n你是不是想跑这个?\n ${prog} ${rewrite.join(' ')}` : ''
|
|
123
|
+
out.write(`未知参数:${shown}${hint}${tail}\n跑 \`${prog}\` 看完整用法\n`)
|
|
124
|
+
return 2
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 把未知参数替换成候选,拼出可照跑的命令行;无候选的未知参数(连同它的值)丢弃
|
|
128
|
+
function rewriteArgv(argv, unknown, allowed) {
|
|
129
|
+
const map = new Map()
|
|
130
|
+
for (const k of unknown) {
|
|
131
|
+
const s = suggest(k, allowed)
|
|
132
|
+
if (s && !map.has(k)) map.set(k, s)
|
|
133
|
+
}
|
|
134
|
+
if (!map.size) return []
|
|
135
|
+
const out = []
|
|
136
|
+
for (let i = 0; i < argv.length; i++) {
|
|
137
|
+
const a = argv[i]
|
|
138
|
+
if (a.startsWith('--') && a !== '--') {
|
|
139
|
+
const eq = a.indexOf('=')
|
|
140
|
+
const k = eq !== -1 ? a.slice(2, eq) : a.slice(2)
|
|
141
|
+
if (map.has(k)) {
|
|
142
|
+
out.push(eq !== -1 ? `--${map.get(k)}=${a.slice(eq + 1)}` : `--${map.get(k)}`)
|
|
143
|
+
if (eq === -1) {
|
|
144
|
+
const next = argv[i + 1]
|
|
145
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
146
|
+
out.push(next)
|
|
147
|
+
i++
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
if (eq === -1) {
|
|
153
|
+
const next = argv[i + 1]
|
|
154
|
+
if (next !== undefined && !next.startsWith('--')) i++
|
|
155
|
+
}
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
out.push(a)
|
|
159
|
+
}
|
|
160
|
+
return out
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = { parseArgs, num, list, suggest, rejectUnknown, rewriteArgv }
|
package/lib/collect.js
CHANGED
|
@@ -162,7 +162,7 @@ async function runCollect(opts) {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
logLine(
|
|
165
|
-
`lark-relay
|
|
165
|
+
`lark-relay-collect: ${chosen.length}/${profiles.length} profiles → ${paths.store}\n` +
|
|
166
166
|
` ${chosen.map((p) => p.name).join(' ')}\n` +
|
|
167
167
|
` event_key=${EVENT_KEY} store 保留 ${opts.retain}天`,
|
|
168
168
|
)
|
package/lib/help.js
CHANGED
|
@@ -52,9 +52,12 @@ function takeHelp(apps) {
|
|
|
52
52
|
|
|
53
53
|
const COLLECT_HELP = `把全部 lark-cli profile 的事件收下来,原子落盘。零参数、零配置。
|
|
54
54
|
|
|
55
|
-
lark-relay
|
|
55
|
+
lark-relay-collect # 前台跑(调试)
|
|
56
56
|
常驻:Linux 用 systemd,macOS 用 launchd(单元模板见仓库 systemd//launchd 目录)
|
|
57
57
|
|
|
58
|
+
独立可执行文件而非 \`lark-relay\` 的子命令 —— 它是常驻服务,服务管理器展示的是
|
|
59
|
+
可执行文件名,与 launchd label(com.adaex.lark-relay-collect)、systemd 单元名同名才认得出。
|
|
60
|
+
|
|
58
61
|
app 列表实时读 \`lark-cli profile list\`,新增 profile 自动纳入,无需改配置。
|
|
59
62
|
每 app 一个子进程;某个挂了单独重启,不影响其他。
|
|
60
63
|
tokenStatus 为 expired 的 profile 自动跳过并 warn(永久失败,重试是死循环)。
|
|
@@ -92,15 +95,17 @@ collect 行不是 running -> 看服务日志(Linux journalctl;
|
|
|
92
95
|
macOS \`tail -f ~/.lark-relay/logs/collect.err.log\` -- 不进统一日志),
|
|
93
96
|
collect 停摆的每一秒都在丢消息。`
|
|
94
97
|
|
|
95
|
-
const GUIDE = `lark-relay -- Lark
|
|
98
|
+
const GUIDE = `lark-relay -- Lark 事件中继站。两件事:收下来 / 我来取。
|
|
96
99
|
|
|
97
|
-
collect
|
|
98
|
-
|
|
100
|
+
\`lark-relay-collect\` 把事件收下来落盘(常驻服务),\`lark-relay take\` 在你需要时取走一批。
|
|
101
|
+
两者分离是硬需求:消费侧崩了、AI 跑 30 分钟、会话关几小时,采集都不能停 --
|
|
102
|
+
停摆的每一秒都在丢消息。所以采集是独立的可执行文件,不是本命令的子命令。
|
|
99
103
|
|
|
100
|
-
|
|
104
|
+
前置:采集必须在跑
|
|
101
105
|
lark-relay status # collect 行应为 running
|
|
102
106
|
进程不在的时刻消息永久丢失,不是延迟送达。
|
|
103
|
-
不在跑 -> 起常驻服务(Linux systemd / macOS launchd
|
|
107
|
+
不在跑 -> 起常驻服务(Linux systemd / macOS launchd,模板见仓库;
|
|
108
|
+
或 \`lark-relay-collect\` 前台调试)
|
|
104
109
|
|
|
105
110
|
take 三步(照做)
|
|
106
111
|
1. 查群 ID lark-cli --profile <app> im +chat-search --as user --query "<群名>"
|
package/lib/utf8.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// UTF-8 兜底:systemd / launchd 环境都无 LANG -> C locale。虽然 Node 的字符串处理
|
|
4
|
+
// 不像 bash 那样按字节切片,但子进程(lark-cli)会继承 locale,中文仍会被切坏。
|
|
5
|
+
//
|
|
6
|
+
// 两个 bin(lark-relay / lark-relay-collect)都要,故抽成模块 —— 复制两份会漂移。
|
|
7
|
+
// 必须在任何 spawn 之前执行,所以两个入口都在最顶上 require 它。
|
|
8
|
+
function ensureUtf8(env = process.env) {
|
|
9
|
+
if (!/utf-?8/i.test(env.LC_ALL || env.LC_CTYPE || env.LANG || '')) {
|
|
10
|
+
env.LANG = 'C.UTF-8'
|
|
11
|
+
env.LC_ALL = 'C.UTF-8'
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { ensureUtf8 }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lark-relay",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "Lark event relay: collect events to disk, take a batch when you need it.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lark",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"node": ">=18"
|
|
18
18
|
},
|
|
19
19
|
"bin": {
|
|
20
|
-
"lark-relay": "bin/lark-relay.js"
|
|
20
|
+
"lark-relay": "bin/lark-relay.js",
|
|
21
|
+
"lark-relay-collect": "bin/lark-relay-collect.js"
|
|
21
22
|
},
|
|
22
23
|
"files": [
|
|
23
24
|
"bin",
|