create-deploy-doc.mjs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. import {
  2. Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell,
  3. AlignmentType, BorderStyle, WidthType, ShadingType, VerticalAlign,
  4. LevelFormat, Header, Footer, PageNumber, PageBreak, HeadingLevel,
  5. TabStopType, TabStopPosition,
  6. } from "docx";
  7. import { writeFileSync } from "fs";
  8. // ── Palette ───────────────────────────────────────────────────────────────────
  9. const NAVY = "1B2A4A";
  10. const ACCENT = "2E86C1";
  11. const GREEN = "1A6B2A";
  12. const GRAY = "F4F6F8";
  13. const LGRAY = "EEEEEE";
  14. const DGRAY = "555555";
  15. const WHITE = "FFFFFF";
  16. const BLACK = "000000";
  17. const BD = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" };
  18. const allBd = { top: BD, bottom: BD, left: BD, right: BD };
  19. const noBd = { top: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
  20. bottom: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
  21. left: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" },
  22. right: { style: BorderStyle.NONE, size: 0, color: "FFFFFF" } };
  23. const W = 9026; // A4 content width (1" margins each side)
  24. // ── Helpers ───────────────────────────────────────────────────────────────────
  25. const sp = (before = 0, after = 120) => ({ before, after });
  26. function h1(text, id) {
  27. const para = new Paragraph({
  28. spacing: sp(360, 120),
  29. border: { bottom: { style: BorderStyle.SINGLE, size: 8, color: ACCENT, space: 4 } },
  30. children: [new TextRun({ text, bold: true, size: 32, color: NAVY, font: "Arial" })],
  31. });
  32. return para;
  33. }
  34. function h2(text) {
  35. return new Paragraph({
  36. spacing: sp(240, 100),
  37. children: [
  38. new TextRun({ text: " ", size: 24, font: "Arial" }),
  39. new TextRun({ text, bold: true, size: 24, color: ACCENT, font: "Arial" }),
  40. ],
  41. border: { left: { style: BorderStyle.THICK, size: 10, color: ACCENT, space: 8 } },
  42. });
  43. }
  44. function p(text, opts = {}) {
  45. const { bold = false, size = 20, color = "333333", indent = false, spacing = sp(0, 100) } = opts;
  46. return new Paragraph({
  47. spacing,
  48. indent: indent ? { left: 360 } : undefined,
  49. children: [new TextRun({ text, bold, size, color, font: "Arial" })],
  50. });
  51. }
  52. function note(text) {
  53. return new Paragraph({
  54. spacing: sp(80, 160),
  55. indent: { left: 200 },
  56. border: { left: { style: BorderStyle.SINGLE, size: 6, color: "F39C12", space: 6 } },
  57. children: [new TextRun({ text: `\u26A0 ${text}`, size: 18, italics: true, color: "7D6608", font: "Arial" })],
  58. });
  59. }
  60. function bullet(text, sub = false) {
  61. return new Paragraph({
  62. numbering: { reference: sub ? "sub-bullets" : "bullets", level: 0 },
  63. spacing: sp(0, 80),
  64. children: [new TextRun({ text, size: 20, font: "Arial", color: "333333" })],
  65. });
  66. }
  67. // Code block: gray box, Courier New
  68. function codeBlock(lines) {
  69. return new Table({
  70. width: { size: W, type: WidthType.DXA },
  71. columnWidths: [W],
  72. 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" } },
  73. rows: [
  74. new TableRow({
  75. children: [
  76. new TableCell({
  77. borders: noBd,
  78. width: { size: W, type: WidthType.DXA },
  79. shading: { fill: "F0F0F0", type: ShadingType.CLEAR },
  80. margins: { top: 120, bottom: 120, left: 200, right: 200 },
  81. children: lines.map(l => new Paragraph({
  82. spacing: sp(0, 40),
  83. children: [new TextRun({ text: l, size: 18, font: "Courier New", color: "1A1A2E" })],
  84. })),
  85. }),
  86. ],
  87. }),
  88. ],
  89. });
  90. }
  91. // Info box (blue bg)
  92. function infoBox(lines) {
  93. return new Table({
  94. width: { size: W, type: WidthType.DXA },
  95. columnWidths: [W],
  96. rows: [new TableRow({ children: [new TableCell({
  97. borders: noBd,
  98. width: { size: W, type: WidthType.DXA },
  99. shading: { fill: "EAF4FB", type: ShadingType.CLEAR },
  100. margins: { top: 120, bottom: 120, left: 200, right: 200 },
  101. children: lines.map(l => new Paragraph({
  102. spacing: sp(0, 60),
  103. children: [new TextRun({ text: l, size: 19, font: "Arial", color: "1A4A6B" })],
  104. })),
  105. })] })],
  106. });
  107. }
  108. // Generic table
  109. function makeTable(headers, rows, colWidths, headerBg = NAVY) {
  110. const total = colWidths.reduce((a, b) => a + b, 0);
  111. function hdrCell(text, w) {
  112. return new TableCell({
  113. borders: allBd, width: { size: w, type: WidthType.DXA },
  114. shading: { fill: headerBg, type: ShadingType.CLEAR },
  115. verticalAlign: VerticalAlign.CENTER,
  116. margins: { top: 80, bottom: 80, left: 120, right: 120 },
  117. children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [new TextRun({ text, bold: true, size: 19, color: WHITE, font: "Arial" })] })],
  118. });
  119. }
  120. function dataCell(text, w, i) {
  121. const isCode = text.startsWith("$ ") || text.startsWith("pnpm ") || text.startsWith("pm2 ") || text.startsWith("systemctl ") || text.startsWith("curl ");
  122. return new TableCell({
  123. borders: allBd, width: { size: w, type: WidthType.DXA },
  124. shading: { fill: i % 2 === 0 ? WHITE : GRAY, type: ShadingType.CLEAR },
  125. verticalAlign: VerticalAlign.CENTER,
  126. margins: { top: 80, bottom: 80, left: 120, right: 120 },
  127. children: [new Paragraph({ children: [new TextRun({ text, size: 19, font: isCode ? "Courier New" : "Arial", color: isCode ? "1A1A2E" : "333333" })] })],
  128. });
  129. }
  130. return new Table({
  131. width: { size: total, type: WidthType.DXA },
  132. columnWidths: colWidths,
  133. rows: [
  134. new TableRow({ tableHeader: true, children: headers.map((h, i) => hdrCell(h, colWidths[i])) }),
  135. ...rows.map((row, ri) => new TableRow({ children: row.map((cell, ci) => dataCell(cell, colWidths[ci], ri)) })),
  136. ],
  137. });
  138. }
  139. function gap(n = 1) { return Array.from({ length: n }, () => new Paragraph({ spacing: sp(0, 60), children: [new TextRun("")] })); }
  140. function pageBreak() { return new Paragraph({ children: [new PageBreak()] }); }
  141. // ── Document ─────────────────────────────────────────────────────────────────
  142. const doc = new Document({
  143. numbering: {
  144. config: [
  145. { reference: "bullets", levels: [{ level: 0, format: LevelFormat.BULLET, text: "\u2022", alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 600, hanging: 300 } } } }] },
  146. { reference: "sub-bullets", levels: [{ level: 0, format: LevelFormat.BULLET, text: "\u25E6", alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 900, hanging: 300 } } } }] },
  147. ],
  148. },
  149. styles: {
  150. default: { document: { run: { font: "Arial", size: 20, color: "333333" } } },
  151. },
  152. sections: [{
  153. properties: {
  154. page: {
  155. size: { width: 11906, height: 16838 }, // A4
  156. margin: { top: 1080, right: 1080, bottom: 1080, left: 1080 },
  157. },
  158. },
  159. headers: {
  160. default: new Header({ children: [
  161. new Paragraph({
  162. border: { bottom: { style: BorderStyle.SINGLE, size: 4, color: ACCENT, space: 4 } },
  163. spacing: sp(0, 120),
  164. children: [
  165. new TextRun({ text: "Homelegance Chatbot \u2014 \u751F\u4EA7\u90E8\u7F72\u6587\u6863", size: 17, color: "888888", font: "Arial" }),
  166. new TextRun({ text: "\t", size: 17, font: "Arial" }),
  167. new TextRun({ text: "2026\u5E744\u6708", size: 17, color: "888888", font: "Arial" }),
  168. ],
  169. tabStops: [{ type: TabStopType.RIGHT, position: TabStopPosition.MAX }],
  170. }),
  171. ] }),
  172. },
  173. footers: {
  174. default: new Footer({ children: [
  175. new Paragraph({
  176. border: { top: { style: BorderStyle.SINGLE, size: 2, color: "CCCCCC", space: 4 } },
  177. spacing: sp(80, 0),
  178. alignment: AlignmentType.CENTER,
  179. children: [
  180. new TextRun({ text: "United-US \u5185\u90E8\u6587\u6863 | \u7AC5\u673A\u5BC6 \u2014 \u8BF7\u52FF\u5916\u4F20 | \u7B2C ", size: 16, color: "888888", font: "Arial" }),
  181. new TextRun({ children: [PageNumber.CURRENT], size: 16, color: "888888", font: "Arial" }),
  182. new TextRun({ text: " \u9875", size: 16, color: "888888", font: "Arial" }),
  183. ],
  184. }),
  185. ] }),
  186. },
  187. children: [
  188. // ── Cover ──────────────────────────────────────────────────────────
  189. new Paragraph({ spacing: sp(400, 0), children: [new TextRun({ text: "", size: 20 })] }),
  190. new Paragraph({
  191. spacing: sp(0, 80),
  192. children: [new TextRun({ text: "Homelegance Chatbot", bold: true, size: 64, color: NAVY, font: "Arial" })],
  193. }),
  194. new Paragraph({
  195. spacing: sp(0, 60),
  196. children: [new TextRun({ text: "\u751F\u4EA7\u90E8\u7F72\u6587\u6863", bold: true, size: 44, color: ACCENT, font: "Arial" })],
  197. }),
  198. new Paragraph({
  199. spacing: sp(0, 400),
  200. border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: ACCENT, space: 6 } },
  201. 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" })],
  202. }),
  203. ...gap(2),
  204. infoBox([
  205. "\uD83D\uDDA5 Web \u670D\u52A1\u5668\uFF1A aga-web1-services / AlmaLinux 8.10",
  206. "\uD83D\uDCC2 \u90E8\u7F72\u8DEF\u5F84\uFF1A /redant/web/homelegance-chatbot",
  207. "\uD83D\uDD17 Git \u4ED3\u5E93\uFF1A https://git.united-us.net/TonyT/chatbot.git",
  208. "\uD83E\uDD16 LLM\uFF1A Anthropic Claude API (claude-sonnet-4-6)",
  209. "\uD83D\uDDB3 \u6570\u636E\u5E93\uFF1A \u5185\u7F51\u72EC\u7ACB PostgreSQL \u670D\u52A1\u5668 \u00B7 Schema: chatbot",
  210. ]),
  211. pageBreak(),
  212. // ── Ch1 ────────────────────────────────────────────────────────────
  213. h1("\u7B2C\u4E00\u7AE0 \u73AF\u5883\u8981\u6C42"),
  214. h2("1.1 \u670D\u52A1\u5668\u4FE1\u606F"),
  215. ...gap(),
  216. makeTable(
  217. ["\u9879\u76EE", "\u8BE6\u60C5"],
  218. [
  219. ["\u64CD\u4F5C\u7CFB\u7EDF", "AlmaLinux 8.10"],
  220. ["Web \u670D\u52A1\u5668", "Apache httpd\uFF08\u7EDF\u4E00\u51FA\u53E3\uFF09"],
  221. ["\u90E8\u7F72\u8DEF\u5F84", "/redant/web/homelegance-chatbot"],
  222. ["Node.js", "v20.20.2 LTS"],
  223. ["\u5305\u7BA1\u7406\u5668", "pnpm v10.x"],
  224. ["\u8FDB\u7A0B\u7BA1\u7406", "PM2"],
  225. ["\u6570\u636E\u5E93\u670D\u52A1\u5668", "\u5185\u7F51\u72EC\u7ACB PostgreSQL \u670D\u52A1\u5668"],
  226. ["\u6570\u636E\u5E93 Schema", "chatbot\uFF08\u72EC\u7ACB Schema\uFF09"],
  227. ["LLM API", "Anthropic Claude API\uFF08claude-sonnet-4-6\uFF09"],
  228. ["ERP \u6865\u63A5\u670D\u52A1", "Python FastAPI\uFF08/redant/web/erp-bridge\uFF09"],
  229. ],
  230. [3000, 6026],
  231. ),
  232. ...gap(2),
  233. h2("1.2 \u7F51\u7EDC\u8981\u6C42"),
  234. ...gap(),
  235. bullet("Web \u670D\u52A1\u5668\u9700\u8BBF\u95EE npm registry\uFF08pnpm install \u4F9D\u8D56\u5B89\u88C5\uFF09"),
  236. bullet("Web \u670D\u52A1\u5668\u9700\u8BBF\u95EE api.anthropic.com\uFF08Claude API \u8C03\u7528\uFF09"),
  237. bullet("\u5185\u7F51\u53EF\u8BBF\u95EE PostgreSQL DB \u670D\u52A1\u5668\uFF085432 \u7AEF\u53E3\uFF09"),
  238. bullet("Apache \u5BF9\u5916\u5F00\u653E 80 / 443 \u7AEF\u53E3"),
  239. bullet("Node.js :3000 \u3001FastAPI :8080 \u4EC5\u672C\u673A\u8BBF\u95EE\uFF0C\u4E0D\u5BF9\u5916\u66B4\u9732"),
  240. pageBreak(),
  241. // ── Ch2 ────────────────────────────────────────────────────────────
  242. h1("\u7B2C\u4E8C\u7AE0 Node.js \u5B89\u88C5"),
  243. 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"),
  244. ...gap(),
  245. h2("2.1 \u79FB\u9664\u65E7\u7248\u672C"),
  246. ...gap(),
  247. codeBlock([
  248. "dnf remove -y nodejs npm",
  249. ]),
  250. ...gap(),
  251. h2("2.2 \u5B89\u88C5 Node.js 20 LTS\uFF08\u901A\u8FC7 NodeSource\uFF09"),
  252. ...gap(),
  253. codeBlock([
  254. "curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -",
  255. "dnf install -y nodejs --allowerasing",
  256. "node --version # \u5E94\u663E\u793A v20.x.x",
  257. ]),
  258. ...gap(),
  259. h2("2.3 \u5B89\u88C5 pnpm \u548C PM2"),
  260. ...gap(),
  261. codeBlock([
  262. "npm install -g pnpm",
  263. "npm install -g pm2",
  264. "pnpm --version",
  265. "pm2 --version",
  266. ]),
  267. ...gap(2),
  268. h2("2.4 \u5E38\u89C1\u95EE\u9898\uFF1Anode \u547D\u4EE4\u627E\u4E0D\u5230"),
  269. ...gap(),
  270. p("\u539F\u56E0\uFF1A\u65E7 nvm \u8DEF\u5F84\u6B8B\u7559\u5728 PATH \u4E2D\uFF0C\u89E3\u51B3\u65B9\u6CD5\uFF1A"),
  271. ...gap(),
  272. codeBlock([
  273. "unset NVM_DIR",
  274. "hash -r",
  275. "which node",
  276. "",
  277. "# \u5982\u4ECD\u65E0\u6CD5\u627E\u5230\uFF0C\u624B\u52A8\u8BBE\u7F6E PATH\uFF1A",
  278. "export PATH=\"/usr/bin:$PATH\"",
  279. "node --version",
  280. ]),
  281. pageBreak(),
  282. // ── Ch3 ────────────────────────────────────────────────────────────
  283. h1("\u7B2C\u4E09\u7AE0 \u4EE3\u7801\u90E8\u7F72"),
  284. h2("3.1 \u521B\u5EFA\u90E8\u7F72\u76EE\u5F55\u5E76\u62C9\u53D6\u4EE3\u7801"),
  285. ...gap(),
  286. codeBlock([
  287. "mkdir -p /redant/web/homelegance-chatbot",
  288. "cd /redant/web/homelegance-chatbot",
  289. "git clone https://git.united-us.net/TonyT/chatbot.git .",
  290. ]),
  291. ...gap(),
  292. h2("3.2 \u5B89\u88C5\u4F9D\u8D56"),
  293. ...gap(),
  294. codeBlock(["pnpm install"]),
  295. p("\u5B89\u88C5\u6210\u529F\u6807\u5FD7\uFF1A\u663E\u793A \u201CDone in Xs using pnpm vX.X.X\u201D", { color: GREEN, indent: true }),
  296. ...gap(),
  297. h2("3.3 \u914D\u7F6E\u73AF\u5883\u53D8\u91CF"),
  298. ...gap(),
  299. codeBlock([
  300. "cp deploy/env.production.example .env.production",
  301. "vi .env.production",
  302. ]),
  303. ...gap(),
  304. p(".env.production \u5185\u5BB9\uFF08\u9700\u586B\u5199\u5B9E\u9645\u503C\uFF09\uFF1A"),
  305. ...gap(),
  306. codeBlock([
  307. "NODE_ENV=production",
  308. "PORT=3000",
  309. "DATABASE_URL=postgresql://chatbot_user:PASSWORD@DB_SERVER_IP:5432/homelegance",
  310. "JWT_SECRET=\uFF08\u4F7F\u7528 openssl rand -hex 32 \u751F\u6210\uFF09",
  311. "ANTHROPIC_API_KEY=sk-ant-xxxxxxxx",
  312. "ERP_API_URL=http://127.0.0.1:8080",
  313. "ERP_API_KEY=\uFF08\u5185\u90E8\u968F\u673A\u5BC6\u9470\uFF09",
  314. "DEALER_PORTAL_SSO_SECRET=\uFF08\u4E0E Dealer Portal \u7EA6\u5B9A\u7684\u5171\u4EAB\u5BC6\u9470\uFF09",
  315. ]),
  316. ...gap(),
  317. h2("3.4 \u6784\u5EFA\u9879\u76EE"),
  318. ...gap(),
  319. codeBlock(["pnpm build"]),
  320. ...gap(),
  321. p("\u6784\u5EFA\u8F93\u51FA\uFF1A"),
  322. bullet("dist/public/ \u524D\u7AEF\u9759\u6001\u6587\u4EF6\uFF08\u7531 Apache \u76F4\u63A5\u670D\u52A1\uFF09"),
  323. bullet("dist/index.js \u540E\u7AEF Node.js bundle\uFF08\u7531 PM2 \u8FD0\u884C\uFF09"),
  324. pageBreak(),
  325. // ── Ch4 ────────────────────────────────────────────────────────────
  326. h1("\u7B2C\u56DB\u7AE0 \u6570\u636E\u5E93\u521D\u59CB\u5316"),
  327. h2("4.1 DBA \u64CD\u4F5C\uFF08\u5728 DB \u670D\u52A1\u5668\u4E0A\u6267\u884C\uFF0C\u4EC5\u9700\u4E00\u6B21\uFF09"),
  328. ...gap(),
  329. codeBlock([
  330. "-- \u521B\u5EFA\u6570\u636E\u5E93\uFF08\u5982\u5C1A\u672A\u5B58\u5728\uFF09",
  331. "CREATE DATABASE homelegance;",
  332. "",
  333. "-- \u521B\u5EFA\u4E13\u7528\u7528\u6237",
  334. "CREATE USER chatbot_user WITH PASSWORD 'your_password';",
  335. "",
  336. "-- \u521B\u5EFA chatbot \u4E13\u5C5E Schema",
  337. "\\c homelegance",
  338. "CREATE SCHEMA chatbot;",
  339. "",
  340. "-- \u6388\u6743",
  341. "GRANT ALL ON SCHEMA chatbot TO chatbot_user;",
  342. "GRANT ALL PRIVILEGES ON DATABASE homelegance TO chatbot_user;",
  343. "ALTER DEFAULT PRIVILEGES IN SCHEMA chatbot",
  344. " GRANT ALL ON TABLES TO chatbot_user;",
  345. "ALTER DEFAULT PRIVILEGES IN SCHEMA chatbot",
  346. " GRANT ALL ON SEQUENCES TO chatbot_user;",
  347. ]),
  348. ...gap(),
  349. h2("4.2 \u5728 Web \u670D\u52A1\u5668\u4E0A\u521D\u59CB\u5316\u8868\u7ED3\u6784"),
  350. ...gap(),
  351. codeBlock([
  352. "cd /redant/web/homelegance-chatbot",
  353. "pnpm db:push",
  354. ]),
  355. ...gap(),
  356. p("\u6B64\u547D\u4EE4\u4F1A\u5728 chatbot schema \u4E0B\u81EA\u52A8\u521B\u5EFA\u6240\u6709\u8868\u548C\u679A\u4E3E\u7C7B\u578B\uFF1A"),
  357. ...gap(),
  358. makeTable(
  359. ["\u8868\u540D", "\u7528\u9014"],
  360. [
  361. ["chatbot.users", "\u7528\u6237 / Agent \u8D26\u53F7"],
  362. ["chatbot.conversations", "\u5BF9\u8BDD\u4F1A\u8BDD"],
  363. ["chatbot.messages", "\u804A\u5929\u6D88\u606F"],
  364. ["chatbot.invitations", "\u9080\u8BF7\u94FE\u63A5"],
  365. ["chatbot.audit_logs", "\u5BA1\u8BA1\u65E5\u5FD7"],
  366. ["chatbot.workflow_nodes/edges", "\u5DE5\u4F5C\u6D41\u8BBE\u8BA1\u5668"],
  367. ["chatbot.analytics_events", "\u8BBF\u95EE\u5206\u6790"],
  368. ["chatbot.data_sources", "\u77E5\u8BC6\u5E93\u914D\u7F6E"],
  369. ["chatbot.api_connections", "API \u8FDE\u63A5\u914D\u7F6E"],
  370. ],
  371. [3500, 5526],
  372. ),
  373. pageBreak(),
  374. // ── Ch5 ────────────────────────────────────────────────────────────
  375. h1("\u7B2C\u4E94\u7AE0 PM2 \u8FDB\u7A0B\u914D\u7F6E"),
  376. h2("5.1 \u542F\u52A8\u670D\u52A1"),
  377. ...gap(),
  378. codeBlock([
  379. "cd /redant/web/homelegance-chatbot",
  380. "pm2 start ecosystem.config.cjs",
  381. "pm2 status",
  382. ]),
  383. ...gap(),
  384. h2("5.2 \u8BBE\u7F6E\u5F00\u673A\u81EA\u542F"),
  385. ...gap(),
  386. codeBlock([
  387. "pm2 save",
  388. "pm2 startup systemd",
  389. "# \u6309\u7167\u8F93\u51FA\u63D0\u793A\u6267\u884C\u5BF9\u5E94\u7684 systemctl \u547D\u4EE4",
  390. ]),
  391. ...gap(),
  392. h2("5.3 \u5E38\u7528 PM2 \u547D\u4EE4"),
  393. ...gap(),
  394. makeTable(
  395. ["\u547D\u4EE4", "\u8BF4\u660E"],
  396. [
  397. ["pm2 status", "\u67E5\u770B\u670D\u52A1\u72B6\u6001"],
  398. ["pm2 logs homelegance-chat", "\u5B9E\u65F6\u67E5\u770B\u65E5\u5FD7"],
  399. ["pm2 restart homelegance-chat", "\u91CD\u542F\u670D\u52A1"],
  400. ["pm2 stop homelegance-chat", "\u505C\u6B62\u670D\u52A1"],
  401. ["pm2 reload homelegance-chat", "\u96F6\u505C\u673A\u91CD\u8F7D\uFF08\u66F4\u65B0\u4EE3\u7801\u540E\u4F7F\u7528\uFF09"],
  402. ],
  403. [4000, 5026],
  404. ),
  405. pageBreak(),
  406. // ── Ch6 ────────────────────────────────────────────────────────────
  407. h1("\u7B2C\u516D\u7AE0 Apache \u53CD\u5411\u4EE3\u7406\u914D\u7F6E"),
  408. h2("6.1 \u9A8C\u8BC1\u5FC5\u8981\u6A21\u5757"),
  409. ...gap(),
  410. codeBlock(["httpd -M | grep -E \"proxy|rewrite|headers\""]),
  411. ...gap(),
  412. h2("6.2 \u90E8\u7F72\u865A\u62DF\u4E3B\u673A\u914D\u7F6E"),
  413. ...gap(),
  414. codeBlock([
  415. "cp /redant/web/homelegance-chatbot/deploy/homelegance-chat.conf /etc/httpd/conf.d/",
  416. "vi /etc/httpd/conf.d/homelegance-chat.conf # \u586B\u5199\u57DF\u540D\u548C SSL \u8BC1\u4E66\u8DEF\u5F84",
  417. ]),
  418. ...gap(),
  419. h2("6.3 \u914D\u7F6E\u8981\u70B9"),
  420. ...gap(),
  421. bullet("DocumentRoot \u2192 /redant/web/homelegance-chatbot/dist/public\uFF08\u9759\u6001\u6587\u4EF6\u7531 Apache \u76F4\u63A5\u670D\u52A1\uFF09"),
  422. bullet("/api/* \u8BF7\u6C42 ProxyPass \u8F6C\u53D1\u81F3 Node.js localhost:3000"),
  423. bullet("\u975E\u6587\u4EF6\u8DEF\u5F84\u8BF7\u6C42 Rewrite \u81F3 index.html\uFF08\u652F\u6301 React SPA \u8DEF\u7531\uFF09"),
  424. bullet("80 \u7AEF\u53E3\u81EA\u52A8\u8DF3\u8F6C HTTPS"),
  425. ...gap(),
  426. h2("6.4 \u9A8C\u8BC1\u5E76\u91CD\u8F7D"),
  427. ...gap(),
  428. codeBlock([
  429. "apachectl configtest # \u9A8C\u8BC1\u914D\u7F6E\u8BED\u6CD5",
  430. "systemctl reload httpd # \u91CD\u8F7D\u914D\u7F6E",
  431. ]),
  432. ...gap(),
  433. h2("6.5 \u9632\u706B\u5899\u8BBE\u7F6E"),
  434. ...gap(),
  435. codeBlock([
  436. "firewall-cmd --permanent --add-service=http",
  437. "firewall-cmd --permanent --add-service=https",
  438. "firewall-cmd --reload",
  439. "# Node.js :3000 \u548C FastAPI :8080 \u4E0D\u5BF9\u5916\u5F00\u653E",
  440. ]),
  441. pageBreak(),
  442. // ── Ch7 ────────────────────────────────────────────────────────────
  443. h1("\u7B2C\u4E03\u7AE0 ERP FastAPI \u6865\u63A5\u670D\u52A1"),
  444. h2("7.1 \u5B89\u88C5 Python \u4F9D\u8D56"),
  445. ...gap(),
  446. codeBlock([
  447. "dnf install -y python3.11 python3.11-pip",
  448. "pip3.11 install fastapi uvicorn asyncpg python-dotenv",
  449. ]),
  450. ...gap(),
  451. h2("7.2 \u521B\u5EFA\u670D\u52A1\u76EE\u5F55"),
  452. ...gap(),
  453. codeBlock(["mkdir -p /redant/web/erp-bridge"]),
  454. ...gap(),
  455. h2("7.3 \u914D\u7F6E\u73AF\u5883\u53D8\u91CF"),
  456. ...gap(),
  457. p("\u521B\u5EFA /redant/web/erp-bridge/.env\uFF1A"),
  458. ...gap(),
  459. codeBlock([
  460. "ERP_DATABASE_URL=postgresql://chatbot_readonly:PASSWORD@ERP_DB_SERVER:5432/erp_db",
  461. "ERP_API_KEY=\uFF08\u4E0E chatbot .env.production \u4E2D ERP_API_KEY \u76F8\u540C\uFF09",
  462. "PORT=8080",
  463. ]),
  464. ...gap(),
  465. h2("7.4 ERP \u6570\u636E\u5E93\u53EA\u8BFB\u8D26\u53F7\uFF08\u7531 ERP DBA \u521B\u5EFA\uFF09"),
  466. ...gap(),
  467. codeBlock([
  468. "CREATE USER chatbot_readonly WITH PASSWORD 'readonly_password';",
  469. "GRANT SELECT ON TABLE",
  470. " items, contacts, sales_orders, sales_order_items, stock",
  471. " TO chatbot_readonly;",
  472. ]),
  473. ...gap(),
  474. h2("7.5 \u90E8\u7F72 Systemd \u670D\u52A1"),
  475. ...gap(),
  476. codeBlock([
  477. "cp /redant/web/homelegance-chatbot/deploy/erp-bridge.service /etc/systemd/system/",
  478. "systemctl daemon-reload",
  479. "systemctl enable --now erp-bridge",
  480. "systemctl status erp-bridge",
  481. ]),
  482. pageBreak(),
  483. // ── Ch8 ────────────────────────────────────────────────────────────
  484. h1("\u7B2C\u516B\u7AE0 \u66F4\u65B0\u90E8\u7F72\u6D41\u7A0B"),
  485. h2("8.1 \u5E38\u89C4\u4EE3\u7801\u66F4\u65B0"),
  486. ...gap(),
  487. codeBlock([
  488. "cd /redant/web/homelegance-chatbot",
  489. "git pull origin master",
  490. "pnpm install # \u5982\u6709\u65B0\u4F9D\u8D56",
  491. "pnpm build # \u91CD\u65B0\u6784\u5EFA",
  492. "pm2 reload homelegance-chat # \u96F6\u505C\u673A\u91CD\u8F7D",
  493. ]),
  494. ...gap(),
  495. h2("8.2 \u5982\u6709\u6570\u636E\u5E93 Schema \u53D8\u66F4"),
  496. ...gap(),
  497. codeBlock(["pnpm db:push"]),
  498. pageBreak(),
  499. // ── Ch9 ────────────────────────────────────────────────────────────
  500. h1("\u7B2C\u4E5D\u7AE0 \u9A8C\u8BC1\u68C0\u67E5\u6E05\u5355"),
  501. h2("9.1 \u670D\u52A1\u72B6\u6001\u68C0\u67E5"),
  502. ...gap(),
  503. makeTable(
  504. ["\u68C0\u67E5\u9879", "\u547D\u4EE4", "\u671F\u671B\u7ED3\u679C"],
  505. [
  506. ["Node.js \u670D\u52A1", "pm2 status", "online"],
  507. ["Apache \u670D\u52A1", "systemctl status httpd", "active (running)"],
  508. ["ERP Bridge", "systemctl status erp-bridge", "active (running)"],
  509. ["\u5065\u5EB7\u68C0\u67E5", "curl http://localhost:3000/api/trpc/system.health", "\u8FD4\u56DE 200"],
  510. ["\u524D\u7AEF\u8BBF\u95EE", "curl -I https://chat.homelegance.com", "HTTP 200"],
  511. ],
  512. [2200, 4500, 2326],
  513. ),
  514. ...gap(2),
  515. h2("9.2 \u65E5\u5FD7\u4F4D\u7F6E"),
  516. ...gap(),
  517. makeTable(
  518. ["\u65E5\u5FD7\u7C7B\u578B", "\u8DEF\u5F84"],
  519. [
  520. ["Node.js \u8F93\u51FA\u65E5\u5FD7", "/var/log/pm2/homelegance-chat-out.log"],
  521. ["Node.js \u9519\u8BEF\u65E5\u5FD7", "/var/log/pm2/homelegance-chat-error.log"],
  522. ["Apache \u8BBF\u95EE\u65E5\u5FD7", "/var/log/httpd/homelegance-chat-access.log"],
  523. ["Apache \u9519\u8BEF\u65E5\u5FD7", "/var/log/httpd/homelegance-chat-error.log"],
  524. ["ERP Bridge \u65E5\u5FD7", "journalctl -u erp-bridge -f"],
  525. ],
  526. [3000, 6026],
  527. ),
  528. pageBreak(),
  529. // ── Ch10 ───────────────────────────────────────────────────────────
  530. h1("\u7B2C\u5341\u7AE0 \u73AF\u5883\u53D8\u91CF\u5B8C\u6574\u53C2\u8003"),
  531. ...gap(),
  532. makeTable(
  533. ["\u53D8\u91CF\u540D", "\u8BF4\u660E", "\u793A\u4F8B\u503C", "\u5FC5\u586B"],
  534. [
  535. ["NODE_ENV", "\u8FD0\u884C\u73AF\u5883", "production", "\u662F"],
  536. ["PORT", "Node.js \u76D1\u542C\u7AEF\u53E3", "3000", "\u5426\uFF08\u9ED8\u8BA43000\uFF09"],
  537. ["DATABASE_URL", "Chatbot PostgreSQL \u8FDE\u63A5\u4E32", "postgresql://user:pwd@host:5432/db", "\u662F"],
  538. ["JWT_SECRET", "Session \u7B7E\u540D\u5BC6\u9470", "openssl rand -hex 32 \u751F\u6210", "\u662F"],
  539. ["ANTHROPIC_API_KEY", "Claude API \u5BC6\u9470", "sk-ant-...", "\u662F"],
  540. ["ERP_API_URL", "ERP Bridge \u5185\u7F51\u5730\u5740", "http://127.0.0.1:8080", "\u662F"],
  541. ["ERP_API_KEY", "ERP Bridge \u8BBF\u95EE\u5BC6\u9470", "\u968F\u673A\u5B57\u7B26\u4E32", "\u662F"],
  542. ["DEALER_PORTAL_SSO_SECRET","Dealer Portal SSO \u5171\u4EAB\u5BC6\u9470","Portal \u56E2\u961F\u7EA6\u5B9A", "SSO \u542F\u7528\u65F6"],
  543. ],
  544. [2600, 2800, 2400, 1226],
  545. ),
  546. pageBreak(),
  547. // ── Appendix ───────────────────────────────────────────────────────
  548. h1("\u9644\u5F55 Git \u4ED3\u5E93\u4E0E\u914D\u7F6E\u6587\u4EF6"),
  549. ...gap(),
  550. infoBox([
  551. "\uD83D\uDD17 \u4ED3\u5E93\u5730\u5740\uFF1A https://git.united-us.net/TonyT/chatbot.git",
  552. "\uD83C\uDF3F \u4E3B\u5206\u652F\uFF1A master",
  553. "\uD83D\uDCC2 \u914D\u7F6E\u6587\u4EF6\u76EE\u5F55\uFF1A deploy/",
  554. ]),
  555. ...gap(2),
  556. makeTable(
  557. ["\u6587\u4EF6\u540D", "\u7528\u9014", "\u90E8\u7F72\u76EE\u6807\u8DEF\u5F84"],
  558. [
  559. ["homelegance-chat.conf", "Apache \u865A\u62DF\u4E3B\u673A\u914D\u7F6E", "/etc/httpd/conf.d/"],
  560. ["erp-bridge.service", "ERP Bridge systemd \u670D\u52A1", "/etc/systemd/system/"],
  561. ["env.production.example", "\u73AF\u5883\u53D8\u91CF\u6A21\u677F", ".env.production\uFF08\u586B\u5199\u540E\uFF09"],
  562. ["ecosystem.config.cjs", "PM2 \u8FDB\u7A0B\u914D\u7F6E", "\u9879\u76EE\u6839\u76EE\u5F55\uFF08\u5DF2\u5305\u542B\uFF09"],
  563. ],
  564. [2800, 3000, 3226],
  565. ),
  566. ],
  567. }],
  568. });
  569. const out = "C:\\Codes\\homelegance-chatbot\\deploy\\Homelegance_Chatbot_\u90E8\u7F72\u6587\u6863.docx";
  570. Packer.toBuffer(doc).then(buf => {
  571. writeFileSync(out, buf);
  572. console.log("Created:", out);
  573. });