openyida 2026.9.11-3 → 2026.9.11

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 (73) hide show
  1. package/README.md +4 -6
  2. package/bin/yida.js +0 -1
  3. package/lib/ai/ai.js +0 -2
  4. package/lib/app/update-app.js +2 -17
  5. package/lib/asset/ai-image.js +65 -37
  6. package/lib/asset/asset-cmd.js +110 -151
  7. package/lib/asset/asset-resolve.js +225 -343
  8. package/lib/asset/asset-status.js +31 -20
  9. package/lib/asset/url-verify.js +130 -90
  10. package/lib/core/agent-capabilities.js +0 -11
  11. package/lib/core/command-manifest.js +10 -24
  12. package/lib/core/locales/en.js +2 -6
  13. package/lib/core/locales/zh.js +2 -6
  14. package/lib/design-plan/materialize.js +9 -7
  15. package/lib/design-plan/normalize.js +1 -11
  16. package/lib/process/services/process-compiler.js +30 -4
  17. package/lib/process/services/process-view-verifier.js +0 -31
  18. package/package.json +1 -1
  19. package/scripts/postinstall.js +1 -1
  20. package/yida-skills/SKILL.md +2 -3
  21. package/yida-skills/skills/yida-app/SKILL.md +1 -1
  22. package/yida-skills/skills/yida-app/workflow/step-2-design.md +1 -11
  23. package/yida-skills/skills/yida-app/workflow/step-3-create-or-reuse-app.md +1 -21
  24. package/yida-skills/skills/yida-app/workflow/step-7-page-code.md +0 -4
  25. package/yida-skills/skills/yida-canvas-custom-page/references/page-generation-guide.md +27 -3
  26. package/yida-skills/skills/yida-create-app/SKILL.md +4 -4
  27. package/yida-skills/skills/yida-create-process/SKILL.md +0 -2
  28. package/yida-skills/skills/yida-custom-page/references/assets-guide.md +12 -9
  29. package/yida-skills/skills/yida-design/SKILL.md +0 -1
  30. package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +4 -4
  31. package/yida-skills/skills/yida-design/references/asset-workflow.md +53 -14
  32. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-compact-schema.md +1 -1
  33. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-schema.md +4 -21
  34. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/scripts/render_build_plan.py +2 -1
  35. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/scripts/validate_design_themes.py +1 -1
  36. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/README.md +1 -1
  37. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-media-grid.md +1 -1
  38. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-modular-clarity.md +1 -1
  39. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-structured-clarity.md +1 -1
  40. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/ambient-halo-layered.md +1 -1
  41. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/dark-focus-layered.md +1 -1
  42. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/dark-luminous-modular.md +1 -1
  43. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/deep-stage-duotone.md +1 -1
  44. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/hairline-runway-clarity.md +1 -1
  45. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/high-contrast-modular.md +1 -1
  46. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/interlocked-vivid-modules.md +1 -1
  47. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/matrix-grid-focus.md +1 -1
  48. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/media-rail-inspector.md +1 -1
  49. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/mist-layered-signal.md +1 -1
  50. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/modular-rail-signal.md +1 -1
  51. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/mono-grid-signal.md +2 -2
  52. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/ribbon-ledger-lift.md +1 -1
  53. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/segmented-meter-clarity.md +1 -1
  54. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/split-rail-precision.md +1 -1
  55. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/status-framed-media-grid.md +1 -1
  56. package/yida-skills/skills/yida-design/workflow/output-design.md +0 -4
  57. package/yida-skills/skills/yida-design/workflow/step-4-wireframe-interaction.md +1 -1
  58. package/yida-skills/skills/yida-design/workflow/step-5-visual-states.md +1 -2
  59. package/yida-skills/skills/yida-nav-shell/references/nav-shell-patterns.md +1 -1
  60. package/yida-skills/skills/yida-prd/workflow/output-prd.md +2 -2
  61. package/yida-skills/skills/yida-process-rule/SKILL.md +1 -6
  62. package/yida-skills/skills/yida-requirement-analysis/SKILL.md +1 -1
  63. package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +13 -22
  64. package/yida-skills/skills-index.json +1 -18
  65. package/lib/asset/asset-plan.js +0 -105
  66. package/lib/asset/attachment-upload.js +0 -29
  67. package/lib/asset/host-capabilities.js +0 -91
  68. package/lib/asset/image-metadata.js +0 -104
  69. package/lib/process/services/process-actions.js +0 -145
  70. package/yida-skills/skills/yida-image-assets/SKILL.md +0 -91
  71. package/yida-skills/skills/yida-image-assets/references/manifest-contract.md +0 -87
  72. package/yida-skills/skills/yida-image-assets/references/source-policy.md +0 -19
  73. package/yida-skills/skills/yida-process-rule/references/approval-actions.md +0 -48
