Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pnpm dev # http://localhost:3000 ;Console 在 /_console/ ;管理员 adm
| A3 开发侧账号 | 当前 Claude Code 会话身份(共享 GitHub 身份,认领评论须带会话 ID) |
| A4 验收侧处理人 | 工作项创建人 |
| B 状态标签 | 待建:`status:待细化 / 待开发 / 方案确认中 / AI开发中 / 自测报告中 / PR审查中 / PM验收中 / 人工测试中 / 已完成 / 已打回 / 已挂起`;标记 `平台能力受限 / 人工介入 / 待复测`;类型 `回归缺陷 / 验收记录 / 试运行缺陷`;优先级 `P1 / P2 / P3`(首次使用 issue 流程前用 `gh label create` 一次建齐) |
| C1 分支命名 | `issue-<n>-<slug>`(本次初建使用平台指定分支 `claude/standard-assessment-system-qvswav`) |
| C1 分支命名 | `issue-<n>-<slug>`(本次初建使用平台指定分支 `claude/standard-assessment-system-qvswav`);PR 合并后 head 分支由 GitHub 自动删除(仓库设置 Automatically delete head branches,维护者 2026-09-03 启用),会话内不再用 git 删远端分支(代理拒绝) |
| C2 worktree 目录 | 仓库外 `../kpi-<task>` |
| C3 worktree 初始化 | `pnpm install` |
| C4 发布级别 | ① main-only(开发期) |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
| 文档 | 内容 |
|---|---|
| [docs/需求/KPI考核管理系统-功能性需求文档-V1.3.md](docs/需求/KPI考核管理系统-功能性需求文档-V1.3.md) | 客户提供的原始功能性需求(V1.3,2026-09-02) |
| [docs/00-设计方案.md](docs/00-设计方案.md) | 设计方案 V1.0(客户 2026-09-02 确认,唯一需求基准) |
| [docs/00-设计方案.md](docs/00-设计方案.md) | 设计方案 V1.0(客户 2026-09-02 确认,唯一需求基准);Word 成品在 [docs/交付/](docs/交付/)(V0.9 送审稿、V1.0 客户确认版),由 `pnpm docs:docx` 从本源稿生成 |
| [docs/01-需求解读报告.md](docs/01-需求解读报告.md) | 场景地图、平台能力覆盖度、风险、疑点(疑点已由设计方案 V1.0 关闭) |
| [docs/02-总体方案蓝图.md](docs/02-总体方案蓝图.md) | os 能力映射、对象模型总图、模块依赖与开发顺序 |
| [docs/03-方案分级.md](docs/03-方案分级.md) | 三问分级;三处多路径选型已确认路径 A |
Expand Down
Binary file not shown.
Binary file not shown.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"typecheck": "tsc --noEmit",
"test": "vitest run",
"verify": "pnpm validate && pnpm typecheck && pnpm test",
"e2e": "node scripts/e2e-flow.mjs"
"e2e": "node scripts/e2e-flow.mjs",
"docs:docx": "node scripts/md2docx.cjs docs/00-设计方案.md docs/交付/KPI考核管理系统-设计方案-V1.0.docx"
},
"dependencies": {
"@objectstack/connector-mcp": "^17.2.0",
Expand All @@ -32,6 +33,7 @@
},
"devDependencies": {
"@objectstack/cli": "^17.2.0",
"docx": "^9.7.1",
"typescript": "^6.0.0",
"vitest": "^4.1.10"
}
Expand Down
331 changes: 198 additions & 133 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

90 changes: 90 additions & 0 deletions scripts/md2docx.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Minimal Markdown → docx converter for the design proposal (headings, cover div,
// version table, tables with captions, bullets, paragraphs, code block, page breaks).
const fs = require('fs');
const path = require('path');
const D = require('docx');
const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, WidthType, HeadingLevel, AlignmentType, PageBreak, ShadingType, BorderStyle, LevelFormat, TableOfContents, Footer, Header, PageNumber } = D;

