@zqagent/shared 0.1.0 → 0.3.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/approval-view.ts","../../src/notify/Notifier.ts","../../src/notify/NotificationHub.ts","../../src/notify/notifiers/BarkNotifier.ts","../../src/notify/notifiers/ServerChanNotifier.ts","../../src/notify/notifiers/WebhookNotifier.ts","../../src/notify/notifiers/EmailNotifier.ts","../../src/notify/index.ts"],"sourcesContent":["/**\n * 审批视图:把审批请求(工具 / 原因 / 参数详情)规范化为统一的「字段行」,\n * 供 CLI、QQ、通用通知、邮件、MQTT、Web 等各端渲染成同一套表格。\n *\n * 唯一数据源是审批事件负载(toolName / reason / arguments),各通道只负责\n * 把这里产出的字段渲染成自己端上最好看的形态(终端框线表 / Markdown / HTML)。\n */\n\n/** 审批表格中的一行 */\nexport interface ApprovalField {\n /** 字段名(工具 / 原因 / 参数名) */\n label: string;\n /** 字段值(已完成换行折叠与截断) */\n value: string;\n}\n\n/** 统一审批输入 */\nexport interface ApprovalViewInput {\n toolName: string;\n reason: string;\n arguments?: Record<string, unknown> | null;\n}\n\n/** 单个字段值的默认最大长度(超出以 … 截断) */\nexport const APPROVAL_VALUE_MAX = 120;\n\n/** 表格列标题(各端保持一致) */\nexport const APPROVAL_COLUMNS = [\"项目\", \"详情\"] as const;\n\n/** 工具 / 原因两行的固定文案 */\nconst TOOL_LABEL = \"🔧 工具\";\nconst REASON_LABEL = \"💬 原因\";\n\n/** 把任意参数值折叠成单行文本并按需截断 */\nfunction formatValue(v: unknown, maxLen: number): string {\n let text: string;\n if (v == null) {\n text = \"\";\n } else if (typeof v === \"string\") {\n text = v;\n } else {\n try {\n text = JSON.stringify(v);\n } catch {\n text = String(v);\n }\n }\n text = text.replace(/\\r?\\n/g, \"↵\");\n return text.length > maxLen ? text.slice(0, maxLen) + \"…\" : text;\n}\n\n/** 从 NotifyEvent.data(审批事件负载)提取统一输入 */\nexport function approvalInputFromData(\n data: Record<string, unknown>,\n): ApprovalViewInput {\n const args = data[\"arguments\"];\n return {\n toolName: String(data[\"toolName\"] ?? \"\"),\n reason: String(data[\"reason\"] ?? \"\"),\n arguments:\n args && typeof args === \"object\"\n ? (args as Record<string, unknown>)\n : undefined,\n };\n}\n\n/**\n * 规范化为统一字段行:工具 → 原因 → 每个参数各一行(行序在所有通道一致)。\n */\nexport function approvalFields(\n input: ApprovalViewInput,\n maxLen = APPROVAL_VALUE_MAX,\n): ApprovalField[] {\n const fields: ApprovalField[] = [\n { label: TOOL_LABEL, value: formatValue(input.toolName, maxLen) },\n { label: REASON_LABEL, value: formatValue(input.reason, maxLen) },\n ];\n for (const [key, value] of Object.entries(input.arguments ?? {})) {\n fields.push({ label: formatValue(key, maxLen), value: formatValue(value, maxLen) });\n }\n return fields;\n}\n\n/** Markdown 表格单元格转义(竖线会破坏列结构) */\nfunction escapeCell(text: string): string {\n return text.replace(/\\|/g, \"\\\\|\").replace(/\\r?\\n/g, \"↵\");\n}\n\n/** 渲染为统一的两列 Markdown 表格 */\nexport function approvalToMarkdownTable(\n input: ApprovalViewInput,\n opts: { maxLen?: number; title?: string } = {},\n): string {\n const lines: string[] = [];\n if (opts.title) lines.push(opts.title, \"\");\n lines.push(`| ${APPROVAL_COLUMNS[0]} | ${APPROVAL_COLUMNS[1]} |`, \"| --- | --- |\");\n for (const f of approvalFields(input, opts.maxLen)) {\n lines.push(`| ${escapeCell(f.label)} | ${escapeCell(f.value)} |`);\n }\n return lines.join(\"\\n\");\n}\n\n/** HTML 转义 */\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n}\n\n/** 渲染为 HTML 表格(邮件 / Web 端直接展示为真正的表格) */\nexport function approvalToHtmlTable(\n input: ApprovalViewInput,\n maxLen = APPROVAL_VALUE_MAX,\n): string {\n const rows = approvalFields(input, maxLen)\n .map(\n (f) =>\n `<tr>` +\n `<th style=\"text-align:left;padding:4px 12px 4px 0;color:#6b7280;font-weight:500;white-space:nowrap;vertical-align:top;\">${escapeHtml(f.label)}</th>` +\n `<td style=\"padding:4px 0;font-family:ui-monospace,Menlo,Consolas,monospace;\">${escapeHtml(f.value)}</td>` +\n `</tr>`,\n )\n .join(\"\");\n return `<table style=\"border-collapse:collapse;font-size:13px;\">${rows}</table>`;\n}\n","/**\n * 通知通道接口\n *\n * 每种通知渠道(Telegram / 企微 / 钉钉 / 飞书 / Bark / 邮件 / 短信 / …)\n * 实现本接口。NotificationHub 按租户配置把 NotifyEvent 分发到各通道。\n * 实现必须是「尽力而为」:发送失败应抛出异常由 Hub 捕获降级,或自行吞掉。\n */\nimport type {\n NotificationChannelConfig,\n NotificationChannelType,\n NotifyEvent,\n} from \"../types.js\";\nimport {\n approvalInputFromData,\n approvalToMarkdownTable,\n} from \"../approval-view.js\";\n\nexport interface Notifier {\n /** 通道类型标识 */\n readonly type: NotificationChannelType;\n\n /**\n * 发送一条通知。\n * @param config 该租户此通道的配置(明文态,敏感字段已由上层解密)\n * @param ev 结构化通知事件\n */\n send(config: NotificationChannelConfig, ev: NotifyEvent): Promise<void>;\n}\n\n/** 依据通知事件类型判断是否命中该通道的事件过滤开关 */\nexport function channelWants(channel: NotificationChannelConfig, kind: NotifyEvent[\"kind\"]): boolean {\n const ev = channel.events;\n switch (kind) {\n case \"approval_request\":\n return ev.approval;\n case \"question_asked\":\n return ev.question;\n case \"turn_completed\":\n case \"turn_stopped\":\n return ev.completed;\n case \"error\":\n return ev.error;\n case \"session_status\":\n // 纯状态同步:频率高、无用户动作,任何第三方渠道都不该收到\n return false;\n default:\n return true;\n }\n}\n\n/** 生成简短的通知标题(供各通道复用) */\nexport function notifyTitle(kind: NotifyEvent[\"kind\"]): string {\n switch (kind) {\n case \"approval_request\":\n return \"需要审批\";\n case \"question_asked\":\n return \"需要回答\";\n case \"turn_completed\":\n return \"任务完成\";\n case \"turn_stopped\":\n return \"达到迭代上限\";\n case \"error\":\n return \"任务出错\";\n default:\n return \"Agent 通知\";\n }\n}\n\n/** 生成简短的通知正文(供各通道复用,超长截断) */\nexport function notifyBody(ev: NotifyEvent, maxLen = 300): string {\n const d = ev.data;\n switch (ev.kind) {\n case \"approval_request\":\n return approvalToMarkdownTable(approvalInputFromData(d), {\n title: \"🔒 需要审批\",\n });\n case \"question_asked\":\n return String(d[\"question\"] ?? \"\");\n case \"turn_completed\":\n return `状态: ${String(d[\"status\"] ?? \"completed\")}`;\n case \"turn_stopped\":\n return \"已达最大迭代次数,可继续完成剩下的任务\";\n case \"error\":\n return String(d[\"error\"] ?? \"未知错误\");\n default:\n return \"\";\n }\n}\n","/**\n * NotificationHub — 通知中枢\n *\n * 注册各通知通道(Notifier),把 SessionManager 产出的 NotifyEvent\n * 按租户配置分发到启用的通道。发送是「尽力而为」,异常降级不冒泡。\n */\nimport type {\n Notifier,\n} from \"./Notifier.js\";\nimport { channelWants } from \"./Notifier.js\";\nimport type {\n NotificationChannelConfig,\n NotificationChannelType,\n NotifyEvent,\n} from \"../types.js\";\n\nexport class NotificationHub {\n private readonly notifiers = new Map<NotificationChannelType, Notifier>();\n\n /** 注册一个通道实现(幂等,后注册覆盖) */\n register(notifier: Notifier): void {\n this.notifiers.set(notifier.type, notifier);\n }\n\n /** 已注册的通道类型 */\n registeredTypes(): NotificationChannelType[] {\n return [...this.notifiers.keys()];\n }\n\n /**\n * 分发通知到给定的启用通道。\n * 单个通道失败仅记录,不影响其他通道与核心逻辑。\n */\n async dispatch(channels: NotificationChannelConfig[], ev: NotifyEvent): Promise<void> {\n for (const ch of channels) {\n if (!ch.enabled) continue;\n if (!channelWants(ch, ev.kind)) continue;\n const notifier = this.notifiers.get(ch.type);\n if (!notifier) continue;\n try {\n await notifier.send(ch, ev);\n } catch (e) {\n console.log(\n `[notify] ${ch.type} 发送失败: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n }\n }\n}\n","/**\n * Bark 通知通道(iOS)\n *\n * Bark 是一个 iOS 推送 App,只需一个 HTTP 请求即可推送。\n * config: { key: \"设备推送Key\", server?: \"https://api.day.app\" }\n */\nimport type { Notifier } from \"../Notifier.js\";\nimport { notifyTitle, notifyBody } from \"../Notifier.js\";\nimport type {\n NotificationChannelConfig,\n NotifyEvent,\n} from \"../../types.js\";\n\nconst TIMEOUT_MS = 10_000;\n\nexport class BarkNotifier implements Notifier {\n readonly type = \"bark\" as const;\n\n async send(config: NotificationChannelConfig, ev: NotifyEvent): Promise<void> {\n const key = config.config[\"key\"];\n if (!key) return;\n const server = (config.config[\"server\"] ?? \"https://api.day.app\").replace(/\\/+$/, \"\");\n const url =\n `${server}/${encodeURIComponent(key)}/` +\n `${encodeURIComponent(notifyTitle(ev.kind))}/${encodeURIComponent(notifyBody(ev))}`;\n const res = await fetch(url, {\n method: \"GET\",\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n }\n}\n","/**\n * Server 酱通知通道(微信服务号推送,国内常用)\n *\n * config: { sendkey: \"SCT 开头的 SendKey\" }\n */\nimport type { Notifier } from \"../Notifier.js\";\nimport { notifyTitle, notifyBody } from \"../Notifier.js\";\nimport type {\n NotificationChannelConfig,\n NotifyEvent,\n} from \"../../types.js\";\n\nconst TIMEOUT_MS = 10_000;\n\nexport class ServerChanNotifier implements Notifier {\n readonly type = \"serverchan\" as const;\n\n async send(config: NotificationChannelConfig, ev: NotifyEvent): Promise<void> {\n const sendkey = config.config[\"sendkey\"];\n if (!sendkey) return;\n const url = `https://sctapi.ftqq.com/${encodeURIComponent(sendkey)}.send`;\n const body = new URLSearchParams({\n title: notifyTitle(ev.kind),\n desp: notifyBody(ev),\n });\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body,\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n }\n}\n","/**\n * 通用 Webhook 通知通道\n *\n * 向用户配置的 URL POST JSON,作为自定义兜底(可对接任意中转/网关)。\n * config: { url: \"https://...\" }\n * payload: { title, body, kind, sessionId, ts, data }\n */\nimport type { Notifier } from \"../Notifier.js\";\nimport { notifyTitle, notifyBody } from \"../Notifier.js\";\nimport type {\n NotificationChannelConfig,\n NotifyEvent,\n} from \"../../types.js\";\n\nconst TIMEOUT_MS = 10_000;\n\nexport class WebhookNotifier implements Notifier {\n readonly type = \"webhook\" as const;\n\n async send(config: NotificationChannelConfig, ev: NotifyEvent): Promise<void> {\n const url = config.config[\"url\"];\n if (!url) return;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n title: notifyTitle(ev.kind),\n body: notifyBody(ev),\n kind: ev.kind,\n sessionId: ev.sessionId,\n ts: ev.ts,\n data: ev.data,\n }),\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n }\n}\n","/**\n * 邮件通知通道(通用 SMTP)\n *\n * 通过 SMTP 发送任务完成 / 审批 / 提问 / 错误等通知。兼容 QQ 邮箱\n * (smtp.qq.com:465 SSL + 授权码)、163、Gmail 等任意 SMTP 服务。\n *\n * config: {\n * host: \"smtp.qq.com\",\n * port: \"465\", // 省略时按 secure 推断 465/587\n * secure: \"true\", // \"true\"=SSL,\"false\"=STARTTLS\n * user: \"you@qq.com\",\n * pass: \"授权码\", // 密文落盘\n * from: \"you@qq.com\", // 省略时取 user\n * to: \"you@qq.com\", // 可多个逗号分隔\n * subjectPrefix: \"[zqagent]\" // 可选,默认 [zqagent]\n * }\n */\nimport nodemailer, { type Transporter } from \"nodemailer\";\nimport type { Notifier } from \"../Notifier.js\";\nimport { notifyTitle } from \"../Notifier.js\";\nimport {\n approvalInputFromData,\n approvalToHtmlTable,\n approvalToMarkdownTable,\n} from \"../../approval-view.js\";\nimport type {\n NotificationChannelConfig,\n NotifyEvent,\n} from \"../../types.js\";\n\n/** SMTP 连接参数(透传给 nodemailer.createTransport) */\nexport interface SmtpOptions {\n host: string;\n port: number;\n secure: boolean;\n auth?: { user: string; pass: string };\n}\n\n/** 可注入的 transport 工厂(测试时替换为 fake) */\nexport type TransportFactory = (opts: SmtpOptions) => Transporter;\n\n/** 默认工厂:创建真实 nodemailer transport */\nexport function defaultTransportFactory(opts: SmtpOptions): Transporter {\n return nodemailer.createTransport(opts);\n}\n\n/** 按事件类型生成纯文本正文(turn_completed 时附产出文件与摘要) */\nfunction buildText(ev: NotifyEvent): string {\n const d = ev.data;\n switch (ev.kind) {\n case \"turn_completed\":\n case \"turn_stopped\": {\n const lines: string[] = [];\n lines.push(`会话: ${ev.sessionId}`);\n lines.push(`状态: ${String(d[\"status\"] ?? ev.kind)}`);\n if (d[\"iterations\"] !== undefined)\n lines.push(`迭代次数: ${String(d[\"iterations\"])}`);\n const files = d[\"produced_files\"] as string[] | undefined;\n if (Array.isArray(files) && files.length > 0)\n lines.push(`产出文件:\\n ${files.join(\"\\n \")}`);\n const summary = typeof d[\"summary\"] === \"string\" ? d[\"summary\"] : \"\";\n if (summary.trim()) lines.push(`\\n摘要:\\n${summary}`);\n return lines.join(\"\\n\");\n }\n case \"approval_request\":\n return `${approvalToMarkdownTable(approvalInputFromData(d), {\n title: \"🔒 需要审批\",\n })}\\n会话: ${ev.sessionId}`;\n case \"question_asked\":\n return `问题: ${String(d[\"question\"] ?? \"\")}\\n会话: ${ev.sessionId}`;\n case \"error\":\n return `错误: ${String(d[\"error\"] ?? \"未知错误\")}\\n会话: ${ev.sessionId}`;\n default:\n return `会话: ${ev.sessionId}\\n时间: ${ev.ts}`;\n }\n}\n\n/** HTML 转义 */\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n}\n\n/** 将正文转成 HTML:审批渲染为真正的表格,其余保持 <pre> 换行 */\nfunction buildHtml(ev: NotifyEvent): string {\n if (ev.kind === \"approval_request\") {\n const table = approvalToHtmlTable(approvalInputFromData(ev.data));\n return (\n table +\n `<p style=\"color:#6b7280;font-size:12px;margin:10px 0 0;\">会话: ${escapeHtml(ev.sessionId)}</p>`\n );\n }\n return `<pre style=\"white-space:pre-wrap;font-family:inherit;\">${escapeHtml(buildText(ev))}</pre>`;\n}\n\nexport class EmailNotifier implements Notifier {\n readonly type = \"email\" as const;\n\n constructor(\n private readonly transportFactory: TransportFactory = defaultTransportFactory,\n ) {}\n\n async send(config: NotificationChannelConfig, ev: NotifyEvent): Promise<void> {\n // 短任务过滤:任务成功完成且执行时长未超过阈值(默认 60s)时不发邮件。\n // 可用 config.minDurationSeconds 覆盖阈值。\n if (ev.kind === \"turn_completed\") {\n const minSec = Number(config.config[\"minDurationSeconds\"] ?? 60);\n const durationMs = ev.data[\"durationMs\"];\n if (typeof durationMs === \"number\" && durationMs < minSec * 1000) {\n return;\n }\n }\n\n const host = config.config[\"host\"];\n const to = config.config[\"to\"];\n if (!host || !to) return;\n\n const secure = config.config[\"secure\"] === \"true\";\n const port =\n Number(config.config[\"port\"]) || (secure ? 465 : 587);\n const user = config.config[\"user\"] ?? \"\";\n const pass = config.config[\"pass\"] ?? \"\";\n const from = config.config[\"from\"] ?? user;\n if (!from) return;\n\n const prefix = config.config[\"subjectPrefix\"] ?? \"[zqagent]\";\n const transport = this.transportFactory({\n host,\n port,\n secure,\n auth: user ? { user, pass } : undefined,\n });\n\n try {\n await transport.sendMail({\n from,\n to,\n subject: `${prefix}${notifyTitle(ev.kind)}`,\n text: buildText(ev),\n html: buildHtml(ev),\n });\n } finally {\n // 关闭连接,避免每次通知都遗留连接(无连接池需求)\n try {\n transport.close();\n } catch {\n // 忽略关闭失败\n }\n }\n }\n}\n","/**\n * 通知模块装配入口\n *\n * 创建 NotificationHub 并注册各通道实现。\n * 新增通道只需实现 Notifier 接口并在 createNotificationHub 里注册。\n */\nimport { NotificationHub } from \"./NotificationHub.js\";\nimport { BarkNotifier } from \"./notifiers/BarkNotifier.js\";\nimport { ServerChanNotifier } from \"./notifiers/ServerChanNotifier.js\";\nimport { WebhookNotifier } from \"./notifiers/WebhookNotifier.js\";\nimport { EmailNotifier } from \"./notifiers/EmailNotifier.js\";\n\nexport { NotificationHub } from \"./NotificationHub.js\";\nexport type { Notifier } from \"./Notifier.js\";\nexport { channelWants, notifyTitle, notifyBody } from \"./Notifier.js\";\nexport {\n BarkNotifier,\n ServerChanNotifier,\n WebhookNotifier,\n EmailNotifier,\n};\n\n/** 装配默认通知中枢(注册内置通道) */\nexport function createNotificationHub(): NotificationHub {\n const hub = new NotificationHub();\n hub.register(new BarkNotifier());\n hub.register(new ServerChanNotifier());\n hub.register(new WebhookNotifier());\n hub.register(new EmailNotifier());\n return hub;\n}\n"],"mappings":"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","names":["TIMEOUT_MS","TIMEOUT_MS","escapeHtml"]}
1
+ {"version":3,"sources":["../../src/approval-view.ts","../../src/notify/Notifier.ts","../../src/notify/NotificationHub.ts","../../src/notify/notifiers/BarkNotifier.ts","../../src/notify/notifiers/ServerChanNotifier.ts","../../src/notify/notifiers/WebhookNotifier.ts","../../src/notify/notifiers/EmailNotifier.ts","../../src/notify/index.ts"],"sourcesContent":["/**\n * 审批视图:把审批请求(工具 / 原因 / 参数详情)规范化为统一的「字段行」,\n * 供 CLI、QQ、通用通知、邮件、MQTT、Web 等各端渲染成同一套表格。\n *\n * 唯一数据源是审批事件负载(toolName / reason / arguments),各通道只负责\n * 把这里产出的字段渲染成自己端上最好看的形态(终端框线表 / Markdown / HTML)。\n */\n\n/** 审批表格中的一行 */\nexport interface ApprovalField {\n /** 字段名(工具 / 原因 / 参数名) */\n label: string;\n /** 字段值(已完成换行折叠与截断) */\n value: string;\n}\n\n/** 统一审批输入 */\nexport interface ApprovalViewInput {\n toolName: string;\n reason: string;\n arguments?: Record<string, unknown> | null;\n}\n\n/** 单个字段值的默认最大长度(超出以 … 截断) */\nexport const APPROVAL_VALUE_MAX = 120;\n\n/** 表格列标题(各端保持一致) */\nexport const APPROVAL_COLUMNS = [\"项目\", \"详情\"] as const;\n\n/** 工具 / 原因两行的固定文案 */\nconst TOOL_LABEL = \"🔧 工具\";\nconst REASON_LABEL = \"💬 原因\";\n\n/** 把任意参数值折叠成单行文本并按需截断 */\nfunction formatValue(v: unknown, maxLen: number): string {\n let text: string;\n if (v == null) {\n text = \"\";\n } else if (typeof v === \"string\") {\n text = v;\n } else {\n try {\n text = JSON.stringify(v);\n } catch {\n text = String(v);\n }\n }\n text = text.replace(/\\r?\\n/g, \"↵\");\n return text.length > maxLen ? text.slice(0, maxLen) + \"…\" : text;\n}\n\n/** 从 NotifyEvent.data(审批事件负载)提取统一输入 */\nexport function approvalInputFromData(\n data: Record<string, unknown>,\n): ApprovalViewInput {\n const args = data[\"arguments\"];\n return {\n toolName: String(data[\"toolName\"] ?? \"\"),\n reason: String(data[\"reason\"] ?? \"\"),\n arguments:\n args && typeof args === \"object\"\n ? (args as Record<string, unknown>)\n : undefined,\n };\n}\n\n/**\n * 规范化为统一字段行:工具 → 原因 → 每个参数各一行(行序在所有通道一致)。\n */\nexport function approvalFields(\n input: ApprovalViewInput,\n maxLen = APPROVAL_VALUE_MAX,\n): ApprovalField[] {\n const fields: ApprovalField[] = [\n { label: TOOL_LABEL, value: formatValue(input.toolName, maxLen) },\n { label: REASON_LABEL, value: formatValue(input.reason, maxLen) },\n ];\n for (const [key, value] of Object.entries(input.arguments ?? {})) {\n fields.push({ label: formatValue(key, maxLen), value: formatValue(value, maxLen) });\n }\n return fields;\n}\n\n/** Markdown 表格单元格转义(竖线会破坏列结构) */\nfunction escapeCell(text: string): string {\n return text.replace(/\\|/g, \"\\\\|\").replace(/\\r?\\n/g, \"↵\");\n}\n\n/** 渲染为统一的两列 Markdown 表格 */\nexport function approvalToMarkdownTable(\n input: ApprovalViewInput,\n opts: { maxLen?: number; title?: string } = {},\n): string {\n const lines: string[] = [];\n if (opts.title) lines.push(opts.title, \"\");\n lines.push(`| ${APPROVAL_COLUMNS[0]} | ${APPROVAL_COLUMNS[1]} |`, \"| --- | --- |\");\n for (const f of approvalFields(input, opts.maxLen)) {\n lines.push(`| ${escapeCell(f.label)} | ${escapeCell(f.value)} |`);\n }\n return lines.join(\"\\n\");\n}\n\n/** HTML 转义 */\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n}\n\n/** 渲染为 HTML 表格(邮件 / Web 端直接展示为真正的表格) */\nexport function approvalToHtmlTable(\n input: ApprovalViewInput,\n maxLen = APPROVAL_VALUE_MAX,\n): string {\n const rows = approvalFields(input, maxLen)\n .map(\n (f) =>\n `<tr>` +\n `<th style=\"text-align:left;padding:4px 12px 4px 0;color:#6b7280;font-weight:500;white-space:nowrap;vertical-align:top;\">${escapeHtml(f.label)}</th>` +\n `<td style=\"padding:4px 0;font-family:ui-monospace,Menlo,Consolas,monospace;\">${escapeHtml(f.value)}</td>` +\n `</tr>`,\n )\n .join(\"\");\n return `<table style=\"border-collapse:collapse;font-size:13px;\">${rows}</table>`;\n}\n","/**\n * 通知通道接口\n *\n * 每种通知渠道(Telegram / 企微 / 钉钉 / 飞书 / Bark / 邮件 / 短信 / …)\n * 实现本接口。NotificationHub 按租户配置把 NotifyEvent 分发到各通道。\n * 实现必须是「尽力而为」:发送失败应抛出异常由 Hub 捕获降级,或自行吞掉。\n */\nimport type {\n NotificationChannelConfig,\n NotificationChannelType,\n NotifyEvent,\n} from \"../types.js\";\nimport {\n approvalInputFromData,\n approvalToMarkdownTable,\n} from \"../approval-view.js\";\n\nexport interface Notifier {\n /** 通道类型标识 */\n readonly type: NotificationChannelType;\n\n /**\n * 发送一条通知。\n * @param config 该租户此通道的配置(明文态,敏感字段已由上层解密)\n * @param ev 结构化通知事件\n */\n send(config: NotificationChannelConfig, ev: NotifyEvent): Promise<void>;\n}\n\n/** 依据通知事件类型判断是否命中该通道的事件过滤开关 */\nexport function channelWants(channel: NotificationChannelConfig, kind: NotifyEvent[\"kind\"]): boolean {\n const ev = channel.events;\n switch (kind) {\n case \"approval_request\":\n return ev.approval;\n case \"question_asked\":\n return ev.question;\n case \"turn_completed\":\n case \"turn_stopped\":\n return ev.completed;\n case \"error\":\n return ev.error;\n case \"session_status\":\n case \"session_snapshot\":\n case \"shell_tasks_changed\":\n // 纯状态同步:频率高、无用户动作,任何第三方渠道都不该收到\n return false;\n default:\n return true;\n }\n}\n\n/** 生成简短的通知标题(供各通道复用) */\nexport function notifyTitle(kind: NotifyEvent[\"kind\"]): string {\n switch (kind) {\n case \"approval_request\":\n return \"需要审批\";\n case \"question_asked\":\n return \"需要回答\";\n case \"turn_completed\":\n return \"任务完成\";\n case \"turn_stopped\":\n return \"达到迭代上限\";\n case \"error\":\n return \"任务出错\";\n default:\n return \"Agent 通知\";\n }\n}\n\n/** 生成简短的通知正文(供各通道复用,超长截断) */\nexport function notifyBody(ev: NotifyEvent, maxLen = 300): string {\n const d = ev.data;\n switch (ev.kind) {\n case \"approval_request\":\n return approvalToMarkdownTable(approvalInputFromData(d), {\n title: \"🔒 需要审批\",\n });\n case \"question_asked\":\n return String(d[\"question\"] ?? \"\");\n case \"turn_completed\":\n return `状态: ${String(d[\"status\"] ?? \"completed\")}`;\n case \"turn_stopped\":\n return \"已达最大迭代次数,可继续完成剩下的任务\";\n case \"error\":\n return String(d[\"error\"] ?? \"未知错误\");\n default:\n return \"\";\n }\n}\n","/**\n * NotificationHub — 通知中枢\n *\n * 注册各通知通道(Notifier),把 SessionManager 产出的 NotifyEvent\n * 按租户配置分发到启用的通道。发送是「尽力而为」,异常降级不冒泡。\n */\nimport type {\n Notifier,\n} from \"./Notifier.js\";\nimport { channelWants } from \"./Notifier.js\";\nimport type {\n NotificationChannelConfig,\n NotificationChannelType,\n NotifyEvent,\n} from \"../types.js\";\n\nexport class NotificationHub {\n private readonly notifiers = new Map<NotificationChannelType, Notifier>();\n\n /** 注册一个通道实现(幂等,后注册覆盖) */\n register(notifier: Notifier): void {\n this.notifiers.set(notifier.type, notifier);\n }\n\n /** 已注册的通道类型 */\n registeredTypes(): NotificationChannelType[] {\n return [...this.notifiers.keys()];\n }\n\n /**\n * 分发通知到给定的启用通道。\n * 单个通道失败仅记录,不影响其他通道与核心逻辑。\n */\n async dispatch(channels: NotificationChannelConfig[], ev: NotifyEvent): Promise<void> {\n for (const ch of channels) {\n if (!ch.enabled) continue;\n if (!channelWants(ch, ev.kind)) continue;\n const notifier = this.notifiers.get(ch.type);\n if (!notifier) continue;\n try {\n await notifier.send(ch, ev);\n } catch (e) {\n console.log(\n `[notify] ${ch.type} 发送失败: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n }\n }\n}\n","/**\n * Bark 通知通道(iOS)\n *\n * Bark 是一个 iOS 推送 App,只需一个 HTTP 请求即可推送。\n * config: { key: \"设备推送Key\", server?: \"https://api.day.app\" }\n */\nimport type { Notifier } from \"../Notifier.js\";\nimport { notifyTitle, notifyBody } from \"../Notifier.js\";\nimport type {\n NotificationChannelConfig,\n NotifyEvent,\n} from \"../../types.js\";\n\nconst TIMEOUT_MS = 10_000;\n\nexport class BarkNotifier implements Notifier {\n readonly type = \"bark\" as const;\n\n async send(config: NotificationChannelConfig, ev: NotifyEvent): Promise<void> {\n const key = config.config[\"key\"];\n if (!key) return;\n const server = (config.config[\"server\"] ?? \"https://api.day.app\").replace(/\\/+$/, \"\");\n const url =\n `${server}/${encodeURIComponent(key)}/` +\n `${encodeURIComponent(notifyTitle(ev.kind))}/${encodeURIComponent(notifyBody(ev))}`;\n const res = await fetch(url, {\n method: \"GET\",\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n }\n}\n","/**\n * Server 酱通知通道(微信服务号推送,国内常用)\n *\n * config: { sendkey: \"SCT 开头的 SendKey\" }\n */\nimport type { Notifier } from \"../Notifier.js\";\nimport { notifyTitle, notifyBody } from \"../Notifier.js\";\nimport type {\n NotificationChannelConfig,\n NotifyEvent,\n} from \"../../types.js\";\n\nconst TIMEOUT_MS = 10_000;\n\nexport class ServerChanNotifier implements Notifier {\n readonly type = \"serverchan\" as const;\n\n async send(config: NotificationChannelConfig, ev: NotifyEvent): Promise<void> {\n const sendkey = config.config[\"sendkey\"];\n if (!sendkey) return;\n const url = `https://sctapi.ftqq.com/${encodeURIComponent(sendkey)}.send`;\n const body = new URLSearchParams({\n title: notifyTitle(ev.kind),\n desp: notifyBody(ev),\n });\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body,\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n }\n}\n","/**\n * 通用 Webhook 通知通道\n *\n * 向用户配置的 URL POST JSON,作为自定义兜底(可对接任意中转/网关)。\n * config: { url: \"https://...\" }\n * payload: { title, body, kind, sessionId, ts, data }\n */\nimport type { Notifier } from \"../Notifier.js\";\nimport { notifyTitle, notifyBody } from \"../Notifier.js\";\nimport type {\n NotificationChannelConfig,\n NotifyEvent,\n} from \"../../types.js\";\n\nconst TIMEOUT_MS = 10_000;\n\nexport class WebhookNotifier implements Notifier {\n readonly type = \"webhook\" as const;\n\n async send(config: NotificationChannelConfig, ev: NotifyEvent): Promise<void> {\n const url = config.config[\"url\"];\n if (!url) return;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n title: notifyTitle(ev.kind),\n body: notifyBody(ev),\n kind: ev.kind,\n sessionId: ev.sessionId,\n ts: ev.ts,\n data: ev.data,\n }),\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n }\n}\n","/**\n * 邮件通知通道(通用 SMTP)\n *\n * 通过 SMTP 发送任务完成 / 审批 / 提问 / 错误等通知。兼容 QQ 邮箱\n * (smtp.qq.com:465 SSL + 授权码)、163、Gmail 等任意 SMTP 服务。\n *\n * config: {\n * host: \"smtp.qq.com\",\n * port: \"465\", // 省略时按 secure 推断 465/587\n * secure: \"true\", // \"true\"=SSL,\"false\"=STARTTLS\n * user: \"you@qq.com\",\n * pass: \"授权码\", // 密文落盘\n * from: \"you@qq.com\", // 省略时取 user\n * to: \"you@qq.com\", // 可多个逗号分隔\n * subjectPrefix: \"[zqagent]\" // 可选,默认 [zqagent]\n * }\n */\nimport nodemailer, { type Transporter } from \"nodemailer\";\nimport type { Notifier } from \"../Notifier.js\";\nimport { notifyTitle } from \"../Notifier.js\";\nimport {\n approvalInputFromData,\n approvalToHtmlTable,\n approvalToMarkdownTable,\n} from \"../../approval-view.js\";\nimport type {\n NotificationChannelConfig,\n NotifyEvent,\n} from \"../../types.js\";\n\n/** SMTP 连接参数(透传给 nodemailer.createTransport) */\nexport interface SmtpOptions {\n host: string;\n port: number;\n secure: boolean;\n auth?: { user: string; pass: string };\n}\n\n/** 可注入的 transport 工厂(测试时替换为 fake) */\nexport type TransportFactory = (opts: SmtpOptions) => Transporter;\n\n/** 默认工厂:创建真实 nodemailer transport */\nexport function defaultTransportFactory(opts: SmtpOptions): Transporter {\n return nodemailer.createTransport(opts);\n}\n\n/** 按事件类型生成纯文本正文(turn_completed 时附产出文件与摘要) */\nfunction buildText(ev: NotifyEvent): string {\n const d = ev.data;\n switch (ev.kind) {\n case \"turn_completed\":\n case \"turn_stopped\": {\n const lines: string[] = [];\n lines.push(`会话: ${ev.sessionId}`);\n lines.push(`状态: ${String(d[\"status\"] ?? ev.kind)}`);\n if (d[\"iterations\"] !== undefined)\n lines.push(`迭代次数: ${String(d[\"iterations\"])}`);\n const files = d[\"produced_files\"] as string[] | undefined;\n if (Array.isArray(files) && files.length > 0)\n lines.push(`产出文件:\\n ${files.join(\"\\n \")}`);\n const summary = typeof d[\"summary\"] === \"string\" ? d[\"summary\"] : \"\";\n if (summary.trim()) lines.push(`\\n摘要:\\n${summary}`);\n return lines.join(\"\\n\");\n }\n case \"approval_request\":\n return `${approvalToMarkdownTable(approvalInputFromData(d), {\n title: \"🔒 需要审批\",\n })}\\n会话: ${ev.sessionId}`;\n case \"question_asked\":\n return `问题: ${String(d[\"question\"] ?? \"\")}\\n会话: ${ev.sessionId}`;\n case \"error\":\n return `错误: ${String(d[\"error\"] ?? \"未知错误\")}\\n会话: ${ev.sessionId}`;\n default:\n return `会话: ${ev.sessionId}\\n时间: ${ev.ts}`;\n }\n}\n\n/** HTML 转义 */\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n}\n\n/** 将正文转成 HTML:审批渲染为真正的表格,其余保持 <pre> 换行 */\nfunction buildHtml(ev: NotifyEvent): string {\n if (ev.kind === \"approval_request\") {\n const table = approvalToHtmlTable(approvalInputFromData(ev.data));\n return (\n table +\n `<p style=\"color:#6b7280;font-size:12px;margin:10px 0 0;\">会话: ${escapeHtml(ev.sessionId)}</p>`\n );\n }\n return `<pre style=\"white-space:pre-wrap;font-family:inherit;\">${escapeHtml(buildText(ev))}</pre>`;\n}\n\nexport class EmailNotifier implements Notifier {\n readonly type = \"email\" as const;\n\n constructor(\n private readonly transportFactory: TransportFactory = defaultTransportFactory,\n ) {}\n\n async send(config: NotificationChannelConfig, ev: NotifyEvent): Promise<void> {\n // 短任务过滤:任务成功完成且执行时长未超过阈值(默认 60s)时不发邮件。\n // 可用 config.minDurationSeconds 覆盖阈值。\n if (ev.kind === \"turn_completed\") {\n const minSec = Number(config.config[\"minDurationSeconds\"] ?? 60);\n const durationMs = ev.data[\"durationMs\"];\n if (typeof durationMs === \"number\" && durationMs < minSec * 1000) {\n return;\n }\n }\n\n const host = config.config[\"host\"];\n const to = config.config[\"to\"];\n if (!host || !to) return;\n\n const secure = config.config[\"secure\"] === \"true\";\n const port =\n Number(config.config[\"port\"]) || (secure ? 465 : 587);\n const user = config.config[\"user\"] ?? \"\";\n const pass = config.config[\"pass\"] ?? \"\";\n const from = config.config[\"from\"] ?? user;\n if (!from) return;\n\n const prefix = config.config[\"subjectPrefix\"] ?? \"[zqagent]\";\n const transport = this.transportFactory({\n host,\n port,\n secure,\n auth: user ? { user, pass } : undefined,\n });\n\n try {\n await transport.sendMail({\n from,\n to,\n subject: `${prefix}${notifyTitle(ev.kind)}`,\n text: buildText(ev),\n html: buildHtml(ev),\n });\n } finally {\n // 关闭连接,避免每次通知都遗留连接(无连接池需求)\n try {\n transport.close();\n } catch {\n // 忽略关闭失败\n }\n }\n }\n}\n","/**\n * 通知模块装配入口\n *\n * 创建 NotificationHub 并注册各通道实现。\n * 新增通道只需实现 Notifier 接口并在 createNotificationHub 里注册。\n */\nimport { NotificationHub } from \"./NotificationHub.js\";\nimport { BarkNotifier } from \"./notifiers/BarkNotifier.js\";\nimport { ServerChanNotifier } from \"./notifiers/ServerChanNotifier.js\";\nimport { WebhookNotifier } from \"./notifiers/WebhookNotifier.js\";\nimport { EmailNotifier } from \"./notifiers/EmailNotifier.js\";\n\nexport { NotificationHub } from \"./NotificationHub.js\";\nexport type { Notifier } from \"./Notifier.js\";\nexport { channelWants, notifyTitle, notifyBody } from \"./Notifier.js\";\nexport {\n BarkNotifier,\n ServerChanNotifier,\n WebhookNotifier,\n EmailNotifier,\n};\n\n/** 装配默认通知中枢(注册内置通道) */\nexport function createNotificationHub(): NotificationHub {\n const hub = new NotificationHub();\n hub.register(new BarkNotifier());\n hub.register(new ServerChanNotifier());\n hub.register(new WebhookNotifier());\n hub.register(new EmailNotifier());\n return hub;\n}\n"],"mappings":"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","names":["TIMEOUT_MS","TIMEOUT_MS","escapeHtml"]}
@@ -26,8 +26,10 @@ __export(wire_exports, {
26
26
  FrameError: () => FrameError,
27
27
  FrameType: () => FrameType,
28
28
  MAX_FRAME_BYTES: () => MAX_FRAME_BYTES,
29
+ MAX_STREAM_BUFFER_BYTES: () => MAX_STREAM_BUFFER_BYTES,
29
30
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
30
31
  Reader: () => Reader,
32
+ StreamDecoder: () => StreamDecoder,
31
33
  Utf8Error: () => Utf8Error,
32
34
  WIRE_FIXED32: () => WIRE_FIXED32,
33
35
  WIRE_FIXED64: () => WIRE_FIXED64,
@@ -373,6 +375,7 @@ var FrameError = class extends Error {
373
375
  }
374
376
  };
375
377
  var MIN_HEADER_BYTES = 5;
378
+ var MAX_STREAM_BUFFER_BYTES = MAX_FRAME_BYTES * 2 + 64;
376
379
  function encodeFrame(frame) {
377
380
  if (!Number.isInteger(frame.type) || frame.type < 0 || frame.type > 15) {
378
381
  throw new FrameError(`\u975E\u6CD5\u5E27\u7C7B\u578B: ${frame.type}`);
@@ -451,8 +454,86 @@ function frameTypeFromName(name) {
451
454
  if (!(name in FrameType)) throw new FrameError(`\u672A\u77E5\u5E27\u7C7B\u578B\u540D: ${name}`);
452
455
  return FrameType[name];
453
456
  }
457
+ function peekVarint(b, at) {
458
+ let value = 0n;
459
+ let shift = 0n;
460
+ for (let i = 0; i < 10; i++) {
461
+ if (at + i >= b.length) return null;
462
+ const byte = b[at + i];
463
+ if (i === 9 && byte > 1) return "bad";
464
+ value |= BigInt(byte & 127) << shift;
465
+ if ((byte & 128) === 0) {
466
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) return "bad";
467
+ return { value: Number(value), size: i + 1 };
468
+ }
469
+ shift += 7n;
470
+ }
471
+ return "bad";
472
+ }
473
+ var StreamDecoder = class {
474
+ constructor(maxBuffer = MAX_STREAM_BUFFER_BYTES) {
475
+ this.maxBuffer = maxBuffer;
476
+ }
477
+ maxBuffer;
478
+ buf = new Uint8Array(0);
479
+ /** 已缓存但尚未解出的字节数 */
480
+ get buffered() {
481
+ return this.buf.length;
482
+ }
483
+ /** 丢弃缓存(协议错误后重新同步用) */
484
+ reset() {
485
+ this.buf = new Uint8Array(0);
486
+ }
487
+ push(chunk) {
488
+ if (this.buf.length === 0) {
489
+ this.buf = chunk.slice();
490
+ } else {
491
+ const next = new Uint8Array(this.buf.length + chunk.length);
492
+ next.set(this.buf, 0);
493
+ next.set(chunk, this.buf.length);
494
+ this.buf = next;
495
+ }
496
+ if (this.buf.length > this.maxBuffer) {
497
+ throw new FrameError(`\u63A5\u6536\u7F13\u51B2\u8D85\u9650: ${this.buf.length} > ${this.maxBuffer}`);
498
+ }
499
+ }
500
+ /** 解出下一帧;不足返回 `null`。协议错误抛 `FrameError`。 */
501
+ nextFrame() {
502
+ const b = this.buf;
503
+ if (b.length < MIN_HEADER_BYTES) return null;
504
+ if (b[0] !== FRAME_MAGIC[0] || b[1] !== FRAME_MAGIC[1]) {
505
+ throw new FrameError("magic \u4E0D\u7B26\uFF08\u975E\u672C\u534F\u8BAE\u6D41\u91CF\uFF09");
506
+ }
507
+ const verType = b[2];
508
+ const version = verType >> 4;
509
+ const type = verType & 15;
510
+ if (version !== PROTOCOL_VERSION) {
511
+ throw new FrameError(`\u4E0D\u652F\u6301\u7684\u534F\u8BAE\u7248\u672C: ${version}`);
512
+ }
513
+ if (!KNOWN_TYPES.has(type)) {
514
+ throw new FrameError(`\u672A\u5B9A\u4E49\u7684\u5E27\u7C7B\u578B: ${type}`);
515
+ }
516
+ const seqPeek = peekVarint(b, 3);
517
+ if (seqPeek === null) return null;
518
+ if (seqPeek === "bad") throw new FrameError("\u5E27\u5934 varint \u975E\u6CD5: seq");
519
+ const lenPeek = peekVarint(b, 3 + seqPeek.size);
520
+ if (lenPeek === null) return null;
521
+ if (lenPeek === "bad") throw new FrameError("\u5E27\u5934 varint \u975E\u6CD5: len");
522
+ if (lenPeek.value > MAX_FRAME_BYTES) {
523
+ throw new FrameError(`\u8F7D\u8377\u8D85\u9650: ${lenPeek.value} > ${MAX_FRAME_BYTES}`);
524
+ }
525
+ const header = 3 + seqPeek.size + lenPeek.size;
526
+ const total = header + lenPeek.value;
527
+ if (b.length < total) return null;
528
+ const payload = b.slice(header, total);
529
+ this.buf = b.subarray(total);
530
+ return { type, seq: seqPeek.value, payload };
531
+ }
532
+ };
454
533
 
455
534
  // src/wire/messages.ts
535
+ var MSG_KIND_NODE_STATE = 1;
536
+ var MSG_KIND_STATUS = 2;
456
537
  function encodeEnvelope(env) {
457
538
  const w = new Writer(128);
458
539
  w.uint64Field(1, env.t);
@@ -578,6 +659,7 @@ function encodeStatus(st) {
578
659
  w.stringField(6, st.mode ?? "");
579
660
  w.stringField(7, st.version ?? "");
580
661
  w.stringField(8, st.ip ?? "");
662
+ w.uint64Field(9, MSG_KIND_STATUS);
581
663
  return w.finish();
582
664
  }
583
665
  function decodeStatus(bytes) {
@@ -618,6 +700,11 @@ function decodeStatus(bytes) {
618
700
  if (wire !== WIRE_LEN) r.skip(wire);
619
701
  else st.ip = r.string();
620
702
  break;
703
+ case 9: {
704
+ if (wire !== WIRE_VARINT) r.skip(wire);
705
+ else checkMsgKind("Status", Number(r.varint()), MSG_KIND_STATUS);
706
+ break;
707
+ }
621
708
  default:
622
709
  r.skip(wire);
623
710
  }
@@ -957,6 +1044,7 @@ function encodeNodeState(n) {
957
1044
  w.stringField(5, n.version ?? "");
958
1045
  w.stringField(6, n.ip ?? "");
959
1046
  w.bytesField(7, n.detail ?? EMPTY);
1047
+ w.uint64Field(9, MSG_KIND_NODE_STATE);
960
1048
  return w.finish();
961
1049
  }
962
1050
  function decodeNodeState(bytes) {
@@ -993,12 +1081,24 @@ function decodeNodeState(bytes) {
993
1081
  if (wire !== WIRE_LEN) r.skip(wire);
994
1082
  else n.detail = r.lenPrefixed();
995
1083
  break;
1084
+ case 9: {
1085
+ if (wire !== WIRE_VARINT) r.skip(wire);
1086
+ else checkMsgKind("NodeState", Number(r.varint()), MSG_KIND_NODE_STATE);
1087
+ break;
1088
+ }
996
1089
  default:
997
1090
  r.skip(wire);
998
1091
  }
999
1092
  }
1000
1093
  return n;
1001
1094
  }
1095
+ function checkMsgKind(label, actual, expected) {
1096
+ if (actual !== 0 && actual !== expected) {
1097
+ throw new WireError(
1098
+ `${label} \u5E27\u7684\u8EAB\u4EFD\u6807\u8BB0\u4E0D\u7B26\uFF08msg_kind=${actual}\uFF0C\u671F\u671B ${expected}\uFF09\uFF1A\u5F88\u53EF\u80FD\u7528\u9519\u4E86\u89E3\u7801\u5668`
1099
+ );
1100
+ }
1101
+ }
1002
1102
  function encodeSnapshot(snap) {
1003
1103
  const w = new Writer(128);
1004
1104
  for (const node of snap.nodes) {
@@ -1109,8 +1209,10 @@ function fromHex(hex) {
1109
1209
  FrameError,
1110
1210
  FrameType,
1111
1211
  MAX_FRAME_BYTES,
1212
+ MAX_STREAM_BUFFER_BYTES,
1112
1213
  PROTOCOL_VERSION,
1113
1214
  Reader,
1215
+ StreamDecoder,
1114
1216
  Utf8Error,
1115
1217
  WIRE_FIXED32,
1116
1218
  WIRE_FIXED64,