botmux 2.78.0 → 2.79.1
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/dist/bot-registry.d.ts +7 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +3 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +71 -8
- package/dist/cli.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +90 -2
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +4 -0
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/session-discovery.d.ts +66 -0
- package/dist/core/session-discovery.d.ts.map +1 -1
- package/dist/core/session-discovery.js +93 -0
- package/dist/core/session-discovery.js.map +1 -1
- package/dist/core/session-marker.d.ts +19 -0
- package/dist/core/session-marker.d.ts.map +1 -1
- package/dist/core/session-marker.js +26 -0
- package/dist/core/session-marker.js.map +1 -1
- package/dist/core/types.d.ts +11 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +48 -0
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +145 -0
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +25 -0
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +8 -0
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard-web/app.js +283 -270
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +17 -2
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +17 -2
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/doc-comment.d.ts +97 -0
- package/dist/im/lark/doc-comment.d.ts.map +1 -0
- package/dist/im/lark/doc-comment.js +376 -0
- package/dist/im/lark/doc-comment.js.map +1 -0
- package/dist/im/lark/event-dispatcher.d.ts +18 -0
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +155 -1
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/services/card-prefs-store.d.ts +2 -0
- package/dist/services/card-prefs-store.d.ts.map +1 -1
- package/dist/services/card-prefs-store.js +16 -0
- package/dist/services/card-prefs-store.js.map +1 -1
- package/dist/services/doc-subs-store.d.ts +43 -0
- package/dist/services/doc-subs-store.d.ts.map +1 -0
- package/dist/services/doc-subs-store.js +83 -0
- package/dist/services/doc-subs-store.js.map +1 -0
- package/dist/setup/verify-permissions.d.ts +4 -0
- package/dist/setup/verify-permissions.d.ts.map +1 -1
- package/dist/setup/verify-permissions.js +21 -0
- package/dist/setup/verify-permissions.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/user-token.d.ts +11 -1
- package/dist/utils/user-token.d.ts.map +1 -1
- package/dist/utils/user-token.js +20 -2
- package/dist/utils/user-token.js.map +1 -1
- package/dist/worker.js +45 -0
- package/dist/worker.js.map +1 -1
- package/dist/workflows/definition.d.ts +8 -8
- package/package.json +1 -1
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 飞书云文档「评论」桥接 —— 把一个 docx 文档变成会话的输入/输出通道。
|
|
3
|
+
*
|
|
4
|
+
* 这一层封装四件事:
|
|
5
|
+
* 1. 把用户贴的文档链接 / token 解析成 { fileToken, fileType }(含 wiki 节点解析)
|
|
6
|
+
* 2. 订阅 / 退订文档事件(评论新增等靠此推送)
|
|
7
|
+
* 3. 读评论(取上下文 / 解析 @bot)
|
|
8
|
+
* 4. 回评论(bot 的回复落点 —— 注意飞书无「往已有评论追加回复」的公开 API,
|
|
9
|
+
* 只能新建一条全文评论,见 {@link createDocComment})
|
|
10
|
+
*
|
|
11
|
+
* 身份:评论 / 订阅事件官方推荐 user_access_token(文档可见性跟着授权用户走)。
|
|
12
|
+
* 因此所有调用 **优先 user token**(裸 fetch + Bearer),失败再回退 tenant(SDK
|
|
13
|
+
* client.request,自动带 tenant_access_token)。和 client.ts 的资源下载同套路,
|
|
14
|
+
* 只是 app/user 的优先级反过来。
|
|
15
|
+
*/
|
|
16
|
+
import { getBotClient, getBot } from '../../bot-registry.js';
|
|
17
|
+
import { resolveUserToken } from '../../utils/user-token.js';
|
|
18
|
+
import { logger } from '../../utils/logger.js';
|
|
19
|
+
import { UserTokenMissingError } from './client.js';
|
|
20
|
+
import { larkHosts, normalizeBrand } from './lark-hosts.js';
|
|
21
|
+
/**
|
|
22
|
+
* bot 回复的隐形哨兵:追加在 bot 发表的评论末尾(零宽字符,用户不可见)。
|
|
23
|
+
*
|
|
24
|
+
* 为什么需要:bot 用 **user_access_token** 发评论 → 评论作者 = 授权用户本人,
|
|
25
|
+
* 无法靠「作者是不是 bot」区分「bot 的回复」和「用户自己的评论」。若不区分,
|
|
26
|
+
* bot 发评论 → 触发 comment_add 事件 → 又喂给 bot → 死循环。
|
|
27
|
+
*
|
|
28
|
+
* 双保险:① 记录 bot 创建过的 reply_id({@link isBotAuthoredReply},同一 daemon
|
|
29
|
+
* 生命周期内权威)② 文本末尾哨兵(跨重启 / reply_id 拿不到时的兜底)。
|
|
30
|
+
*/
|
|
31
|
+
export const BOT_REPLY_SENTINEL = '';
|
|
32
|
+
/** 记录 / 查询 bot 自己创建的评论回复 id(防自触发死循环)。环形上限防泄漏。 */
|
|
33
|
+
const botAuthoredReplyIds = [];
|
|
34
|
+
const BOT_AUTHORED_MAX = 2000;
|
|
35
|
+
export function markBotAuthoredReply(id) {
|
|
36
|
+
if (!id)
|
|
37
|
+
return;
|
|
38
|
+
botAuthoredReplyIds.push(id);
|
|
39
|
+
if (botAuthoredReplyIds.length > BOT_AUTHORED_MAX)
|
|
40
|
+
botAuthoredReplyIds.splice(0, botAuthoredReplyIds.length - BOT_AUTHORED_MAX);
|
|
41
|
+
}
|
|
42
|
+
export function isBotAuthoredReply(id) {
|
|
43
|
+
return !!id && botAuthoredReplyIds.includes(id);
|
|
44
|
+
}
|
|
45
|
+
export function hasBotSentinel(text) {
|
|
46
|
+
return !!text && text.includes(BOT_REPLY_SENTINEL);
|
|
47
|
+
}
|
|
48
|
+
// ─── URL / token 解析 ──────────────────────────────────────────────────────────
|
|
49
|
+
const URL_TYPE_RE = /\/(docx|docs|wiki|sheets|base|bitable|file|slides|mindnote)\/([A-Za-z0-9]+)/;
|
|
50
|
+
const RAW_TOKEN_RE = /^[A-Za-z0-9]{20,}$/;
|
|
51
|
+
/** URL path 段的类型 → 飞书 file_type。 */
|
|
52
|
+
function pathKindToFileType(kind) {
|
|
53
|
+
switch (kind) {
|
|
54
|
+
case 'docx': return 'docx';
|
|
55
|
+
case 'docs': return 'doc';
|
|
56
|
+
case 'sheets': return 'sheet';
|
|
57
|
+
case 'base':
|
|
58
|
+
case 'bitable': return 'bitable';
|
|
59
|
+
case 'slides': return 'slides';
|
|
60
|
+
case 'mindnote': return 'mindnote';
|
|
61
|
+
case 'file': return 'file';
|
|
62
|
+
default: return kind;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 把用户输入解析成 { kind, token }。支持完整飞书链接、`/docx/<token>` 片段、
|
|
67
|
+
* 或裸 token(裸 token 当 docx 处理)。`wiki` 类型需要再过一次节点解析(见
|
|
68
|
+
* {@link resolveDocFile})。无法识别返回 null。
|
|
69
|
+
*/
|
|
70
|
+
export function parseDocRef(input) {
|
|
71
|
+
const s = input.trim();
|
|
72
|
+
const m = s.match(URL_TYPE_RE);
|
|
73
|
+
if (m)
|
|
74
|
+
return { kind: m[1], token: m[2] };
|
|
75
|
+
if (RAW_TOKEN_RE.test(s))
|
|
76
|
+
return { kind: 'docx', token: s };
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 解析成可直接调评论 / 订阅 API 的 { fileToken, fileType }。wiki 节点先调
|
|
81
|
+
* get_node 换出底层 obj_token + obj_type;其余类型直接映射。
|
|
82
|
+
*/
|
|
83
|
+
export async function resolveDocFile(larkAppId, input) {
|
|
84
|
+
const ref = parseDocRef(input);
|
|
85
|
+
if (!ref)
|
|
86
|
+
throw new Error(`无法从「${input.slice(0, 40)}」识别出飞书文档链接或 token`);
|
|
87
|
+
if (ref.kind === 'wiki') {
|
|
88
|
+
const res = await driveApiCall(larkAppId, {
|
|
89
|
+
method: 'GET',
|
|
90
|
+
path: '/open-apis/wiki/v2/spaces/get_node',
|
|
91
|
+
params: { token: ref.token, obj_type: 'wiki' },
|
|
92
|
+
});
|
|
93
|
+
const node = res?.data?.node;
|
|
94
|
+
if (!node?.obj_token || !node?.obj_type) {
|
|
95
|
+
throw new Error(`wiki 节点 ${ref.token} 解析失败(缺 obj_token/obj_type)`);
|
|
96
|
+
}
|
|
97
|
+
return { fileToken: node.obj_token, fileType: node.obj_type };
|
|
98
|
+
}
|
|
99
|
+
return { fileToken: ref.token, fileType: pathKindToFileType(ref.kind) };
|
|
100
|
+
}
|
|
101
|
+
function buildQuery(params) {
|
|
102
|
+
if (!params)
|
|
103
|
+
return '';
|
|
104
|
+
const usp = new URLSearchParams();
|
|
105
|
+
for (const [k, v] of Object.entries(params)) {
|
|
106
|
+
if (v !== undefined)
|
|
107
|
+
usp.set(k, String(v));
|
|
108
|
+
}
|
|
109
|
+
const q = usp.toString();
|
|
110
|
+
return q ? `?${q}` : '';
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 调一个 drive/wiki OpenAPI。优先 user token(裸 fetch),拿不到 token 或遇
|
|
114
|
+
* 401/403 时回退 tenant(SDK client.request 自带 tenant_access_token + GET
|
|
115
|
+
* 空 body 守卫)。返回飞书统一响应体 `{ code, msg, data }`。
|
|
116
|
+
*/
|
|
117
|
+
async function driveApiCall(larkAppId, opts) {
|
|
118
|
+
const bot = getBot(larkAppId);
|
|
119
|
+
const brand = normalizeBrand(bot.config.brand);
|
|
120
|
+
// tenant(应用身份):走 SDK client.request(带 token/缓存/GET 空 body 守卫)。
|
|
121
|
+
const callTenant = async () => {
|
|
122
|
+
const c = getBotClient(larkAppId);
|
|
123
|
+
return c.request({
|
|
124
|
+
method: opts.method,
|
|
125
|
+
url: opts.path,
|
|
126
|
+
params: opts.params,
|
|
127
|
+
...(opts.data !== undefined ? { data: opts.data } : {}),
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
const callUser = async () => {
|
|
131
|
+
const userToken = await resolveUserToken(bot.config.larkAppId, bot.config.larkAppSecret, brand);
|
|
132
|
+
if (!userToken)
|
|
133
|
+
throw new UserTokenMissingError('该操作需要 User Token(请在话题中 /login 授权)。');
|
|
134
|
+
return fetchWithUserToken(brand, userToken, opts);
|
|
135
|
+
};
|
|
136
|
+
if (opts.userOnly)
|
|
137
|
+
return callUser();
|
|
138
|
+
// 发评论:优先应用身份(回复显示为 bot),bot 无访问权(抛错或 code!=0)时回退用户身份。
|
|
139
|
+
if (opts.preferTenant) {
|
|
140
|
+
try {
|
|
141
|
+
const res = await callTenant();
|
|
142
|
+
if (res?.code === 0)
|
|
143
|
+
return res;
|
|
144
|
+
logger.debug(`[doc-comment] tenant call code=${res?.code} (${opts.path});回退 user 身份`);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
logger.debug(`[doc-comment] tenant call threw (${opts.path});回退 user 身份:${err instanceof Error ? err.message : err}`);
|
|
148
|
+
}
|
|
149
|
+
return callUser();
|
|
150
|
+
}
|
|
151
|
+
// 默认:优先 user(有 token),401/403 回退 tenant。
|
|
152
|
+
const userToken = await resolveUserToken(bot.config.larkAppId, bot.config.larkAppSecret, brand);
|
|
153
|
+
if (userToken) {
|
|
154
|
+
try {
|
|
155
|
+
return await fetchWithUserToken(brand, userToken, opts);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
if (!(err instanceof UserTokenMissingError))
|
|
159
|
+
throw err;
|
|
160
|
+
logger.debug(`[doc-comment] user token rejected (${opts.path}); falling back to tenant`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return callTenant();
|
|
164
|
+
}
|
|
165
|
+
async function fetchWithUserToken(brand, userToken, opts) {
|
|
166
|
+
const url = `${larkHosts(brand).openApi}${opts.path}${buildQuery(opts.params)}`;
|
|
167
|
+
const res = await fetch(url, {
|
|
168
|
+
method: opts.method,
|
|
169
|
+
headers: {
|
|
170
|
+
Authorization: `Bearer ${userToken}`,
|
|
171
|
+
...(opts.data !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
172
|
+
},
|
|
173
|
+
...(opts.data !== undefined ? { body: JSON.stringify(opts.data) } : {}),
|
|
174
|
+
});
|
|
175
|
+
if (res.status === 401) {
|
|
176
|
+
throw new UserTokenMissingError('User Token 已失效(HTTP 401)。请在话题中 /login 重新授权。');
|
|
177
|
+
}
|
|
178
|
+
if (res.status === 403) {
|
|
179
|
+
// token 有效但无权访问该文档 —— 视作可回退(也许 tenant 有权)
|
|
180
|
+
throw new UserTokenMissingError(`User Token 无权访问该文档(HTTP 403)。`);
|
|
181
|
+
}
|
|
182
|
+
const body = await res.json().catch(() => ({}));
|
|
183
|
+
if (!res.ok) {
|
|
184
|
+
throw new Error(`drive API ${opts.path} HTTP ${res.status}: ${body?.msg ?? ''}`);
|
|
185
|
+
}
|
|
186
|
+
return body;
|
|
187
|
+
}
|
|
188
|
+
function ensureOk(res, what) {
|
|
189
|
+
if (res?.code !== 0) {
|
|
190
|
+
throw new Error(`${what} 失败: ${res?.msg ?? 'unknown'} (code: ${res?.code})`);
|
|
191
|
+
}
|
|
192
|
+
return res.data;
|
|
193
|
+
}
|
|
194
|
+
// ─── 订阅 / 退订 ────────────────────────────────────────────────────────────────
|
|
195
|
+
/** 订阅文档事件(评论新增等靠此推送)。幂等:重复订阅飞书返回成功。 */
|
|
196
|
+
export async function subscribeDocFile(larkAppId, file) {
|
|
197
|
+
const res = await driveApiCall(larkAppId, {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
path: `/open-apis/drive/v1/files/${encodeURIComponent(file.fileToken)}/subscribe`,
|
|
200
|
+
params: { file_type: file.fileType },
|
|
201
|
+
});
|
|
202
|
+
ensureOk(res, '订阅文档');
|
|
203
|
+
logger.info(`[doc-comment] subscribed file=${file.fileToken.slice(0, 12)} type=${file.fileType}`);
|
|
204
|
+
}
|
|
205
|
+
/** 退订文档事件。best-effort:失败只告警不抛。 */
|
|
206
|
+
export async function unsubscribeDocFile(larkAppId, file) {
|
|
207
|
+
try {
|
|
208
|
+
// 飞书取消订阅是 DELETE .../delete_subscribe(不是 DELETE .../subscribe,后者 404)。
|
|
209
|
+
const res = await driveApiCall(larkAppId, {
|
|
210
|
+
method: 'DELETE',
|
|
211
|
+
path: `/open-apis/drive/v1/files/${encodeURIComponent(file.fileToken)}/delete_subscribe`,
|
|
212
|
+
params: { file_type: file.fileType },
|
|
213
|
+
});
|
|
214
|
+
ensureOk(res, '退订文档');
|
|
215
|
+
logger.info(`[doc-comment] unsubscribed file=${file.fileToken.slice(0, 12)}`);
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
logger.warn(`[doc-comment] unsubscribe failed for ${file.fileToken.slice(0, 12)}: ${err instanceof Error ? err.message : err}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// ─── 读评论 ─────────────────────────────────────────────────────────────────────
|
|
222
|
+
/** 拼接评论内容元素为纯文本。 */
|
|
223
|
+
function elementsToText(elements) {
|
|
224
|
+
if (!Array.isArray(elements))
|
|
225
|
+
return '';
|
|
226
|
+
return elements.map((el) => el?.text_run?.text ?? '').join('');
|
|
227
|
+
}
|
|
228
|
+
/** 从评论内容元素提取 @ 到的 open_id。 */
|
|
229
|
+
function elementsMentions(elements) {
|
|
230
|
+
if (!Array.isArray(elements))
|
|
231
|
+
return [];
|
|
232
|
+
return elements.map((el) => el?.person?.user_id).filter((x) => typeof x === 'string');
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* 读某条评论(含其下所有回复)。用于事件来后取 thread 上下文 + 判断是否 @bot。
|
|
236
|
+
* 拿不到返回 null。
|
|
237
|
+
*/
|
|
238
|
+
export async function getDocComment(larkAppId, file, commentId) {
|
|
239
|
+
try {
|
|
240
|
+
// 用 batch_query 而非 GET /comments/{id}——后者只认「全文评论」,对**局部/锚定
|
|
241
|
+
// 评论**(is_whole:false,真实用户在 UI 选中文字评论就是这种)返回 1069307 not exist。
|
|
242
|
+
// batch_query 两种都支持。
|
|
243
|
+
const res = await driveApiCall(larkAppId, {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
path: `/open-apis/drive/v1/files/${encodeURIComponent(file.fileToken)}/comments/batch_query`,
|
|
246
|
+
params: { file_type: file.fileType, user_id_type: 'open_id' },
|
|
247
|
+
data: { comment_ids: [commentId] },
|
|
248
|
+
});
|
|
249
|
+
const data = ensureOk(res, '获取评论');
|
|
250
|
+
const raw = Array.isArray(data?.items) ? data.items[0] : undefined;
|
|
251
|
+
if (!raw)
|
|
252
|
+
return null;
|
|
253
|
+
return normalizeComment(raw);
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
logger.warn(`[doc-comment] getDocComment ${commentId.slice(0, 12)} failed: ${err instanceof Error ? err.message : err}`);
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function normalizeComment(raw) {
|
|
261
|
+
const replies = Array.isArray(raw?.reply_list?.replies) ? raw.reply_list.replies : [];
|
|
262
|
+
return {
|
|
263
|
+
commentId: raw?.comment_id ?? '',
|
|
264
|
+
isSolved: raw?.is_solved === true,
|
|
265
|
+
replies: replies.map((r) => ({
|
|
266
|
+
replyId: r?.reply_id ?? '',
|
|
267
|
+
userId: r?.user_id,
|
|
268
|
+
text: elementsToText(r?.content?.elements),
|
|
269
|
+
mentions: elementsMentions(r?.content?.elements),
|
|
270
|
+
createdAt: typeof r?.create_time === 'number' ? r.create_time : undefined,
|
|
271
|
+
})),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
// ─── 回评论 ─────────────────────────────────────────────────────────────────────
|
|
275
|
+
/**
|
|
276
|
+
* 往**已有评论 thread 里追加一条回复**(真正的嵌套回复,用户看到 bot 的回复
|
|
277
|
+
* 就挂在自己那条评论下面)。
|
|
278
|
+
*
|
|
279
|
+
* 端点 `POST .../comments/{comment_id}/replies` 是飞书 drive-v1 的公开 API
|
|
280
|
+
* (file.comment.reply.create)—— 我们装的 node-sdk 1.64.0 恰好没暴露 create,
|
|
281
|
+
* 但裸 endpoint 存在,故这里直接打。返回新回复的 reply_id(已登记防自触发)。
|
|
282
|
+
*/
|
|
283
|
+
export async function replyToDocComment(larkAppId, file, commentId, text, mentionOpenId) {
|
|
284
|
+
const elements = buildCommentElements(text, mentionOpenId);
|
|
285
|
+
let res;
|
|
286
|
+
try {
|
|
287
|
+
res = await driveApiCall(larkAppId, {
|
|
288
|
+
method: 'POST',
|
|
289
|
+
path: `/open-apis/drive/v1/files/${encodeURIComponent(file.fileToken)}/comments/${encodeURIComponent(commentId)}/replies`,
|
|
290
|
+
params: { file_type: file.fileType, user_id_type: 'open_id' },
|
|
291
|
+
data: { content: { elements } },
|
|
292
|
+
preferTenant: true, // 回复显示为 bot 本身(应用身份);bot 无访问权时回退 user
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
// 有的评论不允许被回复(飞书 1069302:全文评论 / 已解决 / 文档评论设置受限)。
|
|
297
|
+
// 退回新建一条全文评论,保证 bot 的答复总能落到文档(不嵌套但仍在评论区)。
|
|
298
|
+
if (isReplyNotAllowed(err)) {
|
|
299
|
+
logger.warn(`[doc-comment] comment=${commentId.slice(0, 12)} 不允许回复,退回新建全文评论`);
|
|
300
|
+
const c = await createDocComment(larkAppId, file, text, mentionOpenId);
|
|
301
|
+
return { replyId: c.replyId, commentId: c.commentId };
|
|
302
|
+
}
|
|
303
|
+
throw err;
|
|
304
|
+
}
|
|
305
|
+
// ensureOk 对 code!==0 抛错;同样要识别"不允许回复"并退回新建。
|
|
306
|
+
if (res?.code !== 0) {
|
|
307
|
+
if (isReplyNotAllowed(res)) {
|
|
308
|
+
logger.warn(`[doc-comment] comment=${commentId.slice(0, 12)} 不允许回复(code=${res?.code}),退回新建全文评论`);
|
|
309
|
+
const c = await createDocComment(larkAppId, file, text, mentionOpenId);
|
|
310
|
+
return { replyId: c.replyId, commentId: c.commentId };
|
|
311
|
+
}
|
|
312
|
+
throw new Error(`回复评论 失败: ${res?.msg ?? 'unknown'} (code: ${res?.code})`);
|
|
313
|
+
}
|
|
314
|
+
const replyId = res.data?.reply_id;
|
|
315
|
+
if (replyId)
|
|
316
|
+
markBotAuthoredReply(replyId);
|
|
317
|
+
logger.info(`[doc-comment] replied to comment=${commentId.slice(0, 12)} reply=${String(replyId ?? '').slice(0, 12)} on file=${file.fileToken.slice(0, 12)} (${text.length} chars)`);
|
|
318
|
+
return { replyId };
|
|
319
|
+
}
|
|
320
|
+
/** 构造评论内容元素:可选在开头 @ 某人(person 元素,user_id=open_id),末尾追加
|
|
321
|
+
* 隐形哨兵供事件侧自触发兜底识别。 */
|
|
322
|
+
function buildCommentElements(text, mentionOpenId) {
|
|
323
|
+
const els = [];
|
|
324
|
+
if (mentionOpenId) {
|
|
325
|
+
els.push({ type: 'person', person: { user_id: mentionOpenId } });
|
|
326
|
+
els.push({ type: 'text_run', text_run: { text: ' ' } });
|
|
327
|
+
}
|
|
328
|
+
els.push({ type: 'text_run', text_run: { text: text + BOT_REPLY_SENTINEL } });
|
|
329
|
+
return els;
|
|
330
|
+
}
|
|
331
|
+
/** 识别飞书"该评论不允许回复"的错误(code 1069302 或消息含 does not allow replies)。 */
|
|
332
|
+
function isReplyNotAllowed(errOrRes) {
|
|
333
|
+
const s = errOrRes instanceof Error ? errOrRes.message : JSON.stringify(errOrRes ?? '');
|
|
334
|
+
return s.includes('1069302') || /does not allow replies|不允许回复/.test(s);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* 新建一条**全文评论**(独立的新评论,非嵌套)。用于没有可挂靠 comment_id 的
|
|
338
|
+
* 场景(如主动向文档发评论)。返回 comment_id。
|
|
339
|
+
*/
|
|
340
|
+
export async function createDocComment(larkAppId, file, text, mentionOpenId) {
|
|
341
|
+
const elements = buildCommentElements(text, mentionOpenId);
|
|
342
|
+
const res = await driveApiCall(larkAppId, {
|
|
343
|
+
method: 'POST',
|
|
344
|
+
path: `/open-apis/drive/v1/files/${encodeURIComponent(file.fileToken)}/comments`,
|
|
345
|
+
params: { file_type: file.fileType, user_id_type: 'open_id' },
|
|
346
|
+
data: { reply_list: { replies: [{ content: { elements } }] } },
|
|
347
|
+
preferTenant: true, // 评论显示为 bot 本身(应用身份);bot 无访问权时回退 user
|
|
348
|
+
});
|
|
349
|
+
const data = ensureOk(res, '发表评论');
|
|
350
|
+
const commentId = data?.comment_id ?? '';
|
|
351
|
+
const replyId = data?.reply_list?.replies?.[0]?.reply_id;
|
|
352
|
+
if (replyId)
|
|
353
|
+
markBotAuthoredReply(replyId);
|
|
354
|
+
logger.info(`[doc-comment] created comment=${String(commentId).slice(0, 12)} reply=${String(replyId ?? '').slice(0, 12)} on file=${file.fileToken.slice(0, 12)} (${text.length} chars)`);
|
|
355
|
+
return { commentId, replyId };
|
|
356
|
+
}
|
|
357
|
+
/** 飞书文档评论内容长度上限的保守值,超长 bot 回复按此分块发多条评论。 */
|
|
358
|
+
export const DOC_COMMENT_MAX_CHARS = 3000;
|
|
359
|
+
/** 把长文本按 {@link DOC_COMMENT_MAX_CHARS} 切块(尽量按段落/换行边界)。 */
|
|
360
|
+
export function chunkCommentText(text, max = DOC_COMMENT_MAX_CHARS) {
|
|
361
|
+
if (text.length <= max)
|
|
362
|
+
return [text];
|
|
363
|
+
const chunks = [];
|
|
364
|
+
let rest = text;
|
|
365
|
+
while (rest.length > max) {
|
|
366
|
+
let cut = rest.lastIndexOf('\n', max);
|
|
367
|
+
if (cut < max * 0.5)
|
|
368
|
+
cut = max; // 没有靠后的换行就硬切
|
|
369
|
+
chunks.push(rest.slice(0, cut));
|
|
370
|
+
rest = rest.slice(cut).replace(/^\n+/, '');
|
|
371
|
+
}
|
|
372
|
+
if (rest)
|
|
373
|
+
chunks.push(rest);
|
|
374
|
+
return chunks;
|
|
375
|
+
}
|
|
376
|
+
//# sourceMappingURL=doc-comment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doc-comment.js","sourceRoot":"","sources":["../../../src/im/lark/doc-comment.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Extracted from daemon.ts for modularity.
|
|
5
5
|
*/
|
|
6
6
|
import * as Lark from '@larksuiteoapi/node-sdk';
|
|
7
|
+
import { type DocSubscription } from '../../services/doc-subs-store.js';
|
|
7
8
|
import { type Brand } from './lark-hosts.js';
|
|
8
9
|
/** Set the bot's open_id. Callers should also call writeBotInfoFile() to persist. */
|
|
9
10
|
export declare function setBotOpenId(larkAppId: string, id: string): void;
|
|
@@ -128,6 +129,23 @@ export interface EventHandlers {
|
|
|
128
129
|
* — which in a 话题群 wraps each top-level message in a fresh topic.
|
|
129
130
|
* Best-effort fire-and-forget; the dispatcher proceeds either way. */
|
|
130
131
|
onChatModeConverted?: (chatId: string, larkAppId: string) => void;
|
|
132
|
+
/** 文档评论入口(/subscribe-lark-doc):一条命中订阅的文档评论被喂进其绑定
|
|
133
|
+
* 会话。daemon 负责定位会话、投递给 worker、记录该轮回评论的落点。 */
|
|
134
|
+
handleDocComment?: (ctx: DocCommentContext) => Promise<void>;
|
|
135
|
+
}
|
|
136
|
+
/** 一条已通过订阅 + 触发范围 + 自触发过滤的文档评论,交给 daemon 投递。 */
|
|
137
|
+
export interface DocCommentContext {
|
|
138
|
+
larkAppId: string;
|
|
139
|
+
/** 命中的文档订阅(含 sessionAnchor / scope / chatId)。 */
|
|
140
|
+
sub: DocSubscription;
|
|
141
|
+
/** 触发评论的 comment_id。 */
|
|
142
|
+
commentId: string;
|
|
143
|
+
/** 触发的具体回复 id(作 turnId,回评论落点也按它走);缺省退回 commentId。 */
|
|
144
|
+
replyId?: string;
|
|
145
|
+
/** 评论纯文本(喂给模型的用户消息)。 */
|
|
146
|
+
text: string;
|
|
147
|
+
/** 评论发表者 open_id。 */
|
|
148
|
+
authorOpenId?: string;
|
|
131
149
|
}
|
|
132
150
|
/**
|
|
133
151
|
* Best-effort plain-text extraction from a Lark message for routing-level
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-dispatcher.d.ts","sourceRoot":"","sources":["../../../src/im/lark/event-dispatcher.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,IAAI,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"event-dispatcher.d.ts","sourceRoot":"","sources":["../../../src/im/lark/event-dispatcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,7 +17,9 @@ import { parseForceTopicInvocation } from '../../core/command-handler.js';
|
|
|
17
17
|
import { shouldAutoStartOnNewTopic } from '../../core/auto-start.js';
|
|
18
18
|
import { stripLeadingMentions } from './message-parser.js';
|
|
19
19
|
import { recordObservedBots } from '../../services/observed-bots-store.js';
|
|
20
|
-
import {
|
|
20
|
+
import { getDocSubscription, listAllDocSubscriptions } from '../../services/doc-subs-store.js';
|
|
21
|
+
import { getDocComment, isBotAuthoredReply, hasBotSentinel } from './doc-comment.js';
|
|
22
|
+
import { BOTMUX_REQUIRED_SCOPES, DOC_FEATURE_SCOPES, DOC_COMMENT_EVENT, buildScopeDeepLink } from '../../setup/verify-permissions.js';
|
|
21
23
|
import { larkHosts, normalizeBrand, sdkDomain } from './lark-hosts.js';
|
|
22
24
|
import { tryHandleGrantCommand } from './grant-command.js';
|
|
23
25
|
import { tryHandleReplyModeCommand } from './reply-mode-command.js';
|
|
@@ -215,6 +217,33 @@ export async function checkRequiredScopes(larkAppId) {
|
|
|
215
217
|
return;
|
|
216
218
|
}
|
|
217
219
|
const grantedScopes = new Set(scopesRaw.map(s => typeof s === 'string' ? s : s?.scope).filter(Boolean));
|
|
220
|
+
// 文档评论入口就绪自检:仅对「已订阅过文档」的 bot 生效(opt-in,不打扰其他 bot)。
|
|
221
|
+
// ① 校验文档 app 权限是否开通(可查 → 缺则 DM 深链)② 提醒去后台订阅评论事件
|
|
222
|
+
// drive.notice.comment_add_v1(飞书无 API 可查是否已订阅,只能提醒)——让「订阅
|
|
223
|
+
// 成功却收不到评论」这类配置漏项在重启自检时暴露。
|
|
224
|
+
try {
|
|
225
|
+
const docSubs = listAllDocSubscriptions(config.session.dataDir, larkAppId);
|
|
226
|
+
if (docSubs.length > 0) {
|
|
227
|
+
const missingDoc = DOC_FEATURE_SCOPES.filter(s => !grantedScopes.has(s.name));
|
|
228
|
+
if (missingDoc.length > 0) {
|
|
229
|
+
const summary = missingDoc.map(s => `${s.name}(${s.desc})`).join('、');
|
|
230
|
+
logger.error(`[${larkAppId}] 文档评论入口已在用(${docSubs.length} 个订阅)但缺 ${missingDoc.length} 项文档权限:${summary}。评论将收不到/回不了,请到权限管理开通后 botmux restart。`);
|
|
231
|
+
const adminDoc = getAdminOpenId(bot);
|
|
232
|
+
if (adminDoc) {
|
|
233
|
+
const lines = missingDoc.map((s, i) => `${i + 1}. **${s.desc}** (\`${s.name}\`)\n ${buildScopeDeepLink(bot.config.larkAppId, s.name, brand)}`).join('\n\n');
|
|
234
|
+
await dmAdmin(larkAppId, adminDoc, `⚠️ 机器人 "${bot.botName ?? larkAppId}" 已订阅 ${docSubs.length} 个飞书文档(/subscribe-lark-doc),但缺少文档评论所需权限:\n\n${lines}\n\n` +
|
|
235
|
+
`另外请确认开发者后台「事件订阅」里已添加 **\`${DOC_COMMENT_EVENT}\`**(云文档新增评论)事件——该事件无法被自动检测,缺它则评论永远收不到。\n\n开通 + 订阅事件后执行 \`botmux restart\`。`, `missing doc-feature scopes: ${missingDoc.map(s => s.name).join(',')}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
// 权限齐了——事件订阅查不了,仅 info 记一条提醒(不 DM 免重启刷屏)。
|
|
240
|
+
logger.info(`[${larkAppId}] doc-comment 权限齐全(${docSubs.length} 订阅);请确保后台已订阅事件 ${DOC_COMMENT_EVENT}(无法自动检测)`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
logger.debug(`[${larkAppId}] doc-feature readiness check errored: ${err?.message ?? err}`);
|
|
246
|
+
}
|
|
218
247
|
// Diff against the canonical list. Critical-missing is the main signal;
|
|
219
248
|
// non-critical is mentioned only when something critical is also missing,
|
|
220
249
|
// so deployments don't get nagged about purely optional scopes like
|
|
@@ -970,6 +999,100 @@ export async function decideRouting(larkAppId, message) {
|
|
|
970
999
|
const { scope, anchor } = await decideRoutingWithSource(larkAppId, message);
|
|
971
1000
|
return { scope, anchor };
|
|
972
1001
|
}
|
|
1002
|
+
/** 从评论事件 payload 里挖出 { fileToken, fileType, commentId, replyId,
|
|
1003
|
+
* noticeType, isMentioned, operatorOpenId }。
|
|
1004
|
+
*
|
|
1005
|
+
* 真机实测 drive.notice.comment_add_v1 的 event 体形如
|
|
1006
|
+
* `{ comment_id, is_mentioned, notice_meta, reply_id }` —— **file_token 不在顶层,
|
|
1007
|
+
* 藏在 notice_meta 里**(notice_meta 可能是对象或 JSON 字符串)。故这里展开
|
|
1008
|
+
* notice_meta 并多路径兜底;file_type 拿不到无妨(后续用订阅表里的)。 */
|
|
1009
|
+
function parseCommentEvent(data) {
|
|
1010
|
+
const d = data?.event ?? data ?? {};
|
|
1011
|
+
let meta = d.notice_meta ?? d.noticeMeta;
|
|
1012
|
+
if (typeof meta === 'string') {
|
|
1013
|
+
try {
|
|
1014
|
+
meta = JSON.parse(meta);
|
|
1015
|
+
}
|
|
1016
|
+
catch { /* 保留原字符串 */ }
|
|
1017
|
+
}
|
|
1018
|
+
const m = (meta && typeof meta === 'object') ? meta : {};
|
|
1019
|
+
const pick = (k) => d[k] ?? m[k] ?? m[k.replace(/_([a-z])/g, (_, c) => c.toUpperCase())];
|
|
1020
|
+
return {
|
|
1021
|
+
fileToken: pick('file_token') ?? pick('token') ?? pick('obj_token'),
|
|
1022
|
+
fileType: pick('file_type') ?? pick('obj_type'),
|
|
1023
|
+
commentId: pick('comment_id'),
|
|
1024
|
+
replyId: pick('reply_id'),
|
|
1025
|
+
noticeType: pick('notice_type'),
|
|
1026
|
+
isMentioned: d.is_mentioned === true || m.is_mentioned === true,
|
|
1027
|
+
operatorOpenId: d.operator_id?.open_id ?? d.user_id?.open_id ?? m.operator_id?.open_id,
|
|
1028
|
+
meta,
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
/** 评论事件入口:去重 ACK-safe 包装 + 订阅匹配 + 自触发/触发范围过滤后转 daemon。 */
|
|
1032
|
+
function handleCommentEventAckSafe(data, larkAppId, handlers) {
|
|
1033
|
+
const parsed = parseCommentEvent(data);
|
|
1034
|
+
const eventKey = `drive.comment_add:${larkAppId}:${eventIdForKey(data) ?? `${parsed.fileToken ?? '?'}:${parsed.replyId ?? parsed.commentId ?? '?'}`}`;
|
|
1035
|
+
scheduleAckSafeEvent(eventKey, async () => {
|
|
1036
|
+
try {
|
|
1037
|
+
await processCommentEvent(parsed, larkAppId, handlers);
|
|
1038
|
+
}
|
|
1039
|
+
catch (err) {
|
|
1040
|
+
logger.error(`Error handling doc-comment event: ${err}`);
|
|
1041
|
+
}
|
|
1042
|
+
}, 'doc-comment event');
|
|
1043
|
+
}
|
|
1044
|
+
async function processCommentEvent(parsed, larkAppId, handlers) {
|
|
1045
|
+
if (!handlers.handleDocComment)
|
|
1046
|
+
return;
|
|
1047
|
+
const { fileToken, commentId } = parsed;
|
|
1048
|
+
if (!fileToken || !commentId) {
|
|
1049
|
+
logger.info(`[doc-comment] event dropped: missing fileToken/commentId (fileToken=${fileToken ?? '?'} commentId=${commentId ?? '?'}) — payload 字段路径可能与解析不符`);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
// 1) 必须是已订阅的文档(per-文档订阅 → 主键命中才处理)
|
|
1053
|
+
const sub = getDocSubscription(config.session.dataDir, larkAppId, fileToken);
|
|
1054
|
+
if (!sub) {
|
|
1055
|
+
logger.info(`[doc-comment] event dropped: file_token=${fileToken.slice(0, 12)} 不在订阅表(已订阅的可 /subscribe-lark-doc list 查;注意 wiki 链接会解析成底层 obj_token)`);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
// 2) 拉评论 thread 取权威正文 / 作者 / @ 列表(事件 payload 不保证带全),
|
|
1059
|
+
// 同时用最新一条回复作为"触发回复"。
|
|
1060
|
+
const comment = await getDocComment(larkAppId, { fileToken, fileType: sub.fileType }, commentId);
|
|
1061
|
+
if (!comment || comment.replies.length === 0) {
|
|
1062
|
+
logger.info(`[doc-comment] event dropped: 取不到评论内容 comment=${commentId.slice(0, 12)}(replies=${comment ? comment.replies.length : 'null'})`);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
const trigger = parsed.replyId
|
|
1066
|
+
? comment.replies.find(r => r.replyId === parsed.replyId) ?? comment.replies[comment.replies.length - 1]
|
|
1067
|
+
: comment.replies[comment.replies.length - 1];
|
|
1068
|
+
// 3) 自触发过滤(防死循环):bot 的回复可能以应用身份(作者=bot open_id)或回退
|
|
1069
|
+
// 用户身份(作者=授权用户,无法靠作者区分)发出。三重保险:①作者==本 bot
|
|
1070
|
+
// ②reply_id 在 bot 创建集合 ③文本含隐形哨兵。任一命中即跳过。
|
|
1071
|
+
const selfBotOpenId = getBot(larkAppId).botOpenId;
|
|
1072
|
+
if ((selfBotOpenId && trigger.userId === selfBotOpenId) || isBotAuthoredReply(trigger.replyId) || hasBotSentinel(trigger.text))
|
|
1073
|
+
return;
|
|
1074
|
+
// 4) 触发范围:mention-only 要求评论 @ 到本 bot。优先用事件自带的 is_mentioned
|
|
1075
|
+
// (飞书已判好),拿不到再回退按评论正文里的 @person 列表比对 bot open_id。
|
|
1076
|
+
if (sub.commentTriggerMode === 'mention-only') {
|
|
1077
|
+
const mentioned = parsed.isMentioned === true || (!!selfBotOpenId && trigger.mentions.includes(selfBotOpenId));
|
|
1078
|
+
if (!mentioned) {
|
|
1079
|
+
logger.info(`[doc-comment] event dropped: mention-only 但未 @ 本 bot (comment=${commentId.slice(0, 12)})`);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
const text = trigger.text.trim();
|
|
1084
|
+
if (!text)
|
|
1085
|
+
return;
|
|
1086
|
+
logger.info(`[doc-comment] dispatch file=${fileToken.slice(0, 12)} comment=${commentId.slice(0, 12)} mode=${sub.commentTriggerMode} → session anchor=${sub.sessionAnchor.slice(0, 12)}`);
|
|
1087
|
+
await handlers.handleDocComment({
|
|
1088
|
+
larkAppId,
|
|
1089
|
+
sub,
|
|
1090
|
+
commentId,
|
|
1091
|
+
replyId: trigger.replyId || commentId,
|
|
1092
|
+
text,
|
|
1093
|
+
authorOpenId: trigger.userId,
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
973
1096
|
/**
|
|
974
1097
|
* Create and start the Lark WSClient with event dispatching.
|
|
975
1098
|
* Returns the WSClient instance for lifecycle management.
|
|
@@ -997,6 +1120,11 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
|
|
|
997
1120
|
}
|
|
998
1121
|
}, 'bot-added event');
|
|
999
1122
|
},
|
|
1123
|
+
// 文档评论入口(/subscribe-lark-doc)。飞书评论事件命名在 v1/v2 间有差异,
|
|
1124
|
+
// 两个候选名都注册(同一处理器)——真机一锤定音后可删冗余。需在开发者后台
|
|
1125
|
+
// 订阅对应事件。注意:评论事件是 per-文档 推送(订阅时按 file_token 注册)。
|
|
1126
|
+
'drive.file.comment_add_v1': (data) => handleCommentEventAckSafe(data, larkAppId, handlers),
|
|
1127
|
+
'drive.notice.comment_add_v1': (data) => handleCommentEventAckSafe(data, larkAppId, handlers),
|
|
1000
1128
|
'card.action.trigger': (data) => handleCardActionAckSafe(data, larkAppId, handlers),
|
|
1001
1129
|
'im.message.receive_v1': (data) => {
|
|
1002
1130
|
const messageIdForKey = data?.message?.message_id;
|
|
@@ -1370,6 +1498,32 @@ export function startLarkEventDispatcher(larkAppId, larkAppSecret, handlers, bra
|
|
|
1370
1498
|
}, 'message event');
|
|
1371
1499
|
},
|
|
1372
1500
|
});
|
|
1501
|
+
// 诊断:包一层 invoke,记录长连接收到的**每一个**事件类型(含未注册的)。
|
|
1502
|
+
// 排查云文档评论事件是否真送达 / 实际事件名用——comment 类一律连 payload 关键字段
|
|
1503
|
+
// 一起打。日常其它事件只在 DEBUG 下打,避免刷屏。
|
|
1504
|
+
// 仅当 dispatcher 真有 invoke 方法时才包(单测里 Lark.EventDispatcher 是 mock,
|
|
1505
|
+
// register() 返回的对象没有 invoke → 不包,避免 undefined.bind 抛错)。
|
|
1506
|
+
const __origInvoke = typeof eventDispatcher.invoke === 'function'
|
|
1507
|
+
? eventDispatcher.invoke.bind(eventDispatcher)
|
|
1508
|
+
: undefined;
|
|
1509
|
+
if (__origInvoke) {
|
|
1510
|
+
eventDispatcher.invoke = (data) => {
|
|
1511
|
+
try {
|
|
1512
|
+
const et = data?.header?.event_type ?? data?.event_type ?? data?.type ?? 'unknown';
|
|
1513
|
+
const isCommentish = typeof et === 'string' && et.includes('comment');
|
|
1514
|
+
if (isCommentish) {
|
|
1515
|
+
const ev = data?.event ?? data;
|
|
1516
|
+
const p = parseCommentEvent(data);
|
|
1517
|
+
logger.info(`[ws-event] ${larkAppId} event_type=${et} → parsed fileToken=${p.fileToken ?? '?'} commentId=${p.commentId ?? '?'} replyId=${p.replyId ?? '?'} isMentioned=${p.isMentioned} | notice_meta=${JSON.stringify(ev?.notice_meta ?? ev?.noticeMeta ?? null)}`);
|
|
1518
|
+
}
|
|
1519
|
+
else if (process.env.DEBUG) {
|
|
1520
|
+
logger.info(`[ws-event] ${larkAppId} event_type=${et}`);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
catch { /* 诊断不阻断分发 */ }
|
|
1524
|
+
return __origInvoke(data);
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1373
1527
|
// Start WSClient
|
|
1374
1528
|
const wsClient = new Lark.WSClient({
|
|
1375
1529
|
appId: larkAppId,
|