/** * 由一封邮件构造驱动 ZCode 的提示词。 * * 结构与 pi / dsh / opencode 三桥**同源**(复用 `lib/relay-policy.js` 的 * `inboundHeadline` 与 `replyInstruction`):同一封邮件派到不同 Agent 上, * 模型该看到同样的交代。措辞一旦在某个平台走样,就会出现「同一封邮件 * 在这个 Agent 上会回信、在那个 Agent 上装死」这种只在单平台复现的问题。 * * 两个关键信号都来自服务端,不由插件猜: * * - `from_human`:决定「插件会不会替你回信」。猜错的代价不对称 —— * 把 Agent 的来信说成人的来信,会让模型以为有人会替它开口而什么都不做; * 反之只是多调一次 send_mail。 * - `in_reply_to`:这封是回信还是新任务。模型分不清这两者时,会把对方一句 * 「已收到」当成新待办再做一遍(Agent↔Agent 客套循环的真正成因)。 */ import { inboundHeadline, replyInstruction } from '../lib/relay-policy.js'; import { normalizeMode, DEFAULT_MODE, MODE_PLAN, MODE_FULL } from '../lib/permission-mode.js'; /** * 告诉模型它的执行能力到底长什么样。 * * 不说清楚会发生两件都很糟的事:模型去调 `Bash` 然后拿到一个看不懂的 * 「工具不存在」(于是它反复换名字试,浪费整轮),或者它以为自己不能动手, * 把本来能做完的活写成「建议你自己执行」。 * * 同时要把**授权这件事本身**讲明白:`run_command`/`write_file` 会先向发件人 * 申请,被拒时工具会报错并给出原因。模型必须知道「被拒」是一个正常的、 * 需要它改道的结果,而不是重试同一个动作的理由。 */ export function capabilityNote(tier) { const t = normalizeMode(tier) || DEFAULT_MODE; const head = '## 你在这个平台上的执行能力\n\n' + 'ZCode 自带的 Bash / Write / Edit / js 等工具**已被禁用**(不是故障,是本平台的策略:' + '邮件驱动的会话里没有本地界面可以给人审批,所以平台不做权限判定,改由我们自己的门禁负责)。'; if (t === MODE_PLAN) { return ( head + '\n\n当前是 **plan 档**:只读。你**不能**执行命令或写文件(`run_command`/`write_file` 在' + '本档一律拒绝,不必尝试)。请把需要动手的步骤写进回信,说明「执行什么、为什么」。' ); } const gate = t === MODE_FULL ? '当前是 **full 档**:发件人已声明全权,你的执行类调用**直接生效,不会打扰任何人**。' : '当前是 **workspace 档**:每类执行动作**第一次调用会先向发件人申请授权**' + '(他可以在界面上选「同意」「一直同意」或「拒绝」)。'; return ( head + '\n\n需要动手时请改用这两个工具:\n' + '- `run_command`:执行一条 shell 命令(工作目录默认是本会话工作区)\n' + '- `write_file`:写一个文件(父目录自动创建;平台自身的部署/数据库/服务配置目录' + '无论谁批准都拒绝写入)\n' + '\n' + gate + '\n\n被拒绝时工具会**报错并给出原因**(含决策人)。这是正常的业务结果,不是故障:' + '不要重试同一个动作、也不要绕道去找其它执行手段,而应当在回信里说明' + '「哪一步被拒了、为什么需要它、希望对方怎么做」。\n' + '读写与搜索用自带的 Read / Glob / Grep(这些只读工具始终可用)。' ); } /** 去掉已有的 Re:/答复前缀,避免 `Re: Re: Re: …` 越滚越长。 */ export function replySubject(subject) { const base = String(subject ?? '') .replace(/^\s*(re|答复|回复)\s*[::]\s*/gi, '') .trim(); return base ? `Re: ${base}` : '本轮工作总结'; } /** * 一轮完全跑不起来时的回信正文。 * * **必须发这封信**:模型一次都没跑起来 → 会话里没有任何产出 → 自动转发什么也 * 不会发 → 发件人只会觉得「信发出去了,然后再无音讯」。邮件驱动的会话没有 * 本地界面可以让人看见错误,回信是唯一的出口。 * * 为什么不复用共用的 `renderFailureReport`:它的**建议**是平台特有的 * (「调整可用模型范围」),而 ZCode 跑不起来的常见成因是没登录、缺模型配置、 * CLI 本身报错 —— 照着那句建议去后台改模型范围,什么也解决不了。 * * @param {{kind: string, error: string}[]} failures * @param {string} subject */ export function renderTurnFailure(failures, subject) { const list = Array.isArray(failures) ? failures : []; const lines = [ `本次未能处理「${subject || '(无主题)'}」:ZCode 一轮都没能跑完。`, '', `已尝试 ${list.length} 次:`, '' ]; list.forEach((f, i) => { lines.push(`${i + 1}. **${f?.kind || '失败'}**`); lines.push(` ${String(f?.error ?? '未知错误').replace(/\n/g, '\n ')}`); }); lines.push(''); lines.push('常见成因(按可能性):'); lines.push('- **没有登录**:ZCode 的模型访问要 OAuth,未登录时 headless 会直接报缺模型配置;'); lines.push(' 在服务器上跑 `node login`,或直接开一次客户端扫码。'); lines.push('- **模型配置缺失**:`~/.zcode/cli/config.json` 里没有显式 provider。'); lines.push('- **CLI 路径不对**:`AGENTMAIL_ZCODE_CLI` 指向的 `zcode.cjs` 不存在或不可执行。'); lines.push('- **工作目录不可写**:ZCode 要在里面建会话状态。'); lines.push(''); lines.push('修好后可以重新把原邮件发一次,或直接回复这封信。'); return lines.join('\n'); } /** * @param {{agentName: string, data: any, kind?: string, reused?: boolean}} input * @returns {string} */ export function buildMailPrompt({ agentName, data, kind = 'mail', reused = false }) { // 权限结论不是「一封新邮件」,而是我们自己在等的那件事有了答复。 // 当成普通邮件处理,模型会去 read_inbox 找一封其实已经不需要读的信。 if (kind === 'permission') { return [ `你之前发起的权限请求已有结论:${data?.decision ?? '(未给出)'}` + `(决策人:${data?.decided_by || '用户'})。`, '请据此继续后续工作。' ].join('\n'); } const fromHuman = data?.from_human === true; const lines = [ inboundHeadline({ inReplyTo: data?.in_reply_to, fromHuman, catchup: data?.catchup, reused }), '', `发件人:${data?.from_name || 'unknown'}`, `主题:${data?.subject || '(无主题)'}`, `邮件 ID:${data?.mail_id || 'unknown'}` ]; if (data?.in_reply_to) lines.push(`回的是你那封:${data.in_reply_to}`); if (!reused) lines.push(`身份:你是 ${agentName}`); // 服务端算好的回信地址。带上它是因为模型**确实会**自己发信(抄送第三方、 // 分多封交代不同的事)。让它自己拼三维地址的话,`.new` 会被拼进去, // 于是回信静默开出一条新会话,原线索里再无下文。 if (data?.reply_address) lines.push(`回信地址:${data.reply_address}`); lines.push( '', '请先调用 read_inbox 读取完整正文(附带附件清单,如有附件可用 download_attachment 取回),', '然后处理其中的请求。', ...replyInstruction({ fromHuman, replyAddress: data?.reply_address }), '', capabilityNote(data?.permission_mode) ); return lines.join('\n'); }