Files
crm-ai/scripts/add-whatsapp-inbox.js

154 lines
6.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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://localhost: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 || '<app.root_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);
});