chore: CRM-NEW baseline
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
#!/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);
|
||||
});
|
||||
Reference in New Issue
Block a user