lark-relay 0.4.1 → 0.4.3
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 +4 -1
- package/bin/lark-relay.js +71 -15
- package/lib/help.js +77 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,7 +58,10 @@ Claude Code 用 `run_in_background` 起,进程退出会主动通知 AI,这才是
|
|
|
58
58
|
lark-relay take --app <app> --chats oc_xxx --render text
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
空参数跑一下会输出完整的照做指导(查群 ID -> 验 bot 在群 -> 监听三步)。
|
|
61
|
+
空参数跑一下会输出完整的照做指导(查群 ID -> 验 bot 在群 -> 监听三步),exit 0。
|
|
62
|
+
起来后 stderr 先打一行「监听中」-- 没这行就是没起来,看 stderr 报错。
|
|
63
|
+
退出码:0=有一批,4=超时没消息(正常,再起一个),2=参数错,3=游标被占用。
|
|
64
|
+
参数拼错(如 `--chat-id`)会硬失败并给出改写后的命令,不静默吞掉。
|
|
62
65
|
|
|
63
66
|
业务过滤只有 `--filter`(jq 表达式)一个口子,不为每个业务加参数。
|
|
64
67
|
|
package/bin/lark-relay.js
CHANGED
|
@@ -19,8 +19,9 @@ const VERSION = require('../package.json').version
|
|
|
19
19
|
const TAKE_KEYS = ['app', 'chats', 'name', 'filter', 'debounce', 'max-wait', 'timeout', 'render', 'since']
|
|
20
20
|
const COLLECT_KEYS = ['exclude', 'retain', 'ledger-retain']
|
|
21
21
|
|
|
22
|
-
// 未知参数 -> stderr 短错误 +
|
|
23
|
-
|
|
22
|
+
// 未知参数 -> stderr 短错误 + 候选提示 + 改写后的命令,exit 2。
|
|
23
|
+
// 不打印完整用法 -- 那看着像 --help 成功,会被当成「正常退出但没消息」
|
|
24
|
+
function rejectUnknown(cmd, argv, unknown, allowed) {
|
|
24
25
|
const names = [...new Set(unknown)]
|
|
25
26
|
const shown = names.map((k) => `--${k}`).join(', ')
|
|
26
27
|
let hint = ''
|
|
@@ -31,10 +32,50 @@ function rejectUnknown(cmd, unknown, allowed) {
|
|
|
31
32
|
break
|
|
32
33
|
}
|
|
33
34
|
}
|
|
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`)
|
|
35
40
|
return 2
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
// 把未知参数替换成候选,拼出可照跑的命令行;无候选的未知参数(连同它的值)丢弃
|
|
44
|
+
function rewriteArgv(argv, unknown, allowed) {
|
|
45
|
+
const map = new Map()
|
|
46
|
+
for (const k of unknown) {
|
|
47
|
+
const s = suggest(k, allowed)
|
|
48
|
+
if (s && !map.has(k)) map.set(k, s)
|
|
49
|
+
}
|
|
50
|
+
if (!map.size) return []
|
|
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
|
+
}
|
|
78
|
+
|
|
38
79
|
const USAGE = `lark-relay ${VERSION} —— Lark 事件中继站
|
|
39
80
|
|
|
40
81
|
lark-relay collect 底座:全部 profile 各起 consume → 原子落盘(systemd 常驻)
|
|
@@ -72,15 +113,17 @@ async function main() {
|
|
|
72
113
|
return await cmdStatus(rest)
|
|
73
114
|
case 'dispatch':
|
|
74
115
|
return await cmdDispatch(rest)
|
|
75
|
-
default:
|
|
76
|
-
|
|
116
|
+
default: {
|
|
117
|
+
const s = suggest(cmd, ['collect', 'take', 'dispatch', 'status', 'guide'])
|
|
118
|
+
process.stderr.write(`未知命令:${cmd}${s ? `。你是不是想写 ${s}?` : ''}\n\n${USAGE}\n`)
|
|
77
119
|
return 2
|
|
120
|
+
}
|
|
78
121
|
}
|
|
79
122
|
}
|
|
80
123
|
|
|
81
124
|
async function cmdCollect(argv) {
|
|
82
125
|
const a = parseArgs(argv, { flags: ['help'], keys: COLLECT_KEYS })
|
|
83
|
-
if (a._unknown.length) return rejectUnknown('collect', a._unknown, COLLECT_KEYS)
|
|
126
|
+
if (a._unknown.length) return rejectUnknown('collect', argv, a._unknown, COLLECT_KEYS)
|
|
84
127
|
if (a.help) {
|
|
85
128
|
process.stdout.write(`${help.COLLECT_HELP}\n`)
|
|
86
129
|
return 0
|
|
@@ -95,11 +138,11 @@ async function cmdCollect(argv) {
|
|
|
95
138
|
|
|
96
139
|
async function cmdTake(argv) {
|
|
97
140
|
const a = parseArgs(argv, { flags: ['help'], keys: TAKE_KEYS })
|
|
98
|
-
if (a._unknown.length) return rejectUnknown('take', a._unknown, TAKE_KEYS)
|
|
141
|
+
if (a._unknown.length) return rejectUnknown('take', argv, a._unknown, TAKE_KEYS)
|
|
99
142
|
const larkcli = require('../lib/larkcli')
|
|
100
143
|
|
|
101
|
-
// 空参数输出完整照做指导,而非报错
|
|
102
|
-
// 但带了参数又缺 app/chats 的走下面短错误
|
|
144
|
+
// 空参数输出完整照做指导,而非报错 -- AI 的真实行为是「先空参数跑一下看看」。
|
|
145
|
+
// 但带了参数又缺 app/chats 的走下面短错误 -- 打印完整用法和 --help 长得一样,
|
|
103
146
|
// 会被当成「正常退出但没消息」(实测漏看 bot 第一条 Working)。
|
|
104
147
|
if (a.help || argv.length === 0) {
|
|
105
148
|
let apps = []
|
|
@@ -107,7 +150,7 @@ async function cmdTake(argv) {
|
|
|
107
150
|
apps = (await larkcli.listProfiles()).filter((p) => p.usable).map((p) => p.name)
|
|
108
151
|
} catch {}
|
|
109
152
|
process.stdout.write(`${help.takeHelp(apps)}\n`)
|
|
110
|
-
return
|
|
153
|
+
return 0 // 空参 = 求教,和 --help 一样是成功路径
|
|
111
154
|
}
|
|
112
155
|
|
|
113
156
|
const store = require('../lib/store')
|
|
@@ -116,7 +159,7 @@ async function cmdTake(argv) {
|
|
|
116
159
|
const { acquireExclusive } = require('../lib/lock')
|
|
117
160
|
|
|
118
161
|
if (!a.app || a.app === true) {
|
|
119
|
-
process.stderr.write('需要 --app <name
|
|
162
|
+
process.stderr.write('需要 --app <name>(= lark-cli profile 名)。完整步骤跑 `lark-relay take`\n')
|
|
120
163
|
return 2
|
|
121
164
|
}
|
|
122
165
|
const chats = parseChats(a.chats === true ? '' : a.chats)
|
|
@@ -155,6 +198,14 @@ async function cmdTake(argv) {
|
|
|
155
198
|
const maxWaitMs = num(a['max-wait'], 25) * 1000
|
|
156
199
|
const timeoutMs = num(a.timeout, 12 * 3600) * 1000
|
|
157
200
|
|
|
201
|
+
// 起来先打一行「监听中」-- 没这行就是没起来。
|
|
202
|
+
// 反馈实测:进程静默退出(参数错)被当成「在监听」,漏看 bot 第一条 Working
|
|
203
|
+
const fmtDur = (ms) => (ms >= 3600000 ? `${ms / 3600000}h` : `${ms / 1000}s`)
|
|
204
|
+
process.stderr.write(
|
|
205
|
+
`监听中:app=${a.app} 群=${chats.length} 个(${chats.join(',')}) 游标=${name} ` +
|
|
206
|
+
`防抖=${debounceMs / 1000}s 最长等=${fmtDur(timeoutMs)}\n`,
|
|
207
|
+
)
|
|
208
|
+
|
|
158
209
|
const ac = new AbortController()
|
|
159
210
|
const onSig = () => ac.abort()
|
|
160
211
|
process.on('SIGTERM', onSig)
|
|
@@ -172,7 +223,7 @@ async function cmdTake(argv) {
|
|
|
172
223
|
})
|
|
173
224
|
|
|
174
225
|
if (!res.events.length) {
|
|
175
|
-
process.stderr.write(`没等到消息(${res.reason})。游标身份 ${name}
|
|
226
|
+
process.stderr.write(`没等到消息(${res.reason})。游标身份 ${name},正常退出,再起一个继续\n`)
|
|
176
227
|
return res.reason === 'timeout' ? 4 : 0
|
|
177
228
|
}
|
|
178
229
|
|
|
@@ -184,7 +235,7 @@ async function cmdTake(argv) {
|
|
|
184
235
|
|
|
185
236
|
async function cmdStatus(argv) {
|
|
186
237
|
const a = parseArgs(argv, { flags: ['json', 'help'], keys: [] })
|
|
187
|
-
if (a._unknown.length) return rejectUnknown('status', a._unknown, ['json'])
|
|
238
|
+
if (a._unknown.length) return rejectUnknown('status', argv, a._unknown, ['json'])
|
|
188
239
|
if (a.help) {
|
|
189
240
|
process.stdout.write(`${help.STATUS_HELP}\n`)
|
|
190
241
|
return 0
|
|
@@ -198,7 +249,7 @@ async function cmdStatus(argv) {
|
|
|
198
249
|
|
|
199
250
|
async function cmdDispatch(argv) {
|
|
200
251
|
const a = parseArgs(argv, { flags: ['list', 'once', 'help'], keys: [] })
|
|
201
|
-
if (a._unknown.length) return rejectUnknown('dispatch', a._unknown, ['list', 'once'])
|
|
252
|
+
if (a._unknown.length) return rejectUnknown('dispatch', argv, a._unknown, ['list', 'once'])
|
|
202
253
|
if (a.help) {
|
|
203
254
|
process.stdout.write(`${help.DISPATCH_HELP}\n`)
|
|
204
255
|
return 0
|
|
@@ -207,7 +258,12 @@ async function cmdDispatch(argv) {
|
|
|
207
258
|
if (a.list) return dispatch.cmdList()
|
|
208
259
|
const task = a._[0]
|
|
209
260
|
if (!task) {
|
|
210
|
-
|
|
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')
|
|
211
267
|
return 2
|
|
212
268
|
}
|
|
213
269
|
return await dispatch.run(task, { once: !!a.once })
|
package/lib/help.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
// 帮助文本是 AI 的主入口
|
|
3
|
+
// 帮助文本是 AI 的主入口 -- 实际行为是「先空参数跑一下看看」,
|
|
4
4
|
// 故 take 空参输出必须是完整的照做指导,而非报错。
|
|
5
|
+
// 所有提示按「AI 读到后下一步该做什么」来写:给完整命令、给退出码语义、给判据。
|
|
5
6
|
//
|
|
6
7
|
// ⚠️ 公网包:示例只用占位符(oc_xxx/<app>/ou_xxx),绝不含真实 chat_id/open_id/profile 名。
|
|
7
8
|
|
|
@@ -9,34 +10,41 @@ function takeHelp(apps) {
|
|
|
9
10
|
const appLine = apps && apps.length ? apps.join(' ') : '(跑 `lark-cli profile list` 看)'
|
|
10
11
|
return `需要 --app 和 --chats。监听要求 bot 已在群内。
|
|
11
12
|
|
|
12
|
-
一、查群 ID
|
|
13
|
+
一、查群 ID(用户身份;bot 搜不到自己没加入的群)
|
|
13
14
|
lark-cli --profile <app> im +chat-search --as user --query "<群名>"
|
|
14
|
-
|
|
15
|
+
-> 取 oc_ 开头的 chat_id;多个匹配时向用户确认是哪个
|
|
15
16
|
|
|
16
|
-
二、验 bot
|
|
17
|
+
二、验 bot 在群(用户身份;bot 不在群时用 bot 身份查不到,不能作判据)
|
|
17
18
|
lark-cli --profile <app> im chat.members bots --as user --params '{"chat_id":"oc_xxx"}'
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
-> bots[] 含本 app 才算在群
|
|
20
|
+
-> 不在群:请用户拉 bot 进群(对群可见动作,先征得同意)
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
三、监听(Claude Code 用 run_in_background 起,有消息会自动通知你)
|
|
22
23
|
lark-relay take --app <app> --chats oc_xxx --render text
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
起来后 stderr 先打一行「监听中 ...」-- 没这行就是没起来,看 stderr 报错。
|
|
26
|
+
阻塞等消息,有一批就吐到 stdout 并退出(默认防抖 5s、最长等 12h)。
|
|
25
27
|
处理完再起一个,如此循环。
|
|
26
28
|
|
|
27
|
-
可用 app
|
|
29
|
+
可用 app:${appLine}
|
|
28
30
|
|
|
29
31
|
参数
|
|
30
32
|
--app <name> 必填,= lark-cli profile 名
|
|
31
|
-
--chats <ids> 必填,oc_ 开头;逗号分隔多个;或
|
|
33
|
+
--chats <ids> 必填,oc_ 开头;逗号分隔多个;或 @文件(首列 chat_id)
|
|
32
34
|
--name <n> 游标身份,省略时按 app+chats 自动派生
|
|
33
35
|
--filter <jq> 业务过滤,如 '.sender_id != "ou_xxx"'
|
|
34
|
-
--debounce N
|
|
35
|
-
--render text
|
|
36
|
-
--since now|all
|
|
36
|
+
--debounce N 防抖秒数(默认 5;聊天场景建议 15)
|
|
37
|
+
--render text 按群分组紧凑文本(默认 NDJSON)
|
|
38
|
+
--since now|all 起始位置(默认 now,不重放历史)
|
|
39
|
+
|
|
40
|
+
退出码
|
|
41
|
+
0 吐了一批(stdout);空参求教也是 0
|
|
42
|
+
2 参数错 -- stderr 有短错误和正确写法,照着重跑
|
|
43
|
+
3 游标身份被另一个 take 占用 -- 换 --name
|
|
44
|
+
4 超时没消息 -- 正常,再起一个继续
|
|
37
45
|
|
|
38
46
|
最佳实践
|
|
39
|
-
· 用 run_in_background 起,不要前台跑
|
|
47
|
+
· 用 run_in_background 起,不要前台跑 -- 会阻塞会话最长 12h
|
|
40
48
|
· 一批处理完再起下一个;不需要自己写 while 循环
|
|
41
49
|
· 拿不到消息先查第二步:bot 不在群是最常见原因
|
|
42
50
|
· 多个消费者盯同一 app 用不同 --name,游标互不干扰`
|
|
@@ -44,109 +52,125 @@ function takeHelp(apps) {
|
|
|
44
52
|
|
|
45
53
|
const COLLECT_HELP = `把全部 lark-cli profile 的事件收下来,原子落盘。零参数、零配置。
|
|
46
54
|
|
|
47
|
-
lark-relay collect #
|
|
48
|
-
systemctl enable --now lark-relay-collect #
|
|
55
|
+
lark-relay collect # 前台跑(调试)
|
|
56
|
+
systemctl enable --now lark-relay-collect # 常驻(推荐)
|
|
49
57
|
|
|
50
58
|
app 列表实时读 \`lark-cli profile list\`,新增 profile 自动纳入,无需改配置。
|
|
51
59
|
每 app 一个子进程;某个挂了单独重启,不影响其他。
|
|
52
|
-
tokenStatus 为 expired 的 profile 自动跳过并 warn
|
|
60
|
+
tokenStatus 为 expired 的 profile 自动跳过并 warn(永久失败,重试是死循环)。
|
|
53
61
|
用户重新 login 后下次启动自动纳入。
|
|
54
62
|
|
|
55
|
-
为什么必须常驻:lark
|
|
56
|
-
8s 后才起 consumer,收到 0
|
|
63
|
+
为什么必须常驻:lark 事件是流式的,进程不在的时刻消息永久丢失(实测:消息发出
|
|
64
|
+
8s 后才起 consumer,收到 0 条)。collect 独立常驻,才能让 take/dispatch 侧
|
|
57
65
|
崩了、claude 跑 30 分钟、会话关几小时,都不丢消息。
|
|
58
66
|
|
|
67
|
+
装完先 \`lark-relay status\` 确认 -- collect 行应为 running;不在跑看
|
|
68
|
+
\`systemctl status lark-relay-collect\` 的日志。
|
|
69
|
+
|
|
59
70
|
落盘:~/.lark-relay/store/<app>/<YYYY-MM-DD>/<纳秒>_<pid>_<seq>.json
|
|
60
|
-
回收由 collect
|
|
71
|
+
回收由 collect 进程内每小时自查一次,删超期的整个日期目录(不另起 gc 单元/timer)。
|
|
61
72
|
|
|
62
73
|
参数
|
|
63
|
-
--exclude <apps> 排除指定 profile
|
|
64
|
-
--retain N store
|
|
65
|
-
--ledger-retain N ledger
|
|
74
|
+
--exclude <apps> 排除指定 profile(逗号分隔)
|
|
75
|
+
--retain N store 保留天数(默认 3)
|
|
76
|
+
--ledger-retain N ledger 保留天数(默认 90)`
|
|
66
77
|
|
|
67
78
|
const STATUS_HELP = `一眼看清全局:谁在跑、积压多少、游标在哪。
|
|
68
79
|
|
|
69
80
|
lark-relay status # 概览
|
|
70
81
|
lark-relay status --json # 机器可读
|
|
71
82
|
|
|
72
|
-
排障入口:某消费者「落后」很多
|
|
73
|
-
「无消费者」= 白采,可考虑 --exclude
|
|
83
|
+
排障入口:某消费者「落后」很多 -> 它的 take/dispatch 没在跑或卡住;
|
|
84
|
+
「无消费者」= 白采,可考虑 --exclude。
|
|
85
|
+
collect 行不是 running -> \`systemctl status lark-relay-collect\` 看日志,
|
|
86
|
+
collect 停摆的每一秒都在丢消息。`
|
|
74
87
|
|
|
75
|
-
const DISPATCH_HELP = `常驻:等消息
|
|
88
|
+
const DISPATCH_HELP = `常驻:等消息 -> 唤起 claude 干活 -> 结果回群。无人在场也跑。
|
|
76
89
|
|
|
77
90
|
lark-relay dispatch --list # 列出所有 dispatch 任务及状态
|
|
78
|
-
lark-relay dispatch <task> --once #
|
|
91
|
+
lark-relay dispatch <task> --once # 前台跑一轮(调试,不进 systemd)
|
|
79
92
|
systemctl enable --now lark-relay-dispatch@<task>
|
|
80
93
|
|
|
81
94
|
--list 输出:任务名 / app / 群数 / systemd 单元是否 active / 上次 ledger 时间
|
|
82
95
|
|
|
83
|
-
任务发现:扫 $LR_ROOT/*/dispatch.yaml
|
|
84
|
-
LR_ROOT
|
|
96
|
+
任务发现:扫 $LR_ROOT/*/dispatch.yaml -- 有文件即是任务,无需注册表。
|
|
97
|
+
LR_ROOT 在单元里指定(唯一的目录绑定,与代码无关)。
|
|
85
98
|
|
|
86
|
-
dispatch.yaml
|
|
99
|
+
dispatch.yaml(4 必填 + 3 可选)
|
|
87
100
|
app: <profile 名>
|
|
88
101
|
chats: [oc_xxx]
|
|
89
102
|
dirs: [/path/to/repo] # 首个 = 主工作目录
|
|
90
103
|
instructions: ./instructions.md # 职责/边界,--append-system-prompt-file 注入
|
|
91
104
|
filter: '.mentions[]?.id == "ou_xxx"' # 可选
|
|
92
|
-
# mode: work 默认;work | chat | agent
|
|
105
|
+
# mode: work 默认;work | chat | agent(见下)
|
|
93
106
|
# model: <名称> 默认走终端同一套默认路由
|
|
94
107
|
|
|
95
|
-
mode
|
|
108
|
+
mode 三个场景(一个键定死全部行为,不用拼组合)
|
|
96
109
|
work 处理工作:话题内回复 + 过程挂 COT 消息 + 结论发卡片
|
|
97
|
-
+ 按 thread_id 隔离 session + 防抖 1s
|
|
98
|
-
COT 要求客户端 PC
|
|
99
|
-
「Completed
|
|
100
|
-
chat 简单问答:直发群里 + 纯文本 +
|
|
101
|
-
+ 防抖 15s
|
|
110
|
+
+ 按 thread_id 隔离 session + 防抖 1s(一问一答要跟手)
|
|
111
|
+
COT 要求客户端 PC >= 7.70 / 移动 >= 7.74;老客户端那条过程消息显示为
|
|
112
|
+
「Completed」(不崩),结论卡片不受影响
|
|
113
|
+
chat 简单问答:直发群里 + 纯文本 + 按(群, epoch)隔离 session
|
|
114
|
+
+ 防抖 15s(等人打完多行)
|
|
102
115
|
agent 模型自己当运营者:引擎只拉起它 + 传消息 + 记台账,**一条消息都不发**。
|
|
103
116
|
发不发 / 回哪条 / 用哪个 bot 身份 / 文字还是表情,全由模型按
|
|
104
|
-
instructions
|
|
105
|
-
群运营
|
|
117
|
+
instructions 决定(它有 Bash + lark-cli)。适合「多数轮该沉默」的
|
|
118
|
+
群运营 -- work/chat 会逼模型每轮都产出一段发回群的话
|
|
106
119
|
|
|
107
120
|
最佳实践
|
|
108
|
-
· 边界写 instructions,别指望 --add-dir 目录的 CLAUDE.md
|
|
121
|
+
· 边界写 instructions,别指望 --add-dir 目录的 CLAUDE.md(启动不加载)
|
|
109
122
|
· 改完 yaml 先 --once 跑一轮验证,再 enable
|
|
110
|
-
·
|
|
123
|
+
· 高风险发布仍建议人工执行
|
|
124
|
+
|
|
125
|
+
退出码(--once)
|
|
126
|
+
0 跑了一轮,或没等到消息但正常结束
|
|
127
|
+
1 取批异常(看 stderr)
|
|
128
|
+
2 任务不存在或配置错
|
|
129
|
+
4 等消息超时(5min 没消息)`
|
|
111
130
|
|
|
112
|
-
const GUIDE = `lark-relay
|
|
131
|
+
const GUIDE = `lark-relay -- Lark 事件中继站。三个命令:收下来 / 我来取 / 派给别人。
|
|
113
132
|
|
|
114
|
-
|
|
133
|
+
先判场景(两者不可混用)
|
|
115
134
|
┌──────────┬────────────────────────┬────────────────────────┐
|
|
116
135
|
│ │ 在场取用 take │ 托管派活 dispatch │
|
|
117
136
|
├──────────┼────────────────────────┼────────────────────────┤
|
|
118
|
-
│ 长命的 │ AI
|
|
119
|
-
│ 短命的 │
|
|
137
|
+
│ 长命的 │ AI 会话(人设、上下文) │ 消息(等在队列里) │
|
|
138
|
+
│ 短命的 │ 消息(来一批处理一批) │ AI(每轮新起,跑完就没)│
|
|
120
139
|
│ 谁等谁 │ AI 等消息 │ 消息等 AI │
|
|
121
140
|
│ 控制权 │ AI 手里 │ 服务手里 │
|
|
122
141
|
│ 谁能干 │ 任何 agent │ 需内置唤起知识 │
|
|
123
142
|
│ 配置 │ 纯参数,无文件 │ dispatch.yaml │
|
|
124
143
|
└──────────┴────────────────────────┴────────────────────────┘
|
|
125
144
|
|
|
126
|
-
|
|
145
|
+
你(AI)在会话里盯群 -> take。无人在场也要干活 -> dispatch。
|
|
127
146
|
|
|
128
147
|
前置:collect 必须在跑
|
|
129
148
|
lark-relay status # collect 行应为 running
|
|
130
149
|
进程不在的时刻消息永久丢失,不是延迟送达。
|
|
150
|
+
不在跑 -> systemctl enable --now lark-relay-collect(或 \`lark-relay collect\` 前台调试)
|
|
131
151
|
|
|
132
|
-
take
|
|
152
|
+
take 三步(照做)
|
|
133
153
|
1. 查群 ID lark-cli --profile <app> im +chat-search --as user --query "<群名>"
|
|
134
154
|
2. 验 bot 在群 lark-cli --profile <app> im chat.members bots --as user \\
|
|
135
155
|
--params '{"chat_id":"oc_xxx"}'
|
|
136
|
-
3.
|
|
156
|
+
3. 监听(用 run_in_background 起,别前台)
|
|
137
157
|
lark-relay take --app <app> --chats oc_xxx --render text
|
|
138
|
-
|
|
158
|
+
起来后 stderr 有一行「监听中」-- 没有就是没起来,看 stderr。
|
|
159
|
+
一批处理完再起一个。不要自己写 while 循环 -- 进程退出会通知你。
|
|
160
|
+
退出码:0=有一批,4=超时没消息(正常,再起一个),2=参数错,3=游标被占用。
|
|
139
161
|
|
|
140
162
|
建 dispatch 任务
|
|
141
|
-
1. 在任务目录写 dispatch.yaml
|
|
163
|
+
1. 在任务目录写 dispatch.yaml(\`lark-relay dispatch --help\` 有模板)
|
|
142
164
|
2. lark-relay dispatch <task> --once # 前台验一轮
|
|
143
165
|
3. systemctl enable --now lark-relay-dispatch@<task>
|
|
144
166
|
|
|
145
167
|
常见错误
|
|
146
|
-
· 拿不到消息
|
|
168
|
+
· 拿不到消息 -> 九成是 bot 不在群。验 bot 必须 --as user,
|
|
147
169
|
bot 不在群时用 bot 身份查不到,「查询失败」不能推断「不在群」
|
|
148
|
-
·
|
|
149
|
-
|
|
170
|
+
· 起来后没有「监听中」那行 -> 没起来;参数拼错会直接报错并给正确写法,
|
|
171
|
+
不会静默
|
|
172
|
+
· --chats 只接受 oc_ 开头的 chat_id,不接群名 -- 群名会匹配错群,
|
|
173
|
+
而监听错群是静默失败(你会一直等一个永不来的消息)
|
|
150
174
|
· 默认 --since now 不重放历史。要历史用 --since all
|
|
151
175
|
· 多个消费者盯同一 app 必须用不同 --name,否则互相推游标撕批次
|
|
152
176
|
|