cardbuddy 0.11.0 → 0.12.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 (313) hide show
  1. package/README.md +46 -5
  2. package/dist/cli.js +51 -1
  3. package/dist/config.js +51 -16
  4. package/dist/core.js +2 -2
  5. package/dist/init.js +425 -0
  6. package/dist/preview-editor/assets/index-CHuGGIR3.js +276 -0
  7. package/dist/preview-editor/assets/{index-B4VZgNSK.css → index-CQScMhyg.css} +1 -1
  8. package/dist/preview-editor/index.html +2 -2
  9. package/dist/standalone.js +8 -0
  10. package/dist/vendored/mcp/client.mjs +537 -0
  11. package/dist/vendored/mcp/tools.mjs +268 -0
  12. package/dist/vendored/preview/assets/default-image.png +0 -0
  13. package/dist/{server-core → vendored/server/core}/ai-chat.js +82 -38
  14. package/dist/{server-core → vendored/server/core}/browser.js +1 -1
  15. package/dist/{server-core → vendored/server/core}/cardSave.js +8 -3
  16. package/dist/{server-core → vendored/server/core}/cardSync.js +6 -14
  17. package/dist/{server-core → vendored/server/core}/cardValidate.js +11 -8
  18. package/dist/{server-core → vendored/server/core}/index.js +5 -3
  19. package/dist/vendored/server/core/skillPaths.js +124 -0
  20. package/dist/{server-core → vendored/server/core}/sliceUpload.js +5 -8
  21. package/dist/vendored/skills/README.md +93 -0
  22. package/dist/vendored/skills/card-doctor/SKILL.md +52 -0
  23. package/dist/vendored/skills/card-skill-trainer/SKILL.md +312 -0
  24. package/dist/vendored/skills/card-skill-trainer/baselines/v3-r038n.json +43 -0
  25. package/dist/vendored/skills/card-skill-trainer/baselines/v3-r039n.json +47 -0
  26. package/dist/vendored/skills/card-skill-trainer/baselines/v3-r050n.json +48 -0
  27. package/dist/vendored/skills/card-skill-trainer/scripts/build_dataset.py +472 -0
  28. package/dist/vendored/skills/card-skill-trainer/scripts/build_sample.mjs +162 -0
  29. package/dist/vendored/skills/card-skill-trainer/scripts/diagnose_case.mjs +241 -0
  30. package/dist/vendored/skills/card-skill-trainer/scripts/lib/scoring.mjs +525 -0
  31. package/dist/vendored/skills/card-skill-trainer/scripts/round_diff.mjs +133 -0
  32. package/dist/vendored/skills/card-skill-trainer/scripts/run_round.mjs +1043 -0
  33. package/dist/vendored/skills/card-skill-trainer/scripts/score_round.mjs +195 -0
  34. package/dist/vendored/skills/card-skill-trainer/scripts/test_missing_slice.py +122 -0
  35. package/dist/vendored/skills/dsl-card-ai/SKILL.md +191 -0
  36. package/dist/vendored/skills/dsl-card-generator/SKILL.md +480 -0
  37. package/dist/vendored/skills/dsl-card-generator/config/render-contract.json +11 -0
  38. package/dist/vendored/skills/dsl-card-generator/references/amount_formatting.md +138 -0
  39. package/dist/vendored/skills/dsl-card-generator/references/card_index.json +1597 -0
  40. package/dist/vendored/skills/dsl-card-generator/references/card_templates.json +384 -0
  41. package/dist/vendored/skills/dsl-card-generator/references/card_to_summary.md +56 -0
  42. package/dist/vendored/skills/dsl-card-generator/references/component_rules.md +76 -0
  43. package/dist/vendored/skills/dsl-card-generator/references/component_specs.md +393 -0
  44. package/dist/vendored/skills/dsl-card-generator/references/dsl_structure.md +208 -0
  45. package/dist/vendored/skills/dsl-card-generator/references/event_patterns.md +287 -0
  46. package/dist/vendored/skills/dsl-card-generator/references/expression_engine.md +121 -0
  47. package/dist/vendored/skills/dsl-card-generator/references/field_name_dict.json +34 -0
  48. package/dist/vendored/skills/dsl-card-generator/references/field_naming.md +101 -0
  49. package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +928 -0
  50. package/dist/vendored/skills/dsl-card-generator/references/handcrafted/README.md +69 -0
  51. package/dist/vendored/skills/dsl-card-generator/references/handcrafted/examples_index.json +2967 -0
  52. package/dist/vendored/skills/dsl-card-generator/references/handcrafted/patterns.json +2835 -0
  53. package/dist/vendored/skills/dsl-card-generator/references/handcrafted/recipes_v2.json +648 -0
  54. package/dist/vendored/skills/dsl-card-generator/references/handcrafted/recipes_v2_schema.md +295 -0
  55. package/dist/vendored/skills/dsl-card-generator/references/image_diff.md +48 -0
  56. package/dist/vendored/skills/dsl-card-generator/references/image_to_card.md +129 -0
  57. package/dist/vendored/skills/dsl-card-generator/references/ir-format.md +212 -0
  58. package/dist/vendored/skills/dsl-card-generator/references/message-center-pattern.md +149 -0
  59. package/dist/vendored/skills/dsl-card-generator/references/platform_jsapi.md +196 -0
  60. package/dist/vendored/skills/dsl-card-generator/references/previewer_contract.md +54 -0
  61. package/dist/vendored/skills/dsl-card-generator/references/quickref.md +197 -0
  62. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/absolute-badge.json +84 -0
  63. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/accordion-group-toggle.json +18 -0
  64. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/alertCard-pair.json +12 -0
  65. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/alertDialog-call.json +12 -0
  66. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-countdown.json +15 -0
  67. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-donut.json +15 -0
  68. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-gauge-score.json +11 -0
  69. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-radar.json +10 -0
  70. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/canvas-title-head.json +13 -0
  71. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/globalSet.json +12 -0
  72. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/label-col-modes.json +21 -0
  73. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/list-expand-collapse.json +17 -0
  74. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/money-format.json +11 -0
  75. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/opacity-switch.json +130 -0
  76. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/progress-bar.json +25 -0
  77. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/tab-billMode.json +15 -0
  78. package/dist/vendored/skills/dsl-card-generator/references/recipe_snippets/title-underline-overlay.json +21 -0
  79. package/dist/vendored/skills/dsl-card-generator/references/recipes.md +797 -0
  80. package/dist/vendored/skills/dsl-card-generator/references/rules.md +235 -0
  81. package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +177 -0
  82. package/dist/vendored/skills/dsl-card-generator/references/spec_to_schema_patterns.md +609 -0
  83. package/dist/vendored/skills/dsl-card-generator/references/ui_fidelity_rules.md +131 -0
  84. package/dist/vendored/skills/dsl-card-generator/scripts/add_component.mjs +118 -0
  85. package/dist/vendored/skills/dsl-card-generator/scripts/analyze_component_usage.mjs +153 -0
  86. package/dist/vendored/skills/dsl-card-generator/scripts/apply_opacity_pattern.mjs +343 -0
  87. package/dist/vendored/skills/dsl-card-generator/scripts/build_card_index.mjs +108 -0
  88. package/dist/vendored/skills/dsl-card-generator/scripts/build_card_templates.mjs +190 -0
  89. package/dist/vendored/skills/dsl-card-generator/scripts/check_fidelity.mjs +257 -0
  90. package/dist/vendored/skills/dsl-card-generator/scripts/check_magic_values.mjs +79 -0
  91. package/dist/vendored/skills/dsl-card-generator/scripts/crop_slices.mjs +98 -0
  92. package/dist/vendored/skills/dsl-card-generator/scripts/describe_card.mjs +265 -0
  93. package/dist/vendored/skills/dsl-card-generator/scripts/diff_bands.mjs +257 -0
  94. package/dist/vendored/skills/dsl-card-generator/scripts/diff_card.mjs +316 -0
  95. package/dist/vendored/skills/dsl-card-generator/scripts/expand_ir.mjs +873 -0
  96. package/dist/vendored/skills/dsl-card-generator/scripts/extract_handcrafted.py +1199 -0
  97. package/dist/vendored/skills/dsl-card-generator/scripts/extract_subtree.mjs +191 -0
  98. package/dist/vendored/skills/dsl-card-generator/scripts/generate_cards.mjs +389 -0
  99. package/dist/vendored/skills/dsl-card-generator/scripts/inventory_schemas.py +160 -0
  100. package/dist/vendored/skills/dsl-card-generator/scripts/iterate.mjs +700 -0
  101. package/dist/vendored/skills/dsl-card-generator/scripts/lib/design_image.mjs +56 -0
  102. package/dist/vendored/skills/dsl-card-generator/scripts/lib/design_sample.mjs +328 -0
  103. package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout-tokens.mjs +79 -0
  104. package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.mjs +103 -0
  105. package/dist/vendored/skills/dsl-card-generator/scripts/lib/scaffold.mjs +575 -0
  106. package/dist/vendored/skills/dsl-card-generator/scripts/lib/slicePath.mjs +81 -0
  107. package/dist/vendored/skills/dsl-card-generator/scripts/lib/slice_dedupe.mjs +147 -0
  108. package/dist/vendored/skills/dsl-card-generator/scripts/lint_autofix.mjs +363 -0
  109. package/dist/vendored/skills/dsl-card-generator/scripts/lint_card.mjs +1399 -0
  110. package/dist/vendored/skills/dsl-card-generator/scripts/preflight.mjs +249 -0
  111. package/dist/vendored/skills/dsl-card-generator/scripts/register_card_template.mjs +208 -0
  112. package/dist/vendored/skills/dsl-card-generator/scripts/render_baseline.mjs +231 -0
  113. package/dist/vendored/skills/dsl-card-generator/scripts/render_card.mjs +801 -0
  114. package/dist/vendored/skills/dsl-card-generator/scripts/render_diff.mjs +239 -0
  115. package/dist/vendored/skills/dsl-card-generator/scripts/resolve_card_template.mjs +73 -0
  116. package/dist/vendored/skills/dsl-card-generator/scripts/route_input.mjs +447 -0
  117. package/dist/vendored/skills/dsl-card-generator/scripts/scaffold_card.mjs +81 -0
  118. package/dist/vendored/skills/dsl-card-generator/scripts/slices_normalize.mjs +316 -0
  119. package/dist/vendored/skills/dsl-card-generator/scripts/spec_audit.mjs +462 -0
  120. package/dist/vendored/skills/dsl-card-generator/scripts/spec_to_schema_v3.mjs +7735 -0
  121. package/dist/vendored/skills/dsl-card-generator/scripts/spec_tree.mjs +355 -0
  122. package/dist/vendored/skills/dsl-card-generator/scripts/validate_all_schemas.py +71 -0
  123. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/default-image-wzZYQ86K.png +0 -0
  124. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-C9k-07y7.css +1 -0
  125. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/standalone-oFRM4f4V.js +55 -0
  126. package/dist/vendored/skills/dsl-card-generator/vendor/preview/assets/svg-symbols-B2exBrAr.svg +118 -0
  127. package/dist/vendored/skills/dsl-card-generator/vendor/preview/meta/component-meta.json +2714 -0
  128. package/dist/vendored/skills/dsl-card-generator/vendor/preview/meta/directive-meta.json +40 -0
  129. package/dist/vendored/skills/dsl-card-generator/vendor/preview/meta/style-meta.json +1086 -0
  130. package/dist/vendored/{standalone.html → skills/dsl-card-generator/vendor/preview/standalone.html} +2 -1
  131. package/dist/vendored/skills/finmall-card-sync/SKILL.md +262 -0
  132. package/dist/vendored/skills/finmall-session-navigator/SKILL.md +169 -0
  133. package/dist/vendored/skills/finmall-session-navigator/scripts/finmall_login.mjs +882 -0
  134. package/dist/vendored/skills/finmall-session-navigator/scripts/package-lock.json +59 -0
  135. package/dist/vendored/skills/finmall-session-navigator/scripts/package.json +10 -0
  136. package/dist/vendored/skills/finmall-session-navigator/scripts/session-manager.mjs +96 -0
  137. package/dist/vendored/skills/shared/README.md +55 -0
  138. package/dist/vendored/skills/shared/auth-model.md +79 -0
  139. package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +329 -0
  140. package/dist/vendored/skills/sketch-spec-to-card/references/boundary.json +14 -0
  141. package/dist/vendored/skills/sketch-spec-to-card/references/card_refs.json +13 -0
  142. package/dist/vendored/skills/sketch-spec-to-card/references/spec-schema.md +103 -0
  143. package/dist/vendored/skills/sketch-spec-to-card/scripts/build_prompt.py +797 -0
  144. package/dist/vendored/skills/sketch-spec-to-card/scripts/card_refs.py +94 -0
  145. package/dist/vendored/skills/sketch-spec-to-card/scripts/cluster_states.py +706 -0
  146. package/dist/vendored/skills/sketch-spec-to-card/scripts/parse_meaxure.py +1781 -0
  147. package/dist/vendored/skills/sketch-spec-to-card/scripts/png_utils.py +318 -0
  148. package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +1118 -0
  149. package/dist/vendored/templates/helloCard/data.json +10 -0
  150. package/dist/vendored/templates/helloCard/schema.json +238 -0
  151. package/dist/vendored/tools/link-skills.mjs +485 -0
  152. package/package.json +2 -1
  153. package/dist/preview-editor/assets/index-jl4YJzKv.js +0 -180
  154. package/dist/vendored/App.vue +0 -2858
  155. package/dist/vendored/ansi.js +0 -185
  156. package/dist/vendored/assets/apple-touch-icon.png +0 -0
  157. package/dist/vendored/assets/default-image.png +0 -0
  158. package/dist/vendored/assets/favicon-16.png +0 -0
  159. package/dist/vendored/assets/favicon-32.png +0 -0
  160. package/dist/vendored/assets/site-icon.png +0 -0
  161. package/dist/vendored/assets/svgIcons/icons.svg +0 -87
  162. package/dist/vendored/assets/svgIcons/index.js +0 -39
  163. package/dist/vendored/components/AiSettings.vue +0 -392
  164. package/dist/vendored/components/AlertCardInstance.vue +0 -58
  165. package/dist/vendored/components/AlertCardLayer.vue +0 -59
  166. package/dist/vendored/components/ApiCallPanel.vue +0 -127
  167. package/dist/vendored/components/ApiPayloadTree.vue +0 -201
  168. package/dist/vendored/components/AppToast.vue +0 -37
  169. package/dist/vendored/components/CardGallery.vue +0 -291
  170. package/dist/vendored/components/CardPreview.vue +0 -72
  171. package/dist/vendored/components/CardSelect.vue +0 -206
  172. package/dist/vendored/components/ChatComposer.vue +0 -413
  173. package/dist/vendored/components/ChatMessage.vue +0 -383
  174. package/dist/vendored/components/ChatPanel.vue +0 -937
  175. package/dist/vendored/components/ComponentPalette.vue +0 -301
  176. package/dist/vendored/components/ConfirmDialog.vue +0 -54
  177. package/dist/vendored/components/DatePickerOverlay.vue +0 -345
  178. package/dist/vendored/components/DebugPanel.vue +0 -1734
  179. package/dist/vendored/components/DeviceOverlay.vue +0 -70
  180. package/dist/vendored/components/Dropdown.vue +0 -193
  181. package/dist/vendored/components/EventEditor.vue +0 -273
  182. package/dist/vendored/components/HoverGuides.vue +0 -122
  183. package/dist/vendored/components/JsonTree.vue +0 -547
  184. package/dist/vendored/components/LogValue.vue +0 -52
  185. package/dist/vendored/components/MarkdownBody.vue +0 -64
  186. package/dist/vendored/components/NewCardDialog.vue +0 -132
  187. package/dist/vendored/components/NodeInspector.vue +0 -295
  188. package/dist/vendored/components/NumberStepper.vue +0 -125
  189. package/dist/vendored/components/PickerOverlay.vue +0 -131
  190. package/dist/vendored/components/PropControl.vue +0 -245
  191. package/dist/vendored/components/PropTree.vue +0 -128
  192. package/dist/vendored/components/RemotePreview.vue +0 -173
  193. package/dist/vendored/components/SelectionOverlay.vue +0 -105
  194. package/dist/vendored/components/StructureTree.vue +0 -1002
  195. package/dist/vendored/components/Switch.vue +0 -38
  196. package/dist/vendored/components/setters/SetBgPosition.vue +0 -64
  197. package/dist/vendored/components/setters/SetBgSize.vue +0 -90
  198. package/dist/vendored/components/setters/SetCode.vue +0 -198
  199. package/dist/vendored/components/setters/SetColor.vue +0 -244
  200. package/dist/vendored/components/setters/SetCustomSelect.vue +0 -58
  201. package/dist/vendored/components/setters/SetExpr.vue +0 -107
  202. package/dist/vendored/components/setters/SetGradient.vue +0 -125
  203. package/dist/vendored/components/setters/SetGrid9.vue +0 -68
  204. package/dist/vendored/components/setters/SetIconSelect.vue +0 -67
  205. package/dist/vendored/components/setters/SetIfForm.vue +0 -61
  206. package/dist/vendored/components/setters/SetImage.vue +0 -43
  207. package/dist/vendored/components/setters/SetList.vue +0 -139
  208. package/dist/vendored/components/setters/SetNumber.vue +0 -118
  209. package/dist/vendored/components/setters/SetNumberStyle.vue +0 -113
  210. package/dist/vendored/components/setters/SetPadding.vue +0 -77
  211. package/dist/vendored/components/setters/SetSegment.vue +0 -160
  212. package/dist/vendored/components/setters/SetSelect.vue +0 -51
  213. package/dist/vendored/components/setters/SetSwitch.vue +0 -49
  214. package/dist/vendored/components/setters/SetText.vue +0 -52
  215. package/dist/vendored/components/setters/props.js +0 -23
  216. package/dist/vendored/fidelity/baseline-messageOverview.json +0 -87
  217. package/dist/vendored/fidelity/calibration.json +0 -29
  218. package/dist/vendored/fidelity/fidelity-messageOverview-after.html +0 -114
  219. package/dist/vendored/fidelity/fidelity-messageOverview-after.json +0 -272
  220. package/dist/vendored/fidelity/fidelity-messageOverview.html +0 -185
  221. package/dist/vendored/fidelity/fidelity-messageOverview.json +0 -336
  222. package/dist/vendored/fidelity/fidelity-messageOverview_v2.html +0 -262
  223. package/dist/vendored/fidelity/fidelity-messageOverview_v2.json +0 -502
  224. package/dist/vendored/fidelity/fidelity-messageOverview_v3.html +0 -201
  225. package/dist/vendored/fidelity/fidelity-messageOverview_v3.json +0 -417
  226. package/dist/vendored/fidelity/fidelity-messageOverview_visual.html +0 -125
  227. package/dist/vendored/fidelity/fidelity-messageOverview_visual.json +0 -338
  228. package/dist/vendored/index.html +0 -16
  229. package/dist/vendored/main.js +0 -37
  230. package/dist/vendored/runtime/ai-api.js +0 -165
  231. package/dist/vendored/runtime/alert-card.js +0 -125
  232. package/dist/vendored/runtime/app-toast.js +0 -33
  233. package/dist/vendored/runtime/clipboard-node.js +0 -78
  234. package/dist/vendored/runtime/clipboard.js +0 -29
  235. package/dist/vendored/runtime/event-model.js +0 -300
  236. package/dist/vendored/runtime/hoverGuide.js +0 -31
  237. package/dist/vendored/runtime/jsapi-mock.js +0 -256
  238. package/dist/vendored/runtime/markdown.js +0 -140
  239. package/dist/vendored/runtime/node-model.js +0 -217
  240. package/dist/vendored/skills/.manifest.json +0 -1101
  241. package/dist/vendored/standalone-main.js +0 -36
  242. package/dist/vendored/styles/ai-chat.css +0 -1850
  243. package/dist/vendored/styles/app.css +0 -3648
  244. package/dist/vendored/styles/card-gallery.css +0 -285
  245. package/dist/vendored/styles/card-select.css +0 -288
  246. package/dist/vendored/styles/components.css +0 -383
  247. package/dist/vendored/styles/dropdown.css +0 -324
  248. package/dist/vendored/styles/json-tree.css +0 -308
  249. package/dist/vendored/styles/node-inspector.css +0 -800
  250. package/dist/vendored/styles/setters.css +0 -1050
  251. package/dist/vendored/styles/standalone.css +0 -27
  252. package/dist/vendored/styles/structure-tree.css +0 -460
  253. /package/dist/vendored/{api.js → preview/api.js} +0 -0
  254. /package/dist/vendored/{assets → preview/assets}/svg-symbols.svg +0 -0
  255. /package/dist/vendored/{components → preview/components}/AbsoluteLayout.vue +0 -0
  256. /package/dist/vendored/{components → preview/components}/Button.vue +0 -0
  257. /package/dist/vendored/{components → preview/components}/Canvas.vue +0 -0
  258. /package/dist/vendored/{components → preview/components}/CheckGroup.vue +0 -0
  259. /package/dist/vendored/{components → preview/components}/Column.vue +0 -0
  260. /package/dist/vendored/{components → preview/components}/CustomPlaceholder.vue +0 -0
  261. /package/dist/vendored/{components → preview/components}/Divider.vue +0 -0
  262. /package/dist/vendored/{components → preview/components}/GridLayout.vue +0 -0
  263. /package/dist/vendored/{components → preview/components}/Icon.vue +0 -0
  264. /package/dist/vendored/{components → preview/components}/Image.vue +0 -0
  265. /package/dist/vendored/{components → preview/components}/Input.vue +0 -0
  266. /package/dist/vendored/{components → preview/components}/ListLayout.vue +0 -0
  267. /package/dist/vendored/{components → preview/components}/Map.vue +0 -0
  268. /package/dist/vendored/{components → preview/components}/MultiColumn.vue +0 -0
  269. /package/dist/vendored/{components → preview/components}/MultiText.vue +0 -0
  270. /package/dist/vendored/{components → preview/components}/ProgressSlider.vue +0 -0
  271. /package/dist/vendored/{components → preview/components}/RadioButton.vue +0 -0
  272. /package/dist/vendored/{components → preview/components}/RenderTree.vue +0 -0
  273. /package/dist/vendored/{components → preview/components}/Root.vue +0 -0
  274. /package/dist/vendored/{components → preview/components}/Row.vue +0 -0
  275. /package/dist/vendored/{components → preview/components}/SvgCode.vue +0 -0
  276. /package/dist/vendored/{components → preview/components}/SvgSymbols.vue +0 -0
  277. /package/dist/vendored/{components → preview/components}/Text.vue +0 -0
  278. /package/dist/vendored/{components → preview/components}/Video.vue +0 -0
  279. /package/dist/vendored/{components → preview/components}/ViewContainer.vue +0 -0
  280. /package/dist/vendored/{components → preview/components}/base.js +0 -0
  281. /package/dist/vendored/{components → preview/components}/icons.js +0 -0
  282. /package/dist/vendored/{components → preview/components}/index.js +0 -0
  283. /package/dist/vendored/{meta → preview/meta}/component-meta.json +0 -0
  284. /package/dist/vendored/{meta → preview/meta}/directive-meta.json +0 -0
  285. /package/dist/vendored/{meta → preview/meta}/style-meta.json +0 -0
  286. /package/dist/vendored/{runtime → preview/runtime}/StyleParser.js +0 -0
  287. /package/dist/vendored/{runtime → preview/runtime}/adapter.js +0 -0
  288. /package/dist/vendored/{runtime → preview/runtime}/assets.js +0 -0
  289. /package/dist/vendored/{runtime → preview/runtime}/canvas-context.js +0 -0
  290. /package/dist/vendored/{runtime → preview/runtime}/card-runtime.js +0 -0
  291. /package/dist/vendored/{runtime → preview/runtime}/evaluate.js +0 -0
  292. /package/dist/vendored/{runtime → preview/runtime}/property.js +0 -0
  293. /package/dist/vendored/{runtime → preview/runtime}/selectionOverlay.js +0 -0
  294. /package/dist/vendored/{runtime → preview/runtime}/toPreview.js +0 -0
  295. /package/dist/{server-core → vendored/server/core}/assetProxy.js +0 -0
  296. /package/dist/{server-core → vendored/server/core}/bbox.js +0 -0
  297. /package/dist/{server-core → vendored/server/core}/buildDataFromSchema.js +0 -0
  298. /package/dist/{server-core → vendored/server/core}/cardFiles.js +0 -0
  299. /package/dist/{server-core → vendored/server/core}/cardHistory.js +0 -0
  300. /package/dist/{server-core → vendored/server/core}/cardImage.js +0 -0
  301. /package/dist/{server-core → vendored/server/core}/cardImportMeta.js +0 -0
  302. /package/dist/{server-core → vendored/server/core}/cardListCache.js +0 -0
  303. /package/dist/{server-core → vendored/server/core}/cardOpenFolder.js +0 -0
  304. /package/dist/{server-core → vendored/server/core}/cardSidecar.js +0 -0
  305. /package/dist/{server-core → vendored/server/core}/codeBridge.js +0 -0
  306. /package/dist/{server-core → vendored/server/core}/logStream.js +0 -0
  307. /package/dist/{server-core → vendored/server/core}/logger.js +0 -0
  308. /package/dist/{server-core → vendored/server/core}/nodeModels.js +0 -0
  309. /package/dist/{server-core → vendored/server/core}/renderStatus.js +0 -0
  310. /package/dist/{server-core → vendored/server/core}/screenshot.js +0 -0
  311. /package/dist/{server-core → vendored/server/core}/sliceStatic.js +0 -0
  312. /package/dist/vendored/{meta → skills/dsl-card-generator/vendor/preview/meta}/action-meta.json +0 -0
  313. /package/dist/{server-core/_vendor → vendored/tools}/finmall-env.mjs +0 -0
