tkserver 1.7.23 → 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,234 @@
1
+ import { FULL_CAPABILITIES, LokiDatabase, MongoDatabase, createHandler, resetRequestTimes, scaffoldAdapters } from "@twikoojs/common";
2
+ import { createServer } from "node:http";
3
+ //#region src/database.ts
4
+ /**
5
+ * tkserver 数据库选择(1.x `server.js` 语义:`MONGODB_URI`/`MONGO_URL` → Mongo,
6
+ * 否则 Loki + `TWIKOO_DATA`)。
7
+ *
8
+ * 独立成文件的原因:启动路径需要**先**拿到数据库实例做 demo seed,
9
+ * 与逐请求惰性初始化共用同一份选择逻辑。
10
+ */
11
+ /**
12
+ * 按环境选择数据库实现。
13
+ * @param options 数据目录 / 连接串覆盖项
14
+ * @returns 数据库实例(尚未 init)
15
+ */
16
+ function createTkserverDatabase(options = {}) {
17
+ const mongoUri = options.mongoUri ?? process.env.MONGODB_URI ?? process.env.MONGO_URL ?? "";
18
+ return mongoUri ? new MongoDatabase({ uri: mongoUri }) : new LokiDatabase({ dataDir: options.dataDir ?? process.env.TWIKOO_DATA ?? "./data" });
19
+ }
20
+ //#endregion
21
+ //#region src/main.ts
22
+ /**
23
+ * tkserver 主逻辑(self-hosted 薄适配器)。
24
+ * 业务逻辑全部在 @twikoojs/common;数据库 MONGODB_URI→Mongo,否则 Loki(TWIKOO_DATA)。
25
+ * 平台核对:nodejs.org/api http/信号处理(查阅 2026-09-17)。
26
+ */
27
+ /** tkserver 平台能力:全能力*/
28
+ const tkserverCapabilities = FULL_CAPABILITIES;
29
+ /**
30
+ * Node 请求 → 内部统一请求(headers 小写化;x-real-ip/转发首跳取 IP)。
31
+ * @param req Node 请求
32
+ * @returns 内部统一请求
33
+ */
34
+ function toTkRequest(req) {
35
+ const headers = {};
36
+ for (const [key, value] of Object.entries(req.headers ?? {})) if (value !== void 0) headers[key.toLowerCase()] = Array.isArray(value) ? value[0] : value;
37
+ const forwarded = headers["x-forwarded-for"];
38
+ const ip = headers["x-real-ip"] ?? (forwarded ? forwarded.split(",")[0].trim() : "") ?? "";
39
+ const body = req.body && typeof req.body === "object" ? req.body : {};
40
+ return {
41
+ method: String(req.method ?? "POST").toUpperCase(),
42
+ path: String(req.url ?? "/"),
43
+ query: {},
44
+ body,
45
+ headers,
46
+ ip,
47
+ raw: req
48
+ };
49
+ }
50
+ /**
51
+ * 内部统一响应 → Node 响应(204 无体;业务 JSON 透传 tkRes.status;
52
+ * headers 原样回写——CORS 由 pipeline 算好,业务分支在其后补 Content-Type;
53
+ * 垫片语义对齐 1.x)。
54
+ */
55
+ function fromTkResponse(res, tkRes) {
56
+ if (tkRes.status === 204) {
57
+ res.writeHead(204, tkRes.headers);
58
+ res.end();
59
+ return;
60
+ }
61
+ res.writeHead(tkRes.status, {
62
+ ...tkRes.headers,
63
+ "Content-Type": "application/json"
64
+ });
65
+ res.end(JSON.stringify(tkRes.body));
66
+ }
67
+ /**
68
+ * 创建 tkserver 请求处理器(database 可注入;缺省按 MONGODB_URI/TWIKOO_DATA 选择)。
69
+ * @param options 注入项
70
+ * @returns 逐请求处理器
71
+ */
72
+ function createTkserverHandler(options = {}) {
73
+ let database = options.database ?? null;
74
+ /** 数据库选择(1.x server.js:MONGODB_URI → mongo,否则 loki) */
75
+ const getDatabase = async () => {
76
+ if (!database) database = createTkserverDatabase(options);
77
+ await database.init();
78
+ return database;
79
+ };
80
+ return async (req, res) => {
81
+ const request = toTkRequest(req);
82
+ fromTkResponse(res, await createHandler(scaffoldAdapters({
83
+ request: {
84
+ /** 请求恒等透传(归一化见 toTkRequest) */
85
+ toTkRequest: () => request },
86
+ response: {
87
+ /** TkResponse 恒等透传(写入见 fromTkResponse) */
88
+ fromTkResponse: (r) => r },
89
+ database: await getDatabase(),
90
+ capabilities: tkserverCapabilities
91
+ }))(request));
92
+ };
93
+ }
94
+ /**
95
+ * 优雅退出(1.x shutdown 语义:停限流清理定时器 → 关数据库)。
96
+ * @param options 清理项
97
+ */
98
+ async function shutdown(options = {}) {
99
+ if (options.timer) clearInterval(options.timer);
100
+ await options.database?.close?.();
101
+ }
102
+ /** requestTimes 清理周期(1.x TWIKOO_REQ_TIMES_CLEAR_TIME,缺省 10 分钟) */
103
+ function getRequestTimesClearInterval() {
104
+ return parseInt(process.env.TWIKOO_REQ_TIMES_CLEAR_TIME ?? "", 10) || 6e5;
105
+ }
106
+ /** 启动限流计数定时清理(返回定时器供 shutdown 停用) */
107
+ function startRequestTimesTimer() {
108
+ return setInterval(() => resetRequestTimes(), getRequestTimesClearInterval());
109
+ }
110
+ //#endregion
111
+ //#region src/server.ts
112
+ /**
113
+ * tkserver bin 入口(HTTP 服务器引导;业务逻辑在 @twikoojs/common + main.ts)。
114
+ * 1.x server.js 语义对齐:JSON body 解析、socket 跟踪、退出信号优雅关闭、
115
+ * 关闭期间 503。TWIKOO_SKIP_BOOT=1 时仅导出工厂不启动(测试进程内装配)。
116
+ */
117
+ /**
118
+ * 创建 tkserver 服务器实例(不监听、不注册信号——由 bin 底部或测试驱动)。
119
+ * @param options 注入项(数据库可注入,缺省按 MONGODB_URI/TWIKOO_DATA 选择)
120
+ * @returns 服务器实例与生命周期控制
121
+ */
122
+ function createTkserverServer(options = {}) {
123
+ const database = options.database ?? createTkserverDatabase();
124
+ const handler = createTkserverHandler({ database });
125
+ const timer = startRequestTimesTimer();
126
+ const sockets = /* @__PURE__ */ new Set();
127
+ let isShuttingDown = false;
128
+ const server = createServer((req, res) => {
129
+ if (isShuttingDown) {
130
+ res.writeHead(503, {
131
+ Connection: "close",
132
+ "Content-Type": "application/json"
133
+ });
134
+ res.end(JSON.stringify({
135
+ code: 503,
136
+ message: "Twikoo server is shutting down"
137
+ }));
138
+ return;
139
+ }
140
+ (async () => {
141
+ /** 聚合请求体(JSON 解析失败按空体处理,1.x 语义) */
142
+ const buffers = [];
143
+ for await (const chunk of req) buffers.push(chunk);
144
+ try {
145
+ req.body = JSON.parse(Buffer.concat(buffers).toString() || "{}");
146
+ } catch {
147
+ req.body = {};
148
+ }
149
+ /** status/json 垫片(1.x server.js 对齐) */
150
+ const shimmed = res;
151
+ shimmed.status = (code) => {
152
+ res.statusCode = code;
153
+ return shimmed;
154
+ };
155
+ shimmed.json = (json) => {
156
+ if (!res.writableEnded) {
157
+ res.writeHead(200, { "Content-Type": "application/json" });
158
+ res.end(JSON.stringify(json));
159
+ }
160
+ return shimmed;
161
+ };
162
+ await handler(req, shimmed);
163
+ })().catch(() => {
164
+ if (!res.writableEnded) {
165
+ res.writeHead(200, { "Content-Type": "application/json" });
166
+ res.end(JSON.stringify({
167
+ code: 1e3,
168
+ message: "internal error"
169
+ }));
170
+ }
171
+ });
172
+ });
173
+ server.on("connection", (socket) => {
174
+ sockets.add(socket);
175
+ socket.on("close", () => sockets.delete(socket));
176
+ });
177
+ /** 优雅关闭(关监听 → 排空连接 → shutdown 资源) */
178
+ const gracefulShutdown = async () => {
179
+ if (isShuttingDown) return;
180
+ isShuttingDown = true;
181
+ await new Promise((resolve) => server.close(() => resolve()));
182
+ for (const socket of sockets) socket.destroy();
183
+ await shutdown({ timer });
184
+ };
185
+ /** SIGTERM / SIGINT → 优雅退出(超时兜底强杀) */
186
+ const registerSignalHandlers = () => {
187
+ const timeout = parseInt(process.env.TWIKOO_SHUTDOWN_TIMEOUT ?? "", 10) || 5e3;
188
+ /** 信号回调:竞速优雅关闭与超时兜底 */
189
+ const onSignal = () => {
190
+ Promise.race([gracefulShutdown(), new Promise((resolve) => setTimeout(resolve, timeout))]).then(() => process.exit(0));
191
+ };
192
+ process.on("SIGTERM", onSignal);
193
+ process.on("SIGINT", onSignal);
194
+ };
195
+ return {
196
+ server,
197
+ database,
198
+ gracefulShutdown,
199
+ registerSignalHandlers
200
+ };
201
+ }
202
+ /**
203
+ * 启动 tkserver(bin `dist/server.js` 与 `twikoo-pkg` 的 SEA 产物共用入口):
204
+ * 可选 demo seed → 监听端口 → 注册信号处理。
205
+ * @param options 注入项(数据库可注入,缺省按 MONGODB_URI/TWIKOO_DATA 选择)
206
+ * @returns 已进入监听的服务器实例
207
+ */
208
+ async function startTkserver(options = {}) {
209
+ const instance = createTkserverServer(options);
210
+ const { server, database, registerSignalHandlers } = instance;
211
+ const port = parseInt(process.env.TWIKOO_PORT ?? "", 10) || 8080;
212
+ const host = process.env.TWIKOO_HOST ?? (process.env.TWIKOO_LOCALHOST_ONLY === "true" ? "localhost" : "::");
213
+ if (process.env.TWIKOO_SEED === "1") try {
214
+ /** 动态 import:未开启 seed 时本模块不会被加载(生产不可触达第一道防线) */
215
+ const { seedDemoData } = await import("./seed-C8kiwlWr.mjs");
216
+ await seedDemoData({ database });
217
+ } catch (e) {
218
+ /** seed 属演示辅助:失败不阻断服务启动,但必须显式报错(不静默) */
219
+ console.error("[twikoo-seed] seed 失败,服务仍将启动:", e);
220
+ }
221
+ await new Promise((resolve) => {
222
+ server.listen(port, host, () => {
223
+ const actual = server.address().port;
224
+ console.log(`Twikoo server started on host ${host} port ${actual}`);
225
+ resolve();
226
+ });
227
+ });
228
+ registerSignalHandlers();
229
+ return instance;
230
+ }
231
+ //#endregion
232
+ export { createTkserverDatabase, createTkserverHandler, createTkserverServer, fromTkResponse, getRequestTimesClearInterval, shutdown, startRequestTimesTimer, startTkserver, toTkRequest };
233
+
234
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/database.ts","../src/main.ts","../src/server.ts"],"sourcesContent":["/**\n * tkserver 数据库选择(1.x `server.js` 语义:`MONGODB_URI`/`MONGO_URL` → Mongo,\n * 否则 Loki + `TWIKOO_DATA`)。\n *\n * 独立成文件的原因:启动路径需要**先**拿到数据库实例做 demo seed,\n * 与逐请求惰性初始化共用同一份选择逻辑。\n */\nimport { LokiDatabase, MongoDatabase, type Database } from \"@twikoojs/common\";\n\n/**\n * 按环境选择数据库实现。\n * @param options 数据目录 / 连接串覆盖项\n * @returns 数据库实例(尚未 init)\n */\nexport function createTkserverDatabase(\n options: { dataDir?: string; mongoUri?: string } = {},\n): Database {\n const mongoUri = options.mongoUri ?? process.env.MONGODB_URI ?? process.env.MONGO_URL ?? \"\";\n return mongoUri\n ? new MongoDatabase({ uri: mongoUri })\n : new LokiDatabase({ dataDir: options.dataDir ?? process.env.TWIKOO_DATA ?? \"./data\" });\n}\n","/**\n * tkserver 主逻辑(self-hosted 薄适配器)。\n * 业务逻辑全部在 @twikoojs/common;数据库 MONGODB_URI→Mongo,否则 Loki(TWIKOO_DATA)。\n * 平台核对:nodejs.org/api http/信号处理(查阅 2026-09-17)。\n */\nimport {\n FULL_CAPABILITIES,\n createHandler,\n resetRequestTimes,\n scaffoldAdapters,\n type Database,\n type TkRequest,\n type TkResponse,\n} from \"@twikoojs/common\";\nimport { createTkserverDatabase } from \"./database\";\n\n/** tkserver 平台能力:全能力*/\nconst tkserverCapabilities = FULL_CAPABILITIES;\n\n/** Node 请求的最小结构面(IncomingMessage;body 已由 server.ts 解析挂载) */\nexport interface ServerRequestLike {\n method?: string;\n headers: Record<string, string | string[] | undefined>;\n url?: string;\n body?: unknown;\n}\n\n/** Node 响应的最小结构面(ServerResponse + 1.x status/json 垫片) */\nexport interface ServerResponseLike {\n statusCode?: number;\n writableEnded?: boolean;\n writeHead(code: number, headers: Record<string, string>): unknown;\n end(body?: string): unknown;\n status(code: number): ServerResponseLike;\n json(body: unknown): ServerResponseLike;\n}\n\n/**\n * Node 请求 → 内部统一请求(headers 小写化;x-real-ip/转发首跳取 IP)。\n * @param req Node 请求\n * @returns 内部统一请求\n */\nexport function toTkRequest(req: ServerRequestLike): TkRequest {\n const headers: Record<string, string> = {};\n for (const [key, value] of Object.entries(req.headers ?? {})) {\n if (value !== undefined) headers[key.toLowerCase()] = Array.isArray(value) ? value[0] : value;\n }\n const forwarded = headers[\"x-forwarded-for\"];\n const ip = headers[\"x-real-ip\"] ?? (forwarded ? forwarded.split(\",\")[0].trim() : \"\") ?? \"\";\n const body = (req.body && typeof req.body === \"object\" ? req.body : {}) as TkRequest[\"body\"];\n return {\n method: String(req.method ?? \"POST\").toUpperCase(),\n path: String(req.url ?? \"/\"),\n query: {},\n body,\n headers,\n ip,\n raw: req,\n };\n}\n\n/**\n * 内部统一响应 → Node 响应(204 无体;业务 JSON 透传 tkRes.status;\n * headers 原样回写——CORS 由 pipeline 算好,业务分支在其后补 Content-Type;\n * 垫片语义对齐 1.x)。\n */\nexport function fromTkResponse(res: ServerResponseLike, tkRes: TkResponse): void {\n if (tkRes.status === 204) {\n res.writeHead(204, tkRes.headers);\n res.end();\n return;\n }\n res.writeHead(tkRes.status, { ...tkRes.headers, \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(tkRes.body));\n}\n\n/**\n * 创建 tkserver 请求处理器(database 可注入;缺省按 MONGODB_URI/TWIKOO_DATA 选择)。\n * @param options 注入项\n * @returns 逐请求处理器\n */\nexport function createTkserverHandler(\n options: { database?: Database; dataDir?: string; mongoUri?: string } = {},\n): (req: ServerRequestLike, res: ServerResponseLike) => Promise<void> {\n let database: Database | null = options.database ?? null;\n /** 数据库选择(1.x server.js:MONGODB_URI → mongo,否则 loki) */\n const getDatabase = async (): Promise<Database> => {\n if (!database) database = createTkserverDatabase(options);\n await database.init();\n return database;\n };\n return async (req, res) => {\n const request = toTkRequest(req);\n const handler = createHandler(\n scaffoldAdapters({\n request: {\n /** 请求恒等透传(归一化见 toTkRequest) */\n toTkRequest: () => request,\n },\n response: {\n /** TkResponse 恒等透传(写入见 fromTkResponse) */\n fromTkResponse: (r: TkResponse) => r,\n },\n database: await getDatabase(),\n capabilities: tkserverCapabilities,\n // 后置副作用派发**刻意不传**:tkserver 是常驻进程,scaffoldAdapters 的\n // 默认实现即「进程内直调 postSubmit 服务且不 await」——1.x\n // `postSubmit(comment)` 语义(见 src/server/self-hosted/index.js)。\n }),\n );\n fromTkResponse(res, await handler(request));\n };\n}\n\n/**\n * 优雅退出(1.x shutdown 语义:停限流清理定时器 → 关数据库)。\n * @param options 清理项\n */\nexport async function shutdown(\n options: {\n timer?: NodeJS.Timeout | null;\n database?: Database | null;\n } = {},\n): Promise<void> {\n if (options.timer) clearInterval(options.timer);\n await options.database?.close?.();\n}\n\n/** requestTimes 清理周期(1.x TWIKOO_REQ_TIMES_CLEAR_TIME,缺省 10 分钟) */\nexport function getRequestTimesClearInterval(): number {\n return parseInt(process.env.TWIKOO_REQ_TIMES_CLEAR_TIME ?? \"\", 10) || 10 * 60 * 1000;\n}\n\n/** 启动限流计数定时清理(返回定时器供 shutdown 停用) */\nexport function startRequestTimesTimer(): NodeJS.Timeout {\n return setInterval(() => resetRequestTimes(), getRequestTimesClearInterval());\n}\n","/**\n * tkserver bin 入口(HTTP 服务器引导;业务逻辑在 @twikoojs/common + main.ts)。\n * 1.x server.js 语义对齐:JSON body 解析、socket 跟踪、退出信号优雅关闭、\n * 关闭期间 503。TWIKOO_SKIP_BOOT=1 时仅导出工厂不启动(测试进程内装配)。\n */\nimport { createServer, type Server } from \"node:http\";\nimport type { AddressInfo, Socket } from \"node:net\";\nimport {\n createTkserverHandler,\n getRequestTimesClearInterval,\n shutdown,\n startRequestTimesTimer,\n} from \"./main\";\nimport { createTkserverDatabase } from \"./database\";\nimport type { Database } from \"@twikoojs/common\";\n\n/** 垫片后的响应形态(Node 响应 + status/json) */\ntype ServerResponseShim = import(\"./main\").ServerResponseLike;\n\n/** 垫片中的请求形态(body 已解析挂载) */\ntype ServerRequestShim = import(\"./main\").ServerRequestLike & { body?: unknown };\n\n/** 服务器实例与生命周期控制(createTkserverServer 产物) */\nexport interface TkserverInstance {\n /** HTTP 服务器(未监听;listen 由调用方执行) */\n server: Server;\n /** 已装配的数据库实例(供启动期 seed 复用)*/\n database: Database;\n /** 优雅关闭(关监听 → 排空连接 → 停定时器 → 关数据库) */\n gracefulShutdown: () => Promise<void>;\n /** 注册 SIGTERM/SIGINT 处理(TWIKOO_SHUTDOWN_TIMEOUT 兜底强杀) */\n registerSignalHandlers: () => void;\n}\n\n/**\n * 创建 tkserver 服务器实例(不监听、不注册信号——由 bin 底部或测试驱动)。\n * @param options 注入项(数据库可注入,缺省按 MONGODB_URI/TWIKOO_DATA 选择)\n * @returns 服务器实例与生命周期控制\n */\nexport function createTkserverServer(options: { database?: Database } = {}): TkserverInstance {\n const database = options.database ?? createTkserverDatabase();\n const handler = createTkserverHandler({ database });\n const timer = startRequestTimesTimer();\n const sockets = new Set<Socket>();\n let isShuttingDown = false;\n\n const server = createServer((req, res) => {\n if (isShuttingDown) {\n res.writeHead(503, { Connection: \"close\", \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ code: 503, message: \"Twikoo server is shutting down\" }));\n return;\n }\n void (async () => {\n /** 聚合请求体(JSON 解析失败按空体处理,1.x 语义) */\n const buffers: Buffer[] = [];\n for await (const chunk of req) buffers.push(chunk as Buffer);\n try {\n (req as ServerRequestShim).body = JSON.parse(Buffer.concat(buffers).toString() || \"{}\");\n } catch {\n (req as ServerRequestShim).body = {};\n }\n /** status/json 垫片(1.x server.js 对齐) */\n const shimmed = res as unknown as ServerResponseShim;\n shimmed.status = (code: number) => {\n res.statusCode = code;\n return shimmed;\n };\n shimmed.json = (json: unknown) => {\n if (!res.writableEnded) {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify(json));\n }\n return shimmed;\n };\n await handler(req, shimmed);\n })().catch(() => {\n if (!res.writableEnded) {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ code: 1000, message: \"internal error\" }));\n }\n });\n });\n\n server.on(\"connection\", (socket) => {\n sockets.add(socket);\n socket.on(\"close\", () => sockets.delete(socket));\n });\n\n /** 优雅关闭(关监听 → 排空连接 → shutdown 资源) */\n const gracefulShutdown = async (): Promise<void> => {\n if (isShuttingDown) return;\n isShuttingDown = true;\n await new Promise<void>((resolve) => server.close(() => resolve()));\n for (const socket of sockets) socket.destroy();\n await shutdown({ timer });\n };\n\n /** SIGTERM / SIGINT → 优雅退出(超时兜底强杀) */\n const registerSignalHandlers = (): void => {\n const timeout = parseInt(process.env.TWIKOO_SHUTDOWN_TIMEOUT ?? \"\", 10) || 5000;\n /** 信号回调:竞速优雅关闭与超时兜底 */\n const onSignal = (): void => {\n void Promise.race([\n gracefulShutdown(),\n new Promise((resolve) => setTimeout(resolve, timeout)),\n ]).then(() => process.exit(0));\n };\n process.on(\"SIGTERM\", onSignal);\n process.on(\"SIGINT\", onSignal);\n };\n\n return { server, database, gracefulShutdown, registerSignalHandlers };\n}\n\n/**\n * 启动 tkserver(bin `dist/server.js` 与 `twikoo-pkg` 的 SEA 产物共用入口):\n * 可选 demo seed → 监听端口 → 注册信号处理。\n * @param options 注入项(数据库可注入,缺省按 MONGODB_URI/TWIKOO_DATA 选择)\n * @returns 已进入监听的服务器实例\n */\nexport async function startTkserver(\n options: { database?: Database } = {},\n): Promise<TkserverInstance> {\n const instance = createTkserverServer(options);\n const { server, database, registerSignalHandlers } = instance;\n const port = parseInt(process.env.TWIKOO_PORT ?? \"\", 10) || 8080;\n const host =\n process.env.TWIKOO_HOST ?? (process.env.TWIKOO_LOCALHOST_ONLY === \"true\" ? \"localhost\" : \"::\");\n\n if (process.env.TWIKOO_SEED === \"1\") {\n try {\n /** 动态 import:未开启 seed 时本模块不会被加载(生产不可触达第一道防线) */\n const { seedDemoData } = await import(\"./seed\");\n await seedDemoData({ database });\n } catch (e) {\n /** seed 属演示辅助:失败不阻断服务启动,但必须显式报错(不静默) */\n console.error(\"[twikoo-seed] seed 失败,服务仍将启动:\", e);\n }\n }\n\n await new Promise<void>((resolve) => {\n server.listen(port, host, () => {\n const actual = (server.address() as AddressInfo).port;\n console.log(`Twikoo server started on host ${host} port ${actual}`);\n resolve();\n });\n });\n registerSignalHandlers();\n void getRequestTimesClearInterval;\n return instance;\n}\n"],"mappings":"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"}
@@ -0,0 +1,337 @@
1
+ let node_crypto = require("node:crypto");
2
+ //#region src/seed/fixtures.ts
3
+ /**
4
+ * demo 测试数据 fixtures(设计文档「无数据库时自动生成测试数据」)。
5
+ *
6
+ * 覆盖 11 项场景表;**url 一律 `/demo.html`**(另设第二路径用于分页场景)。
7
+ * 数据为静态常量 + 少量按「基准时间」推导的时间戳,保证每次 seed 结构可复现。
8
+ * 本文件只在显式开启 seed 时被加载(见 `./index.ts` 与 `../server.ts` 的双重守卫)。
9
+ */
10
+ /** demo 页路径(测试数据 url 必须为 `/demo.html`,与 demo 页默认 path 一致)*/
11
+ const DEMO_URL = "/demo.html";
12
+ /** 第二路径:用于「多个 url 路径的评论 → 分页 + 仅显示当前页评论」场景 */
13
+ const OTHER_URL = "/demo-other.html";
14
+ /** 博主昵称(与配置 BLOGGER_NICK 一致,用于 master 徽标) */
15
+ const DEMO_BLOGGER_NICK = "iMaeGoo";
16
+ /**
17
+ * 场景清单(覆盖表逐项)——供 seed 日志与单测逐项断言,避免「表里写了但没造」。
18
+ */
19
+ const DEMO_SCENARIOS = [
20
+ "普通评论(含 _id/url/nick/mail/link/ua/ip)",
21
+ "嵌套回复(多层 pid/rid)",
22
+ "含 owo 表情的评论",
23
+ "含公式的评论($…$ / $$…$$)",
24
+ "含代码块的评论",
25
+ "含链接、图片的评论",
26
+ "已点赞评论(like 字段有值)",
27
+ "标记为垃圾的评论(isSpam: true)",
28
+ "多个 url 路径的评论(分页)",
29
+ "含访客计数的配置",
30
+ "默认配置对象(配置面板全部字段有值)"
31
+ ];
32
+ /** demo 图片(内联 data URI:离线也能显示,避免 demo 依赖外网) */
33
+ const DEMO_IMAGE = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNjAiIGhlaWdodD0iNjAiPjxyZWN0IHdpZHRoPSIxNjAiIGhlaWdodD0iNjAiIGZpbGw9IiMxODVGQTUiLz48dGV4dCB4PSI4MCIgeT0iMzYiIGZvbnQtc2l6ZT0iMTQiIGZpbGw9IiNmZmYiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlR3aWtvbyBEZW1vPC90ZXh0Pjwvc3ZnPg==";
34
+ /**
35
+ * 默认配置对象(场景 11:配置面板**全部字段有值**)。
36
+ *
37
+ * 键集合 = 1.7.24 管理面板配置项全集(`TkAdminConfig.vue` 的 `key:` 清单);
38
+ * 凭证类字段填明显非真实的占位值,仅用于「面板有值」演示。
39
+ */
40
+ const DEMO_CONFIG = {
41
+ SITE_NAME: "Twikoo Demo",
42
+ SITE_URL: "http://localhost:9820",
43
+ CORS_ALLOW_ORIGIN: "http://localhost:9820",
44
+ BLOGGER_NICK: DEMO_BLOGGER_NICK,
45
+ BLOGGER_EMAIL: "hello@imaegoo.com",
46
+ MASTER_TAG: "站长",
47
+ COMMENT_PAGE_SIZE: "8",
48
+ COMMENT_BG_IMG: "",
49
+ COMMENT_PLACEHOLDER: "请友善评论,支持 Markdown / 表情 / 公式 / 代码块",
50
+ GRAVATAR_CDN: "cravatar.cn",
51
+ DEFAULT_GRAVATAR: "mp",
52
+ SHOW_ORDER: "true",
53
+ SHOW_DISLIKE: "true",
54
+ SHOW_EMOTION: "true",
55
+ SHOW_IMAGE: "true",
56
+ SHOW_UA: "true",
57
+ SHOW_REGION: "true",
58
+ LIGHTBOX: "false",
59
+ DISPLAYED_FIELDS: "nick,mail,link",
60
+ REQUIRED_FIELDS: "nick,mail",
61
+ HIDE_ADMIN_CRYPT: "admin",
62
+ HIDE_SPAM: "false",
63
+ LIMIT_LENGTH: "500",
64
+ LIMIT_PER_MINUTE: "3",
65
+ LIMIT_PER_MINUTE_ALL: "10",
66
+ EMOTION_CDN: "https://owo.imaegoo.com/owo.json",
67
+ IMAGE_CDN: "none",
68
+ IMAGE_CDN_URL: "https://piclist.example.com",
69
+ IMAGE_CDN_TOKEN: "demo-token",
70
+ HIGHLIGHT: "true",
71
+ HIGHLIGHT_THEME: "default",
72
+ HIGHLIGHT_PLUGIN: "showLanguage,copyButton",
73
+ CAPTCHA_PROVIDER: "",
74
+ TURNSTILE_SITE_KEY: "demo-turnstile-site-key",
75
+ TURNSTILE_SECRET_KEY: "demo-turnstile-secret-key",
76
+ GEETEST_CAPTCHA_ID: "demo-geetest-id",
77
+ GEETEST_CAPTCHA_KEY: "demo-geetest-key",
78
+ CAP_API_ENDPOINT: "",
79
+ CAP_SECRET_KEY: "demo-cap-secret",
80
+ QQ_API_KEY: "demo-qq-api-key",
81
+ SC_MAIL_NOTIFY: "true",
82
+ SENDER_NAME: "Twikoo Demo",
83
+ SENDER_EMAIL: "noreply@example.com",
84
+ SMTP_SERVICE: "QQ",
85
+ SMTP_HOST: "smtp.example.com",
86
+ SMTP_PORT: "465",
87
+ SMTP_SECURE: "true",
88
+ SMTP_USER: "demo@example.com",
89
+ SMTP_PASS: "demo-smtp-pass",
90
+ MAIL_SUBJECT: "${SITE_NAME} 有新评论",
91
+ MAIL_SUBJECT_ADMIN: "${SITE_NAME} 有新评论(管理员)",
92
+ MAIL_TEMPLATE: "<p>${PARENT_NICK} 回复了你的评论:</p><p>${COMMENT}</p>",
93
+ MAIL_TEMPLATE_ADMIN: "<p>${NICK} 在 ${POST_URL} 评论:</p><p>${COMMENT}</p>",
94
+ PUSHOO_CHANNEL: "none",
95
+ PUSHOO_TOKEN: "",
96
+ NOTIFY_SPAM: "false",
97
+ AKISMET_KEY: "demo-akismet-key",
98
+ BLOCKED_WORDS: "测试屏蔽词,广告",
99
+ FORBIDDEN_WORDS: "违规词",
100
+ NSFW_API_URL: "https://nsfw.example.com/api",
101
+ NSFW_THRESHOLD: "0.8",
102
+ LLM_API_ENDPOINT: "https://api.example.com/v1",
103
+ LLM_API_KEY: "demo-llm-api-key",
104
+ LLM_MODEL: "demo-model",
105
+ LLM_MAX_RETRIES: "3",
106
+ LLM_SPAM_PROMPT: "判断这条评论是否为垃圾评论",
107
+ QCLOUD_CMS_BIZTYPE: "twikoo",
108
+ QCLOUD_SECRET_ID: "demo-qcloud-secret-id",
109
+ QCLOUD_SECRET_KEY: "demo-qcloud-secret-key",
110
+ S3_ENDPOINT: "https://s3.example.com",
111
+ S3_REGION: "ap-shanghai",
112
+ S3_BUCKET: "twikoo-demo",
113
+ S3_ACCESS_KEY_ID: "demo-s3-access-key-id",
114
+ S3_SECRET_ACCESS_KEY: "demo-s3-secret-access-key",
115
+ S3_PATH_PREFIX: "twikoo/",
116
+ S3_CDN_URL: "https://cdn.example.com",
117
+ S3_FORCE_PATH_STYLE: "false"
118
+ };
119
+ /**
120
+ * 计算邮箱哈希(1.x `mailMd5` 语义:头像源;此处用 md5 与 Gravatar/Cravatar 默认形态一致)。
121
+ * @param mail 邮箱
122
+ * @returns 32 位小写十六进制哈希
123
+ */
124
+ function mailHash(mail) {
125
+ return (0, node_crypto.createHash)("md5").update(mail).digest("hex");
126
+ }
127
+ /** 构造 fixture 公共字段(普通评论基础形态) */
128
+ function baseFixture(index, now, overrides = {}) {
129
+ /** 序号越小越新(created 倒序展示) */
130
+ const created = now - index * 36e5;
131
+ return {
132
+ uid: `seed-uid-${index}`,
133
+ ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Twikoo-Demo-Seed",
134
+ ip: "127.0.0.1",
135
+ url: DEMO_URL,
136
+ href: `http://localhost:9820${DEMO_URL}`,
137
+ created,
138
+ updated: created,
139
+ isSpam: false,
140
+ like: [],
141
+ ...overrides
142
+ };
143
+ }
144
+ /**
145
+ * 构造全部 demo 评论 fixture(场景 1–9)。
146
+ * @param now 基准时间戳(毫秒)
147
+ * @returns fixture 列表(顶级在前、回复在后,seed 按序写入)
148
+ */
149
+ function buildDemoComments(now) {
150
+ const list = [];
151
+ list.push(baseFixture(0, now, {
152
+ ref: "master",
153
+ nick: DEMO_BLOGGER_NICK,
154
+ mail: "hello@imaegoo.com",
155
+ mailMd5: mailHash("hello@imaegoo.com"),
156
+ link: "https://www.imaegoo.com",
157
+ master: true,
158
+ like: [
159
+ "seed-uid-1",
160
+ "seed-uid-2",
161
+ "seed-uid-3"
162
+ ],
163
+ comment: `<p>欢迎来到 Twikoo 2.0 本地演示。这条是博主评论,含链接 <a href="https://twikoo.js.org" target="_blank" rel="noopener noreferrer nofollow ugc">Twikoo 官网</a>、图片 <img src="${DEMO_IMAGE}" alt="demo"> 与内联代码 <code>twikoo.init()</code>。</p>`
164
+ }), baseFixture(1, now, {
165
+ ref: "plain",
166
+ nick: "路过的访客",
167
+ mail: "visitor@example.com",
168
+ mailMd5: mailHash("visitor@example.com"),
169
+ link: "",
170
+ comment: "<p>普通评论一条,用来验证基础渲染与昵称/邮箱头像。</p>"
171
+ }), baseFixture(2, now, {
172
+ ref: "owo",
173
+ nick: "表情党",
174
+ mail: "owo@example.com",
175
+ mailMd5: mailHash("owo@example.com"),
176
+ comment: "<p>owo 表情测试::QQ: :tv_doge: :B站: 以及文字混排。</p>"
177
+ }), baseFixture(3, now, {
178
+ ref: "formula",
179
+ nick: "数学老师",
180
+ mail: "math@example.com",
181
+ mailMd5: mailHash("math@example.com"),
182
+ comment: "<p>行内公式 $E = mc^2$ 与块级公式:</p><p>$$\\int_{0}^{1} x^{2}\\,dx = \\frac{1}{3}$$</p>"
183
+ }), baseFixture(4, now, {
184
+ ref: "code",
185
+ nick: "工程师",
186
+ mail: "dev@example.com",
187
+ mailMd5: mailHash("dev@example.com"),
188
+ comment: "<p>代码块测试(期望 Prism 高亮):</p><pre><code class=\"language-js\">const twikoo = window.twikoo;\ntwikoo.init({ envId: \"http://localhost:8080\", el: \"#tcomment\" });\n</code></pre>"
189
+ }));
190
+ list.push(baseFixture(5, now, {
191
+ ref: "reply-1",
192
+ pidRef: "master",
193
+ ridRef: "master",
194
+ nick: "提问者",
195
+ mail: "ask@example.com",
196
+ mailMd5: mailHash("ask@example.com"),
197
+ comment: "<p>请问多层回复怎么展示?</p>"
198
+ }), baseFixture(6, now, {
199
+ ref: "reply-2",
200
+ pidRef: "reply-1",
201
+ ridRef: "master",
202
+ nick: DEMO_BLOGGER_NICK,
203
+ mail: "hello@imaegoo.com",
204
+ mailMd5: mailHash("hello@imaegoo.com"),
205
+ master: true,
206
+ comment: "<p>像这样一层层缩进展示(pid 指直接上级,rid 指根评论)。</p>"
207
+ }), baseFixture(7, now, {
208
+ ref: "reply-3",
209
+ pidRef: "reply-2",
210
+ ridRef: "master",
211
+ nick: "提问者",
212
+ mail: "ask@example.com",
213
+ mailMd5: mailHash("ask@example.com"),
214
+ comment: "<p>明白了,第三层也能正常显示。</p>"
215
+ }));
216
+ for (let i = 0; i < 5; i += 1) {
217
+ const index = 8 + i;
218
+ list.push(baseFixture(index, now, {
219
+ ref: `filler-${i}`,
220
+ nick: `分页测试用户 ${i + 1}`,
221
+ mail: `pager${i + 1}@example.com`,
222
+ mailMd5: mailHash(`pager${i + 1}@example.com`),
223
+ comment: `<p>第 ${i + 1} 条分页测试评论(与其它评论一起超过 COMMENT_PAGE_SIZE,用于验证分页)。</p>`
224
+ }));
225
+ }
226
+ list.push(baseFixture(20, now, {
227
+ ref: "spam",
228
+ nick: "spammer",
229
+ mail: "spam@example.com",
230
+ mailMd5: mailHash("spam@example.com"),
231
+ isSpam: true,
232
+ comment: "<p>这条评论被标记为垃圾评论(isSpam: true),访客不可见、管理员可筛选。</p>"
233
+ }));
234
+ for (let i = 0; i < 3; i += 1) {
235
+ const index = 21 + i;
236
+ list.push(baseFixture(index, now, {
237
+ ref: `other-${i}`,
238
+ url: OTHER_URL,
239
+ href: `http://localhost:9820${OTHER_URL}`,
240
+ nick: `另一页用户 ${i + 1}`,
241
+ mail: `other${i + 1}@example.com`,
242
+ mailMd5: mailHash(`other${i + 1}@example.com`),
243
+ comment: `<p>属于 ${OTHER_URL} 的评论,不应出现在 demo 页的评论列表里。</p>`
244
+ }));
245
+ }
246
+ return list;
247
+ }
248
+ //#endregion
249
+ //#region src/seed/index.ts
250
+ /** seed 开关环境变量名(`TWIKOO_SEED=1` 或 `pnpm demo`)*/
251
+ const SEED_ENV_KEY = "TWIKOO_SEED";
252
+ /** 生产误触达错误(未开启 seed 却调用即抛,绝不静默写入)*/
253
+ var SeedNotAllowedError = class extends Error {
254
+ /**
255
+ * @param envKey 开关环境变量名
256
+ */
257
+ constructor(envKey) {
258
+ super(`demo seed 未开启:仅当 ${envKey}=1 时允许执行(生产环境请勿设置该变量)。本次调用未写入任何数据。`);
259
+ this.name = "SeedNotAllowedError";
260
+ }
261
+ };
262
+ /**
263
+ * 判断 seed 是否开启(缺省读进程环境变量;显式传 env 便于单测)。
264
+ * @param env 环境变量表(缺省 `process.env`)
265
+ * @returns true 表示允许执行 seed
266
+ */
267
+ function isSeedEnabled(env = process.env) {
268
+ return env[SEED_ENV_KEY] === "1";
269
+ }
270
+ /**
271
+ * 写入 demo 测试数据。已有评论时**不改动**任何数据。
272
+ * @param options seed 选项
273
+ * @returns seed 结果
274
+ */
275
+ async function seedDemoData(options) {
276
+ const { database } = options;
277
+ const env = options.env ?? process.env;
278
+ const log = options.log ?? ((message) => console.log(message));
279
+ if (!isSeedEnabled(env)) throw new SeedNotAllowedError(SEED_ENV_KEY);
280
+ await database.init();
281
+ /** 三态检测:任何已有评论都视为「有数据」→ 直接使用不改动 */
282
+ const existing = await database.countComments({});
283
+ if (existing > 0) {
284
+ const visitors = (await database.getCounter("/demo.html"))?.time ?? 0;
285
+ const reason = `已存在 ${existing} 条评论,跳过 seed(不改动现有数据)`;
286
+ log(`[twikoo-seed] ${reason}`);
287
+ return {
288
+ seeded: false,
289
+ comments: existing,
290
+ visitors,
291
+ reason
292
+ };
293
+ }
294
+ /** 顶级评论先写(拿回 `_id`),回复再按 ref 解析 pid/rid —— 与 1.x 提交链路同序 */
295
+ const fixtures = buildDemoComments(Date.now());
296
+ /** ref → 真实 `_id` */
297
+ const idByRef = /* @__PURE__ */ new Map();
298
+ /** 已写入条数 */
299
+ let written = 0;
300
+ for (const fixture of fixtures) {
301
+ if (fixture.pidRef !== void 0) continue;
302
+ const { ref, pidRef, ridRef, ...doc } = fixture;
303
+ const saved = await database.addComment(doc);
304
+ if (ref !== void 0 && saved._id !== void 0) idByRef.set(ref, saved._id);
305
+ written += 1;
306
+ }
307
+ for (const fixture of fixtures) {
308
+ if (fixture.pidRef === void 0) continue;
309
+ const { ref, pidRef, ridRef, ...doc } = fixture;
310
+ const pid = idByRef.get(pidRef) ?? "";
311
+ const rid = ridRef !== void 0 ? idByRef.get(ridRef) ?? "" : "";
312
+ const saved = await database.addComment({
313
+ ...doc,
314
+ pid,
315
+ rid
316
+ });
317
+ if (ref !== void 0 && saved._id !== void 0) idByRef.set(ref, saved._id);
318
+ written += 1;
319
+ }
320
+ /** 场景 11:默认配置对象(配置面板全部字段有值) */
321
+ await database.saveConfig(DEMO_CONFIG);
322
+ /** 场景 10:访客计数(1.x 只有 incCounter 自增语义,故循环写入) */
323
+ for (let i = 0; i < 42; i += 1) await database.incCounter(DEMO_URL, "Twikoo Demo");
324
+ for (let i = 0; i < 3; i += 1) await database.incCounter(OTHER_URL, "Twikoo Demo 第二页");
325
+ const reason = `已生成 ${written} 条测试评论(覆盖 ${DEMO_SCENARIOS.length} 项场景),配置 ${Object.keys(DEMO_CONFIG).length} 项,访客计数 42`;
326
+ log(`[twikoo-seed] ${reason}`);
327
+ return {
328
+ seeded: true,
329
+ comments: written,
330
+ visitors: 42,
331
+ reason
332
+ };
333
+ }
334
+ //#endregion
335
+ exports.seedDemoData = seedDemoData;
336
+
337
+ //# sourceMappingURL=seed-BAmtKrez.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seed-BAmtKrez.js","names":["createHash"],"sources":["../src/seed/fixtures.ts","../src/seed/index.ts"],"sourcesContent":["/**\n * demo 测试数据 fixtures(设计文档「无数据库时自动生成测试数据」)。\n *\n * 覆盖 11 项场景表;**url 一律 `/demo.html`**(另设第二路径用于分页场景)。\n * 数据为静态常量 + 少量按「基准时间」推导的时间戳,保证每次 seed 结构可复现。\n * 本文件只在显式开启 seed 时被加载(见 `./index.ts` 与 `../server.ts` 的双重守卫)。\n */\nimport { createHash } from \"node:crypto\";\nimport type { CommentDoc, ConfigData } from \"@twikoojs/common\";\n\n/** demo 页路径(测试数据 url 必须为 `/demo.html`,与 demo 页默认 path 一致)*/\nexport const DEMO_URL = \"/demo.html\";\n\n/** 第二路径:用于「多个 url 路径的评论 → 分页 + 仅显示当前页评论」场景 */\nexport const OTHER_URL = \"/demo-other.html\";\n\n/** 访客计数种子值(1.x 只有 `incCounter` 自增语义,故循环写入以获得可读数值) */\nexport const DEMO_VISITORS = 42;\n\n/** 博主昵称(与配置 BLOGGER_NICK 一致,用于 master 徽标) */\nexport const DEMO_BLOGGER_NICK = \"iMaeGoo\";\n\n/**\n * 场景清单(覆盖表逐项)——供 seed 日志与单测逐项断言,避免「表里写了但没造」。\n */\nexport const DEMO_SCENARIOS = [\n \"普通评论(含 _id/url/nick/mail/link/ua/ip)\",\n \"嵌套回复(多层 pid/rid)\",\n \"含 owo 表情的评论\",\n \"含公式的评论($…$ / $$…$$)\",\n \"含代码块的评论\",\n \"含链接、图片的评论\",\n \"已点赞评论(like 字段有值)\",\n \"标记为垃圾的评论(isSpam: true)\",\n \"多个 url 路径的评论(分页)\",\n \"含访客计数的配置\",\n \"默认配置对象(配置面板全部字段有值)\",\n] as const;\n\n/** demo 图片(内联 data URI:离线也能显示,避免 demo 依赖外网) */\nconst DEMO_IMAGE =\n \"data:image/svg+xml;base64,\" +\n \"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNjAiIGhlaWdodD0iNjAiPjxyZWN0IHdpZHRoPSIxNjAiIGhlaWdodD0iNjAiIGZpbGw9IiMxODVGQTUiLz48dGV4dCB4PSI4MCIgeT0iMzYiIGZvbnQtc2l6ZT0iMTQiIGZpbGw9IiNmZmYiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlR3aWtvbyBEZW1vPC90ZXh0Pjwvc3ZnPg==\";\n\n/**\n * 默认配置对象(场景 11:配置面板**全部字段有值**)。\n *\n * 键集合 = 1.7.24 管理面板配置项全集(`TkAdminConfig.vue` 的 `key:` 清单);\n * 凭证类字段填明显非真实的占位值,仅用于「面板有值」演示。\n */\nexport const DEMO_CONFIG: ConfigData = {\n SITE_NAME: \"Twikoo Demo\",\n SITE_URL: \"http://localhost:9820\",\n CORS_ALLOW_ORIGIN: \"http://localhost:9820\",\n BLOGGER_NICK: DEMO_BLOGGER_NICK,\n BLOGGER_EMAIL: \"hello@imaegoo.com\",\n MASTER_TAG: \"站长\",\n COMMENT_PAGE_SIZE: \"8\",\n COMMENT_BG_IMG: \"\",\n COMMENT_PLACEHOLDER: \"请友善评论,支持 Markdown / 表情 / 公式 / 代码块\",\n GRAVATAR_CDN: \"cravatar.cn\",\n DEFAULT_GRAVATAR: \"mp\",\n SHOW_ORDER: \"true\",\n SHOW_DISLIKE: \"true\",\n SHOW_EMOTION: \"true\",\n SHOW_IMAGE: \"true\",\n SHOW_UA: \"true\",\n SHOW_REGION: \"true\",\n LIGHTBOX: \"false\",\n DISPLAYED_FIELDS: \"nick,mail,link\",\n REQUIRED_FIELDS: \"nick,mail\",\n HIDE_ADMIN_CRYPT: \"admin\",\n HIDE_SPAM: \"false\",\n LIMIT_LENGTH: \"500\",\n LIMIT_PER_MINUTE: \"3\",\n LIMIT_PER_MINUTE_ALL: \"10\",\n EMOTION_CDN: \"https://owo.imaegoo.com/owo.json\",\n IMAGE_CDN: \"none\",\n IMAGE_CDN_URL: \"https://piclist.example.com\",\n IMAGE_CDN_TOKEN: \"demo-token\",\n HIGHLIGHT: \"true\",\n HIGHLIGHT_THEME: \"default\",\n HIGHLIGHT_PLUGIN: \"showLanguage,copyButton\",\n CAPTCHA_PROVIDER: \"\",\n TURNSTILE_SITE_KEY: \"demo-turnstile-site-key\",\n TURNSTILE_SECRET_KEY: \"demo-turnstile-secret-key\",\n GEETEST_CAPTCHA_ID: \"demo-geetest-id\",\n GEETEST_CAPTCHA_KEY: \"demo-geetest-key\",\n CAP_API_ENDPOINT: \"\",\n CAP_SECRET_KEY: \"demo-cap-secret\",\n QQ_API_KEY: \"demo-qq-api-key\",\n SC_MAIL_NOTIFY: \"true\",\n SENDER_NAME: \"Twikoo Demo\",\n SENDER_EMAIL: \"noreply@example.com\",\n SMTP_SERVICE: \"QQ\",\n SMTP_HOST: \"smtp.example.com\",\n SMTP_PORT: \"465\",\n SMTP_SECURE: \"true\",\n SMTP_USER: \"demo@example.com\",\n SMTP_PASS: \"demo-smtp-pass\",\n MAIL_SUBJECT: \"${SITE_NAME} 有新评论\",\n MAIL_SUBJECT_ADMIN: \"${SITE_NAME} 有新评论(管理员)\",\n MAIL_TEMPLATE: \"<p>${PARENT_NICK} 回复了你的评论:</p><p>${COMMENT}</p>\",\n MAIL_TEMPLATE_ADMIN: \"<p>${NICK} 在 ${POST_URL} 评论:</p><p>${COMMENT}</p>\",\n PUSHOO_CHANNEL: \"none\",\n PUSHOO_TOKEN: \"\",\n NOTIFY_SPAM: \"false\",\n AKISMET_KEY: \"demo-akismet-key\",\n BLOCKED_WORDS: \"测试屏蔽词,广告\",\n FORBIDDEN_WORDS: \"违规词\",\n NSFW_API_URL: \"https://nsfw.example.com/api\",\n NSFW_THRESHOLD: \"0.8\",\n LLM_API_ENDPOINT: \"https://api.example.com/v1\",\n LLM_API_KEY: \"demo-llm-api-key\",\n LLM_MODEL: \"demo-model\",\n LLM_MAX_RETRIES: \"3\",\n LLM_SPAM_PROMPT: \"判断这条评论是否为垃圾评论\",\n QCLOUD_CMS_BIZTYPE: \"twikoo\",\n QCLOUD_SECRET_ID: \"demo-qcloud-secret-id\",\n QCLOUD_SECRET_KEY: \"demo-qcloud-secret-key\",\n S3_ENDPOINT: \"https://s3.example.com\",\n S3_REGION: \"ap-shanghai\",\n S3_BUCKET: \"twikoo-demo\",\n S3_ACCESS_KEY_ID: \"demo-s3-access-key-id\",\n S3_SECRET_ACCESS_KEY: \"demo-s3-secret-access-key\",\n S3_PATH_PREFIX: \"twikoo/\",\n S3_CDN_URL: \"https://cdn.example.com\",\n S3_FORCE_PATH_STYLE: \"false\",\n};\n\n/**\n * 计算邮箱哈希(1.x `mailMd5` 语义:头像源;此处用 md5 与 Gravatar/Cravatar 默认形态一致)。\n * @param mail 邮箱\n * @returns 32 位小写十六进制哈希\n */\nfunction mailHash(mail: string): string {\n return createHash(\"md5\").update(mail).digest(\"hex\");\n}\n\n/** 评论 fixture 形态:`ref` 用于回复关系解析(seed 时替换为真实 `_id`) */\ninterface CommentFixture extends CommentDoc {\n /** fixture 内部引用键(不写入数据库) */\n ref?: string;\n /** 父评论 fixture 引用键(解析为 pid) */\n pidRef?: string;\n /** 根评论 fixture 引用键(解析为 rid) */\n ridRef?: string;\n}\n\n/** 构造 fixture 公共字段(普通评论基础形态) */\nfunction baseFixture(index: number, now: number, overrides: CommentFixture = {}): CommentFixture {\n /** 序号越小越新(created 倒序展示) */\n const created = now - index * 3600_000;\n return {\n uid: `seed-uid-${index}`,\n ua: \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Twikoo-Demo-Seed\",\n ip: \"127.0.0.1\",\n url: DEMO_URL,\n href: `http://localhost:9820${DEMO_URL}`,\n created,\n updated: created,\n isSpam: false,\n like: [],\n ...overrides,\n };\n}\n\n/**\n * 构造全部 demo 评论 fixture(场景 1–9)。\n * @param now 基准时间戳(毫秒)\n * @returns fixture 列表(顶级在前、回复在后,seed 按序写入)\n */\nexport function buildDemoComments(now: number): CommentFixture[] {\n const list: CommentFixture[] = [];\n\n // 场景 1 + 6 + 7:普通评论(含 _id/url/nick/mail/link/ua/ip)、图片与链接、已点赞、博主标识\n list.push(\n baseFixture(0, now, {\n ref: \"master\",\n nick: DEMO_BLOGGER_NICK,\n mail: \"hello@imaegoo.com\",\n mailMd5: mailHash(\"hello@imaegoo.com\"),\n link: \"https://www.imaegoo.com\",\n master: true,\n like: [\"seed-uid-1\", \"seed-uid-2\", \"seed-uid-3\"],\n comment:\n \"<p>欢迎来到 Twikoo 2.0 本地演示。这条是博主评论,含链接 \" +\n '<a href=\"https://twikoo.js.org\" target=\"_blank\" rel=\"noopener noreferrer nofollow ugc\">Twikoo 官网</a>' +\n `、图片 <img src=\"${DEMO_IMAGE}\" alt=\"demo\"> 与内联代码 <code>twikoo.init()</code>。</p>`,\n }),\n baseFixture(1, now, {\n ref: \"plain\",\n nick: \"路过的访客\",\n mail: \"visitor@example.com\",\n mailMd5: mailHash(\"visitor@example.com\"),\n link: \"\",\n comment: \"<p>普通评论一条,用来验证基础渲染与昵称/邮箱头像。</p>\",\n }),\n baseFixture(2, now, {\n ref: \"owo\",\n nick: \"表情党\",\n mail: \"owo@example.com\",\n mailMd5: mailHash(\"owo@example.com\"),\n comment: \"<p>owo 表情测试::QQ: :tv_doge: :B站: 以及文字混排。</p>\",\n }),\n baseFixture(3, now, {\n ref: \"formula\",\n nick: \"数学老师\",\n mail: \"math@example.com\",\n mailMd5: mailHash(\"math@example.com\"),\n comment:\n \"<p>行内公式 $E = mc^2$ 与块级公式:</p><p>$$\\\\int_{0}^{1} x^{2}\\\\,dx = \\\\frac{1}{3}$$</p>\",\n }),\n baseFixture(4, now, {\n ref: \"code\",\n nick: \"工程师\",\n mail: \"dev@example.com\",\n mailMd5: mailHash(\"dev@example.com\"),\n comment:\n \"<p>代码块测试(期望 Prism 高亮):</p>\" +\n '<pre><code class=\"language-js\">const twikoo = window.twikoo;\\n' +\n 'twikoo.init({ envId: \"http://localhost:8080\", el: \"#tcomment\" });\\n</code></pre>',\n }),\n );\n\n // 场景 2:嵌套回复(多层 pid/rid)—— 挂在博主评论下,形成 3 层盖楼\n list.push(\n baseFixture(5, now, {\n ref: \"reply-1\",\n pidRef: \"master\",\n ridRef: \"master\",\n nick: \"提问者\",\n mail: \"ask@example.com\",\n mailMd5: mailHash(\"ask@example.com\"),\n comment: \"<p>请问多层回复怎么展示?</p>\",\n }),\n baseFixture(6, now, {\n ref: \"reply-2\",\n pidRef: \"reply-1\",\n ridRef: \"master\",\n nick: DEMO_BLOGGER_NICK,\n mail: \"hello@imaegoo.com\",\n mailMd5: mailHash(\"hello@imaegoo.com\"),\n master: true,\n comment: \"<p>像这样一层层缩进展示(pid 指直接上级,rid 指根评论)。</p>\",\n }),\n baseFixture(7, now, {\n ref: \"reply-3\",\n pidRef: \"reply-2\",\n ridRef: \"master\",\n nick: \"提问者\",\n mail: \"ask@example.com\",\n mailMd5: mailHash(\"ask@example.com\"),\n comment: \"<p>明白了,第三层也能正常显示。</p>\",\n }),\n );\n\n // 场景 1 补充(多条普通评论,同时为场景 9「分页」凑满一页以上)\n for (let i = 0; i < 5; i += 1) {\n const index = 8 + i;\n list.push(\n baseFixture(index, now, {\n ref: `filler-${i}`,\n nick: `分页测试用户 ${i + 1}`,\n mail: `pager${i + 1}@example.com`,\n mailMd5: mailHash(`pager${i + 1}@example.com`),\n comment: `<p>第 ${i + 1} 条分页测试评论(与其它评论一起超过 COMMENT_PAGE_SIZE,用于验证分页)。</p>`,\n }),\n );\n }\n\n // 场景 8:垃圾评论(管理员视图筛选用)\n list.push(\n baseFixture(20, now, {\n ref: \"spam\",\n nick: \"spammer\",\n mail: \"spam@example.com\",\n mailMd5: mailHash(\"spam@example.com\"),\n isSpam: true,\n comment: \"<p>这条评论被标记为垃圾评论(isSpam: true),访客不可见、管理员可筛选。</p>\",\n }),\n );\n\n // 场景 9:多个 url 路径的评论(分页时不应出现在 /demo.html)\n for (let i = 0; i < 3; i += 1) {\n const index = 21 + i;\n list.push(\n baseFixture(index, now, {\n ref: `other-${i}`,\n url: OTHER_URL,\n href: `http://localhost:9820${OTHER_URL}`,\n nick: `另一页用户 ${i + 1}`,\n mail: `other${i + 1}@example.com`,\n mailMd5: mailHash(`other${i + 1}@example.com`),\n comment: `<p>属于 ${OTHER_URL} 的评论,不应出现在 demo 页的评论列表里。</p>`,\n }),\n );\n }\n\n return list;\n}\n","/**\n * demo 数据 seed(设计文档)。\n *\n * 触发三态:\n * | 数据状态 | 行为 |\n * | --- | --- |\n * | 数据库文件不存在 | 用默认测试数据生成,日志提示「已生成 N 条测试评论」 |\n * | 文件存在但为空 | 同上(补数据) |\n * | 文件存在且有数据 | 直接使用,**不改动** |\n *\n * 生产不可触达(双防线):\n * 1. 启动路径(`../server.ts`)仅在 `TWIKOO_SEED=1` 时**动态 import** 本模块;\n * 2. 本模块入口 {@link seedDemoData} 再次校验开关,未开启即抛\n * {@link SeedNotAllowedError}——即使被误调用也不会写入任何数据。\n */\nimport type { Database } from \"@twikoojs/common\";\nimport {\n DEMO_CONFIG,\n DEMO_SCENARIOS,\n DEMO_URL,\n DEMO_VISITORS,\n OTHER_URL,\n buildDemoComments,\n} from \"./fixtures\";\n\n/** seed 开关环境变量名(`TWIKOO_SEED=1` 或 `pnpm demo`)*/\nexport const SEED_ENV_KEY = \"TWIKOO_SEED\";\n\n/** seed 开关值(1 视为开启) */\nexport const SEED_ENV_VALUE = \"1\";\n\n/** seed 结果(供日志与单测断言) */\nexport interface SeedResult {\n /** 本次是否实际写入数据(false = 已有数据,未改动) */\n seeded: boolean;\n /** 写入的评论条数 */\n comments: number;\n /** 写入/沿用的访客计数(`/demo.html`) */\n visitors: number;\n /** 人读原因(写入/跳过) */\n reason: string;\n}\n\n/** 生产误触达错误(未开启 seed 却调用即抛,绝不静默写入)*/\nexport class SeedNotAllowedError extends Error {\n /**\n * @param envKey 开关环境变量名\n */\n constructor(envKey: string) {\n super(\n `demo seed 未开启:仅当 ${envKey}=1 时允许执行(生产环境请勿设置该变量)。` +\n \"本次调用未写入任何数据。\",\n );\n this.name = \"SeedNotAllowedError\";\n }\n}\n\n/**\n * 判断 seed 是否开启(缺省读进程环境变量;显式传 env 便于单测)。\n * @param env 环境变量表(缺省 `process.env`)\n * @returns true 表示允许执行 seed\n */\nexport function isSeedEnabled(env: NodeJS.ProcessEnv = process.env): boolean {\n return env[SEED_ENV_KEY] === SEED_ENV_VALUE;\n}\n\n/** seed 选项 */\nexport interface SeedOptions {\n /** 目标数据库(已由适配器按 MONGODB_URI / TWIKOO_DATA 选定) */\n database: Database;\n /** 环境变量表(缺省 `process.env`) */\n env?: NodeJS.ProcessEnv;\n /** 日志输出(缺省 `console.log`;测试可注入收集器) */\n log?: (message: string) => void;\n}\n\n/**\n * 写入 demo 测试数据。已有评论时**不改动**任何数据。\n * @param options seed 选项\n * @returns seed 结果\n */\nexport async function seedDemoData(options: SeedOptions): Promise<SeedResult> {\n const { database } = options;\n const env = options.env ?? process.env;\n const log = options.log ?? ((message: string) => console.log(message));\n if (!isSeedEnabled(env)) throw new SeedNotAllowedError(SEED_ENV_KEY);\n\n await database.init();\n\n /** 三态检测:任何已有评论都视为「有数据」→ 直接使用不改动 */\n const existing = await database.countComments({});\n if (existing > 0) {\n const counter = await database.getCounter(DEMO_URL);\n const visitors = counter?.time ?? 0;\n const reason = `已存在 ${existing} 条评论,跳过 seed(不改动现有数据)`;\n log(`[twikoo-seed] ${reason}`);\n return { seeded: false, comments: existing, visitors, reason };\n }\n\n /** 顶级评论先写(拿回 `_id`),回复再按 ref 解析 pid/rid —— 与 1.x 提交链路同序 */\n const fixtures = buildDemoComments(Date.now());\n /** ref → 真实 `_id` */\n const idByRef = new Map<string, string>();\n /** 已写入条数 */\n let written = 0;\n\n for (const fixture of fixtures) {\n if (fixture.pidRef !== undefined) continue;\n const { ref, pidRef, ridRef, ...doc } = fixture;\n void pidRef;\n void ridRef;\n const saved = await database.addComment(doc);\n if (ref !== undefined && saved._id !== undefined) idByRef.set(ref, saved._id);\n written += 1;\n }\n for (const fixture of fixtures) {\n if (fixture.pidRef === undefined) continue;\n const { ref, pidRef, ridRef, ...doc } = fixture;\n const pid = idByRef.get(pidRef) ?? \"\";\n const rid = ridRef !== undefined ? (idByRef.get(ridRef) ?? \"\") : \"\";\n const saved = await database.addComment({ ...doc, pid, rid });\n if (ref !== undefined && saved._id !== undefined) idByRef.set(ref, saved._id);\n written += 1;\n }\n\n /** 场景 11:默认配置对象(配置面板全部字段有值) */\n await database.saveConfig(DEMO_CONFIG);\n\n /** 场景 10:访客计数(1.x 只有 incCounter 自增语义,故循环写入) */\n for (let i = 0; i < DEMO_VISITORS; i += 1) await database.incCounter(DEMO_URL, \"Twikoo Demo\");\n for (let i = 0; i < 3; i += 1) await database.incCounter(OTHER_URL, \"Twikoo Demo 第二页\");\n\n const reason = `已生成 ${written} 条测试评论(覆盖 ${DEMO_SCENARIOS.length} 项场景),配置 ${Object.keys(DEMO_CONFIG).length} 项,访客计数 ${DEMO_VISITORS}`;\n log(`[twikoo-seed] ${reason}`);\n return { seeded: true, comments: written, visitors: DEMO_VISITORS, reason };\n}\n"],"mappings":"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"}