tkserver 1.7.24 → 2.0.0-beta.2
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/LICENSE +21 -0
- package/README.md +21 -20
- package/dist/index.d.mts +97 -0
- package/dist/index.d.ts +97 -0
- package/dist/index.js +243 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +234 -0
- package/dist/index.mjs.map +1 -0
- package/dist/seed-BAmtKrez.js +337 -0
- package/dist/seed-BAmtKrez.js.map +1 -0
- package/dist/seed-C8kiwlWr.mjs +337 -0
- package/dist/seed-C8kiwlWr.mjs.map +1 -0
- package/dist/seed-DZgEijDW.js +338 -0
- package/dist/seed-DZgEijDW.js.map +1 -0
- package/dist/server.js +245 -0
- package/dist/server.js.map +1 -0
- package/package.json +45 -26
- package/index.js +0 -1227
- package/mongo.js +0 -1184
- package/server.js +0 -125
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { createHash } from "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 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
|
+
export { seedDemoData };
|
|
336
|
+
|
|
337
|
+
//# sourceMappingURL=seed-C8kiwlWr.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed-C8kiwlWr.mjs","names":[],"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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"}
|