@@ -0,0 +1,801 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * render_card.mjs —— 离线卡片渲染截图(不启动 npm run dev,也不占 5180/8787)。
4
+ *
5
+ * 自包含架构:
6
+ * - 渲染页面来自 <skill>/vendor/preview 的构建产物(由工程根 npm run build:skill-preview 产出);
7
+ * - 卡片数据目录默认取 cwd 的 src/cards,也可用 DSL_CARD_CARDS_DIR 覆盖;
8
+ * - 切图 /slices/* 由本脚本从 DSL_CARD_SLICES_DIR(默认 cwd/src/slices)直接提供;
9
+ * - 运行时只依赖 Node 内置模块 + 系统 Chrome(CDP),不再依赖 vite/vue/playwright;
10
+ * - 用纯 Node HTTP 托管构建产物,Chrome headless 打开后等 [data-card-ready],
11
+ * 通过 CDP 截取 .phone__screen 元素,调用结束后立即关闭浏览器与 HTTP server。
12
+ *
13
+ * 用法:
14
+ * node render_card.mjs --id <cardId> [--width 375] [--out render.png] [--format json|png]
15
+ *
16
+ * 渲染确定性(冷帧结论,2026-09 实测):
17
+ * - 会话首帧曾缺失卡片背景图/装饰图(同 schema 首帧 vs 热帧差 ~27% 像素):根因是页面侧
18
+ * 「资源静默 300ms」启发式看不见「在途但尚未产生 ResourceTiming 条目」的请求,提前触发截图;
19
+ * - 现改为 CDP Network 域追踪在途请求,截图前等网络静默(inflight 清空且 250ms 无新请求,
20
+ * 封顶 5s 防坏图挂死),首帧与热帧逐像素一致;调用方(训练评分 / lint 守护)无需暖机帧。
21
+ */
22
+ import fs from 'node:fs'
23
+ import http from 'node:http'
24
+ import os from 'node:os'
25
+ import readline from 'node:readline'
26
+ import path from 'node:path'
27
+ import { spawn, spawnSync } from 'node:child_process'
28
+ import { fileURLToPath } from 'node:url'
29
+
30
+ import { cardsDir, slicesDir, previewDir } from './lib/paths.mjs'
31
+
32
+ const DEFAULT_WIDTH = 375
33
+ const NAV_TIMEOUT_MS = 30000
34
+ const SETTLE_MS = 300
35
+ // CDP 单条命令的兜底超时:浏览器崩溃/被杀时响应永远不会回来,无超时会让
36
+ // 调用方(训练轮收尾、MCP 渲染、dev 预览)永久挂起。
37
+ const CDP_SEND_TIMEOUT_MS = 60000
38
+ // 预览 HTTP server 关闭兜底:Chrome 的 keep-alive 连接会让 server.close() 回调不触发。
39
+ const SERVER_CLOSE_TIMEOUT_MS = 2000
40
+
41
+ /* ------------------------------------------------------------------ */
42
+ /* 数据读取 */
43
+ /* ------------------------------------------------------------------ */
44
+ function deriveDataFromSchema(schema) {
45
+ const params = (schema && schema.dictionary && schema.dictionary.inputParams) || []
46
+ return Object.fromEntries(params.map((p) => [p.key, p.defaultValue]))
47
+ }
48
+
49
+ function readCardFiles(cardsRoot, id) {
50
+ const dir = path.join(cardsRoot, id)
51
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return null
52
+ let schema
53
+ try {
54
+ schema = JSON.parse(fs.readFileSync(path.join(dir, 'schema.json'), 'utf8'))
55
+ } catch {
56
+ return null
57
+ }
58
+ return { schema, data: deriveDataFromSchema(schema) }
59
+ }
60
+
61
+ /* ------------------------------------------------------------------ */
62
+ /* 静态 HTTP 服务(构建产物 + 卡片数据 + 切图) */
63
+ /* ------------------------------------------------------------------ */
64
+ const MIME = {
65
+ '.html': 'text/html; charset=utf-8',
66
+ '.js': 'text/javascript; charset=utf-8',
67
+ '.mjs': 'text/javascript; charset=utf-8',
68
+ '.css': 'text/css; charset=utf-8',
69
+ '.json': 'application/json; charset=utf-8',
70
+ '.svg': 'image/svg+xml',
71
+ '.png': 'image/png',
72
+ '.jpg': 'image/jpeg',
73
+ '.jpeg': 'image/jpeg',
74
+ '.webp': 'image/webp',
75
+ '.gif': 'image/gif',
76
+ '.woff': 'font/woff',
77
+ '.woff2': 'font/woff2',
78
+ '.ttf': 'font/ttf',
79
+ }
80
+
81
+ function mimeFor(filePath) {
82
+ return MIME[path.extname(filePath).toLowerCase()] || 'application/octet-stream'
83
+ }
84
+
85
+ function sendFile(res, filePath, contentType) {
86
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
87
+ res.statusCode = 404
88
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8')
89
+ return res.end('Not found')
90
+ }
91
+ res.statusCode = 200
92
+ res.setHeader('Content-Type', contentType || mimeFor(filePath))
93
+ res.setHeader('Cache-Control', 'no-store')
94
+ fs.createReadStream(filePath).pipe(res)
95
+ }
96
+
97
+ function resolveStaticPath(pathname) {
98
+ const rel = pathname.replace(/^\/+/, '')
99
+ if (!rel || rel.includes('..')) return null
100
+ const filePath = path.join(previewDir, rel)
101
+ if (filePath !== previewDir && !filePath.startsWith(previewDir + path.sep)) return null
102
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return null
103
+ return filePath
104
+ }
105
+
106
+ function sendSlice(req, res, next) {
107
+ if (!req.url || !req.url.startsWith('/slices/')) return next()
108
+ const rel = decodeURIComponent(req.url.slice('/slices/'.length).split('?')[0])
109
+ if (!rel || rel.includes('..')) return next()
110
+ const file = path.join(slicesDir, rel)
111
+ if (file !== slicesDir && !file.startsWith(slicesDir + path.sep)) return next()
112
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return next()
113
+ res.statusCode = 200
114
+ res.setHeader('Content-Type', mimeFor(file))
115
+ res.setHeader('Cache-Control', 'no-store')
116
+ fs.createReadStream(file).pipe(res)
117
+ }
118
+
119
+ function createPreviewServer({ cardsRoot }) {
120
+ return new Promise((resolve, reject) => {
121
+ const server = http.createServer((req, res) => {
122
+ const pathname = decodeURIComponent((req.url || '').split('?')[0])
123
+
124
+ if (pathname === '/' || pathname === '/standalone.html') {
125
+ return sendFile(res, path.join(previewDir, 'standalone.html'))
126
+ }
127
+
128
+ const ph = pathname.match(/^\/__ph__\/(\d+)x(\d+)$/)
129
+ if (ph) {
130
+ const pw = Math.max(1, Math.min(2000, Number(ph[1]) || 1))
131
+ const phh = Math.max(1, Math.min(2000, Number(ph[2]) || 1))
132
+ const phText = decodeURIComponent(new URL('http://x' + req.url).searchParams.get('text') || '缺素材')
133
+ const fontSize = Math.max(9, Math.min(14, Math.floor(Math.min(pw, phh) / 3)))
134
+ const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="' + pw + '" height="' + phh + '">'
135
+ + '<rect width="100%" height="100%" fill="#EDEFF3"/>'
136
+ + '<text x="50%" y="50%" font-size="' + fontSize + '" fill="#9AA0A6" text-anchor="middle"'
137
+ + ' dominant-baseline="middle" font-family="sans-serif">' + phText.replace(/[<>&]/g, '') + '</text></svg>'
138
+ res.statusCode = 200
139
+ res.setHeader('Content-Type', 'image/svg+xml')
140
+ res.setHeader('Cache-Control', 'no-store')
141
+ return res.end(svg)
142
+ }
143
+
144
+ const m = pathname.match(/^\/__standalone-card\/([^/]+)$/)
145
+ if (m) {
146
+ const files = readCardFiles(cardsRoot, decodeURIComponent(m[1]))
147
+ if (!files) {
148
+ res.statusCode = 404
149
+ res.setHeader('Content-Type', 'application/json; charset=utf-8')
150
+ return res.end(JSON.stringify({ error: 'card not found', id: decodeURIComponent(m[1]) }))
151
+ }
152
+ // iph.href.lu 占位链接 → 本地 /__ph__/ 路由(仅改渲染侧,schema 契约不变)
153
+ const rewritten = JSON.stringify(files).replace(
154
+ /https?:\/\/iph\.href\.lu\/(\d+)x(\d+)\?text=([^"\\]+)/g,
155
+ (all, w, h, text) => '/__ph__/' + w + 'x' + h + '?text=' + text)
156
+ res.statusCode = 200
157
+ res.setHeader('Content-Type', 'application/json; charset=utf-8')
158
+ res.setHeader('Cache-Control', 'no-store')
159
+ return res.end(rewritten)
160
+ }
161
+
162
+ if (pathname.startsWith('/slices/')) {
163
+ return sendSlice(req, res, () => {
164
+ res.statusCode = 404
165
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8')
166
+ res.end('Not found')
167
+ })
168
+ }
169
+
170
+ const staticPath = resolveStaticPath(pathname)
171
+ if (staticPath) return sendFile(res, staticPath)
172
+
173
+ res.statusCode = 404
174
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8')
175
+ res.end('Not found')
176
+ })
177
+
178
+ server.once('error', reject)
179
+ server.listen(0, '127.0.0.1', () => {
180
+ resolve({
181
+ port: server.address().port,
182
+ close: () => new Promise((resolveClose) => {
183
+ let settled = false
184
+ const finish = () => { if (!settled) { settled = true; resolveClose() } }
185
+ // keep-alive 连接不会自己断开 ⇒ server.close() 回调可能永不触发;
186
+ // 超时后强制断开全部连接兜底,保证调用方(训练轮/MCP/dev)能收尾退出。
187
+ const hardKill = setTimeout(() => {
188
+ try { server.closeAllConnections() } catch {}
189
+ setTimeout(finish, 300).unref?.()
190
+ }, SERVER_CLOSE_TIMEOUT_MS)
191
+ hardKill.unref?.()
192
+ try { server.closeIdleConnections() } catch {}
193
+ server.close(() => { clearTimeout(hardKill); finish() })
194
+ }),
195
+ })
196
+ })
197
+ })
198
+ }
199
+
200
+ /* ------------------------------------------------------------------ */
201
+ /* Chrome + CDP 截屏 */
202
+ /* ------------------------------------------------------------------ */
203
+ function findChromePath() {
204
+ const candidates = [
205
+ process.env.CHROME_PATH,
206
+ process.platform === 'darwin' ? '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' : null,
207
+ process.platform === 'darwin' ? '/Applications/Chromium.app/Contents/MacOS/Chromium' : null,
208
+ 'google-chrome',
209
+ 'google-chrome-stable',
210
+ 'chromium',
211
+ 'chromium-browser',
212
+ 'chrome',
213
+ ].filter(Boolean)
214
+
215
+ for (const c of candidates) {
216
+ if (c.includes('/')) {
217
+ if (fs.existsSync(c)) return c
218
+ continue
219
+ }
220
+ const r = spawnSync('which', [c], { encoding: 'utf8' })
221
+ if (r.status === 0 && r.stdout.trim()) return r.stdout.trim()
222
+ }
223
+ return null
224
+ }
225
+
226
+ function launchChrome() {
227
+ return new Promise((resolve, reject) => {
228
+ const executablePath = findChromePath()
229
+ if (!executablePath) return reject(new Error('未找到系统 Chrome(可设置 CHROME_PATH)'))
230
+
231
+ const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dsl-card-chrome-'))
232
+ const child = spawn(
233
+ executablePath,
234
+ [
235
+ '--headless=new',
236
+ '--disable-gpu',
237
+ '--no-sandbox',
238
+ '--no-first-run',
239
+ '--no-default-browser-check',
240
+ '--hide-scrollbars',
241
+ '--remote-debugging-port=0',
242
+ `--user-data-dir=${userDataDir}`,
243
+ 'about:blank',
244
+ ],
245
+ { stdio: ['ignore', 'pipe', 'pipe'] },
246
+ )
247
+
248
+ let wsUrl = ''
249
+ let settled = false
250
+ const timer = setTimeout(() => {
251
+ if (settled) return
252
+ settled = true
253
+ try { child.kill('SIGKILL') } catch {}
254
+ reject(new Error('Chrome DevTools 启动超时'))
255
+ }, 15000)
256
+
257
+ const onData = (chunk) => {
258
+ const text = String(chunk)
259
+ const matched = /DevTools listening on (ws:\/\/\S+)/.exec(text)
260
+ if (matched && !wsUrl) {
261
+ wsUrl = matched[1]
262
+ const portMatch = /ws:\/\/[^/:]+:(\d+)\//.exec(wsUrl)
263
+ settled = true
264
+ clearTimeout(timer)
265
+ resolve({ child, wsUrl, userDataDir, port: portMatch ? Number(portMatch[1]) : 0 })
266
+ }
267
+ }
268
+
269
+ child.stdout.on('data', onData)
270
+ child.stderr.on('data', onData)
271
+ child.on('error', (err) => {
272
+ if (settled) return
273
+ settled = true
274
+ clearTimeout(timer)
275
+ reject(err)
276
+ })
277
+ child.on('exit', (code) => {
278
+ if (settled) return
279
+ settled = true
280
+ clearTimeout(timer)
281
+ reject(new Error(`Chrome 提前退出(code ${code})`))
282
+ })
283
+ })
284
+ }
285
+
286
+ class CDP {
287
+ constructor(socket) {
288
+ this.socket = socket
289
+ this.id = 0
290
+ this.pending = new Map()
291
+ this.events = []
292
+ this.bindingHandlers = new Map()
293
+ this.listeners = new Map()
294
+ socket.addEventListener('message', (ev) => this._onMessage(ev))
295
+ // 连接断开(浏览器退出/崩溃/被 SIGKILL)时立刻拒绝全部在途请求,
296
+ // 否则 send() 的 Promise 永不 settle,收尾 close() 卡死整轮。
297
+ const abortAll = () => this._abortPending(new Error('CDP WebSocket 已关闭'))
298
+ socket.addEventListener('close', abortAll)
299
+ socket.addEventListener('error', abortAll)
300
+ }
301
+
302
+ _abortPending(error) {
303
+ for (const entry of this.pending.values()) {
304
+ clearTimeout(entry.timer)
305
+ entry.reject(error)
306
+ }
307
+ this.pending.clear()
308
+ }
309
+
310
+ _onMessage(ev) {
311
+ let msg
312
+ try { msg = JSON.parse(ev.data) } catch { return }
313
+ if (!msg || !msg.id) {
314
+ if (msg?.method === 'Runtime.bindingCalled') {
315
+ const name = msg.params?.name
316
+ const handler = this.bindingHandlers.get(name)
317
+ if (handler) handler(msg.params?.payload || '')
318
+ }
319
+ if (msg?.method) {
320
+ this.events.push({ method: msg.method, params: msg.params })
321
+ if (this.events.length > 100) this.events.shift()
322
+ const handlers = this.listeners.get(msg.method)
323
+ if (handlers) for (const handler of handlers.slice()) {
324
+ try { handler(msg.params) } catch {}
325
+ }
326
+ }
327
+ return
328
+ }
329
+ if (!this.pending.has(msg.id)) return
330
+ const { resolve, reject, timer } = this.pending.get(msg.id)
331
+ this.pending.delete(msg.id)
332
+ clearTimeout(timer)
333
+ if (msg.error) reject(new Error(msg.error.message || JSON.stringify(msg.error)))
334
+ else resolve(msg.result)
335
+ }
336
+
337
+ send(method, params = {}, timeoutMs = CDP_SEND_TIMEOUT_MS) {
338
+ const id = ++this.id
339
+ return new Promise((resolve, reject) => {
340
+ const timer = setTimeout(() => {
341
+ this.pending.delete(id)
342
+ reject(new Error(`CDP ${method} 超时 (${timeoutMs}ms)`))
343
+ }, timeoutMs)
344
+ timer.unref?.()
345
+ this.pending.set(id, { resolve, reject, timer })
346
+ try {
347
+ this.socket.send(JSON.stringify({ id, method, params }))
348
+ } catch (error) {
349
+ this.pending.delete(id)
350
+ clearTimeout(timer)
351
+ reject(error instanceof Error ? error : new Error(String(error)))
352
+ }
353
+ })
354
+ }
355
+
356
+ /** 订阅 CDP 事件,返回退订函数 */
357
+ on(method, handler) {
358
+ const list = this.listeners.get(method) || []
359
+ list.push(handler)
360
+ this.listeners.set(method, list)
361
+ return () => {
362
+ const current = this.listeners.get(method) || []
363
+ this.listeners.set(method, current.filter((h) => h !== handler))
364
+ }
365
+ }
366
+
367
+ /** 注册一个 CDP binding,返回一个在 binding 被调用时 resolve 的 Promise */
368
+ waitForBinding(name, timeoutMs = 10000) {
369
+ return new Promise((resolve, reject) => {
370
+ const timer = setTimeout(() => {
371
+ this.bindingHandlers.delete(name)
372
+ reject(new Error(`等待 binding "${name}" 超时 (${timeoutMs}ms)`))
373
+ }, timeoutMs)
374
+ this.bindingHandlers.set(name, (payload) => {
375
+ clearTimeout(timer)
376
+ this.bindingHandlers.delete(name)
377
+ resolve(payload)
378
+ })
379
+ })
380
+ }
381
+ }
382
+
383
+ function connectCDP(wsUrl) {
384
+ return new Promise((resolve, reject) => {
385
+ const socket = new WebSocket(wsUrl)
386
+ socket.addEventListener('open', () => resolve(new CDP(socket)))
387
+ socket.addEventListener('error', () => reject(new Error('CDP WebSocket 连接失败')))
388
+ })
389
+ }
390
+
391
+ async function getPageTargetWsUrl(port) {
392
+ const listUrl = `http://127.0.0.1:${port}/json/list`
393
+ const res = await fetch(listUrl)
394
+ const targets = await res.json()
395
+ const page = targets.find((t) => t.type === 'page' && t.webSocketDebuggerUrl)
396
+ if (!page) throw new Error('未找到 Chrome 页面调试目标(/json/list)')
397
+ return page.webSocketDebuggerUrl
398
+ }
399
+
400
+ async function waitForExpression(cdp, expression, timeoutMs = NAV_TIMEOUT_MS) {
401
+ const deadline = Date.now() + timeoutMs
402
+ while (Date.now() < deadline) {
403
+ const res = await cdp.send('Runtime.evaluate', { expression, returnByValue: true })
404
+ if (res && res.result && res.result.value) return res
405
+ await new Promise((r) => setTimeout(r, 100))
406
+ }
407
+ throw new Error('等待渲染就绪超时(.phone__screen[data-card-ready])')
408
+ }
409
+
410
+ function describeRuntimeEvents(events) {
411
+ return events.map(({ method, params }) => {
412
+ if (method === 'Runtime.consoleAPICalled') {
413
+ const text = (params.args || []).map((arg) => arg.value ?? arg.description ?? '').join(' ')
414
+ return `console.${params.type}: ${text}`
415
+ }
416
+ if (method === 'Runtime.exceptionThrown') {
417
+ const detail = params.exceptionDetails || {}
418
+ return `exception: ${detail.text} ${detail.exception?.description || ''}`
419
+ }
420
+ if (method === 'Log.entryAdded') {
421
+ const entry = params.entry || {}
422
+ return `log.${entry.level}: ${entry.text}`
423
+ }
424
+ return method
425
+ }).filter(Boolean).slice(-20).join(' | ')
426
+ }
427
+
428
+ /* ------------------------------------------------------------------ */
429
+ /* 可复用渲染会话 */
430
+ /* ------------------------------------------------------------------ */
431
+ /** 等网络静默:inflight 清空且 quietMs 内无新请求;超时返回 false(不阻塞截图) */
432
+ async function waitNetworkIdle(inflight, { quietMs = 250, timeoutMs = 5000 } = {}) {
433
+ const deadline = Date.now() + timeoutMs
434
+ let quietStart = null
435
+ while (Date.now() < deadline) {
436
+ if (inflight.size === 0) {
437
+ if (quietStart === null) quietStart = Date.now()
438
+ else if (Date.now() - quietStart >= quietMs) return true
439
+ } else {
440
+ quietStart = null
441
+ }
442
+ await new Promise((resolve) => setTimeout(resolve, 50))
443
+ }
444
+ return false
445
+ }
446
+
447
+ export async function createRenderSession({ cardsRoot = cardsDir } = {}) {
448
+ const htmlPath = path.join(previewDir, 'standalone.html')
449
+ if (!fs.existsSync(htmlPath)) {
450
+ throw new Error(`未找到自包含预览产物:${htmlPath}。请先在工程根目录运行 npm run build:skill-preview`)
451
+ }
452
+
453
+ const server = await createPreviewServer({ cardsRoot })
454
+ const chrome = await launchChrome()
455
+ const pageWsUrl = await getPageTargetWsUrl(chrome.port)
456
+ const cdp = await connectCDP(pageWsUrl)
457
+ await cdp.send('Page.enable')
458
+ await cdp.send('Runtime.enable')
459
+ // 网络在途追踪:截图前等网络静默,闭环「首帧缺背景图/字体」的冷帧根因
460
+ await cdp.send('Network.enable').catch(() => {})
461
+ const inflight = new Set()
462
+ cdp.on('Network.requestWillBeSent', (params) => { if (params.type !== 'WebSocket') inflight.add(params.requestId) })
463
+ cdp.on('Network.loadingFinished', (params) => inflight.delete(params.requestId))
464
+ cdp.on('Network.loadingFailed', (params) => inflight.delete(params.requestId))
465
+ cdp.on('Network.requestServedFromCache', (params) => inflight.delete(params.requestId))
466
+ // 注册渲染完成 binding(事件驱动,替代 SETTLE_MS 轮询)
467
+ try {
468
+ await cdp.send('Runtime.addBinding', { name: '__cardRenderComplete' })
469
+ } catch {}
470
+ let closed = false
471
+ let pageReady = false
472
+
473
+ async function render({
474
+ id,
475
+ width = DEFAULT_WIDTH,
476
+ captureHeight = null,
477
+ format = 'png',
478
+ outFile = null,
479
+ hotReload = false,
480
+ captureElements = false,
481
+ } = {}) {
482
+ if (closed) return { error: 'session-closed', message: '渲染会话已关闭' }
483
+ if (!id) return { error: 'missing-id', message: '缺少卡片 id' }
484
+
485
+ const card = readCardFiles(cardsRoot, id)
486
+ if (!card) return { error: 'card-not-found', id }
487
+ if (format !== 'png' && format !== 'json') {
488
+ return { error: 'invalid-format', message: 'format 仅支持 png 或 json' }
489
+ }
490
+
491
+ const startedAt = performance.now()
492
+ const url = `http://127.0.0.1:${server.port}/standalone.html?card=${encodeURIComponent(id)}&width=${encodeURIComponent(width)}`
493
+
494
+ try {
495
+ cdp.events.length = 0
496
+ await cdp.send('Emulation.setDeviceMetricsOverride', {
497
+ width: Math.ceil(Number(width)) + 40,
498
+ height: Math.max(2000, Math.ceil(Number(captureHeight || 2000)) + 40),
499
+ deviceScaleFactor: 1,
500
+ mobile: false,
501
+ })
502
+ // 热更新路径:同会话内已渲染过该页时,通过 fetch 刷新卡片数据 + 重新触发 boot,跳过整页导航
503
+ if (hotReload && pageReady) {
504
+ await cdp.send('Emulation.setDeviceMetricsOverride', {
505
+ width: Math.ceil(Number(width)) + 40,
506
+ height: Math.max(2000, Math.ceil(Number(captureHeight || 2000)) + 40),
507
+ deviceScaleFactor: 1,
508
+ mobile: false,
509
+ }).catch(() => {})
510
+ // 修改 URL 参数让 boot 函数用新 width,然后重新执行 boot
511
+ const reloadRes = await cdp.send('Runtime.evaluate', {
512
+ expression: `(async function(){history.replaceState(null,'','/standalone.html?card=${encodeURIComponent(id)}&width=${width}');await window.__bootCard&&window.__bootCard();return'ok'})()`,
513
+ returnByValue: true,
514
+ awaitPromise: true,
515
+ }).catch(() => ({ result: { value: null } }))
516
+ if (reloadRes?.result?.value === 'ok') {
517
+ // 外部图片(占位图等)加载慢于 boot:等资源静默 300ms 再截图
518
+ await cdp.send('Runtime.evaluate', {
519
+ expression: `new Promise((resolve)=>{var lastN=performance.getEntriesByType('resource').length,stable=0,tries=0;(function quiet(){var c=performance.getEntriesByType('resource').length;if(c===lastN){stable++}else{stable=0;lastN=c}tries++;if(stable>=6||tries>40){resolve('1');return}setTimeout(quiet,50)})()})`,
520
+ returnByValue: true,
521
+ awaitPromise: true,
522
+ }).catch(() => {})
523
+ await waitNetworkIdle(inflight)
524
+ const boxRes2 = await cdp.send('Runtime.evaluate', {
525
+ expression: `JSON.stringify((()=>{const el=document.querySelector('.phone__screen'); if(!el) return null; const r=el.getBoundingClientRect(); return {x:r.x,y:r.y,width:r.width,height:r.height};})())`,
526
+ returnByValue: true,
527
+ })
528
+ const box2 = boxRes2 && boxRes2.result && boxRes2.result.value ? JSON.parse(boxRes2.result.value) : null
529
+ if (box2 && box2.width && box2.height) {
530
+ const shot2 = await cdp.send('Page.captureScreenshot', {
531
+ format: 'png',
532
+ clip: { x: box2.x, y: box2.y, width: box2.width, height: box2.height, scale: 2 },
533
+ captureBeyondViewport: true,
534
+ })
535
+ const buffer2 = Buffer.from(shot2.data, 'base64')
536
+ return {
537
+ ok: true, source: 'standalone-hot', id,
538
+ width: Math.round(box2.width), height: Math.round(box2.height),
539
+ base64: buffer2.toString('base64'),
540
+ outFile: outFile ? path.resolve(outFile) : null,
541
+ ms: Math.round(performance.now() - startedAt),
542
+ }
543
+ }
544
+ }
545
+ // hot-reload 失败则 fallback 到正常导航
546
+ }
547
+
548
+ // 事件驱动:导航前注册 binding promise,页面渲染完成后 requestAnimationFrame 里回调
549
+ const renderDone = cdp.waitForBinding('__cardRenderComplete', NAV_TIMEOUT_MS).catch(() => null)
550
+ await cdp.send('Page.navigate', { url })
551
+ // 暴露 boot 函数供热更新复用(Vl 是 bundle 中的异步 boot 函数)
552
+ await cdp.send('Runtime.evaluate', { expression: `try{window.__bootCard=Vl}catch(e){window.__bootCard=null}` }).catch(() => {})
553
+ // 注入 rAF 检测器:data-card-ready 出现后等 2 帧(保证 layout)再触发 binding
554
+ await cdp.send('Runtime.evaluate', {
555
+ expression: `(function check(){var el=document.querySelector('.phone__screen');if(!(el&&el.hasAttribute('data-card-ready'))){setTimeout(check,30);return}requestAnimationFrame(()=>{requestAnimationFrame(()=>{var lastN=performance.getEntriesByType('resource').length,stable=0,tries=0;(function quiet(){var c=performance.getEntriesByType('resource').length;if(c===lastN){stable++}else{stable=0;lastN=c}tries++;if(stable>=6||tries>40){window.__cardRenderComplete&&__cardRenderComplete('1');return}setTimeout(quiet,50)})()})})})()`,
556
+ }).catch(() => {})
557
+ await renderDone
558
+ pageReady = true
559
+ // 截图前等网络静默:冷帧根因(背景图/字体在途时截图)在此闭环
560
+ await waitNetworkIdle(inflight)
561
+ const boxRes = await cdp.send('Runtime.evaluate', {
562
+ expression: `JSON.stringify((()=>{const el=document.querySelector('.phone__screen'); if(!el) return null; const r=el.getBoundingClientRect(); return {x:r.x,y:r.y,width:r.width,height:r.height};})())`,
563
+ returnByValue: true,
564
+ })
565
+ const box = boxRes && boxRes.result && boxRes.result.value ? JSON.parse(boxRes.result.value) : null
566
+ if (!box || !box.width || !box.height) {
567
+ return { error: 'capture-target-not-found', message: '.phone__screen 元素未找到' }
568
+ }
569
+
570
+ const shot = await cdp.send('Page.captureScreenshot', {
571
+ format: 'png',
572
+ clip: {
573
+ x: box.x,
574
+ y: box.y,
575
+ width: box.width,
576
+ height: captureHeight ? Math.min(box.height, Math.ceil(Number(captureHeight))) : box.height,
577
+ scale: 2,
578
+ },
579
+ captureBeyondViewport: true,
580
+ })
581
+ const buffer = Buffer.from(shot.data, 'base64')
582
+ let elementBoxes = null
583
+ if (captureElements) {
584
+ try {
585
+ const elRes = await cdp.send('Runtime.evaluate', {
586
+ expression: `(function(){
587
+ var scr = document.querySelector('.phone__screen');
588
+ if (!scr) return JSON.stringify([]);
589
+ var sr = scr.getBoundingClientRect();
590
+ var els = scr.querySelectorAll('[data-cid]');
591
+ var out = [];
592
+ els.forEach(function(el) {
593
+ var r = el.getBoundingClientRect();
594
+ if (r.width < 1 || r.height < 1) return;
595
+ var cls = el.getAttribute('data-class') || '';
596
+ var text = (el.textContent || '').trim();
597
+ // Input 类组件的占位文本在内部 <input placeholder> 属性上,textContent 为空;
598
+ // 评分/位置配对需要把它当作可见文本捕获(否则占位行被误判为文本缺失)。
599
+ if (!text) {
600
+ // Input.vue 根元素即 <input>:自身优先,其次向后兼容包裹式实现
601
+ var innerInput = (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') ? el : el.querySelector('input,textarea');
602
+ if (innerInput) text = (innerInput.getAttribute('placeholder') || innerInput.value || '').trim();
603
+ }
604
+ var cs = window.getComputedStyle(el);
605
+ var fs = parseFloat(cs.fontSize) || 0;
606
+ var color = cs.color || '';
607
+ out.push({
608
+ cid: el.getAttribute('data-cid') || '',
609
+ cls: cls,
610
+ text: text.slice(0, 60),
611
+ x: Math.round((r.left - sr.left) * 100) / 100,
612
+ y: Math.round((r.top - sr.top) * 100) / 100,
613
+ w: Math.round(r.width * 100) / 100,
614
+ h: Math.round(r.height * 100) / 100,
615
+ fs: Math.round(fs * 100) / 100,
616
+ color: color,
617
+ scrollH: el.scrollHeight || 0,
618
+ clientH: el.clientHeight || 0,
619
+ });
620
+ });
621
+ return JSON.stringify(out);
622
+ })()`,
623
+ returnByValue: true,
624
+ })
625
+ if (elRes?.result?.value) elementBoxes = JSON.parse(elRes.result.value)
626
+ } catch (e) { /* element extraction is best-effort */ }
627
+ }
628
+ const json = {
629
+ id,
630
+ mime: 'image/png',
631
+ width: Math.round(box.width),
632
+ height: Math.round(captureHeight ? Math.min(box.height, Number(captureHeight)) : box.height),
633
+ base64: buffer.toString('base64'),
634
+ elementBoxes: elementBoxes,
635
+ }
636
+
637
+ if (format === 'json') return { ok: true, source: 'standalone', ...json }
638
+
639
+ if (outFile) {
640
+ fs.mkdirSync(path.dirname(path.resolve(outFile)), { recursive: true })
641
+ fs.writeFileSync(outFile, buffer)
642
+ }
643
+ return {
644
+ ok: true,
645
+ source: 'standalone',
646
+ ...json,
647
+ outFile: outFile ? path.resolve(outFile) : null,
648
+ ms: Math.round(performance.now() - startedAt),
649
+ }
650
+ } catch (e) {
651
+ const runtime = describeRuntimeEvents(cdp.events)
652
+ return {
653
+ error: 'screenshot-failed',
654
+ message: String((e && e.message) || e) + (runtime ? ` | runtime: ${runtime}` : ''),
655
+ }
656
+ }
657
+ }
658
+
659
+ async function close() {
660
+ if (closed) return
661
+ closed = true
662
+ await cdp.send('Browser.close', {}, 5000).catch(() => {})
663
+ try { chrome.child.kill('SIGKILL') } catch {}
664
+ try { fs.rmSync(chrome.userDataDir, { recursive: true, force: true }) } catch {}
665
+ await server.close().catch(() => {})
666
+ try { cdp.socket.close() } catch {}
667
+ }
668
+
669
+ return { render, close }
670
+ }
671
+
672
+ /* ------------------------------------------------------------------ */
673
+ /* 对外 API */
674
+ /* ------------------------------------------------------------------ */
675
+ export async function renderCardScreenshot({
676
+ id,
677
+ width = DEFAULT_WIDTH,
678
+ captureHeight = null,
679
+ format = 'png',
680
+ outFile = null,
681
+ cardsRoot = cardsDir,
682
+ captureElements = false,
683
+ } = {}) {
684
+ if (!id) return { error: 'missing-id', message: '缺少卡片 id' }
685
+
686
+ let server = null
687
+ let session = null
688
+
689
+ try {
690
+ session = await createRenderSession({ cardsRoot })
691
+ return await session.render({ id, width, captureHeight, format, outFile, captureElements })
692
+ } catch (e) {
693
+ const runtime = session?.render ? describeRuntimeEvents([]) : ''
694
+ return {
695
+ error: 'screenshot-failed',
696
+ message: String((e && e.message) || e) + (runtime ? ` | runtime: ${runtime}` : ''),
697
+ }
698
+ } finally {
699
+ if (session) await session.close().catch(() => {})
700
+ }
701
+ }
702
+
703
+ /* ------------------------------------------------------------------ */
704
+ /* CLI */
705
+ /* ------------------------------------------------------------------ */
706
+ function parseArgs(argv) {
707
+ const out = { id: '', width: DEFAULT_WIDTH, format: 'png', outFile: '', keepAlive: false }
708
+ for (let i = 0; i < argv.length; i++) {
709
+ const a = argv[i]
710
+ if (a === '--id') out.id = argv[++i] || ''
711
+ else if (a === '--width') out.width = Number(argv[++i]) || DEFAULT_WIDTH
712
+ else if (a === '--format') out.format = argv[++i] === 'json' ? 'json' : 'png'
713
+ else if (a === '--out') out.outFile = argv[++i] || ''
714
+ else if (a === '--keep-alive') out.keepAlive = true
715
+ else if (a === '-h' || a === '--help') {
716
+ console.log([
717
+ 'Usage: node render_card.mjs --id <cardId> [--width 375] [--out render.png] [--format json|png]',
718
+ '',
719
+ 'Keep-alive mode (reuse Chrome + HTTP server across renders):',
720
+ ' node render_card.mjs --keep-alive',
721
+ ' Input one JSON object per line: {"id":"<cardId>","width":375,"outFile":"/tmp/render.png"}',
722
+ ' Output one JSON result per line. Close with Ctrl-D / stdin EOF.',
723
+ ].join('\n'))
724
+ process.exit(0)
725
+ }
726
+ }
727
+ if (!out.id && !out.keepAlive) {
728
+ console.error('[error] 缺少 --id')
729
+ process.exit(1)
730
+ }
731
+ return out
732
+ }
733
+
734
+ async function main() {
735
+ const args = parseArgs(process.argv.slice(2))
736
+
737
+ if (args.keepAlive) {
738
+ const session = await createRenderSession()
739
+ const rl = readline.createInterface({ input: process.stdin, terminal: false })
740
+ try {
741
+ for await (const line of rl) {
742
+ const text = line.trim()
743
+ if (!text) continue
744
+ let request
745
+ try {
746
+ request = JSON.parse(text)
747
+ } catch (e) {
748
+ console.log(JSON.stringify({ ok: false, error: 'invalid-json', message: e.message }))
749
+ continue
750
+ }
751
+ if (!request.id || !request.outFile) {
752
+ console.log(JSON.stringify({ ok: false, error: 'invalid-request', message: 'keep-alive 渲染必须提供 id 和 outFile' }))
753
+ continue
754
+ }
755
+ const result = await session.render({
756
+ id: request.id,
757
+ width: request.width ?? DEFAULT_WIDTH,
758
+ captureHeight: request.captureHeight ?? null,
759
+ format: request.format ?? 'png',
760
+ outFile: request.outFile || null,
761
+ })
762
+ const { base64, ...summary } = result
763
+ console.log(JSON.stringify(summary))
764
+ }
765
+ } finally {
766
+ await session.close()
767
+ }
768
+ return
769
+ }
770
+
771
+ const result = await renderCardScreenshot({
772
+ id: args.id,
773
+ width: args.width,
774
+ format: args.format,
775
+ outFile: args.outFile || null,
776
+ })
777
+
778
+ if (result.error) {
779
+ console.error(`[error] ${result.message || result.error}`)
780
+ process.exit(1)
781
+ }
782
+
783
+ if (args.format === 'json') {
784
+ console.log(JSON.stringify(result, null, 2))
785
+ } else {
786
+ const outFile = args.outFile || path.join(process.cwd(), `${args.id}.png`)
787
+ if (!args.outFile) fs.writeFileSync(outFile, Buffer.from(result.base64, 'base64'))
788
+ console.log(outFile)
789
+ }
790
+ }
791
+
792
+ // 通过 realpath 归一化处理符号链接(.codex/skills → .workbuddy/skills)
793
+ const _thisScript = fs.realpathSync(fileURLToPath(import.meta.url))
794
+ const _invokedAs = (process.argv[1] && fs.existsSync(process.argv[1]))
795
+ ? fs.realpathSync(process.argv[1])
796
+ : process.argv[1]
797
+ if (_invokedAs === _thisScript) {
798
+ main()
799
+ }
800
+
801
+ export default renderCardScreenshot