/** Grok Bot 模板导入:解析 x.ai/bot/ 公开页并翻译为夭夭 Bot 的人设草稿。 */ export interface GrokBotMeta { name: string description: string imageUrl?: string } export type GrokBotTemplateKind = 'housekeeper' | 'manager' | 'general' export interface GrokBotDraft { kind: GrokBotTemplateKind job?: string antiJobs?: string[] actBias?: 'ask_first' | 'ask_key_then_act' | 'act_now' canManageTeam?: boolean instructions: string suggestions: string[] } export const GROK_BOT_URL_PATTERN = /^https:\/\/x\.ai\/bot\/([A-Za-z0-9_-]+)/ function decodeEntities(text: string): string { return text .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'|'/g, "'") .replace(/'/g, "'") .trim() } function metaContent(html: string, keys: string[]): string | undefined { for (const key of keys) { // Attributes on real pages are double-quoted; only exclude the quote // actually in use so apostrophes inside descriptions survive. const pattern = new RegExp( `]+(?:property|name)="${key}"[^>]+content="([^"]*)"|]+content="([^"]*)"[^>]+(?:property|name)="${key}"`, 'i', ) const match = pattern.exec(html) const value = match?.[1] ?? match?.[2] if (value) return decodeEntities(value) } return undefined } /** Extract the public metadata from a fetched x.ai bot page. */ export function parseGrokBotPage(html: string): GrokBotMeta | null { const title = metaContent(html, ['og:title', 'twitter:title']) ?? (() => { const match = /]*>([^<]*)<\/title>/i.exec(html) return match ? decodeEntities(match[1]!.replace(/\s*[-|]\s*Grok.*$/i, '')) : undefined })() const description = metaContent(html, ['og:description', 'twitter:description', 'description']) if (!title) return null return { name: title.slice(0, 100), description: (description ?? '').slice(0, 1200), imageUrl: metaContent(html, ['og:image', 'twitter:image']) } } export class GrokBotImportError extends Error { code: string status: number constructor(message: string, code: string, status = 400) { super(message) this.code = code this.status = status } } /** Optional outbound proxy for x.ai only: HTTPS_PROXY / GROK_BOT_PROXY. */ async function proxyDispatcher(target: string): Promise { const proxy = process.env.GROK_BOT_PROXY ?? process.env.HTTPS_PROXY ?? process.env.https_proxy if (!proxy || !/^https:\/\//.test(target)) return undefined try { const undici = await import('undici') return new undici.ProxyAgent(proxy) } catch { return undefined } } export async function fetchGrokBotPage(url: string, fetchImpl: typeof fetch = fetch, timeoutMs = 12_000): Promise { if (!GROK_BOT_URL_PATTERN.test(url.trim())) throw new GrokBotImportError('请输入 x.ai/bot/ 形式的 Grok Bot 链接', 'invalid_grok_bot_url') let response: Response try { const dispatcher = await proxyDispatcher(url) response = await fetchImpl(url.trim(), { headers: { 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15' , accept: 'text/html' }, signal: AbortSignal.timeout(timeoutMs), redirect: 'follow', ...(dispatcher ? { dispatcher } : {}) } as RequestInit) } catch { throw new GrokBotImportError('无法访问 x.ai:当前网络可能需要代理(可配置 GROK_BOT_PROXY/HTTPS_PROXY),或改用“粘贴描述导入”', 'grok_bot_fetch_failed', 502) } if (!response.ok) throw new GrokBotImportError(`x.ai 返回 ${response.status},请确认链接可公开访问`, 'grok_bot_fetch_failed', 502) const html = await response.text() if (!parseGrokBotPage(html)) throw new GrokBotImportError('页面里找不到 Bot 信息,请确认这是 Grok Bot 分享链接', 'grok_bot_page_invalid', 502) return html } export async function fetchAvatarDataUrl(imageUrl: string | undefined, fetchImpl: typeof fetch = fetch, maxBytes = 2_800_000): Promise { if (!imageUrl || !/^https:\/\//.test(imageUrl)) return undefined try { const dispatcher = await proxyDispatcher(imageUrl) const response = await fetchImpl(imageUrl, { signal: AbortSignal.timeout(10_000), redirect: 'follow', ...(dispatcher ? { dispatcher } : {}) } as RequestInit) if (!response.ok) return undefined const type = (response.headers.get('content-type') ?? '').toLowerCase() if (!/^image\/(png|jpeg|webp)/.test(type)) return undefined const buffer = Buffer.from(await response.arrayBuffer()) if (buffer.byteLength > maxBytes) return undefined return `data:${type.split(';')[0]};base64,${buffer.toString('base64')}` } catch { return undefined } } const HOUSEKEEPER_PATTERN = /email|mail|calendar|邮件|日历|日程|invoice|账单|form|表单|rsvp|邀请函/i const MANAGER_PATTERN = /notion|project|项目|团队|team|task|排期|进度|delegate|委派|分派/i /** Translate a Grok Bot description into a yaoyao persona draft. */ export function grokBotDraft(meta: GrokBotMeta): GrokBotDraft { const summary = `${meta.description}` if (MANAGER_PATTERN.test(summary)) { return { kind: 'manager', job: '管理团队项目:每个项目一份任务计划和一个群聊,任务交给合适的 Bot 并跟进到完成', antiJobs: ['绝不替专家 Bot 完成具体工作', '不隐瞒风险和延误,坏消息第一时间上报'], actBias: 'ask_key_then_act', canManageTeam: true, instructions: [ `你从 Grok Bot 模板「${meta.name}」导入,原始描述:${summary}`, '', '工作方式(夭夭版):', '1. 每个新项目:创建一份任务计划(写清目标、验收标准、截止时间),并建一个以项目命名的群聊,把相关 Bot 拉进来。', '2. 任务拆解后写给最合适的成员 Bot,用 @成员名 明确委派;你是管理员,负责分派、跟进、汇总,不亲自做专家工作。', '3. 每天在群里汇总一次进度:完成、进行中、阻塞,以及需要用户决策的事项。', '4. 用户是最终决策者:涉及范围、预算、优先级的变化,先问再做。', ].join('\n'), suggestions: ['为它准备几个专家 Bot 作为队友(可再从 Grok Bot 模板导入或自建)'], } } if (HOUSEKEEPER_PATTERN.test(summary)) { return { kind: 'housekeeper', job: '盯家庭邮箱和日程,学校的表单、账单和回复邀请不遗漏', antiJobs: ['绝不直接发送邮件或回复邀请', '绝不替用户提交任何表单', '不擅自改期或取消日程'], actBias: 'ask_key_then_act', instructions: [ `你从 Grok Bot 模板「${meta.name}」导入,原始描述:${summary}`, '', '工作方式(夭夭版):', '1. 巡检时用 desktop_shell 在服务器 Mac 上执行 osascript 读取「邮件」App 的未读邮件和「日历」App 今明两天的日程。', '2. 发现需要处理的事项(表单截止、账单、接送冲突、RSVP)时,在对话里列出清单并给出建议动作。', '3. 需要回复的邮件:在对话里起草完整回复文本,等用户确认后仍由用户发送。', '4. 保持每条汇报简短:事项、截止时间、建议,不写长篇解释。', ].join('\n'), suggestions: ['在「电脑与定时任务」勾选本机控制并完成文件与命令授权(邮件/日历靠它读取)', '创建每天早晚各一次的定时任务让它自动巡检'], } } return { kind: 'general', instructions: `你从 Grok Bot 模板「${meta.name}」导入,原始描述:${summary}\n\n(此 Bot 从 Grok Bot 模板导入。上方为原始描述,可在「角色与规则」中补充一句话职责、反任务清单与行动偏好,让它更贴合你的使用。)`, suggestions: ['打开「角色与规则」补一句话职责和反任务清单,结构化规范会自动生效'], } }