package/README.md CHANGED
@@ -210,8 +210,6 @@ openyida data create form APP_XXX FORM_XXX --expect-form-name 客户 --expect-fo
210
210
  openyida get-permission APP_XXX FORM_XXX
211
211
  ```
212
212
 
213
- Append and forward permissions are configured in the process definition through `nodes[].actions.normalActions/appendActions`. Both `create-process` and `configure-process` compile these settings into matching designer and runtime properties. See [approval action configuration](yida-skills/skills/yida-process-rule/references/approval-actions.md) for the JSON example, defaults, and verification limits.
214
-
215
213
  `configure-process` 的流程 JSON 中,审批人可配置为发起人、指定成员、指定角色、部门主管或直属主管,例如:
216
214
 
217
215
  ```json
@@ -456,8 +454,8 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
456
454
 
457
455
  | Command | Description |
458
456
  |---------|-------------|
459
- | `openyida configure-process <appType> <formUuid> <definition> [processCode] [--replace]` | Configure and publish process rules; Supports append/forward via JSON nodes[].actions.normalActions/appendActions |
460
- | `openyida create-process <appType> ... [--replace]` | Create process form (all-in-one); Supports append/forward via JSON nodes[].actions.normalActions/appendActions |
457
+ | `openyida configure-process <appType> <formUuid> <definition> [processCode] [--replace]` | Configure and publish process rules |
458
+ | `openyida create-process <appType> ... [--replace]` | Create process form (all-in-one) |
461
459
  | `openyida ai-form-setting <get\|fields\|models\|enable\|disable\|save> <appType> ...` | Manage process form AI approval prompts |
462
460
  | `openyida process preview <appType> ...` | Preview process instance (visual flowchart) |
463
461
 
@@ -531,7 +529,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
531
529
  | `openyida batch <file>\|--commands "cmd1 ; cmd2" [--stop-on-error] [--json]` | Run OpenYida commands in batch |
532
530
  | `openyida flash-to-prd --file <path> --name "<project>"` | Convert flash notes or meeting notes to a PRD prompt |
533
531
  | `openyida ai <text\|image> [options]` | Call Yida AI text and image recognition APIs |
534
- | `openyida asset <status\|resolve\|sources> [options]` | Detect asset capability / resolve materials |
532
+ | `openyida asset <status\|resolve\|generate> [options]` | Detect asset capability / resolve materials |
535
533
  | `openyida cdn-config [options]` | Configure CDN / OSS upload |
536
534
  | `openyida cdn-upload <image-path>` | Upload image to CDN |
537
535
  | `openyida cdn-refresh [options]` | Refresh CDN cache |
@@ -562,7 +560,7 @@ The runner rejects public generic echo services such as httpbin and example.com.
562
560
 
563
561
  The opt-in `node scripts/e2e-real/connector/action-update-runner.js` regression uses the owner-confirmed login-free `www.aliwork.com` fixture and a single owned NONE-auth connector containing a target action plus one preservation sentinel. Enable it with `OPENYIDA_E2E=1 OPENYIDA_E2E_CONNECTOR_ACTION_UPDATE=1`. It verifies isolated `currentPage`, `pageSize`, `userLanguage`, `searchFieldJson`, and dynamic `_stamp` edits, restores each field and the final baseline, and persists only response structure, data count, and SHA-256. It never stores response row values or auth/profile/corp identifiers, never retries unknown writes, and intentionally leaves the owned connector as a `cleanup_blocked` residual because no proven delete API exists.
564
562
 
565
- `openyida asset resolve --input manifest-draft.json --manifest asset-manifest.json --json` validates every image slot and writes a traceable manifest. Search assets only accept Unsplash/Pexels; draft results exit with code 2. Use `--offline` to block URL checks and uploads.
563
+ `openyida asset resolve --hero <path-or-url> --product <path-or-url> --require-hero --upload-assets --json` is the preferred preflight for homepage visuals. It handles local files and external image references in one flow, uploads or mirrors them when CDN is configured, and returns `materialStatus: final|draft|none` so agents do not claim an unfinished visual page is final.
566
564
 
567
565
  #### Environment and Localization
568
566
 
