| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- /** Grok Bot 模板导入:解析 x.ai/bot/<id> 公开页并翻译为夭夭 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(
- `<meta[^>]+(?:property|name)="${key}"[^>]+content="([^"]*)"|<meta[^>]+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[^>]*>([^<]*)<\/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<unknown> {
- 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<string> {
- if (!GROK_BOT_URL_PATTERN.test(url.trim())) throw new GrokBotImportError('请输入 x.ai/bot/<id> 形式的 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<string | undefined> {
- 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: ['打开「角色与规则」补一句话职责和反任务清单,结构化规范会自动生效'],
- }
- }
|