chore: CRM-NEW baseline
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 建默认配置:Support 团队(可选)、AI Assistant、Live chat Inbox、Automation 规则。
|
||||
* 幂等:按名称复用已有对象。
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const NAME = {
|
||||
inbox: 'Website',
|
||||
assistant: 'Yehwang Assistant',
|
||||
rule: 'Assign new 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 && !(body instanceof FormData)) 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 : (body instanceof FormData ? body : 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, 400)}`);
|
||||
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);
|
||||
|
||||
const INSTRUCTIONS = `You are the Yehwang B2B customer assistant for stainless-steel jewelry wholesale.
|
||||
- Always reply in the customer's language.
|
||||
- For policy / FAQ questions, search the knowledge base first and answer from it.
|
||||
- For product questions, call search_products. Never invent catalog items, prices or stock.
|
||||
- For order questions, call search_orders. If it returns verification_required, ask the customer
|
||||
to verify their email with the one-time code, then retry the tool.
|
||||
- For stock / pre-order questions, call search_inventory with the SKU digits.
|
||||
- When you know company + contact name + product/need, call create_opportunity once.
|
||||
- If you cannot help, or the customer asks for a human, hand off to a human agent.
|
||||
- Never reveal another customer's data.`;
|
||||
|
||||
const GUARDRAILS = `Never invent prices, discounts, stock or delivery dates.
|
||||
Never share another customer's order or personal data.
|
||||
Never create an opportunity before the contact is verified.`;
|
||||
|
||||
async function main() {
|
||||
const env = loadEnv(path.join(ROOT, '.env'));
|
||||
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. Assistant
|
||||
const assistants = data(await c.get('/api/v1/ai/assistants')) || [];
|
||||
let assistant = assistants.find((a) => a.name === NAME.assistant);
|
||||
const tools = data(await c.get('/api/v1/ai/tools')) || [];
|
||||
const toolIds = tools.map((t) => t.id);
|
||||
if (!assistant) {
|
||||
const payload = {
|
||||
name: NAME.assistant,
|
||||
description: 'Yehwang sales & service assistant',
|
||||
instructions: INSTRUCTIONS,
|
||||
guardrails: GUARDRAILS,
|
||||
expectation: 'You are chatting with our AI assistant. I can help with products, orders and policies — and connect you to a human when needed.',
|
||||
tone: 'friendly',
|
||||
response_length: 'balanced',
|
||||
max_turns: 8,
|
||||
fallback_team_id: null,
|
||||
citations_enabled: true,
|
||||
handoff_enabled: true,
|
||||
languages: [],
|
||||
enabled: true,
|
||||
tool_ids: toolIds,
|
||||
};
|
||||
const form = new FormData();
|
||||
form.append('data', JSON.stringify(payload));
|
||||
assistant = data(await c.post('/api/v1/ai/assistants', form));
|
||||
console.log(`创建 Assistant #${assistant.id} (user_id=${assistant.user_id}, tools=${toolIds.join(',')})`);
|
||||
} else {
|
||||
console.log(`复用 Assistant #${assistant.id} (user_id=${assistant.user_id})`);
|
||||
}
|
||||
|
||||
// 2. Live chat inbox
|
||||
const inboxPayload = {
|
||||
name: NAME.inbox,
|
||||
channel: 'livechat',
|
||||
enabled: true,
|
||||
csat_enabled: true,
|
||||
prompt_tags_on_reply: false,
|
||||
reopen_window_hours: 0,
|
||||
// 保留 widget JWT 签名用的 secret(更新时服务端会用请求值覆盖)
|
||||
...(env.LIBREDESK_INBOX_SECRET ? { secret: env.LIBREDESK_INBOX_SECRET } : {}),
|
||||
config: {
|
||||
brand_name: 'Yehwang',
|
||||
website_url: 'https://www.yehwang.com',
|
||||
language: 'en-US',
|
||||
fallback_language: 'en-US',
|
||||
theme: 'light',
|
||||
show_powered_by: false,
|
||||
direct_to_conversation: true,
|
||||
campaign_cooldown: '24h',
|
||||
session_duration: '4320h',
|
||||
launcher: { position: 'right', icon_scale: 100, spacing: { side: 24, bottom: 24 } },
|
||||
greeting_message: 'Hi! How can we help you today?',
|
||||
chat_introduction: 'Ask about products, orders, shipping or returns.',
|
||||
trusted_domains: ['192.168.31.61:8088', 'localhost:8088', '127.0.0.1:8088', '192.168.31.61', 'localhost', '127.0.0.1', 'www.yehwang.com', 'yehwang.com'],
|
||||
help: { help_center_id: 0 },
|
||||
users: { allow_start_conversation: true, prevent_multiple_conversations: false },
|
||||
visitors: { allow_start_conversation: true, prevent_multiple_conversations: false },
|
||||
branding: {
|
||||
light: {
|
||||
colors: { primary: '#1f6feb' },
|
||||
logo_url: '',
|
||||
launcher: { logo_url: '', color: '#1f6feb' },
|
||||
home_screen: { header_text_color: 'black', background: { type: 'solid', color: '#ffffff' }, fade_background: false },
|
||||
},
|
||||
dark: {
|
||||
colors: { primary: '#1f6feb' },
|
||||
logo_url: '',
|
||||
launcher: { logo_url: '', color: '#1f6feb' },
|
||||
home_screen: { header_text_color: 'white', background: { type: 'solid', color: '#111827' }, fade_background: false },
|
||||
},
|
||||
},
|
||||
features: { emoji: true, file_upload: true, transcript: true },
|
||||
},
|
||||
};
|
||||
|
||||
const inboxes = data(await c.get('/api/v1/inboxes')) || [];
|
||||
let inbox = inboxes.find((i) => i.name === NAME.inbox && i.channel === 'livechat');
|
||||
if (!inbox) {
|
||||
inbox = data(await c.post('/api/v1/inboxes', inboxPayload));
|
||||
console.log(`创建 Live chat Inbox #${inbox.id} uuid=${inbox.uuid}`);
|
||||
} else {
|
||||
inbox = data(await c.put(`/api/v1/inboxes/${inbox.id}`, inboxPayload));
|
||||
console.log(`更新 Live chat Inbox #${inbox.id} uuid=${inbox.uuid}`);
|
||||
}
|
||||
|
||||
// 3. Automation: new conversation in this inbox -> assign assistant user
|
||||
const rulesRaw = data(await c.get('/api/v1/automations/rules?type=new_conversation')) || [];
|
||||
const rules = Array.isArray(rulesRaw) ? rulesRaw : (rulesRaw.rules || []);
|
||||
const rulePayload = {
|
||||
name: NAME.rule,
|
||||
description: 'Route new live chat conversations to the AI assistant',
|
||||
type: 'new_conversation',
|
||||
events: [],
|
||||
enabled: true,
|
||||
weight: 0,
|
||||
// 注意:rules 必须是数组(引擎按 []Rule 反序列化)
|
||||
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],
|
||||
}],
|
||||
}],
|
||||
};
|
||||
let rule = rules.find((r) => r.name === NAME.rule);
|
||||
if (!rule) {
|
||||
rule = data(await c.post('/api/v1/automations/rules', rulePayload));
|
||||
console.log(`创建 Automation #${rule.id} -> assign user ${assistant.user_id}`);
|
||||
} else {
|
||||
rule = data(await c.put(`/api/v1/automations/rules/${rule.id}`, rulePayload));
|
||||
console.log(`更新 Automation #${rule.id} -> assign user ${assistant.user_id}`);
|
||||
}
|
||||
|
||||
const snippet = `<script>
|
||||
window.LibredeskSettings = {
|
||||
baseURL: '${base}',
|
||||
inboxID: '${inbox.uuid}'
|
||||
};
|
||||
</script>
|
||||
<script async src="${base}/widget.js"></script>`;
|
||||
|
||||
console.log('\n===== Widget 安装代码 =====');
|
||||
console.log(snippet);
|
||||
console.log('\n(用户登录后可用 userJWT 绑定身份,见 docs/libredesk-setup.md 第 6 节)');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('[setup-defaults]', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user