|
|
@@ -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);
|
|
|
+});
|