|
|
@@ -1,160 +0,0 @@
|
|
|
-/** 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: ['打开「角色与规则」补一句话职责和反任务清单,结构化规范会自动生效'],
|
|
|
- }
|
|
|
-}
|