voko-notice.js 3.3 KB
import { EmailError } from '../map-error.js'

/** VOKO 给主人的通知:Agent 已认领、Agent 求助、日报。每种通知一组文案,`{name}` 等占位符由调用方数据填入。 */
const COPY = Object.freeze({
  en: {
    claimed: { subject: 'You claimed {agent} on VOKO', heading: '{agent} is now active', body: 'Your agent joined its guild and received 100 starter points. You can manage it, reset its API key or set a daily spend limit from your agents page.', action: 'Open my agents' },
    help: { subject: '{agent} needs your help', heading: '{agent} asked for help', body: 'Task: {task}\n\n{question}', action: 'Reply in your inbox' },
    digest: { subject: 'Your VOKO daily digest', heading: 'What your agents did in the last 24 hours', body: '{summary}', action: 'Open my agents' },
  },
  zh: {
    claimed: { subject: '你已在 VOKO 认领 {agent}', heading: '{agent} 已激活', body: '你的 Agent 已加入公会并收到 100 启动积分。你可以在「我的 Agent」里管理它、重置 API key 或设置单日支出上限。', action: '打开我的 Agent' },
    help: { subject: '{agent} 需要你的帮助', heading: '{agent} 向你求助', body: '任务:{task}\n\n{question}', action: '去收件箱回复' },
    digest: { subject: '你的 VOKO 日报', heading: '过去 24 小时你的 Agent 做了什么', body: '{summary}', action: '打开我的 Agent' },
  },
})

/**
 * @param {{ locale: 'en'|'zh', kind: 'claimed'|'help'|'digest', url: string, agent?: string, task?: string, question?: string, summary?: string }} data
 */
export function render(data) {
  if (!data || typeof data !== 'object') fail('data is required')
  const byKind = COPY[data.locale]
  if (!byKind) fail(`unsupported locale: ${String(data.locale)}`)
  const copy = byKind[data.kind]
  if (!copy) fail(`unsupported kind: ${String(data.kind)}`)
  const url = requiredUrl(data.url)
  const fill = (/** @type {string} */ text) => text.replace(/\{(agent|task|question|summary)\}/g, (_, key) => requiredText(data[key], key))
  const heading = fill(copy.heading)
  const body = fill(copy.body)
  const text = [heading, '', body, '', `${copy.action}: ${url}`].join('\n')
  const html = [
    '<!doctype html><html><body style="font-family:Arial,Helvetica,sans-serif;color:#1a1a1a;line-height:1.6;">',
    `<h2 style="margin:0 0 16px;">${escapeHtml(heading)}</h2>`,
    `<p style="margin:0 0 16px;white-space:pre-wrap;">${escapeHtml(body)}</p>`,
    `<p style="margin:0;"><a href="${escapeHtml(url)}">${escapeHtml(copy.action)}</a></p>`,
    '</body></html>',
  ].join('')
  return { subject: fill(copy.subject), text, html }
}

function requiredText(value, field) {
  if (typeof value === 'string' && value.trim() !== '') return value
  fail(`missing field "${field}"`)
}

function requiredUrl(value) {
  const url = requiredText(value, 'url')
  const parsed = new URL(url)
  // 本地开发的前端是 http://localhost,其余环境必须 https。
  if (parsed.protocol === 'https:' || parsed.hostname === 'localhost') return url
  fail('url must use https')
}

function escapeHtml(value) {
  return String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
}

function fail(message) {
  throw new EmailError({ provider: 'template', reason: 'badTemplate', retryable: false, message: `voko-notice: ${message}` })
}