@zqagent/shared 0.3.11 → 0.3.12

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.
@@ -87,8 +87,8 @@ interface Notifier {
87
87
  declare function channelWants(channel: NotificationChannelConfig, kind: NotifyEvent["kind"]): boolean;
88
88
  /** 生成简短的通知标题(供各通道复用) */
89
89
  declare function notifyTitle(kind: NotifyEvent["kind"]): string;
90
- /** 生成简短的通知正文(供各通道复用,超长截断) */
91
- declare function notifyBody(ev: NotifyEvent, maxLen?: number): string;
90
+ /** 生成简短的通知正文(供各通道复用) */
91
+ declare function notifyBody(ev: NotifyEvent): string;
92
92
 
93
93
  /**
94
94
  * NotificationHub — 通知中枢
@@ -87,8 +87,8 @@ interface Notifier {
87
87
  declare function channelWants(channel: NotificationChannelConfig, kind: NotifyEvent["kind"]): boolean;
88
88
  /** 生成简短的通知标题(供各通道复用) */
89
89
  declare function notifyTitle(kind: NotifyEvent["kind"]): string;
90
- /** 生成简短的通知正文(供各通道复用,超长截断) */
91
- declare function notifyBody(ev: NotifyEvent, maxLen?: number): string;
90
+ /** 生成简短的通知正文(供各通道复用) */
91
+ declare function notifyBody(ev: NotifyEvent): string;
92
92
 
93
93
  /**
94
94
  * NotificationHub — 通知中枢
package/dist/index.cjs CHANGED
@@ -589,7 +589,7 @@ function notifyTitle(kind) {
589
589
  return "Agent \u901A\u77E5";
590
590
  }
591
591
  }
592
- function notifyBody(ev, maxLen = 300) {
592
+ function notifyBody(ev) {
593
593
  const d = ev.data;
594
594
  switch (ev.kind) {
595
595
  case "approval_request":
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/fleet.ts","../src/common.ts","../src/secrets.ts","../src/approval-view.ts","../src/llm-presets.ts","../src/media.ts","../src/types.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 * @zqagent/shared — core(节点/CLI)与 hub(控制面)共用的基础模块\n *\n * 子路径导出:`./fleet`、`./secrets`、`./approval-view`、`./notify`、`./types`。\n */\nexport * from \"./fleet.js\";\nexport * from \"./secrets.js\";\nexport * from \"./approval-view.js\";\nexport * from \"./llm-presets.js\";\nexport * from \"./media.js\";\nexport * from \"./types.js\";\nexport * from \"./notify/index.js\";\n","/**\n * MQTT 节点管理协议 v2(控制台 ↔ 多终端设备)\n *\n * 与 v1(`protocol.ts`,按租户、deviceId 的手机控制)并存。v2 面向\n * 「公网控制台管理多台终端 Agent 实例」:\n *\n * - **按节点寻址**:topic 含 `node/{nodeId}`,每个节点只订阅自己的 cmd;\n * - **在线状态**:`status` retained + LWT(遗嘱)自动上线/离线,`info` retained\n * (能力/项目快照,上线即重建节点管理视图);\n * - **请求-响应关联**:命令带 `reqId`,回执(ack)回带同一 `reqId`;\n * - **签名密钥**:节点管理级 `fleetSecret`(自托管单所有者模型;每节点独立密钥留待 P3)。\n *\n * 安全前提同 v1:broker 可无认证/ACL,隔离靠「高熵 fleetToken 命名空间 + HMAC 签名」。\n */\nimport * as crypto from \"crypto\";\nimport {\n DEFAULT_CLOCK_SKEW_MS,\n NonceTracker,\n generateToken,\n isFreshTimestamp,\n} from \"./common.js\";\n\nexport { NonceTracker, generateToken, isFreshTimestamp, DEFAULT_CLOCK_SKEW_MS };\n\n/** topic 根前缀 */\nexport const FLEET_TOPIC_PREFIX = \"zqagent/fleet\";\n\n/**\n * 会话状态条目(跨节点总览的唯一数据形状)。\n *\n * 刻意只包含「标识 + 状态」:消息、模型、token 等明细由会话详情页按需拉取。\n * 节点经 info `detail.sessions` 上报全量快照,状态变化经 notify `session_status`\n * 增量推送;接口(REST / 指令)也用同一形状。\n */\n/**\n * 会话状态条目(跨节点总览的唯一数据形状)。\n *\n * - `status` 取 `SessionStatus` 的状态值;另有一个**伪状态** `\"deleted\"`:\n * 会话已删除,只出现在 `session_status` 通知里(不落盘、不进状态机),\n * 供常开的总览页面立即移除该行。\n * - 只含标识与状态:消息、模型、token 等明细由会话详情页按需拉取。\n */\nexport interface SessionStateEntry {\n projectId: string;\n /** 项目展示名(节点侧填充;详情快照里由 projects 补齐) */\n projectName: string;\n sessionId: string;\n name: string | null;\n status: string;\n /** 用户最近一条发送给 Agent 的消息(截断,供总览列表展示;无消息时 null) */\n lastUserMessage?: string | null;\n /**\n * 最近一次**任务请求的开始时刻**(ISO 8601;从未发起过任务时 null)。\n *\n * 工作台的节点/项目/任务列表统一按它倒序——用户找的是「最近在跑什么」,\n * 而不是「最近什么时候跑完的」:长任务运行中时 lastCompletedAt 还是上一轮的,\n * 按它排会把正在干活的排到后面。\n */\n lastStartedAt?: string | null;\n /** 最近一次任务成功完成的时刻(ISO 8601;从未完成时 null) */\n lastCompletedAt?: string | null;\n}\n\n/** 命令类型(控制台 → 节点) */\nexport type FleetCmdType =\n | \"ping\"\n | \"get_node_info\"\n | \"get_node_host\"\n | \"list_projects\"\n | \"create_project\"\n | \"delete_project\"\n | \"list_sessions\"\n /** 跨项目会话状态(只含标识与 status,供节点首页/工作台总览) */\n | \"get_session_states\"\n | \"create_session\"\n | \"delete_session\"\n | \"get_state\"\n | \"get_events\"\n /** 会话轮次汇总(轻量:标题 + 统计,点开某轮再取事件) */\n | \"get_session_turns\"\n /** 单轮详情:用户消息(含补充)+ 最终回复 + 产出文件 */\n | \"get_session_turn_reply\"\n /** 读取聊天记录里保存的附件(用户消息的图片 / 文件) */\n | \"read_session_attachment\"\n /** 变更追踪:本会话的快照列表(每轮一个) */\n | \"get_session_changes\"\n /** 变更追踪:某个快照的完整 patch */\n | \"get_session_change_diff\"\n /** 变更追踪:从快照恢复文件(会改工作区) */\n | \"restore_session_change\"\n | \"get_config\"\n | \"update_config\"\n | \"list_models\"\n | \"list_dirs\"\n | \"make_dir\"\n | \"search_dirs\"\n | \"list_files\"\n | \"read_file\"\n /** 按区间读取文件(音视频预览的数据面;回执为 base64 块,单块 ≤1MB) */\n | \"read_file_range\"\n /**\n * 签发媒体播放票据。\n *\n * `<video>` 发不了 Authorization 头,所以由节点用 fleetSecret 签一张只授权\n * 单个文件、10 分钟有效的票据,网关用同一密钥验签后提供 HTTP Range。\n */\n | \"file_ticket\"\n | \"download_file\"\n | \"write_file\"\n /** 按 base64 写入二进制文件(图片编辑后保存) */\n | \"write_file_base64\"\n | \"create_dir\"\n | \"create_file\"\n | \"delete_files\"\n | \"search_files\"\n /** 在文件**内容**里搜索关键字(返回行号与命中片段) */\n | \"search_content\"\n /** 重命名文件 / 目录(只改同一目录下的名字) */\n | \"rename_path\"\n /** 把文件 / 目录移动到工作区内的目标目录(拖拽移动) */\n | \"move_paths\"\n /** 压缩下载:把选中的文件 / 目录打包成 zip(超过上限直接拒绝) */\n | \"zip_download\"\n | \"list_tools\"\n | \"list_mcp\"\n /** 整体替换项目的 MCP 服务器配置(控制台设置页保存) */\n | \"update_mcp\"\n /** 连接测试单个 MCP 服务器(不落盘) */\n | \"test_mcp\"\n | \"project_stats\"\n | \"run_command\"\n | \"list_shell_tasks\"\n | \"kill_shell_task\"\n // ---------- 交互式终端(控制台「终端」面板) ----------\n // 只承载低频控制动作;用户按键(input)与窗口尺寸(resize)走\n // `FrameType.TERMINAL` 独立帧通道——那条路不等 ACK、不占单节点串行指令队列。\n /** 列出项目下的终端(含已退出未删除的) */\n | \"terminal_list\"\n /** 新建终端(多终端并行:每次一个新进程) */\n | \"terminal_create\"\n /** 取回放缓冲(≤200KB)与元信息:切页签 / 断线重连用 */\n | \"terminal_replay\"\n /** 关闭进程(SIGTERM → 2s → SIGKILL),保留历史与元信息 */\n | \"terminal_close\"\n /** 关闭并删除历史(不可恢复) */\n | \"terminal_remove\"\n | \"terminal_rename\"\n /** 命令/输出/退出历史(受保留期与落盘限速约束) */\n | \"terminal_history\"\n /** 终端设置(当前只有 retentionDays,默认 30 天) */\n | \"terminal_settings\"\n | \"terminal_update_settings\"\n | \"git_repos\"\n | \"git_status\"\n | \"git_diff\"\n | \"git_branches\"\n | \"git_refs\"\n | \"git_log\"\n | \"git_show\"\n | \"git_fetch\"\n | \"git_stage\"\n | \"git_unstage\"\n | \"git_commit\"\n /** AI 生成提交说明(节点侧调用 LLM) */\n | \"git_commit_message\"\n | \"git_checkout\"\n | \"git_pull\"\n | \"git_push\"\n | \"set_node_label\"\n | \"get_usage_summary\"\n /** 大模型账户余额(控制台「余额不足」时查看/充值用) */\n | \"get_llm_balance\"\n | \"get_usage_records\"\n | \"get_usage_pricing\"\n | \"update_usage_pricing\"\n | \"get_node_llm\"\n | \"update_node_llm\"\n /**\n * 节点自更新(自更新仅在「SEA 单文件 + 服务托管 + 配置验签公钥」时可用):\n * payload.action = \"status\" | \"check\" | \"install\"\n * payload.force = true 时立即安装(不等空闲,会打断运行中的任务)\n *\n * install 先回执、后在后台下载安装并退出进程,由 systemd/launchd 拉起新版本;\n * 因此**回执成功不等于已升级**,需再用 status 确认版本。\n */\n | \"self_update\"\n /**\n * 重新拉取平台内置模型(志前 API)的凭据。\n *\n * 内置模型在接入 Hub 时下发,正常情况下用户无需关心;这里用于\n * 「Key 在控制台被重置」或用户在控制台点「刷新」时重取。\n * 会改写节点侧的 LLM 配置,属写指令(不参与只读并发池)。\n */\n | \"refresh_builtin_llm\"\n | \"list_node_llm_models\"\n | \"set_fleet_cred\"\n | \"rename_session\"\n | \"set_session_model\"\n | \"set_session_llm\"\n | \"set_session_mode\"\n | \"set_session_lang\"\n | \"set_session_draft\"\n | \"submit_turn\"\n | \"steer\"\n | \"unsteer\"\n | \"interrupt\"\n | \"resume\"\n | \"approval_answer\"\n | \"question_answer\"\n /**\n * 批量指令:payload.commands 为子指令数组,节点顺序/并发执行后\n * 在单条回执的 data.results 中按序返回各子指令结果(减少往返与请求数)。\n */\n | \"batch\";\n\nexport const FLEET_CMD_TYPES: readonly FleetCmdType[] = [\n \"ping\",\n \"get_node_info\",\n \"get_node_host\",\n \"list_projects\",\n \"create_project\",\n \"delete_project\",\n \"list_sessions\",\n \"get_session_states\",\n \"create_session\",\n \"delete_session\",\n \"get_state\",\n \"get_events\",\n \"get_session_turns\",\n \"get_session_turn_reply\",\n \"read_session_attachment\",\n \"get_session_changes\",\n \"get_session_change_diff\",\n \"restore_session_change\",\n \"get_config\",\n \"update_config\",\n \"list_models\",\n \"list_dirs\",\n \"make_dir\",\n \"search_dirs\",\n \"list_files\",\n \"read_file\",\n \"read_file_range\",\n \"file_ticket\",\n \"download_file\",\n \"write_file\",\n \"write_file_base64\",\n \"create_dir\",\n \"create_file\",\n \"delete_files\",\n \"search_files\",\n \"search_content\",\n \"rename_path\",\n \"move_paths\",\n \"zip_download\",\n \"list_tools\",\n \"list_mcp\",\n \"update_mcp\",\n \"test_mcp\",\n \"project_stats\",\n \"run_command\",\n \"list_shell_tasks\",\n \"kill_shell_task\",\n \"terminal_list\",\n \"terminal_create\",\n \"terminal_replay\",\n \"terminal_close\",\n \"terminal_remove\",\n \"terminal_rename\",\n \"terminal_history\",\n \"terminal_settings\",\n \"terminal_update_settings\",\n \"git_repos\",\n \"git_status\",\n \"git_diff\",\n \"git_branches\",\n \"git_refs\",\n \"git_log\",\n \"git_show\",\n \"git_fetch\",\n \"git_stage\",\n \"git_unstage\",\n \"git_commit\",\n \"git_commit_message\",\n \"git_checkout\",\n \"git_pull\",\n \"git_push\",\n \"set_node_label\",\n \"get_usage_summary\",\n \"get_llm_balance\",\n \"get_usage_records\",\n \"get_usage_pricing\",\n \"update_usage_pricing\",\n \"get_node_llm\",\n \"update_node_llm\",\n \"self_update\",\n \"refresh_builtin_llm\",\n \"list_node_llm_models\",\n \"set_fleet_cred\",\n \"rename_session\",\n \"set_session_model\",\n \"set_session_llm\",\n \"set_session_mode\",\n \"set_session_lang\",\n \"set_session_draft\",\n \"submit_turn\",\n \"steer\",\n \"unsteer\",\n \"interrupt\",\n \"resume\",\n \"approval_answer\",\n \"question_answer\",\n \"batch\",\n];\n\n/** 批量指令的子指令(不含嵌套 batch) */\nexport interface FleetBatchSubCmd {\n type: Exclude<FleetCmdType, \"batch\">;\n projectId?: string;\n sessionId?: string;\n payload?: Record<string, unknown>;\n}\n\n/** 批量指令结果项(与请求按序对应) */\nexport interface FleetBatchResult {\n type: string;\n ok: boolean;\n data?: unknown;\n error?: string;\n}\n\n/** 命令信封 v2(含 HMAC 签名) */\nexport interface FleetCmdEnvelope {\n /** 时间戳(毫秒,±60s 窗口) */\n t: number;\n /** 一次性随机串(防重放) */\n nonce: string;\n /** 请求 ID(回执关联) */\n reqId: string;\n type: FleetCmdType;\n /** 发起控制台客户端 ID */\n clientId: string;\n /** 目标节点 ID */\n nodeId: string;\n /** 目标项目 ID(项目级命令) */\n projectId?: string;\n /** 目标会话 ID(会话级命令) */\n sessionId?: string;\n payload: Record<string, unknown>;\n /** HMAC-SHA256(fleetSecret, 规范化串) base64url */\n sig: string;\n}\n\n/** 命令回执(点对点回给 clientId) */\nexport interface FleetAckPayload {\n reqId: string;\n type: FleetCmdType;\n ok: boolean;\n error?: string;\n data?: unknown;\n}\n\n/** 节点在线状态(retained + LWT) */\nexport interface FleetStatus {\n nodeId: string;\n online: boolean;\n ts: string;\n /** 节点展示名(本地配置;Hub 改名后回写并随之上报) */\n label?: string;\n platform?: string;\n mode?: string;\n version?: string;\n /**\n * 节点自报的主 IP(网卡地址:首个非回环 IPv4,缺省 IPv6)。\n *\n * 它是内网地址,控制台展示节点公网 IP 时应以网关观察到的连接来源为准\n * (见 gateway `NodeStateInfo.peer_ip`)。\n */\n ip?: string;\n}\n\n/** 节点累计运行统计(info 上报;进程启动后累计) */\nexport interface FleetNodeStats {\n /** 完成的回合数(turn_end) */\n turns: number;\n /** 工具调用完成数(tool_finished) */\n toolCalls: number;\n /** 错误事件数(error) */\n errors: number;\n /** 累计输入 token(assistant_message.usage.input) */\n tokensIn: number;\n /** 累计输出 token */\n tokensOut: number;\n /** 统计起点(节点进程启动/桥接建立时间) */\n since: string;\n}\n\n/** 节点管理配对凭据(pairing API 下发) */\nexport interface FleetPairing {\n fleetToken: string;\n fleetSecret: string;\n nodeId: string;\n /** Rust 网关 wss 地址(控制面配置了才下发;节点据此直连网关) */\n gatewayUrl?: string;\n}\n\n/**\n * 节点本地持久化的云端接入凭据(`{DATA_DIR}/fleet.json`)。\n *\n * 由「凭注册码接入」(`POST /api/hub/enroll`)返回后落盘;节点启动时优先\n * 读取此文件,其次回退到环境变量(`FLEET_TOKEN` / `FLEET_SECRET`)。\n *\n * 「去 EMQX」后不再有 per-fleet broker 账号:网关鉴权用**全局** `GATEWAY_FLEET_SECRET`,\n * 因此凭据只需 fleetToken/fleetSecret(前者仅作命名空间标识)与网关地址。\n */\nexport interface FleetCredential {\n /** Hub 控制面地址(接入时使用,便于日后重新配对/展示) */\n hubUrl?: string;\n fleetToken: string;\n fleetSecret: string;\n nodeId: string;\n /**\n * Rust 网关 wss 地址(控制面在配对/接入时下发)。\n *\n * 它是节点接入云端的**唯一**传输,环境变量 `FLEET_GATEWAY_URL` 仅作覆盖。\n */\n gatewayUrl?: string;\n /** 节点展示名(可选) */\n label?: string;\n /** 接入时间(ISO 字符串) */\n enrolledAt?: string;\n /**\n * 平台内置大模型(志前 API):接入 Hub 时由控制面下发,节点侧落盘为\n * `id=__zqai__` 的不可删除供应商。\n *\n * 这里只是「接入那一刻的快照」;凭据轮换或首次缺失时,节点会调用\n * `GET /api/hub/node/builtin-llm` 重新拉取(见 core 的 builtinLlm.ts)。\n */\n builtinLlm?: BuiltinLlmGrant;\n}\n\n/** 平台内置模型的接入凭据(下发给节点) */\nexport interface BuiltinLlmGrant {\n /** 固定为 `__zqai__` */\n providerId: string;\n /** 展示名 */\n name: string;\n /** OpenAI 兼容基址,如 https://zqai-api.cd-zq.com/v1 */\n baseURL: string;\n /** 用户的 API Key(节点侧加密落盘) */\n apiKey: string;\n /** 默认模型 */\n model: string;\n /** 该账号可用的模型列表 */\n models?: string[];\n}\n\n/** topic 构建 */\nexport function fleetCmdTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/cmd`;\n}\nexport function fleetNotifyTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/notify`;\n}\nexport function fleetEventsTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/events`;\n}\nexport function fleetStatusTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/status`;\n}\nexport function fleetInfoTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/info`;\n}\nexport function fleetAckTopic(\n fleetToken: string,\n nodeId: string,\n clientId: string,\n): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/ack/${clientId}`;\n}\n\n/** 签名前的规范化串(字段顺序固定,两侧一致) */\nfunction canonical(env: Omit<FleetCmdEnvelope, \"sig\">): string {\n return JSON.stringify([\n env.t,\n env.nonce,\n env.reqId,\n env.type,\n env.clientId,\n env.nodeId,\n env.projectId ?? \"\",\n env.sessionId ?? \"\",\n env.payload,\n ]);\n}\n\n/** 计算 HMAC-SHA256 签名 */\nexport function signFleetCmd(\n secret: string,\n env: Omit<FleetCmdEnvelope, \"sig\">,\n): string {\n return crypto\n .createHmac(\"sha256\", Buffer.from(secret, \"utf8\"))\n .update(canonical(env))\n .digest(\"base64url\");\n}\n\n/** 常量时间比较校验签名 */\nexport function verifyFleetSig(secret: string, env: FleetCmdEnvelope): boolean {\n if (typeof env.sig !== \"string\" || env.sig.length === 0) return false;\n const expected = signFleetCmd(secret, env);\n const a = Buffer.from(expected, \"utf8\");\n const b = Buffer.from(env.sig, \"utf8\");\n if (a.length !== b.length) return false;\n return crypto.timingSafeEqual(a, b);\n}\n\n/** 解析并校验命令信封结构(不校验签名;签名由调用方验) */\nexport function parseFleetEnvelope(raw: unknown): FleetCmdEnvelope | null {\n if (!raw || typeof raw !== \"object\") return null;\n const o = raw as Record<string, unknown>;\n if (\n typeof o.t !== \"number\" ||\n typeof o.nonce !== \"string\" ||\n typeof o.reqId !== \"string\" ||\n typeof o.type !== \"string\" ||\n typeof o.clientId !== \"string\" ||\n typeof o.nodeId !== \"string\" ||\n typeof o.sig !== \"string\"\n ) {\n return null;\n }\n if (!FLEET_CMD_TYPES.includes(o.type as FleetCmdType)) return null;\n if (!o.payload || typeof o.payload !== \"object\") return null;\n return {\n t: o.t,\n nonce: o.nonce,\n reqId: o.reqId,\n type: o.type as FleetCmdType,\n clientId: o.clientId,\n nodeId: o.nodeId,\n projectId: typeof o.projectId === \"string\" ? o.projectId : undefined,\n sessionId: typeof o.sessionId === \"string\" ? o.sessionId : undefined,\n payload: o.payload as Record<string, unknown>,\n sig: o.sig,\n };\n}\n","/**\n * 共享通用工具(节点管理协议与 MQTT 相关)\n *\n * 从 v1 协议层抽出,供节点管理协议(node 与 hub 共用)使用,避免 shared 依赖 core。\n */\nimport * as crypto from \"crypto\";\n\n/** 时间戳允许的最大时钟偏差(毫秒) */\nexport const DEFAULT_CLOCK_SKEW_MS = 60_000;\n\n/** 生成高熵随机 token(base64url,默认 32 字节) */\nexport function generateToken(bytes = 32): string {\n return crypto.randomBytes(bytes).toString(\"base64url\");\n}\n\n/** 时间戳是否在允许窗口内(防重放) */\nexport function isFreshTimestamp(\n t: number,\n now = Date.now(),\n skewMs = DEFAULT_CLOCK_SKEW_MS,\n): boolean {\n return typeof t === \"number\" && Number.isFinite(t) && Math.abs(now - t) <= skewMs;\n}\n\n/**\n * 一次性 nonce 去重器:记录已见 nonce(带过期清理),拒绝重放。\n * 每个 MQTT 连接(租户/节点)持有一个实例。\n */\nexport class NonceTracker {\n private readonly seen = new Map<string, number>();\n\n constructor(private readonly ttlMs = DEFAULT_CLOCK_SKEW_MS * 5) {}\n\n /** 返回 true 表示首次见到(可用);false 表示重放 */\n checkAndRecord(nonce: string, now = Date.now()): boolean {\n this.prune(now);\n if (this.seen.has(nonce)) return false;\n this.seen.set(nonce, now);\n return true;\n }\n\n private prune(now: number): void {\n for (const [k, ts] of this.seen) {\n if (now - ts > this.ttlMs) this.seen.delete(k);\n }\n }\n}\n","/**\n * 对称加密共享模块\n *\n * 从 CLI 的 connection.ts 抽出的 AES-256-GCM 加解密实现,供 CLI 与\n * 云端 Server 复用。密文格式 `v1:iv:tag:ciphertext`(均 base64)。\n *\n * 安全说明:这是「本机混淆式加密」(纯 Node 无外部依赖),主密钥与密文\n * 可存于同一台机器,能防止明文泄露 / 被 grep / 误提交,但无法抵御能读取\n * 本机主密钥文件的攻击者(如需 OS 级密钥链,可替换为 keytar / libsecret 等)。\n */\nimport * as crypto from \"crypto\";\nimport * as fs from \"fs\";\nimport * as path from \"path\";\n\n/** 用给定主密钥做 AES-256-GCM 加密,输出 `v1:iv:tag:ciphertext`(base64) */\nexport function encryptWithKey(key: Buffer, plain: string): string {\n const iv = crypto.randomBytes(12);\n const cipher = crypto.createCipheriv(\"aes-256-gcm\", key, iv);\n const enc = Buffer.concat([cipher.update(plain, \"utf8\"), cipher.final()]);\n const tag = cipher.getAuthTag();\n return [\n \"v1\",\n iv.toString(\"base64\"),\n tag.toString(\"base64\"),\n enc.toString(\"base64\"),\n ].join(\":\");\n}\n\n/** 用给定主密钥解密;格式非法 / 认证失败返回 null */\nexport function decryptWithKey(key: Buffer, payload: string): string | null {\n try {\n const parts = payload.split(\":\");\n if (parts.length !== 4 || parts[0] !== \"v1\") return null;\n const iv = Buffer.from(parts[1]!, \"base64\");\n const tag = Buffer.from(parts[2]!, \"base64\");\n const data = Buffer.from(parts[3]!, \"base64\");\n const decipher = crypto.createDecipheriv(\"aes-256-gcm\", key, iv);\n decipher.setAuthTag(tag);\n return Buffer.concat([\n decipher.update(data),\n decipher.final(),\n ]).toString(\"utf8\");\n } catch {\n return null;\n }\n}\n\n/** 读取或生成主密钥(32 字节随机,权限 600)。文件损坏则重建。 */\nexport function loadOrCreateMasterKey(masterKeyFile: string): Buffer {\n if (fs.existsSync(masterKeyFile)) {\n const hex = fs.readFileSync(masterKeyFile, \"utf8\").trim();\n if (/^[0-9a-f]{64}$/i.test(hex)) {\n return Buffer.from(hex, \"hex\");\n }\n console.warn(`⚠️ 主密钥文件损坏,已重新生成(旧密文将无法解密)`);\n }\n const key = crypto.randomBytes(32);\n fs.mkdirSync(path.dirname(masterKeyFile), { recursive: true, mode: 0o700 });\n fs.writeFileSync(masterKeyFile, key.toString(\"hex\"), {\n mode: 0o600,\n encoding: \"utf8\",\n });\n return key;\n}\n\n/** 从 64 位 hex 字符串解析主密钥;非法返回 null */\nexport function masterKeyFromHex(hex: string): Buffer | null {\n const h = hex.trim();\n return /^[0-9a-f]{64}$/i.test(h) ? Buffer.from(h, \"hex\") : null;\n}\n\n/**\n * 密文工具:持有固定主密钥,对字符串做加解密。\n * CLI 与 Server 各自构造实例(CLI 用 ~/.config/zqagent/secret.key,\n * Server 用 DATA_DIR/secret.key 或 SECRET_KEY 环境变量)。\n */\nexport class SecretBox {\n constructor(private readonly key: Buffer) {}\n\n /** 主密钥来源一:本机密钥文件(不存在则创建) */\n static fromFile(masterKeyFile: string): SecretBox {\n return new SecretBox(loadOrCreateMasterKey(masterKeyFile));\n }\n\n /** 主密钥来源二:hex 字符串(如环境变量 SECRET_KEY);非法返回 null */\n static fromHex(hex: string): SecretBox | null {\n const key = masterKeyFromHex(hex);\n return key ? new SecretBox(key) : null;\n }\n\n encrypt(plain: string): string {\n return encryptWithKey(this.key, plain);\n }\n\n decrypt(payload: string): string | null {\n return decryptWithKey(this.key, payload);\n }\n}\n","/**\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 * LLM 供应商预设(CLI `/connect` 与 Web/桌面「节点配置」共用的唯一来源)\n *\n * 纯数据模块:不依赖 Node API,前端(console-ui)与后端(core)都可直接引用,\n * 保证两端「添加供应商」时看到完全一致的预置列表。\n *\n * `id` 会持久化为配置项的 `type` 字段(历史字段名,仅展示用途)。\n */\n\nexport interface LlmPreset {\n /** 稳定标识(持久化为 provider.type) */\n id: string;\n /** 展示名 */\n name: string;\n /** OpenAI 兼容端点;空串表示需用户自行填写 */\n baseURL: string;\n /** 建议默认模型;空串表示需要刷新模型列表后由用户选择 */\n model: string;\n /** 是否支持推理强度(reasoning effort)设置 */\n supportsReasoningEffort: boolean;\n /** 选择列表中的补充说明 */\n hint?: string;\n}\n\n/** 预置供应商(顺序即界面展示顺序) */\nexport const LLM_PRESETS: readonly LlmPreset[] = [\n {\n id: \"openai\",\n name: \"OpenAI\",\n baseURL: \"https://api.openai.com/v1\",\n // 不预填模型:预填等于替用户拍板一个他既没选过、也可能没有权限的型号,\n // 之后控制台的模型下拉就凭空多出这个「系统默认模型」。填完 Key 刷新模型,\n // 由供应商实际返回的列表来选。\n model: \"\",\n supportsReasoningEffort: true,\n },\n {\n id: \"deepseek\",\n name: \"DeepSeek\",\n baseURL: \"https://api.deepseek.com\",\n model: \"deepseek-flash\",\n supportsReasoningEffort: false,\n hint: \"deepseek-flash = 通用对话, deepseek-reasoner = 深度推理\",\n },\n {\n id: \"qwen\",\n name: \"通义千问\",\n baseURL: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n model: \"qwen-plus\",\n supportsReasoningEffort: false,\n },\n {\n id: \"zhipu\",\n name: \"智谱 GLM\",\n baseURL: \"https://open.bigmodel.cn/api/paas/v4\",\n model: \"glm-4-plus\",\n supportsReasoningEffort: false,\n },\n {\n id: \"moonshot\",\n name: \"月之暗面 Kimi\",\n baseURL: \"https://api.moonshot.cn/v1\",\n model: \"moonshot-v1-8k\",\n supportsReasoningEffort: false,\n },\n {\n id: \"xiaomi\",\n name: \"小米 MiMo\",\n baseURL: \"\",\n model: \"\",\n supportsReasoningEffort: false,\n hint: \"需自行填写端点与模型\",\n },\n {\n id: \"custom\",\n name: \"其它 (OpenAI 兼容)\",\n baseURL: \"\",\n model: \"\",\n supportsReasoningEffort: true,\n hint: \"任意兼容 OpenAI API 的服务端点(如本地 vLLM 等)\",\n },\n];\n\n/** 按 id 取预设;未命中返回 undefined */\nexport function llmPreset(id: string): LlmPreset | undefined {\n return LLM_PRESETS.find((p) => p.id === id);\n}\n","/**\n * 音视频判定与 HTTP Range 解析(节点 / 控制台 / 网关共用同一套规则)\n *\n * 只收录浏览器原生 `<audio>` / `<video>` 能直接播放的容器。mkv / avi / wmv / flv\n * 等需要转码的格式返回 null,由上层提示「下载后查看」而不是给一个永远播不了的\n * 播放器(错误提示会更难解释)。\n *\n * Range 解析按 RFC 7233:只接受单段(多段请求直接忽略,回整个文件),\n * 语法不合法回 null(调用方忽略 Range),语法合法但区间落在文件外回\n * \"unsatisfiable\"(调用方回 416)。\n */\n\nexport type MediaKind = \"audio\" | \"video\";\n\nexport interface MediaInfo {\n /** 响应用 Content-Type */\n mime: string;\n kind: MediaKind;\n}\n\n/**\n * 扩展名 → MIME / 类别。\n *\n * `mov` 只有 Safari 稳定支持;`webm` 在旧 Safari 上不支持。是否真能播由浏览器\n * 决定,前端监听 `<video onError>` 兜底,不在这里做设备探测。\n */\nconst MEDIA_MIME: Record<string, MediaInfo> = {\n // 视频\n mp4: { mime: \"video/mp4\", kind: \"video\" },\n m4v: { mime: \"video/mp4\", kind: \"video\" },\n webm: { mime: \"video/webm\", kind: \"video\" },\n ogv: { mime: \"video/ogg\", kind: \"video\" },\n mov: { mime: \"video/quicktime\", kind: \"video\" },\n // 音频\n mp3: { mime: \"audio/mpeg\", kind: \"audio\" },\n wav: { mime: \"audio/wav\", kind: \"audio\" },\n m4a: { mime: \"audio/mp4\", kind: \"audio\" },\n aac: { mime: \"audio/aac\", kind: \"audio\" },\n flac: { mime: \"audio/flac\", kind: \"audio\" },\n ogg: { mime: \"audio/ogg\", kind: \"audio\" },\n oga: { mime: \"audio/ogg\", kind: \"audio\" },\n opus: { mime: \"audio/ogg\", kind: \"audio\" },\n weba: { mime: \"audio/webm\", kind: \"audio\" },\n};\n\nfunction extOf(fileName: string): string {\n return fileName.toLowerCase().split(\".\").pop() ?? \"\";\n}\n\n/** 文件名 → 可播放媒体的 MIME 与类别;不可(或浏览器不支持)返回 null */\nexport function mediaInfo(fileName: string): MediaInfo | null {\n return MEDIA_MIME[extOf(fileName)] ?? null;\n}\n\n/** 文件名 → MIME;非可播放媒体返回 null */\nexport function mediaMime(fileName: string): string | null {\n return MEDIA_MIME[extOf(fileName)]?.mime ?? null;\n}\n\n/** 文件名 → audio / video;非可播放媒体返回 null */\nexport function mediaKind(fileName: string): MediaKind | null {\n return MEDIA_MIME[extOf(fileName)]?.kind ?? null;\n}\n\n/**\n * 浏览器可**内嵌**渲染的文档 MIME。\n *\n * 与 `MEDIA_MIME` 分开维护:那张表的语义是「`<video>` / `<audio>` 能直接播放」,\n * PDF 不是音视频,混进去会让 `mediaKind()` 返回一个不存在的 kind。这里只声明\n * 「浏览器自带查看器能显示」,由前端决定用 `<iframe>` 还是保底提示。\n *\n * 注意浏览器支持面不是重点:Chrome / Edge / Firefox / Safari / WebView2 都有\n * 内置 PDF 查看器,WebKitGTK(Linux 桌面壳)没有——前端必须能优雅退化。\n */\nconst DOC_PREVIEW_MIME: Record<string, string> = {\n pdf: \"application/pdf\",\n};\n\n/** 文件名 → 可内嵌预览的文档 MIME;非文档返回 null */\nexport function docPreviewMime(fileName: string): string | null {\n return DOC_PREVIEW_MIME[extOf(fileName)] ?? null;\n}\n\n/**\n * 文件名 → 可用于 **HTTP Range 流式预览**的 MIME(音视频 + 可内嵌文档)。\n *\n * 节点签发文件票据 / `/files/range` 用它决定 `Content-Type`:票据里没有 MIME 时\n * 前端会直接放弃流式通道,退回整文件下载(大 PDF 尤其痛),所以 PDF 必须在这里\n * 被认出来。\n */\nexport function previewMime(fileName: string): string | null {\n return mediaMime(fileName) ?? docPreviewMime(fileName);\n}\n\nexport interface ByteRange {\n /** 起始偏移(含) */\n start: number;\n /** 结束偏移(含) */\n end: number;\n}\n\n/**\n * 解析 `Range: bytes=...`。\n *\n * @param header 原始头值(可为 null / 空)\n * @param size 文件总字节数\n * @returns 单段区间;`null` = 忽略该头(无 Range / 多段 / 语法不合法);\n * `\"unsatisfiable\"` = 范围越界,调用方应回 416。\n */\nexport function parseByteRange(\n header: string | null | undefined,\n size: number,\n): ByteRange | \"unsatisfiable\" | null {\n const raw = (header ?? \"\").trim();\n if (!raw) return null;\n const m = /^bytes=(\\d*)-(\\d*)$/i.exec(raw);\n if (!m) return null; // 多段与其它单位一律忽略\n\n const startRaw = m[1] ?? \"\";\n const endRaw = m[2] ?? \"\";\n if (!startRaw && !endRaw) return null; // \"bytes=-\"\n if (size <= 0) return \"unsatisfiable\";\n\n // 后缀形式:bytes=-N → 最后 N 字节\n if (!startRaw) {\n const suffix = Number(endRaw);\n if (!Number.isFinite(suffix) || suffix <= 0) return \"unsatisfiable\";\n return { start: Math.max(size - suffix, 0), end: size - 1 };\n }\n\n const start = Number(startRaw);\n if (!Number.isFinite(start) || start < 0) return null;\n if (start >= size) return \"unsatisfiable\";\n\n // 省略结束:bytes=N- → 到文件末尾\n if (!endRaw) return { start, end: size - 1 };\n\n const end = Number(endRaw);\n if (!Number.isFinite(end) || end < start) return null;\n return { start, end: Math.min(end, size - 1) };\n}\n\n/** Range 响应的 `Content-Range` 头值 */\nexport function contentRangeHeader(range: ByteRange, size: number): string {\n return `bytes ${range.start}-${range.end}/${size}`;\n}\n\n/**\n * 文件 ETag(强校验)。\n *\n * 由 size + mtime 组成:文件内容变化几乎必然改变其中一个;不读文件本身,\n * 因此 `HEAD` / `304` 可以在完全不触达节点的情况下应答。\n */\nexport function fileEtag(size: number, mtimeMs: number): string {\n return `\"${size.toString(16)}-${Math.trunc(mtimeMs).toString(16)}\"`;\n}\n","/**\n * 共享类型:通知事件与渠道配置\n *\n * 从 server/types.ts 抽出,供 core(节点)与 hub(控制面)共用的通知模块引用。\n */\n\n/** 通知种类 */\nexport type NotifyKind =\n | \"approval_request\" // 需要审批(data: approvalId / toolName / arguments / reason)\n | \"question_asked\" // 需要回答提问(data: questionId / kind / question / options / ...)\n | \"turn_completed\" // 一轮 Turn 正常完成(data: status / iterations / produced_files)\n | \"turn_stopped\" // 达到最大迭代次数等提前终止\n | \"error\" // 出错(data: error / retriable)\n /**\n * 会话状态变化(data: status)。\n *\n * 只承载状态机取值,不含消息/模型/token 等明细——跨节点总览只靠它同步,\n * 明细仍去会话详情页拉。与上面几种「需要人处理」的通知不同,它**不面向\n * 第三方渠道**(见 `channelWants`),也不弹提醒。\n */\n | \"session_status\"\n /**\n * 会话状态全量快照(data: `{ state }`,字段与 `GET /sessions/:id/state` 一致)。\n *\n * 控制台收到后直接替换本地快照,因此**不再周期性拉 `/state`**。\n * 只在状态机、待办、排队信息、草稿、名称真的变化时才发(工具执行期间不发)。\n * 与 `session_status` 一样只服务控制台,不面向第三方渠道。\n */\n | \"session_snapshot\"\n /**\n * 后台 shell 任务变化(data: `{ projectId, tasks }`,tasks 为精简字段)。\n *\n * 代替对 `GET /shell/tasks` 的轮询:任务启动 / 退出 / 被终止时推送一次。\n */\n | \"shell_tasks_changed\";\n\n/**\n * 通知事件:SessionManager 在审批挂起 / 提问挂起 / 完成 / 出错等关键点产出,\n * 交给通知中枢(NotificationHub / 节点桥)广播到多设备或第三方渠道。\n */\nexport interface NotifyEvent {\n kind: NotifyKind;\n sessionId: string;\n ts: string;\n data: Record<string, unknown>;\n}\n\n/** 通知渠道类型 */\nexport type NotificationChannelType =\n | \"mqtt\"\n | \"telegram\"\n | \"wecom\"\n | \"dingtalk\"\n | \"feishu\"\n | \"serverchan\"\n | \"pushplus\"\n | \"bark\"\n | \"email\"\n | \"sms\"\n | \"qq\"\n | \"webpush\"\n | \"webhook\";\n\n/** 通知渠道配置(敏感字段如 token / password 以密文存储) */\nexport interface NotificationChannelConfig {\n id: string;\n type: NotificationChannelType;\n enabled: boolean;\n config: Record<string, string>;\n events: {\n approval: boolean;\n question: boolean;\n completed: boolean;\n error: boolean;\n };\n}\n\n/**\n * 内容增量事件:逐 token 的高频帧。\n *\n * 控制台「折叠过程输出」时,这两类事件既不会被渲染,又占聊天流量的绝大部分,\n * 因此服务端按订阅参数 `detail=0` 直接在源头丢弃(SSE / 网关两条链路一致)。\n *\n * **改这里必须同步改 `gateway/src/codec.rs` 的 `CONTENT_DELTA_TYPES`**,\n * 否则两条链路会出现「一侧省了、另一侧还在发」。\n */\nexport const CONTENT_DELTA_TYPES: ReadonlySet<string> = new Set([\n \"assistant_delta\",\n \"reasoning_delta\",\n // 节点把「只含内容增量」的一批事件合并成 `{zqBatch:[...]}` 时用的顶层标记。\n // 混入结构事件的一批不能打这个标记,否则整帧被丢会连正文一起丢。\n \"content_delta\",\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":["crypto","crypto","TIMEOUT_MS","TIMEOUT_MS","nodemailer","escapeHtml"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/fleet.ts","../src/common.ts","../src/secrets.ts","../src/approval-view.ts","../src/llm-presets.ts","../src/media.ts","../src/types.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 * @zqagent/shared — core(节点/CLI)与 hub(控制面)共用的基础模块\n *\n * 子路径导出:`./fleet`、`./secrets`、`./approval-view`、`./notify`、`./types`。\n */\nexport * from \"./fleet.js\";\nexport * from \"./secrets.js\";\nexport * from \"./approval-view.js\";\nexport * from \"./llm-presets.js\";\nexport * from \"./media.js\";\nexport * from \"./types.js\";\nexport * from \"./notify/index.js\";\n","/**\n * MQTT 节点管理协议 v2(控制台 ↔ 多终端设备)\n *\n * 与 v1(`protocol.ts`,按租户、deviceId 的手机控制)并存。v2 面向\n * 「公网控制台管理多台终端 Agent 实例」:\n *\n * - **按节点寻址**:topic 含 `node/{nodeId}`,每个节点只订阅自己的 cmd;\n * - **在线状态**:`status` retained + LWT(遗嘱)自动上线/离线,`info` retained\n * (能力/项目快照,上线即重建节点管理视图);\n * - **请求-响应关联**:命令带 `reqId`,回执(ack)回带同一 `reqId`;\n * - **签名密钥**:节点管理级 `fleetSecret`(自托管单所有者模型;每节点独立密钥留待 P3)。\n *\n * 安全前提同 v1:broker 可无认证/ACL,隔离靠「高熵 fleetToken 命名空间 + HMAC 签名」。\n */\nimport * as crypto from \"crypto\";\nimport {\n DEFAULT_CLOCK_SKEW_MS,\n NonceTracker,\n generateToken,\n isFreshTimestamp,\n} from \"./common.js\";\n\nexport { NonceTracker, generateToken, isFreshTimestamp, DEFAULT_CLOCK_SKEW_MS };\n\n/** topic 根前缀 */\nexport const FLEET_TOPIC_PREFIX = \"zqagent/fleet\";\n\n/**\n * 会话状态条目(跨节点总览的唯一数据形状)。\n *\n * 刻意只包含「标识 + 状态」:消息、模型、token 等明细由会话详情页按需拉取。\n * 节点经 info `detail.sessions` 上报全量快照,状态变化经 notify `session_status`\n * 增量推送;接口(REST / 指令)也用同一形状。\n */\n/**\n * 会话状态条目(跨节点总览的唯一数据形状)。\n *\n * - `status` 取 `SessionStatus` 的状态值;另有一个**伪状态** `\"deleted\"`:\n * 会话已删除,只出现在 `session_status` 通知里(不落盘、不进状态机),\n * 供常开的总览页面立即移除该行。\n * - 只含标识与状态:消息、模型、token 等明细由会话详情页按需拉取。\n */\nexport interface SessionStateEntry {\n projectId: string;\n /** 项目展示名(节点侧填充;详情快照里由 projects 补齐) */\n projectName: string;\n sessionId: string;\n name: string | null;\n status: string;\n /** 用户最近一条发送给 Agent 的消息(截断,供总览列表展示;无消息时 null) */\n lastUserMessage?: string | null;\n /**\n * 最近一次**任务请求的开始时刻**(ISO 8601;从未发起过任务时 null)。\n *\n * 工作台的节点/项目/任务列表统一按它倒序——用户找的是「最近在跑什么」,\n * 而不是「最近什么时候跑完的」:长任务运行中时 lastCompletedAt 还是上一轮的,\n * 按它排会把正在干活的排到后面。\n */\n lastStartedAt?: string | null;\n /** 最近一次任务成功完成的时刻(ISO 8601;从未完成时 null) */\n lastCompletedAt?: string | null;\n}\n\n/** 命令类型(控制台 → 节点) */\nexport type FleetCmdType =\n | \"ping\"\n | \"get_node_info\"\n | \"get_node_host\"\n | \"list_projects\"\n | \"create_project\"\n | \"delete_project\"\n | \"list_sessions\"\n /** 跨项目会话状态(只含标识与 status,供节点首页/工作台总览) */\n | \"get_session_states\"\n | \"create_session\"\n | \"delete_session\"\n | \"get_state\"\n | \"get_events\"\n /** 会话轮次汇总(轻量:标题 + 统计,点开某轮再取事件) */\n | \"get_session_turns\"\n /** 单轮详情:用户消息(含补充)+ 最终回复 + 产出文件 */\n | \"get_session_turn_reply\"\n /** 读取聊天记录里保存的附件(用户消息的图片 / 文件) */\n | \"read_session_attachment\"\n /** 变更追踪:本会话的快照列表(每轮一个) */\n | \"get_session_changes\"\n /** 变更追踪:某个快照的完整 patch */\n | \"get_session_change_diff\"\n /** 变更追踪:从快照恢复文件(会改工作区) */\n | \"restore_session_change\"\n | \"get_config\"\n | \"update_config\"\n | \"list_models\"\n | \"list_dirs\"\n | \"make_dir\"\n | \"search_dirs\"\n | \"list_files\"\n | \"read_file\"\n /** 按区间读取文件(音视频预览的数据面;回执为 base64 块,单块 ≤1MB) */\n | \"read_file_range\"\n /**\n * 签发媒体播放票据。\n *\n * `<video>` 发不了 Authorization 头,所以由节点用 fleetSecret 签一张只授权\n * 单个文件、10 分钟有效的票据,网关用同一密钥验签后提供 HTTP Range。\n */\n | \"file_ticket\"\n | \"download_file\"\n | \"write_file\"\n /** 按 base64 写入二进制文件(图片编辑后保存) */\n | \"write_file_base64\"\n | \"create_dir\"\n | \"create_file\"\n | \"delete_files\"\n | \"search_files\"\n /** 在文件**内容**里搜索关键字(返回行号与命中片段) */\n | \"search_content\"\n /** 重命名文件 / 目录(只改同一目录下的名字) */\n | \"rename_path\"\n /** 把文件 / 目录移动到工作区内的目标目录(拖拽移动) */\n | \"move_paths\"\n /** 压缩下载:把选中的文件 / 目录打包成 zip(超过上限直接拒绝) */\n | \"zip_download\"\n | \"list_tools\"\n | \"list_mcp\"\n /** 整体替换项目的 MCP 服务器配置(控制台设置页保存) */\n | \"update_mcp\"\n /** 连接测试单个 MCP 服务器(不落盘) */\n | \"test_mcp\"\n | \"project_stats\"\n | \"run_command\"\n | \"list_shell_tasks\"\n | \"kill_shell_task\"\n // ---------- 交互式终端(控制台「终端」面板) ----------\n // 只承载低频控制动作;用户按键(input)与窗口尺寸(resize)走\n // `FrameType.TERMINAL` 独立帧通道——那条路不等 ACK、不占单节点串行指令队列。\n /** 列出项目下的终端(含已退出未删除的) */\n | \"terminal_list\"\n /** 新建终端(多终端并行:每次一个新进程) */\n | \"terminal_create\"\n /** 取回放缓冲(≤200KB)与元信息:切页签 / 断线重连用 */\n | \"terminal_replay\"\n /** 关闭进程(SIGTERM → 2s → SIGKILL),保留历史与元信息 */\n | \"terminal_close\"\n /** 关闭并删除历史(不可恢复) */\n | \"terminal_remove\"\n | \"terminal_rename\"\n /** 命令/输出/退出历史(受保留期与落盘限速约束) */\n | \"terminal_history\"\n /** 终端设置(当前只有 retentionDays,默认 30 天) */\n | \"terminal_settings\"\n | \"terminal_update_settings\"\n | \"git_repos\"\n | \"git_status\"\n | \"git_diff\"\n | \"git_branches\"\n | \"git_refs\"\n | \"git_log\"\n | \"git_show\"\n | \"git_fetch\"\n | \"git_stage\"\n | \"git_unstage\"\n | \"git_commit\"\n /** AI 生成提交说明(节点侧调用 LLM) */\n | \"git_commit_message\"\n | \"git_checkout\"\n | \"git_pull\"\n | \"git_push\"\n | \"set_node_label\"\n | \"get_usage_summary\"\n /** 大模型账户余额(控制台「余额不足」时查看/充值用) */\n | \"get_llm_balance\"\n | \"get_usage_records\"\n | \"get_usage_pricing\"\n | \"update_usage_pricing\"\n | \"get_node_llm\"\n | \"update_node_llm\"\n /**\n * 节点自更新(自更新仅在「SEA 单文件 + 服务托管 + 配置验签公钥」时可用):\n * payload.action = \"status\" | \"check\" | \"install\"\n * payload.force = true 时立即安装(不等空闲,会打断运行中的任务)\n *\n * install 先回执、后在后台下载安装并退出进程,由 systemd/launchd 拉起新版本;\n * 因此**回执成功不等于已升级**,需再用 status 确认版本。\n */\n | \"self_update\"\n /**\n * 重新拉取平台内置模型(志前 API)的凭据。\n *\n * 内置模型在接入 Hub 时下发,正常情况下用户无需关心;这里用于\n * 「Key 在控制台被重置」或用户在控制台点「刷新」时重取。\n * 会改写节点侧的 LLM 配置,属写指令(不参与只读并发池)。\n */\n | \"refresh_builtin_llm\"\n | \"list_node_llm_models\"\n | \"set_fleet_cred\"\n | \"rename_session\"\n | \"set_session_model\"\n | \"set_session_llm\"\n | \"set_session_mode\"\n | \"set_session_lang\"\n | \"set_session_draft\"\n | \"submit_turn\"\n | \"steer\"\n | \"unsteer\"\n | \"interrupt\"\n | \"resume\"\n | \"approval_answer\"\n | \"question_answer\"\n /**\n * 批量指令:payload.commands 为子指令数组,节点顺序/并发执行后\n * 在单条回执的 data.results 中按序返回各子指令结果(减少往返与请求数)。\n */\n | \"batch\";\n\nexport const FLEET_CMD_TYPES: readonly FleetCmdType[] = [\n \"ping\",\n \"get_node_info\",\n \"get_node_host\",\n \"list_projects\",\n \"create_project\",\n \"delete_project\",\n \"list_sessions\",\n \"get_session_states\",\n \"create_session\",\n \"delete_session\",\n \"get_state\",\n \"get_events\",\n \"get_session_turns\",\n \"get_session_turn_reply\",\n \"read_session_attachment\",\n \"get_session_changes\",\n \"get_session_change_diff\",\n \"restore_session_change\",\n \"get_config\",\n \"update_config\",\n \"list_models\",\n \"list_dirs\",\n \"make_dir\",\n \"search_dirs\",\n \"list_files\",\n \"read_file\",\n \"read_file_range\",\n \"file_ticket\",\n \"download_file\",\n \"write_file\",\n \"write_file_base64\",\n \"create_dir\",\n \"create_file\",\n \"delete_files\",\n \"search_files\",\n \"search_content\",\n \"rename_path\",\n \"move_paths\",\n \"zip_download\",\n \"list_tools\",\n \"list_mcp\",\n \"update_mcp\",\n \"test_mcp\",\n \"project_stats\",\n \"run_command\",\n \"list_shell_tasks\",\n \"kill_shell_task\",\n \"terminal_list\",\n \"terminal_create\",\n \"terminal_replay\",\n \"terminal_close\",\n \"terminal_remove\",\n \"terminal_rename\",\n \"terminal_history\",\n \"terminal_settings\",\n \"terminal_update_settings\",\n \"git_repos\",\n \"git_status\",\n \"git_diff\",\n \"git_branches\",\n \"git_refs\",\n \"git_log\",\n \"git_show\",\n \"git_fetch\",\n \"git_stage\",\n \"git_unstage\",\n \"git_commit\",\n \"git_commit_message\",\n \"git_checkout\",\n \"git_pull\",\n \"git_push\",\n \"set_node_label\",\n \"get_usage_summary\",\n \"get_llm_balance\",\n \"get_usage_records\",\n \"get_usage_pricing\",\n \"update_usage_pricing\",\n \"get_node_llm\",\n \"update_node_llm\",\n \"self_update\",\n \"refresh_builtin_llm\",\n \"list_node_llm_models\",\n \"set_fleet_cred\",\n \"rename_session\",\n \"set_session_model\",\n \"set_session_llm\",\n \"set_session_mode\",\n \"set_session_lang\",\n \"set_session_draft\",\n \"submit_turn\",\n \"steer\",\n \"unsteer\",\n \"interrupt\",\n \"resume\",\n \"approval_answer\",\n \"question_answer\",\n \"batch\",\n];\n\n/** 批量指令的子指令(不含嵌套 batch) */\nexport interface FleetBatchSubCmd {\n type: Exclude<FleetCmdType, \"batch\">;\n projectId?: string;\n sessionId?: string;\n payload?: Record<string, unknown>;\n}\n\n/** 批量指令结果项(与请求按序对应) */\nexport interface FleetBatchResult {\n type: string;\n ok: boolean;\n data?: unknown;\n error?: string;\n}\n\n/** 命令信封 v2(含 HMAC 签名) */\nexport interface FleetCmdEnvelope {\n /** 时间戳(毫秒,±60s 窗口) */\n t: number;\n /** 一次性随机串(防重放) */\n nonce: string;\n /** 请求 ID(回执关联) */\n reqId: string;\n type: FleetCmdType;\n /** 发起控制台客户端 ID */\n clientId: string;\n /** 目标节点 ID */\n nodeId: string;\n /** 目标项目 ID(项目级命令) */\n projectId?: string;\n /** 目标会话 ID(会话级命令) */\n sessionId?: string;\n payload: Record<string, unknown>;\n /** HMAC-SHA256(fleetSecret, 规范化串) base64url */\n sig: string;\n}\n\n/** 命令回执(点对点回给 clientId) */\nexport interface FleetAckPayload {\n reqId: string;\n type: FleetCmdType;\n ok: boolean;\n error?: string;\n data?: unknown;\n}\n\n/** 节点在线状态(retained + LWT) */\nexport interface FleetStatus {\n nodeId: string;\n online: boolean;\n ts: string;\n /** 节点展示名(本地配置;Hub 改名后回写并随之上报) */\n label?: string;\n platform?: string;\n mode?: string;\n version?: string;\n /**\n * 节点自报的主 IP(网卡地址:首个非回环 IPv4,缺省 IPv6)。\n *\n * 它是内网地址,控制台展示节点公网 IP 时应以网关观察到的连接来源为准\n * (见 gateway `NodeStateInfo.peer_ip`)。\n */\n ip?: string;\n}\n\n/** 节点累计运行统计(info 上报;进程启动后累计) */\nexport interface FleetNodeStats {\n /** 完成的回合数(turn_end) */\n turns: number;\n /** 工具调用完成数(tool_finished) */\n toolCalls: number;\n /** 错误事件数(error) */\n errors: number;\n /** 累计输入 token(assistant_message.usage.input) */\n tokensIn: number;\n /** 累计输出 token */\n tokensOut: number;\n /** 统计起点(节点进程启动/桥接建立时间) */\n since: string;\n}\n\n/** 节点管理配对凭据(pairing API 下发) */\nexport interface FleetPairing {\n fleetToken: string;\n fleetSecret: string;\n nodeId: string;\n /** Rust 网关 wss 地址(控制面配置了才下发;节点据此直连网关) */\n gatewayUrl?: string;\n}\n\n/**\n * 节点本地持久化的云端接入凭据(`{DATA_DIR}/fleet.json`)。\n *\n * 由「凭注册码接入」(`POST /api/hub/enroll`)返回后落盘;节点启动时优先\n * 读取此文件,其次回退到环境变量(`FLEET_TOKEN` / `FLEET_SECRET`)。\n *\n * 「去 EMQX」后不再有 per-fleet broker 账号:网关鉴权用**全局** `GATEWAY_FLEET_SECRET`,\n * 因此凭据只需 fleetToken/fleetSecret(前者仅作命名空间标识)与网关地址。\n */\nexport interface FleetCredential {\n /** Hub 控制面地址(接入时使用,便于日后重新配对/展示) */\n hubUrl?: string;\n fleetToken: string;\n fleetSecret: string;\n nodeId: string;\n /**\n * Rust 网关 wss 地址(控制面在配对/接入时下发)。\n *\n * 它是节点接入云端的**唯一**传输,环境变量 `FLEET_GATEWAY_URL` 仅作覆盖。\n */\n gatewayUrl?: string;\n /** 节点展示名(可选) */\n label?: string;\n /** 接入时间(ISO 字符串) */\n enrolledAt?: string;\n /**\n * 平台内置大模型(志前 API):接入 Hub 时由控制面下发,节点侧落盘为\n * `id=__zqai__` 的不可删除供应商。\n *\n * 这里只是「接入那一刻的快照」;凭据轮换或首次缺失时,节点会调用\n * `GET /api/hub/node/builtin-llm` 重新拉取(见 core 的 builtinLlm.ts)。\n */\n builtinLlm?: BuiltinLlmGrant;\n}\n\n/** 平台内置模型的接入凭据(下发给节点) */\nexport interface BuiltinLlmGrant {\n /** 固定为 `__zqai__` */\n providerId: string;\n /** 展示名 */\n name: string;\n /** OpenAI 兼容基址,如 https://zqai-api.cd-zq.com/v1 */\n baseURL: string;\n /** 用户的 API Key(节点侧加密落盘) */\n apiKey: string;\n /** 默认模型 */\n model: string;\n /** 该账号可用的模型列表 */\n models?: string[];\n}\n\n/** topic 构建 */\nexport function fleetCmdTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/cmd`;\n}\nexport function fleetNotifyTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/notify`;\n}\nexport function fleetEventsTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/events`;\n}\nexport function fleetStatusTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/status`;\n}\nexport function fleetInfoTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/info`;\n}\nexport function fleetAckTopic(\n fleetToken: string,\n nodeId: string,\n clientId: string,\n): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/ack/${clientId}`;\n}\n\n/** 签名前的规范化串(字段顺序固定,两侧一致) */\nfunction canonical(env: Omit<FleetCmdEnvelope, \"sig\">): string {\n return JSON.stringify([\n env.t,\n env.nonce,\n env.reqId,\n env.type,\n env.clientId,\n env.nodeId,\n env.projectId ?? \"\",\n env.sessionId ?? \"\",\n env.payload,\n ]);\n}\n\n/** 计算 HMAC-SHA256 签名 */\nexport function signFleetCmd(\n secret: string,\n env: Omit<FleetCmdEnvelope, \"sig\">,\n): string {\n return crypto\n .createHmac(\"sha256\", Buffer.from(secret, \"utf8\"))\n .update(canonical(env))\n .digest(\"base64url\");\n}\n\n/** 常量时间比较校验签名 */\nexport function verifyFleetSig(secret: string, env: FleetCmdEnvelope): boolean {\n if (typeof env.sig !== \"string\" || env.sig.length === 0) return false;\n const expected = signFleetCmd(secret, env);\n const a = Buffer.from(expected, \"utf8\");\n const b = Buffer.from(env.sig, \"utf8\");\n if (a.length !== b.length) return false;\n return crypto.timingSafeEqual(a, b);\n}\n\n/** 解析并校验命令信封结构(不校验签名;签名由调用方验) */\nexport function parseFleetEnvelope(raw: unknown): FleetCmdEnvelope | null {\n if (!raw || typeof raw !== \"object\") return null;\n const o = raw as Record<string, unknown>;\n if (\n typeof o.t !== \"number\" ||\n typeof o.nonce !== \"string\" ||\n typeof o.reqId !== \"string\" ||\n typeof o.type !== \"string\" ||\n typeof o.clientId !== \"string\" ||\n typeof o.nodeId !== \"string\" ||\n typeof o.sig !== \"string\"\n ) {\n return null;\n }\n if (!FLEET_CMD_TYPES.includes(o.type as FleetCmdType)) return null;\n if (!o.payload || typeof o.payload !== \"object\") return null;\n return {\n t: o.t,\n nonce: o.nonce,\n reqId: o.reqId,\n type: o.type as FleetCmdType,\n clientId: o.clientId,\n nodeId: o.nodeId,\n projectId: typeof o.projectId === \"string\" ? o.projectId : undefined,\n sessionId: typeof o.sessionId === \"string\" ? o.sessionId : undefined,\n payload: o.payload as Record<string, unknown>,\n sig: o.sig,\n };\n}\n","/**\n * 共享通用工具(节点管理协议与 MQTT 相关)\n *\n * 从 v1 协议层抽出,供节点管理协议(node 与 hub 共用)使用,避免 shared 依赖 core。\n */\nimport * as crypto from \"crypto\";\n\n/** 时间戳允许的最大时钟偏差(毫秒) */\nexport const DEFAULT_CLOCK_SKEW_MS = 60_000;\n\n/** 生成高熵随机 token(base64url,默认 32 字节) */\nexport function generateToken(bytes = 32): string {\n return crypto.randomBytes(bytes).toString(\"base64url\");\n}\n\n/** 时间戳是否在允许窗口内(防重放) */\nexport function isFreshTimestamp(\n t: number,\n now = Date.now(),\n skewMs = DEFAULT_CLOCK_SKEW_MS,\n): boolean {\n return typeof t === \"number\" && Number.isFinite(t) && Math.abs(now - t) <= skewMs;\n}\n\n/**\n * 一次性 nonce 去重器:记录已见 nonce(带过期清理),拒绝重放。\n * 每个 MQTT 连接(租户/节点)持有一个实例。\n */\nexport class NonceTracker {\n private readonly seen = new Map<string, number>();\n\n constructor(private readonly ttlMs = DEFAULT_CLOCK_SKEW_MS * 5) {}\n\n /** 返回 true 表示首次见到(可用);false 表示重放 */\n checkAndRecord(nonce: string, now = Date.now()): boolean {\n this.prune(now);\n if (this.seen.has(nonce)) return false;\n this.seen.set(nonce, now);\n return true;\n }\n\n private prune(now: number): void {\n for (const [k, ts] of this.seen) {\n if (now - ts > this.ttlMs) this.seen.delete(k);\n }\n }\n}\n","/**\n * 对称加密共享模块\n *\n * 从 CLI 的 connection.ts 抽出的 AES-256-GCM 加解密实现,供 CLI 与\n * 云端 Server 复用。密文格式 `v1:iv:tag:ciphertext`(均 base64)。\n *\n * 安全说明:这是「本机混淆式加密」(纯 Node 无外部依赖),主密钥与密文\n * 可存于同一台机器,能防止明文泄露 / 被 grep / 误提交,但无法抵御能读取\n * 本机主密钥文件的攻击者(如需 OS 级密钥链,可替换为 keytar / libsecret 等)。\n */\nimport * as crypto from \"crypto\";\nimport * as fs from \"fs\";\nimport * as path from \"path\";\n\n/** 用给定主密钥做 AES-256-GCM 加密,输出 `v1:iv:tag:ciphertext`(base64) */\nexport function encryptWithKey(key: Buffer, plain: string): string {\n const iv = crypto.randomBytes(12);\n const cipher = crypto.createCipheriv(\"aes-256-gcm\", key, iv);\n const enc = Buffer.concat([cipher.update(plain, \"utf8\"), cipher.final()]);\n const tag = cipher.getAuthTag();\n return [\n \"v1\",\n iv.toString(\"base64\"),\n tag.toString(\"base64\"),\n enc.toString(\"base64\"),\n ].join(\":\");\n}\n\n/** 用给定主密钥解密;格式非法 / 认证失败返回 null */\nexport function decryptWithKey(key: Buffer, payload: string): string | null {\n try {\n const parts = payload.split(\":\");\n if (parts.length !== 4 || parts[0] !== \"v1\") return null;\n const iv = Buffer.from(parts[1]!, \"base64\");\n const tag = Buffer.from(parts[2]!, \"base64\");\n const data = Buffer.from(parts[3]!, \"base64\");\n const decipher = crypto.createDecipheriv(\"aes-256-gcm\", key, iv);\n decipher.setAuthTag(tag);\n return Buffer.concat([\n decipher.update(data),\n decipher.final(),\n ]).toString(\"utf8\");\n } catch {\n return null;\n }\n}\n\n/** 读取或生成主密钥(32 字节随机,权限 600)。文件损坏则重建。 */\nexport function loadOrCreateMasterKey(masterKeyFile: string): Buffer {\n if (fs.existsSync(masterKeyFile)) {\n const hex = fs.readFileSync(masterKeyFile, \"utf8\").trim();\n if (/^[0-9a-f]{64}$/i.test(hex)) {\n return Buffer.from(hex, \"hex\");\n }\n console.warn(`⚠️ 主密钥文件损坏,已重新生成(旧密文将无法解密)`);\n }\n const key = crypto.randomBytes(32);\n fs.mkdirSync(path.dirname(masterKeyFile), { recursive: true, mode: 0o700 });\n fs.writeFileSync(masterKeyFile, key.toString(\"hex\"), {\n mode: 0o600,\n encoding: \"utf8\",\n });\n return key;\n}\n\n/** 从 64 位 hex 字符串解析主密钥;非法返回 null */\nexport function masterKeyFromHex(hex: string): Buffer | null {\n const h = hex.trim();\n return /^[0-9a-f]{64}$/i.test(h) ? Buffer.from(h, \"hex\") : null;\n}\n\n/**\n * 密文工具:持有固定主密钥,对字符串做加解密。\n * CLI 与 Server 各自构造实例(CLI 用 ~/.config/zqagent/secret.key,\n * Server 用 DATA_DIR/secret.key 或 SECRET_KEY 环境变量)。\n */\nexport class SecretBox {\n constructor(private readonly key: Buffer) {}\n\n /** 主密钥来源一:本机密钥文件(不存在则创建) */\n static fromFile(masterKeyFile: string): SecretBox {\n return new SecretBox(loadOrCreateMasterKey(masterKeyFile));\n }\n\n /** 主密钥来源二:hex 字符串(如环境变量 SECRET_KEY);非法返回 null */\n static fromHex(hex: string): SecretBox | null {\n const key = masterKeyFromHex(hex);\n return key ? new SecretBox(key) : null;\n }\n\n encrypt(plain: string): string {\n return encryptWithKey(this.key, plain);\n }\n\n decrypt(payload: string): string | null {\n return decryptWithKey(this.key, payload);\n }\n}\n","/**\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 * LLM 供应商预设(CLI `/connect` 与 Web/桌面「节点配置」共用的唯一来源)\n *\n * 纯数据模块:不依赖 Node API,前端(console-ui)与后端(core)都可直接引用,\n * 保证两端「添加供应商」时看到完全一致的预置列表。\n *\n * `id` 会持久化为配置项的 `type` 字段(历史字段名,仅展示用途)。\n */\n\nexport interface LlmPreset {\n /** 稳定标识(持久化为 provider.type) */\n id: string;\n /** 展示名 */\n name: string;\n /** OpenAI 兼容端点;空串表示需用户自行填写 */\n baseURL: string;\n /** 建议默认模型;空串表示需要刷新模型列表后由用户选择 */\n model: string;\n /** 是否支持推理强度(reasoning effort)设置 */\n supportsReasoningEffort: boolean;\n /** 选择列表中的补充说明 */\n hint?: string;\n}\n\n/** 预置供应商(顺序即界面展示顺序) */\nexport const LLM_PRESETS: readonly LlmPreset[] = [\n {\n id: \"openai\",\n name: \"OpenAI\",\n baseURL: \"https://api.openai.com/v1\",\n // 不预填模型:预填等于替用户拍板一个他既没选过、也可能没有权限的型号,\n // 之后控制台的模型下拉就凭空多出这个「系统默认模型」。填完 Key 刷新模型,\n // 由供应商实际返回的列表来选。\n model: \"\",\n supportsReasoningEffort: true,\n },\n {\n id: \"deepseek\",\n name: \"DeepSeek\",\n baseURL: \"https://api.deepseek.com\",\n model: \"deepseek-flash\",\n supportsReasoningEffort: false,\n hint: \"deepseek-flash = 通用对话, deepseek-reasoner = 深度推理\",\n },\n {\n id: \"qwen\",\n name: \"通义千问\",\n baseURL: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n model: \"qwen-plus\",\n supportsReasoningEffort: false,\n },\n {\n id: \"zhipu\",\n name: \"智谱 GLM\",\n baseURL: \"https://open.bigmodel.cn/api/paas/v4\",\n model: \"glm-4-plus\",\n supportsReasoningEffort: false,\n },\n {\n id: \"moonshot\",\n name: \"月之暗面 Kimi\",\n baseURL: \"https://api.moonshot.cn/v1\",\n model: \"moonshot-v1-8k\",\n supportsReasoningEffort: false,\n },\n {\n id: \"xiaomi\",\n name: \"小米 MiMo\",\n baseURL: \"\",\n model: \"\",\n supportsReasoningEffort: false,\n hint: \"需自行填写端点与模型\",\n },\n {\n id: \"custom\",\n name: \"其它 (OpenAI 兼容)\",\n baseURL: \"\",\n model: \"\",\n supportsReasoningEffort: true,\n hint: \"任意兼容 OpenAI API 的服务端点(如本地 vLLM 等)\",\n },\n];\n\n/** 按 id 取预设;未命中返回 undefined */\nexport function llmPreset(id: string): LlmPreset | undefined {\n return LLM_PRESETS.find((p) => p.id === id);\n}\n","/**\n * 音视频判定与 HTTP Range 解析(节点 / 控制台 / 网关共用同一套规则)\n *\n * 只收录浏览器原生 `<audio>` / `<video>` 能直接播放的容器。mkv / avi / wmv / flv\n * 等需要转码的格式返回 null,由上层提示「下载后查看」而不是给一个永远播不了的\n * 播放器(错误提示会更难解释)。\n *\n * Range 解析按 RFC 7233:只接受单段(多段请求直接忽略,回整个文件),\n * 语法不合法回 null(调用方忽略 Range),语法合法但区间落在文件外回\n * \"unsatisfiable\"(调用方回 416)。\n */\n\nexport type MediaKind = \"audio\" | \"video\";\n\nexport interface MediaInfo {\n /** 响应用 Content-Type */\n mime: string;\n kind: MediaKind;\n}\n\n/**\n * 扩展名 → MIME / 类别。\n *\n * `mov` 只有 Safari 稳定支持;`webm` 在旧 Safari 上不支持。是否真能播由浏览器\n * 决定,前端监听 `<video onError>` 兜底,不在这里做设备探测。\n */\nconst MEDIA_MIME: Record<string, MediaInfo> = {\n // 视频\n mp4: { mime: \"video/mp4\", kind: \"video\" },\n m4v: { mime: \"video/mp4\", kind: \"video\" },\n webm: { mime: \"video/webm\", kind: \"video\" },\n ogv: { mime: \"video/ogg\", kind: \"video\" },\n mov: { mime: \"video/quicktime\", kind: \"video\" },\n // 音频\n mp3: { mime: \"audio/mpeg\", kind: \"audio\" },\n wav: { mime: \"audio/wav\", kind: \"audio\" },\n m4a: { mime: \"audio/mp4\", kind: \"audio\" },\n aac: { mime: \"audio/aac\", kind: \"audio\" },\n flac: { mime: \"audio/flac\", kind: \"audio\" },\n ogg: { mime: \"audio/ogg\", kind: \"audio\" },\n oga: { mime: \"audio/ogg\", kind: \"audio\" },\n opus: { mime: \"audio/ogg\", kind: \"audio\" },\n weba: { mime: \"audio/webm\", kind: \"audio\" },\n};\n\nfunction extOf(fileName: string): string {\n return fileName.toLowerCase().split(\".\").pop() ?? \"\";\n}\n\n/** 文件名 → 可播放媒体的 MIME 与类别;不可(或浏览器不支持)返回 null */\nexport function mediaInfo(fileName: string): MediaInfo | null {\n return MEDIA_MIME[extOf(fileName)] ?? null;\n}\n\n/** 文件名 → MIME;非可播放媒体返回 null */\nexport function mediaMime(fileName: string): string | null {\n return MEDIA_MIME[extOf(fileName)]?.mime ?? null;\n}\n\n/** 文件名 → audio / video;非可播放媒体返回 null */\nexport function mediaKind(fileName: string): MediaKind | null {\n return MEDIA_MIME[extOf(fileName)]?.kind ?? null;\n}\n\n/**\n * 浏览器可**内嵌**渲染的文档 MIME。\n *\n * 与 `MEDIA_MIME` 分开维护:那张表的语义是「`<video>` / `<audio>` 能直接播放」,\n * PDF 不是音视频,混进去会让 `mediaKind()` 返回一个不存在的 kind。这里只声明\n * 「浏览器自带查看器能显示」,由前端决定用 `<iframe>` 还是保底提示。\n *\n * 注意浏览器支持面不是重点:Chrome / Edge / Firefox / Safari / WebView2 都有\n * 内置 PDF 查看器,WebKitGTK(Linux 桌面壳)没有——前端必须能优雅退化。\n */\nconst DOC_PREVIEW_MIME: Record<string, string> = {\n pdf: \"application/pdf\",\n};\n\n/** 文件名 → 可内嵌预览的文档 MIME;非文档返回 null */\nexport function docPreviewMime(fileName: string): string | null {\n return DOC_PREVIEW_MIME[extOf(fileName)] ?? null;\n}\n\n/**\n * 文件名 → 可用于 **HTTP Range 流式预览**的 MIME(音视频 + 可内嵌文档)。\n *\n * 节点签发文件票据 / `/files/range` 用它决定 `Content-Type`:票据里没有 MIME 时\n * 前端会直接放弃流式通道,退回整文件下载(大 PDF 尤其痛),所以 PDF 必须在这里\n * 被认出来。\n */\nexport function previewMime(fileName: string): string | null {\n return mediaMime(fileName) ?? docPreviewMime(fileName);\n}\n\nexport interface ByteRange {\n /** 起始偏移(含) */\n start: number;\n /** 结束偏移(含) */\n end: number;\n}\n\n/**\n * 解析 `Range: bytes=...`。\n *\n * @param header 原始头值(可为 null / 空)\n * @param size 文件总字节数\n * @returns 单段区间;`null` = 忽略该头(无 Range / 多段 / 语法不合法);\n * `\"unsatisfiable\"` = 范围越界,调用方应回 416。\n */\nexport function parseByteRange(\n header: string | null | undefined,\n size: number,\n): ByteRange | \"unsatisfiable\" | null {\n const raw = (header ?? \"\").trim();\n if (!raw) return null;\n const m = /^bytes=(\\d*)-(\\d*)$/i.exec(raw);\n if (!m) return null; // 多段与其它单位一律忽略\n\n const startRaw = m[1] ?? \"\";\n const endRaw = m[2] ?? \"\";\n if (!startRaw && !endRaw) return null; // \"bytes=-\"\n if (size <= 0) return \"unsatisfiable\";\n\n // 后缀形式:bytes=-N → 最后 N 字节\n if (!startRaw) {\n const suffix = Number(endRaw);\n if (!Number.isFinite(suffix) || suffix <= 0) return \"unsatisfiable\";\n return { start: Math.max(size - suffix, 0), end: size - 1 };\n }\n\n const start = Number(startRaw);\n if (!Number.isFinite(start) || start < 0) return null;\n if (start >= size) return \"unsatisfiable\";\n\n // 省略结束:bytes=N- → 到文件末尾\n if (!endRaw) return { start, end: size - 1 };\n\n const end = Number(endRaw);\n if (!Number.isFinite(end) || end < start) return null;\n return { start, end: Math.min(end, size - 1) };\n}\n\n/** Range 响应的 `Content-Range` 头值 */\nexport function contentRangeHeader(range: ByteRange, size: number): string {\n return `bytes ${range.start}-${range.end}/${size}`;\n}\n\n/**\n * 文件 ETag(强校验)。\n *\n * 由 size + mtime 组成:文件内容变化几乎必然改变其中一个;不读文件本身,\n * 因此 `HEAD` / `304` 可以在完全不触达节点的情况下应答。\n */\nexport function fileEtag(size: number, mtimeMs: number): string {\n return `\"${size.toString(16)}-${Math.trunc(mtimeMs).toString(16)}\"`;\n}\n","/**\n * 共享类型:通知事件与渠道配置\n *\n * 从 server/types.ts 抽出,供 core(节点)与 hub(控制面)共用的通知模块引用。\n */\n\n/** 通知种类 */\nexport type NotifyKind =\n | \"approval_request\" // 需要审批(data: approvalId / toolName / arguments / reason)\n | \"question_asked\" // 需要回答提问(data: questionId / kind / question / options / ...)\n | \"turn_completed\" // 一轮 Turn 正常完成(data: status / iterations / produced_files)\n | \"turn_stopped\" // 达到最大迭代次数等提前终止\n | \"error\" // 出错(data: error / retriable)\n /**\n * 会话状态变化(data: status)。\n *\n * 只承载状态机取值,不含消息/模型/token 等明细——跨节点总览只靠它同步,\n * 明细仍去会话详情页拉。与上面几种「需要人处理」的通知不同,它**不面向\n * 第三方渠道**(见 `channelWants`),也不弹提醒。\n */\n | \"session_status\"\n /**\n * 会话状态全量快照(data: `{ state }`,字段与 `GET /sessions/:id/state` 一致)。\n *\n * 控制台收到后直接替换本地快照,因此**不再周期性拉 `/state`**。\n * 只在状态机、待办、排队信息、草稿、名称真的变化时才发(工具执行期间不发)。\n * 与 `session_status` 一样只服务控制台,不面向第三方渠道。\n */\n | \"session_snapshot\"\n /**\n * 后台 shell 任务变化(data: `{ projectId, tasks }`,tasks 为精简字段)。\n *\n * 代替对 `GET /shell/tasks` 的轮询:任务启动 / 退出 / 被终止时推送一次。\n */\n | \"shell_tasks_changed\";\n\n/**\n * 通知事件:SessionManager 在审批挂起 / 提问挂起 / 完成 / 出错等关键点产出,\n * 交给通知中枢(NotificationHub / 节点桥)广播到多设备或第三方渠道。\n */\nexport interface NotifyEvent {\n kind: NotifyKind;\n sessionId: string;\n ts: string;\n data: Record<string, unknown>;\n}\n\n/** 通知渠道类型 */\nexport type NotificationChannelType =\n | \"mqtt\"\n | \"telegram\"\n | \"wecom\"\n | \"dingtalk\"\n | \"feishu\"\n | \"serverchan\"\n | \"pushplus\"\n | \"bark\"\n | \"email\"\n | \"sms\"\n | \"qq\"\n | \"webpush\"\n | \"webhook\";\n\n/** 通知渠道配置(敏感字段如 token / password 以密文存储) */\nexport interface NotificationChannelConfig {\n id: string;\n type: NotificationChannelType;\n enabled: boolean;\n config: Record<string, string>;\n events: {\n approval: boolean;\n question: boolean;\n completed: boolean;\n error: boolean;\n };\n}\n\n/**\n * 内容增量事件:逐 token 的高频帧。\n *\n * 控制台「折叠过程输出」时,这两类事件既不会被渲染,又占聊天流量的绝大部分,\n * 因此服务端按订阅参数 `detail=0` 直接在源头丢弃(SSE / 网关两条链路一致)。\n *\n * **改这里必须同步改 `gateway/src/codec.rs` 的 `CONTENT_DELTA_TYPES`**,\n * 否则两条链路会出现「一侧省了、另一侧还在发」。\n */\nexport const CONTENT_DELTA_TYPES: ReadonlySet<string> = new Set([\n \"assistant_delta\",\n \"reasoning_delta\",\n // 节点把「只含内容增量」的一批事件合并成 `{zqBatch:[...]}` 时用的顶层标记。\n // 混入结构事件的一批不能打这个标记,否则整帧被丢会连正文一起丢。\n \"content_delta\",\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): 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":["crypto","crypto","TIMEOUT_MS","TIMEOUT_MS","nodemailer","escapeHtml"]}
package/dist/index.d.cts CHANGED
@@ -3,5 +3,5 @@ export { SecretBox, decryptWithKey, encryptWithKey, loadOrCreateMasterKey, maste
3
3
  export { APPROVAL_COLUMNS, APPROVAL_VALUE_MAX, ApprovalField, ApprovalViewInput, approvalFields, approvalInputFromData, approvalToHtmlTable, approvalToMarkdownTable } from './approval-view.cjs';
4
4
  export { LLM_PRESETS, LlmPreset, llmPreset } from './llm-presets.cjs';
5
5
  export { ByteRange, MediaInfo, MediaKind, contentRangeHeader, docPreviewMime, fileEtag, mediaInfo, mediaKind, mediaMime, parseByteRange, previewMime } from './media.cjs';
6
- export { B as BarkNotifier, C as CONTENT_DELTA_TYPES, E as EmailNotifier, N as NotificationChannelConfig, a as NotificationChannelType, b as NotificationHub, c as Notifier, d as NotifyEvent, e as NotifyKind, S as ServerChanNotifier, W as WebhookNotifier, f as channelWants, g as createNotificationHub, n as notifyBody, h as notifyTitle } from './index-DulV0GQp.cjs';
6
+ export { B as BarkNotifier, C as CONTENT_DELTA_TYPES, E as EmailNotifier, N as NotificationChannelConfig, a as NotificationChannelType, b as NotificationHub, c as Notifier, d as NotifyEvent, e as NotifyKind, S as ServerChanNotifier, W as WebhookNotifier, f as channelWants, g as createNotificationHub, n as notifyBody, h as notifyTitle } from './index-BrWxcNt4.cjs';
7
7
  import 'nodemailer';
package/dist/index.d.ts CHANGED
@@ -3,5 +3,5 @@ export { SecretBox, decryptWithKey, encryptWithKey, loadOrCreateMasterKey, maste
3
3
  export { APPROVAL_COLUMNS, APPROVAL_VALUE_MAX, ApprovalField, ApprovalViewInput, approvalFields, approvalInputFromData, approvalToHtmlTable, approvalToMarkdownTable } from './approval-view.js';
4
4
  export { LLM_PRESETS, LlmPreset, llmPreset } from './llm-presets.js';
5
5
  export { ByteRange, MediaInfo, MediaKind, contentRangeHeader, docPreviewMime, fileEtag, mediaInfo, mediaKind, mediaMime, parseByteRange, previewMime } from './media.js';
6
- export { B as BarkNotifier, C as CONTENT_DELTA_TYPES, E as EmailNotifier, N as NotificationChannelConfig, a as NotificationChannelType, b as NotificationHub, c as Notifier, d as NotifyEvent, e as NotifyKind, S as ServerChanNotifier, W as WebhookNotifier, f as channelWants, g as createNotificationHub, n as notifyBody, h as notifyTitle } from './index-DulV0GQp.js';
6
+ export { B as BarkNotifier, C as CONTENT_DELTA_TYPES, E as EmailNotifier, N as NotificationChannelConfig, a as NotificationChannelType, b as NotificationHub, c as Notifier, d as NotifyEvent, e as NotifyKind, S as ServerChanNotifier, W as WebhookNotifier, f as channelWants, g as createNotificationHub, n as notifyBody, h as notifyTitle } from './index-BrWxcNt4.js';
7
7
  import 'nodemailer';
package/dist/index.js CHANGED
@@ -508,7 +508,7 @@ function notifyTitle(kind) {
508
508
  return "Agent \u901A\u77E5";
509
509
  }
510
510
  }
511
- function notifyBody(ev, maxLen = 300) {
511
+ function notifyBody(ev) {
512
512
  const d = ev.data;
513
513
  switch (ev.kind) {
514
514
  case "approval_request":
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/fleet.ts","../src/common.ts","../src/secrets.ts","../src/approval-view.ts","../src/llm-presets.ts","../src/media.ts","../src/types.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 * MQTT 节点管理协议 v2(控制台 ↔ 多终端设备)\n *\n * 与 v1(`protocol.ts`,按租户、deviceId 的手机控制)并存。v2 面向\n * 「公网控制台管理多台终端 Agent 实例」:\n *\n * - **按节点寻址**:topic 含 `node/{nodeId}`,每个节点只订阅自己的 cmd;\n * - **在线状态**:`status` retained + LWT(遗嘱)自动上线/离线,`info` retained\n * (能力/项目快照,上线即重建节点管理视图);\n * - **请求-响应关联**:命令带 `reqId`,回执(ack)回带同一 `reqId`;\n * - **签名密钥**:节点管理级 `fleetSecret`(自托管单所有者模型;每节点独立密钥留待 P3)。\n *\n * 安全前提同 v1:broker 可无认证/ACL,隔离靠「高熵 fleetToken 命名空间 + HMAC 签名」。\n */\nimport * as crypto from \"crypto\";\nimport {\n DEFAULT_CLOCK_SKEW_MS,\n NonceTracker,\n generateToken,\n isFreshTimestamp,\n} from \"./common.js\";\n\nexport { NonceTracker, generateToken, isFreshTimestamp, DEFAULT_CLOCK_SKEW_MS };\n\n/** topic 根前缀 */\nexport const FLEET_TOPIC_PREFIX = \"zqagent/fleet\";\n\n/**\n * 会话状态条目(跨节点总览的唯一数据形状)。\n *\n * 刻意只包含「标识 + 状态」:消息、模型、token 等明细由会话详情页按需拉取。\n * 节点经 info `detail.sessions` 上报全量快照,状态变化经 notify `session_status`\n * 增量推送;接口(REST / 指令)也用同一形状。\n */\n/**\n * 会话状态条目(跨节点总览的唯一数据形状)。\n *\n * - `status` 取 `SessionStatus` 的状态值;另有一个**伪状态** `\"deleted\"`:\n * 会话已删除,只出现在 `session_status` 通知里(不落盘、不进状态机),\n * 供常开的总览页面立即移除该行。\n * - 只含标识与状态:消息、模型、token 等明细由会话详情页按需拉取。\n */\nexport interface SessionStateEntry {\n projectId: string;\n /** 项目展示名(节点侧填充;详情快照里由 projects 补齐) */\n projectName: string;\n sessionId: string;\n name: string | null;\n status: string;\n /** 用户最近一条发送给 Agent 的消息(截断,供总览列表展示;无消息时 null) */\n lastUserMessage?: string | null;\n /**\n * 最近一次**任务请求的开始时刻**(ISO 8601;从未发起过任务时 null)。\n *\n * 工作台的节点/项目/任务列表统一按它倒序——用户找的是「最近在跑什么」,\n * 而不是「最近什么时候跑完的」:长任务运行中时 lastCompletedAt 还是上一轮的,\n * 按它排会把正在干活的排到后面。\n */\n lastStartedAt?: string | null;\n /** 最近一次任务成功完成的时刻(ISO 8601;从未完成时 null) */\n lastCompletedAt?: string | null;\n}\n\n/** 命令类型(控制台 → 节点) */\nexport type FleetCmdType =\n | \"ping\"\n | \"get_node_info\"\n | \"get_node_host\"\n | \"list_projects\"\n | \"create_project\"\n | \"delete_project\"\n | \"list_sessions\"\n /** 跨项目会话状态(只含标识与 status,供节点首页/工作台总览) */\n | \"get_session_states\"\n | \"create_session\"\n | \"delete_session\"\n | \"get_state\"\n | \"get_events\"\n /** 会话轮次汇总(轻量:标题 + 统计,点开某轮再取事件) */\n | \"get_session_turns\"\n /** 单轮详情:用户消息(含补充)+ 最终回复 + 产出文件 */\n | \"get_session_turn_reply\"\n /** 读取聊天记录里保存的附件(用户消息的图片 / 文件) */\n | \"read_session_attachment\"\n /** 变更追踪:本会话的快照列表(每轮一个) */\n | \"get_session_changes\"\n /** 变更追踪:某个快照的完整 patch */\n | \"get_session_change_diff\"\n /** 变更追踪:从快照恢复文件(会改工作区) */\n | \"restore_session_change\"\n | \"get_config\"\n | \"update_config\"\n | \"list_models\"\n | \"list_dirs\"\n | \"make_dir\"\n | \"search_dirs\"\n | \"list_files\"\n | \"read_file\"\n /** 按区间读取文件(音视频预览的数据面;回执为 base64 块,单块 ≤1MB) */\n | \"read_file_range\"\n /**\n * 签发媒体播放票据。\n *\n * `<video>` 发不了 Authorization 头,所以由节点用 fleetSecret 签一张只授权\n * 单个文件、10 分钟有效的票据,网关用同一密钥验签后提供 HTTP Range。\n */\n | \"file_ticket\"\n | \"download_file\"\n | \"write_file\"\n /** 按 base64 写入二进制文件(图片编辑后保存) */\n | \"write_file_base64\"\n | \"create_dir\"\n | \"create_file\"\n | \"delete_files\"\n | \"search_files\"\n /** 在文件**内容**里搜索关键字(返回行号与命中片段) */\n | \"search_content\"\n /** 重命名文件 / 目录(只改同一目录下的名字) */\n | \"rename_path\"\n /** 把文件 / 目录移动到工作区内的目标目录(拖拽移动) */\n | \"move_paths\"\n /** 压缩下载:把选中的文件 / 目录打包成 zip(超过上限直接拒绝) */\n | \"zip_download\"\n | \"list_tools\"\n | \"list_mcp\"\n /** 整体替换项目的 MCP 服务器配置(控制台设置页保存) */\n | \"update_mcp\"\n /** 连接测试单个 MCP 服务器(不落盘) */\n | \"test_mcp\"\n | \"project_stats\"\n | \"run_command\"\n | \"list_shell_tasks\"\n | \"kill_shell_task\"\n // ---------- 交互式终端(控制台「终端」面板) ----------\n // 只承载低频控制动作;用户按键(input)与窗口尺寸(resize)走\n // `FrameType.TERMINAL` 独立帧通道——那条路不等 ACK、不占单节点串行指令队列。\n /** 列出项目下的终端(含已退出未删除的) */\n | \"terminal_list\"\n /** 新建终端(多终端并行:每次一个新进程) */\n | \"terminal_create\"\n /** 取回放缓冲(≤200KB)与元信息:切页签 / 断线重连用 */\n | \"terminal_replay\"\n /** 关闭进程(SIGTERM → 2s → SIGKILL),保留历史与元信息 */\n | \"terminal_close\"\n /** 关闭并删除历史(不可恢复) */\n | \"terminal_remove\"\n | \"terminal_rename\"\n /** 命令/输出/退出历史(受保留期与落盘限速约束) */\n | \"terminal_history\"\n /** 终端设置(当前只有 retentionDays,默认 30 天) */\n | \"terminal_settings\"\n | \"terminal_update_settings\"\n | \"git_repos\"\n | \"git_status\"\n | \"git_diff\"\n | \"git_branches\"\n | \"git_refs\"\n | \"git_log\"\n | \"git_show\"\n | \"git_fetch\"\n | \"git_stage\"\n | \"git_unstage\"\n | \"git_commit\"\n /** AI 生成提交说明(节点侧调用 LLM) */\n | \"git_commit_message\"\n | \"git_checkout\"\n | \"git_pull\"\n | \"git_push\"\n | \"set_node_label\"\n | \"get_usage_summary\"\n /** 大模型账户余额(控制台「余额不足」时查看/充值用) */\n | \"get_llm_balance\"\n | \"get_usage_records\"\n | \"get_usage_pricing\"\n | \"update_usage_pricing\"\n | \"get_node_llm\"\n | \"update_node_llm\"\n /**\n * 节点自更新(自更新仅在「SEA 单文件 + 服务托管 + 配置验签公钥」时可用):\n * payload.action = \"status\" | \"check\" | \"install\"\n * payload.force = true 时立即安装(不等空闲,会打断运行中的任务)\n *\n * install 先回执、后在后台下载安装并退出进程,由 systemd/launchd 拉起新版本;\n * 因此**回执成功不等于已升级**,需再用 status 确认版本。\n */\n | \"self_update\"\n /**\n * 重新拉取平台内置模型(志前 API)的凭据。\n *\n * 内置模型在接入 Hub 时下发,正常情况下用户无需关心;这里用于\n * 「Key 在控制台被重置」或用户在控制台点「刷新」时重取。\n * 会改写节点侧的 LLM 配置,属写指令(不参与只读并发池)。\n */\n | \"refresh_builtin_llm\"\n | \"list_node_llm_models\"\n | \"set_fleet_cred\"\n | \"rename_session\"\n | \"set_session_model\"\n | \"set_session_llm\"\n | \"set_session_mode\"\n | \"set_session_lang\"\n | \"set_session_draft\"\n | \"submit_turn\"\n | \"steer\"\n | \"unsteer\"\n | \"interrupt\"\n | \"resume\"\n | \"approval_answer\"\n | \"question_answer\"\n /**\n * 批量指令:payload.commands 为子指令数组,节点顺序/并发执行后\n * 在单条回执的 data.results 中按序返回各子指令结果(减少往返与请求数)。\n */\n | \"batch\";\n\nexport const FLEET_CMD_TYPES: readonly FleetCmdType[] = [\n \"ping\",\n \"get_node_info\",\n \"get_node_host\",\n \"list_projects\",\n \"create_project\",\n \"delete_project\",\n \"list_sessions\",\n \"get_session_states\",\n \"create_session\",\n \"delete_session\",\n \"get_state\",\n \"get_events\",\n \"get_session_turns\",\n \"get_session_turn_reply\",\n \"read_session_attachment\",\n \"get_session_changes\",\n \"get_session_change_diff\",\n \"restore_session_change\",\n \"get_config\",\n \"update_config\",\n \"list_models\",\n \"list_dirs\",\n \"make_dir\",\n \"search_dirs\",\n \"list_files\",\n \"read_file\",\n \"read_file_range\",\n \"file_ticket\",\n \"download_file\",\n \"write_file\",\n \"write_file_base64\",\n \"create_dir\",\n \"create_file\",\n \"delete_files\",\n \"search_files\",\n \"search_content\",\n \"rename_path\",\n \"move_paths\",\n \"zip_download\",\n \"list_tools\",\n \"list_mcp\",\n \"update_mcp\",\n \"test_mcp\",\n \"project_stats\",\n \"run_command\",\n \"list_shell_tasks\",\n \"kill_shell_task\",\n \"terminal_list\",\n \"terminal_create\",\n \"terminal_replay\",\n \"terminal_close\",\n \"terminal_remove\",\n \"terminal_rename\",\n \"terminal_history\",\n \"terminal_settings\",\n \"terminal_update_settings\",\n \"git_repos\",\n \"git_status\",\n \"git_diff\",\n \"git_branches\",\n \"git_refs\",\n \"git_log\",\n \"git_show\",\n \"git_fetch\",\n \"git_stage\",\n \"git_unstage\",\n \"git_commit\",\n \"git_commit_message\",\n \"git_checkout\",\n \"git_pull\",\n \"git_push\",\n \"set_node_label\",\n \"get_usage_summary\",\n \"get_llm_balance\",\n \"get_usage_records\",\n \"get_usage_pricing\",\n \"update_usage_pricing\",\n \"get_node_llm\",\n \"update_node_llm\",\n \"self_update\",\n \"refresh_builtin_llm\",\n \"list_node_llm_models\",\n \"set_fleet_cred\",\n \"rename_session\",\n \"set_session_model\",\n \"set_session_llm\",\n \"set_session_mode\",\n \"set_session_lang\",\n \"set_session_draft\",\n \"submit_turn\",\n \"steer\",\n \"unsteer\",\n \"interrupt\",\n \"resume\",\n \"approval_answer\",\n \"question_answer\",\n \"batch\",\n];\n\n/** 批量指令的子指令(不含嵌套 batch) */\nexport interface FleetBatchSubCmd {\n type: Exclude<FleetCmdType, \"batch\">;\n projectId?: string;\n sessionId?: string;\n payload?: Record<string, unknown>;\n}\n\n/** 批量指令结果项(与请求按序对应) */\nexport interface FleetBatchResult {\n type: string;\n ok: boolean;\n data?: unknown;\n error?: string;\n}\n\n/** 命令信封 v2(含 HMAC 签名) */\nexport interface FleetCmdEnvelope {\n /** 时间戳(毫秒,±60s 窗口) */\n t: number;\n /** 一次性随机串(防重放) */\n nonce: string;\n /** 请求 ID(回执关联) */\n reqId: string;\n type: FleetCmdType;\n /** 发起控制台客户端 ID */\n clientId: string;\n /** 目标节点 ID */\n nodeId: string;\n /** 目标项目 ID(项目级命令) */\n projectId?: string;\n /** 目标会话 ID(会话级命令) */\n sessionId?: string;\n payload: Record<string, unknown>;\n /** HMAC-SHA256(fleetSecret, 规范化串) base64url */\n sig: string;\n}\n\n/** 命令回执(点对点回给 clientId) */\nexport interface FleetAckPayload {\n reqId: string;\n type: FleetCmdType;\n ok: boolean;\n error?: string;\n data?: unknown;\n}\n\n/** 节点在线状态(retained + LWT) */\nexport interface FleetStatus {\n nodeId: string;\n online: boolean;\n ts: string;\n /** 节点展示名(本地配置;Hub 改名后回写并随之上报) */\n label?: string;\n platform?: string;\n mode?: string;\n version?: string;\n /**\n * 节点自报的主 IP(网卡地址:首个非回环 IPv4,缺省 IPv6)。\n *\n * 它是内网地址,控制台展示节点公网 IP 时应以网关观察到的连接来源为准\n * (见 gateway `NodeStateInfo.peer_ip`)。\n */\n ip?: string;\n}\n\n/** 节点累计运行统计(info 上报;进程启动后累计) */\nexport interface FleetNodeStats {\n /** 完成的回合数(turn_end) */\n turns: number;\n /** 工具调用完成数(tool_finished) */\n toolCalls: number;\n /** 错误事件数(error) */\n errors: number;\n /** 累计输入 token(assistant_message.usage.input) */\n tokensIn: number;\n /** 累计输出 token */\n tokensOut: number;\n /** 统计起点(节点进程启动/桥接建立时间) */\n since: string;\n}\n\n/** 节点管理配对凭据(pairing API 下发) */\nexport interface FleetPairing {\n fleetToken: string;\n fleetSecret: string;\n nodeId: string;\n /** Rust 网关 wss 地址(控制面配置了才下发;节点据此直连网关) */\n gatewayUrl?: string;\n}\n\n/**\n * 节点本地持久化的云端接入凭据(`{DATA_DIR}/fleet.json`)。\n *\n * 由「凭注册码接入」(`POST /api/hub/enroll`)返回后落盘;节点启动时优先\n * 读取此文件,其次回退到环境变量(`FLEET_TOKEN` / `FLEET_SECRET`)。\n *\n * 「去 EMQX」后不再有 per-fleet broker 账号:网关鉴权用**全局** `GATEWAY_FLEET_SECRET`,\n * 因此凭据只需 fleetToken/fleetSecret(前者仅作命名空间标识)与网关地址。\n */\nexport interface FleetCredential {\n /** Hub 控制面地址(接入时使用,便于日后重新配对/展示) */\n hubUrl?: string;\n fleetToken: string;\n fleetSecret: string;\n nodeId: string;\n /**\n * Rust 网关 wss 地址(控制面在配对/接入时下发)。\n *\n * 它是节点接入云端的**唯一**传输,环境变量 `FLEET_GATEWAY_URL` 仅作覆盖。\n */\n gatewayUrl?: string;\n /** 节点展示名(可选) */\n label?: string;\n /** 接入时间(ISO 字符串) */\n enrolledAt?: string;\n /**\n * 平台内置大模型(志前 API):接入 Hub 时由控制面下发,节点侧落盘为\n * `id=__zqai__` 的不可删除供应商。\n *\n * 这里只是「接入那一刻的快照」;凭据轮换或首次缺失时,节点会调用\n * `GET /api/hub/node/builtin-llm` 重新拉取(见 core 的 builtinLlm.ts)。\n */\n builtinLlm?: BuiltinLlmGrant;\n}\n\n/** 平台内置模型的接入凭据(下发给节点) */\nexport interface BuiltinLlmGrant {\n /** 固定为 `__zqai__` */\n providerId: string;\n /** 展示名 */\n name: string;\n /** OpenAI 兼容基址,如 https://zqai-api.cd-zq.com/v1 */\n baseURL: string;\n /** 用户的 API Key(节点侧加密落盘) */\n apiKey: string;\n /** 默认模型 */\n model: string;\n /** 该账号可用的模型列表 */\n models?: string[];\n}\n\n/** topic 构建 */\nexport function fleetCmdTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/cmd`;\n}\nexport function fleetNotifyTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/notify`;\n}\nexport function fleetEventsTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/events`;\n}\nexport function fleetStatusTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/status`;\n}\nexport function fleetInfoTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/info`;\n}\nexport function fleetAckTopic(\n fleetToken: string,\n nodeId: string,\n clientId: string,\n): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/ack/${clientId}`;\n}\n\n/** 签名前的规范化串(字段顺序固定,两侧一致) */\nfunction canonical(env: Omit<FleetCmdEnvelope, \"sig\">): string {\n return JSON.stringify([\n env.t,\n env.nonce,\n env.reqId,\n env.type,\n env.clientId,\n env.nodeId,\n env.projectId ?? \"\",\n env.sessionId ?? \"\",\n env.payload,\n ]);\n}\n\n/** 计算 HMAC-SHA256 签名 */\nexport function signFleetCmd(\n secret: string,\n env: Omit<FleetCmdEnvelope, \"sig\">,\n): string {\n return crypto\n .createHmac(\"sha256\", Buffer.from(secret, \"utf8\"))\n .update(canonical(env))\n .digest(\"base64url\");\n}\n\n/** 常量时间比较校验签名 */\nexport function verifyFleetSig(secret: string, env: FleetCmdEnvelope): boolean {\n if (typeof env.sig !== \"string\" || env.sig.length === 0) return false;\n const expected = signFleetCmd(secret, env);\n const a = Buffer.from(expected, \"utf8\");\n const b = Buffer.from(env.sig, \"utf8\");\n if (a.length !== b.length) return false;\n return crypto.timingSafeEqual(a, b);\n}\n\n/** 解析并校验命令信封结构(不校验签名;签名由调用方验) */\nexport function parseFleetEnvelope(raw: unknown): FleetCmdEnvelope | null {\n if (!raw || typeof raw !== \"object\") return null;\n const o = raw as Record<string, unknown>;\n if (\n typeof o.t !== \"number\" ||\n typeof o.nonce !== \"string\" ||\n typeof o.reqId !== \"string\" ||\n typeof o.type !== \"string\" ||\n typeof o.clientId !== \"string\" ||\n typeof o.nodeId !== \"string\" ||\n typeof o.sig !== \"string\"\n ) {\n return null;\n }\n if (!FLEET_CMD_TYPES.includes(o.type as FleetCmdType)) return null;\n if (!o.payload || typeof o.payload !== \"object\") return null;\n return {\n t: o.t,\n nonce: o.nonce,\n reqId: o.reqId,\n type: o.type as FleetCmdType,\n clientId: o.clientId,\n nodeId: o.nodeId,\n projectId: typeof o.projectId === \"string\" ? o.projectId : undefined,\n sessionId: typeof o.sessionId === \"string\" ? o.sessionId : undefined,\n payload: o.payload as Record<string, unknown>,\n sig: o.sig,\n };\n}\n","/**\n * 共享通用工具(节点管理协议与 MQTT 相关)\n *\n * 从 v1 协议层抽出,供节点管理协议(node 与 hub 共用)使用,避免 shared 依赖 core。\n */\nimport * as crypto from \"crypto\";\n\n/** 时间戳允许的最大时钟偏差(毫秒) */\nexport const DEFAULT_CLOCK_SKEW_MS = 60_000;\n\n/** 生成高熵随机 token(base64url,默认 32 字节) */\nexport function generateToken(bytes = 32): string {\n return crypto.randomBytes(bytes).toString(\"base64url\");\n}\n\n/** 时间戳是否在允许窗口内(防重放) */\nexport function isFreshTimestamp(\n t: number,\n now = Date.now(),\n skewMs = DEFAULT_CLOCK_SKEW_MS,\n): boolean {\n return typeof t === \"number\" && Number.isFinite(t) && Math.abs(now - t) <= skewMs;\n}\n\n/**\n * 一次性 nonce 去重器:记录已见 nonce(带过期清理),拒绝重放。\n * 每个 MQTT 连接(租户/节点)持有一个实例。\n */\nexport class NonceTracker {\n private readonly seen = new Map<string, number>();\n\n constructor(private readonly ttlMs = DEFAULT_CLOCK_SKEW_MS * 5) {}\n\n /** 返回 true 表示首次见到(可用);false 表示重放 */\n checkAndRecord(nonce: string, now = Date.now()): boolean {\n this.prune(now);\n if (this.seen.has(nonce)) return false;\n this.seen.set(nonce, now);\n return true;\n }\n\n private prune(now: number): void {\n for (const [k, ts] of this.seen) {\n if (now - ts > this.ttlMs) this.seen.delete(k);\n }\n }\n}\n","/**\n * 对称加密共享模块\n *\n * 从 CLI 的 connection.ts 抽出的 AES-256-GCM 加解密实现,供 CLI 与\n * 云端 Server 复用。密文格式 `v1:iv:tag:ciphertext`(均 base64)。\n *\n * 安全说明:这是「本机混淆式加密」(纯 Node 无外部依赖),主密钥与密文\n * 可存于同一台机器,能防止明文泄露 / 被 grep / 误提交,但无法抵御能读取\n * 本机主密钥文件的攻击者(如需 OS 级密钥链,可替换为 keytar / libsecret 等)。\n */\nimport * as crypto from \"crypto\";\nimport * as fs from \"fs\";\nimport * as path from \"path\";\n\n/** 用给定主密钥做 AES-256-GCM 加密,输出 `v1:iv:tag:ciphertext`(base64) */\nexport function encryptWithKey(key: Buffer, plain: string): string {\n const iv = crypto.randomBytes(12);\n const cipher = crypto.createCipheriv(\"aes-256-gcm\", key, iv);\n const enc = Buffer.concat([cipher.update(plain, \"utf8\"), cipher.final()]);\n const tag = cipher.getAuthTag();\n return [\n \"v1\",\n iv.toString(\"base64\"),\n tag.toString(\"base64\"),\n enc.toString(\"base64\"),\n ].join(\":\");\n}\n\n/** 用给定主密钥解密;格式非法 / 认证失败返回 null */\nexport function decryptWithKey(key: Buffer, payload: string): string | null {\n try {\n const parts = payload.split(\":\");\n if (parts.length !== 4 || parts[0] !== \"v1\") return null;\n const iv = Buffer.from(parts[1]!, \"base64\");\n const tag = Buffer.from(parts[2]!, \"base64\");\n const data = Buffer.from(parts[3]!, \"base64\");\n const decipher = crypto.createDecipheriv(\"aes-256-gcm\", key, iv);\n decipher.setAuthTag(tag);\n return Buffer.concat([\n decipher.update(data),\n decipher.final(),\n ]).toString(\"utf8\");\n } catch {\n return null;\n }\n}\n\n/** 读取或生成主密钥(32 字节随机,权限 600)。文件损坏则重建。 */\nexport function loadOrCreateMasterKey(masterKeyFile: string): Buffer {\n if (fs.existsSync(masterKeyFile)) {\n const hex = fs.readFileSync(masterKeyFile, \"utf8\").trim();\n if (/^[0-9a-f]{64}$/i.test(hex)) {\n return Buffer.from(hex, \"hex\");\n }\n console.warn(`⚠️ 主密钥文件损坏,已重新生成(旧密文将无法解密)`);\n }\n const key = crypto.randomBytes(32);\n fs.mkdirSync(path.dirname(masterKeyFile), { recursive: true, mode: 0o700 });\n fs.writeFileSync(masterKeyFile, key.toString(\"hex\"), {\n mode: 0o600,\n encoding: \"utf8\",\n });\n return key;\n}\n\n/** 从 64 位 hex 字符串解析主密钥;非法返回 null */\nexport function masterKeyFromHex(hex: string): Buffer | null {\n const h = hex.trim();\n return /^[0-9a-f]{64}$/i.test(h) ? Buffer.from(h, \"hex\") : null;\n}\n\n/**\n * 密文工具:持有固定主密钥,对字符串做加解密。\n * CLI 与 Server 各自构造实例(CLI 用 ~/.config/zqagent/secret.key,\n * Server 用 DATA_DIR/secret.key 或 SECRET_KEY 环境变量)。\n */\nexport class SecretBox {\n constructor(private readonly key: Buffer) {}\n\n /** 主密钥来源一:本机密钥文件(不存在则创建) */\n static fromFile(masterKeyFile: string): SecretBox {\n return new SecretBox(loadOrCreateMasterKey(masterKeyFile));\n }\n\n /** 主密钥来源二:hex 字符串(如环境变量 SECRET_KEY);非法返回 null */\n static fromHex(hex: string): SecretBox | null {\n const key = masterKeyFromHex(hex);\n return key ? new SecretBox(key) : null;\n }\n\n encrypt(plain: string): string {\n return encryptWithKey(this.key, plain);\n }\n\n decrypt(payload: string): string | null {\n return decryptWithKey(this.key, payload);\n }\n}\n","/**\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 * LLM 供应商预设(CLI `/connect` 与 Web/桌面「节点配置」共用的唯一来源)\n *\n * 纯数据模块:不依赖 Node API,前端(console-ui)与后端(core)都可直接引用,\n * 保证两端「添加供应商」时看到完全一致的预置列表。\n *\n * `id` 会持久化为配置项的 `type` 字段(历史字段名,仅展示用途)。\n */\n\nexport interface LlmPreset {\n /** 稳定标识(持久化为 provider.type) */\n id: string;\n /** 展示名 */\n name: string;\n /** OpenAI 兼容端点;空串表示需用户自行填写 */\n baseURL: string;\n /** 建议默认模型;空串表示需要刷新模型列表后由用户选择 */\n model: string;\n /** 是否支持推理强度(reasoning effort)设置 */\n supportsReasoningEffort: boolean;\n /** 选择列表中的补充说明 */\n hint?: string;\n}\n\n/** 预置供应商(顺序即界面展示顺序) */\nexport const LLM_PRESETS: readonly LlmPreset[] = [\n {\n id: \"openai\",\n name: \"OpenAI\",\n baseURL: \"https://api.openai.com/v1\",\n // 不预填模型:预填等于替用户拍板一个他既没选过、也可能没有权限的型号,\n // 之后控制台的模型下拉就凭空多出这个「系统默认模型」。填完 Key 刷新模型,\n // 由供应商实际返回的列表来选。\n model: \"\",\n supportsReasoningEffort: true,\n },\n {\n id: \"deepseek\",\n name: \"DeepSeek\",\n baseURL: \"https://api.deepseek.com\",\n model: \"deepseek-flash\",\n supportsReasoningEffort: false,\n hint: \"deepseek-flash = 通用对话, deepseek-reasoner = 深度推理\",\n },\n {\n id: \"qwen\",\n name: \"通义千问\",\n baseURL: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n model: \"qwen-plus\",\n supportsReasoningEffort: false,\n },\n {\n id: \"zhipu\",\n name: \"智谱 GLM\",\n baseURL: \"https://open.bigmodel.cn/api/paas/v4\",\n model: \"glm-4-plus\",\n supportsReasoningEffort: false,\n },\n {\n id: \"moonshot\",\n name: \"月之暗面 Kimi\",\n baseURL: \"https://api.moonshot.cn/v1\",\n model: \"moonshot-v1-8k\",\n supportsReasoningEffort: false,\n },\n {\n id: \"xiaomi\",\n name: \"小米 MiMo\",\n baseURL: \"\",\n model: \"\",\n supportsReasoningEffort: false,\n hint: \"需自行填写端点与模型\",\n },\n {\n id: \"custom\",\n name: \"其它 (OpenAI 兼容)\",\n baseURL: \"\",\n model: \"\",\n supportsReasoningEffort: true,\n hint: \"任意兼容 OpenAI API 的服务端点(如本地 vLLM 等)\",\n },\n];\n\n/** 按 id 取预设;未命中返回 undefined */\nexport function llmPreset(id: string): LlmPreset | undefined {\n return LLM_PRESETS.find((p) => p.id === id);\n}\n","/**\n * 音视频判定与 HTTP Range 解析(节点 / 控制台 / 网关共用同一套规则)\n *\n * 只收录浏览器原生 `<audio>` / `<video>` 能直接播放的容器。mkv / avi / wmv / flv\n * 等需要转码的格式返回 null,由上层提示「下载后查看」而不是给一个永远播不了的\n * 播放器(错误提示会更难解释)。\n *\n * Range 解析按 RFC 7233:只接受单段(多段请求直接忽略,回整个文件),\n * 语法不合法回 null(调用方忽略 Range),语法合法但区间落在文件外回\n * \"unsatisfiable\"(调用方回 416)。\n */\n\nexport type MediaKind = \"audio\" | \"video\";\n\nexport interface MediaInfo {\n /** 响应用 Content-Type */\n mime: string;\n kind: MediaKind;\n}\n\n/**\n * 扩展名 → MIME / 类别。\n *\n * `mov` 只有 Safari 稳定支持;`webm` 在旧 Safari 上不支持。是否真能播由浏览器\n * 决定,前端监听 `<video onError>` 兜底,不在这里做设备探测。\n */\nconst MEDIA_MIME: Record<string, MediaInfo> = {\n // 视频\n mp4: { mime: \"video/mp4\", kind: \"video\" },\n m4v: { mime: \"video/mp4\", kind: \"video\" },\n webm: { mime: \"video/webm\", kind: \"video\" },\n ogv: { mime: \"video/ogg\", kind: \"video\" },\n mov: { mime: \"video/quicktime\", kind: \"video\" },\n // 音频\n mp3: { mime: \"audio/mpeg\", kind: \"audio\" },\n wav: { mime: \"audio/wav\", kind: \"audio\" },\n m4a: { mime: \"audio/mp4\", kind: \"audio\" },\n aac: { mime: \"audio/aac\", kind: \"audio\" },\n flac: { mime: \"audio/flac\", kind: \"audio\" },\n ogg: { mime: \"audio/ogg\", kind: \"audio\" },\n oga: { mime: \"audio/ogg\", kind: \"audio\" },\n opus: { mime: \"audio/ogg\", kind: \"audio\" },\n weba: { mime: \"audio/webm\", kind: \"audio\" },\n};\n\nfunction extOf(fileName: string): string {\n return fileName.toLowerCase().split(\".\").pop() ?? \"\";\n}\n\n/** 文件名 → 可播放媒体的 MIME 与类别;不可(或浏览器不支持)返回 null */\nexport function mediaInfo(fileName: string): MediaInfo | null {\n return MEDIA_MIME[extOf(fileName)] ?? null;\n}\n\n/** 文件名 → MIME;非可播放媒体返回 null */\nexport function mediaMime(fileName: string): string | null {\n return MEDIA_MIME[extOf(fileName)]?.mime ?? null;\n}\n\n/** 文件名 → audio / video;非可播放媒体返回 null */\nexport function mediaKind(fileName: string): MediaKind | null {\n return MEDIA_MIME[extOf(fileName)]?.kind ?? null;\n}\n\n/**\n * 浏览器可**内嵌**渲染的文档 MIME。\n *\n * 与 `MEDIA_MIME` 分开维护:那张表的语义是「`<video>` / `<audio>` 能直接播放」,\n * PDF 不是音视频,混进去会让 `mediaKind()` 返回一个不存在的 kind。这里只声明\n * 「浏览器自带查看器能显示」,由前端决定用 `<iframe>` 还是保底提示。\n *\n * 注意浏览器支持面不是重点:Chrome / Edge / Firefox / Safari / WebView2 都有\n * 内置 PDF 查看器,WebKitGTK(Linux 桌面壳)没有——前端必须能优雅退化。\n */\nconst DOC_PREVIEW_MIME: Record<string, string> = {\n pdf: \"application/pdf\",\n};\n\n/** 文件名 → 可内嵌预览的文档 MIME;非文档返回 null */\nexport function docPreviewMime(fileName: string): string | null {\n return DOC_PREVIEW_MIME[extOf(fileName)] ?? null;\n}\n\n/**\n * 文件名 → 可用于 **HTTP Range 流式预览**的 MIME(音视频 + 可内嵌文档)。\n *\n * 节点签发文件票据 / `/files/range` 用它决定 `Content-Type`:票据里没有 MIME 时\n * 前端会直接放弃流式通道,退回整文件下载(大 PDF 尤其痛),所以 PDF 必须在这里\n * 被认出来。\n */\nexport function previewMime(fileName: string): string | null {\n return mediaMime(fileName) ?? docPreviewMime(fileName);\n}\n\nexport interface ByteRange {\n /** 起始偏移(含) */\n start: number;\n /** 结束偏移(含) */\n end: number;\n}\n\n/**\n * 解析 `Range: bytes=...`。\n *\n * @param header 原始头值(可为 null / 空)\n * @param size 文件总字节数\n * @returns 单段区间;`null` = 忽略该头(无 Range / 多段 / 语法不合法);\n * `\"unsatisfiable\"` = 范围越界,调用方应回 416。\n */\nexport function parseByteRange(\n header: string | null | undefined,\n size: number,\n): ByteRange | \"unsatisfiable\" | null {\n const raw = (header ?? \"\").trim();\n if (!raw) return null;\n const m = /^bytes=(\\d*)-(\\d*)$/i.exec(raw);\n if (!m) return null; // 多段与其它单位一律忽略\n\n const startRaw = m[1] ?? \"\";\n const endRaw = m[2] ?? \"\";\n if (!startRaw && !endRaw) return null; // \"bytes=-\"\n if (size <= 0) return \"unsatisfiable\";\n\n // 后缀形式:bytes=-N → 最后 N 字节\n if (!startRaw) {\n const suffix = Number(endRaw);\n if (!Number.isFinite(suffix) || suffix <= 0) return \"unsatisfiable\";\n return { start: Math.max(size - suffix, 0), end: size - 1 };\n }\n\n const start = Number(startRaw);\n if (!Number.isFinite(start) || start < 0) return null;\n if (start >= size) return \"unsatisfiable\";\n\n // 省略结束:bytes=N- → 到文件末尾\n if (!endRaw) return { start, end: size - 1 };\n\n const end = Number(endRaw);\n if (!Number.isFinite(end) || end < start) return null;\n return { start, end: Math.min(end, size - 1) };\n}\n\n/** Range 响应的 `Content-Range` 头值 */\nexport function contentRangeHeader(range: ByteRange, size: number): string {\n return `bytes ${range.start}-${range.end}/${size}`;\n}\n\n/**\n * 文件 ETag(强校验)。\n *\n * 由 size + mtime 组成:文件内容变化几乎必然改变其中一个;不读文件本身,\n * 因此 `HEAD` / `304` 可以在完全不触达节点的情况下应答。\n */\nexport function fileEtag(size: number, mtimeMs: number): string {\n return `\"${size.toString(16)}-${Math.trunc(mtimeMs).toString(16)}\"`;\n}\n","/**\n * 共享类型:通知事件与渠道配置\n *\n * 从 server/types.ts 抽出,供 core(节点)与 hub(控制面)共用的通知模块引用。\n */\n\n/** 通知种类 */\nexport type NotifyKind =\n | \"approval_request\" // 需要审批(data: approvalId / toolName / arguments / reason)\n | \"question_asked\" // 需要回答提问(data: questionId / kind / question / options / ...)\n | \"turn_completed\" // 一轮 Turn 正常完成(data: status / iterations / produced_files)\n | \"turn_stopped\" // 达到最大迭代次数等提前终止\n | \"error\" // 出错(data: error / retriable)\n /**\n * 会话状态变化(data: status)。\n *\n * 只承载状态机取值,不含消息/模型/token 等明细——跨节点总览只靠它同步,\n * 明细仍去会话详情页拉。与上面几种「需要人处理」的通知不同,它**不面向\n * 第三方渠道**(见 `channelWants`),也不弹提醒。\n */\n | \"session_status\"\n /**\n * 会话状态全量快照(data: `{ state }`,字段与 `GET /sessions/:id/state` 一致)。\n *\n * 控制台收到后直接替换本地快照,因此**不再周期性拉 `/state`**。\n * 只在状态机、待办、排队信息、草稿、名称真的变化时才发(工具执行期间不发)。\n * 与 `session_status` 一样只服务控制台,不面向第三方渠道。\n */\n | \"session_snapshot\"\n /**\n * 后台 shell 任务变化(data: `{ projectId, tasks }`,tasks 为精简字段)。\n *\n * 代替对 `GET /shell/tasks` 的轮询:任务启动 / 退出 / 被终止时推送一次。\n */\n | \"shell_tasks_changed\";\n\n/**\n * 通知事件:SessionManager 在审批挂起 / 提问挂起 / 完成 / 出错等关键点产出,\n * 交给通知中枢(NotificationHub / 节点桥)广播到多设备或第三方渠道。\n */\nexport interface NotifyEvent {\n kind: NotifyKind;\n sessionId: string;\n ts: string;\n data: Record<string, unknown>;\n}\n\n/** 通知渠道类型 */\nexport type NotificationChannelType =\n | \"mqtt\"\n | \"telegram\"\n | \"wecom\"\n | \"dingtalk\"\n | \"feishu\"\n | \"serverchan\"\n | \"pushplus\"\n | \"bark\"\n | \"email\"\n | \"sms\"\n | \"qq\"\n | \"webpush\"\n | \"webhook\";\n\n/** 通知渠道配置(敏感字段如 token / password 以密文存储) */\nexport interface NotificationChannelConfig {\n id: string;\n type: NotificationChannelType;\n enabled: boolean;\n config: Record<string, string>;\n events: {\n approval: boolean;\n question: boolean;\n completed: boolean;\n error: boolean;\n };\n}\n\n/**\n * 内容增量事件:逐 token 的高频帧。\n *\n * 控制台「折叠过程输出」时,这两类事件既不会被渲染,又占聊天流量的绝大部分,\n * 因此服务端按订阅参数 `detail=0` 直接在源头丢弃(SSE / 网关两条链路一致)。\n *\n * **改这里必须同步改 `gateway/src/codec.rs` 的 `CONTENT_DELTA_TYPES`**,\n * 否则两条链路会出现「一侧省了、另一侧还在发」。\n */\nexport const CONTENT_DELTA_TYPES: ReadonlySet<string> = new Set([\n \"assistant_delta\",\n \"reasoning_delta\",\n // 节点把「只含内容增量」的一批事件合并成 `{zqBatch:[...]}` 时用的顶层标记。\n // 混入结构事件的一批不能打这个标记,否则整帧被丢会连正文一起丢。\n \"content_delta\",\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":["crypto","crypto","TIMEOUT_MS","TIMEOUT_MS","escapeHtml"]}
1
+ {"version":3,"sources":["../src/fleet.ts","../src/common.ts","../src/secrets.ts","../src/approval-view.ts","../src/llm-presets.ts","../src/media.ts","../src/types.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 * MQTT 节点管理协议 v2(控制台 ↔ 多终端设备)\n *\n * 与 v1(`protocol.ts`,按租户、deviceId 的手机控制)并存。v2 面向\n * 「公网控制台管理多台终端 Agent 实例」:\n *\n * - **按节点寻址**:topic 含 `node/{nodeId}`,每个节点只订阅自己的 cmd;\n * - **在线状态**:`status` retained + LWT(遗嘱)自动上线/离线,`info` retained\n * (能力/项目快照,上线即重建节点管理视图);\n * - **请求-响应关联**:命令带 `reqId`,回执(ack)回带同一 `reqId`;\n * - **签名密钥**:节点管理级 `fleetSecret`(自托管单所有者模型;每节点独立密钥留待 P3)。\n *\n * 安全前提同 v1:broker 可无认证/ACL,隔离靠「高熵 fleetToken 命名空间 + HMAC 签名」。\n */\nimport * as crypto from \"crypto\";\nimport {\n DEFAULT_CLOCK_SKEW_MS,\n NonceTracker,\n generateToken,\n isFreshTimestamp,\n} from \"./common.js\";\n\nexport { NonceTracker, generateToken, isFreshTimestamp, DEFAULT_CLOCK_SKEW_MS };\n\n/** topic 根前缀 */\nexport const FLEET_TOPIC_PREFIX = \"zqagent/fleet\";\n\n/**\n * 会话状态条目(跨节点总览的唯一数据形状)。\n *\n * 刻意只包含「标识 + 状态」:消息、模型、token 等明细由会话详情页按需拉取。\n * 节点经 info `detail.sessions` 上报全量快照,状态变化经 notify `session_status`\n * 增量推送;接口(REST / 指令)也用同一形状。\n */\n/**\n * 会话状态条目(跨节点总览的唯一数据形状)。\n *\n * - `status` 取 `SessionStatus` 的状态值;另有一个**伪状态** `\"deleted\"`:\n * 会话已删除,只出现在 `session_status` 通知里(不落盘、不进状态机),\n * 供常开的总览页面立即移除该行。\n * - 只含标识与状态:消息、模型、token 等明细由会话详情页按需拉取。\n */\nexport interface SessionStateEntry {\n projectId: string;\n /** 项目展示名(节点侧填充;详情快照里由 projects 补齐) */\n projectName: string;\n sessionId: string;\n name: string | null;\n status: string;\n /** 用户最近一条发送给 Agent 的消息(截断,供总览列表展示;无消息时 null) */\n lastUserMessage?: string | null;\n /**\n * 最近一次**任务请求的开始时刻**(ISO 8601;从未发起过任务时 null)。\n *\n * 工作台的节点/项目/任务列表统一按它倒序——用户找的是「最近在跑什么」,\n * 而不是「最近什么时候跑完的」:长任务运行中时 lastCompletedAt 还是上一轮的,\n * 按它排会把正在干活的排到后面。\n */\n lastStartedAt?: string | null;\n /** 最近一次任务成功完成的时刻(ISO 8601;从未完成时 null) */\n lastCompletedAt?: string | null;\n}\n\n/** 命令类型(控制台 → 节点) */\nexport type FleetCmdType =\n | \"ping\"\n | \"get_node_info\"\n | \"get_node_host\"\n | \"list_projects\"\n | \"create_project\"\n | \"delete_project\"\n | \"list_sessions\"\n /** 跨项目会话状态(只含标识与 status,供节点首页/工作台总览) */\n | \"get_session_states\"\n | \"create_session\"\n | \"delete_session\"\n | \"get_state\"\n | \"get_events\"\n /** 会话轮次汇总(轻量:标题 + 统计,点开某轮再取事件) */\n | \"get_session_turns\"\n /** 单轮详情:用户消息(含补充)+ 最终回复 + 产出文件 */\n | \"get_session_turn_reply\"\n /** 读取聊天记录里保存的附件(用户消息的图片 / 文件) */\n | \"read_session_attachment\"\n /** 变更追踪:本会话的快照列表(每轮一个) */\n | \"get_session_changes\"\n /** 变更追踪:某个快照的完整 patch */\n | \"get_session_change_diff\"\n /** 变更追踪:从快照恢复文件(会改工作区) */\n | \"restore_session_change\"\n | \"get_config\"\n | \"update_config\"\n | \"list_models\"\n | \"list_dirs\"\n | \"make_dir\"\n | \"search_dirs\"\n | \"list_files\"\n | \"read_file\"\n /** 按区间读取文件(音视频预览的数据面;回执为 base64 块,单块 ≤1MB) */\n | \"read_file_range\"\n /**\n * 签发媒体播放票据。\n *\n * `<video>` 发不了 Authorization 头,所以由节点用 fleetSecret 签一张只授权\n * 单个文件、10 分钟有效的票据,网关用同一密钥验签后提供 HTTP Range。\n */\n | \"file_ticket\"\n | \"download_file\"\n | \"write_file\"\n /** 按 base64 写入二进制文件(图片编辑后保存) */\n | \"write_file_base64\"\n | \"create_dir\"\n | \"create_file\"\n | \"delete_files\"\n | \"search_files\"\n /** 在文件**内容**里搜索关键字(返回行号与命中片段) */\n | \"search_content\"\n /** 重命名文件 / 目录(只改同一目录下的名字) */\n | \"rename_path\"\n /** 把文件 / 目录移动到工作区内的目标目录(拖拽移动) */\n | \"move_paths\"\n /** 压缩下载:把选中的文件 / 目录打包成 zip(超过上限直接拒绝) */\n | \"zip_download\"\n | \"list_tools\"\n | \"list_mcp\"\n /** 整体替换项目的 MCP 服务器配置(控制台设置页保存) */\n | \"update_mcp\"\n /** 连接测试单个 MCP 服务器(不落盘) */\n | \"test_mcp\"\n | \"project_stats\"\n | \"run_command\"\n | \"list_shell_tasks\"\n | \"kill_shell_task\"\n // ---------- 交互式终端(控制台「终端」面板) ----------\n // 只承载低频控制动作;用户按键(input)与窗口尺寸(resize)走\n // `FrameType.TERMINAL` 独立帧通道——那条路不等 ACK、不占单节点串行指令队列。\n /** 列出项目下的终端(含已退出未删除的) */\n | \"terminal_list\"\n /** 新建终端(多终端并行:每次一个新进程) */\n | \"terminal_create\"\n /** 取回放缓冲(≤200KB)与元信息:切页签 / 断线重连用 */\n | \"terminal_replay\"\n /** 关闭进程(SIGTERM → 2s → SIGKILL),保留历史与元信息 */\n | \"terminal_close\"\n /** 关闭并删除历史(不可恢复) */\n | \"terminal_remove\"\n | \"terminal_rename\"\n /** 命令/输出/退出历史(受保留期与落盘限速约束) */\n | \"terminal_history\"\n /** 终端设置(当前只有 retentionDays,默认 30 天) */\n | \"terminal_settings\"\n | \"terminal_update_settings\"\n | \"git_repos\"\n | \"git_status\"\n | \"git_diff\"\n | \"git_branches\"\n | \"git_refs\"\n | \"git_log\"\n | \"git_show\"\n | \"git_fetch\"\n | \"git_stage\"\n | \"git_unstage\"\n | \"git_commit\"\n /** AI 生成提交说明(节点侧调用 LLM) */\n | \"git_commit_message\"\n | \"git_checkout\"\n | \"git_pull\"\n | \"git_push\"\n | \"set_node_label\"\n | \"get_usage_summary\"\n /** 大模型账户余额(控制台「余额不足」时查看/充值用) */\n | \"get_llm_balance\"\n | \"get_usage_records\"\n | \"get_usage_pricing\"\n | \"update_usage_pricing\"\n | \"get_node_llm\"\n | \"update_node_llm\"\n /**\n * 节点自更新(自更新仅在「SEA 单文件 + 服务托管 + 配置验签公钥」时可用):\n * payload.action = \"status\" | \"check\" | \"install\"\n * payload.force = true 时立即安装(不等空闲,会打断运行中的任务)\n *\n * install 先回执、后在后台下载安装并退出进程,由 systemd/launchd 拉起新版本;\n * 因此**回执成功不等于已升级**,需再用 status 确认版本。\n */\n | \"self_update\"\n /**\n * 重新拉取平台内置模型(志前 API)的凭据。\n *\n * 内置模型在接入 Hub 时下发,正常情况下用户无需关心;这里用于\n * 「Key 在控制台被重置」或用户在控制台点「刷新」时重取。\n * 会改写节点侧的 LLM 配置,属写指令(不参与只读并发池)。\n */\n | \"refresh_builtin_llm\"\n | \"list_node_llm_models\"\n | \"set_fleet_cred\"\n | \"rename_session\"\n | \"set_session_model\"\n | \"set_session_llm\"\n | \"set_session_mode\"\n | \"set_session_lang\"\n | \"set_session_draft\"\n | \"submit_turn\"\n | \"steer\"\n | \"unsteer\"\n | \"interrupt\"\n | \"resume\"\n | \"approval_answer\"\n | \"question_answer\"\n /**\n * 批量指令:payload.commands 为子指令数组,节点顺序/并发执行后\n * 在单条回执的 data.results 中按序返回各子指令结果(减少往返与请求数)。\n */\n | \"batch\";\n\nexport const FLEET_CMD_TYPES: readonly FleetCmdType[] = [\n \"ping\",\n \"get_node_info\",\n \"get_node_host\",\n \"list_projects\",\n \"create_project\",\n \"delete_project\",\n \"list_sessions\",\n \"get_session_states\",\n \"create_session\",\n \"delete_session\",\n \"get_state\",\n \"get_events\",\n \"get_session_turns\",\n \"get_session_turn_reply\",\n \"read_session_attachment\",\n \"get_session_changes\",\n \"get_session_change_diff\",\n \"restore_session_change\",\n \"get_config\",\n \"update_config\",\n \"list_models\",\n \"list_dirs\",\n \"make_dir\",\n \"search_dirs\",\n \"list_files\",\n \"read_file\",\n \"read_file_range\",\n \"file_ticket\",\n \"download_file\",\n \"write_file\",\n \"write_file_base64\",\n \"create_dir\",\n \"create_file\",\n \"delete_files\",\n \"search_files\",\n \"search_content\",\n \"rename_path\",\n \"move_paths\",\n \"zip_download\",\n \"list_tools\",\n \"list_mcp\",\n \"update_mcp\",\n \"test_mcp\",\n \"project_stats\",\n \"run_command\",\n \"list_shell_tasks\",\n \"kill_shell_task\",\n \"terminal_list\",\n \"terminal_create\",\n \"terminal_replay\",\n \"terminal_close\",\n \"terminal_remove\",\n \"terminal_rename\",\n \"terminal_history\",\n \"terminal_settings\",\n \"terminal_update_settings\",\n \"git_repos\",\n \"git_status\",\n \"git_diff\",\n \"git_branches\",\n \"git_refs\",\n \"git_log\",\n \"git_show\",\n \"git_fetch\",\n \"git_stage\",\n \"git_unstage\",\n \"git_commit\",\n \"git_commit_message\",\n \"git_checkout\",\n \"git_pull\",\n \"git_push\",\n \"set_node_label\",\n \"get_usage_summary\",\n \"get_llm_balance\",\n \"get_usage_records\",\n \"get_usage_pricing\",\n \"update_usage_pricing\",\n \"get_node_llm\",\n \"update_node_llm\",\n \"self_update\",\n \"refresh_builtin_llm\",\n \"list_node_llm_models\",\n \"set_fleet_cred\",\n \"rename_session\",\n \"set_session_model\",\n \"set_session_llm\",\n \"set_session_mode\",\n \"set_session_lang\",\n \"set_session_draft\",\n \"submit_turn\",\n \"steer\",\n \"unsteer\",\n \"interrupt\",\n \"resume\",\n \"approval_answer\",\n \"question_answer\",\n \"batch\",\n];\n\n/** 批量指令的子指令(不含嵌套 batch) */\nexport interface FleetBatchSubCmd {\n type: Exclude<FleetCmdType, \"batch\">;\n projectId?: string;\n sessionId?: string;\n payload?: Record<string, unknown>;\n}\n\n/** 批量指令结果项(与请求按序对应) */\nexport interface FleetBatchResult {\n type: string;\n ok: boolean;\n data?: unknown;\n error?: string;\n}\n\n/** 命令信封 v2(含 HMAC 签名) */\nexport interface FleetCmdEnvelope {\n /** 时间戳(毫秒,±60s 窗口) */\n t: number;\n /** 一次性随机串(防重放) */\n nonce: string;\n /** 请求 ID(回执关联) */\n reqId: string;\n type: FleetCmdType;\n /** 发起控制台客户端 ID */\n clientId: string;\n /** 目标节点 ID */\n nodeId: string;\n /** 目标项目 ID(项目级命令) */\n projectId?: string;\n /** 目标会话 ID(会话级命令) */\n sessionId?: string;\n payload: Record<string, unknown>;\n /** HMAC-SHA256(fleetSecret, 规范化串) base64url */\n sig: string;\n}\n\n/** 命令回执(点对点回给 clientId) */\nexport interface FleetAckPayload {\n reqId: string;\n type: FleetCmdType;\n ok: boolean;\n error?: string;\n data?: unknown;\n}\n\n/** 节点在线状态(retained + LWT) */\nexport interface FleetStatus {\n nodeId: string;\n online: boolean;\n ts: string;\n /** 节点展示名(本地配置;Hub 改名后回写并随之上报) */\n label?: string;\n platform?: string;\n mode?: string;\n version?: string;\n /**\n * 节点自报的主 IP(网卡地址:首个非回环 IPv4,缺省 IPv6)。\n *\n * 它是内网地址,控制台展示节点公网 IP 时应以网关观察到的连接来源为准\n * (见 gateway `NodeStateInfo.peer_ip`)。\n */\n ip?: string;\n}\n\n/** 节点累计运行统计(info 上报;进程启动后累计) */\nexport interface FleetNodeStats {\n /** 完成的回合数(turn_end) */\n turns: number;\n /** 工具调用完成数(tool_finished) */\n toolCalls: number;\n /** 错误事件数(error) */\n errors: number;\n /** 累计输入 token(assistant_message.usage.input) */\n tokensIn: number;\n /** 累计输出 token */\n tokensOut: number;\n /** 统计起点(节点进程启动/桥接建立时间) */\n since: string;\n}\n\n/** 节点管理配对凭据(pairing API 下发) */\nexport interface FleetPairing {\n fleetToken: string;\n fleetSecret: string;\n nodeId: string;\n /** Rust 网关 wss 地址(控制面配置了才下发;节点据此直连网关) */\n gatewayUrl?: string;\n}\n\n/**\n * 节点本地持久化的云端接入凭据(`{DATA_DIR}/fleet.json`)。\n *\n * 由「凭注册码接入」(`POST /api/hub/enroll`)返回后落盘;节点启动时优先\n * 读取此文件,其次回退到环境变量(`FLEET_TOKEN` / `FLEET_SECRET`)。\n *\n * 「去 EMQX」后不再有 per-fleet broker 账号:网关鉴权用**全局** `GATEWAY_FLEET_SECRET`,\n * 因此凭据只需 fleetToken/fleetSecret(前者仅作命名空间标识)与网关地址。\n */\nexport interface FleetCredential {\n /** Hub 控制面地址(接入时使用,便于日后重新配对/展示) */\n hubUrl?: string;\n fleetToken: string;\n fleetSecret: string;\n nodeId: string;\n /**\n * Rust 网关 wss 地址(控制面在配对/接入时下发)。\n *\n * 它是节点接入云端的**唯一**传输,环境变量 `FLEET_GATEWAY_URL` 仅作覆盖。\n */\n gatewayUrl?: string;\n /** 节点展示名(可选) */\n label?: string;\n /** 接入时间(ISO 字符串) */\n enrolledAt?: string;\n /**\n * 平台内置大模型(志前 API):接入 Hub 时由控制面下发,节点侧落盘为\n * `id=__zqai__` 的不可删除供应商。\n *\n * 这里只是「接入那一刻的快照」;凭据轮换或首次缺失时,节点会调用\n * `GET /api/hub/node/builtin-llm` 重新拉取(见 core 的 builtinLlm.ts)。\n */\n builtinLlm?: BuiltinLlmGrant;\n}\n\n/** 平台内置模型的接入凭据(下发给节点) */\nexport interface BuiltinLlmGrant {\n /** 固定为 `__zqai__` */\n providerId: string;\n /** 展示名 */\n name: string;\n /** OpenAI 兼容基址,如 https://zqai-api.cd-zq.com/v1 */\n baseURL: string;\n /** 用户的 API Key(节点侧加密落盘) */\n apiKey: string;\n /** 默认模型 */\n model: string;\n /** 该账号可用的模型列表 */\n models?: string[];\n}\n\n/** topic 构建 */\nexport function fleetCmdTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/cmd`;\n}\nexport function fleetNotifyTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/notify`;\n}\nexport function fleetEventsTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/events`;\n}\nexport function fleetStatusTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/status`;\n}\nexport function fleetInfoTopic(fleetToken: string, nodeId: string): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/info`;\n}\nexport function fleetAckTopic(\n fleetToken: string,\n nodeId: string,\n clientId: string,\n): string {\n return `${FLEET_TOPIC_PREFIX}/${fleetToken}/node/${nodeId}/ack/${clientId}`;\n}\n\n/** 签名前的规范化串(字段顺序固定,两侧一致) */\nfunction canonical(env: Omit<FleetCmdEnvelope, \"sig\">): string {\n return JSON.stringify([\n env.t,\n env.nonce,\n env.reqId,\n env.type,\n env.clientId,\n env.nodeId,\n env.projectId ?? \"\",\n env.sessionId ?? \"\",\n env.payload,\n ]);\n}\n\n/** 计算 HMAC-SHA256 签名 */\nexport function signFleetCmd(\n secret: string,\n env: Omit<FleetCmdEnvelope, \"sig\">,\n): string {\n return crypto\n .createHmac(\"sha256\", Buffer.from(secret, \"utf8\"))\n .update(canonical(env))\n .digest(\"base64url\");\n}\n\n/** 常量时间比较校验签名 */\nexport function verifyFleetSig(secret: string, env: FleetCmdEnvelope): boolean {\n if (typeof env.sig !== \"string\" || env.sig.length === 0) return false;\n const expected = signFleetCmd(secret, env);\n const a = Buffer.from(expected, \"utf8\");\n const b = Buffer.from(env.sig, \"utf8\");\n if (a.length !== b.length) return false;\n return crypto.timingSafeEqual(a, b);\n}\n\n/** 解析并校验命令信封结构(不校验签名;签名由调用方验) */\nexport function parseFleetEnvelope(raw: unknown): FleetCmdEnvelope | null {\n if (!raw || typeof raw !== \"object\") return null;\n const o = raw as Record<string, unknown>;\n if (\n typeof o.t !== \"number\" ||\n typeof o.nonce !== \"string\" ||\n typeof o.reqId !== \"string\" ||\n typeof o.type !== \"string\" ||\n typeof o.clientId !== \"string\" ||\n typeof o.nodeId !== \"string\" ||\n typeof o.sig !== \"string\"\n ) {\n return null;\n }\n if (!FLEET_CMD_TYPES.includes(o.type as FleetCmdType)) return null;\n if (!o.payload || typeof o.payload !== \"object\") return null;\n return {\n t: o.t,\n nonce: o.nonce,\n reqId: o.reqId,\n type: o.type as FleetCmdType,\n clientId: o.clientId,\n nodeId: o.nodeId,\n projectId: typeof o.projectId === \"string\" ? o.projectId : undefined,\n sessionId: typeof o.sessionId === \"string\" ? o.sessionId : undefined,\n payload: o.payload as Record<string, unknown>,\n sig: o.sig,\n };\n}\n","/**\n * 共享通用工具(节点管理协议与 MQTT 相关)\n *\n * 从 v1 协议层抽出,供节点管理协议(node 与 hub 共用)使用,避免 shared 依赖 core。\n */\nimport * as crypto from \"crypto\";\n\n/** 时间戳允许的最大时钟偏差(毫秒) */\nexport const DEFAULT_CLOCK_SKEW_MS = 60_000;\n\n/** 生成高熵随机 token(base64url,默认 32 字节) */\nexport function generateToken(bytes = 32): string {\n return crypto.randomBytes(bytes).toString(\"base64url\");\n}\n\n/** 时间戳是否在允许窗口内(防重放) */\nexport function isFreshTimestamp(\n t: number,\n now = Date.now(),\n skewMs = DEFAULT_CLOCK_SKEW_MS,\n): boolean {\n return typeof t === \"number\" && Number.isFinite(t) && Math.abs(now - t) <= skewMs;\n}\n\n/**\n * 一次性 nonce 去重器:记录已见 nonce(带过期清理),拒绝重放。\n * 每个 MQTT 连接(租户/节点)持有一个实例。\n */\nexport class NonceTracker {\n private readonly seen = new Map<string, number>();\n\n constructor(private readonly ttlMs = DEFAULT_CLOCK_SKEW_MS * 5) {}\n\n /** 返回 true 表示首次见到(可用);false 表示重放 */\n checkAndRecord(nonce: string, now = Date.now()): boolean {\n this.prune(now);\n if (this.seen.has(nonce)) return false;\n this.seen.set(nonce, now);\n return true;\n }\n\n private prune(now: number): void {\n for (const [k, ts] of this.seen) {\n if (now - ts > this.ttlMs) this.seen.delete(k);\n }\n }\n}\n","/**\n * 对称加密共享模块\n *\n * 从 CLI 的 connection.ts 抽出的 AES-256-GCM 加解密实现,供 CLI 与\n * 云端 Server 复用。密文格式 `v1:iv:tag:ciphertext`(均 base64)。\n *\n * 安全说明:这是「本机混淆式加密」(纯 Node 无外部依赖),主密钥与密文\n * 可存于同一台机器,能防止明文泄露 / 被 grep / 误提交,但无法抵御能读取\n * 本机主密钥文件的攻击者(如需 OS 级密钥链,可替换为 keytar / libsecret 等)。\n */\nimport * as crypto from \"crypto\";\nimport * as fs from \"fs\";\nimport * as path from \"path\";\n\n/** 用给定主密钥做 AES-256-GCM 加密,输出 `v1:iv:tag:ciphertext`(base64) */\nexport function encryptWithKey(key: Buffer, plain: string): string {\n const iv = crypto.randomBytes(12);\n const cipher = crypto.createCipheriv(\"aes-256-gcm\", key, iv);\n const enc = Buffer.concat([cipher.update(plain, \"utf8\"), cipher.final()]);\n const tag = cipher.getAuthTag();\n return [\n \"v1\",\n iv.toString(\"base64\"),\n tag.toString(\"base64\"),\n enc.toString(\"base64\"),\n ].join(\":\");\n}\n\n/** 用给定主密钥解密;格式非法 / 认证失败返回 null */\nexport function decryptWithKey(key: Buffer, payload: string): string | null {\n try {\n const parts = payload.split(\":\");\n if (parts.length !== 4 || parts[0] !== \"v1\") return null;\n const iv = Buffer.from(parts[1]!, \"base64\");\n const tag = Buffer.from(parts[2]!, \"base64\");\n const data = Buffer.from(parts[3]!, \"base64\");\n const decipher = crypto.createDecipheriv(\"aes-256-gcm\", key, iv);\n decipher.setAuthTag(tag);\n return Buffer.concat([\n decipher.update(data),\n decipher.final(),\n ]).toString(\"utf8\");\n } catch {\n return null;\n }\n}\n\n/** 读取或生成主密钥(32 字节随机,权限 600)。文件损坏则重建。 */\nexport function loadOrCreateMasterKey(masterKeyFile: string): Buffer {\n if (fs.existsSync(masterKeyFile)) {\n const hex = fs.readFileSync(masterKeyFile, \"utf8\").trim();\n if (/^[0-9a-f]{64}$/i.test(hex)) {\n return Buffer.from(hex, \"hex\");\n }\n console.warn(`⚠️ 主密钥文件损坏,已重新生成(旧密文将无法解密)`);\n }\n const key = crypto.randomBytes(32);\n fs.mkdirSync(path.dirname(masterKeyFile), { recursive: true, mode: 0o700 });\n fs.writeFileSync(masterKeyFile, key.toString(\"hex\"), {\n mode: 0o600,\n encoding: \"utf8\",\n });\n return key;\n}\n\n/** 从 64 位 hex 字符串解析主密钥;非法返回 null */\nexport function masterKeyFromHex(hex: string): Buffer | null {\n const h = hex.trim();\n return /^[0-9a-f]{64}$/i.test(h) ? Buffer.from(h, \"hex\") : null;\n}\n\n/**\n * 密文工具:持有固定主密钥,对字符串做加解密。\n * CLI 与 Server 各自构造实例(CLI 用 ~/.config/zqagent/secret.key,\n * Server 用 DATA_DIR/secret.key 或 SECRET_KEY 环境变量)。\n */\nexport class SecretBox {\n constructor(private readonly key: Buffer) {}\n\n /** 主密钥来源一:本机密钥文件(不存在则创建) */\n static fromFile(masterKeyFile: string): SecretBox {\n return new SecretBox(loadOrCreateMasterKey(masterKeyFile));\n }\n\n /** 主密钥来源二:hex 字符串(如环境变量 SECRET_KEY);非法返回 null */\n static fromHex(hex: string): SecretBox | null {\n const key = masterKeyFromHex(hex);\n return key ? new SecretBox(key) : null;\n }\n\n encrypt(plain: string): string {\n return encryptWithKey(this.key, plain);\n }\n\n decrypt(payload: string): string | null {\n return decryptWithKey(this.key, payload);\n }\n}\n","/**\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 * LLM 供应商预设(CLI `/connect` 与 Web/桌面「节点配置」共用的唯一来源)\n *\n * 纯数据模块:不依赖 Node API,前端(console-ui)与后端(core)都可直接引用,\n * 保证两端「添加供应商」时看到完全一致的预置列表。\n *\n * `id` 会持久化为配置项的 `type` 字段(历史字段名,仅展示用途)。\n */\n\nexport interface LlmPreset {\n /** 稳定标识(持久化为 provider.type) */\n id: string;\n /** 展示名 */\n name: string;\n /** OpenAI 兼容端点;空串表示需用户自行填写 */\n baseURL: string;\n /** 建议默认模型;空串表示需要刷新模型列表后由用户选择 */\n model: string;\n /** 是否支持推理强度(reasoning effort)设置 */\n supportsReasoningEffort: boolean;\n /** 选择列表中的补充说明 */\n hint?: string;\n}\n\n/** 预置供应商(顺序即界面展示顺序) */\nexport const LLM_PRESETS: readonly LlmPreset[] = [\n {\n id: \"openai\",\n name: \"OpenAI\",\n baseURL: \"https://api.openai.com/v1\",\n // 不预填模型:预填等于替用户拍板一个他既没选过、也可能没有权限的型号,\n // 之后控制台的模型下拉就凭空多出这个「系统默认模型」。填完 Key 刷新模型,\n // 由供应商实际返回的列表来选。\n model: \"\",\n supportsReasoningEffort: true,\n },\n {\n id: \"deepseek\",\n name: \"DeepSeek\",\n baseURL: \"https://api.deepseek.com\",\n model: \"deepseek-flash\",\n supportsReasoningEffort: false,\n hint: \"deepseek-flash = 通用对话, deepseek-reasoner = 深度推理\",\n },\n {\n id: \"qwen\",\n name: \"通义千问\",\n baseURL: \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n model: \"qwen-plus\",\n supportsReasoningEffort: false,\n },\n {\n id: \"zhipu\",\n name: \"智谱 GLM\",\n baseURL: \"https://open.bigmodel.cn/api/paas/v4\",\n model: \"glm-4-plus\",\n supportsReasoningEffort: false,\n },\n {\n id: \"moonshot\",\n name: \"月之暗面 Kimi\",\n baseURL: \"https://api.moonshot.cn/v1\",\n model: \"moonshot-v1-8k\",\n supportsReasoningEffort: false,\n },\n {\n id: \"xiaomi\",\n name: \"小米 MiMo\",\n baseURL: \"\",\n model: \"\",\n supportsReasoningEffort: false,\n hint: \"需自行填写端点与模型\",\n },\n {\n id: \"custom\",\n name: \"其它 (OpenAI 兼容)\",\n baseURL: \"\",\n model: \"\",\n supportsReasoningEffort: true,\n hint: \"任意兼容 OpenAI API 的服务端点(如本地 vLLM 等)\",\n },\n];\n\n/** 按 id 取预设;未命中返回 undefined */\nexport function llmPreset(id: string): LlmPreset | undefined {\n return LLM_PRESETS.find((p) => p.id === id);\n}\n","/**\n * 音视频判定与 HTTP Range 解析(节点 / 控制台 / 网关共用同一套规则)\n *\n * 只收录浏览器原生 `<audio>` / `<video>` 能直接播放的容器。mkv / avi / wmv / flv\n * 等需要转码的格式返回 null,由上层提示「下载后查看」而不是给一个永远播不了的\n * 播放器(错误提示会更难解释)。\n *\n * Range 解析按 RFC 7233:只接受单段(多段请求直接忽略,回整个文件),\n * 语法不合法回 null(调用方忽略 Range),语法合法但区间落在文件外回\n * \"unsatisfiable\"(调用方回 416)。\n */\n\nexport type MediaKind = \"audio\" | \"video\";\n\nexport interface MediaInfo {\n /** 响应用 Content-Type */\n mime: string;\n kind: MediaKind;\n}\n\n/**\n * 扩展名 → MIME / 类别。\n *\n * `mov` 只有 Safari 稳定支持;`webm` 在旧 Safari 上不支持。是否真能播由浏览器\n * 决定,前端监听 `<video onError>` 兜底,不在这里做设备探测。\n */\nconst MEDIA_MIME: Record<string, MediaInfo> = {\n // 视频\n mp4: { mime: \"video/mp4\", kind: \"video\" },\n m4v: { mime: \"video/mp4\", kind: \"video\" },\n webm: { mime: \"video/webm\", kind: \"video\" },\n ogv: { mime: \"video/ogg\", kind: \"video\" },\n mov: { mime: \"video/quicktime\", kind: \"video\" },\n // 音频\n mp3: { mime: \"audio/mpeg\", kind: \"audio\" },\n wav: { mime: \"audio/wav\", kind: \"audio\" },\n m4a: { mime: \"audio/mp4\", kind: \"audio\" },\n aac: { mime: \"audio/aac\", kind: \"audio\" },\n flac: { mime: \"audio/flac\", kind: \"audio\" },\n ogg: { mime: \"audio/ogg\", kind: \"audio\" },\n oga: { mime: \"audio/ogg\", kind: \"audio\" },\n opus: { mime: \"audio/ogg\", kind: \"audio\" },\n weba: { mime: \"audio/webm\", kind: \"audio\" },\n};\n\nfunction extOf(fileName: string): string {\n return fileName.toLowerCase().split(\".\").pop() ?? \"\";\n}\n\n/** 文件名 → 可播放媒体的 MIME 与类别;不可(或浏览器不支持)返回 null */\nexport function mediaInfo(fileName: string): MediaInfo | null {\n return MEDIA_MIME[extOf(fileName)] ?? null;\n}\n\n/** 文件名 → MIME;非可播放媒体返回 null */\nexport function mediaMime(fileName: string): string | null {\n return MEDIA_MIME[extOf(fileName)]?.mime ?? null;\n}\n\n/** 文件名 → audio / video;非可播放媒体返回 null */\nexport function mediaKind(fileName: string): MediaKind | null {\n return MEDIA_MIME[extOf(fileName)]?.kind ?? null;\n}\n\n/**\n * 浏览器可**内嵌**渲染的文档 MIME。\n *\n * 与 `MEDIA_MIME` 分开维护:那张表的语义是「`<video>` / `<audio>` 能直接播放」,\n * PDF 不是音视频,混进去会让 `mediaKind()` 返回一个不存在的 kind。这里只声明\n * 「浏览器自带查看器能显示」,由前端决定用 `<iframe>` 还是保底提示。\n *\n * 注意浏览器支持面不是重点:Chrome / Edge / Firefox / Safari / WebView2 都有\n * 内置 PDF 查看器,WebKitGTK(Linux 桌面壳)没有——前端必须能优雅退化。\n */\nconst DOC_PREVIEW_MIME: Record<string, string> = {\n pdf: \"application/pdf\",\n};\n\n/** 文件名 → 可内嵌预览的文档 MIME;非文档返回 null */\nexport function docPreviewMime(fileName: string): string | null {\n return DOC_PREVIEW_MIME[extOf(fileName)] ?? null;\n}\n\n/**\n * 文件名 → 可用于 **HTTP Range 流式预览**的 MIME(音视频 + 可内嵌文档)。\n *\n * 节点签发文件票据 / `/files/range` 用它决定 `Content-Type`:票据里没有 MIME 时\n * 前端会直接放弃流式通道,退回整文件下载(大 PDF 尤其痛),所以 PDF 必须在这里\n * 被认出来。\n */\nexport function previewMime(fileName: string): string | null {\n return mediaMime(fileName) ?? docPreviewMime(fileName);\n}\n\nexport interface ByteRange {\n /** 起始偏移(含) */\n start: number;\n /** 结束偏移(含) */\n end: number;\n}\n\n/**\n * 解析 `Range: bytes=...`。\n *\n * @param header 原始头值(可为 null / 空)\n * @param size 文件总字节数\n * @returns 单段区间;`null` = 忽略该头(无 Range / 多段 / 语法不合法);\n * `\"unsatisfiable\"` = 范围越界,调用方应回 416。\n */\nexport function parseByteRange(\n header: string | null | undefined,\n size: number,\n): ByteRange | \"unsatisfiable\" | null {\n const raw = (header ?? \"\").trim();\n if (!raw) return null;\n const m = /^bytes=(\\d*)-(\\d*)$/i.exec(raw);\n if (!m) return null; // 多段与其它单位一律忽略\n\n const startRaw = m[1] ?? \"\";\n const endRaw = m[2] ?? \"\";\n if (!startRaw && !endRaw) return null; // \"bytes=-\"\n if (size <= 0) return \"unsatisfiable\";\n\n // 后缀形式:bytes=-N → 最后 N 字节\n if (!startRaw) {\n const suffix = Number(endRaw);\n if (!Number.isFinite(suffix) || suffix <= 0) return \"unsatisfiable\";\n return { start: Math.max(size - suffix, 0), end: size - 1 };\n }\n\n const start = Number(startRaw);\n if (!Number.isFinite(start) || start < 0) return null;\n if (start >= size) return \"unsatisfiable\";\n\n // 省略结束:bytes=N- → 到文件末尾\n if (!endRaw) return { start, end: size - 1 };\n\n const end = Number(endRaw);\n if (!Number.isFinite(end) || end < start) return null;\n return { start, end: Math.min(end, size - 1) };\n}\n\n/** Range 响应的 `Content-Range` 头值 */\nexport function contentRangeHeader(range: ByteRange, size: number): string {\n return `bytes ${range.start}-${range.end}/${size}`;\n}\n\n/**\n * 文件 ETag(强校验)。\n *\n * 由 size + mtime 组成:文件内容变化几乎必然改变其中一个;不读文件本身,\n * 因此 `HEAD` / `304` 可以在完全不触达节点的情况下应答。\n */\nexport function fileEtag(size: number, mtimeMs: number): string {\n return `\"${size.toString(16)}-${Math.trunc(mtimeMs).toString(16)}\"`;\n}\n","/**\n * 共享类型:通知事件与渠道配置\n *\n * 从 server/types.ts 抽出,供 core(节点)与 hub(控制面)共用的通知模块引用。\n */\n\n/** 通知种类 */\nexport type NotifyKind =\n | \"approval_request\" // 需要审批(data: approvalId / toolName / arguments / reason)\n | \"question_asked\" // 需要回答提问(data: questionId / kind / question / options / ...)\n | \"turn_completed\" // 一轮 Turn 正常完成(data: status / iterations / produced_files)\n | \"turn_stopped\" // 达到最大迭代次数等提前终止\n | \"error\" // 出错(data: error / retriable)\n /**\n * 会话状态变化(data: status)。\n *\n * 只承载状态机取值,不含消息/模型/token 等明细——跨节点总览只靠它同步,\n * 明细仍去会话详情页拉。与上面几种「需要人处理」的通知不同,它**不面向\n * 第三方渠道**(见 `channelWants`),也不弹提醒。\n */\n | \"session_status\"\n /**\n * 会话状态全量快照(data: `{ state }`,字段与 `GET /sessions/:id/state` 一致)。\n *\n * 控制台收到后直接替换本地快照,因此**不再周期性拉 `/state`**。\n * 只在状态机、待办、排队信息、草稿、名称真的变化时才发(工具执行期间不发)。\n * 与 `session_status` 一样只服务控制台,不面向第三方渠道。\n */\n | \"session_snapshot\"\n /**\n * 后台 shell 任务变化(data: `{ projectId, tasks }`,tasks 为精简字段)。\n *\n * 代替对 `GET /shell/tasks` 的轮询:任务启动 / 退出 / 被终止时推送一次。\n */\n | \"shell_tasks_changed\";\n\n/**\n * 通知事件:SessionManager 在审批挂起 / 提问挂起 / 完成 / 出错等关键点产出,\n * 交给通知中枢(NotificationHub / 节点桥)广播到多设备或第三方渠道。\n */\nexport interface NotifyEvent {\n kind: NotifyKind;\n sessionId: string;\n ts: string;\n data: Record<string, unknown>;\n}\n\n/** 通知渠道类型 */\nexport type NotificationChannelType =\n | \"mqtt\"\n | \"telegram\"\n | \"wecom\"\n | \"dingtalk\"\n | \"feishu\"\n | \"serverchan\"\n | \"pushplus\"\n | \"bark\"\n | \"email\"\n | \"sms\"\n | \"qq\"\n | \"webpush\"\n | \"webhook\";\n\n/** 通知渠道配置(敏感字段如 token / password 以密文存储) */\nexport interface NotificationChannelConfig {\n id: string;\n type: NotificationChannelType;\n enabled: boolean;\n config: Record<string, string>;\n events: {\n approval: boolean;\n question: boolean;\n completed: boolean;\n error: boolean;\n };\n}\n\n/**\n * 内容增量事件:逐 token 的高频帧。\n *\n * 控制台「折叠过程输出」时,这两类事件既不会被渲染,又占聊天流量的绝大部分,\n * 因此服务端按订阅参数 `detail=0` 直接在源头丢弃(SSE / 网关两条链路一致)。\n *\n * **改这里必须同步改 `gateway/src/codec.rs` 的 `CONTENT_DELTA_TYPES`**,\n * 否则两条链路会出现「一侧省了、另一侧还在发」。\n */\nexport const CONTENT_DELTA_TYPES: ReadonlySet<string> = new Set([\n \"assistant_delta\",\n \"reasoning_delta\",\n // 节点把「只含内容增量」的一批事件合并成 `{zqBatch:[...]}` 时用的顶层标记。\n // 混入结构事件的一批不能打这个标记,否则整帧被丢会连正文一起丢。\n \"content_delta\",\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): 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":["crypto","crypto","TIMEOUT_MS","TIMEOUT_MS","escapeHtml"]}
@@ -140,7 +140,7 @@ function notifyTitle(kind) {
140
140
  return "Agent \u901A\u77E5";
141
141
  }
142
142
  }
143
- function notifyBody(ev, maxLen = 300) {
143
+ function notifyBody(ev) {
144
144
  const d = ev.data;
145
145
  switch (ev.kind) {
146
146
  case "approval_request":
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/notify/index.ts","../../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"],"sourcesContent":["/**\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","/**\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"],"mappings":"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","names":["TIMEOUT_MS","TIMEOUT_MS","nodemailer","escapeHtml"]}
1
+ {"version":3,"sources":["../../src/notify/index.ts","../../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"],"sourcesContent":["/**\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","/**\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): 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"],"mappings":"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","names":["TIMEOUT_MS","TIMEOUT_MS","nodemailer","escapeHtml"]}
@@ -1,2 +1,2 @@
1
- export { B as BarkNotifier, E as EmailNotifier, b as NotificationHub, c as Notifier, S as ServerChanNotifier, W as WebhookNotifier, f as channelWants, g as createNotificationHub, n as notifyBody, h as notifyTitle } from '../index-DulV0GQp.cjs';
1
+ export { B as BarkNotifier, E as EmailNotifier, b as NotificationHub, c as Notifier, S as ServerChanNotifier, W as WebhookNotifier, f as channelWants, g as createNotificationHub, n as notifyBody, h as notifyTitle } from '../index-BrWxcNt4.cjs';
2
2
  import 'nodemailer';
@@ -1,2 +1,2 @@
1
- export { B as BarkNotifier, E as EmailNotifier, b as NotificationHub, c as Notifier, S as ServerChanNotifier, W as WebhookNotifier, f as channelWants, g as createNotificationHub, n as notifyBody, h as notifyTitle } from '../index-DulV0GQp.js';
1
+ export { B as BarkNotifier, E as EmailNotifier, b as NotificationHub, c as Notifier, S as ServerChanNotifier, W as WebhookNotifier, f as channelWants, g as createNotificationHub, n as notifyBody, h as notifyTitle } from '../index-BrWxcNt4.js';
2
2
  import 'nodemailer';
@@ -96,7 +96,7 @@ function notifyTitle(kind) {
96
96
  return "Agent \u901A\u77E5";
97
97
  }
98
98
  }
99
- function notifyBody(ev, maxLen = 300) {
99
+ function notifyBody(ev) {
100
100
  const d = ev.data;
101
101
  switch (ev.kind) {
102
102
  case "approval_request":
@@ -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 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"]}
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): 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zqagent/shared",
3
- "version": "0.3.11",
3
+ "version": "0.3.12",
4
4
  "description": "core 与 hub 共用的基础模块:节点管理协议、凭据加密、审批视图、通知渠道",
5
5
  "author": "成都志前科技有限公司",
6
6
  "license": "Apache-2.0",
@@ -96,7 +96,8 @@
96
96
  "build": "tsup",
97
97
  "dev": "tsup --watch",
98
98
  "typecheck": "tsc --noEmit",
99
+ "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
99
100
  "test": "vitest run",
100
- "test:coverage": "vitest run --coverage"
101
+ "test:coverage": "vitest run --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary --coverage.reporter=lcov --coverage.include='src/**'"
101
102
  }
102
103
  }