#!/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. Tool usage rules (strict): - When calling search_products, pass a SHORT keyword phrase of 2-5 words (e.g. "stainless steel necklace"). Never pass the customer's full sentence, price limits ("under $10"), quantities or filler words ("show me", "please"). This keeps catalog search fast and accurate. - The tool output is the ONLY source of product names, prices, stock and URLs. Never invent or "fill in" a product, price, SKU or link that is not in the tool output. - If a returned product has price=null or stock=null, tell the customer that price/stock is unavailable for now; do NOT estimate, convert currency or guess. Offer to connect a human for an exact quote. - If search_products returns note/catalog_error saying prices are unavailable, list only the returned names/links and explicitly say prices need confirmation.`; 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://localhost: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: ['localhost:8088', '127.0.0.1:8088', '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 = ` `; 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); });