/** * 收件箱渲染与已读策略 —— 所有平台插件共用。 * * 提到 lib/ 是因为这几条规则每一条都对应过一次真实的错误行为,而它们与 * 平台 SDK 无关:无论 opencode 的 zod 工具还是 DSH 的 defineTool, * 渲染出的文本与标记已读的时机都该一致。新接一个平台时直接复用这里。 */ /** 人类可读的字节数,用于附件清单展示。 */ export function formatSize(n) { if (typeof n !== 'number' || !Number.isFinite(n)) return '?'; if (n < 1024) return `${n} B`; if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; return `${(n / 1024 / 1024).toFixed(1)} MB`; } /** * 把一封邮件渲染成模型可读的文本块。 * * @param {any} m `/mail/inbox` 返回的一封邮件 * @param {number} bodyLimit 正文截断长度 * @returns {string} */ export function renderMail(m, bodyLimit = 200) { const lines = [ `[${m?.status ?? 'unknown'}] ${m?.from_name ?? 'unknown'}: ${m?.subject ?? '(无主题)'}`, `邮件 ID: ${m?.mail_id ?? 'unknown'}`, `会话: #${m?.session_alias || '未命名'}`, ]; // 抄送要显示:一封邮件为什么同时到了几个人手上,只有抄送能解释。 // 不显示的话模型会以为这是私下发给它一个人的,回信时漏掉其他参与方。 if (Array.isArray(m?.cc_list) && m.cc_list.length > 0) { lines.push('抄送: ' + m.cc_list.map(c => c?.raw || c?.name || '?').join('、')); } // **必须给出 attachment_id**:只说「有附件」模型就无从下载。 if (Array.isArray(m?.attachments) && m.attachments.length > 0) { lines.push( '附件: ' + m.attachments .map(a => `${a?.filename ?? '?'}(${formatSize(a?.size_bytes)}, id=${a?.attachment_id ?? '?'})`) .join('、') ); lines.push('下载附件请用 download_attachment 工具。'); } // 列表接口只给 body_preview(省带宽),单封接口才有 body。两者都兜住。 const body = m?.body_preview || m?.body || ''; lines.push(`内容: ${String(body).slice(0, bodyLimit)}`); return lines.join('\n'); } /** * 渲染整个收件箱。 * @param {any[]} mails * @param {number} bodyLimit * @returns {string} */ export function renderInbox(mails, bodyLimit = 200) { const list = Array.isArray(mails) ? mails : []; if (list.length === 0) return '收件箱为空。'; return list.map(m => renderMail(m, bodyLimit)).join('\n\n'); } /** * 判断本次读取该标记哪些邮件为已读。 * * 两条规则: * * 1. **只标本次真正列出来的**,不是全部未读。`limit` 之外的还没看过, * 一并标掉等于让它们凭空消失。 * 2. **`status=all` 时不标**。那是「回顾历史」的读法,把历史邮件标成已读 * 会让下一轮真正的新邮件混在里面认不出来。 * * 不标的后果是每次拉收件箱都重复捞同一批,处理过的和新来的混在一起, * 模型分不清哪封该回。 * * @param {string|undefined} status 本次查询用的过滤条件 * @param {any[]} mails 本次返回的邮件 * @returns {string[]} 待标记的 mail_id,空数组表示不需要标记 */ export function idsToMarkRead(status, mails) { if (status === 'all') return []; const list = Array.isArray(mails) ? mails : []; return list.map(m => m?.mail_id).filter(id => typeof id === 'string' && id); } /** 收件箱默认过滤条件。默认只看未读 —— 默认 all 会让模型每轮重读旧邮件。 */ export const DEFAULT_INBOX_STATUS = 'unread'; /** 收件箱默认返回条数。 */ export const DEFAULT_INBOX_LIMIT = 5;