Files
crm-ai/scripts/add-email-inbox.js
T
2026-10-23 10:35:28 +08:00

214 lines
7.4 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';
/**
* 配置 Email 渠道(163):
* 1. 创建/更新 email inbox(IMAP 走 imap-id-proxy,绕开 163 的 "Unsafe Login")
* 2. 创建/更新默认外发邮件模板(email_outgoing)
* 3. 创建/更新 automation:email inbox 新会话 -> 分配 AI Assistant
*
* 依赖:docker compose 里的 imap-proxy 服务(services/imap-id-proxy)。
* 用法:node scripts/add-email-inbox.js
*/
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '..');
const OLD_MAIL_CFG = '/home/dev/code/crm/crm-ai-demo/bridge/mail.config.json';
const INBOX_NAME = 'Email 163';
const RULE_NAME = 'Assign email conversations to AI Assistant';
const ASSISTANT_NAME = 'Yehwang Assistant';
const TEMPLATE_NAME = 'Default outgoing email';
const TEMPLATE_BODY = `<!DOCTYPE html>
<html><body style="font-family: Arial, sans-serif; color: #1f2937; line-height: 1.6;">
<p>Hello,</p>
{{ template "content" . }}
<p style="margin-top: 24px;">Best regards,<br>Yehwang Support</p>
</body></html>`;
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) || text.slice(0, 300)}`);
return json;
}
return { post: (p, b) => req('POST', p, b), get: (p) => req('GET', p), 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 old = JSON.parse(fs.readFileSync(OLD_MAIL_CFG, 'utf8'));
const base = (env.LIBREDESK_BASE_URL || 'http://192.168.31.61:9000').replace(/\/+$/, '');
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('登录成功');
// 1. Email inbox
const inboxes = data(await c.get('/api/v1/inboxes')) || [];
let inbox = inboxes.find((i) => i.channel === 'email' && i.name === INBOX_NAME);
const payload = {
name: INBOX_NAME,
channel: 'email',
enabled: true,
csat_enabled: false,
prompt_tags_on_reply: false,
reopen_window_hours: 0,
from: old.smtp.auth.user,
aliases: [],
config: {
auth_type: 'password',
imap: [{
host: 'imap-proxy',
port: 1993,
username: old.imap.auth.user,
password: old.imap.auth.pass,
mailbox: 'INBOX',
read_interval: '1m',
scan_inbox_since: '48h',
tls_type: 'none',
tls_skip_verify: false,
}],
smtp: [{
host: old.smtp.host,
port: old.smtp.port,
username: old.smtp.auth.user,
password: old.smtp.auth.pass,
tls_type: 'tls',
auth_protocol: 'plain',
max_conns: 5,
idle_timeout: '30s',
pool_wait_timeout: '10s',
max_msg_retries: 3,
hello_hostname: '',
}],
reply_to: '',
enable_plus_addressing: false,
},
};
if (!inbox) {
inbox = data(await c.post('/api/v1/inboxes', payload));
console.log(`创建 Email Inbox #${inbox.id}`);
} else {
inbox = data(await c.put(`/api/v1/inboxes/${inbox.id}`, payload));
console.log(`更新 Email Inbox #${inbox.id}`);
}
// 2. 默认外发邮件模板
const tpls = data(await c.get('/api/v1/templates?type=email_outgoing')) || [];
let tpl = tpls.find((t) => t.is_default);
if (!tpl) {
tpl = data(await c.post('/api/v1/templates', {
name: TEMPLATE_NAME,
body: TEMPLATE_BODY,
subject: '',
is_default: true,
type: 'email_outgoing',
}));
console.log(`创建默认外发模板 #${tpl.id}`);
} else if (tpl.body !== TEMPLATE_BODY) {
tpl = data(await c.put(`/api/v1/templates/${tpl.id}`, {
name: tpl.name,
body: TEMPLATE_BODY,
subject: tpl.subject || '',
is_default: true,
type: 'email_outgoing',
}));
console.log(`更新默认外发模板 #${tpl.id}`);
} else {
console.log(`复用默认外发模板 #${tpl.id}`);
}
// 3. Automation:email inbox -> assistant
const assistants = data(await c.get('/api/v1/ai/assistants')) || [];
const assistant = assistants.find((a) => a.name === ASSISTANT_NAME);
if (!assistant) throw new Error(`未找到 Assistant "${ASSISTANT_NAME}",先运行 scripts/setup-defaults.js`);
const rulesRaw = data(await c.get('/api/v1/automations/rules?type=new_conversation')) || [];
const rules = Array.isArray(rulesRaw) ? rulesRaw : (rulesRaw.rules || []);
const rulePayload = {
name: RULE_NAME,
description: 'Route new email 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 === RULE_NAME);
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})`);
}
console.log('\nEmail 渠道配置完成。IMAP 走 imap-proxy(自动注入 ID),SMTP 直连 smtp.163.com:465。');
}
main().catch((err) => {
console.error('[add-email-inbox]', err.message);
process.exit(1);
});