const src = fs.readFileSync(process.argv[2], 'utf8').split('\n');
const out = process.argv[3];
let coverChildren = []; let versionText = "";
const FONT_CN = 'SimSun', FONT_HEAD = 'SimHei', FONT_EN = 'Times New Roman', FONT_MONO = 'Consolas';
const runs = (text, extra = {}) => {
// inline **bold** and `code`
const parts = [];
const re = /(\*\*[^*]+\*\*|`[^`]+`)/g; let last = 0; let m;
while ((m = re.exec(text))) {
if (m.index > last) parts.push(new TextRun({ text: text.slice(last, m.index), font: { eastAsia: FONT_CN, ascii: FONT_EN, hAnsi: FONT_EN }, size: 24, ...extra }));
const t = m[0];
if (t.startsWith('**')) parts.push(new TextRun({ text: t.slice(2, -2), bold: true, font: { eastAsia: FONT_CN, ascii: FONT_EN, hAnsi: FONT_EN }, size: 24, ...extra }));
else parts.push(new TextRun({ text: t.slice(1, -1), font: { eastAsia: FONT_MONO, ascii: FONT_MONO, hAnsi: FONT_MONO }, size: 22, ...extra }));
last = m.index + t.length;
}
if (last < text.length) parts.push(new TextRun({ text: text.slice(last), font: { eastAsia: FONT_CN, ascii: FONT_EN, hAnsi: FONT_EN }, size: 24, ...extra }));
return parts;
};
const para = (text, opts = {}) => new Paragraph({ children: runs(text, opts.run || {}), spacing: { after: 120, line: 360 }, alignment: opts.align, indent: opts.indent });
const children = [];
let i = 0, inCover = false, inCode = false, codeLines = [], tableRows = [], pendingCaption = null, sawFirstH1 = false;
const flushTable = () => {
if (!tableRows.length) return;
const rows = tableRows.map(r => r.split('|').slice(1, -1).map(c => c.trim()));
const body = rows.filter((r, idx) => !(idx === 1 && r.every(c => /^:?-+:?$/.test(c))));
const cols = body[0].length; const total = 9000; const w = Math.floor(total / cols);
const table = new Table({
width: { size: total, type: WidthType.DXA }, columnWidths: Array(cols).fill(w),
rows: body.map((r, ri) => new TableRow({ tableHeader: ri === 0, cantSplit: true, children: r.map(c => new TableCell({
width: { size: w, type: WidthType.DXA },
shading: ri === 0 ? { fill: 'E7EBF0', type: ShadingType.CLEAR, color: 'auto' } : undefined,
margins: { top: 60, bottom: 60, left: 100, right: 100 },
children: [new Paragraph({ children: runs(c === '' ? '—' : c, { size: 21, bold: ri === 0 }), spacing: { after: 0, line: 300 } })],
})) })),
});
if (pendingCaption) { children.push(new Paragraph({ children: runs(pendingCaption, { bold: true, size: 22 }), spacing: { before: 160, after: 80 }, keepNext: true })); pendingCaption = null; }
children.push(table); children.push(new Paragraph({ text: '', spacing: { after: 60 } }));
tableRows = [];
};
while (i < src.length) {
const line = src[i]; i++;
if (line.startsWith('```')) { if (inCode) { children.push(new Paragraph({ children: [new TextRun({ text: codeLines.join('\n').replace(/\n/g, ' / '), font: { eastAsia: FONT_MONO, ascii: FONT_MONO, hAnsi: FONT_MONO }, size: 19 })], shading: { fill: 'F2F4F7', type: ShadingType.CLEAR, color: 'auto' }, spacing: { after: 120 } })); codeLines = []; inCode = false; } else inCode = true; continue; }
if (inCode) { codeLines.push(line); continue; }
if (line.startsWith('|')) { tableRows.push(line); continue; } else flushTable();
if (line.startsWith('<div align="center">')) { inCover = true; continue; }
if (line.startsWith('</div>')) { inCover = false; continue; }
if (line.trim() === '<br>' || line.trim() === '') { if (inCover) coverChildren.push(new Paragraph({ text: '' })); continue; }
if (line.trim() === '---') { continue; }
if (inCover) {
const t = line.replace(/^#+\s*/, '').replace(/\*\*/g, '');
const isTitle = line.startsWith('#');
if (/^版本号/.test(t)) versionText = t.replace(/^版本号[::]\s*/, "").replace(/[((].*$/, "").trim();
coverChildren.push(new Paragraph({ children: [new TextRun({ text: t, bold: true, size: isTitle ? 48 : 28, font: { eastAsia: isTitle ? FONT_HEAD : FONT_CN, ascii: FONT_EN, hAnsi: FONT_EN } })], alignment: AlignmentType.CENTER, spacing: { before: isTitle ? 360 : 120, after: 120 } }));
continue;
}
if (line.startsWith('## 目录')) { children.push(new Paragraph({ children: [new TextRun({ text: '目录', bold: true, size: 32, font: { eastAsia: FONT_HEAD } })], spacing: { before: 240, after: 120 } })); children.push(new TableOfContents('目录', { hyperlink: true, headingStyleRange: '1-2' })); while (i < src.length && !src[i].startsWith('---')) i++; children.push(new Paragraph({ children: [new PageBreak()] })); continue; }
let m;
if ((m = line.match(/^# (.+)/))) { children.push(new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun({ text: m[1], bold: true, size: 32, font: { eastAsia: FONT_HEAD, ascii: FONT_EN, hAnsi: FONT_EN }, color: '1B2540' })], spacing: { before: 360, after: 160 }, pageBreakBefore: sawFirstH1 && !/^1\./.test(m[1]) ? false : false })); sawFirstH1 = true; continue; }
if ((m = line.match(/^## (.+)/))) { children.push(new Paragraph({ heading: HeadingLevel.HEADING_2, children: [new TextRun({ text: m[1], bold: true, size: 28, font: { eastAsia: FONT_HEAD, ascii: FONT_EN, hAnsi: FONT_EN }, color: '1B2540' })], spacing: { before: 240, after: 120 } })); continue; }
if ((m = line.match(/^### (.+)/))) { children.push(new Paragraph({ heading: HeadingLevel.HEADING_3, children: [new TextRun({ text: m[1], bold: true, size: 24, font: { eastAsia: FONT_HEAD, ascii: FONT_EN, hAnsi: FONT_EN }, color: '1B2540' })], spacing: { before: 200, after: 100 } })); continue; }
if ((m = line.match(/^\*\*(表 \d+ [^*]+)\*\*$/))) { pendingCaption = m[1]; continue; }
if (line.startsWith(">")) { const qt = line.replace(/^>\s?/, ""); if (qt.trim() === "") continue; children.push(new Paragraph({ children: runs(qt, { size: 21 }), indent: { left: 480, right: 480 }, shading: { fill: "F2F4F7", type: ShadingType.CLEAR, color: "auto" }, spacing: { after: 40, line: 300 } })); continue; }
if ((m = line.match(/^(图 \d+ .+)$/))) { children.push(new Paragraph({ children: runs(m[1], { bold: true, size: 22 }), alignment: AlignmentType.CENTER, spacing: { after: 160 } })); continue; }
if ((m = line.match(/^- (.+)/))) { children.push(new Paragraph({ children: runs(m[1]), numbering: { reference: 'bul', level: 0 }, spacing: { after: 80, line: 340 } })); continue; }
children.push(para(line));
}
flushTable();
const doc = new Document({
creator: 'KPI 项目组', title: 'KPI 考核管理系统 设计方案 ' + versionText,
styles: { default: { document: { run: { font: { eastAsia: FONT_CN, ascii: FONT_EN, hAnsi: FONT_EN }, size: 24 } } } },
numbering: { config: [{ reference: 'bul', levels: [{ level: 0, format: LevelFormat.BULLET, text: '•', alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 480, hanging: 240 } } } }] }] },
features: { updateFields: true },
sections: [{
properties: { page: { margin: { top: 1440, bottom: 1440, left: 1500, right: 1500 } } },
children: coverChildren,
}, {
properties: { page: { margin: { top: 1440, bottom: 1440, left: 1500, right: 1500 }, pageNumbers: { start: 1 } } },
headers: { default: new Header({ children: [new Paragraph({ children: [new TextRun({ text: 'KPI 考核管理系统 · 设计方案 ' + versionText, size: 18, color: '6C7686', font: { eastAsia: FONT_CN } })], alignment: AlignmentType.RIGHT })] }) },
footers: { default: new Footer({ children: [new Paragraph({ children: [new TextRun({ text: '第 ', size: 18, font: { eastAsia: FONT_CN } }), new TextRun({ children: [PageNumber.CURRENT], size: 18 }), new TextRun({ text: ' 页 共 ', size: 18, font: { eastAsia: FONT_CN } }), new TextRun({ children: [PageNumber.TOTAL_PAGES], size: 18 }), new TextRun({ text: ' 页', size: 18, font: { eastAsia: FONT_CN } })], alignment: AlignmentType.CENTER })] }) },
children,
}],
});
Packer.toBuffer(doc).then(b => { fs.writeFileSync(out, b); console.log('wrote', out, b.length); });
Loading