@openprd/cli 0.1.8 → 0.1.10

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 (134) hide show
  1. package/.openprd/benchmarks/index.md +15 -1
  2. package/.openprd/config.yaml +10 -0
  3. package/.openprd/design/README.md +47 -0
  4. package/.openprd/design/README_EN.md +25 -0
  5. package/.openprd/design/active/asset-spec.md +15 -0
  6. package/.openprd/design/active/direction-plan.md +7 -0
  7. package/.openprd/design/active/facts-sheet.md +13 -0
  8. package/.openprd/design/active/image-preflight.md +9 -0
  9. package/.openprd/design/active/selected-direction.md +11 -0
  10. package/.openprd/design/anti-slop.md +16 -0
  11. package/.openprd/design/assets/dark-mesh.svg +14 -0
  12. package/.openprd/design/assets/paper-grid.svg +14 -0
  13. package/.openprd/design/assets/surface-presets.md +21 -0
  14. package/.openprd/design/assets/workbench-grid.svg +16 -0
  15. package/.openprd/design/checklists/ui-quality-gate.md +22 -0
  16. package/.openprd/design/components/component-catalog.md +42 -0
  17. package/.openprd/design/layouts/layout-catalog.json +45 -0
  18. package/.openprd/design/lenses/frontend-lenses.md +31 -0
  19. package/.openprd/design/recipes/content-experience.md +26 -0
  20. package/.openprd/design/recipes/operational-tool.md +25 -0
  21. package/.openprd/design/recipes/product-launch.md +26 -0
  22. package/.openprd/design/templates/README.md +36 -0
  23. package/.openprd/design/templates/README_EN.md +31 -0
  24. package/.openprd/design/templates/content-home.html +415 -0
  25. package/.openprd/design/templates/ops-dashboard.html +340 -0
  26. package/.openprd/design/templates/product-launch.html +360 -0
  27. package/.openprd/design/themes/theme-catalog.json +77 -0
  28. package/.openprd/engagements/active/prd.md +111 -100
  29. package/.openprd/schema/prd.schema.yaml +10 -0
  30. package/.openprd/templates/base/intake.md +4 -0
  31. package/.openprd/templates/base/prd.md +16 -6
  32. package/AGENTS.md +10 -7
  33. package/README.md +28 -10
  34. package/README_EN.md +23 -3
  35. package/package.json +41 -2
  36. package/scripts/openprd-codex-isolated-worker.mjs +799 -0
  37. package/skills/openprd-benchmark-router/SKILL.md +4 -0
  38. package/skills/openprd-frontend-design/SKILL.md +161 -0
  39. package/skills/openprd-frontend-design/references/design-asset-contract.md +34 -0
  40. package/skills/openprd-frontend-design/references/direction-engine.md +46 -0
  41. package/skills/openprd-harness/SKILL.md +38 -21
  42. package/skills/openprd-learning-review/SKILL.md +2 -1
  43. package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +1 -1
  44. package/skills/openprd-quality/SKILL.md +6 -4
  45. package/skills/openprd-requirement-intake/SKILL.md +27 -5
  46. package/skills/openprd-requirement-intake/references/prd-template-lenses.md +6 -3
  47. package/skills/openprd-requirement-intake/references/routing-rubric.md +9 -5
  48. package/skills/openprd-requirement-intake/references/startup-validation-lens.md +23 -0
  49. package/skills/openprd-router/SKILL.md +13 -5
  50. package/skills/openprd-shared/SKILL.md +38 -25
  51. package/src/agent-integration.js +126 -75
  52. package/src/brainstorm-artifacts.js +1657 -0
  53. package/src/brainstorm-presentation.js +223 -0
  54. package/src/brainstorm.js +603 -0
  55. package/src/cli/args.js +51 -3
  56. package/src/cli/doctor-print.js +34 -8
  57. package/src/cli/print.js +6 -0
  58. package/src/cli/quality-print.js +70 -0
  59. package/src/cli/run-print.js +43 -1
  60. package/src/cli/shared-print.js +33 -2
  61. package/src/cli/workflow-print.js +21 -0
  62. package/src/codex-hook-runner-template.mjs +858 -65
  63. package/src/design-starter-support.js +462 -0
  64. package/src/design-starter.js +1207 -0
  65. package/src/knowledge.js +377 -21
  66. package/src/learning-review.js +3 -3
  67. package/src/loop.js +839 -101
  68. package/src/openprd.js +86 -1
  69. package/src/openspec/change-validate.js +1 -0
  70. package/src/prd-core.js +18 -0
  71. package/src/quality-learning.js +50 -6
  72. package/src/quality.js +2 -2
  73. package/src/run-harness.js +191 -29
  74. package/src/self-update.js +301 -4
  75. package/src/standards.js +46 -5
  76. package/src/visual-prepare.js +940 -0
  77. package/src/workspace-core.js +137 -0
  78. package/src/workspace-workflow.js +190 -7
  79. package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +0 -14
  80. package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +0 -14
  81. package/.openprd/discovery/config.json +0 -35
  82. package/.openprd/engagements/active/flows.md +0 -35
  83. package/.openprd/engagements/active/handoff.md +0 -16
  84. package/.openprd/engagements/active/review.html +0 -61
  85. package/.openprd/engagements/active/roles.md +0 -22
  86. package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +0 -23
  87. package/.openprd/engagements/work-units/wu-20260602113956-a99b5b88.json +0 -18
  88. package/.openprd/engagements/work-units/wu-20260602122244-78656aaf.json +0 -18
  89. package/.openprd/engagements/work-units/wu-20260602122442-e96489e2.json +0 -18
  90. package/.openprd/engagements/work-units/wu-20260602132835-695429e8.json +0 -18
  91. package/.openprd/exports/.gitkeep +0 -0
  92. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/candidate.json +0 -78
  93. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/diagnostic-report.json +0 -129
  94. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/root-cause-candidates.json +0 -41
  95. package/.openprd/knowledge/candidates/candidate-turn-1780116203372-5f266a79e968c758/timeline.json +0 -14
  96. package/.openprd/knowledge/drafts/openprd-experience-diagnostic-candidate-turn-1780116203372-5f266a79e968c758/SKILL.md +0 -49
  97. package/.openprd/knowledge/index.json +0 -47
  98. package/.openprd/reviews/v0001.html +0 -1322
  99. package/.openprd/reviews/v0002.html +0 -1150
  100. package/.openprd/reviews/v0003.html +0 -1150
  101. package/.openprd/reviews/v0004.html +0 -1150
  102. package/.openprd/reviews/v0005.html +0 -1150
  103. package/.openprd/sessions/.gitkeep +0 -0
  104. package/.openprd/state/changes.json +0 -27
  105. package/.openprd/state/current.json +0 -505
  106. package/.openprd/state/release-ledger.json +0 -344
  107. package/.openprd/state/version-index.json +0 -67
  108. package/.openprd/state/versions/.gitkeep +0 -0
  109. package/.openprd/state/versions/v0001.json +0 -121
  110. package/.openprd/state/versions/v0001.md +0 -161
  111. package/.openprd/state/versions/v0002.json +0 -264
  112. package/.openprd/state/versions/v0002.md +0 -183
  113. package/.openprd/state/versions/v0003.json +0 -269
  114. package/.openprd/state/versions/v0003.md +0 -188
  115. package/.openprd/state/versions/v0004.json +0 -274
  116. package/.openprd/state/versions/v0004.md +0 -193
  117. package/.openprd/state/versions/v0005.json +0 -299
  118. package/.openprd/state/versions/v0005.md +0 -189
  119. package/docs/assets/openprd-capability-overview-en.png +0 -0
  120. package/docs/assets/openprd-capability-overview-zh.png +0 -0
  121. package/docs/assets/openprd-learning-html.png +0 -0
  122. package/docs/assets/openprd-quality-html.png +0 -0
  123. package/docs/assets/openprd-requirement-routing-en.png +0 -0
  124. package/docs/assets/openprd-requirement-routing-en.svg +0 -102
  125. package/docs/assets/openprd-requirement-routing-zh-refined.png +0 -0
  126. package/docs/assets/openprd-requirement-routing-zh.png +0 -0
  127. package/docs/assets/openprd-requirement-routing-zh.svg +0 -102
  128. package/docs/assets/openprd-review-html.png +0 -0
  129. package/docs/assets/openprd-scenario-overview.png +0 -0
  130. package/docs/assets/openprd-scenario-overview.svg +0 -114
  131. package/docs/assets/openprd-self-evolving-mechanisms-en.png +0 -0
  132. package/docs/assets/openprd-self-evolving-mechanisms-zh.png +0 -0
  133. package/docs/assets/openprd-visual-compare-case-study-en.png +0 -0
  134. package/docs/assets/openprd-visual-compare-case-study-zh.png +0 -0
