import { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, AlignmentType, BorderStyle, WidthType, ShadingType, VerticalAlign, LevelFormat, Header, Footer, PageNumber, PageBreak, HeadingLevel, TabStopType, TabStopPosition, } from "docx"; import { writeFileSync } from "fs"; // ── Palette ─────────────────────────────────────────────────────────────────── const NAVY = "1B2A4A"; const ACCENT = "2E86C1"; const GREEN = "1A6B2A"; const GRAY = "F4F6F8"; const LGRAY = "EEEEEE"; const DGRAY = "555555"; const WHITE = "FFFFFF"; const BLACK = "000000"; const BD = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" }; const allBd = { top: BD, bottom: BD, left: BD, right: BD }; const noBd = { top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }, bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }, left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }, right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" } }; const W = 9026; // A4 content width (1" margins each side) // ── Helpers ─────────────────────────────────────────────────────────────────── const sp = (before = 0, after = 120) => ({ before, after }); function h1(text, id) { const para = new Paragraph({ spacing: sp(360, 120), border: { bottom: { style: BorderStyle.SINGLE, size: 8, color: ACCENT, space: 4 } }, children: [new TextRun({ text, bold: true, size: 32, color: NAVY, font: "Arial" })], }); return para; } function h2(text) { return new Paragraph({ spacing: sp(240, 100), children: [ new TextRun({ text: " ", size: 24, font: "Arial" }), new TextRun({ text, bold: true, size: 24, color: ACCENT, font: "Arial" }), ], border: { left: { style: BorderStyle.THICK, size: 10, color: ACCENT, space: 8 } }, }); } function p(text, opts = {}) { const { bold = false, size = 20, color = "333333", indent = false, spacing = sp(0, 100) } = opts; return new Paragraph({ spacing, indent: indent ? { left: 360 } : undefined, children: [new TextRun({ text, bold, size, color, font: "Arial" })], }); } function note(text) { return new Paragraph({ spacing: sp(80, 160), indent: { left: 200 }, border: { left: { style: BorderStyle.SINGLE, size: 6, color: "F39C12", space: 6 } }, children: [new TextRun({ text: `\u26A0 ${text}`, size: 18, italics: true, color: "7D6608", font: "Arial" })], }); } function bullet(text, sub = false) { return new Paragraph({ numbering: { reference: sub ? "sub-bullets" : "bullets", level: 0 }, spacing: sp(0, 80), children: [new TextRun({ text, size: 20, font: "Arial", color: "333333" })], }); } // Code block: gray box, Courier New function codeBlock(lines) { return new Table({ width: { size: W, type: WidthType.DXA }, columnWidths: [W], borders: { top: BD, bottom: BD, left: BD, right: BD, insideH: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }, insideV: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" } }, rows: [ new TableRow({ children: [ new TableCell({ borders: noBd, width: { size: W, type: WidthType.DXA }, shading: { fill: "F0F0F0", type: ShadingType.CLEAR }, margins: { top: 120, bottom: 120, left: 200, right: 200 }, children: lines.map(l => new Paragraph({ spacing: sp(0, 40), children: [new TextRun({ text: l, size: 18, font: "Courier New", color: "1A1A2E" })], })), }), ], }), ], }); } // Info box (blue bg) function infoBox(lines) { return new Table({ width: { size: W, type: WidthType.DXA }, columnWidths: [W], rows: [new TableRow({ children: [new TableCell({ borders: noBd, width: { size: W, type: WidthType.DXA }, shading: { fill: "EAF4FB", type: ShadingType.CLEAR }, margins: { top: 120, bottom: 120, left: 200, right: 200 }, children: lines.map(l => new Paragraph({ spacing: sp(0, 60), children: [new TextRun({ text: l, size: 19, font: "Arial", color: "1A4A6B" })], })), })] })], }); } // Generic table function makeTable(headers, rows, colWidths, headerBg = NAVY) { const total = colWidths.reduce((a, b) => a + b, 0); function hdrCell(text, w) { return new TableCell({ borders: allBd, width: { size: w, type: WidthType.DXA }, shading: { fill: headerBg, type: ShadingType.CLEAR }, verticalAlign: VerticalAlign.CENTER, margins: { top: 80, bottom: 80, left: 120, right: 120 }, children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [new TextRun({ text, bold: true, size: 19, color: WHITE, font: "Arial" })] })], }); } function dataCell(text, w, i) { const isCode = text.startsWith("$ ") || text.startsWith("pnpm ") || text.startsWith("pm2 ") || text.startsWith("systemctl ") || text.startsWith("curl "); return new TableCell({ borders: allBd, width: { size: w, type: WidthType.DXA }, shading: { fill: i % 2 === 0 ? WHITE : GRAY, type: ShadingType.CLEAR }, verticalAlign: VerticalAlign.CENTER, margins: { top: 80, bottom: 80, left: 120, right: 120 }, children: [new Paragraph({ children: [new TextRun({ text, size: 19, font: isCode ? "Courier New" : "Arial", color: isCode ? "1A1A2E" : "333333" })] })], }); } return new Table({ width: { size: total, type: WidthType.DXA }, columnWidths: colWidths, rows: [ new TableRow({ tableHeader: true, children: headers.map((h, i) => hdrCell(h, colWidths[i])) }), ...rows.map((row, ri) => new TableRow({ children: row.map((cell, ci) => dataCell(cell, colWidths[ci], ri)) })), ], }); } function gap(n = 1) { return Array.from({ length: n }, () => new Paragraph({ spacing: sp(0, 60), children: [new TextRun("")] })); } function pageBreak() { return new Paragraph({ children: [new PageBreak()] }); } // ── Document ───────────────────────────────────────────────────────────────── const doc = new Document({ numbering: { config: [ { reference: "bullets", levels: [{ level: 0, format: LevelFormat.BULLET, text: "\u2022", alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 600, hanging: 300 } } } }] }, { reference: "sub-bullets", levels: [{ level: 0, format: LevelFormat.BULLET, text: "\u25E6", alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 900, hanging: 300 } } } }] }, ], }, styles: { default: { document: { run: { font: "Arial", size: 20, color: "333333" } } }, }, sections: [{ properties: { page: { size: { width: 11906, height: 16838 }, // A4 margin: { top: 1080, right: 1080, bottom: 1080, left: 1080 }, }, }, headers: { default: new Header({ children: [ new Paragraph({ border: { bottom: { style: BorderStyle.SINGLE, size: 4, color: ACCENT, space: 4 } }, spacing: sp(0, 120), children: [ new TextRun({ text: "Homelegance Chatbot \u2014 \u751F\u4EA7\u90E8\u7F72\u6587\u6863", size: 17, color: "888888", font: "Arial" }), new TextRun({ text: "\t", size: 17, font: "Arial" }), new TextRun({ text: "2026\u5E744\u6708", size: 17, color: "888888", font: "Arial" }), ], tabStops: [{ type: TabStopType.RIGHT, position: TabStopPosition.MAX }], }), ] }), }, footers: { default: new Footer({ children: [ new Paragraph({ border: { top: { style: BorderStyle.SINGLE, size: 2, color: "CCCCCC", space: 4 } }, spacing: sp(80, 0), alignment: AlignmentType.CENTER, children: [ new TextRun({ text: "United-US \u5185\u90E8\u6587\u6863 | \u7AC5\u673A\u5BC6 \u2014 \u8BF7\u52FF\u5916\u4F20 | \u7B2C ", size: 16, color: "888888", font: "Arial" }), new TextRun({ children: [PageNumber.CURRENT], size: 16, color: "888888", font: "Arial" }), new TextRun({ text: " \u9875", size: 16, color: "888888", font: "Arial" }), ], }), ] }), }, children: [ // ── Cover ────────────────────────────────────────────────────────── new Paragraph({ spacing: sp(400, 0), children: [new TextRun({ text: "", size: 20 })] }), new Paragraph({ spacing: sp(0, 80), children: [new TextRun({ text: "Homelegance Chatbot", bold: true, size: 64, color: NAVY, font: "Arial" })], }), new Paragraph({ spacing: sp(0, 60), children: [new TextRun({ text: "\u751F\u4EA7\u90E8\u7F72\u6587\u6863", bold: true, size: 44, color: ACCENT, font: "Arial" })], }), new Paragraph({ spacing: sp(0, 400), border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: ACCENT, space: 6 } }, children: [new TextRun({ text: "AlmaLinux 8.10 | Apache \u53CD\u5411\u4EE3\u7406 | PostgreSQL \u72EC\u7ACB DB \u670D\u52A1\u5668", size: 20, color: DGRAY, font: "Arial" })], }), ...gap(2), infoBox([ "\uD83D\uDDA5 Web \u670D\u52A1\u5668\uFF1A aga-web1-services / AlmaLinux 8.10", "\uD83D\uDCC2 \u90E8\u7F72\u8DEF\u5F84\uFF1A /redant/web/homelegance-chatbot", "\uD83D\uDD17 Git \u4ED3\u5E93\uFF1A https://git.united-us.net/TonyT/chatbot.git", "\uD83E\uDD16 LLM\uFF1A Anthropic Claude API (claude-sonnet-4-6)", "\uD83D\uDDB3 \u6570\u636E\u5E93\uFF1A \u5185\u7F51\u72EC\u7ACB PostgreSQL \u670D\u52A1\u5668 \u00B7 Schema: chatbot", ]), pageBreak(), // ── Ch1 ──────────────────────────────────────────────────────────── h1("\u7B2C\u4E00\u7AE0 \u73AF\u5883\u8981\u6C42"), h2("1.1 \u670D\u52A1\u5668\u4FE1\u606F"), ...gap(), makeTable( ["\u9879\u76EE", "\u8BE6\u60C5"], [ ["\u64CD\u4F5C\u7CFB\u7EDF", "AlmaLinux 8.10"], ["Web \u670D\u52A1\u5668", "Apache httpd\uFF08\u7EDF\u4E00\u51FA\u53E3\uFF09"], ["\u90E8\u7F72\u8DEF\u5F84", "/redant/web/homelegance-chatbot"], ["Node.js", "v20.20.2 LTS"], ["\u5305\u7BA1\u7406\u5668", "pnpm v10.x"], ["\u8FDB\u7A0B\u7BA1\u7406", "PM2"], ["\u6570\u636E\u5E93\u670D\u52A1\u5668", "\u5185\u7F51\u72EC\u7ACB PostgreSQL \u670D\u52A1\u5668"], ["\u6570\u636E\u5E93 Schema", "chatbot\uFF08\u72EC\u7ACB Schema\uFF09"], ["LLM API", "Anthropic Claude API\uFF08claude-sonnet-4-6\uFF09"], ["ERP \u6865\u63A5\u670D\u52A1", "Python FastAPI\uFF08/redant/web/erp-bridge\uFF09"], ], [3000, 6026], ), ...gap(2), h2("1.2 \u7F51\u7EDC\u8981\u6C42"), ...gap(), bullet("Web \u670D\u52A1\u5668\u9700\u8BBF\u95EE npm registry\uFF08pnpm install \u4F9D\u8D56\u5B89\u88C5\uFF09"), bullet("Web \u670D\u52A1\u5668\u9700\u8BBF\u95EE api.anthropic.com\uFF08Claude API \u8C03\u7528\uFF09"), bullet("\u5185\u7F51\u53EF\u8BBF\u95EE PostgreSQL DB \u670D\u52A1\u5668\uFF085432 \u7AEF\u53E3\uFF09"), bullet("Apache \u5BF9\u5916\u5F00\u653E 80 / 443 \u7AEF\u53E3"), bullet("Node.js :3000 \u3001FastAPI :8080 \u4EC5\u672C\u673A\u8BBF\u95EE\uFF0C\u4E0D\u5BF9\u5916\u66B4\u9732"), pageBreak(), // ── Ch2 ──────────────────────────────────────────────────────────── h1("\u7B2C\u4E8C\u7AE0 Node.js \u5B89\u88C5"), note("AlmaLinux 8 \u7CFB\u7EDF\u81EA\u5E26 Node.js 10\uFF0C\u4E0E\u9879\u76EE\u4E0D\u517C\u5BB9\uFF0C\u5FC5\u987B\u5347\u7EA7\u5230 v20\u3002"), ...gap(), h2("2.1 \u79FB\u9664\u65E7\u7248\u672C"), ...gap(), codeBlock([ "dnf remove -y nodejs npm", ]), ...gap(), h2("2.2 \u5B89\u88C5 Node.js 20 LTS\uFF08\u901A\u8FC7 NodeSource\uFF09"), ...gap(), codeBlock([ "curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -", "dnf install -y nodejs --allowerasing", "node --version # \u5E94\u663E\u793A v20.x.x", ]), ...gap(), h2("2.3 \u5B89\u88C5 pnpm \u548C PM2"), ...gap(), codeBlock([ "npm install -g pnpm", "npm install -g pm2", "pnpm --version", "pm2 --version", ]), ...gap(2), h2("2.4 \u5E38\u89C1\u95EE\u9898\uFF1Anode \u547D\u4EE4\u627E\u4E0D\u5230"), ...gap(), p("\u539F\u56E0\uFF1A\u65E7 nvm \u8DEF\u5F84\u6B8B\u7559\u5728 PATH \u4E2D\uFF0C\u89E3\u51B3\u65B9\u6CD5\uFF1A"), ...gap(), codeBlock([ "unset NVM_DIR", "hash -r", "which node", "", "# \u5982\u4ECD\u65E0\u6CD5\u627E\u5230\uFF0C\u624B\u52A8\u8BBE\u7F6E PATH\uFF1A", "export PATH=\"/usr/bin:$PATH\"", "node --version", ]), pageBreak(), // ── Ch3 ──────────────────────────────────────────────────────────── h1("\u7B2C\u4E09\u7AE0 \u4EE3\u7801\u90E8\u7F72"), h2("3.1 \u521B\u5EFA\u90E8\u7F72\u76EE\u5F55\u5E76\u62C9\u53D6\u4EE3\u7801"), ...gap(), codeBlock([ "mkdir -p /redant/web/homelegance-chatbot", "cd /redant/web/homelegance-chatbot", "git clone https://git.united-us.net/TonyT/chatbot.git .", ]), ...gap(), h2("3.2 \u5B89\u88C5\u4F9D\u8D56"), ...gap(), codeBlock(["pnpm install"]), p("\u5B89\u88C5\u6210\u529F\u6807\u5FD7\uFF1A\u663E\u793A \u201CDone in Xs using pnpm vX.X.X\u201D", { color: GREEN, indent: true }), ...gap(), h2("3.3 \u914D\u7F6E\u73AF\u5883\u53D8\u91CF"), ...gap(), codeBlock([ "cp deploy/env.production.example .env.production", "vi .env.production", ]), ...gap(), p(".env.production \u5185\u5BB9\uFF08\u9700\u586B\u5199\u5B9E\u9645\u503C\uFF09\uFF1A"), ...gap(), codeBlock([ "NODE_ENV=production", "PORT=3000", "DATABASE_URL=postgresql://chatbot_user:PASSWORD@DB_SERVER_IP:5432/homelegance", "JWT_SECRET=\uFF08\u4F7F\u7528 openssl rand -hex 32 \u751F\u6210\uFF09", "ANTHROPIC_API_KEY=sk-ant-xxxxxxxx", "ERP_API_URL=http://127.0.0.1:8080", "ERP_API_KEY=\uFF08\u5185\u90E8\u968F\u673A\u5BC6\u9470\uFF09", "DEALER_PORTAL_SSO_SECRET=\uFF08\u4E0E Dealer Portal \u7EA6\u5B9A\u7684\u5171\u4EAB\u5BC6\u9470\uFF09", ]), ...gap(), h2("3.4 \u6784\u5EFA\u9879\u76EE"), ...gap(), codeBlock(["pnpm build"]), ...gap(), p("\u6784\u5EFA\u8F93\u51FA\uFF1A"), bullet("dist/public/ \u524D\u7AEF\u9759\u6001\u6587\u4EF6\uFF08\u7531 Apache \u76F4\u63A5\u670D\u52A1\uFF09"), bullet("dist/index.js \u540E\u7AEF Node.js bundle\uFF08\u7531 PM2 \u8FD0\u884C\uFF09"), pageBreak(), // ── Ch4 ──────────────────────────────────────────────────────────── h1("\u7B2C\u56DB\u7AE0 \u6570\u636E\u5E93\u521D\u59CB\u5316"), h2("4.1 DBA \u64CD\u4F5C\uFF08\u5728 DB \u670D\u52A1\u5668\u4E0A\u6267\u884C\uFF0C\u4EC5\u9700\u4E00\u6B21\uFF09"), ...gap(), codeBlock([ "-- \u521B\u5EFA\u6570\u636E\u5E93\uFF08\u5982\u5C1A\u672A\u5B58\u5728\uFF09", "CREATE DATABASE homelegance;", "", "-- \u521B\u5EFA\u4E13\u7528\u7528\u6237", "CREATE USER chatbot_user WITH PASSWORD 'your_password';", "", "-- \u521B\u5EFA chatbot \u4E13\u5C5E Schema", "\\c homelegance", "CREATE SCHEMA chatbot;", "", "-- \u6388\u6743", "GRANT ALL ON SCHEMA chatbot TO chatbot_user;", "GRANT ALL PRIVILEGES ON DATABASE homelegance TO chatbot_user;", "ALTER DEFAULT PRIVILEGES IN SCHEMA chatbot", " GRANT ALL ON TABLES TO chatbot_user;", "ALTER DEFAULT PRIVILEGES IN SCHEMA chatbot", " GRANT ALL ON SEQUENCES TO chatbot_user;", ]), ...gap(), h2("4.2 \u5728 Web \u670D\u52A1\u5668\u4E0A\u521D\u59CB\u5316\u8868\u7ED3\u6784"), ...gap(), codeBlock([ "cd /redant/web/homelegance-chatbot", "pnpm db:push", ]), ...gap(), p("\u6B64\u547D\u4EE4\u4F1A\u5728 chatbot schema \u4E0B\u81EA\u52A8\u521B\u5EFA\u6240\u6709\u8868\u548C\u679A\u4E3E\u7C7B\u578B\uFF1A"), ...gap(), makeTable( ["\u8868\u540D", "\u7528\u9014"], [ ["chatbot.users", "\u7528\u6237 / Agent \u8D26\u53F7"], ["chatbot.conversations", "\u5BF9\u8BDD\u4F1A\u8BDD"], ["chatbot.messages", "\u804A\u5929\u6D88\u606F"], ["chatbot.invitations", "\u9080\u8BF7\u94FE\u63A5"], ["chatbot.audit_logs", "\u5BA1\u8BA1\u65E5\u5FD7"], ["chatbot.workflow_nodes/edges", "\u5DE5\u4F5C\u6D41\u8BBE\u8BA1\u5668"], ["chatbot.analytics_events", "\u8BBF\u95EE\u5206\u6790"], ["chatbot.data_sources", "\u77E5\u8BC6\u5E93\u914D\u7F6E"], ["chatbot.api_connections", "API \u8FDE\u63A5\u914D\u7F6E"], ], [3500, 5526], ), pageBreak(), // ── Ch5 ──────────────────────────────────────────────────────────── h1("\u7B2C\u4E94\u7AE0 PM2 \u8FDB\u7A0B\u914D\u7F6E"), h2("5.1 \u542F\u52A8\u670D\u52A1"), ...gap(), codeBlock([ "cd /redant/web/homelegance-chatbot", "pm2 start ecosystem.config.cjs", "pm2 status", ]), ...gap(), h2("5.2 \u8BBE\u7F6E\u5F00\u673A\u81EA\u542F"), ...gap(), codeBlock([ "pm2 save", "pm2 startup systemd", "# \u6309\u7167\u8F93\u51FA\u63D0\u793A\u6267\u884C\u5BF9\u5E94\u7684 systemctl \u547D\u4EE4", ]), ...gap(), h2("5.3 \u5E38\u7528 PM2 \u547D\u4EE4"), ...gap(), makeTable( ["\u547D\u4EE4", "\u8BF4\u660E"], [ ["pm2 status", "\u67E5\u770B\u670D\u52A1\u72B6\u6001"], ["pm2 logs homelegance-chat", "\u5B9E\u65F6\u67E5\u770B\u65E5\u5FD7"], ["pm2 restart homelegance-chat", "\u91CD\u542F\u670D\u52A1"], ["pm2 stop homelegance-chat", "\u505C\u6B62\u670D\u52A1"], ["pm2 reload homelegance-chat", "\u96F6\u505C\u673A\u91CD\u8F7D\uFF08\u66F4\u65B0\u4EE3\u7801\u540E\u4F7F\u7528\uFF09"], ], [4000, 5026], ), pageBreak(), // ── Ch6 ──────────────────────────────────────────────────────────── h1("\u7B2C\u516D\u7AE0 Apache \u53CD\u5411\u4EE3\u7406\u914D\u7F6E"), h2("6.1 \u9A8C\u8BC1\u5FC5\u8981\u6A21\u5757"), ...gap(), codeBlock(["httpd -M | grep -E \"proxy|rewrite|headers\""]), ...gap(), h2("6.2 \u90E8\u7F72\u865A\u62DF\u4E3B\u673A\u914D\u7F6E"), ...gap(), codeBlock([ "cp /redant/web/homelegance-chatbot/deploy/homelegance-chat.conf /etc/httpd/conf.d/", "vi /etc/httpd/conf.d/homelegance-chat.conf # \u586B\u5199\u57DF\u540D\u548C SSL \u8BC1\u4E66\u8DEF\u5F84", ]), ...gap(), h2("6.3 \u914D\u7F6E\u8981\u70B9"), ...gap(), bullet("DocumentRoot \u2192 /redant/web/homelegance-chatbot/dist/public\uFF08\u9759\u6001\u6587\u4EF6\u7531 Apache \u76F4\u63A5\u670D\u52A1\uFF09"), bullet("/api/* \u8BF7\u6C42 ProxyPass \u8F6C\u53D1\u81F3 Node.js localhost:3000"), bullet("\u975E\u6587\u4EF6\u8DEF\u5F84\u8BF7\u6C42 Rewrite \u81F3 index.html\uFF08\u652F\u6301 React SPA \u8DEF\u7531\uFF09"), bullet("80 \u7AEF\u53E3\u81EA\u52A8\u8DF3\u8F6C HTTPS"), ...gap(), h2("6.4 \u9A8C\u8BC1\u5E76\u91CD\u8F7D"), ...gap(), codeBlock([ "apachectl configtest # \u9A8C\u8BC1\u914D\u7F6E\u8BED\u6CD5", "systemctl reload httpd # \u91CD\u8F7D\u914D\u7F6E", ]), ...gap(), h2("6.5 \u9632\u706B\u5899\u8BBE\u7F6E"), ...gap(), codeBlock([ "firewall-cmd --permanent --add-service=http", "firewall-cmd --permanent --add-service=https", "firewall-cmd --reload", "# Node.js :3000 \u548C FastAPI :8080 \u4E0D\u5BF9\u5916\u5F00\u653E", ]), pageBreak(), // ── Ch7 ──────────────────────────────────────────────────────────── h1("\u7B2C\u4E03\u7AE0 ERP FastAPI \u6865\u63A5\u670D\u52A1"), h2("7.1 \u5B89\u88C5 Python \u4F9D\u8D56"), ...gap(), codeBlock([ "dnf install -y python3.11 python3.11-pip", "pip3.11 install fastapi uvicorn asyncpg python-dotenv", ]), ...gap(), h2("7.2 \u521B\u5EFA\u670D\u52A1\u76EE\u5F55"), ...gap(), codeBlock(["mkdir -p /redant/web/erp-bridge"]), ...gap(), h2("7.3 \u914D\u7F6E\u73AF\u5883\u53D8\u91CF"), ...gap(), p("\u521B\u5EFA /redant/web/erp-bridge/.env\uFF1A"), ...gap(), codeBlock([ "ERP_DATABASE_URL=postgresql://chatbot_readonly:PASSWORD@ERP_DB_SERVER:5432/erp_db", "ERP_API_KEY=\uFF08\u4E0E chatbot .env.production \u4E2D ERP_API_KEY \u76F8\u540C\uFF09", "PORT=8080", ]), ...gap(), h2("7.4 ERP \u6570\u636E\u5E93\u53EA\u8BFB\u8D26\u53F7\uFF08\u7531 ERP DBA \u521B\u5EFA\uFF09"), ...gap(), codeBlock([ "CREATE USER chatbot_readonly WITH PASSWORD 'readonly_password';", "GRANT SELECT ON TABLE", " items, contacts, sales_orders, sales_order_items, stock", " TO chatbot_readonly;", ]), ...gap(), h2("7.5 \u90E8\u7F72 Systemd \u670D\u52A1"), ...gap(), codeBlock([ "cp /redant/web/homelegance-chatbot/deploy/erp-bridge.service /etc/systemd/system/", "systemctl daemon-reload", "systemctl enable --now erp-bridge", "systemctl status erp-bridge", ]), pageBreak(), // ── Ch8 ──────────────────────────────────────────────────────────── h1("\u7B2C\u516B\u7AE0 \u66F4\u65B0\u90E8\u7F72\u6D41\u7A0B"), h2("8.1 \u5E38\u89C4\u4EE3\u7801\u66F4\u65B0"), ...gap(), codeBlock([ "cd /redant/web/homelegance-chatbot", "git pull origin master", "pnpm install # \u5982\u6709\u65B0\u4F9D\u8D56", "pnpm build # \u91CD\u65B0\u6784\u5EFA", "pm2 reload homelegance-chat # \u96F6\u505C\u673A\u91CD\u8F7D", ]), ...gap(), h2("8.2 \u5982\u6709\u6570\u636E\u5E93 Schema \u53D8\u66F4"), ...gap(), codeBlock(["pnpm db:push"]), pageBreak(), // ── Ch9 ──────────────────────────────────────────────────────────── h1("\u7B2C\u4E5D\u7AE0 \u9A8C\u8BC1\u68C0\u67E5\u6E05\u5355"), h2("9.1 \u670D\u52A1\u72B6\u6001\u68C0\u67E5"), ...gap(), makeTable( ["\u68C0\u67E5\u9879", "\u547D\u4EE4", "\u671F\u671B\u7ED3\u679C"], [ ["Node.js \u670D\u52A1", "pm2 status", "online"], ["Apache \u670D\u52A1", "systemctl status httpd", "active (running)"], ["ERP Bridge", "systemctl status erp-bridge", "active (running)"], ["\u5065\u5EB7\u68C0\u67E5", "curl http://localhost:3000/api/trpc/system.health", "\u8FD4\u56DE 200"], ["\u524D\u7AEF\u8BBF\u95EE", "curl -I https://chat.homelegance.com", "HTTP 200"], ], [2200, 4500, 2326], ), ...gap(2), h2("9.2 \u65E5\u5FD7\u4F4D\u7F6E"), ...gap(), makeTable( ["\u65E5\u5FD7\u7C7B\u578B", "\u8DEF\u5F84"], [ ["Node.js \u8F93\u51FA\u65E5\u5FD7", "/var/log/pm2/homelegance-chat-out.log"], ["Node.js \u9519\u8BEF\u65E5\u5FD7", "/var/log/pm2/homelegance-chat-error.log"], ["Apache \u8BBF\u95EE\u65E5\u5FD7", "/var/log/httpd/homelegance-chat-access.log"], ["Apache \u9519\u8BEF\u65E5\u5FD7", "/var/log/httpd/homelegance-chat-error.log"], ["ERP Bridge \u65E5\u5FD7", "journalctl -u erp-bridge -f"], ], [3000, 6026], ), pageBreak(), // ── Ch10 ─────────────────────────────────────────────────────────── h1("\u7B2C\u5341\u7AE0 \u73AF\u5883\u53D8\u91CF\u5B8C\u6574\u53C2\u8003"), ...gap(), makeTable( ["\u53D8\u91CF\u540D", "\u8BF4\u660E", "\u793A\u4F8B\u503C", "\u5FC5\u586B"], [ ["NODE_ENV", "\u8FD0\u884C\u73AF\u5883", "production", "\u662F"], ["PORT", "Node.js \u76D1\u542C\u7AEF\u53E3", "3000", "\u5426\uFF08\u9ED8\u8BA43000\uFF09"], ["DATABASE_URL", "Chatbot PostgreSQL \u8FDE\u63A5\u4E32", "postgresql://user:pwd@host:5432/db", "\u662F"], ["JWT_SECRET", "Session \u7B7E\u540D\u5BC6\u9470", "openssl rand -hex 32 \u751F\u6210", "\u662F"], ["ANTHROPIC_API_KEY", "Claude API \u5BC6\u9470", "sk-ant-...", "\u662F"], ["ERP_API_URL", "ERP Bridge \u5185\u7F51\u5730\u5740", "http://127.0.0.1:8080", "\u662F"], ["ERP_API_KEY", "ERP Bridge \u8BBF\u95EE\u5BC6\u9470", "\u968F\u673A\u5B57\u7B26\u4E32", "\u662F"], ["DEALER_PORTAL_SSO_SECRET","Dealer Portal SSO \u5171\u4EAB\u5BC6\u9470","Portal \u56E2\u961F\u7EA6\u5B9A", "SSO \u542F\u7528\u65F6"], ], [2600, 2800, 2400, 1226], ), pageBreak(), // ── Appendix ─────────────────────────────────────────────────────── h1("\u9644\u5F55 Git \u4ED3\u5E93\u4E0E\u914D\u7F6E\u6587\u4EF6"), ...gap(), infoBox([ "\uD83D\uDD17 \u4ED3\u5E93\u5730\u5740\uFF1A https://git.united-us.net/TonyT/chatbot.git", "\uD83C\uDF3F \u4E3B\u5206\u652F\uFF1A master", "\uD83D\uDCC2 \u914D\u7F6E\u6587\u4EF6\u76EE\u5F55\uFF1A deploy/", ]), ...gap(2), makeTable( ["\u6587\u4EF6\u540D", "\u7528\u9014", "\u90E8\u7F72\u76EE\u6807\u8DEF\u5F84"], [ ["homelegance-chat.conf", "Apache \u865A\u62DF\u4E3B\u673A\u914D\u7F6E", "/etc/httpd/conf.d/"], ["erp-bridge.service", "ERP Bridge systemd \u670D\u52A1", "/etc/systemd/system/"], ["env.production.example", "\u73AF\u5883\u53D8\u91CF\u6A21\u677F", ".env.production\uFF08\u586B\u5199\u540E\uFF09"], ["ecosystem.config.cjs", "PM2 \u8FDB\u7A0B\u914D\u7F6E", "\u9879\u76EE\u6839\u76EE\u5F55\uFF08\u5DF2\u5305\u542B\uFF09"], ], [2800, 3000, 3226], ), ], }], }); const out = "C:\\Codes\\homelegance-chatbot\\deploy\\Homelegance_Chatbot_\u90E8\u7F72\u6587\u6863.docx"; Packer.toBuffer(doc).then(buf => { writeFileSync(out, buf); console.log("Created:", out); });