lark-relay 0.4.8 → 0.4.9
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 +7 -10
- 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 +3 -3
- 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
|
-
(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
| 控制权 | AI 手里(自己决定何时再取) |
|
|
36
|
-
| 谁能干 | 任何 agent(Claude Code / Codex / 手敲) |
|
|
37
|
-
| 配置 | 纯参数,无文件 |
|
|
28
|
+
(launchd)展示的是可执行文件名,与 label 同名才认得出是谁。
|
|
29
|
+
|
|
30
|
+
`take` 的形态:
|
|
31
|
+
|
|
32
|
+
- **长命的是 AI 会话**(人设、上下文连续),**短命的是消息**(来一批处理一批)
|
|
33
|
+
- **AI 等消息**,不是消息推给 AI —— 控制权在 AI 手里,自己决定何时再取
|
|
34
|
+
- 任何 agent 都能干(Claude Code / Codex / 手敲),纯参数、无配置文件
|
|
38
35
|
|
|
39
36
|
## 命令
|
|
40
37
|
|
|
@@ -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
|
@@ -53,10 +53,10 @@ function takeHelp(apps) {
|
|
|
53
53
|
const COLLECT_HELP = `把全部 lark-cli profile 的事件收下来,原子落盘。零参数、零配置。
|
|
54
54
|
|
|
55
55
|
lark-relay-collect # 前台跑(调试)
|
|
56
|
-
常驻:
|
|
56
|
+
常驻:macOS 用 launchd(plist 模板见仓库 launchd/ 目录)
|
|
57
57
|
|
|
58
58
|
独立可执行文件而非 \`lark-relay\` 的子命令 —— 它是常驻服务,服务管理器展示的是
|
|
59
|
-
可执行文件名,与 launchd label(com.adaex.lark-relay-collect)
|
|
59
|
+
可执行文件名,与 launchd label(com.adaex.lark-relay-collect)同名才认得出。
|
|
60
60
|
|
|
61
61
|
app 列表实时读 \`lark-cli profile list\`,新增 profile 自动纳入,无需改配置。
|
|
62
62
|
每 app 一个子进程;某个挂了单独重启,不影响其他。
|
|
@@ -104,7 +104,7 @@ const GUIDE = `lark-relay -- Lark 事件中继站。两件事:收下来 / 我来
|
|
|
104
104
|
前置:采集必须在跑
|
|
105
105
|
lark-relay status # collect 行应为 running
|
|
106
106
|
进程不在的时刻消息永久丢失,不是延迟送达。
|
|
107
|
-
不在跑 -> 起常驻服务(
|
|
107
|
+
不在跑 -> 起常驻服务(macOS launchd,plist 模板见仓库;
|
|
108
108
|
或 \`lark-relay-collect\` 前台调试)
|
|
109
109
|
|
|
110
110
|
take 三步(照做)
|
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.9",
|
|
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
|
}
|