import type { CommentTriggerMode } from '../../services/doc-subs-store.js'; /** * bot 回复的隐形哨兵:追加在 bot 发表的评论末尾(零宽字符,用户不可见)。 * * 为什么需要:bot 用 **user_access_token** 发评论 → 评论作者 = 授权用户本人, * 无法靠「作者是不是 bot」区分「bot 的回复」和「用户自己的评论」。若不区分, * bot 发评论 → 触发 comment_add 事件 → 又喂给 bot → 死循环。 * * 双保险:① 记录 bot 创建过的 reply_id({@link isBotAuthoredReply},同一 daemon * 生命周期内权威)② 文本末尾哨兵(跨重启 / reply_id 拿不到时的兜底)。 */ export declare const BOT_REPLY_SENTINEL = "\u200B\u2063\u200B"; export declare function markBotAuthoredReply(id: string): void; export declare function isBotAuthoredReply(id: string | undefined): boolean; export declare function hasBotSentinel(text: string | undefined): boolean; /** * 触发范围闸:给定订阅的 `commentTriggerMode` + 触发评论正文 @ 到的 open_id 列表 * + 本 bot 自己的 open_id,判定这条评论是否应触发会话。 * * • 'all' —— 该文档所有新评论都触发。 * • 'mention-only' —— 仅当评论正文真的 @ 了「本 bot」才触发。 * * ⚠️ 这里是用户报的 bug 的根因所在:mention-only **绝不能**用飞书评论事件里的 * `is_mentioned` 字段判定。实测该字段含义是「这条评论里存在任意 @」——@ 了 * 别人(同事)时它同样为 true。早先用 `is_mentioned === true || …` 短路放行, * 导致「只 @ 同事、没 @bot」的评论也被误触发,mention-only 形同虚设 * (现象:「只有 @ 别人时才被触发」)。唯一可靠依据是拉到的评论正文 * @person(open_id) 列表里是否含本 bot 自己的 open_id。 * * `selfBotOpenId` 缺失(daemon 启动期 open_id 尚未探到)时一律判否:mention-only * 宁可漏触发也不误触发。调用方应在此之前 `await ensureBotOpenId` 关掉该启动 * 竞态,避免把合法的 @bot 评论误丢(事件已被 ACK,飞书不会重投)。 */ export declare function commentTriggerAllowed(mode: CommentTriggerMode, triggerMentions: string[], selfBotOpenId: string | undefined): boolean; /** 飞书云文档评论里富文本元素的最小子集(够 bot 发纯文本 + @人)。 */ export interface CommentElement { type: 'text_run' | 'person' | 'docs_link'; text_run?: { text: string; }; person?: { user_id: string; }; docs_link?: { url: string; }; } /** 一条评论(含其下回复)的归一化形态,listDocComments 返回。 */ export interface DocComment { commentId: string; /** 评论是否已解决。 */ isSolved: boolean; /** 局部评论选中的文档原文;全文评论通常为空。 */ quote?: string; /** 是否为整篇文档的全文评论。 */ isWhole?: boolean; /** 该评论 thread 下所有回复(飞书把评论建模成 reply_list)。 */ replies: Array<{ replyId: string; /** 发表者 open_id(user_id_type=open_id 时)。 */ userId?: string; /** 纯文本内容(拼接所有 text_run)。 */ text: string; /** 该回复 @ 到的 open_id 列表(从 person 元素提取)。 */ mentions: string[]; createdAt?: number; }>; } export interface ResolvedDocFile { fileToken: string; /** 飞书 file_type:docx / doc / sheet / bitable / file / slides。本特性主攻 docx。 */ fileType: string; } /** * 把用户输入解析成 { kind, token }。支持完整飞书链接、`/docx/` 片段、 * 或裸 token(裸 token 当 docx 处理)。`wiki` 类型需要再过一次节点解析(见 * {@link resolveDocFile})。无法识别返回 null。 */ export declare function parseDocRef(input: string): { kind: string; token: string; } | null; /** * 解析成可直接调评论 / 订阅 API 的 { fileToken, fileType }。wiki 节点先调 * get_node 换出底层 obj_token + obj_type;其余类型直接映射。 */ export declare function resolveDocFile(larkAppId: string, input: string): Promise; export interface DocProviderEffectOptions { beforeProviderEffect?: () => void | Promise; } export type DocSubscriptionPermissionSource = 'user' | 'tenant' | 'both' | 'unknown'; export interface DocSubscriptionPermissionDetails { source: DocSubscriptionPermissionSource; userLarkMessage?: string; tenantLarkMessage?: string; tenantHttpStatus?: number; } /** * 飞书订阅接口返回 1069603。保留实际返回该业务码的身份,避免 tenant-only * 失败被错误归因成“当前用户无权限”;只保留业务字段,不把可能含 token/header * 的 AxiosError 挂进 cause。 */ export declare class DocSubscriptionPermissionError extends Error { readonly details: DocSubscriptionPermissionDetails; readonly larkCode = 1069603; constructor(details: DocSubscriptionPermissionDetails); get source(): DocSubscriptionPermissionSource; } /** 订阅文档事件(评论新增等靠此推送)。幂等:重复订阅飞书返回成功。 */ export declare function subscribeDocFile(larkAppId: string, file: ResolvedDocFile): Promise; /** 退订文档事件。best-effort:失败只告警不抛。 */ export declare function unsubscribeDocFile(larkAppId: string, file: ResolvedDocFile): Promise; /** * 读某条评论(含其下所有回复)。用于事件来后取 thread 上下文 + 判断是否 @bot。 * 拿不到返回 null。 */ export declare function getDocComment(larkAppId: string, file: ResolvedDocFile, commentId: string): Promise; /** * 列出文档当前可见的全部评论。`/watch-comment --all` 用它做增量轮询: * 评论读取优先应用身份,因此不需要 User Token;只有应用身份无权访问文档时才 * 回退已有的用户授权。 */ export declare function listDocComments(larkAppId: string, file: ResolvedDocFile): Promise; /** * 往**已有评论 thread 里追加一条回复**(真正的嵌套回复,用户看到 bot 的回复 * 就挂在自己那条评论下面)。 * * 端点 `POST .../comments/{comment_id}/replies` 是飞书 drive-v1 的公开 API * (file.comment.reply.create)—— 我们装的 node-sdk 1.64.0 恰好没暴露 create, * 但裸 endpoint 存在,故这里直接打。返回新回复的 reply_id(已登记防自触发)。 */ export declare function replyToDocComment(larkAppId: string, file: ResolvedDocFile, commentId: string, text: string, mentionOpenId?: string, options?: DocProviderEffectOptions): Promise<{ replyId?: string; commentId?: string; }>; /** * 新建一条**全文评论**(独立的新评论,非嵌套)。用于没有可挂靠 comment_id 的 * 场景(如主动向文档发评论)。返回 comment_id。 */ export declare function createDocComment(larkAppId: string, file: ResolvedDocFile, text: string, mentionOpenId?: string, options?: DocProviderEffectOptions): Promise<{ commentId: string; replyId?: string; }>; /** 飞书文档评论内容长度上限的保守值,超长 bot 回复按此分块发多条评论。 */ export declare const DOC_COMMENT_MAX_CHARS = 3000; /** 把长文本按 {@link DOC_COMMENT_MAX_CHARS} 切块(尽量按段落/换行边界)。 */ export declare function chunkCommentText(text: string, max?: number): string[]; /** * 给评论回复加 reaction("Typing" 处理中指示器)。 * * 用户在文档评论里 @bot 后,bot 立即给那条回复加一个 "Typing" emoji,让用户 * 知道 bot 收到了、正在处理。bot 回复发出后再删掉。 * * 端点:`POST drive/v2/files/{token}/comments/reaction`(v2,评论 reaction 专用)。 * 优先应用身份(reaction 显示为 bot 加的)。 * * @returns 新创建的 reaction_id(删除时要用);失败返回 undefined(不阻塞主流程)。 */ export declare function addCommentReaction(larkAppId: string, file: ResolvedDocFile, commentId: string, replyId: string, reactionType: string, options?: DocProviderEffectOptions): Promise; /** * 删除评论回复的 reaction(bot 回复发出后清理 "Typing" 指示器)。 * * 端点:`DELETE drive/v2/files/{token}/comments/reaction`,参数全走 query string。 * best-effort:失败只告警不抛(bot 已经成功回复了,reaction 留着也不影响)。 */ export declare function removeCommentReaction(larkAppId: string, file: ResolvedDocFile, commentId: string, replyId: string, reactionId: string, options?: DocProviderEffectOptions): Promise; //# sourceMappingURL=doc-comment.d.ts.map