package/bin/yida.js CHANGED
@@ -496,7 +496,6 @@ const MANIFEST_HELP_PATHS = Object.freeze({
496
496
  data: ['data'],
497
497
  report: ['report'],
498
498
  'create-process': ['create-process'],
499
- 'configure-process': ['configure-process'],
500
499
  'create-report': ['create-report'],
501
500
  'append-chart': ['append-chart'],
502
501
  'save-share-config': ['save-share-config'],
package/lib/ai/ai.js CHANGED
@@ -31,7 +31,6 @@ const IMAGE_MIME_TYPES = {
31
31
  '.gif': 'image/gif',
32
32
  '.webp': 'image/webp',
33
33
  '.bmp': 'image/bmp',
34
- '.svg': 'image/svg+xml',
35
34
  };
36
35
 
37
36
  function printHelp() {
@@ -501,7 +500,6 @@ module.exports = {
501
500
  getAuthRef,
502
501
  callTextFromAI,
503
502
  uploadImageForAI,
504
- inferAppType,
505
503
  invokeImageRecognition,
506
504
  normalizeImageResult,
507
505
  parseArgs,
@@ -221,16 +221,6 @@ function normalizeLayoutDirection(layoutDirection) {
221
221
  return normalized;
222
222
  }
223
223
 
224
- /** 与 yc-utils 的应用配置归一化一致;不应用详情页 Shell 的 top_fold 覆盖。 */
225
- function normalizeStoredLayoutDirection(layoutDirection, navType) {
226
- if (SUPPORTED_LAYOUT_DIRECTIONS.includes(layoutDirection)) {return layoutDirection;}
227
- if (layoutDirection === 'hoz') {return navType === 'top_side' ? 'l_shape' : 'top';}
228
- if (layoutDirection === 'ver') {return 'side';}
229
- if (navType === 'top_fold') {return 'top';}
230
- if (navType === 'top_side') {return 'l_shape';}
231
- return 'side';
232
- }
233
-
234
224
  function pickAppField(currentApp, fieldName, configName) {
235
225
  if (currentApp[fieldName] !== undefined && currentApp[fieldName] !== null) {
236
226
  return currentApp[fieldName];
@@ -306,8 +296,6 @@ function buildUpdateAppPostData(params, currentApp = {}, authRef) {
306
296
  const currentCustomThemeStyle = pickAppField(currentApp, 'customThemeStyle', 'CUSTOM_THEME_STYLE');
307
297
  const addWaterMark = pickAppField(currentApp, 'addWaterMark', 'ADDWATERMARK');
308
298
  const sentryMode = pickAppField(currentApp, 'sentryMode', 'SENTRY_MODE');
309
- const currentNavType = currentApp.navType || (currentApp.config && currentApp.config.NAVTYPE);
310
- const currentLayoutDirection = currentApp.layoutDirection || (currentApp.config && currentApp.config.LAY_OUT_DIRECTION);
311
299
  if (params.colour === 'custom' && !params.themeColor && !currentThemeColor) {
312
300
  throw new Error(t('update_app.custom_theme_color_required'));
313
301
  }
@@ -331,10 +319,9 @@ function buildUpdateAppPostData(params, currentApp = {}, authRef) {
331
319
  mode: currentApp.mode || (currentApp.config && currentApp.config.APPMODE) || 'normal',
332
320
  type: currentApp.type || (currentApp.config && currentApp.config.APPTYPE) || 'single',
333
321
  navTheme: params.navTheme || currentApp.navTheme || (currentApp.config && currentApp.config.NAV_THEME) || 'light',
322
+ navType: currentApp.navType || (currentApp.config && currentApp.config.NAVTYPE) || 'top_side',
334
323
  navLayout: currentApp.navLayout || (currentApp.config && currentApp.config.NAVLAYOUT) || 'auto',
335
- layoutDirection: params.layoutDirection
336
- ? normalizeLayoutDirection(params.layoutDirection)
337
- : normalizeStoredLayoutDirection(currentLayoutDirection, currentNavType),
324
+ layoutDirection: params.layoutDirection || currentApp.layoutDirection || (currentApp.config && currentApp.config.LAY_OUT_DIRECTION) || 'side',
338
325
  homepageLogo: currentApp.homepageLogo || (currentApp.config && currentApp.config.HOMEPAGELOGO) || '',
339
326
  logoSource: params.logoSource || currentApp.logoSource || (currentApp.config && currentApp.config.LOGO_SOURCE) || 'appIcon',
340
327
  logoLink: currentApp.logoLink || (currentApp.config && currentApp.config.LOGOLINK) || '',
@@ -359,8 +346,6 @@ function buildUpdateAppPostData(params, currentApp = {}, authRef) {
359
346
  pageFooter: currentApp.pageFooter || '',
360
347
  };
361
348
 
362
- // ThemeNavSetting 保存现有 navType;新布局由 layoutDirection 控制,不反写或补造旧字段。
363
- if (currentNavType) {postDataObj.navType = currentNavType;}
364
349
  if (addWaterMark !== undefined && addWaterMark !== null) {
365
350
  postDataObj.addWaterMark = addWaterMark;
366
351
  }
@@ -1,60 +1,89 @@
1
1
 
2
+ /**
3
+ * ai-image.js - 图片素材来源能力检测(诚实降级 + 引导本地 agent)
4
+ *
5
+ * P3 素材工具化的关键澄清:openyida 本身没有内置「文生图」API,
6
+ * `openyida ai` 只支持文生文与识图,不能凭空生成图片 URL。
7
+ *
8
+ * 因此本模块不假装能生成图片,而是:
9
+ * 1. 诚实地检测「是否存在内置图片生成能力」(目前恒为 false);
10
+ * 2. 把「生成/寻找素材」这件事交回给正在驱动本 CLI 的智能体(Claude/Codex 等),
11
+ * 由 skill 文档引导它:能自己生成就生成,否则去免费素材库搜真实图片;
12
+ * 3. 提供推荐的免费/可商用素材库清单与使用纪律,供 skill / agent 参考。
13
+ *
14
+ * 无论走哪条路,候选素材都交给 asset resolve 统一解析;有 CDN 时再转存,
15
+ * 绝不把编造的 URL 直接写进页面。
16
+ *
17
+ * 用法:
18
+ * const { detectImageGenerator, getFreeStockLibraries, getMaterialSourcingGuidance } = require('./ai-image');
19
+ * const g = detectImageGenerator(); // { available:false, reason, delegateToAgent:true }
20
+ */
21
+
2
22
  'use strict';
3
23
 
24
+ /**
25
+ * 推荐的免费 / 可商用图片素材库
26
+ * 说明:这些站点提供免费可商用(多为 CC0 / 自有免费授权)的高质量图片,
27
+ * 适合官网 Hero、产品图、场景配图。agent 应优先从这里取真实图片 URL,
28
+ * 取到后交给 `openyida asset resolve` 统一处理。
29
+ */
4
30
  const FREE_STOCK_LIBRARIES = [
5
31
  {
6
32
  name: 'Unsplash',
7
33
  site: 'https://unsplash.com',
8
- license: '遵守 Unsplash License 和 API Guidelines',
9
- bestFor: 'Hero、生活方式和场景图',
10
- directImageHint: '使用 API 返回的 images.unsplash.com URL。',
11
- deliveryPolicy: '下载授权图片后上传宜搭附件;保留署名和 download_location 记录。',
34
+ license: 'Unsplash License(免费可商用,无需署名)',
35
+ bestFor: '高质量摄影级 Hero 大图、生活方式/场景图',
36
+ directImageHint:
37
+ 'images.unsplash.com/photo-... 形式的直链可直接用于 <img>;页面链接需先取得图片直链。',
12
38
  },
13
39
  {
14
40
  name: 'Pexels',
15
41
  site: 'https://www.pexels.com',
16
- license: '遵守 Pexels License 和 API Guidelines',
17
- bestFor: '产品、办公、人物和场景图',
42
+ license: 'Pexels License(免费可商用,无需署名)',
43
+ bestFor: '产品/办公/团队/自然等主题摄影图与短视频',
18
44
  directImageHint: 'images.pexels.com/photos/... 形式为图片直链。',
19
- deliveryPolicy: '下载后上传宜搭附件;保留 Pexels 链接和摄影师信息。',
45
+ },
46
+ {
47
+ name: 'Pixabay',
48
+ site: 'https://pixabay.com',
49
+ license: 'Pixabay Content License(免费可商用)',
50
+ bestFor: '插画、矢量、照片综合库',
51
+ directImageHint: 'cdn.pixabay.com/photo/... 形式为图片直链。',
52
+ },
53
+ {
54
+ name: 'Undraw',
55
+ site: 'https://undraw.co/illustrations',
56
+ license: 'unDraw open-source license(免费可商用,可改色)',
57
+ bestFor: '扁平风品牌插画、空状态/引导插画(SVG,可随品牌色变量着色)',
58
+ directImageHint: '导出 SVG 或使用官方 CDN SVG 直链。',
20
59
  },
21
60
  ];
22
61
 
23
62
  /**
24
63
  * 检测是否存在「内置图片生成能力」。
25
64
  *
26
- * openyida 无内置文生图连接器。宿主能力未确认时返回 available:null,
27
- * 由 Agent 检查真实工具清单。
65
+ * 目前 openyida 无内置文生图连接器,恒返回 available:false,并显式声明
66
+ * 由 agent 接管素材来源(避免任何模块据此伪造图片 URL)。
28
67
  *
29
68
  * @param {object} [options]
30
- * @returns {{ status: string, available: boolean|null, reason: string, delegateToAgent: boolean }}
69
+ * @param {boolean} [options.hasImageGenConnector] 预留:未来若配置了文生图连接器可置 true
70
+ * @returns {{ available: boolean, reason: string, delegateToAgent: boolean }}
31
71
  */
32
72
  function detectImageGenerator(options = {}) {
33
- const capability = options.hostCapabilities && options.hostCapabilities.image_generation;
34
- if (capability && capability.status === 'available') {
73
+ if (options.hasImageGenConnector) {
35
74
  return {
36
- status: 'available',
37
75
  available: true,
38
- reason: '当前宿主可生成图片;生成后交给 asset resolve。',
39
- delegateToAgent: true,
40
- source: capability.source || 'host_capability',
41
- };
42
- }
43
- if (capability && capability.status === 'unavailable') {
44
- return {
45
- status: 'unavailable',
46
- available: false,
47
- reason: '当前宿主不支持图片生成。',
76
+ reason: '检测到已配置的文生图连接器,可由连接器生成图片后再转存 CDN。',
48
77
  delegateToAgent: false,
49
- source: capability.source,
50
78
  };
51
79
  }
52
80
  return {
53
- status: 'unknown',
54
- available: null,
55
- reason: '当前宿主未确认图片生成能力。',
81
+ available: false,
82
+ reason:
83
+ 'openyida 无内置文生图能力(`openyida ai` 仅支持文生文/识图)。' +
84
+ '图片素材应由智能体自行生成,或到免费素材库(Unsplash/Pexels 等)检索真实图片,' +
85
+ '再交给 `openyida asset resolve` 解析、(有 CDN 时)转存 CDN 后回填 spec。',
56
86
  delegateToAgent: true,
57
- source: (capability && capability.source) || 'host_tool_inventory_required',
58
87
  };
59
88
  }
60
89
 
@@ -73,18 +102,17 @@ function getFreeStockLibraries() {
73
102
  function getMaterialSourcingGuidance() {
74
103
  return {
75
104
  steps: [
76
- '1. 先用用户提供的素材。',
77
- '2. 再从 Unsplash / Pexels 搜图。',
78
- '3. 仍有缺口时使用宿主生图。',
79
- '4. 最后使用中性占位并保持 draft。',
80
- '5. 所有候选图通过 asset resolve 写入 manifest。',
105
+ '1. 判断页面是否需要真实图片(官网 Hero / 产品图 / 场景图通常需要)。',
106
+ '2. 优先:若智能体自身具备图片生成能力,按品牌调性生成图片并保存到本地。',
107
+ '3. 备选:到免费可商用素材库(见 libraries)检索与主题匹配的真实图片,拿到图片直链。',
108
+ '4. 解析:把本地文件或外链交给 `openyida asset resolve` 统一处理并回填 spec.assets;已配置 CDN 时转存为稳定 URL。',
109
+ '5. 兜底:若既无生成能力又找不到可用素材,交付「标注素材缺口」的低保真草稿,绝不编造图片 URL 或声称已上传。',
81
110
  ],
82
111
  libraries: getFreeStockLibraries(),
83
112
  rules: [
84
- '不编造图片 URL;按 manifest 的页面状态继续,只使用当前页 materialStatus=final 的图片。',
85
- '默认下载并上传宜搭图片附件;超过 20 MiB 保留原链接;不额外探测 URL,下载失败跳过并记录缺口。',
86
- '联网搜图仅使用 Unsplash / Pexels。',
87
- '来源不明时使用中性占位或宿主生成图。',
113
+ '绝不编造图片 URL;素材经 `openyida asset resolve` 统一处理后再写进页面。',
114
+ '无 CDN 时不得声称「已上传」或「已是最终版」。',
115
+ '素材授权需为免费可商用;如不确定授权,改用文字排版/数据图示/插画(如 unDraw)替代。',
88
116
  ],
89
117
  };
90
118
  }
@@ -1,104 +1,82 @@
1
+
2
+ /**
3
+ * asset-cmd.js - `openyida asset` 素材能力命令
4
+ *
5
+ * 素材工具的统一入口,把「检测素材能力 / 解析回填 / 素材来源引导」
6
+ * 暴露为可脚本化命令,供本地智能体在生成官网/落地页前调用,
7
+ * 从而做到:无 CDN 不谎称已上传、无可用素材不交付「最终版」。
8
+ *
9
+ * 子命令:
10
+ * status 检测当前素材能力(CDN / 生成 / 推荐策略)
11
+ * resolve --hero <..> [--product <..>] [--upload-assets] 解析并回填一组素材,输出 materialStatus/缺口
12
+ * generate 输出素材来源引导(免费素材库清单 + 纪律)
13
+ *
14
+ * 所有子命令均支持 --json 便于智能体解析。
15
+ */
16
+
1
17
  'use strict';
2
18
 
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { readDesignAssetStrategy } = require('./asset-plan');
6
19
  const { getAssetStatus } = require('./asset-status');
7
- const { resolveAssets, ASSET_SOURCES } = require('./asset-resolve');
8
- const { detectImageGenerator, getMaterialSourcingGuidance } = require('./ai-image');
9
- const { throwCommandError, throwStatus, throwUsage } = require('../core/command-errors');
10
-
11
- const RESOLVE_USAGE = '用法: openyida asset resolve (--input <manifest-draft.json> | --slot <slotId>=<路径或URL> ...) [--manifest <asset-manifest.json>] [--design <design.md>] [--source search|user|generated] [--app-type <appType>] [--upload-assets] [--offline] [--json]';
20
+ const { resolveAssets } = require('./asset-resolve');
21
+ const {
22
+ detectImageGenerator,
23
+ getMaterialSourcingGuidance,
24
+ } = require('./ai-image');
25
+ const { throwCommandError, throwStatus } = require('../core/command-errors');
12
26
 
13
27
  function printHelp() {
14
28
  console.log(`
15
29
  用法:
16
30
  openyida asset status [--offline] [--json]
17
- openyida asset resolve (--input <manifest-draft.json> | --slot <slotId>=<路径或URL> ...) [--manifest <asset-manifest.json>] [--design <design.md>] [--source search|user|generated] [--app-type <appType>] [--upload-assets] [--offline] [--json]
18
- openyida asset sources [--json]
31
+ openyida asset resolve --hero <路径或URL> [--product <路径或URL> ...] [--require-hero] [--upload-assets] [--offline] [--json]
32
+ openyida asset generate [--json]
19
33
 
20
34
  子命令:
21
- status 检测素材能力
22
- resolve 校验图片、默认上传宜搭图片附件并生成清单
35
+ status 检测当前素材能力:是否配置 CDN、是否可生成、推荐素材策略
36
+ resolve 把本地/外链素材解析为稳定 URL 并回填,输出 materialStatus(final|draft|none) 与缺口
37
+ generate 输出素材来源引导(免费可商用素材库清单 + 使用纪律,供智能体检索真实图片)
23
38
 
24
- 图片不超过 20 MiB 时上传;超过限制的外链保留原地址。
25
- --app-type 指定目标应用,省略时读取项目 config.json。
26
- --upload-assets 仅兼容旧命令,默认已上传;--offline 不联网、不上传。
27
- sources 输出 Unsplash/Pexels 使用规则
39
+ 说明:
40
+ openyida 无内置文生图能力;图片应由智能体自行生成或到免费素材库检索,
41
+ 再通过 resolve 解析,并在配置 CDN 时转存后回填 spec。
28
42
  `);
29
43
  }
30
44
 
31
- function parseSlot(value) {
32
- const index = String(value || '').indexOf('=');
33
- if (index <= 0 || index === String(value).length - 1) {return null;}
34
- const slotId = String(value).slice(0, index).trim();
35
- const input = String(value).slice(index + 1).trim();
36
- if (!slotId || !input) {return null;}
37
- return {
38
- slotId,
39
- input,
40
- required: true,
41
- };
42
- }
43
-
44
45
  function parseArgs(args) {
45
46
  const parsed = {
46
47
  subCommand: args[0],
47
- slots: [],
48
- input: '',
49
- manifest: '',
50
- design: '',
48
+ hero: '',
49
+ products: [],
50
+ requireHero: false,
51
51
  offline: false,
52
52
  uploadAssets: false,
53
- appType: '',
54
- source: 'search',
55
53
  timeout: 0,
56
54
  json: false,
57
55
  help: false,
58
- errors: [],
59
- usedOptions: [],
60
56
  };
57
+
61
58
  for (let i = 1; i < args.length; i++) {
62
59
  const arg = args[i];
63
60
  if (arg === '--help' || arg === '-h') {
64
61
  parsed.help = true;
65
62
  } else if (arg === '--json') {
66
- parsed.usedOptions.push('--json');
67
63
  parsed.json = true;
68
64
  } else if (arg === '--offline') {
69
- parsed.usedOptions.push('--offline');
70
65
  parsed.offline = true;
71
- } else if (arg === '--app-type') {
72
- parsed.usedOptions.push('--app-type');
73
- parsed.appType = args[++i] || '';
74
- if (!parsed.appType || parsed.appType.startsWith('--')) {parsed.errors.push('ASSET_APP_TYPE_REQUIRED');}
75
- } else if (arg === '--upload-assets') {
76
- parsed.usedOptions.push('--upload-assets');
66
+ } else if (arg === '--require-hero') {
67
+ parsed.requireHero = true;
68
+ } else if (arg === '--upload-assets' || arg === '--mirror-external') {
77
69
  parsed.uploadAssets = true;
78
- } else if (arg === '--source') {
79
- parsed.usedOptions.push('--source');
80
- parsed.source = args[++i] || '';
81
- } else if (arg === '--input') {
82
- parsed.usedOptions.push('--input');
83
- parsed.input = args[++i] || '';
84
- } else if (arg === '--design') {
85
- parsed.usedOptions.push('--design');
86
- parsed.design = args[++i] || '';
87
- if (!parsed.design || parsed.design.startsWith('--')) {parsed.errors.push('ASSET_DESIGN_REQUIRED');}
88
- } else if (arg === '--manifest') {
89
- parsed.usedOptions.push('--manifest');
90
- parsed.manifest = args[++i] || '';
91
- } else if (arg === '--slot') {
92
- parsed.usedOptions.push('--slot');
93
- const slot = parseSlot(args[++i]);
94
- if (slot) {parsed.slots.push(slot);} else {parsed.errors.push('--slot 格式应为 <slotId>=<路径或URL>');}
70
+ } else if (arg === '--hero') {
71
+ parsed.hero = args[++i] || '';
72
+ } else if (arg === '--product') {
73
+ const v = args[++i];
74
+ if (v) {parsed.products.push(v);}
95
75
  } else if (arg === '--timeout') {
96
- parsed.usedOptions.push('--timeout');
97
76
  parsed.timeout = parseInt(args[++i], 10) || 0;
98
- } else {
99
- parsed.errors.push(`未知参数:${arg}`);
100
77
  }
101
78
  }
79
+
102
80
  return parsed;
103
81
  }
104
82
 
@@ -109,88 +87,74 @@ function runStatus(options) {
109
87
  return;
110
88
  }
111
89
  console.log('素材能力检测');
112
- console.log(' 上传目标 : 宜搭图片附件(需要登录态和 appType)');
113
- console.log(' 上传条件 : ' + (status.canUpload ? '联网已启用;实际上传时验证登录态和应用权限' : '离线,不上传'));
114
- console.log(' 可生成图片 : ' + (status.canGenerate === true ? '是' : status.canGenerate === false ? '否' : '未确认'));
115
- console.log(' 建议策略 : ' + status.recommendedStrategy);
116
- status.reasons.forEach(reason => console.log(' - ' + reason));
117
- }
118
-
119
- function readDraft(filePath) {
120
- let draft;
121
- try {
122
- draft = JSON.parse(fs.readFileSync(path.resolve(filePath), 'utf8'));
123
- } catch (error) {
124
- throwCommandError(`无法读取素材草稿:${error.message}`, { code: 'ASSET_INPUT_INVALID' });
90
+ console.log(' CDN 已配置 : ' + (status.cdnConfigured ? '是' : '否'));
91
+ console.log(' 可上传转存 : ' + (status.canUpload ? '是' : '否'));
92
+ console.log(' 可 AI 生成 : ' + (status.canGenerate ? '是' : '否(由智能体生成/检索素材)'));
93
+ console.log(' 推荐素材策略 : ' + status.recommendedStrategy);
94
+ console.log(' CDN 配置文件 : ' + status.cdn.configPath);
95
+ if (status.cdn.missing.length) {
96
+ console.log(' 缺失 CDN 字段 : ' + status.cdn.missing.join(', '));
125
97
  }
126
- if (!draft || !Array.isArray(draft.assets)) {
127
- throwCommandError('素材草稿必须包含 assets 数组', { code: 'ASSET_INPUT_INVALID' });
128
- }
129
- return draft;
130
- }
131
-
132
- function writeManifest(filePath, manifest) {
133
- const target = path.resolve(filePath);
134
- fs.mkdirSync(path.dirname(target), { recursive: true });
135
- fs.writeFileSync(target, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
136
- return target;
98
+ console.log('原因/建议:');
99
+ status.reasons.forEach((r) => console.log(' - ' + r));
137
100
  }
138
101
 
139
102
  async function runResolve(options) {
140
- if (options.errors.length > 0) {
141
- throwUsage(options.errors.join('\n'), RESOLVE_USAGE, { code: 'ASSET_INVALID_ARGUMENTS' });
142
- }
143
- if (options.input && options.slots.length > 0) {
144
- throwUsage('--input 与 --slot 不能同时使用', RESOLVE_USAGE, { code: 'ASSET_INPUT_CONFLICT' });
145
- }
146
- if (!ASSET_SOURCES.includes(options.source)) {
147
- throwUsage('--source 仅支持 search、user 或 generated', RESOLVE_USAGE, { code: 'ASSET_INVALID_SOURCE' });
148
- }
149
- const draft = options.input ? readDraft(options.input) : {};
150
- const assetStrategy = options.design ? readDesignAssetStrategy(path.resolve(options.design)) : draft.assetStrategy;
151
- const assets = [
152
- ...(draft.assets || []),
153
- ...options.slots.map(slot => ({ ...slot, source: options.source })),
154
- ];
155
- if (assets.length === 0 && !options.input) {
156
- throwUsage('至少提供一个 --slot 或 --input', RESOLVE_USAGE, { code: 'ASSET_INPUT_REQUIRED' });
157
- }
158
-
103
+ const assets = {
104
+ heroImage: options.hero,
105
+ productImages: options.products,
106
+ };
159
107
  const result = await resolveAssets(assets, {
160
- assetStrategy,
108
+ requireHero: options.requireHero,
161
109
  online: !options.offline,
162
- appType: options.appType,
163
- assetSource: options.source,
110
+ mirrorExternal: !!options.uploadAssets,
164
111
  timeout: options.timeout || undefined,
165
112
  });
166
- const manifestPath = options.manifest ? writeManifest(options.manifest, result) : '';
167
- if (result.materialStatus === 'draft') {
113
+ if (options.json) {
114
+ console.log(JSON.stringify(result, null, 2));
115
+ return;
116
+ }
117
+ console.log('素材解析结果');
118
+ console.log(' materialStatus : ' + result.materialStatus +
119
+ (result.materialStatus === 'final' ? '(可交付最终版)'
120
+ : result.materialStatus === 'draft' ? '(仅可交付标注缺口的草稿)'
121
+ : '(无可用素材,只能低保真草稿)'));
122
+ console.log(' strategy : ' + result.strategy);
123
+ console.log(' Hero : ' + (result.assets.heroImage || '(无)'));
124
+ console.log(' 产品图 : ' + (result.assets.productImages.length
125
+ ? result.assets.productImages.join(', ') : '(无)'));
126
+ if (result.materialGaps.length) {
127
+ console.log('素材缺口:');
128
+ result.materialGaps.forEach((g) => console.log(' - ' + g));
129
+ }
130
+ // 非 final 时以非零退出码提示调用方(生成链路可据此拦截「最终版」)
131
+ if (result.materialStatus !== 'final') {
168
132
  throwStatus('素材未达到 final 状态', 2, {
169
133
  code: 'ASSET_MATERIAL_NOT_FINAL',
170
- details: { ...result, manifestPath },
134
+ details: result,
171
135
  });
172
136
  }
173
- if (options.json) {
174
- console.log(JSON.stringify({ ...result, manifestPath }, null, 2));
175
- return;
176
- }
177
- console.log('素材状态: ' + result.materialStatus);
178
- console.log('素材数量: ' + result.assets.length);
179
- if (manifestPath) {console.log('素材清单: ' + manifestPath);}
180
137
  }
181
138
 
182
- function runSources(options) {
183
- const status = getAssetStatus();
184
- const output = {
185
- generator: detectImageGenerator({ hostCapabilities: status.hostCapabilities }),
186
- guidance: getMaterialSourcingGuidance(),
187
- };
139
+ function runGenerate(options) {
140
+ const generator = detectImageGenerator();
141
+ const guidance = getMaterialSourcingGuidance();
188
142
  if (options.json) {
189
- console.log(JSON.stringify(output, null, 2));
143
+ console.log(JSON.stringify({ generator, guidance }, null, 2));
190
144
  return;
191
145
  }
192
- console.log('图片来源: 用户素材、Unsplash、Pexels 或宿主生成');
193
- output.guidance.rules.forEach(rule => console.log(' - ' + rule));
146
+ console.log('图片生成能力: ' + (generator.available ? '可用' : '不可用'));
147
+ console.log(' ' + generator.reason);
148
+ console.log('\n素材来源步骤:');
149
+ guidance.steps.forEach((s) => console.log(' ' + s));
150
+ console.log('\n免费可商用素材库:');
151
+ guidance.libraries.forEach((lib) => {
152
+ console.log(' · ' + lib.name + ' (' + lib.site + ')');
153
+ console.log(' 授权: ' + lib.license);
154
+ console.log(' 适合: ' + lib.bestFor);
155
+ });
156
+ console.log('\n纪律:');
157
+ guidance.rules.forEach((r) => console.log(' - ' + r));
194
158
  }
195
159
 
196
160
  async function run(args) {
@@ -199,27 +163,22 @@ async function run(args) {
199
163
  printHelp();
200
164
  return;
201
165
  }
202
- const allowedOptions = {
203
- status: new Set(['--offline', '--json']),
204
- sources: new Set(['--json']),
205
- };
206
- if (allowedOptions[options.subCommand]) {
207
- const invalid = options.usedOptions.filter(option => !allowedOptions[options.subCommand].has(option));
208
- const errors = [...options.errors, ...invalid.map(option => `参数不适用于 ${options.subCommand}:${option}`)];
209
- if (errors.length > 0) {
210
- throwCommandError(errors.join('\n'), { code: 'ASSET_INVALID_ARGUMENTS' });
211
- }
166
+ switch (options.subCommand) {
167
+ case 'status':
168
+ runStatus(options);
169
+ break;
170
+ case 'resolve':
171
+ await runResolve(options);
172
+ break;
173
+ case 'generate':
174
+ runGenerate(options);
175
+ break;
176
+ default:
177
+ printHelp();
178
+ throwCommandError('未知子命令: ' + options.subCommand, {
179
+ code: 'ASSET_UNKNOWN_SUBCOMMAND',
180
+ });
212
181
  }
213
- if (options.subCommand === 'status') {runStatus(options); return;}
214
- if (options.subCommand === 'resolve') {await runResolve(options); return;}
215
- if (options.subCommand === 'sources') {runSources(options); return;}
216
- throwCommandError(`未知子命令:${options.subCommand}`, { code: 'ASSET_UNKNOWN_SUBCOMMAND' });
217
182
  }
218
183
 
219
- module.exports = {
220
- run,
221
- parseArgs,
222
- parseSlot,
223
- readDraft,
224
- writeManifest,
225
- };
184
+ module.exports = { run, parseArgs };