#!/usr/bin/env node 'use strict'; /** * 创建/更新 WhatsApp inbox,并配置「新会话 → AI Assistant」automation。 * * .env 需要: * WA_PHONE_NUMBER_ID / WA_WABA_ID / WA_ACCESS_TOKEN / WA_APP_SECRET * WA_VERIFY_TOKEN / WA_INBOX_NAME(可选) * WA_API_VERSION(可选,默认 v26.0) * * 前提:LibreDesk app.root_url 已是公网 HTTPS(scripts/public-https.sh), * 否则 Meta 无法回调,自动订阅也会失败。 */ const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); const RULE_PREFIX = 'Assign WhatsApp conversations to AI Assistant'; function loadEnv(file) { const out = {}; for (const line of fs.readFileSync(file, 'utf8').split('\n')) { const t = line.trim(); if (!t || t.startsWith('#')) continue; const eq = t.indexOf('='); if (eq < 0) continue; let v = t.slice(eq + 1).trim(); if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1); out[t.slice(0, eq).trim()] = v; } return out; } function createClient(baseURL) { const cookies = new Map(); let csrf = ''; const cookieHeader = () => [...cookies.entries()].map(([k, v]) => `${k}=${v}`).join('; '); function store(res) { const list = typeof res.headers.getSetCookie === 'function' ? res.headers.getSetCookie() : [res.headers.get('set-cookie')].filter(Boolean); for (const c of list) { const [pair] = c.split(';'); const eq = pair.indexOf('='); if (eq > 0) { const k = pair.slice(0, eq).trim(); const v = pair.slice(eq + 1).trim(); cookies.set(k, v); if (k === 'csrf_token') csrf = v; } } } async function req(method, p, body) { const headers = { Accept: 'application/json' }; if (cookies.size) headers.Cookie = cookieHeader(); if (body !== undefined) headers['Content-Type'] = 'application/json'; if (csrf && method !== 'GET') headers['X-CSRFTOKEN'] = csrf; const res = await fetch(baseURL + p, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) }); store(res); const text = await res.text(); let json = null; try { json = text ? JSON.parse(text) : null; } catch { /* ignore */ } if (!res.ok) throw new Error(`${method} ${p} -> HTTP ${res.status}: ${(json && (json.message || json.error)) || text.slice(0, 300)}`); return json; } return { get: (p) => req('GET', p), post: (p, b) => req('POST', p, b), put: (p, b) => req('PUT', p, b) }; } const data = (j) => (j && j.data !== undefined ? j.data : j); async function main() { const env = loadEnv(path.join(ROOT, '.env')); const base = (env.LIBREDESK_BASE_URL || 'http://192.168.31.61:9000').replace(/\/+$/, ''); const required = ['WA_PHONE_NUMBER_ID', 'WA_WABA_ID', 'WA_ACCESS_TOKEN', 'WA_APP_SECRET', 'WA_VERIFY_TOKEN']; const missing = required.filter((k) => !env[k]); if (missing.length) throw new Error(`.env 缺少: ${missing.join(', ')}`); const c = createClient(base); await c.post('/api/v1/auth/login', { email: env.LIBREDESK_SYSTEM_EMAIL || 'System', password: env.LIBREDESK_SYSTEM_USER_PASSWORD }); console.log('登录成功'); const inboxName = env.WA_INBOX_NAME || 'WhatsApp US'; const payload = { name: inboxName, channel: 'whatsapp', enabled: true, config: { phone_number_id: env.WA_PHONE_NUMBER_ID, waba_id: env.WA_WABA_ID, access_token: env.WA_ACCESS_TOKEN, app_secret: env.WA_APP_SECRET, webhook_verify_token: env.WA_VERIFY_TOKEN, api_version: env.WA_API_VERSION || 'v26.0', }, }; const inboxes = data(await c.get('/api/v1/inboxes')) || []; let inbox = inboxes.find((i) => i.channel === 'whatsapp' && i.name === inboxName); if (!inbox) { inbox = data(await c.post('/api/v1/inboxes', payload)); console.log(`创建 WhatsApp Inbox #${inbox.id}`); } else { inbox = data(await c.put(`/api/v1/inboxes/${inbox.id}`, payload)); console.log(`更新 WhatsApp Inbox #${inbox.id}`); } // automation: WhatsApp inbox 新会话 -> AI Assistant const assistants = data(await c.get('/api/v1/ai/assistants')) || []; const assistant = assistants.find((a) => a.name === (env.AI_ASSISTANT_NAME || 'Yehwang Assistant')); if (!assistant) throw new Error('未找到 AI Assistant,先运行 scripts/setup-defaults.js'); const ruleName = `${RULE_PREFIX} #${inbox.id}`; const rulesRaw = data(await c.get('/api/v1/automations/rules?type=new_conversation')) || []; const rules = Array.isArray(rulesRaw) ? rulesRaw : (rulesRaw.rules || []); const rulePayload = { name: ruleName, description: 'Route new WhatsApp conversations to the AI assistant', type: 'new_conversation', events: [], enabled: true, weight: 0, rules: [{ type: 'new_conversation', execution_mode: 'all', event: [], group_operator: 'AND', groups: [{ logical_op: 'AND', rules: [{ field: 'inbox', field_type: 'conversation', operator: 'equals', value: String(inbox.id), case_sensitive_match: false }], }], actions: [{ type: 'assign_user', value: [String(assistant.user_id)], display_value: [assistant.name] }], }], }; const existing = rules.find((r) => r.name === ruleName); if (!existing) { const rule = data(await c.post('/api/v1/automations/rules', rulePayload)); console.log(`创建 Automation #${rule.id}(inbox ${inbox.id} → user ${assistant.user_id})`); } else { const rule = data(await c.put(`/api/v1/automations/rules/${existing.id}`, rulePayload)); console.log(`更新 Automation #${rule.id}(inbox ${inbox.id} → user ${assistant.user_id})`); } const root = env.PUBLIC_BASE_URL || ''; console.log('\nWhatsApp Inbox 配置完成'); console.log(`Inbox ID: ${inbox.id}`); console.log(`Callback URL: ${root}/webhooks/whatsapp/${inbox.id}`); console.log(`Verify Token: ${env.WA_VERIFY_TOKEN}`); console.log('Meta -> WhatsApp -> Configuration -> Webhook 里填上面两项,订阅 messages'); } main().catch((err) => { console.error('[add-whatsapp-inbox]', err.message); process.exit(1); });