wucaishi-generative-react-skill 0.1.0

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.
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, readdirSync, statSync } from "node:fs";
4
+ import { join, relative } from "node:path";
5
+
6
+ const root = process.argv[2] || "src";
7
+ const allowedPxPatterns = [
8
+ /max-width\s*:\s*\d+(?:\.\d+)?px/,
9
+ /min-height\s*:\s*max\(\s*44px,/,
10
+ /border(?:-[a-z]+)?\s*:\s*1px\b/,
11
+ /border-(?:top|right|bottom|left)\s*:\s*1px\b/,
12
+ /outline\s*:\s*1px\b/,
13
+ ];
14
+
15
+ function walk(dir) {
16
+ const entries = readdirSync(dir);
17
+ return entries.flatMap((entry) => {
18
+ const path = join(dir, entry);
19
+ const stat = statSync(path);
20
+ if (stat.isDirectory()) {
21
+ return walk(path);
22
+ }
23
+ return /\.(styles\.ts|styles\.tsx|tsx|ts)$/.test(entry) ? [path] : [];
24
+ });
25
+ }
26
+
27
+ const findings = [];
28
+
29
+ for (const file of walk(root)) {
30
+ const source = readFileSync(file, "utf8");
31
+ const lines = source.split(/\r?\n/);
32
+
33
+ lines.forEach((line, index) => {
34
+ if (!/\d+(?:\.\d+)?px/.test(line)) {
35
+ return;
36
+ }
37
+ if (!/[a-zA-Z-]+\s*:.*\d+(?:\.\d+)?px/.test(line)) {
38
+ return;
39
+ }
40
+ if (/["'`][^"'`]*\d+(?:\.\d+)?px/.test(line)) {
41
+ return;
42
+ }
43
+ if (allowedPxPatterns.some((pattern) => pattern.test(line))) {
44
+ return;
45
+ }
46
+ findings.push({
47
+ file: relative(process.cwd(), file),
48
+ line: index + 1,
49
+ text: line.trim(),
50
+ });
51
+ });
52
+ }
53
+
54
+ if (findings.length > 0) {
55
+ console.error("H5 vw unit check failed. Convert visual px values to vw():");
56
+ for (const finding of findings) {
57
+ console.error(`${finding.file}:${finding.line} ${finding.text}`);
58
+ }
59
+ process.exit(1);
60
+ }
61
+
62
+ console.log("H5 vw unit check passed.");
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: build-version-confirm-zh
3
+ description: 当父 skill `component-package-workflow-zh` 进入 build、打包、打包发布、生成发布产物、准备发布、发布组件包或执行会产生可交付构建结果的命令时使用。输出和说明使用中文。
4
+ ---
5
+
6
+ # 打包前版本确认 Skill
7
+
8
+ 这个 Skill 用于约束“打包前的版本确认流程”。目标不是决定如何构建项目,而是在真正执行构建、打包或发布相关命令前,先让用户明确本次版本号应该如何变化。
9
+
10
+ ## 适用场景
11
+
12
+ 当用户出现以下意图时触发:
13
+
14
+ - 要你“打包项目”
15
+ - 要你“执行 build”
16
+ - 要你“出一个发布包”
17
+ - 要你“构建生产产物”
18
+ - 要你“准备发布”
19
+ - 要你“发版前先帮我构建”
20
+
21
+ 只要任务会产出可交付构建结果,或明显进入发版流程,就应先使用本 Skill。
22
+
23
+ ## 不适用场景
24
+
25
+ 以下场景不应使用本 Skill 作为主 Skill:
26
+
27
+ - 用户只是让你修改代码、修 bug、写组件、补文档
28
+ - 用户只是运行测试、lint、typecheck
29
+ - 用户只是问版本规则,但没有要求实际打包
30
+
31
+ ## 核心规则
32
+
33
+ - 在执行 `build`、打包、发布前,不要默认替用户决定如何升级版本
34
+ - 每次打包发布都必须调整版本号,不能沿用原版本直接发布
35
+ - 必须先向用户确认本次版本号更新方式
36
+ - 发起确认前,应先读取并告诉用户当前版本号
37
+ - 如果用户没有明确指定,应暂停执行打包,并向用户发起确认
38
+ - 允许用户选择:
39
+ - `major`(大版本,比如 `1.2.3 -> 2.0.0`,通常表示有较大变化或不兼容改动)
40
+ - `minor`(小版本,比如 `1.2.3 -> 1.3.0`,通常表示新增功能)
41
+ - `patch`(修订版本,比如 `1.2.3 -> 1.2.4`,通常表示修复问题或小改动)
42
+ - 发布打包场景不允许选择“不修改版本”
43
+ - 只有在用户明确给出选择后,才继续执行后续命令
44
+ - 用户确认版本更新方式后,除主版本文件外,还必须同步修改 `manifest.json` 里的版本号,保持版本一致
45
+ - 如果项目存在 `manifest.json`,在执行版本修改前应检查其当前版本;如发现与主版本不一致,应先告知用户
46
+
47
+ ## 推荐确认方式
48
+
49
+ 如果用户只说“帮我打包”或“执行 build”,应先用用户容易理解的中文解释来确认,不要只丢英文术语。推荐提问方式:
50
+
51
+ - 当前版本是 `1.2.3`。
52
+ - 这次打包前要把版本更新成哪一种?
53
+ - 可以选:
54
+ - `大版本`:例如 `1.2.3 -> 2.0.0`,一般用于大改动
55
+ - `小版本`:例如 `1.2.3 -> 1.3.0`,一般用于新增功能
56
+ - `修订版本`:例如 `1.2.3 -> 1.2.4`,一般用于修 bug 或小改动
57
+ - 你回复 `大版本`、`小版本` 或 `修订版本` 都可以,我再继续打包。
58
+
59
+ 如果需要保留英文,也应写成中英对照,而不是只写 `major`、`minor`、`patch`。
60
+
61
+ 如果项目里已有对应脚本,也可以在用户确认后再选择执行:
62
+
63
+ - `npm run build`
64
+
65
+ ## 执行顺序
66
+
67
+ 处理打包任务时,按下面顺序思考:
68
+
69
+ 1. 先识别这是不是一次真正的打包 / 构建 / 发版动作。
70
+ 2. 读取项目当前版本号;如果项目存在 `manifest.json`,一并读取其中的版本号。
71
+ 3. 检查用户是否已经明确说明本次版本策略。
72
+ 4. 如果没有说明,先告诉用户当前版本,再确认版本策略,不要直接执行构建命令。
73
+ 5. 如果发现主版本文件和 `manifest.json` 版本不一致,应先告知用户,再继续确认或修改流程。
74
+ 6. 用户确认后,先同步更新主版本文件和 `manifest.json` 中的版本。
75
+ 7. 完成版本同步后,再执行构建命令。
76
+
77
+ ## 输出要求
78
+
79
+ - 所有确认和说明使用中文。
80
+ - 询问要简洁,不要一次抛出太多额外问题。
81
+ - 不要默认替用户选 `patch`。
82
+ - 不要把“打包成功”建立在隐式修改版本号的前提上。
83
+ - 不要假设用户理解 `major`、`minor`、`patch` 这些英文术语。
84
+ - 提示用户做选择前,要先明确说出当前版本,例如“当前版本是 `1.2.3`”。
85
+ - 本次必须修改版本号,要确保 `manifest.json` 中的版本也同步更新,不要只改一处。
86
+ - 优先使用 `大版本 / 小版本 / 修订版本` 这样的中文表达。
87
+ - 最好附一个像 `1.2.3 -> 1.2.4` 这样的例子,帮助用户快速判断。
88
+
89
+ ## 边界
90
+
91
+ - 这个 Skill 只负责“打包前是否改版本、改哪一级”的确认流程。
92
+ - 这个 Skill 不负责定义项目自己的发布语义。
93
+ - 这个 Skill 不负责自动生成 changelog、tag 或 release note,除非用户额外要求。
@@ -0,0 +1,134 @@
1
+ ---
2
+ name: html-template-to-react-components-zh
3
+ description: 当父 skill `component-package-workflow-zh` 需要从移动端 HTML 预览、HTML 模板、模板说明 Markdown、场景模块表格中拆分独立 React 组件包,或需要把模板说明字段约束转成 Props、schema、manifest 时使用。输出和说明使用中文。
4
+ ---
5
+
6
+ # HTML 模板拆 React 组件 Skill
7
+
8
+ 这个子 Skill 用于把“一个包含多个模块的移动端 HTML 视觉预览”和“模板说明 Markdown”拆成多个可复用 React 组件包。它只处理拆分、映射和字段契约,后续组件创建、开发、预览、打包、上传仍服从父 Skill `component-package-workflow-zh`。
9
+
10
+ ## 必须同时使用
11
+
12
+ - 组件代码实现必须继续使用 `react-component-spec-zh`。
13
+ - 如果进入打包或上传,继续使用 `build-version-confirm-zh` 和 `upload-aliyun-oss-zh`。
14
+
15
+ ## 输入要求
16
+
17
+ 默认需要两个文件:
18
+
19
+ - HTML 文件:包含多个移动端模块,通常以 `<section class="module" id="...">` 表示一个模块。
20
+ - 模板说明 Markdown:必须包含“场景模块”表格,且表头包含 `模块名称`、`场景模块名称`、`用户要解决的问题`、`结构规范`。
21
+
22
+ ### 输入文件确认
23
+
24
+ 当用户要求从 HTML 模板、HTML 预览、移动端 HTML、模板说明 Markdown、场景模块表格中拆分 React 组件时,不要直接运行解析脚本,也不要自行猜测文件路径。
25
+
26
+ 执行解析前必须先用中文向用户确认:
27
+
28
+ - 本次要转换的 HTML 文件是哪一个?
29
+ - 本次要作为字段契约来源的 Markdown 模板说明文件是哪一个?
30
+
31
+ 即使当前目录中只有一个明显的 HTML 文件和一个明显的 Markdown 文件,也仍然需要向用户确认文件名后再继续。
32
+
33
+ 如果目录中存在多个 HTML 文件或多个 Markdown 文件,必须列出候选文件路径,让用户明确选择,不能按文件名相似度自行决定。
34
+
35
+ 询问时必须明确说明:
36
+
37
+ - HTML 文件只作为视觉、布局、交互和示例数据参考。
38
+ - Markdown 文件才是字段契约来源,包括组件名称、组件描述、字段、约束、默认值等。
39
+ - 如果 HTML 和 Markdown 内容冲突,以 Markdown 为准;但视觉样式以 HTML 为准。
40
+
41
+ ## 核心规则
42
+
43
+ - 字段契约只以模板说明 Markdown 为准,HTML 只作为视觉、布局、交互和示例数据参考。
44
+ - 生成组件的名称必须取模板说明表格中的 `场景模块名称`,不要自行改写。
45
+ - 生成组件的描述必须取模板说明表格中的 `用户要解决的问题`,不要自行概括。
46
+ - 发布到平台的组件名必须使用 `<中文场景或模板>_<中文组件名>_<english_component_code>`,例如 `会议总结_一句话看懂_meeting_minutes_summary`;其中中文场景或模板优先来自模板标题,中文组件名来自 `场景模块名称`,英文 code 优先使用 HTML `id` 或让用户确认。
47
+ - `package.json` 的包名和创建模板用的目录名是机器名,应与平台组件名分开;默认使用 `english_component_code` 转成 kebab-case,例如 `meeting-minutes-summary`。
48
+ - HTML 中出现但模板说明未声明的字段,不能自动加入 Props、`schema.ts` 或 `manifest.json`。
49
+ - 模板说明中声明的字段,即使 HTML 示例中没有展示,也要进入 Props、`schema.ts` 和 `manifest.json`。
50
+ - HTML 示例文案只能作为 demo data,不能替代字段约束。
51
+
52
+ ## 样式还原要求
53
+
54
+ HTML 转 React 组件时,React 组件的计算样式必须尽量与原 HTML 对应模块一致,不能只根据 DOM 结构重写大概样式。
55
+
56
+ - 必须读取并分析 HTML 中与目标 section 相关的 CSS,包括 class 选择器、父级布局、CSS 变量、字体、颜色、间距、圆角、阴影、边框、背景、字号、行高、宽高、flex/grid 布局等。
57
+ - 必须同时考虑 `<style>` 内联样式块、元素上的 `style=""`、HTML 引用的本地 CSS 文件,以及与目标 section 有关的全局基础样式。
58
+ - 生成 React + `styled-components` 时,应把 HTML 的视觉 token 和布局规则迁移到组件样式中。
59
+ - 如果原 HTML 依赖全局样式、CSS 变量或 reset,需要把目标组件正常显示所需的最小样式一并迁移。
60
+ - 如果 HTML 引用远程 CSS 或缺失的 CSS 文件,应告知用户无法完整还原,并请求提供对应 CSS 文件或确认继续。
61
+ - 如果某些样式无法可靠迁移,必须在实现前说明不确定点,不能静默忽略。
62
+ - 本地 demo 中渲染出来的组件默认视觉效果应与 HTML 对应模块尽量一致。
63
+
64
+ ## 推荐流程
65
+
66
+ 1. 先确认输入文件,明确本次使用的 HTML 文件和 Markdown 模板说明文件;未确认前不要运行解析脚本。
67
+
68
+ 2. 运行解析脚本生成模块契约:
69
+
70
+ ```bash
71
+ node .agents/skills/component-package-workflow-zh/subskills/html-template-to-react-components-zh/scripts/analyze_template_pair.mjs \
72
+ --html 移动端HTML预览.html \
73
+ --template 模板说明.md \
74
+ --out /tmp/template-contract.json
75
+ ```
76
+
77
+ 3. 先检查契约中的 `modules`,确认每个模块都正确映射:
78
+ - `componentDisplayName` 等于 `场景模块名称`
79
+ - `componentDescription` 等于 `用户要解决的问题`
80
+ - `fields` 来自 `结构规范` 的顶层字段
81
+ - `htmlId` 对应 HTML 中的模块 id
82
+
83
+ 4. 对每个要生成的模块,按父 Skill 流程创建组件包模板;创建前仍需完成组件名称存在性校验。
84
+
85
+ 5. 在模板基础上实现 React 组件:
86
+ - 视觉结构参考对应模块的 `bodyHtml`、`classNames` 和原 HTML 样式。
87
+ - 必须分析目标模块相关 CSS,并把必要的计算样式迁移到 `styled-components`。
88
+ - Props、`schema.ts`、`manifest.json` 以契约中的 `fields` 和 `constraints` 为准。
89
+ - 折叠、勾选、横滑等交互转成 React 状态,不把状态写死在 DOM 属性里。
90
+
91
+ 6. 完成后运行契约校验脚本:
92
+
93
+ ```bash
94
+ node .agents/skills/component-package-workflow-zh/subskills/html-template-to-react-components-zh/scripts/validate_component_contract.mjs \
95
+ --contract /tmp/template-contract.json \
96
+ --module 模块htmlId或场景模块名称 \
97
+ --manifest 组件包/src/manifest.json \
98
+ --schema 组件包/src/schema.json
99
+ ```
100
+
101
+ 如果项目的 `schema.ts` 不是 JSON,先人工对照契约检查,或导出一份等价 JSON 再运行脚本。
102
+
103
+ 7. 生成组件后必须启动本地预览,对比原 HTML 目标模块与 React 本地 demo 的视觉表现;如发现明显差异,应优先修复样式差异,再进入打包或上传。
104
+
105
+ ## 输出约定
106
+
107
+ 向用户说明拆分结果时,使用以下字段:
108
+
109
+ | 输出项 | 来源 |
110
+ | --- | --- |
111
+ | 组件名称 | `场景模块名称` |
112
+ | 组件描述 | `用户要解决的问题` |
113
+ | 字段约束 | `结构规范` |
114
+ | 视觉参考 | HTML 对应 section |
115
+ | 平台组件名 | `<中文场景或模板>_<场景模块名称>_<english_component_code>` |
116
+ | 包名建议 | `english_component_code` 转 kebab-case 后的机器名 |
117
+
118
+ 反馈给用户时还必须说明:
119
+
120
+ - 已使用的 HTML 文件路径
121
+ - 已使用的 Markdown 文件路径
122
+ - 目标模块对应的 HTML section id
123
+ - 本地预览是否已与 HTML 对应模块完成视觉对比
124
+
125
+ ## 常见错误
126
+
127
+ - 不要把 `模块名称` 当成组件名称;组件名称必须用 `场景模块名称`。
128
+ - 不要把平台组件名简化成纯英文;发布平台、名称校验和 `manifest.json.name` 必须使用 `<中文场景或模板>_<中文组件名>_<english_component_code>`。
129
+ - 不要把中文平台组件名直接作为 `package.json.name` 或本地目录名;机器名使用 `english_component_code` 的 kebab-case。
130
+ - 不要把 HTML 里的预览摘要、示例表格列或补充文案当成新增字段。
131
+ - 不要因为 HTML 视觉一致而省略模板说明中的字段上限、枚举和固定标题。
132
+ - 不要让 `manifest.json` 的 `name` 和 `description` 脱离模板说明字段。
133
+ - 不要在用户确认 HTML 和 Markdown 文件前自行运行解析脚本。
134
+ - 不要忽略 HTML 中真实 CSS,只凭视觉印象重写近似样式。
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const FIELD_PROP_NAMES = new Map([
7
+ ["标题", "title"],
8
+ ["摘要", "summary"],
9
+ ["主线列表", "mainLineList"],
10
+ ["主线标题", "mainLineTitle"],
11
+ ["说明", "description"],
12
+ ["适配度", "fitScore"],
13
+ ["适配结论", "fitConclusion"],
14
+ ["结论说明", "conclusionDescription"],
15
+ ["评分依据", "scoreBasis"],
16
+ ["适配证据列表", "fitEvidenceList"],
17
+ ["适配维度", "fitDimension"],
18
+ ["表现证据", "performanceEvidence"],
19
+ ["问答列表", "qaList"],
20
+ ["问题类型", "questionType"],
21
+ ["问题原意", "questionIntent"],
22
+ ["回答重点", "answerFocus"],
23
+ ["表现判断", "performanceJudgment"],
24
+ ["可补强点", "improvementPoint"],
25
+ ["积极信号列表", "positiveSignalList"],
26
+ ["保留信号列表", "reservedSignalList"],
27
+ ["信号描述", "signalDescription"],
28
+ ["录音依据", "audioEvidence"],
29
+ ["风险列表", "riskList"],
30
+ ["风险点", "riskPoint"],
31
+ ["对话表现", "dialoguePerformance"],
32
+ ["可能疑虑", "possibleConcern"],
33
+ ["补强方向", "improvementDirection"],
34
+ ["准备列表", "preparationList"],
35
+ ["优先级", "priority"],
36
+ ["准备主题", "preparationTopic"],
37
+ ["准备方式", "preparationMethod"],
38
+ ["反问分组列表", "reverseQuestionGroupList"],
39
+ ["分组名称", "groupName"],
40
+ ["问题列表", "questionList"],
41
+ ["反问问题", "reverseQuestion"],
42
+ ["反问目的", "reverseQuestionPurpose"],
43
+ ]);
44
+
45
+ function fail(message, code = 1) {
46
+ console.error(`错误:${message}`);
47
+ process.exit(code);
48
+ }
49
+
50
+ function parseArgs(argv) {
51
+ const args = {
52
+ html: null,
53
+ template: null,
54
+ out: null,
55
+ };
56
+
57
+ for (let i = 0; i < argv.length; i += 1) {
58
+ const arg = argv[i];
59
+ const nextValue = () => {
60
+ const value = argv[i + 1];
61
+ if (!value || value.startsWith("--")) {
62
+ fail(`${arg} 缺少参数值`);
63
+ }
64
+ i += 1;
65
+ return value;
66
+ };
67
+
68
+ if (arg === "-h" || arg === "--help") {
69
+ printHelp();
70
+ process.exit(0);
71
+ } else if (arg === "--html") {
72
+ args.html = nextValue();
73
+ } else if (arg === "--template") {
74
+ args.template = nextValue();
75
+ } else if (arg === "--out") {
76
+ args.out = nextValue();
77
+ } else {
78
+ fail(`未知参数:${arg}`);
79
+ }
80
+ }
81
+
82
+ if (!args.html) {
83
+ fail("必须传入 --html");
84
+ }
85
+ if (!args.template) {
86
+ fail("必须传入 --template");
87
+ }
88
+
89
+ return args;
90
+ }
91
+
92
+ function printHelp() {
93
+ console.log(`usage: analyze_template_pair.mjs --html HTML_FILE --template TEMPLATE_MD [--out OUT_JSON]
94
+
95
+ 解析移动端 HTML 预览和模板说明,输出可用于拆分 React 组件的模块契约。`);
96
+ }
97
+
98
+ function stripTags(value) {
99
+ return String(value || "")
100
+ .replace(/<[^>]+>/g, "")
101
+ .replace(/&nbsp;/g, " ")
102
+ .replace(/&amp;/g, "&")
103
+ .replace(/&lt;/g, "<")
104
+ .replace(/&gt;/g, ">")
105
+ .replace(/\s+/g, " ")
106
+ .trim();
107
+ }
108
+
109
+ function stripMarkdown(value) {
110
+ return String(value || "")
111
+ .replace(/`([^`]+)`/g, "$1")
112
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
113
+ .replace(/\s+/g, " ")
114
+ .trim();
115
+ }
116
+
117
+ function splitMarkdownRow(line) {
118
+ return line
119
+ .trim()
120
+ .replace(/^\|/, "")
121
+ .replace(/\|$/, "")
122
+ .split("|")
123
+ .map((cell) => cell.trim());
124
+ }
125
+
126
+ function extractTemplateTitle(templateText) {
127
+ const match = templateText.match(/^#\s+(.+)$/m);
128
+ return match ? stripMarkdown(match[1]).replace(/模板说明$/, "") : "";
129
+ }
130
+
131
+ function extractSceneModuleRows(templateText) {
132
+ const lines = templateText.split(/\r?\n/);
133
+ const tableStart = lines.findIndex((line) =>
134
+ /^\|\s*顺序\s*\|\s*模块名称\s*\|\s*场景模块名称\s*\|\s*用户要解决的问题\s*\|\s*结构规范\s*\|/.test(line),
135
+ );
136
+
137
+ if (tableStart === -1) {
138
+ throw new Error("模板说明中未找到场景模块表格");
139
+ }
140
+
141
+ const rows = [];
142
+ for (let i = tableStart + 2; i < lines.length; i += 1) {
143
+ const line = lines[i];
144
+ if (!line.trim().startsWith("|")) {
145
+ break;
146
+ }
147
+ const cells = splitMarkdownRow(line);
148
+ if (cells.length < 5) {
149
+ continue;
150
+ }
151
+ rows.push({
152
+ order: Number.parseInt(cells[0], 10),
153
+ moduleName: stripMarkdown(cells[1]),
154
+ sceneModuleName: stripMarkdown(cells[2]),
155
+ userProblem: stripMarkdown(cells[3]),
156
+ structureSpec: cells.slice(4).join("|").trim(),
157
+ });
158
+ }
159
+
160
+ return rows;
161
+ }
162
+
163
+ function extractBacktickValues(text) {
164
+ return Array.from(String(text || "").matchAll(/`([^`]+)`/g), (match) => match[1].trim())
165
+ .filter(Boolean);
166
+ }
167
+
168
+ function propNameForField(fieldName, index) {
169
+ return FIELD_PROP_NAMES.get(fieldName) || `field${index + 1}`;
170
+ }
171
+
172
+ function extractTopLevelFields(structureSpec) {
173
+ const match = structureSpec.match(/字段:(.+?)(?:。|$)/);
174
+ const source = match ? match[1] : structureSpec;
175
+ return extractBacktickValues(source).map((name, index) => ({
176
+ name,
177
+ propName: propNameForField(name, index),
178
+ required: true,
179
+ }));
180
+ }
181
+
182
+ function segmentForField(structureSpec, fieldName) {
183
+ const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
184
+ const matches = Array.from(
185
+ structureSpec.matchAll(new RegExp(`\`${escaped}\`([^;。|]*)`, "g")),
186
+ (match) => match[1].split("`")[0],
187
+ );
188
+ return (
189
+ matches.find((segment) => /固定为|不超过|最多|只使用/.test(segment)) ||
190
+ matches.at(-1) ||
191
+ ""
192
+ );
193
+ }
194
+
195
+ function extractQuotedValue(text) {
196
+ const match = text.match(/固定为[“"]([^”"]+)[”"]/);
197
+ return match ? match[1].trim() : null;
198
+ }
199
+
200
+ function extractEnum(text) {
201
+ const match = text.match(/只使用([^,;。]+)/);
202
+ if (!match) {
203
+ return null;
204
+ }
205
+ const values = match[1]
206
+ .split(/[、/]/)
207
+ .map((item) => item.trim())
208
+ .filter(Boolean);
209
+ return values.length > 0 ? values : null;
210
+ }
211
+
212
+ function extractConstraints(structureSpec, fields) {
213
+ const constraints = {};
214
+ const allFieldNames = extractBacktickValues(structureSpec);
215
+
216
+ allFieldNames.forEach((fieldName, index) => {
217
+ const propName = propNameForField(fieldName, index);
218
+ const segment = segmentForField(structureSpec, fieldName);
219
+ const fieldConstraints = {
220
+ sourceFieldName: fieldName,
221
+ };
222
+
223
+ const fixed = extractQuotedValue(segment);
224
+ if (fixed) {
225
+ fieldConstraints.fixed = fixed;
226
+ }
227
+
228
+ const maxLength = segment.match(/不超过\s*(\d+)\s*字/);
229
+ if (maxLength) {
230
+ fieldConstraints.maxLength = Number.parseInt(maxLength[1], 10);
231
+ }
232
+
233
+ const maxItems = segment.match(/最多\s*(\d+)\s*条/);
234
+ if (maxItems) {
235
+ fieldConstraints.maxItems = Number.parseInt(maxItems[1], 10);
236
+ }
237
+
238
+ const enumValues = extractEnum(segment);
239
+ if (enumValues) {
240
+ fieldConstraints.enum = enumValues;
241
+ }
242
+
243
+ if (Object.keys(fieldConstraints).length > 1) {
244
+ constraints[propName] = fieldConstraints;
245
+ }
246
+ });
247
+
248
+ fields.forEach((field) => {
249
+ constraints[field.propName] = {
250
+ sourceFieldName: field.name,
251
+ ...(constraints[field.propName] || {}),
252
+ };
253
+ });
254
+
255
+ return constraints;
256
+ }
257
+
258
+ function getAttributeValue(attributes, name) {
259
+ const match = attributes.match(new RegExp(`${name}\\s*=\\s*["']([^"']+)["']`, "i"));
260
+ return match ? match[1].trim() : "";
261
+ }
262
+
263
+ function extractClassNames(html) {
264
+ const names = new Set();
265
+ for (const match of html.matchAll(/class\s*=\s*["']([^"']+)["']/gi)) {
266
+ match[1].split(/\s+/).filter(Boolean).forEach((name) => names.add(name));
267
+ }
268
+ return Array.from(names).sort();
269
+ }
270
+
271
+ function extractHtmlSections(htmlText) {
272
+ const sections = [];
273
+ const sectionPattern = /<section\b([^>]*)>([\s\S]*?)<\/section>/gi;
274
+ for (const match of htmlText.matchAll(sectionPattern)) {
275
+ const attributes = match[1];
276
+ const fullHtml = match[0];
277
+ const className = getAttributeValue(attributes, "class");
278
+ if (!className.split(/\s+/).includes("module")) {
279
+ continue;
280
+ }
281
+
282
+ const title = fullHtml.match(/class\s*=\s*["'][^"']*\bmodule__title\b[^"']*["'][^>]*>([\s\S]*?)<\/[^>]+>/i);
283
+ const preview = fullHtml.match(/class\s*=\s*["'][^"']*\bmodule__preview\b[^"']*["'][^>]*>([\s\S]*?)<\/[^>]+>/i);
284
+
285
+ sections.push({
286
+ htmlId: getAttributeValue(attributes, "id"),
287
+ title: title ? stripTags(title[1]) : "",
288
+ previewText: preview ? stripTags(preview[1]) : "",
289
+ bodyHtml: fullHtml,
290
+ classNames: extractClassNames(fullHtml),
291
+ });
292
+ }
293
+ return sections;
294
+ }
295
+
296
+ function packageNameFromSection(section, fallbackIndex) {
297
+ const source = section?.htmlId || section?.title || `component-${fallbackIndex + 1}`;
298
+ const ascii = source
299
+ .normalize("NFKD")
300
+ .toLowerCase()
301
+ .replace(/[^a-z0-9]+/g, "-")
302
+ .replace(/^-+|-+$/g, "");
303
+ return ascii || `component-${fallbackIndex + 1}`;
304
+ }
305
+
306
+ function mapTemplateRowsToHtmlSections(rows, sections) {
307
+ const sectionByTitle = new Map(sections.map((section) => [section.title, section]));
308
+ const usedSectionIds = new Set();
309
+
310
+ const modules = rows.map((row, index) => {
311
+ const section = sectionByTitle.get(row.sceneModuleName) || null;
312
+ if (section?.htmlId) {
313
+ usedSectionIds.add(section.htmlId);
314
+ }
315
+ const fields = extractTopLevelFields(row.structureSpec);
316
+
317
+ return {
318
+ order: row.order,
319
+ moduleName: row.moduleName,
320
+ sceneModuleName: row.sceneModuleName,
321
+ userProblem: row.userProblem,
322
+ componentDisplayName: row.sceneModuleName,
323
+ componentDescription: row.userProblem,
324
+ packageNameSuggestion: packageNameFromSection(section, index),
325
+ htmlId: section?.htmlId || "",
326
+ htmlTitle: section?.title || "",
327
+ previewText: section?.previewText || "",
328
+ fields,
329
+ constraints: extractConstraints(row.structureSpec, fields),
330
+ structureSpec: row.structureSpec,
331
+ bodyHtml: section?.bodyHtml || "",
332
+ classNames: section?.classNames || [],
333
+ hasHtmlMatch: Boolean(section),
334
+ };
335
+ });
336
+
337
+ const unmatchedHtmlSections = sections.filter((section) => !usedSectionIds.has(section.htmlId));
338
+ return { modules, unmatchedHtmlSections };
339
+ }
340
+
341
+ export function analyzeTemplatePair({ htmlText, templateText }) {
342
+ if (!htmlText || !templateText) {
343
+ throw new Error("必须同时传入 htmlText 和 templateText");
344
+ }
345
+
346
+ const templateTitle = extractTemplateTitle(templateText);
347
+ const rows = extractSceneModuleRows(templateText);
348
+ const sections = extractHtmlSections(htmlText);
349
+ const { modules, unmatchedHtmlSections } = mapTemplateRowsToHtmlSections(rows, sections);
350
+
351
+ return {
352
+ templateTitle,
353
+ moduleCount: modules.length,
354
+ modules,
355
+ unmatchedHtmlSections,
356
+ };
357
+ }
358
+
359
+ async function main() {
360
+ const args = parseArgs(process.argv.slice(2));
361
+ const [htmlText, templateText] = await Promise.all([
362
+ readFile(args.html, "utf8"),
363
+ readFile(args.template, "utf8"),
364
+ ]);
365
+ const result = analyzeTemplatePair({ htmlText, templateText });
366
+ const output = `${JSON.stringify(result, null, 2)}\n`;
367
+
368
+ if (args.out) {
369
+ await writeFile(args.out, output);
370
+ } else {
371
+ process.stdout.write(output);
372
+ }
373
+ }
374
+
375
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
376
+ main().catch((error) => fail(error.stack || error.message || String(error)));
377
+ }