import { createReplyPrefixContext as _createReplyPrefixContext, createTypingCallbacks, type ClawdbotConfig, type ReplyPayload, type RuntimeEnv, } from "openclaw/plugin-sdk"; import { resolveQiaoqiaoAccount } from "./accounts.js"; import { createQiaoqiaoClient } from "./client.js"; import { sendMessageQiaoqiao } from "./send.js"; import { getQiaoqiaoRuntime } from "./runtime.js"; const RAW_TOOL_CALL_PATTERNS = [ //i, //i, ]; function looksLikeLeakedToolCallMarkup(text: string): boolean { const normalized = String(text || "").trim(); if (!normalized) return false; return RAW_TOOL_CALL_PATTERNS.some((pattern) => pattern.test(normalized)); } function buildLeakedToolCallFallback(): string { return "抱歉,我刚才没有成功处理这次请求。请稍后再试,或换个说法试试。"; } const createReplyPrefixContext: typeof _createReplyPrefixContext = typeof _createReplyPrefixContext === "function" ? _createReplyPrefixContext : () => ({ responsePrefix: undefined as any, responsePrefixContextProvider: undefined as any, onModelSelected: () => {}, }); export type CreateQiaoqiaoReplyDispatcherParams = { cfg: ClawdbotConfig; agentId: string; runtime: RuntimeEnv; chatId: string; replyToMessageId?: string; mentionTargets?: any[]; accountId?: string; messageCreateTimeMs?: number; }; export function createQiaoqiaoReplyDispatcher(params: CreateQiaoqiaoReplyDispatcherParams) { const core = getQiaoqiaoRuntime(); const { cfg, agentId, chatId, replyToMessageId, mentionTargets, accountId } = params; const account = resolveQiaoqiaoAccount({ cfg, accountId }); const prefixContext = createReplyPrefixContext({ cfg, agentId }); const textChunkLimit = core.channel.text.resolveTextChunkLimit(cfg, "qiaoqiao", account.accountId, { fallbackLimit: 4000, }); const chunkMode = core.channel.text.resolveChunkMode(cfg, "qiaoqiao", account.accountId); const tableMode = core.channel.text.resolveMarkdownTableMode({ cfg, channel: "qiaoqiao", accountId: account.accountId, }); // 使用 OpenClaw 的 dispatcher 创建器 const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({ responsePrefix: prefixContext.responsePrefix, responsePrefixContextProvider: prefixContext.responsePrefixContextProvider, humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId), onReplyStart: () => { // Qiaoqiao 暂不支持打字指示器 }, deliver: async (payload: ReplyPayload, info) => { let text = payload.text ?? ""; if (!text.trim()) { return; } if (looksLikeLeakedToolCallMarkup(text)) { params.runtime.error?.( `qiaoqiao[${account.accountId}] leaked raw tool-call markup in final reply; replacing outbound text`, ); console.warn( `[Qiaoqiao Reply] leaked_tool_call_markup accountId=${account.accountId} agentId=${agentId} session=${chatId} kind=${info?.kind || "unknown"}`, ); text = buildLeakedToolCallFallback(); } // 处理最终回复 if (info?.kind === "final" || !info) { const converted = core.channel.text.convertMarkdownTables(text, tableMode); // 分块发送长消息 let first = true; for (const chunk of core.channel.text.chunkTextWithMode( converted, textChunkLimit, chunkMode, )) { await sendMessageQiaoqiao({ cfg, to: chatId, text: chunk, replyToMessageId, mentions: first ? mentionTargets : undefined, accountId, }); first = false; } } }, onError: async (error: any, info?: any) => { params.runtime.error?.( `qiaoqiao[${account.accountId}] ${info?.kind || 'unknown'} reply failed: ${String(error)}`, ); }, onIdle: async () => { // 清理资源 }, onCleanup: () => { // 清理资源 }, }); return { dispatcher, replyOptions: { ...replyOptions, onModelSelected: prefixContext.onModelSelected, }, markDispatchIdle, }; }