@zqagent/shared 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +21 -2
- package/dist/approval-view.cjs +99 -0
- package/dist/approval-view.cjs.map +1 -0
- package/dist/approval-view.d.cts +39 -0
- package/dist/approval-view.d.ts +39 -0
- package/dist/approval-view.js +69 -0
- package/dist/approval-view.js.map +1 -0
- package/dist/fleet.cjs +263 -0
- package/dist/fleet.cjs.map +1 -0
- package/dist/fleet.d.cts +284 -0
- package/dist/fleet.d.ts +284 -0
- package/dist/fleet.js +214 -0
- package/dist/fleet.js.map +1 -0
- package/dist/hub-api.cjs +901 -0
- package/dist/hub-api.cjs.map +1 -0
- package/dist/hub-api.d.cts +1677 -0
- package/dist/hub-api.d.ts +1677 -0
- package/dist/hub-api.js +776 -0
- package/dist/hub-api.js.map +1 -0
- package/dist/index-DokiNlGJ.d.cts +182 -0
- package/dist/index-DokiNlGJ.d.ts +182 -0
- package/dist/index.cjs +861 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +779 -0
- package/dist/index.js.map +1 -0
- package/dist/llm-presets.cjs +92 -0
- package/dist/llm-presets.cjs.map +1 -0
- package/dist/llm-presets.d.cts +28 -0
- package/dist/llm-presets.d.ts +28 -0
- package/dist/llm-presets.js +66 -0
- package/dist/llm-presets.js.map +1 -0
- package/dist/media.cjs +111 -0
- package/dist/media.cjs.map +1 -0
- package/dist/media.d.cts +59 -0
- package/dist/media.d.ts +59 -0
- package/dist/media.js +79 -0
- package/dist/media.js.map +1 -0
- package/dist/messages-CiXjdR1Z.d.cts +257 -0
- package/dist/messages-CiXjdR1Z.d.ts +257 -0
- package/dist/model-capabilities.cjs +105 -0
- package/dist/model-capabilities.cjs.map +1 -0
- package/dist/model-capabilities.d.cts +46 -0
- package/dist/model-capabilities.d.ts +46 -0
- package/dist/model-capabilities.js +74 -0
- package/dist/model-capabilities.js.map +1 -0
- package/dist/notify/index.cjs +375 -0
- package/dist/notify/index.cjs.map +1 -0
- package/dist/notify/index.d.cts +2 -0
- package/dist/notify/index.d.ts +2 -0
- package/dist/notify/index.js +330 -0
- package/dist/notify/index.js.map +1 -0
- package/dist/secrets.cjs +121 -0
- package/dist/secrets.cjs.map +1 -0
- package/dist/secrets.d.cts +25 -0
- package/dist/secrets.d.ts +25 -0
- package/dist/secrets.js +82 -0
- package/dist/secrets.js.map +1 -0
- package/dist/wire/index.cjs +1163 -0
- package/dist/wire/index.cjs.map +1 -0
- package/dist/wire/index.d.cts +166 -0
- package/dist/wire/index.d.ts +166 -0
- package/dist/wire/index.js +1081 -0
- package/dist/wire/index.js.map +1 -0
- package/dist/wire/node.cjs +224 -0
- package/dist/wire/node.cjs.map +1 -0
- package/dist/wire/node.d.cts +14 -0
- package/dist/wire/node.d.ts +14 -0
- package/dist/wire/node.js +195 -0
- package/dist/wire/node.js.map +1 -0
- package/package.json +99 -3
|
@@ -0,0 +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, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\");\n}\n\n/** 渲染为 HTML 表格(邮件 / Web 端直接展示为真正的表格) */\nexport function approvalToHtmlTable(\n input: ApprovalViewInput,\n maxLen = APPROVAL_VALUE_MAX,\n): string {\n const rows = approvalFields(input, maxLen)\n .map(\n (f) =>\n `<tr>` +\n `<th style=\"text-align:left;padding:4px 12px 4px 0;color:#6b7280;font-weight:500;white-space:nowrap;vertical-align:top;\">${escapeHtml(f.label)}</th>` +\n `<td style=\"padding:4px 0;font-family:ui-monospace,Menlo,Consolas,monospace;\">${escapeHtml(f.value)}</td>` +\n `</tr>`,\n )\n .join(\"\");\n return `<table style=\"border-collapse:collapse;font-size:13px;\">${rows}</table>`;\n}\n","/**\n * 通知通道接口\n *\n * 每种通知渠道(Telegram / 企微 / 钉钉 / 飞书 / Bark / 邮件 / 短信 / …)\n * 实现本接口。NotificationHub 按租户配置把 NotifyEvent 分发到各通道。\n * 实现必须是「尽力而为」:发送失败应抛出异常由 Hub 捕获降级,或自行吞掉。\n */\nimport type {\n NotificationChannelConfig,\n NotificationChannelType,\n NotifyEvent,\n} from \"../types.js\";\nimport {\n approvalInputFromData,\n approvalToMarkdownTable,\n} from \"../approval-view.js\";\n\nexport interface Notifier {\n /** 通道类型标识 */\n readonly type: NotificationChannelType;\n\n /**\n * 发送一条通知。\n * @param config 该租户此通道的配置(明文态,敏感字段已由上层解密)\n * @param ev 结构化通知事件\n */\n send(config: NotificationChannelConfig, ev: NotifyEvent): Promise<void>;\n}\n\n/** 依据通知事件类型判断是否命中该通道的事件过滤开关 */\nexport function channelWants(channel: NotificationChannelConfig, kind: NotifyEvent[\"kind\"]): boolean {\n const ev = channel.events;\n switch (kind) {\n case \"approval_request\":\n return ev.approval;\n case \"question_asked\":\n return ev.question;\n case \"turn_completed\":\n case \"turn_stopped\":\n return ev.completed;\n case \"error\":\n return ev.error;\n case \"session_status\":\n // 纯状态同步:频率高、无用户动作,任何第三方渠道都不该收到\n return false;\n default:\n return true;\n }\n}\n\n/** 生成简短的通知标题(供各通道复用) */\nexport function notifyTitle(kind: NotifyEvent[\"kind\"]): string {\n switch (kind) {\n case \"approval_request\":\n return \"需要审批\";\n case \"question_asked\":\n return \"需要回答\";\n case \"turn_completed\":\n return \"任务完成\";\n case \"turn_stopped\":\n return \"达到迭代上限\";\n case \"error\":\n return \"任务出错\";\n default:\n return \"Agent 通知\";\n }\n}\n\n/** 生成简短的通知正文(供各通道复用,超长截断) */\nexport function notifyBody(ev: NotifyEvent, maxLen = 300): string {\n const d = ev.data;\n switch (ev.kind) {\n case \"approval_request\":\n return approvalToMarkdownTable(approvalInputFromData(d), {\n title: \"🔒 需要审批\",\n });\n case \"question_asked\":\n return String(d[\"question\"] ?? \"\");\n case \"turn_completed\":\n return `状态: ${String(d[\"status\"] ?? \"completed\")}`;\n case \"turn_stopped\":\n return \"已达最大迭代次数,可继续完成剩下的任务\";\n case \"error\":\n return String(d[\"error\"] ?? \"未知错误\");\n default:\n return \"\";\n }\n}\n","/**\n * NotificationHub — 通知中枢\n *\n * 注册各通知通道(Notifier),把 SessionManager 产出的 NotifyEvent\n * 按租户配置分发到启用的通道。发送是「尽力而为」,异常降级不冒泡。\n */\nimport type {\n Notifier,\n} from \"./Notifier.js\";\nimport { channelWants } from \"./Notifier.js\";\nimport type {\n NotificationChannelConfig,\n NotificationChannelType,\n NotifyEvent,\n} from \"../types.js\";\n\nexport class NotificationHub {\n private readonly notifiers = new Map<NotificationChannelType, Notifier>();\n\n /** 注册一个通道实现(幂等,后注册覆盖) */\n register(notifier: Notifier): void {\n this.notifiers.set(notifier.type, notifier);\n }\n\n /** 已注册的通道类型 */\n registeredTypes(): NotificationChannelType[] {\n return [...this.notifiers.keys()];\n }\n\n /**\n * 分发通知到给定的启用通道。\n * 单个通道失败仅记录,不影响其他通道与核心逻辑。\n */\n async dispatch(channels: NotificationChannelConfig[], ev: NotifyEvent): Promise<void> {\n for (const ch of channels) {\n if (!ch.enabled) continue;\n if (!channelWants(ch, ev.kind)) continue;\n const notifier = this.notifiers.get(ch.type);\n if (!notifier) continue;\n try {\n await notifier.send(ch, ev);\n } catch (e) {\n console.log(\n `[notify] ${ch.type} 发送失败: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n }\n }\n}\n","/**\n * Bark 通知通道(iOS)\n *\n * Bark 是一个 iOS 推送 App,只需一个 HTTP 请求即可推送。\n * config: { key: \"设备推送Key\", server?: \"https://api.day.app\" }\n */\nimport type { Notifier } from \"../Notifier.js\";\nimport { notifyTitle, notifyBody } from \"../Notifier.js\";\nimport type {\n NotificationChannelConfig,\n NotifyEvent,\n} from \"../../types.js\";\n\nconst TIMEOUT_MS = 10_000;\n\nexport class BarkNotifier implements Notifier {\n readonly type = \"bark\" as const;\n\n async send(config: NotificationChannelConfig, ev: NotifyEvent): Promise<void> {\n const key = config.config[\"key\"];\n if (!key) return;\n const server = (config.config[\"server\"] ?? \"https://api.day.app\").replace(/\\/+$/, \"\");\n const url =\n `${server}/${encodeURIComponent(key)}/` +\n `${encodeURIComponent(notifyTitle(ev.kind))}/${encodeURIComponent(notifyBody(ev))}`;\n const res = await fetch(url, {\n method: \"GET\",\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n }\n}\n","/**\n * Server 酱通知通道(微信服务号推送,国内常用)\n *\n * config: { sendkey: \"SCT 开头的 SendKey\" }\n */\nimport type { Notifier } from \"../Notifier.js\";\nimport { notifyTitle, notifyBody } from \"../Notifier.js\";\nimport type {\n NotificationChannelConfig,\n NotifyEvent,\n} from \"../../types.js\";\n\nconst TIMEOUT_MS = 10_000;\n\nexport class ServerChanNotifier implements Notifier {\n readonly type = \"serverchan\" as const;\n\n async send(config: NotificationChannelConfig, ev: NotifyEvent): Promise<void> {\n const sendkey = config.config[\"sendkey\"];\n if (!sendkey) return;\n const url = `https://sctapi.ftqq.com/${encodeURIComponent(sendkey)}.send`;\n const body = new URLSearchParams({\n title: notifyTitle(ev.kind),\n desp: notifyBody(ev),\n });\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body,\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n }\n}\n","/**\n * 通用 Webhook 通知通道\n *\n * 向用户配置的 URL POST JSON,作为自定义兜底(可对接任意中转/网关)。\n * config: { url: \"https://...\" }\n * payload: { title, body, kind, sessionId, ts, data }\n */\nimport type { Notifier } from \"../Notifier.js\";\nimport { notifyTitle, notifyBody } from \"../Notifier.js\";\nimport type {\n NotificationChannelConfig,\n NotifyEvent,\n} from \"../../types.js\";\n\nconst TIMEOUT_MS = 10_000;\n\nexport class WebhookNotifier implements Notifier {\n readonly type = \"webhook\" as const;\n\n async send(config: NotificationChannelConfig, ev: NotifyEvent): Promise<void> {\n const url = config.config[\"url\"];\n if (!url) return;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n title: notifyTitle(ev.kind),\n body: notifyBody(ev),\n kind: ev.kind,\n sessionId: ev.sessionId,\n ts: ev.ts,\n data: ev.data,\n }),\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n }\n}\n","/**\n * 邮件通知通道(通用 SMTP)\n *\n * 通过 SMTP 发送任务完成 / 审批 / 提问 / 错误等通知。兼容 QQ 邮箱\n * (smtp.qq.com:465 SSL + 授权码)、163、Gmail 等任意 SMTP 服务。\n *\n * config: {\n * host: \"smtp.qq.com\",\n * port: \"465\", // 省略时按 secure 推断 465/587\n * secure: \"true\", // \"true\"=SSL,\"false\"=STARTTLS\n * user: \"you@qq.com\",\n * pass: \"授权码\", // 密文落盘\n * from: \"you@qq.com\", // 省略时取 user\n * to: \"you@qq.com\", // 可多个逗号分隔\n * subjectPrefix: \"[zqagent]\" // 可选,默认 [zqagent]\n * }\n */\nimport nodemailer, { type Transporter } from \"nodemailer\";\nimport type { Notifier } from \"../Notifier.js\";\nimport { notifyTitle } from \"../Notifier.js\";\nimport {\n approvalInputFromData,\n approvalToHtmlTable,\n approvalToMarkdownTable,\n} from \"../../approval-view.js\";\nimport type {\n NotificationChannelConfig,\n NotifyEvent,\n} from \"../../types.js\";\n\n/** SMTP 连接参数(透传给 nodemailer.createTransport) */\nexport interface SmtpOptions {\n host: string;\n port: number;\n secure: boolean;\n auth?: { user: string; pass: string };\n}\n\n/** 可注入的 transport 工厂(测试时替换为 fake) */\nexport type TransportFactory = (opts: SmtpOptions) => Transporter;\n\n/** 默认工厂:创建真实 nodemailer transport */\nexport function defaultTransportFactory(opts: SmtpOptions): Transporter {\n return nodemailer.createTransport(opts);\n}\n\n/** 按事件类型生成纯文本正文(turn_completed 时附产出文件与摘要) */\nfunction buildText(ev: NotifyEvent): string {\n const d = ev.data;\n switch (ev.kind) {\n case \"turn_completed\":\n case \"turn_stopped\": {\n const lines: string[] = [];\n lines.push(`会话: ${ev.sessionId}`);\n lines.push(`状态: ${String(d[\"status\"] ?? ev.kind)}`);\n if (d[\"iterations\"] !== undefined)\n lines.push(`迭代次数: ${String(d[\"iterations\"])}`);\n const files = d[\"produced_files\"] as string[] | undefined;\n if (Array.isArray(files) && files.length > 0)\n lines.push(`产出文件:\\n ${files.join(\"\\n \")}`);\n const summary = typeof d[\"summary\"] === \"string\" ? d[\"summary\"] : \"\";\n if (summary.trim()) lines.push(`\\n摘要:\\n${summary}`);\n return lines.join(\"\\n\");\n }\n case \"approval_request\":\n return `${approvalToMarkdownTable(approvalInputFromData(d), {\n title: \"🔒 需要审批\",\n })}\\n会话: ${ev.sessionId}`;\n case \"question_asked\":\n return `问题: ${String(d[\"question\"] ?? \"\")}\\n会话: ${ev.sessionId}`;\n case \"error\":\n return `错误: ${String(d[\"error\"] ?? \"未知错误\")}\\n会话: ${ev.sessionId}`;\n default:\n return `会话: ${ev.sessionId}\\n时间: ${ev.ts}`;\n }\n}\n\n/** HTML 转义 */\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\");\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACwBO,IAAM,qBAAqB;AAG3B,IAAM,mBAAmB,CAAC,gBAAM,cAAI;AAG3C,IAAM,aAAa;AACnB,IAAM,eAAe;AAGrB,SAAS,YAAY,GAAY,QAAwB;AACvD,MAAI;AACJ,MAAI,KAAK,MAAM;AACb,WAAO;AAAA,EACT,WAAW,OAAO,MAAM,UAAU;AAChC,WAAO;AAAA,EACT,OAAO;AACL,QAAI;AACF,aAAO,KAAK,UAAU,CAAC;AAAA,IACzB,QAAQ;AACN,aAAO,OAAO,CAAC;AAAA,IACjB;AAAA,EACF;AACA,SAAO,KAAK,QAAQ,UAAU,QAAG;AACjC,SAAO,KAAK,SAAS,SAAS,KAAK,MAAM,GAAG,MAAM,IAAI,WAAM;AAC9D;AAGO,SAAS,sBACd,MACmB;AACnB,QAAM,OAAO,KAAK,WAAW;AAC7B,SAAO;AAAA,IACL,UAAU,OAAO,KAAK,UAAU,KAAK,EAAE;AAAA,IACvC,QAAQ,OAAO,KAAK,QAAQ,KAAK,EAAE;AAAA,IACnC,WACE,QAAQ,OAAO,SAAS,WACnB,OACD;AAAA,EACR;AACF;AAKO,SAAS,eACd,OACA,SAAS,oBACQ;AACjB,QAAM,SAA0B;AAAA,IAC9B,EAAE,OAAO,YAAY,OAAO,YAAY,MAAM,UAAU,MAAM,EAAE;AAAA,IAChE,EAAE,OAAO,cAAc,OAAO,YAAY,MAAM,QAAQ,MAAM,EAAE;AAAA,EAClE;AACA,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,aAAa,CAAC,CAAC,GAAG;AAChE,WAAO,KAAK,EAAE,OAAO,YAAY,KAAK,MAAM,GAAG,OAAO,YAAY,OAAO,MAAM,EAAE,CAAC;AAAA,EACpF;AACA,SAAO;AACT;AAGA,SAAS,WAAW,MAAsB;AACxC,SAAO,KAAK,QAAQ,OAAO,KAAK,EAAE,QAAQ,UAAU,QAAG;AACzD;AAGO,SAAS,wBACd,OACA,OAA4C,CAAC,GACrC;AACR,QAAM,QAAkB,CAAC;AACzB,MAAI,KAAK,MAAO,OAAM,KAAK,KAAK,OAAO,EAAE;AACzC,QAAM,KAAK,KAAK,iBAAiB,CAAC,CAAC,MAAM,iBAAiB,CAAC,CAAC,MAAM,eAAe;AACjF,aAAW,KAAK,eAAe,OAAO,KAAK,MAAM,GAAG;AAClD,UAAM,KAAK,KAAK,WAAW,EAAE,KAAK,CAAC,MAAM,WAAW,EAAE,KAAK,CAAC,IAAI;AAAA,EAClE;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAGA,SAAS,WAAW,MAAsB;AACxC,SAAO,KACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM;AACzB;AAGO,SAAS,oBACd,OACA,SAAS,oBACD;AACR,QAAM,OAAO,eAAe,OAAO,MAAM,EACtC;AAAA,IACC,CAAC,MACC,+HAC2H,WAAW,EAAE,KAAK,CAAC,qFAC9D,WAAW,EAAE,KAAK,CAAC;AAAA,EAEvG,EACC,KAAK,EAAE;AACV,SAAO,2DAA2D,IAAI;AACxE;;;AC/FO,SAAS,aAAa,SAAoC,MAAoC;AACnG,QAAM,KAAK,QAAQ;AACnB,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,GAAG;AAAA,IACZ,KAAK;AACH,aAAO,GAAG;AAAA,IACZ,KAAK;AAAA,IACL,KAAK;AACH,aAAO,GAAG;AAAA,IACZ,KAAK;AACH,aAAO,GAAG;AAAA,IACZ,KAAK;AAEH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAGO,SAAS,YAAY,MAAmC;AAC7D,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAGO,SAAS,WAAW,IAAiB,SAAS,KAAa;AAChE,QAAM,IAAI,GAAG;AACb,UAAQ,GAAG,MAAM;AAAA,IACf,KAAK;AACH,aAAO,wBAAwB,sBAAsB,CAAC,GAAG;AAAA,QACvD,OAAO;AAAA,MACT,CAAC;AAAA,IACH,KAAK;AACH,aAAO,OAAO,EAAE,UAAU,KAAK,EAAE;AAAA,IACnC,KAAK;AACH,aAAO,iBAAO,OAAO,EAAE,QAAQ,KAAK,WAAW,CAAC;AAAA,IAClD,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,OAAO,EAAE,OAAO,KAAK,0BAAM;AAAA,IACpC;AACE,aAAO;AAAA,EACX;AACF;;;ACvEO,IAAM,kBAAN,MAAsB;AAAA,EACV,YAAY,oBAAI,IAAuC;AAAA;AAAA,EAGxE,SAAS,UAA0B;AACjC,SAAK,UAAU,IAAI,SAAS,MAAM,QAAQ;AAAA,EAC5C;AAAA;AAAA,EAGA,kBAA6C;AAC3C,WAAO,CAAC,GAAG,KAAK,UAAU,KAAK,CAAC;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,SAAS,UAAuC,IAAgC;AACpF,eAAW,MAAM,UAAU;AACzB,UAAI,CAAC,GAAG,QAAS;AACjB,UAAI,CAAC,aAAa,IAAI,GAAG,IAAI,EAAG;AAChC,YAAM,WAAW,KAAK,UAAU,IAAI,GAAG,IAAI;AAC3C,UAAI,CAAC,SAAU;AACf,UAAI;AACF,cAAM,SAAS,KAAK,IAAI,EAAE;AAAA,MAC5B,SAAS,GAAG;AACV,gBAAQ;AAAA,UACN,YAAY,GAAG,IAAI,8BAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;AAAA,QACzE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;ACnCA,IAAM,aAAa;AAEZ,IAAM,eAAN,MAAuC;AAAA,EACnC,OAAO;AAAA,EAEhB,MAAM,KAAK,QAAmC,IAAgC;AAC5E,UAAM,MAAM,OAAO,OAAO,KAAK;AAC/B,QAAI,CAAC,IAAK;AACV,UAAM,UAAU,OAAO,OAAO,QAAQ,KAAK,uBAAuB,QAAQ,QAAQ,EAAE;AACpF,UAAM,MACJ,GAAG,MAAM,IAAI,mBAAmB,GAAG,CAAC,IACjC,mBAAmB,YAAY,GAAG,IAAI,CAAC,CAAC,IAAI,mBAAmB,WAAW,EAAE,CAAC,CAAC;AACnF,UAAM,MAAM,MAAM,MAAM,KAAK;AAAA,MAC3B,QAAQ;AAAA,MACR,QAAQ,YAAY,QAAQ,UAAU;AAAA,IACxC,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,QAAQ,IAAI,MAAM,EAAE;AAAA,EACnD;AACF;;;ACnBA,IAAMA,cAAa;AAEZ,IAAM,qBAAN,MAA6C;AAAA,EACzC,OAAO;AAAA,EAEhB,MAAM,KAAK,QAAmC,IAAgC;AAC5E,UAAM,UAAU,OAAO,OAAO,SAAS;AACvC,QAAI,CAAC,QAAS;AACd,UAAM,MAAM,2BAA2B,mBAAmB,OAAO,CAAC;AAClE,UAAM,OAAO,IAAI,gBAAgB;AAAA,MAC/B,OAAO,YAAY,GAAG,IAAI;AAAA,MAC1B,MAAM,WAAW,EAAE;AAAA,IACrB,CAAC;AACD,UAAM,MAAM,MAAM,MAAM,KAAK;AAAA,MAC3B,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,MAC/D;AAAA,MACA,QAAQ,YAAY,QAAQA,WAAU;AAAA,IACxC,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,QAAQ,IAAI,MAAM,EAAE;AAAA,EACnD;AACF;;;ACnBA,IAAMC,cAAa;AAEZ,IAAM,kBAAN,MAA0C;AAAA,EACtC,OAAO;AAAA,EAEhB,MAAM,KAAK,QAAmC,IAAgC;AAC5E,UAAM,MAAM,OAAO,OAAO,KAAK;AAC/B,QAAI,CAAC,IAAK;AACV,UAAM,MAAM,MAAM,MAAM,KAAK;AAAA,MAC3B,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU;AAAA,QACnB,OAAO,YAAY,GAAG,IAAI;AAAA,QAC1B,MAAM,WAAW,EAAE;AAAA,QACnB,MAAM,GAAG;AAAA,QACT,WAAW,GAAG;AAAA,QACd,IAAI,GAAG;AAAA,QACP,MAAM,GAAG;AAAA,MACX,CAAC;AAAA,MACD,QAAQ,YAAY,QAAQA,WAAU;AAAA,IACxC,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,QAAQ,IAAI,MAAM,EAAE;AAAA,EACnD;AACF;;;ACpBA,wBAA6C;AAyBtC,SAAS,wBAAwB,MAAgC;AACtE,SAAO,kBAAAC,QAAW,gBAAgB,IAAI;AACxC;AAGA,SAAS,UAAU,IAAyB;AAC1C,QAAM,IAAI,GAAG;AACb,UAAQ,GAAG,MAAM;AAAA,IACf,KAAK;AAAA,IACL,KAAK,gBAAgB;AACnB,YAAM,QAAkB,CAAC;AACzB,YAAM,KAAK,iBAAO,GAAG,SAAS,EAAE;AAChC,YAAM,KAAK,iBAAO,OAAO,EAAE,QAAQ,KAAK,GAAG,IAAI,CAAC,EAAE;AAClD,UAAI,EAAE,YAAY,MAAM;AACtB,cAAM,KAAK,6BAAS,OAAO,EAAE,YAAY,CAAC,CAAC,EAAE;AAC/C,YAAM,QAAQ,EAAE,gBAAgB;AAChC,UAAI,MAAM,QAAQ,KAAK,KAAK,MAAM,SAAS;AACzC,cAAM,KAAK;AAAA,IAAY,MAAM,KAAK,MAAM,CAAC,EAAE;AAC7C,YAAM,UAAU,OAAO,EAAE,SAAS,MAAM,WAAW,EAAE,SAAS,IAAI;AAClE,UAAI,QAAQ,KAAK,EAAG,OAAM,KAAK;AAAA;AAAA,EAAU,OAAO,EAAE;AAClD,aAAO,MAAM,KAAK,IAAI;AAAA,IACxB;AAAA,IACA,KAAK;AACH,aAAO,GAAG,wBAAwB,sBAAsB,CAAC,GAAG;AAAA,QAC1D,OAAO;AAAA,MACT,CAAC,CAAC;AAAA,gBAAS,GAAG,SAAS;AAAA,IACzB,KAAK;AACH,aAAO,iBAAO,OAAO,EAAE,UAAU,KAAK,EAAE,CAAC;AAAA,gBAAS,GAAG,SAAS;AAAA,IAChE,KAAK;AACH,aAAO,iBAAO,OAAO,EAAE,OAAO,KAAK,0BAAM,CAAC;AAAA,gBAAS,GAAG,SAAS;AAAA,IACjE;AACE,aAAO,iBAAO,GAAG,SAAS;AAAA,gBAAS,GAAG,EAAE;AAAA,EAC5C;AACF;AAGA,SAASC,YAAW,MAAsB;AACxC,SAAO,KACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM;AACzB;AAGA,SAAS,UAAU,IAAyB;AAC1C,MAAI,GAAG,SAAS,oBAAoB;AAClC,UAAM,QAAQ,oBAAoB,sBAAsB,GAAG,IAAI,CAAC;AAChE,WACE,QACA,0EAAgEA,YAAW,GAAG,SAAS,CAAC;AAAA,EAE5F;AACA,SAAO,0DAA0DA,YAAW,UAAU,EAAE,CAAC,CAAC;AAC5F;AAEO,IAAM,gBAAN,MAAwC;AAAA,EAG7C,YACmB,mBAAqC,yBACtD;AADiB;AAAA,EAChB;AAAA,EADgB;AAAA,EAHV,OAAO;AAAA,EAMhB,MAAM,KAAK,QAAmC,IAAgC;AAG5E,QAAI,GAAG,SAAS,kBAAkB;AAChC,YAAM,SAAS,OAAO,OAAO,OAAO,oBAAoB,KAAK,EAAE;AAC/D,YAAM,aAAa,GAAG,KAAK,YAAY;AACvC,UAAI,OAAO,eAAe,YAAY,aAAa,SAAS,KAAM;AAChE;AAAA,MACF;AAAA,IACF;AAEA,UAAM,OAAO,OAAO,OAAO,MAAM;AACjC,UAAM,KAAK,OAAO,OAAO,IAAI;AAC7B,QAAI,CAAC,QAAQ,CAAC,GAAI;AAElB,UAAM,SAAS,OAAO,OAAO,QAAQ,MAAM;AAC3C,UAAM,OACJ,OAAO,OAAO,OAAO,MAAM,CAAC,MAAM,SAAS,MAAM;AACnD,UAAM,OAAO,OAAO,OAAO,MAAM,KAAK;AACtC,UAAM,OAAO,OAAO,OAAO,MAAM,KAAK;AACtC,UAAM,OAAO,OAAO,OAAO,MAAM,KAAK;AACtC,QAAI,CAAC,KAAM;AAEX,UAAM,SAAS,OAAO,OAAO,eAAe,KAAK;AACjD,UAAM,YAAY,KAAK,iBAAiB;AAAA,MACtC;AAAA,MACA;AAAA,MACA;AAAA,MACA,MAAM,OAAO,EAAE,MAAM,KAAK,IAAI;AAAA,IAChC,CAAC;AAED,QAAI;AACF,YAAM,UAAU,SAAS;AAAA,QACvB;AAAA,QACA;AAAA,QACA,SAAS,GAAG,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;AAAA,QACzC,MAAM,UAAU,EAAE;AAAA,QAClB,MAAM,UAAU,EAAE;AAAA,MACpB,CAAC;AAAA,IACH,UAAE;AAEA,UAAI;AACF,kBAAU,MAAM;AAAA,MAClB,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;;;APjIO,SAAS,wBAAyC;AACvD,QAAM,MAAM,IAAI,gBAAgB;AAChC,MAAI,SAAS,IAAI,aAAa,CAAC;AAC/B,MAAI,SAAS,IAAI,mBAAmB,CAAC;AACrC,MAAI,SAAS,IAAI,gBAAgB,CAAC;AAClC,MAAI,SAAS,IAAI,cAAc,CAAC;AAChC,SAAO;AACT;","names":["TIMEOUT_MS","TIMEOUT_MS","nodemailer","escapeHtml"]}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// src/approval-view.ts
|
|
2
|
+
var APPROVAL_VALUE_MAX = 120;
|
|
3
|
+
var APPROVAL_COLUMNS = ["\u9879\u76EE", "\u8BE6\u60C5"];
|
|
4
|
+
var TOOL_LABEL = "\u{1F527} \u5DE5\u5177";
|
|
5
|
+
var REASON_LABEL = "\u{1F4AC} \u539F\u56E0";
|
|
6
|
+
function formatValue(v, maxLen) {
|
|
7
|
+
let text;
|
|
8
|
+
if (v == null) {
|
|
9
|
+
text = "";
|
|
10
|
+
} else if (typeof v === "string") {
|
|
11
|
+
text = v;
|
|
12
|
+
} else {
|
|
13
|
+
try {
|
|
14
|
+
text = JSON.stringify(v);
|
|
15
|
+
} catch {
|
|
16
|
+
text = String(v);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
text = text.replace(/\r?\n/g, "\u21B5");
|
|
20
|
+
return text.length > maxLen ? text.slice(0, maxLen) + "\u2026" : text;
|
|
21
|
+
}
|
|
22
|
+
function approvalInputFromData(data) {
|
|
23
|
+
const args = data["arguments"];
|
|
24
|
+
return {
|
|
25
|
+
toolName: String(data["toolName"] ?? ""),
|
|
26
|
+
reason: String(data["reason"] ?? ""),
|
|
27
|
+
arguments: args && typeof args === "object" ? args : void 0
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function approvalFields(input, maxLen = APPROVAL_VALUE_MAX) {
|
|
31
|
+
const fields = [
|
|
32
|
+
{ label: TOOL_LABEL, value: formatValue(input.toolName, maxLen) },
|
|
33
|
+
{ label: REASON_LABEL, value: formatValue(input.reason, maxLen) }
|
|
34
|
+
];
|
|
35
|
+
for (const [key, value] of Object.entries(input.arguments ?? {})) {
|
|
36
|
+
fields.push({ label: formatValue(key, maxLen), value: formatValue(value, maxLen) });
|
|
37
|
+
}
|
|
38
|
+
return fields;
|
|
39
|
+
}
|
|
40
|
+
function escapeCell(text) {
|
|
41
|
+
return text.replace(/\|/g, "\\|").replace(/\r?\n/g, "\u21B5");
|
|
42
|
+
}
|
|
43
|
+
function approvalToMarkdownTable(input, opts = {}) {
|
|
44
|
+
const lines = [];
|
|
45
|
+
if (opts.title) lines.push(opts.title, "");
|
|
46
|
+
lines.push(`| ${APPROVAL_COLUMNS[0]} | ${APPROVAL_COLUMNS[1]} |`, "| --- | --- |");
|
|
47
|
+
for (const f of approvalFields(input, opts.maxLen)) {
|
|
48
|
+
lines.push(`| ${escapeCell(f.label)} | ${escapeCell(f.value)} |`);
|
|
49
|
+
}
|
|
50
|
+
return lines.join("\n");
|
|
51
|
+
}
|
|
52
|
+
function escapeHtml(text) {
|
|
53
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
54
|
+
}
|
|
55
|
+
function approvalToHtmlTable(input, maxLen = APPROVAL_VALUE_MAX) {
|
|
56
|
+
const rows = approvalFields(input, maxLen).map(
|
|
57
|
+
(f) => `<tr><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><td style="padding:4px 0;font-family:ui-monospace,Menlo,Consolas,monospace;">${escapeHtml(f.value)}</td></tr>`
|
|
58
|
+
).join("");
|
|
59
|
+
return `<table style="border-collapse:collapse;font-size:13px;">${rows}</table>`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/notify/Notifier.ts
|
|
63
|
+
function channelWants(channel, kind) {
|
|
64
|
+
const ev = channel.events;
|
|
65
|
+
switch (kind) {
|
|
66
|
+
case "approval_request":
|
|
67
|
+
return ev.approval;
|
|
68
|
+
case "question_asked":
|
|
69
|
+
return ev.question;
|
|
70
|
+
case "turn_completed":
|
|
71
|
+
case "turn_stopped":
|
|
72
|
+
return ev.completed;
|
|
73
|
+
case "error":
|
|
74
|
+
return ev.error;
|
|
75
|
+
case "session_status":
|
|
76
|
+
return false;
|
|
77
|
+
default:
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function notifyTitle(kind) {
|
|
82
|
+
switch (kind) {
|
|
83
|
+
case "approval_request":
|
|
84
|
+
return "\u9700\u8981\u5BA1\u6279";
|
|
85
|
+
case "question_asked":
|
|
86
|
+
return "\u9700\u8981\u56DE\u7B54";
|
|
87
|
+
case "turn_completed":
|
|
88
|
+
return "\u4EFB\u52A1\u5B8C\u6210";
|
|
89
|
+
case "turn_stopped":
|
|
90
|
+
return "\u8FBE\u5230\u8FED\u4EE3\u4E0A\u9650";
|
|
91
|
+
case "error":
|
|
92
|
+
return "\u4EFB\u52A1\u51FA\u9519";
|
|
93
|
+
default:
|
|
94
|
+
return "Agent \u901A\u77E5";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function notifyBody(ev, maxLen = 300) {
|
|
98
|
+
const d = ev.data;
|
|
99
|
+
switch (ev.kind) {
|
|
100
|
+
case "approval_request":
|
|
101
|
+
return approvalToMarkdownTable(approvalInputFromData(d), {
|
|
102
|
+
title: "\u{1F512} \u9700\u8981\u5BA1\u6279"
|
|
103
|
+
});
|
|
104
|
+
case "question_asked":
|
|
105
|
+
return String(d["question"] ?? "");
|
|
106
|
+
case "turn_completed":
|
|
107
|
+
return `\u72B6\u6001: ${String(d["status"] ?? "completed")}`;
|
|
108
|
+
case "turn_stopped":
|
|
109
|
+
return "\u5DF2\u8FBE\u6700\u5927\u8FED\u4EE3\u6B21\u6570\uFF0C\u53EF\u7EE7\u7EED\u5B8C\u6210\u5269\u4E0B\u7684\u4EFB\u52A1";
|
|
110
|
+
case "error":
|
|
111
|
+
return String(d["error"] ?? "\u672A\u77E5\u9519\u8BEF");
|
|
112
|
+
default:
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/notify/NotificationHub.ts
|
|
118
|
+
var NotificationHub = class {
|
|
119
|
+
notifiers = /* @__PURE__ */ new Map();
|
|
120
|
+
/** 注册一个通道实现(幂等,后注册覆盖) */
|
|
121
|
+
register(notifier) {
|
|
122
|
+
this.notifiers.set(notifier.type, notifier);
|
|
123
|
+
}
|
|
124
|
+
/** 已注册的通道类型 */
|
|
125
|
+
registeredTypes() {
|
|
126
|
+
return [...this.notifiers.keys()];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 分发通知到给定的启用通道。
|
|
130
|
+
* 单个通道失败仅记录,不影响其他通道与核心逻辑。
|
|
131
|
+
*/
|
|
132
|
+
async dispatch(channels, ev) {
|
|
133
|
+
for (const ch of channels) {
|
|
134
|
+
if (!ch.enabled) continue;
|
|
135
|
+
if (!channelWants(ch, ev.kind)) continue;
|
|
136
|
+
const notifier = this.notifiers.get(ch.type);
|
|
137
|
+
if (!notifier) continue;
|
|
138
|
+
try {
|
|
139
|
+
await notifier.send(ch, ev);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.log(
|
|
142
|
+
`[notify] ${ch.type} \u53D1\u9001\u5931\u8D25: ${e instanceof Error ? e.message : String(e)}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// src/notify/notifiers/BarkNotifier.ts
|
|
150
|
+
var TIMEOUT_MS = 1e4;
|
|
151
|
+
var BarkNotifier = class {
|
|
152
|
+
type = "bark";
|
|
153
|
+
async send(config, ev) {
|
|
154
|
+
const key = config.config["key"];
|
|
155
|
+
if (!key) return;
|
|
156
|
+
const server = (config.config["server"] ?? "https://api.day.app").replace(/\/+$/, "");
|
|
157
|
+
const url = `${server}/${encodeURIComponent(key)}/${encodeURIComponent(notifyTitle(ev.kind))}/${encodeURIComponent(notifyBody(ev))}`;
|
|
158
|
+
const res = await fetch(url, {
|
|
159
|
+
method: "GET",
|
|
160
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
161
|
+
});
|
|
162
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// src/notify/notifiers/ServerChanNotifier.ts
|
|
167
|
+
var TIMEOUT_MS2 = 1e4;
|
|
168
|
+
var ServerChanNotifier = class {
|
|
169
|
+
type = "serverchan";
|
|
170
|
+
async send(config, ev) {
|
|
171
|
+
const sendkey = config.config["sendkey"];
|
|
172
|
+
if (!sendkey) return;
|
|
173
|
+
const url = `https://sctapi.ftqq.com/${encodeURIComponent(sendkey)}.send`;
|
|
174
|
+
const body = new URLSearchParams({
|
|
175
|
+
title: notifyTitle(ev.kind),
|
|
176
|
+
desp: notifyBody(ev)
|
|
177
|
+
});
|
|
178
|
+
const res = await fetch(url, {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
181
|
+
body,
|
|
182
|
+
signal: AbortSignal.timeout(TIMEOUT_MS2)
|
|
183
|
+
});
|
|
184
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// src/notify/notifiers/WebhookNotifier.ts
|
|
189
|
+
var TIMEOUT_MS3 = 1e4;
|
|
190
|
+
var WebhookNotifier = class {
|
|
191
|
+
type = "webhook";
|
|
192
|
+
async send(config, ev) {
|
|
193
|
+
const url = config.config["url"];
|
|
194
|
+
if (!url) return;
|
|
195
|
+
const res = await fetch(url, {
|
|
196
|
+
method: "POST",
|
|
197
|
+
headers: { "Content-Type": "application/json" },
|
|
198
|
+
body: JSON.stringify({
|
|
199
|
+
title: notifyTitle(ev.kind),
|
|
200
|
+
body: notifyBody(ev),
|
|
201
|
+
kind: ev.kind,
|
|
202
|
+
sessionId: ev.sessionId,
|
|
203
|
+
ts: ev.ts,
|
|
204
|
+
data: ev.data
|
|
205
|
+
}),
|
|
206
|
+
signal: AbortSignal.timeout(TIMEOUT_MS3)
|
|
207
|
+
});
|
|
208
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// src/notify/notifiers/EmailNotifier.ts
|
|
213
|
+
import nodemailer from "nodemailer";
|
|
214
|
+
function defaultTransportFactory(opts) {
|
|
215
|
+
return nodemailer.createTransport(opts);
|
|
216
|
+
}
|
|
217
|
+
function buildText(ev) {
|
|
218
|
+
const d = ev.data;
|
|
219
|
+
switch (ev.kind) {
|
|
220
|
+
case "turn_completed":
|
|
221
|
+
case "turn_stopped": {
|
|
222
|
+
const lines = [];
|
|
223
|
+
lines.push(`\u4F1A\u8BDD: ${ev.sessionId}`);
|
|
224
|
+
lines.push(`\u72B6\u6001: ${String(d["status"] ?? ev.kind)}`);
|
|
225
|
+
if (d["iterations"] !== void 0)
|
|
226
|
+
lines.push(`\u8FED\u4EE3\u6B21\u6570: ${String(d["iterations"])}`);
|
|
227
|
+
const files = d["produced_files"];
|
|
228
|
+
if (Array.isArray(files) && files.length > 0)
|
|
229
|
+
lines.push(`\u4EA7\u51FA\u6587\u4EF6:
|
|
230
|
+
${files.join("\n ")}`);
|
|
231
|
+
const summary = typeof d["summary"] === "string" ? d["summary"] : "";
|
|
232
|
+
if (summary.trim()) lines.push(`
|
|
233
|
+
\u6458\u8981:
|
|
234
|
+
${summary}`);
|
|
235
|
+
return lines.join("\n");
|
|
236
|
+
}
|
|
237
|
+
case "approval_request":
|
|
238
|
+
return `${approvalToMarkdownTable(approvalInputFromData(d), {
|
|
239
|
+
title: "\u{1F512} \u9700\u8981\u5BA1\u6279"
|
|
240
|
+
})}
|
|
241
|
+
\u4F1A\u8BDD: ${ev.sessionId}`;
|
|
242
|
+
case "question_asked":
|
|
243
|
+
return `\u95EE\u9898: ${String(d["question"] ?? "")}
|
|
244
|
+
\u4F1A\u8BDD: ${ev.sessionId}`;
|
|
245
|
+
case "error":
|
|
246
|
+
return `\u9519\u8BEF: ${String(d["error"] ?? "\u672A\u77E5\u9519\u8BEF")}
|
|
247
|
+
\u4F1A\u8BDD: ${ev.sessionId}`;
|
|
248
|
+
default:
|
|
249
|
+
return `\u4F1A\u8BDD: ${ev.sessionId}
|
|
250
|
+
\u65F6\u95F4: ${ev.ts}`;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function escapeHtml2(text) {
|
|
254
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
255
|
+
}
|
|
256
|
+
function buildHtml(ev) {
|
|
257
|
+
if (ev.kind === "approval_request") {
|
|
258
|
+
const table = approvalToHtmlTable(approvalInputFromData(ev.data));
|
|
259
|
+
return table + `<p style="color:#6b7280;font-size:12px;margin:10px 0 0;">\u4F1A\u8BDD: ${escapeHtml2(ev.sessionId)}</p>`;
|
|
260
|
+
}
|
|
261
|
+
return `<pre style="white-space:pre-wrap;font-family:inherit;">${escapeHtml2(buildText(ev))}</pre>`;
|
|
262
|
+
}
|
|
263
|
+
var EmailNotifier = class {
|
|
264
|
+
constructor(transportFactory = defaultTransportFactory) {
|
|
265
|
+
this.transportFactory = transportFactory;
|
|
266
|
+
}
|
|
267
|
+
transportFactory;
|
|
268
|
+
type = "email";
|
|
269
|
+
async send(config, ev) {
|
|
270
|
+
if (ev.kind === "turn_completed") {
|
|
271
|
+
const minSec = Number(config.config["minDurationSeconds"] ?? 60);
|
|
272
|
+
const durationMs = ev.data["durationMs"];
|
|
273
|
+
if (typeof durationMs === "number" && durationMs < minSec * 1e3) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const host = config.config["host"];
|
|
278
|
+
const to = config.config["to"];
|
|
279
|
+
if (!host || !to) return;
|
|
280
|
+
const secure = config.config["secure"] === "true";
|
|
281
|
+
const port = Number(config.config["port"]) || (secure ? 465 : 587);
|
|
282
|
+
const user = config.config["user"] ?? "";
|
|
283
|
+
const pass = config.config["pass"] ?? "";
|
|
284
|
+
const from = config.config["from"] ?? user;
|
|
285
|
+
if (!from) return;
|
|
286
|
+
const prefix = config.config["subjectPrefix"] ?? "[zqagent]";
|
|
287
|
+
const transport = this.transportFactory({
|
|
288
|
+
host,
|
|
289
|
+
port,
|
|
290
|
+
secure,
|
|
291
|
+
auth: user ? { user, pass } : void 0
|
|
292
|
+
});
|
|
293
|
+
try {
|
|
294
|
+
await transport.sendMail({
|
|
295
|
+
from,
|
|
296
|
+
to,
|
|
297
|
+
subject: `${prefix}${notifyTitle(ev.kind)}`,
|
|
298
|
+
text: buildText(ev),
|
|
299
|
+
html: buildHtml(ev)
|
|
300
|
+
});
|
|
301
|
+
} finally {
|
|
302
|
+
try {
|
|
303
|
+
transport.close();
|
|
304
|
+
} catch {
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// src/notify/index.ts
|
|
311
|
+
function createNotificationHub() {
|
|
312
|
+
const hub = new NotificationHub();
|
|
313
|
+
hub.register(new BarkNotifier());
|
|
314
|
+
hub.register(new ServerChanNotifier());
|
|
315
|
+
hub.register(new WebhookNotifier());
|
|
316
|
+
hub.register(new EmailNotifier());
|
|
317
|
+
return hub;
|
|
318
|
+
}
|
|
319
|
+
export {
|
|
320
|
+
BarkNotifier,
|
|
321
|
+
EmailNotifier,
|
|
322
|
+
NotificationHub,
|
|
323
|
+
ServerChanNotifier,
|
|
324
|
+
WebhookNotifier,
|
|
325
|
+
channelWants,
|
|
326
|
+
createNotificationHub,
|
|
327
|
+
notifyBody,
|
|
328
|
+
notifyTitle
|
|
329
|
+
};
|
|
330
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\");\n}\n\n/** 渲染为 HTML 表格(邮件 / Web 端直接展示为真正的表格) */\nexport function approvalToHtmlTable(\n input: ApprovalViewInput,\n maxLen = APPROVAL_VALUE_MAX,\n): string {\n const rows = approvalFields(input, maxLen)\n .map(\n (f) =>\n `<tr>` +\n `<th style=\"text-align:left;padding:4px 12px 4px 0;color:#6b7280;font-weight:500;white-space:nowrap;vertical-align:top;\">${escapeHtml(f.label)}</th>` +\n `<td style=\"padding:4px 0;font-family:ui-monospace,Menlo,Consolas,monospace;\">${escapeHtml(f.value)}</td>` +\n `</tr>`,\n )\n .join(\"\");\n return `<table style=\"border-collapse:collapse;font-size:13px;\">${rows}</table>`;\n}\n","/**\n * 通知通道接口\n *\n * 每种通知渠道(Telegram / 企微 / 钉钉 / 飞书 / Bark / 邮件 / 短信 / …)\n * 实现本接口。NotificationHub 按租户配置把 NotifyEvent 分发到各通道。\n * 实现必须是「尽力而为」:发送失败应抛出异常由 Hub 捕获降级,或自行吞掉。\n */\nimport type {\n NotificationChannelConfig,\n NotificationChannelType,\n NotifyEvent,\n} from \"../types.js\";\nimport {\n approvalInputFromData,\n approvalToMarkdownTable,\n} from \"../approval-view.js\";\n\nexport interface Notifier {\n /** 通道类型标识 */\n readonly type: NotificationChannelType;\n\n /**\n * 发送一条通知。\n * @param config 该租户此通道的配置(明文态,敏感字段已由上层解密)\n * @param ev 结构化通知事件\n */\n send(config: NotificationChannelConfig, ev: NotifyEvent): Promise<void>;\n}\n\n/** 依据通知事件类型判断是否命中该通道的事件过滤开关 */\nexport function channelWants(channel: NotificationChannelConfig, kind: NotifyEvent[\"kind\"]): boolean {\n const ev = channel.events;\n switch (kind) {\n case \"approval_request\":\n return ev.approval;\n case \"question_asked\":\n return ev.question;\n case \"turn_completed\":\n case \"turn_stopped\":\n return ev.completed;\n case \"error\":\n return ev.error;\n case \"session_status\":\n // 纯状态同步:频率高、无用户动作,任何第三方渠道都不该收到\n return false;\n default:\n return true;\n }\n}\n\n/** 生成简短的通知标题(供各通道复用) */\nexport function notifyTitle(kind: NotifyEvent[\"kind\"]): string {\n switch (kind) {\n case \"approval_request\":\n return \"需要审批\";\n case \"question_asked\":\n return \"需要回答\";\n case \"turn_completed\":\n return \"任务完成\";\n case \"turn_stopped\":\n return \"达到迭代上限\";\n case \"error\":\n return \"任务出错\";\n default:\n return \"Agent 通知\";\n }\n}\n\n/** 生成简短的通知正文(供各通道复用,超长截断) */\nexport function notifyBody(ev: NotifyEvent, maxLen = 300): string {\n const d = ev.data;\n switch (ev.kind) {\n case \"approval_request\":\n return approvalToMarkdownTable(approvalInputFromData(d), {\n title: \"🔒 需要审批\",\n });\n case \"question_asked\":\n return String(d[\"question\"] ?? \"\");\n case \"turn_completed\":\n return `状态: ${String(d[\"status\"] ?? \"completed\")}`;\n case \"turn_stopped\":\n return \"已达最大迭代次数,可继续完成剩下的任务\";\n case \"error\":\n return String(d[\"error\"] ?? \"未知错误\");\n default:\n return \"\";\n }\n}\n","/**\n * NotificationHub — 通知中枢\n *\n * 注册各通知通道(Notifier),把 SessionManager 产出的 NotifyEvent\n * 按租户配置分发到启用的通道。发送是「尽力而为」,异常降级不冒泡。\n */\nimport type {\n Notifier,\n} from \"./Notifier.js\";\nimport { channelWants } from \"./Notifier.js\";\nimport type {\n NotificationChannelConfig,\n NotificationChannelType,\n NotifyEvent,\n} from \"../types.js\";\n\nexport class NotificationHub {\n private readonly notifiers = new Map<NotificationChannelType, Notifier>();\n\n /** 注册一个通道实现(幂等,后注册覆盖) */\n register(notifier: Notifier): void {\n this.notifiers.set(notifier.type, notifier);\n }\n\n /** 已注册的通道类型 */\n registeredTypes(): NotificationChannelType[] {\n return [...this.notifiers.keys()];\n }\n\n /**\n * 分发通知到给定的启用通道。\n * 单个通道失败仅记录,不影响其他通道与核心逻辑。\n */\n async dispatch(channels: NotificationChannelConfig[], ev: NotifyEvent): Promise<void> {\n for (const ch of channels) {\n if (!ch.enabled) continue;\n if (!channelWants(ch, ev.kind)) continue;\n const notifier = this.notifiers.get(ch.type);\n if (!notifier) continue;\n try {\n await notifier.send(ch, ev);\n } catch (e) {\n console.log(\n `[notify] ${ch.type} 发送失败: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n }\n }\n}\n","/**\n * Bark 通知通道(iOS)\n *\n * Bark 是一个 iOS 推送 App,只需一个 HTTP 请求即可推送。\n * config: { key: \"设备推送Key\", server?: \"https://api.day.app\" }\n */\nimport type { Notifier } from \"../Notifier.js\";\nimport { notifyTitle, notifyBody } from \"../Notifier.js\";\nimport type {\n NotificationChannelConfig,\n NotifyEvent,\n} from \"../../types.js\";\n\nconst TIMEOUT_MS = 10_000;\n\nexport class BarkNotifier implements Notifier {\n readonly type = \"bark\" as const;\n\n async send(config: NotificationChannelConfig, ev: NotifyEvent): Promise<void> {\n const key = config.config[\"key\"];\n if (!key) return;\n const server = (config.config[\"server\"] ?? \"https://api.day.app\").replace(/\\/+$/, \"\");\n const url =\n `${server}/${encodeURIComponent(key)}/` +\n `${encodeURIComponent(notifyTitle(ev.kind))}/${encodeURIComponent(notifyBody(ev))}`;\n const res = await fetch(url, {\n method: \"GET\",\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n }\n}\n","/**\n * Server 酱通知通道(微信服务号推送,国内常用)\n *\n * config: { sendkey: \"SCT 开头的 SendKey\" }\n */\nimport type { Notifier } from \"../Notifier.js\";\nimport { notifyTitle, notifyBody } from \"../Notifier.js\";\nimport type {\n NotificationChannelConfig,\n NotifyEvent,\n} from \"../../types.js\";\n\nconst TIMEOUT_MS = 10_000;\n\nexport class ServerChanNotifier implements Notifier {\n readonly type = \"serverchan\" as const;\n\n async send(config: NotificationChannelConfig, ev: NotifyEvent): Promise<void> {\n const sendkey = config.config[\"sendkey\"];\n if (!sendkey) return;\n const url = `https://sctapi.ftqq.com/${encodeURIComponent(sendkey)}.send`;\n const body = new URLSearchParams({\n title: notifyTitle(ev.kind),\n desp: notifyBody(ev),\n });\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body,\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n }\n}\n","/**\n * 通用 Webhook 通知通道\n *\n * 向用户配置的 URL POST JSON,作为自定义兜底(可对接任意中转/网关)。\n * config: { url: \"https://...\" }\n * payload: { title, body, kind, sessionId, ts, data }\n */\nimport type { Notifier } from \"../Notifier.js\";\nimport { notifyTitle, notifyBody } from \"../Notifier.js\";\nimport type {\n NotificationChannelConfig,\n NotifyEvent,\n} from \"../../types.js\";\n\nconst TIMEOUT_MS = 10_000;\n\nexport class WebhookNotifier implements Notifier {\n readonly type = \"webhook\" as const;\n\n async send(config: NotificationChannelConfig, ev: NotifyEvent): Promise<void> {\n const url = config.config[\"url\"];\n if (!url) return;\n const res = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n title: notifyTitle(ev.kind),\n body: notifyBody(ev),\n kind: ev.kind,\n sessionId: ev.sessionId,\n ts: ev.ts,\n data: ev.data,\n }),\n signal: AbortSignal.timeout(TIMEOUT_MS),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n }\n}\n","/**\n * 邮件通知通道(通用 SMTP)\n *\n * 通过 SMTP 发送任务完成 / 审批 / 提问 / 错误等通知。兼容 QQ 邮箱\n * (smtp.qq.com:465 SSL + 授权码)、163、Gmail 等任意 SMTP 服务。\n *\n * config: {\n * host: \"smtp.qq.com\",\n * port: \"465\", // 省略时按 secure 推断 465/587\n * secure: \"true\", // \"true\"=SSL,\"false\"=STARTTLS\n * user: \"you@qq.com\",\n * pass: \"授权码\", // 密文落盘\n * from: \"you@qq.com\", // 省略时取 user\n * to: \"you@qq.com\", // 可多个逗号分隔\n * subjectPrefix: \"[zqagent]\" // 可选,默认 [zqagent]\n * }\n */\nimport nodemailer, { type Transporter } from \"nodemailer\";\nimport type { Notifier } from \"../Notifier.js\";\nimport { notifyTitle } from \"../Notifier.js\";\nimport {\n approvalInputFromData,\n approvalToHtmlTable,\n approvalToMarkdownTable,\n} from \"../../approval-view.js\";\nimport type {\n NotificationChannelConfig,\n NotifyEvent,\n} from \"../../types.js\";\n\n/** SMTP 连接参数(透传给 nodemailer.createTransport) */\nexport interface SmtpOptions {\n host: string;\n port: number;\n secure: boolean;\n auth?: { user: string; pass: string };\n}\n\n/** 可注入的 transport 工厂(测试时替换为 fake) */\nexport type TransportFactory = (opts: SmtpOptions) => Transporter;\n\n/** 默认工厂:创建真实 nodemailer transport */\nexport function defaultTransportFactory(opts: SmtpOptions): Transporter {\n return nodemailer.createTransport(opts);\n}\n\n/** 按事件类型生成纯文本正文(turn_completed 时附产出文件与摘要) */\nfunction buildText(ev: NotifyEvent): string {\n const d = ev.data;\n switch (ev.kind) {\n case \"turn_completed\":\n case \"turn_stopped\": {\n const lines: string[] = [];\n lines.push(`会话: ${ev.sessionId}`);\n lines.push(`状态: ${String(d[\"status\"] ?? ev.kind)}`);\n if (d[\"iterations\"] !== undefined)\n lines.push(`迭代次数: ${String(d[\"iterations\"])}`);\n const files = d[\"produced_files\"] as string[] | undefined;\n if (Array.isArray(files) && files.length > 0)\n lines.push(`产出文件:\\n ${files.join(\"\\n \")}`);\n const summary = typeof d[\"summary\"] === \"string\" ? d[\"summary\"] : \"\";\n if (summary.trim()) lines.push(`\\n摘要:\\n${summary}`);\n return lines.join(\"\\n\");\n }\n case \"approval_request\":\n return `${approvalToMarkdownTable(approvalInputFromData(d), {\n title: \"🔒 需要审批\",\n })}\\n会话: ${ev.sessionId}`;\n case \"question_asked\":\n return `问题: ${String(d[\"question\"] ?? \"\")}\\n会话: ${ev.sessionId}`;\n case \"error\":\n return `错误: ${String(d[\"error\"] ?? \"未知错误\")}\\n会话: ${ev.sessionId}`;\n default:\n return `会话: ${ev.sessionId}\\n时间: ${ev.ts}`;\n }\n}\n\n/** HTML 转义 */\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\");\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/dist/secrets.cjs
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/secrets.ts
|
|
31
|
+
var secrets_exports = {};
|
|
32
|
+
__export(secrets_exports, {
|
|
33
|
+
SecretBox: () => SecretBox,
|
|
34
|
+
decryptWithKey: () => decryptWithKey,
|
|
35
|
+
encryptWithKey: () => encryptWithKey,
|
|
36
|
+
loadOrCreateMasterKey: () => loadOrCreateMasterKey,
|
|
37
|
+
masterKeyFromHex: () => masterKeyFromHex
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(secrets_exports);
|
|
40
|
+
var crypto = __toESM(require("crypto"), 1);
|
|
41
|
+
var fs = __toESM(require("fs"), 1);
|
|
42
|
+
var path = __toESM(require("path"), 1);
|
|
43
|
+
function encryptWithKey(key, plain) {
|
|
44
|
+
const iv = crypto.randomBytes(12);
|
|
45
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
46
|
+
const enc = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
|
47
|
+
const tag = cipher.getAuthTag();
|
|
48
|
+
return [
|
|
49
|
+
"v1",
|
|
50
|
+
iv.toString("base64"),
|
|
51
|
+
tag.toString("base64"),
|
|
52
|
+
enc.toString("base64")
|
|
53
|
+
].join(":");
|
|
54
|
+
}
|
|
55
|
+
function decryptWithKey(key, payload) {
|
|
56
|
+
try {
|
|
57
|
+
const parts = payload.split(":");
|
|
58
|
+
if (parts.length !== 4 || parts[0] !== "v1") return null;
|
|
59
|
+
const iv = Buffer.from(parts[1], "base64");
|
|
60
|
+
const tag = Buffer.from(parts[2], "base64");
|
|
61
|
+
const data = Buffer.from(parts[3], "base64");
|
|
62
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
63
|
+
decipher.setAuthTag(tag);
|
|
64
|
+
return Buffer.concat([
|
|
65
|
+
decipher.update(data),
|
|
66
|
+
decipher.final()
|
|
67
|
+
]).toString("utf8");
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function loadOrCreateMasterKey(masterKeyFile) {
|
|
73
|
+
if (fs.existsSync(masterKeyFile)) {
|
|
74
|
+
const hex = fs.readFileSync(masterKeyFile, "utf8").trim();
|
|
75
|
+
if (/^[0-9a-f]{64}$/i.test(hex)) {
|
|
76
|
+
return Buffer.from(hex, "hex");
|
|
77
|
+
}
|
|
78
|
+
console.warn(`\u26A0\uFE0F \u4E3B\u5BC6\u94A5\u6587\u4EF6\u635F\u574F\uFF0C\u5DF2\u91CD\u65B0\u751F\u6210\uFF08\u65E7\u5BC6\u6587\u5C06\u65E0\u6CD5\u89E3\u5BC6\uFF09`);
|
|
79
|
+
}
|
|
80
|
+
const key = crypto.randomBytes(32);
|
|
81
|
+
fs.mkdirSync(path.dirname(masterKeyFile), { recursive: true, mode: 448 });
|
|
82
|
+
fs.writeFileSync(masterKeyFile, key.toString("hex"), {
|
|
83
|
+
mode: 384,
|
|
84
|
+
encoding: "utf8"
|
|
85
|
+
});
|
|
86
|
+
return key;
|
|
87
|
+
}
|
|
88
|
+
function masterKeyFromHex(hex) {
|
|
89
|
+
const h = hex.trim();
|
|
90
|
+
return /^[0-9a-f]{64}$/i.test(h) ? Buffer.from(h, "hex") : null;
|
|
91
|
+
}
|
|
92
|
+
var SecretBox = class _SecretBox {
|
|
93
|
+
constructor(key) {
|
|
94
|
+
this.key = key;
|
|
95
|
+
}
|
|
96
|
+
key;
|
|
97
|
+
/** 主密钥来源一:本机密钥文件(不存在则创建) */
|
|
98
|
+
static fromFile(masterKeyFile) {
|
|
99
|
+
return new _SecretBox(loadOrCreateMasterKey(masterKeyFile));
|
|
100
|
+
}
|
|
101
|
+
/** 主密钥来源二:hex 字符串(如环境变量 SECRET_KEY);非法返回 null */
|
|
102
|
+
static fromHex(hex) {
|
|
103
|
+
const key = masterKeyFromHex(hex);
|
|
104
|
+
return key ? new _SecretBox(key) : null;
|
|
105
|
+
}
|
|
106
|
+
encrypt(plain) {
|
|
107
|
+
return encryptWithKey(this.key, plain);
|
|
108
|
+
}
|
|
109
|
+
decrypt(payload) {
|
|
110
|
+
return decryptWithKey(this.key, payload);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
114
|
+
0 && (module.exports = {
|
|
115
|
+
SecretBox,
|
|
116
|
+
decryptWithKey,
|
|
117
|
+
encryptWithKey,
|
|
118
|
+
loadOrCreateMasterKey,
|
|
119
|
+
masterKeyFromHex
|
|
120
|
+
});
|
|
121
|
+
//# sourceMappingURL=secrets.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/secrets.ts"],"sourcesContent":["/**\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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,aAAwB;AACxB,SAAoB;AACpB,WAAsB;AAGf,SAAS,eAAe,KAAa,OAAuB;AACjE,QAAM,KAAY,mBAAY,EAAE;AAChC,QAAM,SAAgB,sBAAe,eAAe,KAAK,EAAE;AAC3D,QAAM,MAAM,OAAO,OAAO,CAAC,OAAO,OAAO,OAAO,MAAM,GAAG,OAAO,MAAM,CAAC,CAAC;AACxE,QAAM,MAAM,OAAO,WAAW;AAC9B,SAAO;AAAA,IACL;AAAA,IACA,GAAG,SAAS,QAAQ;AAAA,IACpB,IAAI,SAAS,QAAQ;AAAA,IACrB,IAAI,SAAS,QAAQ;AAAA,EACvB,EAAE,KAAK,GAAG;AACZ;AAGO,SAAS,eAAe,KAAa,SAAgC;AAC1E,MAAI;AACF,UAAM,QAAQ,QAAQ,MAAM,GAAG;AAC/B,QAAI,MAAM,WAAW,KAAK,MAAM,CAAC,MAAM,KAAM,QAAO;AACpD,UAAM,KAAK,OAAO,KAAK,MAAM,CAAC,GAAI,QAAQ;AAC1C,UAAM,MAAM,OAAO,KAAK,MAAM,CAAC,GAAI,QAAQ;AAC3C,UAAM,OAAO,OAAO,KAAK,MAAM,CAAC,GAAI,QAAQ;AAC5C,UAAM,WAAkB,wBAAiB,eAAe,KAAK,EAAE;AAC/D,aAAS,WAAW,GAAG;AACvB,WAAO,OAAO,OAAO;AAAA,MACnB,SAAS,OAAO,IAAI;AAAA,MACpB,SAAS,MAAM;AAAA,IACjB,CAAC,EAAE,SAAS,MAAM;AAAA,EACpB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,sBAAsB,eAA+B;AACnE,MAAO,cAAW,aAAa,GAAG;AAChC,UAAM,MAAS,gBAAa,eAAe,MAAM,EAAE,KAAK;AACxD,QAAI,kBAAkB,KAAK,GAAG,GAAG;AAC/B,aAAO,OAAO,KAAK,KAAK,KAAK;AAAA,IAC/B;AACA,YAAQ,KAAK,0JAA6B;AAAA,EAC5C;AACA,QAAM,MAAa,mBAAY,EAAE;AACjC,EAAG,aAAe,aAAQ,aAAa,GAAG,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AAC1E,EAAG,iBAAc,eAAe,IAAI,SAAS,KAAK,GAAG;AAAA,IACnD,MAAM;AAAA,IACN,UAAU;AAAA,EACZ,CAAC;AACD,SAAO;AACT;AAGO,SAAS,iBAAiB,KAA4B;AAC3D,QAAM,IAAI,IAAI,KAAK;AACnB,SAAO,kBAAkB,KAAK,CAAC,IAAI,OAAO,KAAK,GAAG,KAAK,IAAI;AAC7D;AAOO,IAAM,YAAN,MAAM,WAAU;AAAA,EACrB,YAA6B,KAAa;AAAb;AAAA,EAAc;AAAA,EAAd;AAAA;AAAA,EAG7B,OAAO,SAAS,eAAkC;AAChD,WAAO,IAAI,WAAU,sBAAsB,aAAa,CAAC;AAAA,EAC3D;AAAA;AAAA,EAGA,OAAO,QAAQ,KAA+B;AAC5C,UAAM,MAAM,iBAAiB,GAAG;AAChC,WAAO,MAAM,IAAI,WAAU,GAAG,IAAI;AAAA,EACpC;AAAA,EAEA,QAAQ,OAAuB;AAC7B,WAAO,eAAe,KAAK,KAAK,KAAK;AAAA,EACvC;AAAA,EAEA,QAAQ,SAAgC;AACtC,WAAO,eAAe,KAAK,KAAK,OAAO;AAAA,EACzC;AACF;","names":[]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** 用给定主密钥做 AES-256-GCM 加密,输出 `v1:iv:tag:ciphertext`(base64) */
|
|
2
|
+
declare function encryptWithKey(key: Buffer, plain: string): string;
|
|
3
|
+
/** 用给定主密钥解密;格式非法 / 认证失败返回 null */
|
|
4
|
+
declare function decryptWithKey(key: Buffer, payload: string): string | null;
|
|
5
|
+
/** 读取或生成主密钥(32 字节随机,权限 600)。文件损坏则重建。 */
|
|
6
|
+
declare function loadOrCreateMasterKey(masterKeyFile: string): Buffer;
|
|
7
|
+
/** 从 64 位 hex 字符串解析主密钥;非法返回 null */
|
|
8
|
+
declare function masterKeyFromHex(hex: string): Buffer | null;
|
|
9
|
+
/**
|
|
10
|
+
* 密文工具:持有固定主密钥,对字符串做加解密。
|
|
11
|
+
* CLI 与 Server 各自构造实例(CLI 用 ~/.config/zqagent/secret.key,
|
|
12
|
+
* Server 用 DATA_DIR/secret.key 或 SECRET_KEY 环境变量)。
|
|
13
|
+
*/
|
|
14
|
+
declare class SecretBox {
|
|
15
|
+
private readonly key;
|
|
16
|
+
constructor(key: Buffer);
|
|
17
|
+
/** 主密钥来源一:本机密钥文件(不存在则创建) */
|
|
18
|
+
static fromFile(masterKeyFile: string): SecretBox;
|
|
19
|
+
/** 主密钥来源二:hex 字符串(如环境变量 SECRET_KEY);非法返回 null */
|
|
20
|
+
static fromHex(hex: string): SecretBox | null;
|
|
21
|
+
encrypt(plain: string): string;
|
|
22
|
+
decrypt(payload: string): string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { SecretBox, decryptWithKey, encryptWithKey, loadOrCreateMasterKey, masterKeyFromHex };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** 用给定主密钥做 AES-256-GCM 加密,输出 `v1:iv:tag:ciphertext`(base64) */
|
|
2
|
+
declare function encryptWithKey(key: Buffer, plain: string): string;
|
|
3
|
+
/** 用给定主密钥解密;格式非法 / 认证失败返回 null */
|
|
4
|
+
declare function decryptWithKey(key: Buffer, payload: string): string | null;
|
|
5
|
+
/** 读取或生成主密钥(32 字节随机,权限 600)。文件损坏则重建。 */
|
|
6
|
+
declare function loadOrCreateMasterKey(masterKeyFile: string): Buffer;
|
|
7
|
+
/** 从 64 位 hex 字符串解析主密钥;非法返回 null */
|
|
8
|
+
declare function masterKeyFromHex(hex: string): Buffer | null;
|
|
9
|
+
/**
|
|
10
|
+
* 密文工具:持有固定主密钥,对字符串做加解密。
|
|
11
|
+
* CLI 与 Server 各自构造实例(CLI 用 ~/.config/zqagent/secret.key,
|
|
12
|
+
* Server 用 DATA_DIR/secret.key 或 SECRET_KEY 环境变量)。
|
|
13
|
+
*/
|
|
14
|
+
declare class SecretBox {
|
|
15
|
+
private readonly key;
|
|
16
|
+
constructor(key: Buffer);
|
|
17
|
+
/** 主密钥来源一:本机密钥文件(不存在则创建) */
|
|
18
|
+
static fromFile(masterKeyFile: string): SecretBox;
|
|
19
|
+
/** 主密钥来源二:hex 字符串(如环境变量 SECRET_KEY);非法返回 null */
|
|
20
|
+
static fromHex(hex: string): SecretBox | null;
|
|
21
|
+
encrypt(plain: string): string;
|
|
22
|
+
decrypt(payload: string): string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { SecretBox, decryptWithKey, encryptWithKey, loadOrCreateMasterKey, masterKeyFromHex };
|