Browse Source

Add production deployment documentation

Includes Word document (部署文档.docx) covering all 10 deployment
chapters: environment requirements, Node.js setup, code deployment,
PostgreSQL DB init, PM2, Apache vhost, FastAPI ERP bridge, update
process, verification checklist, and environment variables reference.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tony T 2 tuần trước cách đây
mục cha
commit
89dc9c9d0f
2 tập tin đã thay đổi với 597 bổ sung0 xóa
  1. BIN
      deploy/Homelegance_Chatbot_部署文档.docx
  2. 597 0
      deploy/create-deploy-doc.mjs

BIN
deploy/Homelegance_Chatbot_部署文档.docx


+ 597 - 0
deploy/create-deploy-doc.mjs

@@ -0,0 +1,597 @@
+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);
+});