@@ -0,0 +1,462 @@
1
+ /*
2
+ * 核心功能
3
+ * 为 design-starter 提供主题锁定、表面资产注入和 Wikimedia Commons 真实图片补充。
4
+ *
5
+ * 输入
6
+ * 接收 starter 上下文、输出文件路径和工作区路径,并在需要时访问主题 catalog 与 Commons API。
7
+ *
8
+ * 输出
9
+ * 返回主题样式片段、自动补图结果和可写回设计合同的图片元数据。
10
+ *
11
+ * 定位
12
+ * 位于前端实现准备层,承接“选模板之后怎样真正吃到设计资产和真实图片”的运行时能力。
13
+ *
14
+ * 依赖
15
+ * 依赖 fs-utils 读写 JSON、检查文件存在,并使用 Node 原生 fetch 下载远程图片。
16
+ *
17
+ * 维护规则
18
+ * 新增 theme 字段、表面预设或图片来源策略时,优先在这里扩展,再回到 design-starter 调整占位映射。
19
+ */
20
+ import fs from 'node:fs/promises';
21
+ import path from 'node:path';
22
+ import { URL } from 'node:url';
23
+ import { exists, readJson, writeJson } from './fs-utils.js';
24
+
25
+ const REAL_IMAGE_HINT_RE = /(?:museum|gallery|exhibition|travel|traveler|guide|visitor guide|tour|destination|city|architecture|landscape|wildlife|birdwatch|species|heritage|cultural|park|island|coast|forest|rainforest|wetland|reserve|bird|parrot|zoo|aquarium|历史|文化|导览|馆藏|展览|博物馆|旅行|旅游|城市|建筑|风景|自然|野生|观鸟|物种|国家公园|海岛|海岸|雨林|湿地|保护区|动物园|水族馆)/iu;
26
+ const REAL_IMAGE_SKIP_RE = /(?:dashboard|ops|backstage|control center|cms|admin|blog|portfolio|saas|crm|工作台|后台|控制台|博客|作品集|内容发布|开发者|独立开发者)/iu;
27
+ const COMMONS_IMAGE_COUNT = 3;
28
+ const COMMONS_IMAGE_WIDTH = 1600;
29
+
30
+ const SURFACE_PRESETS = {
31
+ 'paper-grid': {
32
+ asset: 'paper-grid.svg',
33
+ bodyLayers: [
34
+ 'radial-gradient(circle at top left, rgba(191, 142, 108, 0.18), transparent 34%)',
35
+ 'linear-gradient(180deg, rgba(255, 251, 244, 0.96) 0%, var(--bg) 100%)',
36
+ ],
37
+ overlayOpacity: 0.4,
38
+ },
39
+ 'dark-mesh': {
40
+ asset: 'dark-mesh.svg',
41
+ bodyLayers: [
42
+ 'radial-gradient(circle at top center, rgba(52, 211, 162, 0.14), transparent 26%)',
43
+ 'linear-gradient(180deg, rgba(7, 13, 20, 0.98) 0%, var(--bg) 100%)',
44
+ ],
45
+ overlayOpacity: 0.38,
46
+ },
47
+ 'workbench-grid': {
48
+ asset: 'workbench-grid.svg',
49
+ bodyLayers: [
50
+ 'radial-gradient(circle at top left, rgba(47, 128, 237, 0.12), transparent 28%)',
51
+ 'linear-gradient(180deg, rgba(248, 250, 252, 0.96) 0%, var(--bg) 100%)',
52
+ ],
53
+ overlayOpacity: 0.32,
54
+ },
55
+ };
56
+
57
+ function normalizePathForDisplay(filePath) {
58
+ return String(filePath ?? '').replaceAll('\\', '/');
59
+ }
60
+
61
+ function decodeHtmlEntities(text) {
62
+ return String(text ?? '')
63
+ .replaceAll('&', '&')
64
+ .replaceAll('"', '"')
65
+ .replaceAll(''', '\'')
66
+ .replaceAll('&lt;', '<')
67
+ .replaceAll('&gt;', '>')
68
+ .replaceAll('&nbsp;', ' ');
69
+ }
70
+
71
+ function stripHtml(value) {
72
+ return decodeHtmlEntities(String(value ?? '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim());
73
+ }
74
+
75
+ function withAlpha(hexColor, alpha) {
76
+ const normalized = String(hexColor ?? '').trim();
77
+ const hex = normalized.startsWith('#') ? normalized.slice(1) : normalized;
78
+ if (!/^[0-9a-fA-F]{6}$/.test(hex)) {
79
+ return normalized;
80
+ }
81
+ const r = Number.parseInt(hex.slice(0, 2), 16);
82
+ const g = Number.parseInt(hex.slice(2, 4), 16);
83
+ const b = Number.parseInt(hex.slice(4, 6), 16);
84
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
85
+ }
86
+
87
+ function commonsApiBaseUrl() {
88
+ return process.env.OPENPRD_COMMONS_API_BASE_URL?.trim() || 'https://commons.wikimedia.org/w/api.php';
89
+ }
90
+
91
+ function derivedQueryFromText(text) {
92
+ const asciiWords = String(text ?? '')
93
+ .match(/[A-Za-z0-9][A-Za-z0-9.+-]*/g) ?? [];
94
+ const filteredAscii = asciiWords
95
+ .map((word) => word.trim())
96
+ .filter(Boolean)
97
+ .filter((word) => !/^(a|an|the|and|for|with|home|page|homepage|landing|site|prototype|guide|app|web|build)$/i.test(word));
98
+ if (filteredAscii.length >= 2) {
99
+ return filteredAscii.slice(0, 6).join(' ');
100
+ }
101
+ return String(text ?? '')
102
+ .replace(/(?:首页|页面|网站|原型|工作台|发布页|导览页|内容型|第一版|请直接实现|帮助|提供|用户|一版)/g, ' ')
103
+ .replace(/\s+/g, ' ')
104
+ .trim();
105
+ }
106
+
107
+ function buildStarterImageHaystack(context) {
108
+ if (!context) {
109
+ return '';
110
+ }
111
+ return [
112
+ context.displayTitle,
113
+ context.brief,
114
+ ...(Array.isArray(context.sections) ? context.sections : []),
115
+ ]
116
+ .map((item) => String(item ?? '').trim())
117
+ .filter(Boolean)
118
+ .join(' ');
119
+ }
120
+
121
+ function isRealImageDependentStarterContext(starter, context) {
122
+ if (!context || starter?.id === 'ops-dashboard') {
123
+ return false;
124
+ }
125
+ const haystack = buildStarterImageHaystack(context);
126
+ if (!haystack) {
127
+ return false;
128
+ }
129
+ return REAL_IMAGE_HINT_RE.test(haystack);
130
+ }
131
+
132
+ function shouldAttemptStarterImages(starter, context) {
133
+ if (!context || context.noRealImages || starter?.id === 'ops-dashboard') {
134
+ return false;
135
+ }
136
+ const haystack = buildStarterImageHaystack(context);
137
+ if (!haystack) {
138
+ return false;
139
+ }
140
+ if (isRealImageDependentStarterContext(starter, context)) {
141
+ return true;
142
+ }
143
+ return !REAL_IMAGE_SKIP_RE.test(haystack) && starter?.id === 'product-launch';
144
+ }
145
+
146
+ function deriveCommonsQuery(context) {
147
+ const fromTitle = derivedQueryFromText(context?.displayTitle);
148
+ if (fromTitle) {
149
+ return fromTitle;
150
+ }
151
+ return derivedQueryFromText(context?.brief);
152
+ }
153
+
154
+ function normalizeTheme(theme) {
155
+ if (!theme?.tokens) {
156
+ return null;
157
+ }
158
+ return {
159
+ ...theme,
160
+ bodyFont: String(theme.bodyFont ?? '"Inter", "PingFang SC", sans-serif'),
161
+ headlineFont: String(theme.headlineFont ?? '"Inter", "PingFang SC", sans-serif'),
162
+ surfacePreset: String(theme.surfacePreset ?? 'paper-grid'),
163
+ };
164
+ }
165
+
166
+ async function loadThemeConfig(themeCatalogPath, themeId) {
167
+ if (!themeId || !(await exists(themeCatalogPath))) {
168
+ return null;
169
+ }
170
+ const catalog = await readJson(themeCatalogPath).catch(() => null);
171
+ const match = catalog?.themes?.find((theme) => theme.id === themeId) ?? null;
172
+ return normalizeTheme(match);
173
+ }
174
+
175
+ function relativeAssetUrl(outputPath, absoluteAssetPath) {
176
+ return normalizePathForDisplay(path.relative(path.dirname(outputPath), absoluteAssetPath));
177
+ }
178
+
179
+ function buildThemeLockStyle({ theme, projectRoot, outputPath }) {
180
+ if (!theme?.tokens) {
181
+ return '';
182
+ }
183
+ const preset = SURFACE_PRESETS[theme.surfacePreset] ?? SURFACE_PRESETS['paper-grid'];
184
+ const assetPath = path.join(projectRoot, '.openprd', 'design', 'assets', preset.asset);
185
+ const overlayUrl = relativeAssetUrl(outputPath, assetPath);
186
+ const radius = theme.tokens.radius ?? '8px';
187
+ const styleLines = [
188
+ ':root {',
189
+ ` --bg: ${theme.tokens.background};`,
190
+ ` --bg-deep: ${theme.tokens.background};`,
191
+ ` --panel: ${theme.tokens.surface};`,
192
+ ` --panel-strong: ${theme.tokens.surface};`,
193
+ ` --panel-soft: ${withAlpha(theme.tokens.surface, 0.82)};`,
194
+ ` --line: ${withAlpha(theme.tokens.border, 0.85)};`,
195
+ ` --line-strong: ${withAlpha(theme.tokens.border, 1)};`,
196
+ ` --ink: ${theme.tokens.text};`,
197
+ ` --text: ${theme.tokens.text};`,
198
+ ` --muted: ${theme.tokens.muted};`,
199
+ ` --soft: ${theme.tokens.muted};`,
200
+ ` --accent: ${theme.tokens.accent};`,
201
+ ` --accent-strong: ${theme.tokens.accent};`,
202
+ ` --accent-soft: ${withAlpha(theme.tokens.accent, 0.14)};`,
203
+ ` --warm: ${theme.tokens.accent};`,
204
+ ` --warm-soft: ${withAlpha(theme.tokens.accent, 0.16)};`,
205
+ ` --shadow: ${theme.tokens.shadow};`,
206
+ ` --radius: ${radius};`,
207
+ ` --radius-md: ${radius};`,
208
+ ` --radius-lg: calc(${radius} + 6px);`,
209
+ ` --radius-xl: calc(${radius} + 14px);`,
210
+ ` --body-font: ${theme.bodyFont};`,
211
+ ` --headline-font: ${theme.headlineFont};`,
212
+ '}',
213
+ 'body {',
214
+ ` font-family: var(--body-font, ${theme.bodyFont});`,
215
+ ` background: ${preset.bodyLayers.join(', ')};`,
216
+ '}',
217
+ 'body::after {',
218
+ ' content: "";',
219
+ ' position: fixed;',
220
+ ' inset: 0;',
221
+ ` background: url("${overlayUrl}") center / cover repeat;`,
222
+ ` opacity: ${preset.overlayOpacity};`,
223
+ ' pointer-events: none;',
224
+ ' z-index: 0;',
225
+ '}',
226
+ '.page {',
227
+ ' position: relative;',
228
+ ' z-index: 1;',
229
+ '}',
230
+ 'h1, h2, h3, .brand, .sidebar-copy h1, .topbar-copy h2 {',
231
+ ` font-family: var(--headline-font, ${theme.headlineFont}) !important;`,
232
+ '}',
233
+ '.hero, .section, .cta, .sidebar, .topbar-shell, .metric-card, .panel {',
234
+ ' box-shadow: var(--shadow);',
235
+ '}',
236
+ ];
237
+ return `<style data-openprd-theme-lock>\n${styleLines.join('\n')}\n</style>`;
238
+ }
239
+
240
+ function injectThemeLockStyle(html, themeLockStyle) {
241
+ if (!themeLockStyle) {
242
+ return html;
243
+ }
244
+ if (html.includes('</head>')) {
245
+ return html.replace('</head>', `${themeLockStyle}\n </head>`);
246
+ }
247
+ return `${themeLockStyle}\n${html}`;
248
+ }
249
+
250
+ function buildCommonsSearchUrl(query, limit) {
251
+ const url = new URL(commonsApiBaseUrl());
252
+ url.searchParams.set('action', 'query');
253
+ url.searchParams.set('generator', 'search');
254
+ url.searchParams.set('gsrnamespace', '6');
255
+ url.searchParams.set('gsrsearch', query);
256
+ url.searchParams.set('gsrlimit', String(limit));
257
+ url.searchParams.set('prop', 'imageinfo');
258
+ url.searchParams.set('iiprop', 'url|extmetadata');
259
+ url.searchParams.set('iiurlwidth', String(COMMONS_IMAGE_WIDTH));
260
+ url.searchParams.set('format', 'json');
261
+ url.searchParams.set('origin', '*');
262
+ return url.toString();
263
+ }
264
+
265
+ function normalizeImageExtension(imageUrl, mimeType) {
266
+ const parsedUrl = new URL(imageUrl);
267
+ const extension = path.extname(parsedUrl.pathname).toLowerCase();
268
+ if (extension) {
269
+ return extension;
270
+ }
271
+ if (mimeType === 'image/png') {
272
+ return '.png';
273
+ }
274
+ if (mimeType === 'image/webp') {
275
+ return '.webp';
276
+ }
277
+ if (mimeType === 'image/svg+xml') {
278
+ return '.svg';
279
+ }
280
+ return '.jpg';
281
+ }
282
+
283
+ function normalizeTitle(title) {
284
+ return String(title ?? '')
285
+ .replace(/^File:/i, '')
286
+ .replaceAll('_', ' ')
287
+ .trim();
288
+ }
289
+
290
+ function buildAltText(candidate) {
291
+ const description = stripHtml(candidate?.description);
292
+ if (description) {
293
+ return description;
294
+ }
295
+ return normalizeTitle(candidate?.title) || '参考图片';
296
+ }
297
+
298
+ async function fetchJson(url, fetchImpl) {
299
+ const response = await fetchImpl(url, {
300
+ headers: {
301
+ 'user-agent': '@openprd/cli design-starter image sourcing',
302
+ },
303
+ });
304
+ if (!response.ok) {
305
+ throw new Error(`图片搜索失败: ${response.status}`);
306
+ }
307
+ return response.json();
308
+ }
309
+
310
+ async function downloadBinary(url, fetchImpl) {
311
+ const response = await fetchImpl(url, {
312
+ headers: {
313
+ 'user-agent': '@openprd/cli design-starter image sourcing',
314
+ },
315
+ });
316
+ if (!response.ok) {
317
+ throw new Error(`图片下载失败: ${response.status}`);
318
+ }
319
+ const bytes = Buffer.from(await response.arrayBuffer());
320
+ return {
321
+ bytes,
322
+ mimeType: response.headers.get('content-type')?.split(';')[0]?.trim() || 'image/jpeg',
323
+ };
324
+ }
325
+
326
+ async function sourceStarterImages({ projectRoot, outputPath, starter, context, fetchImpl = globalThis.fetch }) {
327
+ if (typeof fetchImpl !== 'function') {
328
+ return {
329
+ attempted: false,
330
+ succeeded: false,
331
+ assets: [],
332
+ query: null,
333
+ manifestPath: null,
334
+ relativeManifestPath: null,
335
+ reason: '当前运行环境没有可用 fetch',
336
+ };
337
+ }
338
+ if (!shouldAttemptStarterImages(starter, context)) {
339
+ return {
340
+ attempted: false,
341
+ succeeded: false,
342
+ assets: [],
343
+ query: null,
344
+ manifestPath: null,
345
+ relativeManifestPath: null,
346
+ reason: '当前页面不满足自动补图条件',
347
+ };
348
+ }
349
+ const query = deriveCommonsQuery(context);
350
+ if (!query) {
351
+ return {
352
+ attempted: false,
353
+ succeeded: false,
354
+ assets: [],
355
+ query: null,
356
+ manifestPath: null,
357
+ relativeManifestPath: null,
358
+ reason: '无法从页面主题中提取可搜索关键词',
359
+ };
360
+ }
361
+ try {
362
+ const payload = await fetchJson(buildCommonsSearchUrl(query, COMMONS_IMAGE_COUNT), fetchImpl);
363
+ const pages = Object.values(payload?.query?.pages ?? {})
364
+ .map((page) => {
365
+ const imageinfo = page?.imageinfo?.[0] ?? null;
366
+ return {
367
+ title: page?.title ?? '',
368
+ description: imageinfo?.extmetadata?.ImageDescription?.value ?? '',
369
+ sourceUrl: imageinfo?.thumburl ?? imageinfo?.url ?? '',
370
+ pageUrl: imageinfo?.descriptionurl ?? '',
371
+ licenseShortName: stripHtml(imageinfo?.extmetadata?.LicenseShortName?.value),
372
+ licenseUrl: stripHtml(imageinfo?.extmetadata?.LicenseUrl?.value),
373
+ artist: stripHtml(imageinfo?.extmetadata?.Artist?.value),
374
+ credit: stripHtml(imageinfo?.extmetadata?.Credit?.value),
375
+ };
376
+ })
377
+ .filter((page) => page.sourceUrl);
378
+ if (pages.length === 0) {
379
+ return {
380
+ attempted: true,
381
+ succeeded: false,
382
+ assets: [],
383
+ query,
384
+ manifestPath: null,
385
+ relativeManifestPath: null,
386
+ reason: 'Commons 没有返回可下载图片',
387
+ };
388
+ }
389
+ const bundleId = String(context.displayTitle ?? context.brief ?? starter.id)
390
+ .toLowerCase()
391
+ .replace(/[^a-z0-9]+/g, '-')
392
+ .replace(/^-+|-+$/g, '')
393
+ .slice(0, 48) || starter.id;
394
+ const outputDir = path.join(projectRoot, '.openprd', 'design', 'assets', 'fetched', bundleId);
395
+ await fs.mkdir(outputDir, { recursive: true });
396
+ const assets = [];
397
+ for (const [index, page] of pages.entries()) {
398
+ const { bytes, mimeType } = await downloadBinary(page.sourceUrl, fetchImpl);
399
+ const fileName = `image-${index + 1}${normalizeImageExtension(page.sourceUrl, mimeType)}`;
400
+ const absolutePath = path.join(outputDir, fileName);
401
+ await fs.writeFile(absolutePath, bytes);
402
+ assets.push({
403
+ index: index + 1,
404
+ title: normalizeTitle(page.title),
405
+ alt: buildAltText(page),
406
+ sourceUrl: page.sourceUrl,
407
+ pageUrl: page.pageUrl,
408
+ licenseShortName: page.licenseShortName || 'License unavailable',
409
+ licenseUrl: page.licenseUrl || '',
410
+ artist: page.artist || '',
411
+ credit: page.credit || '',
412
+ absolutePath,
413
+ relativePath: relativeAssetUrl(outputPath, absolutePath),
414
+ });
415
+ }
416
+ const manifestPath = path.join(outputDir, 'manifest.json');
417
+ await writeJson(manifestPath, {
418
+ source: 'Wikimedia Commons',
419
+ query,
420
+ fetchedAt: new Date().toISOString(),
421
+ assets: assets.map((asset) => ({
422
+ index: asset.index,
423
+ title: asset.title,
424
+ alt: asset.alt,
425
+ relativePath: asset.relativePath,
426
+ pageUrl: asset.pageUrl,
427
+ sourceUrl: asset.sourceUrl,
428
+ licenseShortName: asset.licenseShortName,
429
+ licenseUrl: asset.licenseUrl,
430
+ artist: asset.artist,
431
+ credit: asset.credit,
432
+ })),
433
+ });
434
+ return {
435
+ attempted: true,
436
+ succeeded: assets.length > 0,
437
+ assets,
438
+ query,
439
+ manifestPath,
440
+ relativeManifestPath: normalizePathForDisplay(path.relative(projectRoot, manifestPath)),
441
+ reason: assets.length > 0 ? null : '下载结果为空',
442
+ };
443
+ } catch (error) {
444
+ return {
445
+ attempted: true,
446
+ succeeded: false,
447
+ assets: [],
448
+ query,
449
+ manifestPath: null,
450
+ relativeManifestPath: null,
451
+ reason: error instanceof Error ? error.message : '自动补图失败',
452
+ };
453
+ }
454
+ }
455
+
456
+ export {
457
+ buildThemeLockStyle,
458
+ injectThemeLockStyle,
459
+ isRealImageDependentStarterContext,
460
+ loadThemeConfig,
461
+ sourceStarterImages,
462
+ };