data-preheating-astro 0.1.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.
Files changed (53) hide show
  1. package/README.md +40 -0
  2. package/dist/apis/paginate.js +103 -0
  3. package/dist/apis/query.js +21 -0
  4. package/dist/apis/request.js +132 -0
  5. package/dist/cache/keywordHitCache.js +202 -0
  6. package/dist/cli.js +460 -0
  7. package/dist/config/app.config.js +150 -0
  8. package/dist/config/keywords.js +47 -0
  9. package/dist/config/modules/bannerSetting.js +121 -0
  10. package/dist/config/modules/cases.js +74 -0
  11. package/dist/config/modules/categories.js +99 -0
  12. package/dist/config/modules/companies.js +42 -0
  13. package/dist/config/modules/contact.js +56 -0
  14. package/dist/config/modules/contactSidebar.js +86 -0
  15. package/dist/config/modules/download.js +56 -0
  16. package/dist/config/modules/honor.js +72 -0
  17. package/dist/config/modules/insertCode.js +37 -0
  18. package/dist/config/modules/keywords.js +29 -0
  19. package/dist/config/modules/languages.js +33 -0
  20. package/dist/config/modules/menu.js +33 -0
  21. package/dist/config/modules/mobileMenu.js +51 -0
  22. package/dist/config/modules/news.js +74 -0
  23. package/dist/config/modules/partner.js +56 -0
  24. package/dist/config/modules/products.js +79 -0
  25. package/dist/config/modules/robot.js +41 -0
  26. package/dist/config/modules/seo.js +102 -0
  27. package/dist/config/modules/singlepages.js +57 -0
  28. package/dist/config/modules/siteConfiguration.js +128 -0
  29. package/dist/config/modules/template.js +38 -0
  30. package/dist/config/modules/types.js +10 -0
  31. package/dist/config/modules/video.js +75 -0
  32. package/dist/config/modules/websites.js +40 -0
  33. package/dist/exporter/buildModuleOutput.js +352 -0
  34. package/dist/exporter/combo.js +21 -0
  35. package/dist/exporter/fetchAll.js +29 -0
  36. package/dist/exporter/keywordRules.js +19 -0
  37. package/dist/exporter/languages.js +26 -0
  38. package/dist/exporter/outputUtils.js +48 -0
  39. package/dist/exporter/strapiExtract.js +59 -0
  40. package/dist/monitor/progress.js +244 -0
  41. package/dist/prereq/italkinForm.js +185 -0
  42. package/dist/tools/exportCountsCsv.js +146 -0
  43. package/dist/tools/httpTrace.js +152 -0
  44. package/dist/tools/monitorFormat.js +46 -0
  45. package/dist/tools/robotsTxt.js +69 -0
  46. package/dist/transform/images.js +142 -0
  47. package/dist/transform/objectPath.js +188 -0
  48. package/dist/transform/pathNormalize.js +51 -0
  49. package/dist/transform/richtext.js +468 -0
  50. package/dist/transform/slug.js +38 -0
  51. package/dist/write/index.js +123 -0
  52. package/dist/write/output.js +60 -0
  53. package/package.json +34 -0
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ /**
3
+ * HTTP 请求追踪工具(独立于业务逻辑)。
4
+ *
5
+ * 目标:
6
+ * - 记录:当前任务执行了哪些接口、语言、每个请求耗时、状态码等(可扩展字段)
7
+ * - 落盘:jsonl(每行一个 JSON,后续字段可随时追加)
8
+ * - 汇总:任务结束打印 table(按模块/语言聚合 + 慢请求 Top)
9
+ *
10
+ * 说明:
11
+ * - 该工具不依赖业务流程,只接收 “请求结束事件” 并进行记录/汇总
12
+ * - 为减少 IO,内部做简单缓冲,达到阈值再 append
13
+ */
14
+ var __importDefault = (this && this.__importDefault) || function (mod) {
15
+ return (mod && mod.__esModule) ? mod : { "default": mod };
16
+ };
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.createHttpTrace = createHttpTrace;
19
+ const node_fs_1 = require("node:fs");
20
+ const node_path_1 = __importDefault(require("node:path"));
21
+ const monitorFormat_1 = require("./monitorFormat");
22
+ /**
23
+ * 创建 HTTP 追踪实例。
24
+ */
25
+ function createHttpTrace(init) {
26
+ const startedAt = Date.now();
27
+ const modules = (init.modules || [])
28
+ .filter((m) => Boolean(m && m.name && m.endpoint))
29
+ .slice()
30
+ // endpoint 长的优先匹配(避免 /api/products 先匹配到 /api)
31
+ .sort((a, b) => (b.endpoint.length || 0) - (a.endpoint.length || 0));
32
+ // 生成日志文件路径:<logDirAbs>/export-http.<timestamp>.jsonl
33
+ const stamp = (0, monitorFormat_1.makeStamp)();
34
+ const logDir = init.logDirAbs;
35
+ const logFile = node_path_1.default.join(logDir, `export-http.${stamp}.jsonl`);
36
+ const summaryFile = node_path_1.default.join(logDir, `export-http.${stamp}.summary.log`);
37
+ // mkdir 返回 string|undefined(递归创建时),这里统一转为 Promise<void>,便于队列串行。
38
+ let writeQueue = node_fs_1.promises.mkdir(logDir, { recursive: true }).then(() => void 0);
39
+ let buffer = [];
40
+ // 汇总:lang::module -> count/avg/max/errors
41
+ const agg = new Map();
42
+ const slowTop = [];
43
+ function parseLocale(pathWithQuery) {
44
+ // 只解析 locale=...(保持实现简单)
45
+ const idx = pathWithQuery.indexOf("locale=");
46
+ if (idx === -1)
47
+ return "";
48
+ const start = idx + "locale=".length;
49
+ const end = pathWithQuery.indexOf("&", start);
50
+ const raw = end === -1 ? pathWithQuery.slice(start) : pathWithQuery.slice(start, end);
51
+ try {
52
+ return decodeURIComponent(raw).trim();
53
+ }
54
+ catch {
55
+ return raw.trim();
56
+ }
57
+ }
58
+ function inferModuleName(pathWithQuery) {
59
+ for (const m of modules) {
60
+ if (pathWithQuery.startsWith(m.endpoint))
61
+ return m.name;
62
+ }
63
+ return "unknown";
64
+ }
65
+ function inferLangDir(pathWithQuery) {
66
+ const locale = parseLocale(pathWithQuery);
67
+ return locale || "shared";
68
+ }
69
+ function enqueueWrite(lines) {
70
+ const chunk = lines.join("");
71
+ if (!chunk)
72
+ return;
73
+ writeQueue = writeQueue.then(async () => {
74
+ await node_fs_1.promises.appendFile(logFile, chunk, "utf8");
75
+ });
76
+ }
77
+ function flush() {
78
+ if (buffer.length === 0)
79
+ return;
80
+ const lines = buffer;
81
+ buffer = [];
82
+ enqueueWrite(lines);
83
+ }
84
+ function addSlow(x) {
85
+ slowTop.push(x);
86
+ slowTop.sort((a, b) => b.ms - a.ms);
87
+ if (slowTop.length > 15)
88
+ slowTop.length = 15;
89
+ }
90
+ function buildSummaryText() {
91
+ const lines = [];
92
+ lines.push("[export] HTTP\n");
93
+ lines.push(`[export] httpLog=${logFile}\n`);
94
+ if (init.baseUrl)
95
+ lines.push(`[export] httpBaseUrl=${init.baseUrl}\n`);
96
+ const rows = Array.from(agg.entries())
97
+ .map(([k, v]) => {
98
+ const avg = v.count > 0 ? Math.round(v.totalMs / v.count) : 0;
99
+ return { k, count: v.count, avg, max: v.maxMs, errors: v.errors };
100
+ })
101
+ .sort((a, b) => b.max - a.max);
102
+ lines.push("[export] TABLE lang::module | count | avg_ms | max_ms | errors\n");
103
+ for (const r of rows) {
104
+ lines.push(`[export] TABLE ${(0, monitorFormat_1.fmtCell)(r.k, 22)} ${(0, monitorFormat_1.fmtCell)(String(r.count), 6)} ${(0, monitorFormat_1.fmtCell)(String(r.avg), 7)} ${(0, monitorFormat_1.fmtCell)(String(r.max), 7)} ${String(r.errors)}\n`);
105
+ }
106
+ if (slowTop.length > 0) {
107
+ lines.push("[export] TABLE slow_requests(ms | status | lang | module | path)\n");
108
+ for (const s of slowTop) {
109
+ lines.push(`[export] TABLE ${(0, monitorFormat_1.fmtCell)(String(s.ms), 6)} ${(0, monitorFormat_1.fmtCell)(String(s.status || ""), 6)} ${(0, monitorFormat_1.fmtCell)(s.lang || "", 8)} ${(0, monitorFormat_1.fmtCell)(s.module || "", 12)} ${s.path || ""}\n`);
110
+ }
111
+ }
112
+ lines.push(`[export] httpElapsedMs=${Math.max(0, Date.now() - startedAt)}\n`);
113
+ return lines.join("");
114
+ }
115
+ return {
116
+ onHttpLog(entry) {
117
+ const pathWithQuery = typeof entry.pathWithQuery === "string" ? entry.pathWithQuery : "";
118
+ const durationMs = typeof entry.durationMs === "number" ? Math.max(0, Math.round(entry.durationMs)) : 0;
119
+ const ok = typeof entry.ok === "boolean" ? entry.ok : true;
120
+ const status = typeof entry.status === "number" ? entry.status : undefined;
121
+ const moduleName = inferModuleName(pathWithQuery);
122
+ const locale = parseLocale(pathWithQuery);
123
+ const langDir = inferLangDir(pathWithQuery);
124
+ // 写 jsonl:保留原 entry 字段,并追加推导字段(后续扩展只需加字段)。
125
+ const lineObj = {
126
+ ...entry,
127
+ moduleName,
128
+ langDir,
129
+ locale
130
+ };
131
+ buffer.push(`${JSON.stringify(lineObj)}\n`);
132
+ if (buffer.length >= 200)
133
+ flush();
134
+ // 更新聚合
135
+ const k = `${langDir}::${moduleName}`;
136
+ const cur = agg.get(k) || { count: 0, totalMs: 0, maxMs: 0, errors: 0 };
137
+ cur.count += 1;
138
+ cur.totalMs += durationMs;
139
+ cur.maxMs = Math.max(cur.maxMs, durationMs);
140
+ if (!ok)
141
+ cur.errors += 1;
142
+ agg.set(k, cur);
143
+ addSlow({ ms: durationMs, status, lang: langDir, module: moduleName, path: pathWithQuery });
144
+ },
145
+ async close() {
146
+ flush();
147
+ await writeQueue;
148
+ // 汇总仅写入文件,不输出到控制台。
149
+ await node_fs_1.promises.writeFile(summaryFile, buildSummaryText(), "utf8");
150
+ },
151
+ };
152
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ /**
3
+ * 监控/日志通用格式化工具(最小集合)。
4
+ *
5
+ * 目标:
6
+ * - 复用时间戳、列对齐等重复逻辑
7
+ * - 不引入复杂依赖,保持实现最简
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.pad2 = pad2;
11
+ exports.makeStamp = makeStamp;
12
+ exports.fmtCell = fmtCell;
13
+ exports.formatElapsedMs = formatElapsedMs;
14
+ /**
15
+ * 固定两位数字字符串(不足补 0)。
16
+ */
17
+ function pad2(n) {
18
+ return String(n).padStart(2, "0");
19
+ }
20
+ /**
21
+ * 生成日志/文件名用时间戳:YYYYMMDD-HHMMSS
22
+ *
23
+ * 说明:
24
+ * - 该格式与项目现有日志保持一致
25
+ * - 默认使用当前时间
26
+ */
27
+ function makeStamp(d = new Date()) {
28
+ return `${d.getFullYear()}${pad2(d.getMonth() + 1)}${pad2(d.getDate())}-${pad2(d.getHours())}${pad2(d.getMinutes())}${pad2(d.getSeconds())}`;
29
+ }
30
+ /**
31
+ * 固定宽度字符串:用于输出 table 对齐。
32
+ */
33
+ function fmtCell(v, n) {
34
+ const s = v ?? "";
35
+ return s.length >= n ? s.slice(0, n) : s.padEnd(n, " ");
36
+ }
37
+ /**
38
+ * 将毫秒转为 HH:MM:SS
39
+ */
40
+ function formatElapsedMs(ms) {
41
+ const sec = Math.max(0, Math.floor(ms / 1000));
42
+ const h = Math.floor(sec / 3600);
43
+ const m = Math.floor((sec % 3600) / 60);
44
+ const s = sec % 60;
45
+ return `${pad2(h)}:${pad2(m)}:${pad2(s)}`;
46
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ /**
3
+ * robots.txt 生成工具。
4
+ *
5
+ * 需求:
6
+ * - 接口 `/api/robot` 返回 `is_open` 用于判断是否生成 robots.txt
7
+ * - 当 `is_open=true` 时,从 `robots` 字段取值写入 robots.txt
8
+ * - 生成 robots.txt 不走缓存:每次运行只要 `is_open=true` 都使用本次接口的最新结果
9
+ */
10
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11
+ return (mod && mod.__esModule) ? mod : { "default": mod };
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.syncRobotsTxt = syncRobotsTxt;
15
+ exports.readRobotConfigFromEntry = readRobotConfigFromEntry;
16
+ const node_path_1 = __importDefault(require("node:path"));
17
+ const node_fs_1 = require("node:fs");
18
+ const output_1 = require("../write/output");
19
+ /**
20
+ * 根据接口结果生成/删除 robots.txt。
21
+ *
22
+ * 说明:
23
+ * - robots.txt 固定写入到输出目录根:`${outputDirAbs}/robots.txt`
24
+ * - 当关闭开关(is_open=false)时,会删除 robots.txt(若存在),避免旧文件残留
25
+ */
26
+ async function syncRobotsTxt(params) {
27
+ const robotsPath = node_path_1.default.join(params.outputDirAbs, "robots.txt");
28
+ // 关闭:删除文件(若存在),保持输出干净且不残留旧规则。
29
+ if (!params.isOpen) {
30
+ try {
31
+ await node_fs_1.promises.rm(robotsPath, { force: true });
32
+ }
33
+ catch {
34
+ // 删除失败不阻断导出(保持简单)。
35
+ }
36
+ return;
37
+ }
38
+ // 开启:写入 robots 内容。若为空,仍生成空文件(有效但不拦截)。
39
+ const raw = typeof params.robotsContent === "string" ? params.robotsContent : "";
40
+ const text = raw.endsWith("\n") ? raw : `${raw}\n`;
41
+ await (0, output_1.writeTextAtomic)(robotsPath, text);
42
+ }
43
+ /**
44
+ * 从导出条目中读取 robots 配置字段(兼容两种结构)。
45
+ *
46
+ * 说明:
47
+ * - 常见结构 A:字段在顶层(例如 `{ id, is_open, robots }`)
48
+ */
49
+ function readRobotConfigFromEntry(entry) {
50
+ /**
51
+ * 字段读取规则(严格,不做字段兼容):
52
+ * - 只读取 entry 顶层字段:`is_open`(boolean)与 `robots`(string)
53
+ * - 若字段缺失或类型不符:直接抛错,让调用方明确接口字段
54
+ */
55
+ const e = entry;
56
+ if (!e || typeof e !== "object") {
57
+ throw new Error("robot 接口返回为空:无法读取 is_open/robots");
58
+ }
59
+ const isOpenRaw = e.is_open;
60
+ const robotsRaw = e.robots;
61
+ if (typeof isOpenRaw !== "boolean") {
62
+ throw new Error(`robot 接口字段类型不符:is_open 期望 boolean,实际=${typeof isOpenRaw}`);
63
+ }
64
+ // 仅当开启时才强制要求 robots 为 string(符合“开关关闭时无需生成内容”的直觉)。
65
+ if (isOpenRaw && typeof robotsRaw !== "string") {
66
+ throw new Error(`robot 接口字段类型不符:robots 期望 string,实际=${typeof robotsRaw}`);
67
+ }
68
+ return { isOpen: isOpenRaw, robots: typeof robotsRaw === "string" ? robotsRaw : "" };
69
+ }
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ /**
3
+ * Strapi 媒体字段扁平化。
4
+ *
5
+ * 目标:
6
+ * - 将常见的媒体字段提取为稳定结构(统一为数组):
7
+ * [{ url, alt, name, width, height }]
8
+ *
9
+ * 说明:
10
+ * - Strapi 常见结构为:
11
+ * { data: { attributes: { url, name, alternativeText, width, height } } }
12
+ * - 或数组:{ data: [ { attributes: ... } ] }
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.flattenStrapiMedia = flattenStrapiMedia;
16
+ exports.normalizeImageFieldsInPlace = normalizeImageFieldsInPlace;
17
+ /**
18
+ * 将 Strapi 媒体字段转换为统一图片数组结构。
19
+ */
20
+ function flattenStrapiMedia(input) {
21
+ const images = toImages(input);
22
+ return images && images.length > 0 ? images : undefined;
23
+ }
24
+ /**
25
+ * 递归扫描对象中的“图片相关字段”,并将其就地替换为统一图片数组结构。
26
+ *
27
+ * 说明(保持简单):
28
+ * - 仅在 key 名称看起来像图片字段时才尝试转换,避免误伤普通 relation。
29
+ * - 支持常见结构:
30
+ * - Strapi media:{ data: ... }(由 flattenStrapiMedia 处理)
31
+ * - 组件包裹:{ alt, media: { id,url,... } } 或 [{ alt, media: ... }]
32
+ * - 直接对象:{ url, alt, name, width, height }
33
+ */
34
+ function normalizeImageFieldsInPlace(node) {
35
+ if (Array.isArray(node)) {
36
+ for (const it of node)
37
+ normalizeImageFieldsInPlace(it);
38
+ return;
39
+ }
40
+ if (!node || typeof node !== "object")
41
+ return;
42
+ const obj = node;
43
+ for (const key of Object.keys(obj)) {
44
+ const v = obj[key];
45
+ normalizeImageFieldsInPlace(v);
46
+ // 只在 key “像图片字段”时才尝试替换(避免误伤普通字段)
47
+ if (!looksLikeImageKey(key))
48
+ continue;
49
+ const images = toImages(v);
50
+ if (images && images.length > 0)
51
+ obj[key] = images;
52
+ }
53
+ }
54
+ /**
55
+ * 判断字段名是否像图片字段(简单规则)。
56
+ */
57
+ function looksLikeImageKey(key) {
58
+ return /image|img|picture|photo|icon|logo|banner|cover|media/i.test(key);
59
+ }
60
+ /**
61
+ * 从未知结构中提取统一图片数组(提取不到返回 undefined)。
62
+ *
63
+ * 说明(尽量少代码,但覆盖常见场景):
64
+ * - Strapi media:{ data: ... }
65
+ * - 组件包裹:{ alt, media: ... } 或 [{ alt, media: ... }]
66
+ * - 直接对象:{ url, alt, name, width, height }
67
+ */
68
+ function toImages(input) {
69
+ // 1) 数组:把每个元素的结果拼起来
70
+ if (Array.isArray(input)) {
71
+ const all = [];
72
+ for (const it of input) {
73
+ const sub = toImages(it);
74
+ if (sub && sub.length > 0)
75
+ all.push(...sub);
76
+ }
77
+ return all.length ? all : undefined;
78
+ }
79
+ // 2) 直接对象 / Strapi media / 组件包裹
80
+ if (!input || typeof input !== "object")
81
+ return undefined;
82
+ const obj = input;
83
+ // 2.1) 组件包裹:{ alt, media: ... }
84
+ if (obj.media) {
85
+ const images = toImages(obj.media);
86
+ if (!images || images.length === 0)
87
+ return undefined;
88
+ const wrapperAlt = typeof obj.alt === "string" ? obj.alt : "";
89
+ return wrapperAlt ? images.map((x) => (x.alt ? x : { ...x, alt: wrapperAlt })) : images;
90
+ }
91
+ // 2.2) Strapi media:{ data: ... }
92
+ if (obj.data) {
93
+ const data = obj.data;
94
+ if (Array.isArray(data)) {
95
+ const arr = data.map((x) => fromStrapiNode(x)).filter((x) => Boolean(x));
96
+ return arr.length ? arr : undefined;
97
+ }
98
+ const one = fromStrapiNode(data);
99
+ return one ? [one] : undefined;
100
+ }
101
+ // 2.3) 直接对象:{ url, alt, name, width, height }
102
+ const url = typeof obj.url === "string" ? obj.url : "";
103
+ if (!url)
104
+ return undefined;
105
+ const alt = pickString(obj.alt) || pickString(obj.alternativeText) || "";
106
+ const name = pickString(obj.name) || "";
107
+ const width = pickNumberString(obj.width);
108
+ const height = pickNumberString(obj.height);
109
+ return [{ url, alt, name, width, height }];
110
+ }
111
+ /**
112
+ * 从 Strapi media 节点中提取 attributes。
113
+ */
114
+ function fromStrapiNode(node) {
115
+ if (!node || typeof node !== "object")
116
+ return null;
117
+ const attrs = node.attributes;
118
+ if (!attrs || typeof attrs !== "object")
119
+ return null;
120
+ const url = pickString(attrs.url) || "";
121
+ if (!url)
122
+ return null;
123
+ const name = pickString(attrs.name) || "";
124
+ const alt = pickString(attrs.alternativeText) || pickString(attrs.alt) || "";
125
+ const width = pickNumberString(attrs.width);
126
+ const height = pickNumberString(attrs.height);
127
+ return { url, alt, name, width, height };
128
+ }
129
+ /**
130
+ * 读取字符串(非字符串返回空)。
131
+ */
132
+ function pickString(v) {
133
+ return typeof v === "string" ? v : "";
134
+ }
135
+ /**
136
+ * 读取宽高并统一转为字符串(缺失返回空字符串)。
137
+ */
138
+ function pickNumberString(v) {
139
+ if (typeof v === "number" && Number.isFinite(v))
140
+ return String(v);
141
+ return typeof v === "string" ? v : "";
142
+ }
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ /**
3
+ * 对象路径取值工具。
4
+ *
5
+ * 说明:
6
+ * - 用于从 Strapi 返回的记录中按路径读取字段(如 attributes.slug)。
7
+ * - 保持实现极简,不做复杂类型推断。
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.getByPath = getByPath;
11
+ exports.setByPath = setByPath;
12
+ exports.deleteByPath = deleteByPath;
13
+ exports.deleteLeafFieldsInDeepTree = deleteLeafFieldsInDeepTree;
14
+ exports.deleteByPathAllowArray = deleteByPathAllowArray;
15
+ function getByPath(obj, path) {
16
+ if (!path)
17
+ return undefined;
18
+ const parts = path.split(".").filter(Boolean);
19
+ let cur = obj;
20
+ for (const p of parts) {
21
+ if (cur == null)
22
+ return undefined;
23
+ cur = cur[p];
24
+ }
25
+ return cur;
26
+ }
27
+ function setByPath(obj, path, value) {
28
+ const parts = path.split(".").filter(Boolean);
29
+ if (parts.length === 0)
30
+ return;
31
+ let cur = obj;
32
+ for (let i = 0; i < parts.length - 1; i += 1) {
33
+ const k = parts[i];
34
+ if (cur[k] == null || typeof cur[k] !== "object") {
35
+ cur[k] = {};
36
+ }
37
+ cur = cur[k];
38
+ }
39
+ cur[parts[parts.length - 1]] = value;
40
+ }
41
+ /**
42
+ * 按路径删除字段(不存在则忽略)。
43
+ *
44
+ * 说明:
45
+ * - 用于“字段重命名/映射”时删除旧字段,避免 index.json 保留重复字段。
46
+ * - 实现保持极简:只支持对象层级,不处理数组下标语法(如 a.0.b)。
47
+ */
48
+ function deleteByPath(obj, path) {
49
+ const parts = path.split(".").filter(Boolean);
50
+ if (parts.length === 0)
51
+ return;
52
+ let cur = obj;
53
+ for (let i = 0; i < parts.length - 1; i += 1) {
54
+ const k = parts[i];
55
+ if (cur == null || typeof cur !== "object")
56
+ return;
57
+ cur = cur[k];
58
+ }
59
+ if (cur == null || typeof cur !== "object")
60
+ return;
61
+ delete cur[parts[parts.length - 1]];
62
+ }
63
+ /**
64
+ * 在“树形同名 key”结构中删除多个叶子字段(一次遍历删除多个字段)。
65
+ *
66
+ * 使用场景:
67
+ * - categories 这种多级 children:希望一次遍历删掉 children 节点上的多个内部字段
68
+ *
69
+ * 规则(保持简单):
70
+ * - 仅处理:treeKey.leafKey 这种“二段路径”的删除(leafKey 仅为字段名,不含点号)
71
+ * - 对所有层级递归生效(children -> children -> ...),并支持 children 为数组或对象
72
+ * - 遇到非对象元素会跳过;循环引用用 WeakSet 避免死循环
73
+ */
74
+ function deleteLeafFieldsInDeepTree(obj, treeKey, leafKeys) {
75
+ if (!treeKey || !leafKeys || leafKeys.length === 0)
76
+ return;
77
+ const root = obj[treeKey];
78
+ if (typeof root === "undefined")
79
+ return;
80
+ const seen = new WeakSet();
81
+ const walk = (v) => {
82
+ if (v == null)
83
+ return;
84
+ if (Array.isArray(v)) {
85
+ for (const item of v) {
86
+ if (!item || typeof item !== "object")
87
+ continue;
88
+ walk(item);
89
+ }
90
+ return;
91
+ }
92
+ if (typeof v !== "object")
93
+ return;
94
+ const o = v;
95
+ if (seen.has(o))
96
+ return;
97
+ seen.add(o);
98
+ // 当前节点:删除多个叶子字段
99
+ for (const k of leafKeys) {
100
+ if (!k)
101
+ continue;
102
+ delete v[k];
103
+ }
104
+ // 同名 key:继续向下遍历(不限深度)
105
+ walk(v[treeKey]);
106
+ };
107
+ walk(root);
108
+ }
109
+ /**
110
+ * 按路径删除字段(支持“数组中对象”的遍历删除)。
111
+ *
112
+ * 使用场景:
113
+ * - 用于导出前删除字段(removeFields),避免输出一些不需要的内部字段
114
+ *
115
+ * 规则(保持简单):
116
+ * - 路径仍然是点号路径(例如 children.ts_status_txt)
117
+ * - 当路径中间节点为数组时:会遍历数组中每个“对象元素”,继续按剩余路径删除
118
+ * - 支持“树形同名 key”的自动深度删除:例如 children.ts_at 会对多级 children 节点递归生效(不限深度)
119
+ * - 不支持数组下标语法(如 a.0.b),也不支持通配符
120
+ * - 遇到非对象元素会直接跳过(不报错)
121
+ */
122
+ function deleteByPathAllowArray(obj, path) {
123
+ const parts = path.split(".").filter(Boolean);
124
+ if (parts.length === 0)
125
+ return;
126
+ /**
127
+ * 递归保持极简:
128
+ * - 遇到数组就遍历
129
+ * - 遇到对象按路径向下
130
+ * - 对“树形同名 key”(例如 children)做无限深度遍历,并对每个节点应用剩余路径删除
131
+ */
132
+ const del = (cur, idx) => {
133
+ if (cur == null)
134
+ return;
135
+ // 数组:对每个对象元素继续处理(idx 不前进,因为数组不是路径的一段)
136
+ if (Array.isArray(cur)) {
137
+ for (const item of cur) {
138
+ if (!item || typeof item !== "object")
139
+ continue;
140
+ del(item, idx);
141
+ }
142
+ return;
143
+ }
144
+ // 非对象:无法继续,直接结束
145
+ if (typeof cur !== "object")
146
+ return;
147
+ // 最后一段:删除字段
148
+ if (idx >= parts.length - 1) {
149
+ delete cur[parts[parts.length - 1]];
150
+ return;
151
+ }
152
+ const k = parts[idx];
153
+ const next = cur[k];
154
+ if (typeof next === "undefined")
155
+ return;
156
+ /**
157
+ * 自动深度遍历:
158
+ * - 对 next(cur[k])做遍历
159
+ * - 对每个节点执行剩余路径删除(idx+1)
160
+ * - 若节点仍然包含同名 key(k),继续向下遍历(children -> children -> ...)
161
+ */
162
+ const seen = new WeakSet();
163
+ const walkDeep = (v) => {
164
+ if (v == null)
165
+ return;
166
+ if (Array.isArray(v)) {
167
+ for (const item of v) {
168
+ if (!item || typeof item !== "object")
169
+ continue;
170
+ walkDeep(item);
171
+ }
172
+ return;
173
+ }
174
+ if (typeof v !== "object")
175
+ return;
176
+ const obj0 = v;
177
+ if (seen.has(obj0))
178
+ return;
179
+ seen.add(obj0);
180
+ // 对当前节点应用剩余路径删除(从 idx+1 开始)
181
+ del(v, idx + 1);
182
+ // 继续沿着相同 key 向下遍历(若存在)
183
+ walkDeep(v[k]);
184
+ };
185
+ walkDeep(next);
186
+ };
187
+ del(obj, 0);
188
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ /**
3
+ * 路径类字段规范化工具。
4
+ *
5
+ * 说明:
6
+ * - 用于把接口返回的 url_slug/slug 这类字段统一去掉前后 `/`
7
+ * - 仅对“导出字段值”做处理,不影响富文本 html 文件名生成(文件名仍单独做安全化)
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.trimSlashes = trimSlashes;
11
+ exports.normalizePathLikeKeysInPlace = normalizePathLikeKeysInPlace;
12
+ /**
13
+ * 去掉字符串前后连续的 `/`。
14
+ *
15
+ * 规则:
16
+ * - `/a/` -> `a`
17
+ * - `//a//` -> `a`
18
+ * - `/` -> ``(空串)
19
+ */
20
+ function trimSlashes(input) {
21
+ const raw = typeof input === "string" ? input.trim() : "";
22
+ if (!raw)
23
+ return "";
24
+ return raw.replace(/^\/+|\/+$/g, "");
25
+ }
26
+ /**
27
+ * 递归处理对象/数组内的“路径类字符串字段”(例如 url_slug)。
28
+ *
29
+ * 规则:
30
+ * - 遍历对象/数组
31
+ * - 命中 keys 中的字段且值为 string:就地替换为 trimSlashes(v)
32
+ */
33
+ function normalizePathLikeKeysInPlace(node, keys) {
34
+ if (!node)
35
+ return;
36
+ if (Array.isArray(node)) {
37
+ for (const it of node)
38
+ normalizePathLikeKeysInPlace(it, keys);
39
+ return;
40
+ }
41
+ if (typeof node !== "object")
42
+ return;
43
+ const obj = node;
44
+ for (const [k, v] of Object.entries(obj)) {
45
+ if (keys.has(k) && typeof v === "string") {
46
+ obj[k] = trimSlashes(v);
47
+ continue;
48
+ }
49
+ normalizePathLikeKeysInPlace(v, keys);
50
+ }
51
+ }