dsh-drawai 0.4.1 → 0.4.3

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 (365) hide show
  1. package/README.md +96 -253
  2. package/lib/client.js +28 -7
  3. package/lib/index.js +474 -97
  4. package/lib/motion/asset-pack.js +109 -50
  5. package/lib/motion/assets.js +291 -291
  6. package/lib/motion/demo-assets-lock.js +10 -0
  7. package/lib/motion/mgstyles/vendor/README.md +28 -0
  8. package/lib/motion/mgstyles/vendor/modules/d3/LICENSE +13 -0
  9. package/lib/motion/mgstyles/vendor/modules/d3/dist/d3.min.js +2 -0
  10. package/lib/motion/mgstyles/vendor/modules/opentype.js/LICENSE +20 -0
  11. package/lib/motion/mgstyles/vendor/modules/opentype.js/dist/opentype.mjs +14460 -0
  12. package/lib/motion/mgstyles/vendor/modules/simplex-noise/LICENSE +21 -0
  13. package/lib/motion/mgstyles/vendor/modules/simplex-noise/dist/esm/simplex-noise.js +466 -0
  14. package/lib/motion/mgstyles/vendor/modules/topojson-client/LICENSE +13 -0
  15. package/lib/motion/mgstyles/vendor/modules/topojson-client/dist/topojson-client.min.js +2 -0
  16. package/lib/motion/preview.js +10 -3
  17. package/lib/motion/render.js +19 -1
  18. package/lib/motion/shotcraft/upstream/SKILL.md +286 -286
  19. package/lib/motion/shotcraft/upstream/assets/lib/Caption.tsx +54 -54
  20. package/lib/motion/shotcraft/upstream/assets/lib/ClipCard.tsx +200 -200
  21. package/lib/motion/shotcraft/upstream/assets/lib/DigitRoll.tsx +45 -45
  22. package/lib/motion/shotcraft/upstream/assets/lib/FlashCut.tsx +20 -20
  23. package/lib/motion/shotcraft/upstream/assets/lib/FlatPanel.tsx +65 -65
  24. package/lib/motion/shotcraft/upstream/assets/lib/PageCam.tsx +151 -151
  25. package/lib/motion/shotcraft/upstream/assets/lib/VerticalTicker.tsx +129 -129
  26. package/lib/motion/shotcraft/upstream/assets/lib/helpers/camera.tsx +75 -75
  27. package/lib/motion/shotcraft/upstream/assets/lib/helpers/motion.ts +40 -40
  28. package/lib/motion/shotcraft/upstream/assets/lib/helpers/rand.ts +11 -11
  29. package/lib/motion/shotcraft/upstream/assets/lib/helpers/shake.ts +10 -10
  30. package/lib/motion/shotcraft/upstream/assets/scripts/build-readme-cover.mjs +49 -49
  31. package/lib/motion/shotcraft/upstream/assets/scripts/capture-template.mjs +210 -210
  32. package/lib/motion/shotcraft/upstream/assets/scripts/sfx-source.py +423 -423
  33. package/lib/motion/shotcraft/upstream/assets/scripts/smoke-render-demos.py +170 -170
  34. package/lib/motion/shotcraft/upstream/assets/scripts/test_sfx_source.py +157 -157
  35. package/lib/motion/shotcraft/upstream/demos/README.md +120 -120
  36. package/lib/motion/shotcraft/upstream/demos/_fixtures/Brand.tsx +112 -112
  37. package/lib/motion/shotcraft/upstream/demos/_fixtures/Fixtures.tsx +647 -647
  38. package/lib/motion/shotcraft/upstream/demos/_fixtures/Look.tsx +316 -316
  39. package/lib/motion/shotcraft/upstream/demos/_fixtures/Motion.tsx +82 -82
  40. package/lib/motion/shotcraft/upstream/demos/_fixtures/PageCam2D.tsx +115 -115
  41. package/lib/motion/shotcraft/upstream/demos/_fixtures/Polish.tsx +243 -243
  42. package/lib/motion/shotcraft/upstream/demos/_textures/live-layout.json +388 -388
  43. package/lib/motion/shotcraft/upstream/demos/camera/basic-3d-scene/Basic3DScene.tsx +250 -250
  44. package/lib/motion/shotcraft/upstream/demos/camera/crash-zoom-punch/CrashImpactReal.tsx +305 -305
  45. package/lib/motion/shotcraft/upstream/demos/camera/crash-zoom-punch/CrashZoomReal.tsx +246 -246
  46. package/lib/motion/shotcraft/upstream/demos/camera/cursor-flyover/CursorFlyover.tsx +390 -390
  47. package/lib/motion/shotcraft/upstream/demos/camera/depth-layer-moves/MultiplaneReal.tsx +138 -138
  48. package/lib/motion/shotcraft/upstream/demos/camera/graze-face-tour/GrazeFaceTour.tsx +542 -542
  49. package/lib/motion/shotcraft/upstream/demos/camera/overhead-camera-moves/OverheadTabletopDrop.tsx +315 -315
  50. package/lib/motion/shotcraft/upstream/demos/camera/overhead-camera-moves/TiltReveal.tsx +299 -299
  51. package/lib/motion/shotcraft/upstream/demos/camera/space-camera-moves/DroneDiveLanding.tsx +173 -173
  52. package/lib/motion/shotcraft/upstream/demos/camera/space-camera-moves/ExplodedView.tsx +348 -348
  53. package/lib/motion/shotcraft/upstream/demos/camera/steep-tilt-glide/SteepTiltGlide.tsx +366 -366
  54. package/lib/motion/shotcraft/upstream/demos/camera/tension-camera-moves/BulletTimeFreezeOrbit.tsx +279 -279
  55. package/lib/motion/shotcraft/upstream/demos/camera/tension-camera-moves/PullBackIsolation.tsx +184 -184
  56. package/lib/motion/shotcraft/upstream/demos/camera/terminal-3d/Terminal3D.tsx +390 -390
  57. package/lib/motion/shotcraft/upstream/demos/data/before-after-slider-scrub/BeforeAfterSliderScrub.tsx +155 -155
  58. package/lib/motion/shotcraft/upstream/demos/data/chart-live-moves/AxisRescaleShockV2.tsx +303 -303
  59. package/lib/motion/shotcraft/upstream/demos/data/chart-live-moves/OscilloscopeStreamV2.tsx +290 -290
  60. package/lib/motion/shotcraft/upstream/demos/data/chart-live-moves/UnitDotSwarmRegroupV2.tsx +278 -278
  61. package/lib/motion/shotcraft/upstream/demos/data/gauge-readout-moves/NeedleSweepSelftest.tsx +307 -307
  62. package/lib/motion/shotcraft/upstream/demos/data/gauge-readout-moves/TapeScrollFixedPointer.tsx +215 -215
  63. package/lib/motion/shotcraft/upstream/demos/data/scroll-brake-moves/BrakeReticleLock.tsx +299 -299
  64. package/lib/motion/shotcraft/upstream/demos/data/scroll-brake-moves/ChangelogScrollBrake.tsx +232 -232
  65. package/lib/motion/shotcraft/upstream/demos/data/timeline-travel/TimelineTravel.tsx +290 -290
  66. package/lib/motion/shotcraft/upstream/demos/effects/assemble-then-type-flyin/AssembleThenTypeFlyin.tsx +273 -273
  67. package/lib/motion/shotcraft/upstream/demos/effects/aurora-bloom-bg-flip/AuroraBloomBgFlip.tsx +167 -167
  68. package/lib/motion/shotcraft/upstream/demos/effects/brand-frame-snap/BrandFrameSnap.tsx +536 -536
  69. package/lib/motion/shotcraft/upstream/demos/effects/fui-hud-moves/ReticleLockOn.tsx +164 -164
  70. package/lib/motion/shotcraft/upstream/demos/effects/glow-flyline-moves/FlylineArc.tsx +175 -175
  71. package/lib/motion/shotcraft/upstream/demos/effects/glow-flyline-moves/GlowOrbAmbient.tsx +197 -197
  72. package/lib/motion/shotcraft/upstream/demos/effects/glow-flyline-moves/OrbFlylineRelay.tsx +241 -241
  73. package/lib/motion/shotcraft/upstream/demos/effects/icon-performance-moves/AttentionBounce.tsx +279 -279
  74. package/lib/motion/shotcraft/upstream/demos/effects/impact-feedback/AnimeImpact.tsx +230 -230
  75. package/lib/motion/shotcraft/upstream/demos/effects/impact-feedback/HitCounter.tsx +345 -345
  76. package/lib/motion/shotcraft/upstream/demos/effects/light-play-moves/SheenSweepRetry.tsx +275 -275
  77. package/lib/motion/shotcraft/upstream/demos/effects/light-play-moves/SpotlightSweepReveal.tsx +229 -229
  78. package/lib/motion/shotcraft/upstream/demos/effects/radial-ripple-phone-chips/RadialRipplePhoneChips.tsx +312 -312
  79. package/lib/motion/shotcraft/upstream/demos/effects/riso-print-hits/RisoMisregistrationHit.tsx +222 -222
  80. package/lib/motion/shotcraft/upstream/demos/effects/scan-bracket-sweep/ScanBracketSweep.tsx +295 -295
  81. package/lib/motion/shotcraft/upstream/demos/effects/scanline-annotate-focus/ScanlineAnnotateFocus.tsx +329 -329
  82. package/lib/motion/shotcraft/upstream/demos/effects/scanline-assemble-flyin/ScanlineAssembleFlyin.tsx +315 -315
  83. package/lib/motion/shotcraft/upstream/demos/effects/slam-entrance-moves/ImpactBurstKit.tsx +264 -264
  84. package/lib/motion/shotcraft/upstream/demos/effects/slam-entrance-moves/KanadaPerspectiveSnap.tsx +260 -260
  85. package/lib/motion/shotcraft/upstream/demos/effects/slam-entrance-moves/ScoreSlam.tsx +263 -263
  86. package/lib/motion/shotcraft/upstream/demos/effects/spotlight-sweep-moves/CornerSpotlightReveal.tsx +123 -123
  87. package/lib/motion/shotcraft/upstream/demos/effects/spotlight-sweep-moves/GlowWakeSleepPanel.tsx +187 -187
  88. package/lib/motion/shotcraft/upstream/demos/effects/spotlight-sweep-moves/SlideSpotlightPan.tsx +175 -175
  89. package/lib/motion/shotcraft/upstream/demos/interaction/autolayout-gap-dial/AutolayoutGapDial.tsx +305 -305
  90. package/lib/motion/shotcraft/upstream/demos/interaction/canvas-materialize-moves/DiagramCascadeBuild.tsx +350 -350
  91. package/lib/motion/shotcraft/upstream/demos/interaction/canvas-materialize-moves/PanelToCanvasMaterialize.tsx +352 -352
  92. package/lib/motion/shotcraft/upstream/demos/interaction/chip-grid-single-select-blackout/ChipGridSingleSelectBlackout.tsx +218 -218
  93. package/lib/motion/shotcraft/upstream/demos/interaction/collab-cursor-moves/CursorCastEnsemble.tsx +297 -297
  94. package/lib/motion/shotcraft/upstream/demos/interaction/glass-pill-dictation-typing/GlassPillDictationTyping.tsx +157 -157
  95. package/lib/motion/shotcraft/upstream/demos/interaction/hashtag-to-pill-materialize/HashtagToPillMaterialize.tsx +218 -218
  96. package/lib/motion/shotcraft/upstream/demos/interaction/picker-carousel-feature-cycle/PickerCarouselFeatureCycle.tsx +202 -202
  97. package/lib/motion/shotcraft/upstream/demos/interaction/segmented-thumb-hero/SegmentedThumbHero.tsx +265 -265
  98. package/lib/motion/shotcraft/upstream/demos/interaction/theme-switch-moves/PaletteThemeRipple.tsx +457 -457
  99. package/lib/motion/shotcraft/upstream/demos/interaction/theme-switch-moves/ThemeSweepToggle.tsx +446 -446
  100. package/lib/motion/shotcraft/upstream/demos/interaction/type-and-filter/TypeAndFilter.tsx +249 -249
  101. package/lib/motion/shotcraft/upstream/demos/interaction/voice-waveform-live/VoiceWaveformLive.tsx +278 -278
  102. package/lib/motion/shotcraft/upstream/demos/opening/crane-rise-reveal/CraneRiseReveal.tsx +332 -332
  103. package/lib/motion/shotcraft/upstream/demos/opening/dataviz-landscape-open/DatavizLandscapeOpen.tsx +358 -358
  104. package/lib/motion/shotcraft/upstream/demos/opening/fracture/Fracture.tsx +201 -201
  105. package/lib/motion/shotcraft/upstream/demos/opening/icon-field-colorize/IconFieldColorize.tsx +200 -200
  106. package/lib/motion/shotcraft/upstream/demos/opening/letterspace-materialize/LetterspaceMaterialize.tsx +208 -208
  107. package/lib/motion/shotcraft/upstream/demos/opening/magician-card-flourish/MagicianCardFlourish.tsx +341 -341
  108. package/lib/motion/shotcraft/upstream/demos/opening/orbit-ring-title-open/OrbitRingTitleOpen.tsx +389 -389
  109. package/lib/motion/shotcraft/upstream/demos/opening/spotlight-hero-card/SpotlightHeroCard.tsx +293 -293
  110. package/lib/motion/shotcraft/upstream/demos/opening/stroke-segment-build/StrokeSegmentBuild.tsx +183 -183
  111. package/lib/motion/shotcraft/upstream/demos/opening/text-as-mask/TextAsMask.tsx +245 -245
  112. package/lib/motion/shotcraft/upstream/demos/outro/grain-dissolve/GrainDissolve.tsx +222 -222
  113. package/lib/motion/shotcraft/upstream/demos/outro/logo-shrink-wordmark-lockup/LogoShrinkWordmarkLockup.tsx +223 -223
  114. package/lib/motion/shotcraft/upstream/demos/outro/neon-triple-marquee/NeonTripleMarquee.tsx +217 -217
  115. package/lib/motion/shotcraft/upstream/demos/outro/outro-group-photo-launch/OutroGroupPhotoLaunch.tsx +343 -343
  116. package/lib/motion/shotcraft/upstream/demos/outro/ui-strip-away-outro/UiStripAwayOutro.tsx +357 -357
  117. package/lib/motion/shotcraft/upstream/demos/outro/ui-to-brand-morph/IconFlipBloomLogo.tsx +187 -187
  118. package/lib/motion/shotcraft/upstream/demos/outro/ui-to-brand-morph/InputMorphsIntoLogo.tsx +333 -333
  119. package/lib/motion/shotcraft/upstream/demos/rhythm/beat-cut-moves/PaparazziFlash.tsx +274 -274
  120. package/lib/motion/shotcraft/upstream/demos/rhythm/beat-step-list-theme-cycle/BeatStepListThemeCycle.tsx +270 -270
  121. package/lib/motion/shotcraft/upstream/demos/rhythm/panel-grid-moves/ComicPanelSplit.tsx +430 -430
  122. package/lib/motion/shotcraft/upstream/demos/rhythm/quad-split-parallel-scenes/QuadSplitParallelScenes.tsx +252 -252
  123. package/lib/motion/shotcraft/upstream/demos/rhythm/rhythm-interrupt-moves/JumpCutPunchIn.tsx +106 -106
  124. package/lib/motion/shotcraft/upstream/demos/rhythm/sakuga-timing-shift/SakugaTimingShift.tsx +288 -288
  125. package/lib/motion/shotcraft/upstream/demos/rhythm/speed-ramp-freeze/FreezeAnnotateReal.tsx +362 -362
  126. package/lib/motion/shotcraft/upstream/demos/rhythm/trailer-grammar-moves/TrailerBumper.tsx +168 -168
  127. package/lib/motion/shotcraft/upstream/demos/transition/bottom-push-stack-wipe/BottomPushStackWipe.tsx +310 -310
  128. package/lib/motion/shotcraft/upstream/demos/transition/card-flip-reveal/CardFlipReveal.tsx +261 -261
  129. package/lib/motion/shotcraft/upstream/demos/transition/card-flock-tumble/CardFlockTumble.tsx +459 -459
  130. package/lib/motion/shotcraft/upstream/demos/transition/circle-match-iris/CircleMatchIris.tsx +294 -294
  131. package/lib/motion/shotcraft/upstream/demos/transition/gradient-transition/GradientTransition.tsx +161 -161
  132. package/lib/motion/shotcraft/upstream/demos/transition/line-carry-transition/LineCarryTransition.tsx +248 -248
  133. package/lib/motion/shotcraft/upstream/demos/transition/mosaic-reframe/MosaicReframe.tsx +359 -359
  134. package/lib/motion/shotcraft/upstream/demos/transition/page-turn-transitions/BarnDoorSplit.tsx +227 -227
  135. package/lib/motion/shotcraft/upstream/demos/transition/print-texture-transitions/InkBleedReveal.tsx +162 -162
  136. package/lib/motion/shotcraft/upstream/demos/transition/shot-transitions/PortalWipeV2.tsx +248 -248
  137. package/lib/motion/shotcraft/upstream/demos/transition/shot-transitions/WhipBrakeReal.tsx +186 -186
  138. package/lib/motion/shotcraft/upstream/demos/transition/tear-streak-transitions/GlitchDisplace.tsx +355 -355
  139. package/lib/motion/shotcraft/upstream/demos/transition/transition-hidden-cut/InvisibleCut.tsx +272 -272
  140. package/lib/motion/shotcraft/upstream/demos/transition/transition-hidden-cut/LightLeakBurn.tsx +262 -262
  141. package/lib/motion/shotcraft/upstream/demos/transition/transition-hidden-cut/VersusSlam.tsx +217 -217
  142. package/lib/motion/shotcraft/upstream/demos/transition/transition-travel/LetterformZoom.tsx +285 -285
  143. package/lib/motion/shotcraft/upstream/demos/transition/white-flash-logo-simplify-cut/WhiteFlashLogoSimplifyCut.tsx +205 -205
  144. package/lib/motion/shotcraft/upstream/demos/transition/wipe-transitions/BlindsSlice.tsx +178 -178
  145. package/lib/motion/shotcraft/upstream/demos/typography/blur-slide/BlurSlide.tsx +147 -147
  146. package/lib/motion/shotcraft/upstream/demos/typography/brace-expand/BraceExpand.tsx +143 -143
  147. package/lib/motion/shotcraft/upstream/demos/typography/brand-ink-open/BrandInkOpen.tsx +134 -134
  148. package/lib/motion/shotcraft/upstream/demos/typography/countdown-arc-scatter/CountdownArcScatter.tsx +217 -217
  149. package/lib/motion/shotcraft/upstream/demos/typography/flying-words/FlyingWords.tsx +198 -198
  150. package/lib/motion/shotcraft/upstream/demos/typography/glitch-cycle/GlitchCycle.tsx +221 -221
  151. package/lib/motion/shotcraft/upstream/demos/typography/gradient-word-sweep/GradientWordSweep.tsx +312 -312
  152. package/lib/motion/shotcraft/upstream/demos/typography/lead-word-zoom-assemble/LeadWordZoomAssemble.tsx +261 -261
  153. package/lib/motion/shotcraft/upstream/demos/typography/marker-underline-title/MarkerUnderlineTitle.tsx +121 -121
  154. package/lib/motion/shotcraft/upstream/demos/typography/outline-word-fill/OutlineWordFill.tsx +153 -153
  155. package/lib/motion/shotcraft/upstream/demos/typography/paper-title-card/PaperTitleCard.tsx +179 -179
  156. package/lib/motion/shotcraft/upstream/demos/typography/pill-chip-slot-cycle-handled/PillChipSlotCycleHandled.tsx +140 -140
  157. package/lib/motion/shotcraft/upstream/demos/typography/pill-slot-cycle/PillSlotCycle.tsx +309 -309
  158. package/lib/motion/shotcraft/upstream/demos/typography/text-column-converge/TextColumnConverge.tsx +191 -191
  159. package/lib/motion/shotcraft/upstream/demos/typography/title-demote-to-label/TitleDemoteToLabel.tsx +292 -292
  160. package/lib/motion/shotcraft/upstream/demos/typography/type-assembly-moves/LetterformDriftAssembly.tsx +135 -135
  161. package/lib/motion/shotcraft/upstream/demos/typography/type-assembly-moves/SplitTextStagger.tsx +124 -124
  162. package/lib/motion/shotcraft/upstream/demos/typography/type-assembly-moves/TextOnPath.tsx +255 -255
  163. package/lib/motion/shotcraft/upstream/demos/typography/type-entrance-moves/LetterDropPhysics.tsx +173 -173
  164. package/lib/motion/shotcraft/upstream/demos/typography/type-rhythm-sync/FontWeightPump.tsx +225 -225
  165. package/lib/motion/shotcraft/upstream/demos/typography/type-rhythm-sync/KaraokeFillSync.tsx +141 -141
  166. package/lib/motion/shotcraft/upstream/demos/typography/typewriter-moves/TypewriterErrorRetype.tsx +137 -137
  167. package/lib/motion/shotcraft/upstream/demos/typography/vertical-word-roll-blur-cycle/VerticalWordRollBlurCycle.tsx +147 -147
  168. package/lib/motion/shotcraft/upstream/demos/typography/word-relay-filmstrip/WordRelayFilmstrip.tsx +409 -409
  169. package/lib/motion/shotcraft/upstream/demos/typography/word-relay-geometry/WordRelayGeometry.tsx +280 -280
  170. package/lib/motion/shotcraft/upstream/demos/ui-entrance/avatar-bracket-carousel/AvatarBracketCarousel.tsx +271 -271
  171. package/lib/motion/shotcraft/upstream/demos/ui-entrance/bezier-source-converge-merge/BezierSourceConvergeMerge.tsx +385 -385
  172. package/lib/motion/shotcraft/upstream/demos/ui-entrance/card-stack/CardStack.tsx +312 -312
  173. package/lib/motion/shotcraft/upstream/demos/ui-entrance/carousel-3d/Carousel3D.tsx +303 -303
  174. package/lib/motion/shotcraft/upstream/demos/ui-entrance/clipcard-looping/ClipCardLooping.tsx +77 -77
  175. package/lib/motion/shotcraft/upstream/demos/ui-entrance/cloner-depth-echo/ClonerDepthEcho.tsx +311 -311
  176. package/lib/motion/shotcraft/upstream/demos/ui-entrance/deck-deal-flyin/DeckDealFlyin.tsx +307 -307
  177. package/lib/motion/shotcraft/upstream/demos/ui-entrance/doc-park-left-pill-deal/DocParkLeftPillDeal.tsx +349 -349
  178. package/lib/motion/shotcraft/upstream/demos/ui-entrance/document-typewriter-reveal/DocumentTypewriterReveal.tsx +255 -255
  179. package/lib/motion/shotcraft/upstream/demos/ui-entrance/draw-svg-trace/DrawSvgTrace.tsx +244 -244
  180. package/lib/motion/shotcraft/upstream/demos/ui-entrance/element-body-moves/ContactShadowLift.tsx +217 -217
  181. package/lib/motion/shotcraft/upstream/demos/ui-entrance/integration-hub-map/IntegrationHubMap.tsx +652 -652
  182. package/lib/motion/shotcraft/upstream/demos/ui-entrance/list-reveal/ListReveal.tsx +153 -153
  183. package/lib/motion/shotcraft/upstream/demos/ui-entrance/list-stack-press/ListStackPress.tsx +238 -238
  184. package/lib/motion/shotcraft/upstream/demos/ui-entrance/neon-frame-forerun/NeonFrameForerun.tsx +324 -324
  185. package/lib/motion/shotcraft/upstream/demos/ui-entrance/neon-frame-orbit-drop/NeonFrameForerunOrbit.tsx +354 -354
  186. package/lib/motion/shotcraft/upstream/demos/ui-entrance/page-waterfall-wall/PageWaterfallWall.tsx +101 -101
  187. package/lib/motion/shotcraft/upstream/demos/ui-entrance/page-waterfall-wall/VerticalTicker.tsx +129 -129
  188. package/lib/motion/shotcraft/upstream/demos/ui-entrance/product-card-progressive-assemble/ProductCardProgressiveAssemble.tsx +297 -297
  189. package/lib/motion/shotcraft/upstream/demos/ui-entrance/radial-wave/RadialWave.tsx +57 -57
  190. package/lib/motion/shotcraft/upstream/demos/ui-entrance/research-card-stack-scroll/ResearchCardStackScroll.tsx +155 -155
  191. package/lib/motion/shotcraft/upstream/demos/ui-entrance/row-embed/RowEmbed.tsx +175 -175
  192. package/lib/motion/shotcraft/upstream/demos/ui-entrance/value-stagger-gradient/ValueStaggerGradient.tsx +153 -153
  193. package/lib/motion/shotcraft/upstream/demos/ui-entrance/wall-reveal-moves/BentoLightUp.tsx +146 -146
  194. package/lib/motion/shotcraft/upstream/demos/ui-entrance/wall-reveal-moves/WireframeDrawOn.tsx +226 -226
  195. package/lib/motion/shotcraft/upstream/gallery/api/library.json +1 -1
  196. package/lib/motion/shotcraft/upstream/references/aesthetic-rules.md +193 -193
  197. package/lib/motion/shotcraft/upstream/references/final-review.md +121 -121
  198. package/lib/motion/shotcraft/upstream/references/guided-free-creation.md +199 -199
  199. package/lib/motion/shotcraft/upstream/references/jianying-export.md +183 -183
  200. package/lib/motion/shotcraft/upstream/references/music-beat-sync.md +257 -257
  201. package/lib/motion/shotcraft/upstream/references/pipeline.md +382 -382
  202. package/lib/motion/shotcraft/upstream/references/publish-upload-post.md +111 -111
  203. package/lib/motion/shotcraft/upstream/references/sequences/promo-energy-arc.md +50 -50
  204. package/lib/motion/shotcraft/upstream/references/sfx-sourcing.md +95 -95
  205. package/lib/motion/shotcraft/upstream/references/shots/camera/basic-3d-scene.md +41 -41
  206. package/lib/motion/shotcraft/upstream/references/shots/camera/crash-zoom-punch.md +52 -52
  207. package/lib/motion/shotcraft/upstream/references/shots/camera/cursor-flyover.md +39 -39
  208. package/lib/motion/shotcraft/upstream/references/shots/camera/depth-layer-moves.md +39 -39
  209. package/lib/motion/shotcraft/upstream/references/shots/camera/graze-face-tour.md +57 -57
  210. package/lib/motion/shotcraft/upstream/references/shots/camera/overhead-camera-moves.md +51 -51
  211. package/lib/motion/shotcraft/upstream/references/shots/camera/space-camera-moves.md +46 -46
  212. package/lib/motion/shotcraft/upstream/references/shots/camera/steep-tilt-glide.md +52 -52
  213. package/lib/motion/shotcraft/upstream/references/shots/camera/tension-camera-moves.md +43 -43
  214. package/lib/motion/shotcraft/upstream/references/shots/camera/terminal-3d.md +42 -42
  215. package/lib/motion/shotcraft/upstream/references/shots/data/before-after-slider-scrub.md +48 -48
  216. package/lib/motion/shotcraft/upstream/references/shots/data/chart-live-moves.md +52 -52
  217. package/lib/motion/shotcraft/upstream/references/shots/data/gauge-readout-moves.md +45 -45
  218. package/lib/motion/shotcraft/upstream/references/shots/data/scroll-brake-moves.md +46 -46
  219. package/lib/motion/shotcraft/upstream/references/shots/data/timeline-travel.md +44 -44
  220. package/lib/motion/shotcraft/upstream/references/shots/effects/assemble-then-type-flyin.md +63 -63
  221. package/lib/motion/shotcraft/upstream/references/shots/effects/aurora-bloom-bg-flip.md +54 -54
  222. package/lib/motion/shotcraft/upstream/references/shots/effects/brand-frame-snap.md +58 -58
  223. package/lib/motion/shotcraft/upstream/references/shots/effects/fui-hud-moves.md +35 -35
  224. package/lib/motion/shotcraft/upstream/references/shots/effects/glow-flyline-moves.md +53 -53
  225. package/lib/motion/shotcraft/upstream/references/shots/effects/icon-performance-moves.md +42 -42
  226. package/lib/motion/shotcraft/upstream/references/shots/effects/impact-feedback.md +50 -50
  227. package/lib/motion/shotcraft/upstream/references/shots/effects/light-play-moves.md +46 -46
  228. package/lib/motion/shotcraft/upstream/references/shots/effects/radial-ripple-phone-chips.md +56 -56
  229. package/lib/motion/shotcraft/upstream/references/shots/effects/riso-print-hits.md +49 -49
  230. package/lib/motion/shotcraft/upstream/references/shots/effects/scan-bracket-sweep.md +50 -50
  231. package/lib/motion/shotcraft/upstream/references/shots/effects/scanline-annotate-focus.md +53 -53
  232. package/lib/motion/shotcraft/upstream/references/shots/effects/scanline-assemble-flyin.md +53 -53
  233. package/lib/motion/shotcraft/upstream/references/shots/effects/slam-entrance-moves.md +55 -55
  234. package/lib/motion/shotcraft/upstream/references/shots/effects/spotlight-sweep-moves.md +52 -52
  235. package/lib/motion/shotcraft/upstream/references/shots/interaction/autolayout-gap-dial.md +60 -60
  236. package/lib/motion/shotcraft/upstream/references/shots/interaction/canvas-materialize-moves.md +57 -57
  237. package/lib/motion/shotcraft/upstream/references/shots/interaction/chip-grid-single-select-blackout.md +51 -51
  238. package/lib/motion/shotcraft/upstream/references/shots/interaction/collab-cursor-moves.md +41 -41
  239. package/lib/motion/shotcraft/upstream/references/shots/interaction/glass-pill-dictation-typing.md +53 -53
  240. package/lib/motion/shotcraft/upstream/references/shots/interaction/hashtag-to-pill-materialize.md +60 -60
  241. package/lib/motion/shotcraft/upstream/references/shots/interaction/picker-carousel-feature-cycle.md +55 -55
  242. package/lib/motion/shotcraft/upstream/references/shots/interaction/segmented-thumb-hero.md +57 -57
  243. package/lib/motion/shotcraft/upstream/references/shots/interaction/theme-switch-moves.md +47 -47
  244. package/lib/motion/shotcraft/upstream/references/shots/interaction/type-and-filter.md +37 -37
  245. package/lib/motion/shotcraft/upstream/references/shots/interaction/voice-waveform-live.md +54 -54
  246. package/lib/motion/shotcraft/upstream/references/shots/opening/brand-ink-open.md +36 -36
  247. package/lib/motion/shotcraft/upstream/references/shots/opening/crane-rise-reveal.md +56 -56
  248. package/lib/motion/shotcraft/upstream/references/shots/opening/dataviz-landscape-open.md +56 -56
  249. package/lib/motion/shotcraft/upstream/references/shots/opening/fracture.md +40 -40
  250. package/lib/motion/shotcraft/upstream/references/shots/opening/icon-field-colorize.md +60 -60
  251. package/lib/motion/shotcraft/upstream/references/shots/opening/letterspace-materialize.md +57 -57
  252. package/lib/motion/shotcraft/upstream/references/shots/opening/magician-card-flourish.md +73 -73
  253. package/lib/motion/shotcraft/upstream/references/shots/opening/orbit-ring-title-open.md +87 -87
  254. package/lib/motion/shotcraft/upstream/references/shots/opening/spotlight-hero-card.md +55 -55
  255. package/lib/motion/shotcraft/upstream/references/shots/opening/stroke-segment-build.md +50 -50
  256. package/lib/motion/shotcraft/upstream/references/shots/opening/text-as-mask.md +59 -59
  257. package/lib/motion/shotcraft/upstream/references/shots/outro/grain-dissolve.md +38 -38
  258. package/lib/motion/shotcraft/upstream/references/shots/outro/logo-shrink-wordmark-lockup.md +38 -38
  259. package/lib/motion/shotcraft/upstream/references/shots/outro/neon-triple-marquee.md +59 -59
  260. package/lib/motion/shotcraft/upstream/references/shots/outro/outro-group-photo-launch.md +39 -39
  261. package/lib/motion/shotcraft/upstream/references/shots/outro/ui-strip-away-outro.md +61 -61
  262. package/lib/motion/shotcraft/upstream/references/shots/outro/ui-to-brand-morph.md +52 -52
  263. package/lib/motion/shotcraft/upstream/references/shots/rhythm/beat-cut-moves.md +50 -50
  264. package/lib/motion/shotcraft/upstream/references/shots/rhythm/beat-step-list-theme-cycle.md +66 -66
  265. package/lib/motion/shotcraft/upstream/references/shots/rhythm/panel-grid-moves.md +43 -43
  266. package/lib/motion/shotcraft/upstream/references/shots/rhythm/quad-split-parallel-scenes.md +38 -38
  267. package/lib/motion/shotcraft/upstream/references/shots/rhythm/rhythm-interrupt-moves.md +38 -38
  268. package/lib/motion/shotcraft/upstream/references/shots/rhythm/sakuga-timing-shift.md +56 -56
  269. package/lib/motion/shotcraft/upstream/references/shots/rhythm/speed-ramp-freeze.md +44 -44
  270. package/lib/motion/shotcraft/upstream/references/shots/rhythm/trailer-grammar-moves.md +38 -38
  271. package/lib/motion/shotcraft/upstream/references/shots/transition/bottom-push-stack-wipe.md +63 -63
  272. package/lib/motion/shotcraft/upstream/references/shots/transition/card-flip-reveal.md +57 -57
  273. package/lib/motion/shotcraft/upstream/references/shots/transition/card-flock-tumble.md +63 -63
  274. package/lib/motion/shotcraft/upstream/references/shots/transition/circle-match-iris.md +58 -58
  275. package/lib/motion/shotcraft/upstream/references/shots/transition/gradient-transition.md +45 -45
  276. package/lib/motion/shotcraft/upstream/references/shots/transition/line-carry-transition.md +55 -55
  277. package/lib/motion/shotcraft/upstream/references/shots/transition/mosaic-reframe.md +49 -49
  278. package/lib/motion/shotcraft/upstream/references/shots/transition/page-turn-transitions.md +42 -42
  279. package/lib/motion/shotcraft/upstream/references/shots/transition/print-texture-transitions.md +42 -42
  280. package/lib/motion/shotcraft/upstream/references/shots/transition/shot-transitions.md +45 -45
  281. package/lib/motion/shotcraft/upstream/references/shots/transition/tear-streak-transitions.md +48 -48
  282. package/lib/motion/shotcraft/upstream/references/shots/transition/transition-hidden-cut.md +55 -55
  283. package/lib/motion/shotcraft/upstream/references/shots/transition/transition-travel.md +43 -43
  284. package/lib/motion/shotcraft/upstream/references/shots/transition/white-flash-logo-simplify-cut.md +41 -41
  285. package/lib/motion/shotcraft/upstream/references/shots/transition/wipe-transitions.md +42 -42
  286. package/lib/motion/shotcraft/upstream/references/shots/typography/blur-slide.md +59 -59
  287. package/lib/motion/shotcraft/upstream/references/shots/typography/brace-expand.md +55 -55
  288. package/lib/motion/shotcraft/upstream/references/shots/typography/countdown-arc-scatter.md +65 -65
  289. package/lib/motion/shotcraft/upstream/references/shots/typography/document-typewriter-reveal.md +42 -42
  290. package/lib/motion/shotcraft/upstream/references/shots/typography/flying-words.md +64 -64
  291. package/lib/motion/shotcraft/upstream/references/shots/typography/glitch-cycle.md +64 -64
  292. package/lib/motion/shotcraft/upstream/references/shots/typography/gradient-word-sweep.md +54 -54
  293. package/lib/motion/shotcraft/upstream/references/shots/typography/lead-word-zoom-assemble.md +91 -91
  294. package/lib/motion/shotcraft/upstream/references/shots/typography/marker-underline-title.md +51 -51
  295. package/lib/motion/shotcraft/upstream/references/shots/typography/outline-word-fill.md +66 -66
  296. package/lib/motion/shotcraft/upstream/references/shots/typography/paper-title-card.md +38 -38
  297. package/lib/motion/shotcraft/upstream/references/shots/typography/pill-chip-slot-cycle-handled.md +66 -66
  298. package/lib/motion/shotcraft/upstream/references/shots/typography/pill-slot-cycle.md +58 -58
  299. package/lib/motion/shotcraft/upstream/references/shots/typography/text-column-converge.md +61 -61
  300. package/lib/motion/shotcraft/upstream/references/shots/typography/title-demote-to-label.md +64 -64
  301. package/lib/motion/shotcraft/upstream/references/shots/typography/type-assembly-moves.md +45 -45
  302. package/lib/motion/shotcraft/upstream/references/shots/typography/type-entrance-moves.md +41 -41
  303. package/lib/motion/shotcraft/upstream/references/shots/typography/type-rhythm-sync.md +51 -51
  304. package/lib/motion/shotcraft/upstream/references/shots/typography/typewriter-moves.md +42 -42
  305. package/lib/motion/shotcraft/upstream/references/shots/typography/vertical-word-roll-blur-cycle.md +63 -63
  306. package/lib/motion/shotcraft/upstream/references/shots/typography/word-relay-filmstrip.md +57 -57
  307. package/lib/motion/shotcraft/upstream/references/shots/typography/word-relay-geometry.md +75 -75
  308. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/avatar-bracket-carousel.md +59 -59
  309. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/bezier-source-converge-merge.md +67 -67
  310. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/card-stack.md +60 -60
  311. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/carousel-3d.md +66 -66
  312. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/cloner-depth-echo.md +40 -40
  313. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/deck-deal-flyin.md +45 -45
  314. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/doc-park-left-pill-deal.md +62 -62
  315. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/draw-svg-trace.md +55 -55
  316. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/element-body-moves.md +37 -37
  317. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/integration-hub-map.md +55 -55
  318. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/list-reveal.md +50 -50
  319. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/list-stack-press.md +43 -43
  320. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/neon-frame-forerun.md +55 -55
  321. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/neon-frame-orbit-drop.md +51 -51
  322. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/page-waterfall-wall.md +51 -51
  323. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/product-card-progressive-assemble.md +56 -56
  324. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/radial-wave.md +49 -49
  325. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/research-card-stack-scroll.md +57 -57
  326. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/row-embed.md +36 -36
  327. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/value-stagger-gradient.md +64 -64
  328. package/lib/motion/shotcraft/upstream/references/shots/ui-entrance/wall-reveal-moves.md +47 -47
  329. package/lib/motion/shotcraft/upstream/references/sound-design.md +170 -170
  330. package/lib/motion/shotcraft/upstream/references/workbench.md +157 -157
  331. package/lib/motion/shotcraft/upstream/template/TEMPLATE.md +103 -103
  332. package/lib/motion/shotcraft/upstream/template/THEMES.md +44 -44
  333. package/lib/motion/shotcraft/upstream/template/package.json +20 -20
  334. package/lib/motion/shotcraft/upstream/template/remotion.config.ts +6 -6
  335. package/lib/motion/shotcraft/upstream/template/src/Root.tsx +17 -17
  336. package/lib/motion/shotcraft/upstream/template/src/aifl/Caption.tsx +65 -65
  337. package/lib/motion/shotcraft/upstream/template/src/aifl/DigitRoll.tsx +48 -48
  338. package/lib/motion/shotcraft/upstream/template/src/aifl/FlashCut.tsx +24 -24
  339. package/lib/motion/shotcraft/upstream/template/src/aifl/Main.tsx +192 -192
  340. package/lib/motion/shotcraft/upstream/template/src/aifl/PaperTitleCard.tsx +111 -111
  341. package/lib/motion/shotcraft/upstream/template/src/aifl/live/PageCam.tsx +150 -150
  342. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneDetail.tsx +158 -158
  343. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneFlyIn.tsx +468 -468
  344. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneOpen.tsx +552 -552
  345. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneOutroLive.tsx +411 -411
  346. package/lib/motion/shotcraft/upstream/template/src/aifl/live/ScenePapers.tsx +200 -200
  347. package/lib/motion/shotcraft/upstream/template/src/aifl/live/SceneWbr.tsx +324 -324
  348. package/lib/motion/shotcraft/upstream/template/src/aifl/live-layout.json +388 -388
  349. package/lib/motion/shotcraft/upstream/template/src/index.ts +4 -4
  350. package/lib/motion/shotcraft/upstream/template/src/themes/ThemedFilm.tsx +7 -7
  351. package/lib/motion/shotcraft/upstream/template/src/themes/manifest.tsx +68 -68
  352. package/lib/motion/shotcraft/upstream/template/src/themes/palette-assets.json +1 -1
  353. package/lib/motion/shotcraft/upstream/template/src/themes/palette-assets.ts +24 -24
  354. package/lib/motion/shotcraft/upstream/template/src/themes/palettes.json +74 -74
  355. package/lib/motion/shotcraft/upstream/template/src/themes/visual-theme.tsx +91 -91
  356. package/lib/motion/shotcraft/upstream/template/src/workbench.ts +213 -213
  357. package/lib/motion/shotcraft/upstream/template/tsconfig.json +15 -15
  358. package/lib/motion/style-frames.js +47 -12
  359. package/lib/motion-project.js +22 -3
  360. package/lib/motion-render-worker.js +31 -9
  361. package/lib/routing/router.js +12 -1
  362. package/lib/style-kernel.js +16 -6
  363. package/lib/video-trace-pool.js +128 -128
  364. package/package.json +1 -1
  365. package/LICENSE +0 -4175
package/lib/index.js CHANGED
@@ -257,6 +257,26 @@ function messageOf(error) {
257
257
  return String(error)
258
258
  }
259
259
 
260
+ // json 参数在工具 schema 里编译成 `{}`(没有类型约束),有的模型/宿主会把它当成字符串送进来,
261
+ // 比如 frames 收到的是 "[0, 120]"。这里统一还原一次:看起来像 JSON 且解析成功就用解析结果,
262
+ // 否则原样返回。不这么做的话,Array.isArray(字符串) 恒为假,参数会被静默忽略、退回默认值。
263
+ function jsonValue(value) {
264
+ if (typeof value !== 'string') return value
265
+ const text = value.trim()
266
+ if (!text || !'[{"]-0123456789ntf'.includes(text[0])) return value
267
+ try { return JSON.parse(text) } catch { return value }
268
+ }
269
+
270
+ function jsonObject(value) {
271
+ const parsed = jsonValue(value)
272
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
273
+ }
274
+
275
+ function jsonArray(value) {
276
+ const parsed = jsonValue(value)
277
+ return Array.isArray(parsed) ? parsed : []
278
+ }
279
+
260
280
  function numberOr(value, fallback) {
261
281
  const n = Number(value)
262
282
  return Number.isFinite(n) ? n : fallback
@@ -2386,6 +2406,12 @@ function placeMissing(doc) {
2386
2406
 
2387
2407
  /** 画布写回端点:人工编辑唯一的落盘通道。 */
2388
2408
  export const SAVE_PATH = '/drawai/api/save'
2409
+ /**
2410
+ * 只读诊断端点:把"这次到底注册了哪几个 host 工具"直接摆出来。
2411
+ * 起因是 issue #8 —— 画布能用、apply 也确实跑过,但模型侧一个工具都没有;
2412
+ * 现场只有一张截图,缺的正是这条数据(没注册 / 注册报错 / 注册完又被回收)。
2413
+ */
2414
+ export const DIAGNOSTICS_PATH = '/drawai/api/diagnostics'
2389
2415
  /** 「打开」菜单最多下钻几个子目录 —— 不做全盘递归,避免撞上 node_modules 这类大目录。 */
2390
2416
  const MAX_SCAN_DIRS = 12
2391
2417
  /** 自定义请求头 —— 跨站表单设不了它,跨域 fetch 会被预检拦住(CSRF 围栏)。 */
@@ -2441,9 +2467,40 @@ export const inject = ['tools', 'fs', 'sessions', 'sandboxPolicy', 'webServer']
2441
2467
  * 它出现在会话的技能目录里,模型需要时按需取全文 —— 于是"怎么在这张画布上操作"这件事
2442
2468
  * 跟着插件走,而不是跟着这份仓库走。
2443
2469
  *
2470
+ * **拆成两份**(drawai-canvas / drawai-video),因为这是两种任务、两套工具族:画布那份讲
2471
+ * diagram_read / diagram_apply 的全部 op 与几何规则,视频那份讲十步制作流程与 motion_* 那组工具。
2472
+ * 合成一份时正文近三万字,而技能目录里只显示 description 那一句 —— 用户说"做个宣传片"会先被
2473
+ * "读写画布"这句挡住;分开后两条简介各自对口,画图的人也不必每次为视频流程付上下文。
2474
+ * 视频那份自带一段"画布最小集"(见下),单独加载也不会写错 op;画布那份末尾指向视频技能。
2475
+ *
2444
2476
  * 写成模板串但**不用反引号**(markdown 里要写示例,反引号会破坏模板串):代码块用四空格缩进。
2477
+ * 两份各自是独立模板串;`tools/build.mjs` 的反引号配平检查按整份文件数,照样管得住。
2445
2478
  */
2446
- const SKILL_BODY = `# DrawAI 画布:怎么读、怎么改
2479
+
2480
+ /** 技能 1/2:视频。位置保持原样(正文接在原来的地方),改动只剩标题与开头那段最小集。 */
2481
+ const SKILL_BODY_VIDEO = `# DrawAI 画布做视频:从问到出片
2482
+
2483
+ 这个技能只讲**怎么把一条片子做出来**。画布怎么读、怎么改(diagram_read / diagram_apply 的完整签名、
2484
+ 折点与端点、图层与顺序、traceImage 位图矢量化)在技能 drawai-canvas 里 —— 视频流程真正用到的那几个 op
2485
+ 下面列全了,不够用时再加载那个技能。
2486
+
2487
+ ## 画布最小集(视频只用到这些 op)
2488
+
2489
+ // 1) 先读回当前那张画布:用户说"这张"就别猜路径,不传 path 读一次
2490
+ diagram_read() // → { path, absolute, nodes, edges, ... },path 拿去给 apply
2491
+ // 2) 一次写完:建节点、连卡、放分镜表;几何已经规划好就 layout:"none"
2492
+ diagram_apply({ path, layout:"none", ops:[
2493
+ {op:"addNode", label:"分镜表", shape:"text", x:40, y:40, w:520, h:360, as:"board"},
2494
+ {op:"addNode", label:"主片", shape:"video", x:600, y:40, w:480, h:270, as:"film"},
2495
+ {op:"addEdge", from:"shot-card", to:"film"} // shot-card 是画布上已存在的镜头卡 id
2496
+ ]})
2497
+
2498
+ - **path 必填**:diagram_apply 永远要显式传 path;diagram_read 不传时返回当前聚焦的那张。
2499
+ - **from / to 必须是已存在的节点 id**(写错在写盘前就报错);as 起的别名只在同一次 ops 数组里有效。
2500
+ - **建了镜头卡就必须连**:断连的卡既没进 assets、也不排期,motion_project create 会直接报错。
2501
+ - 连线用圆角折线,折点自己规划(points 只放中间转折,不放源/目标连接点),不要启用自动避障。
2502
+ - 挪位置用 move,改文字用 setLabel,换样式用 setStyle;改一组就传 ids:[...]。改接边用 setEdge,
2503
+ 不要 remove + addEdge 重画 —— 那会丢掉端点约束和折点。
2447
2504
 
2448
2505
  ## 视频节点与工作流编排
2449
2506
 
@@ -2500,12 +2557,19 @@ const SKILL_BODY = `# DrawAI 画布:怎么读、怎么改
2500
2557
  参考图:diagram_apply 连接图片节点后会把画布快照冻结进工程 assets/,场景里通过 props.assets 取用。视频参数(宽高/fps/duration)在 motion_project create 时写入节点;用户指定时长必须显式传 duration(支持 9.3 等小数秒),不能只写在 prompt 里。
2501
2558
  旧任务记录用 motion_task list/status/cancel/apply/focus/remove 管理,不要再指望它 start。多页或多个视频时明确指定 page/nodeId。
2502
2559
  不要把这条流程想成"提交一个任务等结果":你是在一个普通工程里写代码、看图、渲染,每一步都由本次对话直接完成。
2560
+ `
2561
+
2562
+ /** 技能 2/2:画布。正文从原来的位置原样接上,只多了标题下的一条"做视频去那边"。 */
2563
+ const SKILL_BODY_CANVAS = `# DrawAI 画布:怎么读、怎么改
2503
2564
 
2504
2565
  ## 一句话
2505
2566
  工作区里任何一个 .drawio 文件**就是**一张 DrawAI 画布 —— 它是 drawio 自己的 mxfile 格式。
2506
2567
  用户在 DSH 右栏画布面板里看到的、以及用 drawio 桌面版打开的,是同一份文件;
2507
2568
  没有"导入/导出"这一步,你直接读写这个文件就是读写那张画布。
2508
2569
 
2570
+ 视频节点、镜头卡、分镜表也都是这张画布上的对象,但**做视频**(选卡、写分镜表、出样片与成片)
2571
+ 是另一套流程:加载技能 drawai-video。这里只讲怎么读写它们。
2572
+
2509
2573
  ## 两个工具怎么配合
2510
2574
  1. **diagram_read(path?, layer?, ids?)** —— 先读。返回节点(id/label/shape/style/**x·y·w·h 坐标尺寸**)、
2511
2575
  边(id/from/to/label/style/dash/arrow/折点/悬空端的自由点/标签位置 labelX·labelY)、
@@ -2741,8 +2805,12 @@ diagram_read —— 返回里的 path 就是它(这两条是一致的,同一
2741
2805
  ## 想看效果图的时候(diagram_read 带 render:true)
2742
2806
  - 传 render:true 就是"让我看一眼这张画布":正交路由、避让、圆角、文字断线这些
2743
2807
  **只有看图才知道**的东西,几何数据里没有(宿主也不算走线,那是画布算的)。
2744
- - 渲染在画布那一半做,所以是两步:第一次返回 look.status="pending"(已请求),
2745
- **等几秒再调一次** diagram_read(render:true),图就作为图片内容块跟回来了。
2808
+ - 渲染在**浏览器那一半**做,所以只有用户在右侧栏**打开着这张画布**时才有人接活:
2809
+ · 画布没打开(宿主知道你打开的是别的文件)→ look.status="not-open",附原因,别重试;
2810
+ · 打开了 → 第一次返回 look.status="pending"(已请求,带 waitedMs),**等几秒再调一次**
2811
+ diagram_read(render:true),图就作为图片内容块跟回来了;
2812
+ · 挂了请求却一直没人接(25 秒)→ look.status="timeout",带已等待秒数 —— 这时别再
2813
+ 傻等,要么请用户先打开画布,要么直接用几何数据。
2746
2814
  - **不会在用户的工作区里生成任何文件**:图片存在宿主那边的附件库里(与用户上传图片同一套存储,
2747
2815
  在工作区之外),看完即取走 —— 要再看就再要一次(图不会被反复塞给你)。
2748
2816
  - **纯改图不需要它**:几何、样式、图层、纸张从普通 diagram_read 里就有。
@@ -2782,16 +2850,30 @@ HTML 标签按纯文本显示、图层结构原样保留(画布把它们叠在
2782
2850
  `
2783
2851
 
2784
2852
  /**
2785
- * 技能目录里那一段(模型先看到简介,需要时才取全文)。
2853
+ * 技能目录里那两条(模型先看到简介,需要时才取全文)。
2786
2854
  * 名字用 kebab-case —— DSH 的注册表按名字寻址并要求这个形状。
2855
+ *
2856
+ * description 是**唯一的检索入口**:会话的技能目录只渲染 name + description(whenToUse 不出现在
2857
+ * 目录里),所以两边各写各的任务词,互不侵占 —— 这正是拆分的主要收益。
2787
2858
  */
2788
- const SKILL_SUMMARY = {
2789
- name: 'drawai-canvas',
2790
- description:
2791
- '读写 DrawAI 画布(.drawio):diagram_read 看现状、diagram_apply 结构化改图;做视频先问清风格与品牌素材(motion_assets gather 可整站自动收集素材/配色/字体/logo)、出过样片再开拍;支持图像复刻、视频分段复刻及 SVG 帧序列导出。',
2792
- whenToUse:
2793
- '用户让你画流程图/架构图/泳道图、复刻图片或视频、将视频转为 SVG 帧动画,或让你读/改某张 .drawio 画布,或问"这张图里有什么""画布上怎么操作"时。',
2794
- }
2859
+ const SKILL_DEFINITIONS = [
2860
+ {
2861
+ name: 'drawai-canvas',
2862
+ description:
2863
+ '读写 DrawAI 画布(.drawio):diagram_read 看现状、diagram_apply 结构化改图;支持图像复刻、视频分段复刻及 SVG 帧序列导出。',
2864
+ whenToUse:
2865
+ '用户让你画流程图/架构图/泳道图、复刻图片或视频、将视频转为 SVG 帧动画,或让你读/改某张 .drawio 画布,或问"这张图里有什么""画布上怎么操作"时。',
2866
+ content: SKILL_BODY_CANVAS,
2867
+ },
2868
+ {
2869
+ name: 'drawai-video',
2870
+ description:
2871
+ '用 DrawAI 画布做视频:定镜头卡与整片风格、写分镜表、生成工程、出样片与成片;含品牌素材收集、配音与踩点。',
2872
+ whenToUse:
2873
+ '用户让你做视频/宣传片/产品片/MG 动画、给已有视频改镜头或配音、把画布上的流程做成片子,或问"样片怎么出""镜头卡怎么连""配乐怎么踩点"时。',
2874
+ content: SKILL_BODY_VIDEO,
2875
+ },
2876
+ ]
2795
2877
 
2796
2878
  /**
2797
2879
  * 列出工作区里的画布文档(非递归进子目录,最多扫 MAX_SCAN_DIRS 个)。
@@ -2853,7 +2935,7 @@ async function listCanvases(root, cwd, dir) {
2853
2935
  if (first.entries === null) notes.push('无法列出 ' + startAbs + ':' + first.error)
2854
2936
  else collect(first.entries, '')
2855
2937
  for (let d = 0; d < dirs.length; d += 1) {
2856
- const subAbs = toAbsolute(dirs[d], cwd)
2938
+ const subAbs = toAbsolute(dirs[d], startAbs)
2857
2939
  const sub = await readDir(subAbs)
2858
2940
  if (sub.entries === null) {
2859
2941
  notes.push('无法列出 ' + subAbs + ':' + sub.error)
@@ -2868,6 +2950,102 @@ async function listCanvases(root, cwd, dir) {
2868
2950
 
2869
2951
  export function apply(ctx) {
2870
2952
  const documents = createDocumentCoordinator(), mediaSlots = createMediaWorkSlots(2), taskPatches = createTaskPatchJournal()
2953
+
2954
+ // ── host 工具注册的可观测化(issue #8)────────────────────────────────────
2955
+ // 现场是"画布全好、apply 也执行了、模型侧一个工具都没有,而且哪儿都不报错"。
2956
+ // 三种机制必须能被区分开,光凭"apply 进去了"证明不了什么:
2957
+ // ① apply 期间 tools 服务还没 ACTIVE —— 注册等于扔进空处;
2958
+ // ② 某个注册抛错,把同一段里后面的注册和收尾一起带走;
2959
+ // ③ 注册成功了,之后整个 effect scope 被回收 —— 只有"注册了 N 个"这条日志能反推。
2960
+ // 所以每个工具逐项记名、逐项兜错,结果同时挂到 DIAGNOSTICS_PATH 上供远端取证。
2961
+ const toolRegistrations = []
2962
+ const pendingToolRegistrations = []
2963
+ let pendingFlushRequested = false, toolReportLogged = false
2964
+
2965
+ /** 取工具登记表本身;取不到返回 undefined(不抛),调用方据此走挂起路径。 */
2966
+ function toolsService() {
2967
+ const usable = (value) => value !== null && value !== undefined && typeof value.register === 'function'
2968
+ // 两条路都要试:真实 Cordis 走 service 解析(ctx.get),而宿主注入的 ctx 上
2969
+ // tools 也可能是普通属性 —— 只看其中一条会把"可用"误判成"没就绪"。
2970
+ try {
2971
+ if (typeof ctx.get === 'function' && usable(ctx.get('tools'))) return ctx.get('tools')
2972
+ } catch (error) {
2973
+ // 解析本身失败(服务未 ACTIVE)不该中断:接着看直接属性。
2974
+ }
2975
+ try {
2976
+ return usable(ctx.tools) ? ctx.tools : undefined
2977
+ } catch (error) {
2978
+ return undefined
2979
+ }
2980
+ }
2981
+
2982
+ function registerHostTool(tool) {
2983
+ const named = tool !== null && tool !== undefined && typeof tool.name === 'string' && tool.name.length > 0
2984
+ const entry = { name: named ? tool.name : '(unnamed)', ok: false, deferred: false, error: '', disposer: undefined }
2985
+ toolRegistrations.push(entry)
2986
+ const attempt = () => {
2987
+ const tools = toolsService()
2988
+ if (tools === undefined) return false
2989
+ try {
2990
+ const disposer = tools.register(tool)
2991
+ entry.disposer = disposer
2992
+ entry.ok = typeof disposer === 'function'
2993
+ entry.error = entry.ok ? '' : 'register() 没有返回 disposer(tools 服务可能未 ACTIVE)'
2994
+ } catch (error) {
2995
+ entry.ok = false
2996
+ entry.error = messageOf(error)
2997
+ }
2998
+ if (!entry.ok) console.error('[drawai] 工具注册失败 ' + entry.name + ':' + entry.error)
2999
+ return true
3000
+ }
3001
+ if (!attempt()) {
3002
+ // tools 还没就绪:挂起,等它 ACTIVE 再补注册。这里绝不能抛 —— 抛出去会把 apply
3003
+ // 后半段(包括其余 17 个工具)一起带走,那正是 issue #8 里"全没了"的样子。
3004
+ entry.deferred = true
3005
+ pendingToolRegistrations.push({ entry: entry, attempt: attempt })
3006
+ schedulePendingFlush()
3007
+ }
3008
+ return () => {
3009
+ const index = pendingToolRegistrations.findIndex((item) => item.entry === entry)
3010
+ if (index >= 0) pendingToolRegistrations.splice(index, 1)
3011
+ if (typeof entry.disposer === 'function') entry.disposer()
3012
+ }
3013
+ }
3014
+
3015
+ /** 只申请一次;ctx.inject 的回调在 tools ACTIVE 后立刻跑(本来就 ACTIVE 就当场跑)。 */
3016
+ function schedulePendingFlush() {
3017
+ if (pendingFlushRequested) return
3018
+ pendingFlushRequested = true
3019
+ try {
3020
+ ctx.inject(['tools'], () => {
3021
+ for (const item of pendingToolRegistrations.splice(0)) item.attempt()
3022
+ reportToolRegistrations(true)
3023
+ })
3024
+ } catch (error) {
3025
+ console.error('[drawai] 等待 tools 服务就绪失败:' + messageOf(error))
3026
+ }
3027
+ }
3028
+
3029
+ function reportToolRegistrations(final) {
3030
+ const registered = toolRegistrations.filter((item) => item.ok).map((item) => item.name)
3031
+ const failed = toolRegistrations.filter((item) => !item.ok && !item.deferred)
3032
+ const waiting = toolRegistrations.filter((item) => item.deferred).length
3033
+ if (waiting > 0 && final !== true) {
3034
+ console.error('[drawai] ' + registered.length + ' 个 host 工具已注册,' + waiting + ' 个等待 tools 服务就绪')
3035
+ return
3036
+ }
3037
+ if (toolReportLogged) return
3038
+ toolReportLogged = true
3039
+ if (failed.length === 0) console.error('[drawai] 已注册 ' + registered.length + ' 个 host 工具:' + registered.join('、'))
3040
+ else console.error('[drawai] host 工具注册 ' + registered.length + '/' + toolRegistrations.length + ' 成功,失败 ' + failed.length + ' 个:' + failed.map((item) => item.name + '(' + item.error + ')').join(';'))
3041
+ }
3042
+
3043
+ // 注册成功、模型侧却看不见时,这条日志是"插件被卸载 / 上下文被回收"唯一的线索。
3044
+ ctx.effect(() => () => {
3045
+ const registered = toolRegistrations.filter((item) => item.ok).length
3046
+ if (registered > 0) console.error('[drawai] 插件上下文释放:' + registered + ' 个已注册 host 工具随之注销')
3047
+ })
3048
+
2871
3049
  function sessionOf(sessionId) {
2872
3050
  try {
2873
3051
  if (typeof sessionId !== 'string' || sessionId.length === 0) return undefined
@@ -3053,7 +3231,9 @@ export function apply(ctx) {
3053
3231
  type: 'boolean',
3054
3232
  description:
3055
3233
  '**看一眼这张画布的渲染结果**(正交路由、避让、圆角、文字断线这些只有看图才知道的东西)。' +
3056
- '渲染在画布那一半做,所以第一次调用通常回 look.status="pending"(已请求,画布几秒内渲染完),**再调一次**就会带上图片块。' +
3234
+ '渲染在浏览器那一半做,只有用户**在右侧栏打开着这张画布**时才有人接:没打开会回 look.status="not-open"(别重试);' +
3235
+ '打开了第一次调用回 look.status="pending"(已请求,带 waitedMs),**等几秒再调一次**就会带上图片块;' +
3236
+ '挂着的请求 25 秒没人接就变成 look.status="timeout"(带已等待秒数),别傻等。' +
3057
3237
  '图片经附件通道直接进上下文 —— **不会在你的工作区里生成任何文件**。' +
3058
3238
  '只在确实需要"看"的时候用(判断重叠/压线/观感),纯粹改图不需要它。',
3059
3239
  },
@@ -3103,7 +3283,8 @@ export function apply(ctx) {
3103
3283
  error: { type: 'string' },
3104
3284
  },
3105
3285
  },
3106
- // 「看一眼」的状态(只有传了 render:true 才有):pending / ready / unsupported / failed。
3286
+ // 「看一眼」的状态(只有传了 render:true 才有):
3287
+ // pending / ready / unsupported / failed / not-open / timeout。
3107
3288
  // 图片本身不走 schema,而是走 render 的输出块(见下面的 render())。
3108
3289
  look: {
3109
3290
  type: 'object',
@@ -3112,6 +3293,8 @@ export function apply(ctx) {
3112
3293
  status: { type: 'string', required: true },
3113
3294
  stale: { type: 'boolean' },
3114
3295
  error: { type: 'string' },
3296
+ // 已经等了多久(毫秒):pending 时是"已挂请求多久",timeout 时是"没人接多久"。
3297
+ waitedMs: { type: 'number' },
3115
3298
  },
3116
3299
  },
3117
3300
  // 这张图的附件引用(有图才有)。render() 用它拼出 `{type:'image', attachment}` 块。
@@ -3328,11 +3511,21 @@ export function apply(ctx) {
3328
3511
  if (value.look !== undefined && value.look !== null && typeof value.look.status === 'string') {
3329
3512
  const look = value.look
3330
3513
  if (look.status === 'pending') {
3331
- lines.push(' 👁 已请求渲染这张画布:画布几秒内渲好,**再调一次 diagram_read(render:true)**就能看到图')
3514
+ const waited = Number(look.waitedMs)
3515
+ lines.push(
3516
+ ' 👁 已请求渲染这张画布' + (Number.isFinite(waited) && waited > 0 ? '(已等 ' + Math.round(waited / 1000) + ' 秒)' : '') +
3517
+ ':画布几秒内渲好,**再调一次 diagram_read(render:true)**就能看到图',
3518
+ )
3332
3519
  } else if (look.status === 'ready') {
3333
3520
  lines.push(' 👁 渲染结果在图块里' + (look.stale === true ? '(**注意:这是你请求渲染时的样子,之后文件又被改过**)' : ''))
3334
3521
  } else if (look.status === 'unsupported') {
3335
3522
  lines.push(' 👁 看不了图:' + String(look.error === undefined ? '这个部署没有可用的图片通道' : look.error))
3523
+ } else if (look.status === 'not-open' || look.status === 'timeout') {
3524
+ // 这两种都不是"渲坏了",而是"根本没人在渲":别让模型以为再等等就好了。
3525
+ lines.push(
3526
+ ' 👁 ' + (look.status === 'not-open' ? '画布没打开,图不会渲出来:' : '渲染请求没人接(超时):') +
3527
+ String(look.error === undefined ? '请先在右侧栏打开这张画布' : look.error),
3528
+ )
3336
3529
  } else {
3337
3530
  lines.push(' 👁 渲染失败:' + String(look.error === undefined ? '未知原因' : look.error))
3338
3531
  }
@@ -3502,8 +3695,9 @@ export function apply(ctx) {
3502
3695
  const exportState = exportStateFor(sessionIdOf(exec), loaded.absolute)
3503
3696
  // 「看一眼」(render:true):交图,或挂一个渲染请求。
3504
3697
  //
3505
- // 三道门依次过:附件服务在不在(图片的落脚处)→ 模型声明不声明图片输入 → 手上有没有渲好的图。
3506
- // 前两道过不去时**给一句人话**(而不是抛错)—— AI 还能照常读几何、改图,只是看不到图。
3698
+ // 四道门依次过:附件服务在不在(图片的落脚处)→ 模型声明不声明图片输入 → 手上有没有渲好的图
3699
+ // → 画布是不是真开着(渲染在浏览器里做,没打开就没人接请求)。
3700
+ // 过不去时**给一句人话**(而不是抛错)—— AI 还能照常读几何、改图,只是看不到图。
3507
3701
  let lookState = undefined
3508
3702
  let imageValue = undefined
3509
3703
  if (args.render === true) {
@@ -3526,18 +3720,45 @@ export function apply(ctx) {
3526
3720
  imageValue = taken.image
3527
3721
  }
3528
3722
  } else {
3529
- // 同一张画布已有请求就沿用(客户端按 requestId 去重,换 id 会让它再渲一遍)。
3530
- const existing = renderRequests.get(sessionIdOf(exec))
3531
- if (existing === undefined || existing.absolute !== loaded.absolute) {
3532
- // 换了画布:上一张渲好的图作废(否则会把 A 图的画面当 B 图交给模型)。
3533
- renderedImages.delete(sessionIdOf(exec))
3534
- renderRequests.set(sessionIdOf(exec), {
3535
- requestId: 'r' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8),
3536
- absolute: loaded.absolute,
3537
- revision: doc.revision,
3538
- })
3723
+ // 渲染在**浏览器**里做:只有用户正打开着这张画布时客户端才会接这个请求。
3724
+ // 宿主知道用户看的是**别的**画布时就直接说清楚(not-open)—— 否则会挂一个
3725
+ // 永远等不到回执的请求,模型只能反复重试(issue #12)。聚焦未知时仍按老规矩挂
3726
+ // pending:刚打开的画布可能还没来得及上报,误判成"没开"会更让人困惑。
3727
+ const focusedAbsolute = await focusedAbsoluteOf(sessionIdOf(exec))
3728
+ if (focusedAbsolute !== undefined && samePath(focusedAbsolute, loaded.absolute) === false) {
3729
+ lookState = {
3730
+ status: 'not-open',
3731
+ error:
3732
+ '看不了这张画布的渲染图:右侧栏现在打开的是 ' + focusedAbsolute +
3733
+ ',渲染请求没有画布接。先把这张画布打开再调一次;只要几何数据的话去掉 render:true。',
3734
+ }
3735
+ } else if (state !== undefined && state.status === 'timeout') {
3736
+ lookState = {
3737
+ status: 'timeout',
3738
+ waitedMs: state.waitedMs,
3739
+ error:
3740
+ '渲染请求挂了 ' + Math.round(state.waitedMs / 1000) + ' 秒没人接:多半是右侧栏没打开这张画布(或画布与宿主的连接断了)。' +
3741
+ '先打开它再调一次;只想要几何数据的话去掉 render:true。',
3742
+ }
3743
+ } else {
3744
+ // 同一张画布已有请求就沿用(客户端按 requestId 去重,换 id 会让它再渲一遍)。
3745
+ const existing = renderRequests.get(sessionIdOf(exec))
3746
+ if (existing === undefined || existing.absolute !== loaded.absolute) {
3747
+ // 换了画布:上一张渲好的图作废(否则会把 A 图的画面当 B 图交给模型)。
3748
+ renderedImages.delete(sessionIdOf(exec))
3749
+ renderRequests.set(sessionIdOf(exec), {
3750
+ requestId: 'r' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8),
3751
+ absolute: loaded.absolute,
3752
+ revision: doc.revision,
3753
+ // 请求挂上的时刻:UI 那半边一直不回执时,靠它算"已经等了多久"(见 renderStateFor 的超时)。
3754
+ at: Date.now(),
3755
+ })
3756
+ }
3757
+ lookState = {
3758
+ status: 'pending',
3759
+ waitedMs: state !== undefined && state.status === 'pending' ? state.waitedMs : 0,
3760
+ }
3539
3761
  }
3540
- lookState = { status: 'pending' }
3541
3762
  }
3542
3763
  }
3543
3764
  }
@@ -4023,7 +4244,11 @@ export function apply(ctx) {
4023
4244
  // mode 回落到 'dagre-tb',人手工摆好的版面被整张重排 —— 正是上面那个判断要防的事。
4024
4245
  // 教训:用整体赋值覆盖 meta 时,"没被显式处理"的字段会静默消失;新增 meta 字段时先看这里。
4025
4246
  const routingResult = documentRouter.update(routingBefore, doc)
4026
- if (routingResult.failed.length) notes.push('~ 自动避障暂无可用通道:' + routingResult.failed.join(', '))
4247
+ if (routingResult.unavailable !== undefined) {
4248
+ // 引擎整体不可用(最常见:从 git 装的包少了 lib/routing/vendor/*)时,写回照常进行,
4249
+ // 只是这次没有自动路线。回执里必须说清楚,否则模型会以为"改完就是没走线"。
4250
+ notes.push('~ 自动避障不可用(' + routingResult.unavailable + '):这次保留现有折点,新画的边按直线写回')
4251
+ } else if (routingResult.failed.length) notes.push('~ 自动避障暂无可用通道:' + routingResult.failed.join(', '))
4027
4252
  doc.meta = Object.assign({}, doc.meta, { layout: mode }, pinned ? { pinned: true } : {})
4028
4253
  const rendered = renderDocText(loaded.text, doc, { page: loaded.pageIndex, format: isNativeCanvasPath(loaded.absolute) ? 'drawai' : 'drawio' })
4029
4254
  doc.revision = rendered.revision
@@ -4143,6 +4368,14 @@ function sanitizeNewName(raw) {
4143
4368
  const renderedImages = new Map()
4144
4369
  /** 渲染结果的保鲜期:超过它就不再交付(要新的请重新请求一次,免得给模型看一张老图)。 */
4145
4370
  const RENDER_TTL_MS = 5 * 60 * 1000
4371
+ /**
4372
+ * 挂着的渲染请求多久没人接就算超时。
4373
+ *
4374
+ * 渲染在浏览器里做:用户没打开画布时这条请求**根本没人接**,而旧代码只会一直回 pending
4375
+ * (issue #12:请求没有过期,模型分不清"正在渲"和"没人渲")。25 秒足够大画布渲完,
4376
+ * 超过它就该如实说"没人接"而不是让模型反复重试。
4377
+ */
4378
+ const RENDER_PENDING_TTL_MS = 25 * 1000
4146
4379
 
4147
4380
  /** 附件服务(可选依赖):这个部署没装它就没法"看图",但插件其余功能照旧。 */
4148
4381
  function attachmentsService() {
@@ -4196,13 +4429,21 @@ function sanitizeNewName(raw) {
4196
4429
  /**
4197
4430
  * 「看图」这条路当前的可用性 + 待交付的图。
4198
4431
  *
4199
- * 状态:`pending`(已请求,等画布渲染)/ `ready`(有图,这一次带回模型)/
4432
+ * 状态:`pending`(已请求,等画布渲染,带 waitedMs)/ `ready`(有图,这一次带回模型)/
4433
+ * `timeout`(挂了超过 RENDER_PENDING_TTL_MS 没人接,带 waitedMs —— 多半是画布没打开)/
4200
4434
  * `unsupported`(这个部署/模型看不了图,附一句为什么)/ `failed`(渲染回执说失败了)。
4201
4435
  */
4202
4436
  function renderStateFor(sessionId, absolute, revision) {
4203
4437
  if (typeof sessionId !== 'string' || sessionId.length === 0) return undefined
4204
4438
  const pending = renderRequests.get(sessionId)
4205
- if (pending !== undefined && pending.absolute === absolute) return { status: 'pending' }
4439
+ if (pending !== undefined && samePath(pending.absolute, absolute)) {
4440
+ const since = Number(pending.at)
4441
+ const waitedMs = Number.isFinite(since) ? Math.max(0, Date.now() - since) : 0
4442
+ // 不删这条请求:迟到一步的回执还要按 requestId 配回来(删了就永远配不上)。
4443
+ // 只是不再说"pending"——模型至少知道"没人接",而不是"正在渲"。
4444
+ if (waitedMs > RENDER_PENDING_TTL_MS) return { status: 'timeout', waitedMs: waitedMs }
4445
+ return { status: 'pending', waitedMs: waitedMs }
4446
+ }
4206
4447
  const done = renderedImages.get(sessionId)
4207
4448
  if (done === undefined || done.absolute !== absolute) return undefined
4208
4449
  if (Date.now() - done.at > RENDER_TTL_MS) {
@@ -4351,6 +4592,33 @@ function sanitizeNewName(raw) {
4351
4592
  return focusedCanvas.get(sessionId)
4352
4593
  }
4353
4594
 
4595
+ /** 两个绝对路径是不是同一个文件(Windows 大小写不敏感、斜杠两种都认)。 */
4596
+ function samePath(a, b) {
4597
+ if (a === b) return true
4598
+ if (typeof a !== 'string' || typeof b !== 'string') return false
4599
+ if (process.platform !== 'win32') return false
4600
+ return a.replace(/\//g, '\\').toLowerCase() === b.replace(/\//g, '\\').toLowerCase()
4601
+ }
4602
+
4603
+ /**
4604
+ * 用户此刻打开的那张画布的**绝对路径**(归一后);没上报过 / 解析不出来 → undefined。
4605
+ *
4606
+ * 只给「看图」的门禁用(diagram_read 的 render:true 分支):渲染发生在浏览器里,只有**用户正打开着**
4607
+ * 这张画布时客户端才接请求,否则请求永远挂着(issue #12)。聚焦路径是客户端上报的、
4608
+ * 可能是相对工作区的,所以这里统一 resolve + processPath 成与 `loaded.absolute` 同一套形状。
4609
+ */
4610
+ async function focusedAbsoluteOf(sessionId) {
4611
+ const raw = focusedPathFor(sessionId)
4612
+ if (raw === undefined || raw.length === 0) return undefined
4613
+ const root = workspaceRootOf(sessionId)
4614
+ if (root === undefined) return undefined
4615
+ try {
4616
+ return ctx.fs.processPath(await ctx.fs.resolve(raw, { cwd: root }))
4617
+ } catch (error) {
4618
+ return undefined
4619
+ }
4620
+ }
4621
+
4354
4622
  /** 用户画布当前页(0 起);没上报过则 0。 */
4355
4623
  function focusedPageFor(sessionId) {
4356
4624
  if (typeof sessionId !== 'string' || sessionId.length === 0) return 0
@@ -4917,8 +5185,10 @@ function sanitizeNewName(raw) {
4917
5185
  sendJson(res, 400, { ok: false, error: 'resolve failed: ' + messageOf(error) })
4918
5186
  return
4919
5187
  }
5188
+ // rootTarget 两个 try 都要用(包含校验 + 响应里的 root 字段),声明放到 if 块这一层。
5189
+ let rootTarget
4920
5190
  try {
4921
- const rootTarget = await ctx.fs.resolve(root)
5191
+ rootTarget = await ctx.fs.resolve(root)
4922
5192
  if (!ctx.fs.contains(rootTarget, dirTarget)) {
4923
5193
  sendJson(res, 403, { ok: false, error: '目录不在工作区内:' + absoluteHint(dirTarget, dir) })
4924
5194
  return
@@ -4932,15 +5202,18 @@ function sanitizeNewName(raw) {
4932
5202
  // 同时给出**绝对路径**:客户端按路径去重(同一个文件不该开出两个标签),
4933
5203
  // 而标签上的路径可能来自 tab 的资源地址(绝对),与列表里的相对路径不相等。
4934
5204
  // 让客户端用绝对路径当"身份",就不会重复开标签。
5205
+ // listed.files 里的相对路径是**相对 dir** 的,必须拼回 dir —— 拼到工作区根会指到
5206
+ // 一个不存在的同名文件(issue #19:子目录里的画布「列得出、打不开」)。
5207
+ const dirAbs = absoluteHint(dirTarget, dir)
4935
5208
  const abs = []
4936
- for (let i = 0; i < listed.files.length; i += 1) abs.push(toAbsolute(listed.files[i], root))
5209
+ for (let i = 0; i < listed.files.length; i += 1) abs.push(toAbsolute(listed.files[i], dirAbs))
4937
5210
  sendJson(res, 200, {
4938
5211
  ok: true,
4939
5212
  files: listed.files,
4940
5213
  absolute: abs,
4941
5214
  notes: listed.notes,
4942
- root: absoluteHint(root, root),
4943
- dir: absoluteHint(dirTarget, dir),
5215
+ root: absoluteHint(rootTarget, root),
5216
+ dir: dirAbs,
4944
5217
  })
4945
5218
  } catch (error) {
4946
5219
  sendJson(res, 500, { ok: false, error: 'list failed: ' + messageOf(error) })
@@ -4956,7 +5229,7 @@ function sanitizeNewName(raw) {
4956
5229
  files: listed.files,
4957
5230
  absolute: abs,
4958
5231
  notes: listed.notes,
4959
- root: absoluteHint(root, root),
5232
+ root: absoluteHint(await ctx.fs.resolve(root), root),
4960
5233
  })
4961
5234
  } catch (error) {
4962
5235
  sendJson(res, 500, { ok: false, error: 'list failed: ' + messageOf(error) })
@@ -6189,9 +6462,27 @@ function sanitizeNewName(raw) {
6189
6462
  sendJson(res, 200, { ok: true, revision: rendered.revision, path: absolute, dropped: rendered.dropped, ...(backgroundPatch ? { backgroundPatch } : {}) })
6190
6463
  }
6191
6464
 
6465
+ /**
6466
+ * 只读诊断:不碰画布、不改文件,只回答"这个进程里到底注册上了哪几个 host 工具"。
6467
+ * issue #8 那种"画布能用、工具全没"的现场,把这个 URL 的 JSON 贴回来就够了。
6468
+ */
6469
+ function handleDiagnostics(_req, res) {
6470
+ sendJson(res, 200, {
6471
+ ok: true,
6472
+ plugin: 'dsh-drawai',
6473
+ toolsService: toolsService() !== undefined,
6474
+ registered: toolRegistrations.filter((item) => item.ok).map((item) => item.name),
6475
+ pending: toolRegistrations.filter((item) => item.deferred).map((item) => item.name),
6476
+ failed: toolRegistrations.filter((item) => !item.ok && !item.deferred).map((item) => ({ name: item.name, error: item.error })),
6477
+ })
6478
+ }
6479
+
6480
+ // 诊断路由排在写回路由之前:自测按 routes[last] 取写回路由,这个顺序不能反过来。
6481
+ ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: DIAGNOSTICS_PATH, handler: handleDiagnostics }), 'drawai: ' + DIAGNOSTICS_PATH)
6482
+
6192
6483
  ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: SAVE_PATH, handler: handleSave }), 'drawai: ' + SAVE_PATH)
6193
6484
 
6194
- ctx.effect(() => ctx.tools.register(defineTool({
6485
+ ctx.effect(() => registerHostTool(defineTool({
6195
6486
  name: 'shotcraft_search',
6196
6487
  description: '搜索 DrawAI 内置的 124 张 Shotcraft 镜头卡 / 150 个准确样式。按关键词(中英文卡名、样式、用途)和分类检索;返回唯一 demo、样片及可用于 diagram_apply 的节点参数。先搜索再添加 shape:shotcard 节点并连到视频节点,支持同时参考多卡。',
6197
6488
  parameters: { query: { type: 'string' }, category: { type: 'string' }, cardId: { type: 'string', description: '精确卡名,查看该卡所有样式;配合 styleKey 会直接返回这张卡的配方与 demo 源码' },
@@ -6218,7 +6509,7 @@ function sanitizeNewName(raw) {
6218
6509
  styles: matches.slice(offset, offset + limit).map((item) => ({ ...withPath(item), node: { op: 'addNode', shape: 'shotcard', cardId: item.cardId, styleKey: item.styleKey } })) }) }
6219
6510
  },
6220
6511
  })))
6221
- ctx.effect(() => ctx.tools.register(defineTool({
6512
+ ctx.effect(() => registerHostTool(defineTool({
6222
6513
  name: 'mg_style',
6223
6514
  description: 'DrawAI 内置的 14 种整片 MG 动效风格(mg-styles 库):扁平矢量 / 线条动画 / 等轴 2.5D / 逐帧手绘 / 拼贴 / 液态 / 形变 / 包豪斯 / 80s 复古 / 玻璃拟态 / 综艺花字 / 粗描边贴纸科普 / 像素 / 赛博 HUD。上游那支 04-3d-render(C4D/Blender 质感的 CGI 软着陆)没有收录:画面是 Blender 离线渲的,插件里没有同等质感的等价物——用 CSS 滤镜凑出来的 CGI 只会更假,需要那种质感就外部渲好当素材回灌(motion_assets capture / 参考段)。这是「整片风格」(一种片子的整体视觉语言),与 shotcraft_search 的「单镜动效卡」互补、可同时用。action:"list" 列出全部;"search" 按关键词检索;"read" 读某个风格的施工包(可检验判据 + 色板 + 字体 + 节奏 + 效果配方 + 上游提示词 + Remotion 移植 brief);"source" 读上游成片源码正文;**"frames" 从上游 demo 按 cue 时间点抽参考帧**——动态、转场、冲击、颗粒这些只有看帧才知道,写场景前先取。选中后 diagram_apply 加 shape:"mgstyle" 节点并连到视频节点,motion_project 会把风格写进工程主题与 STYLE.md。',
6224
6515
  parameters: { action: { type: 'string', enum: ['list', 'search', 'read', 'source', 'frames'], required: true },
@@ -6283,10 +6574,10 @@ function sanitizeNewName(raw) {
6283
6574
  throw new Error('未知 action:' + action)
6284
6575
  },
6285
6576
  })))
6286
- ctx.effect(() => ctx.tools.register(defineTool({
6577
+ ctx.effect(() => registerHostTool(defineTool({
6287
6578
  name:'audio_asset',
6288
6579
  description:'为画布搜索和导入音频。search 从 Freesound 搜索;get 按 fs:数字 下载并创建音频节点;import 从当前工作区导入 mp3/wav/m4a/aac/ogg/flac/webm,32MB/10分钟以内;targetId 可自动连接视频。configure 修改音频节点的 audioRole(sfx/bgm/voice)、audioGainDb、audioTrimStart/End(秒,End=0至末尾)、audioAt(全片秒)、audioFadeIn/Out(秒)、audioLoop(字符串0/1)。inspect 返回素材来源、哈希和检测结果。音频需要连接到视频;motion_task settings.audioMode=auto让AI安排镜头声音,manual使用节点参数或audioPlan,off静音;remix只重混已有成片。没有真实试听不得宣称听感通过。',
6289
- parameters:{action:{type:'string',enum:['search','get','import','configure','inspect'],required:true},path:{type:'string'},page:{type:'number'},query:{type:'string'},limit:{type:'number'},id:{type:'string'},file:{type:'string'},nodeId:{type:'string'},targetId:{type:'string'},role:{type:'string',enum:['sfx','bgm','voice']},settings:{type:'json'},x:{type:'number'},y:{type:'number'}},
6580
+ parameters:{action:{type:'string',enum:['search','get','import','configure','inspect'],required:true},path:{type:'string'},page:{type:'number'},query:{type:'string'},limit:{type:'number'},id:{type:'string'},file:{type:'string'},nodeId:{type:'string'},targetId:{type:'string'},role:{type:'string',enum:['sfx','bgm','voice']},settings:{type:'object',additionalProperties:false,properties:{audioRole:{type:'string',enum:['sfx','bgm','voice']},audioGainDb:{type:'number'},audioTrimStart:{type:'number'},audioTrimEnd:{type:'number'},audioAt:{type:'number'},audioFadeIn:{type:'number'},audioFadeOut:{type:'number'},audioLoop:{type:'string',enum:['0','1']}},description:'configure:要改的音频设置,键名就是节点 data 里的名字(audioRole / audioGainDb / audioTrimStart…)'},x:{type:'number'},y:{type:'number'}},
6290
6581
  output:{schema:{type:'object',additionalProperties:false,properties:{summary:{type:'string',required:true},details:{type:'string',required:true}}},render:(_args,value)=>[{type:'text',text:value.summary+'\n'+value.details}]},
6291
6582
  async execute(args,exec){
6292
6583
  const sessionId=sessionIdOf(exec),path=args.path||focusedPathFor(sessionId)
@@ -6330,8 +6621,8 @@ function sanitizeNewName(raw) {
6330
6621
  })
6331
6622
  }
6332
6623
  })))
6333
- ctx.effect(() => ctx.tools.register(readTool))
6334
- ctx.effect(() => ctx.tools.register(defineTool({
6624
+ ctx.effect(() => registerHostTool(readTool))
6625
+ ctx.effect(() => registerHostTool(defineTool({
6335
6626
  name: 'motion_task',
6336
6627
  description: '画布视频节点的任务记录:list 列出任务、status 查某个任务、cancel 取消、focus 在面板定位、remove 移除记录。开始制作请走 motion_project → 改 scene-*.tsx → motion_preview → motion_render;成片回填节点用 motion_render apply。',
6337
6628
  parameters: {
@@ -6505,7 +6796,7 @@ function sanitizeNewName(raw) {
6505
6796
  }
6506
6797
  }
6507
6798
 
6508
- ctx.effect(() => ctx.tools.register(defineTool({
6799
+ ctx.effect(() => registerHostTool(defineTool({
6509
6800
  name: 'motion_project',
6510
6801
  description: '视频工程:把画布里的一个视频节点落成磁盘上的小文件工程(timeline.json + scene-<n>.tsx + assets/ + theme.ts)。create 建工程并返回文件清单与分镜表,之后用 read/edit/write 直接改场景文件;open 重新读时间轴、文件清单和主题;theme 换配色(重写 theme.ts,场景里用它的常量就一起变)。这里不启动任何模型,也不渲染。按用户要求确定风格和素材;除非用户要求跳过,先制作 3–6 秒样片并回填到独立的样片视频节点,用户要求先审样时等待反馈。create 前镜头卡必须 addEdge 连到视频节点:连线同时是参考、镜头顺序和段长,画布上有断连的卡会直接报错。',
6511
6802
  parameters: {
@@ -6513,12 +6804,29 @@ function sanitizeNewName(raw) {
6513
6804
  path: { type: 'string', description: '画布路径,省略时使用当前画布' },
6514
6805
  nodeId: { type: 'string', required: true, description: '视频节点 id' },
6515
6806
  page: { type: 'number' },
6516
- spec: { type: 'json', description: '视频规格 {width,height,fps,duration},省略时沿用节点设置' },
6807
+ spec: {
6808
+ type: 'object',
6809
+ additionalProperties: true,
6810
+ properties: { width: { type: 'number' }, height: { type: 'number' }, fps: { type: 'number' }, duration: { type: 'number' } },
6811
+ description: '视频规格 {width,height,fps,duration}(像素与秒),省略时沿用节点设置',
6812
+ },
6517
6813
  prompt: { type: 'string', description: '这条视频要什么,会写进 BRIEF.md' },
6518
6814
  segments: { type: 'number', description: '初始分几段,默认 4' },
6519
- storyboard: { type: 'json', description: '分镜表:[{intent:"这一镜要做什么", onScreen:"画面上出现什么", copy:"配字"}],按镜头顺序;也可以给 ["一句话"] 简写。会写成 storyboard.md。' },
6815
+ storyboard: {
6816
+ type: 'array',
6817
+ items: { type: 'json' },
6818
+ description: '分镜表:[{intent:"这一镜要做什么", onScreen:"画面上出现什么", copy:"配字"}],按镜头顺序;也可以给 ["一句话"] 简写。会写成 storyboard.md。',
6819
+ },
6520
6820
  theme: { type: 'string', description: '配色主题名:vintage-kraft / deep-ocean / obsidian-violet / modern-light / midnight / solar-pop / coral-burst / color-play / paper-ink。给了就在工程里生成 theme.ts,场景用 {{PAPER, INK, DUST, STAMP}} 引用。' },
6521
- themeTokens: { type: 'json', description: '直接给一套色 {page,text,muted,accent,surface,field,border}(优先于 theme)。' },
6821
+ themeTokens: {
6822
+ type: 'object',
6823
+ additionalProperties: true,
6824
+ properties: {
6825
+ page: { type: 'string' }, text: { type: 'string' }, muted: { type: 'string' }, accent: { type: 'string' },
6826
+ surface: { type: 'string' }, field: { type: 'string' }, border: { type: 'string' },
6827
+ },
6828
+ description: '直接给一套色 {page,text,muted,accent,surface,field,border}(#RRGGBB,优先于 theme;也认 paper/card/ink 这些别名)。',
6829
+ },
6522
6830
  style: { type: 'string', description: '整片风格 slug(mg_style list 里的 slug,如 09-bauhaus)。给了就用这个风格:色板当主题、节奏当规格缺省、施工说明与引擎契约写进 STYLE.md。也可以不传,改为在画布上连一个「整片风格」节点。' },
6523
6831
  allowUnlinkedCards: { type: 'boolean', description: 'create 时允许画布上存在没连线的镜头卡(当作灵感,不进 assets/timeline)。默认不允许:断连的卡会同时漏掉参考与排期,所以直接报错,让你补 addEdge。' },
6524
6832
  },
@@ -6532,10 +6840,15 @@ function sanitizeNewName(raw) {
6532
6840
  const node = loaded.doc.nodes.find((item) => item.id === args.nodeId)
6533
6841
  if (!node) throw new Error('找不到视频节点 ' + args.nodeId)
6534
6842
  const dir = projectDirOf(node, scope)
6843
+ // json 参数有可能是字符串(见 jsonValue 注释),先统一还原,后面只读这几个局部量。
6844
+ const specArg = jsonObject(args.spec)
6845
+ const styleArg = jsonValue(args.style)
6846
+ const storyboardArg = jsonValue(args.storyboard)
6847
+ const themeTokensArg = jsonValue(args.themeTokens)
6535
6848
  if (args.action === 'theme') {
6536
6849
  if (policyFor(scope.sessionId)?.mode === 'read-only') throw new Error('当前会话为只读')
6537
6850
  if (!existsSync(dir)) throw new Error('工程还没建:先 action:"create"')
6538
- const resolved = resolveTheme(args.themeTokens || args.theme)
6851
+ const resolved = resolveTheme(themeTokensArg || args.theme)
6539
6852
  if (!resolved) throw new Error('请给 theme(主题名)或 themeTokens(一套色)。可选主题:' + listThemes().map((item) => item.name).join('、'))
6540
6853
  const written = writeTheme(dir, resolved)
6541
6854
  return { summary: '已换主题:' + written.name, details: JSON.stringify({ dir, theme: written, themes: listThemes() }) }
@@ -6547,10 +6860,10 @@ function sanitizeNewName(raw) {
6547
6860
  }
6548
6861
  if (!isMotionNode(node)) throw new Error('请选择空视频或绘制视频节点')
6549
6862
  const before = { data: JSON.parse(JSON.stringify(node.data)), style: node.style }
6550
- const style = args.style ? resolveMgStyle(mgStyleCatalog, args.style) : mgStyleOf(loaded.doc, node.id)
6863
+ const style = styleArg ? resolveMgStyle(mgStyleCatalog, styleArg) : mgStyleOf(loaded.doc, node.id)
6551
6864
  const kit = style ? mgStyleKit({ slug: style.slug }) : null
6552
6865
  // 规格优先级:风格节奏(最弱)< 节点上存的 < 这次显式给的
6553
- const spec = readSpec({ ...rhythmSpec(style?.rhythm), ...(node.data.motion || {}), ...(args.spec || {}) })
6866
+ const spec = readSpec({ ...rhythmSpec(style?.rhythm), ...(node.data.motion || {}), ...specArg })
6554
6867
  const errors = validateSpec(spec)
6555
6868
  if (errors.length) throw new Error('视频规格不合法:' + errors.join(';'))
6556
6869
  const cards = shotCardsOf(loaded.doc, node.id)
@@ -6566,14 +6879,14 @@ function sanitizeNewName(raw) {
6566
6879
  // 这也是"分镜被拆成八条文本节点"能被发现的地方 —— readCanvasSpec 会把它们列出来。
6567
6880
  const canvasSpec = readCanvasSpec(loaded.doc, { fps: spec.fps })
6568
6881
  const storyboardInput = args.storyboard !== undefined
6569
- ? args.storyboard
6882
+ ? storyboardArg
6570
6883
  : canvasSpec.storyboard.length > 0 ? canvasSpec.storyboard : undefined
6571
6884
  // 显式给了时长就以时长为总长:卡片之间按相对时长等比拉伸,不再被卡片总和改短。
6572
- const durationGiven = Number(args.spec?.duration) > 0
6885
+ const durationGiven = Number(specArg.duration) > 0
6573
6886
  // 建工程要往 assets/fonts 里拷字体:先确保素材包在(首次会下载一次)。
6574
6887
  await ensureMotionAssets('建视频工程')
6575
6888
  const created = createProject(dir, { spec, prompt: args.prompt ?? node.data.motion?.prompt, segments: args.segments, cards, storyboard: storyboardInput,
6576
- theme: args.themeTokens || args.theme, style: kit, fitFrames: durationGiven ? spec.totalFrames : 0 })
6889
+ theme: themeTokensArg || args.theme, style: kit, fitFrames: durationGiven ? spec.totalFrames : 0 })
6577
6890
  registerProject(dir)
6578
6891
  const seconds = created.timeline.durationInFrames / created.timeline.fps
6579
6892
  node.data.motion = { ...(node.data.motion || {}), mode: spec.mode, width: created.timeline.width, height: created.timeline.height, fps: created.timeline.fps, duration: Math.round(seconds * 100) / 100,
@@ -6624,7 +6937,7 @@ function sanitizeNewName(raw) {
6624
6937
  // 以前这三块只活在对话里(或 storyboard.md 里),于是"分镜表"在画布上就变成
6625
6938
  // **一镜一个文本节点**——八镜八条,谁也不觉得那是一张表,motion_project 也读不到。
6626
6939
  // 这里给一个工具:建(scaffold)、读(read)、验(validate)。
6627
- ctx.effect(() => ctx.tools.register(defineTool({
6940
+ ctx.effect(() => registerHostTool(defineTool({
6628
6941
  name: 'motion_spec',
6629
6942
  description: '画布上的视频规格三块,用**一个节点**写清楚:\n【分镜表】一张 Markdown 六列表(镜头 | 帧区间 | 镜头卡 | 这一镜要做什么 | 屏幕上出现什么 | 文案 / 声音),每一镜一行——**不要一镜建一个节点**;\n【风格】一行一个键(page #F7F3EA / serif "Noto Serif SC" / materials: kraft);\n【旁白】`## shot-N` 分节,一句一行。\naction:"scaffold" 返回把缺的块摆到视频附近空位的 diagram_apply ops(已有一张【分镜表】就不再建第二张);"read" 读回来;"validate" 逐条报错(分镜被拆成多条、帧数之和对不上节点时长、镜号不连号…)。motion_project create 不给 storyboard 时会直接读这张表。',
6630
6943
  parameters: {
@@ -6632,10 +6945,21 @@ function sanitizeNewName(raw) {
6632
6945
  path: { type: 'string', description: '画布路径,省略时使用当前画布' },
6633
6946
  nodeId: { type: 'string', description: '视频节点 id(规格块的锚点)' },
6634
6947
  page: { type: 'number' },
6635
- storyboard: { type: 'json', description: '分镜行 [{intent,onScreen,copy,card,durationFrames}],或一段 markdown 表;省略时 scaffold 只出表头' },
6636
- style: { type: 'json', description: '风格:一段文本,或 {palette,fonts,materials}' },
6948
+ storyboard: {
6949
+ oneOf: [{ type: 'array', items: { type: 'json' } }, { type: 'string' }],
6950
+ description: '分镜行 [{intent,onScreen,copy,card,durationFrames}],或一段 markdown 表;省略时 scaffold 只出表头',
6951
+ },
6952
+ style: {
6953
+ oneOf: [{ type: 'string' }, { type: 'object', additionalProperties: true }],
6954
+ description: '风格:一段文本,或 {palette,fonts,materials}',
6955
+ },
6637
6956
  narration: { type: 'string', description: '旁白脚本(## shot-N 分节,一句一行)' },
6638
- spec: { type: 'json', description: '视频规格 {width,height,fps,duration},省略时读节点上的' },
6957
+ spec: {
6958
+ type: 'object',
6959
+ additionalProperties: true,
6960
+ properties: { width: { type: 'number' }, height: { type: 'number' }, fps: { type: 'number' }, duration: { type: 'number' } },
6961
+ description: '视频规格 {width,height,fps,duration}(像素与秒),省略时读节点上的',
6962
+ },
6639
6963
  },
6640
6964
  output: { schema: { type: 'object', additionalProperties: false, properties: { summary: { type: 'string', required: true }, details: { type: 'string', required: true } } },
6641
6965
  render: (_args, value) => [{ type: 'text', text: value.summary + '\n' + value.details }] },
@@ -6644,10 +6968,10 @@ function sanitizeNewName(raw) {
6644
6968
  const loaded = await loadDoc(scope.path, scope.sessionId, args.page)
6645
6969
  const node = args.nodeId ? loaded.doc.nodes.find((item) => item.id === args.nodeId) : loaded.doc.nodes.find(isMotionNode)
6646
6970
  if (!node) throw new Error('找不到视频节点' + (args.nodeId ? ' ' + args.nodeId : ''))
6647
- const spec = readSpec({ ...(node.data?.motion || {}), ...(args.spec || {}) })
6971
+ const spec = readSpec({ ...(node.data?.motion || {}), ...jsonObject(args.spec) })
6648
6972
  if (args.action === 'scaffold') {
6649
6973
  if (policyFor(scope.sessionId)?.mode === 'read-only') throw new Error('当前会话为只读')
6650
- const built = scaffoldOps(loaded.doc, { videoNode: node, storyboard: args.storyboard, style: args.style,
6974
+ const built = scaffoldOps(loaded.doc, { videoNode: node, storyboard: jsonValue(args.storyboard), style: jsonValue(args.style),
6651
6975
  narration: args.narration, fps: spec.fps, totalFrames: spec.totalFrames })
6652
6976
  if (built.ops.length === 0) {
6653
6977
  return { summary: '规格块已经齐了(' + built.already.join('、') + '),不必再建',
@@ -6681,14 +7005,22 @@ function sanitizeNewName(raw) {
6681
7005
  },
6682
7006
  })))
6683
7007
 
6684
- ctx.effect(() => ctx.tools.register(defineTool({
7008
+ ctx.effect(() => registerHostTool(defineTool({
6685
7009
  name: 'motion_preview',
6686
7010
  description: '渲染工程里的几帧并**直接返回拼图**(同一轮就能看见画面)。第一次调用要打包(约 10–30 秒),之后同一份源码复用缓存只要几秒。frames 是绝对帧号,例如 [0,60,120,180];省略时取每段的中点(最多 6 帧),定调阶段只写 scene-0 时用它看一张定妆帧。**要看画质(锯齿、色带、字重)就传 crop:只出 1:1 原尺寸局部**,拼图是缩过的,缩过的图看不出这些毛病。',
6687
7011
  parameters: {
6688
7012
  path: { type: 'string' }, page: { type: 'number' }, nodeId: { type: 'string', required: true },
6689
- frames: { type: 'json', description: '绝对帧号数组,最多 9 帧;省略 = 每段中点' },
7013
+ frames: { type: 'array', items: { type: 'number' }, description: '绝对帧号数组,最多 9 帧;省略 = 每段中点' },
6690
7014
  cols: { type: 'number', description: '拼图列数,默认 3' },
6691
- crop: { type: 'json', description: '只出局部:{x,y,w,h} 或 {cx,cy,w,h},像素。传了就按 1:1 原尺寸返回(不缩放),用来判断 AA / 色带 / 字体' },
7015
+ crop: {
7016
+ type: 'object',
7017
+ additionalProperties: false,
7018
+ properties: {
7019
+ x: { type: 'number' }, y: { type: 'number' }, w: { type: 'number' }, h: { type: 'number' },
7020
+ cx: { type: 'number' }, cy: { type: 'number' },
7021
+ },
7022
+ description: '只出局部:{x,y,w,h} 或 {cx,cy,w,h},像素。传了就按 1:1 原尺寸返回(不缩放),用来判断 AA / 色带 / 字体',
7023
+ },
6692
7024
  },
6693
7025
  output: { schema: { type: 'object', additionalProperties: false, properties: {
6694
7026
  summary: { type: 'string', required: true },
@@ -6704,12 +7036,13 @@ function sanitizeNewName(raw) {
6704
7036
  const node = loaded.doc.nodes.find((item) => item.id === args.nodeId)
6705
7037
  if (!node) throw new Error('找不到视频节点 ' + args.nodeId)
6706
7038
  const dir = projectDirOf(node, scope)
6707
- const requested = Array.isArray(args.frames) ? args.frames.map(Number).filter((n) => Number.isFinite(n)).slice(0, 9) : []
7039
+ const requestedFrames = jsonArray(args.frames)
7040
+ const requested = requestedFrames.length ? requestedFrames.map(Number).filter((n) => Number.isFinite(n)).slice(0, 9) : []
6708
7041
  const frames = requested.length ? requested : (() => {
6709
7042
  const timeline = readTimeline(dir)
6710
7043
  return timeline.shots.slice(0, 6).map((shot) => shot.startFrame + Math.floor(shot.durationFrames * 0.6))
6711
7044
  })()
6712
- const preview = await previewProject(dir, { frames, cols: args.cols, crop: args.crop, attachments: ctx.get?.('attachments') })
7045
+ const preview = await previewProject(dir, { frames, cols: args.cols, crop: jsonValue(args.crop), attachments: ctx.get?.('attachments') })
6713
7046
  const payload = { summary: '已渲染 ' + frames.length + ' 帧并拼图' + (preview.crop ? '(1:1 局部 ' + preview.crop.w + '×' + preview.crop.h + ')' : ''), details: JSON.stringify({ frames: preview.frames, sheet: preview.sheet, crop: preview.crop }) }
6714
7047
  const ref = preview.attachment
6715
7048
  if (ref && typeof ref.attachmentId === 'string') {
@@ -6725,15 +7058,15 @@ function sanitizeNewName(raw) {
6725
7058
  },
6726
7059
  })))
6727
7060
 
6728
- ctx.effect(() => ctx.tools.register(defineTool({
7061
+ ctx.effect(() => registerHostTool(defineTool({
6729
7062
  name: 'motion_render',
6730
7063
  description: '渲染工程。start 立即返回 jobId(后台跑,不要等);**start 前会按 STYLE.md 的引擎契约扫工程源码,three / webgl2 风格缺真引擎会直接拒绝渲染**(样片也拦;确实要换实现先在 STYLE.md 写「偏离:引擎 …」);range 渲的是**样片**(写 .out/pilot.mp4,任务 kind=pilot),不传就是整片(.out/film.mp4);status 查进度;list 列最近任务;cancel 取消;apply 把渲好的片子回填到视频节点——**nodeId 决定回填到哪个节点**(样片回填到另建的「样片」节点,成片回填到主视频节点),不给 jobId 时主视频节点取整片、样片节点取样片。frames 只渲染指定帧。',
6731
7064
  parameters: {
6732
7065
  action: { type: 'string', enum: ['start', 'status', 'list', 'cancel', 'apply'], required: true },
6733
7066
  path: { type: 'string' }, page: { type: 'number' }, nodeId: { type: 'string', description: 'start:从哪个视频节点对应的工程渲;apply:回填到哪个节点(省略时:主视频节点回填整片、另建的样片节点回填样片)' }, jobId: { type: 'string' },
6734
7067
  source: { type: 'string', enum: ['film', 'pilot'], description: 'apply 时回填哪一种:film=整片(主视频节点)、pilot=样片(样片节点)' },
6735
- frames: { type: 'json', description: '只渲染这些帧' },
6736
- range: { type: 'json', description: '帧区间 [from,to]:渲样片(写 pilot.mp4)' },
7068
+ frames: { type: 'array', items: { type: 'number' }, description: '只渲染这些帧' },
7069
+ range: { type: 'array', items: { type: 'number' }, description: '帧区间 [from,to]:渲样片(写 pilot.mp4)' },
6737
7070
  },
6738
7071
  output: { schema: { type: 'object', additionalProperties: false, properties: { summary: { type: 'string', required: true }, details: { type: 'string', required: true } } },
6739
7072
  render: (_args, value) => [{ type: 'text', text: value.summary + '\n' + value.details }] },
@@ -6824,8 +7157,10 @@ function sanitizeNewName(raw) {
6824
7157
  })
6825
7158
  }
6826
7159
  if (policyFor(scope.sessionId)?.mode === 'read-only') throw new Error('当前会话为只读')
6827
- const operation = Array.isArray(args.frames) && args.frames.length ? 'still' : 'full'
6828
- const ranged = Array.isArray(args.range) && args.range.length === 2
7160
+ const renderFrames = jsonArray(args.frames)
7161
+ const renderRange = jsonArray(args.range)
7162
+ const operation = renderFrames.length ? 'still' : 'full'
7163
+ const ranged = renderRange.length === 2
6829
7164
  // 样片的输出必须跟整片分开:以前 range 渲染也写 film.mp4,下一次 apply 就会把 4 秒样片当整片塞进主节点。
6830
7165
  const kind = operation === 'still' ? 'still' : ranged ? 'pilot' : 'film'
6831
7166
  const outputName = kind === 'still' ? 'still.mp4' : kind === 'pilot' ? 'pilot.mp4' : 'film.mp4'
@@ -6847,7 +7182,7 @@ function sanitizeNewName(raw) {
6847
7182
  const qualityGate = kind === 'still' ? null : assertQualityEvidence(dir, styleForGate)
6848
7183
  const gateWarnings = [engineGate?.warning, ...(qualityGate?.warnings || [])].filter(Boolean)
6849
7184
  const job = motionJobs.create({ kind, dir, nodeId: node.id })
6850
- const handle = renderProject(dir, { operation, frames: args.frames, frameRange: ranged ? args.range : undefined,
7185
+ const handle = renderProject(dir, { operation, frames: renderFrames.length ? renderFrames : undefined, frameRange: ranged ? renderRange : undefined,
6851
7186
  output: outputPath(dir, outputName),
6852
7187
  onProgress: (message) => motionJobs.progress(job, message.phase, message.progress, message.message) })
6853
7188
  motionJobs.attach(job, handle)
@@ -6863,7 +7198,7 @@ function sanitizeNewName(raw) {
6863
7198
  })))
6864
7199
 
6865
7200
  // 帧检:把"片子有没有毛病"变成一张带帧号的清单,而不是靠眼睛。
6866
- ctx.effect(() => ctx.tools.register(defineTool({
7201
+ ctx.effect(() => registerHostTool(defineTool({
6867
7202
  name: 'motion_qa',
6868
7203
  description: '成片帧检:不靠眼睛,靠像素和响度。inspect 立刻检一遍(默认检工程里的 .out/film.mp4,也可以指定 file):帧数对不对、有没有黑场、有没有静止太久、有没有单帧闪烁、有没有空白帧、画面有没有顶到边、音轨有没有削波。report 读上次的结果。每条问题都带帧号和镜头号,能直接回去改对应场景。',
6869
7204
  parameters: {
@@ -6902,7 +7237,7 @@ function sanitizeNewName(raw) {
6902
7237
  })))
6903
7238
 
6904
7239
  // 剪映导出:把成片拆成"还能继续剪"的素材包。
6905
- ctx.effect(() => ctx.tools.register(defineTool({
7240
+ ctx.effect(() => registerHostTool(defineTool({
6906
7241
  name: 'motion_export',
6907
7242
  description: '把工程导出成剪映可继续剪的素材包:逐镜头无声音轨 clips/、原音轨 audio/(BGM 与音效分开)、shots.json / audio.json / README.md,有字幕数据时再给 subtitles.srt,draft:true 另写一份 draft_content.json 草稿。run 导出,list 看导出过哪些。镜头按时间轴精确切(重编码,不靠关键帧),cut 点不会偏。',
6908
7243
  parameters: {
@@ -6910,7 +7245,15 @@ function sanitizeNewName(raw) {
6910
7245
  path: { type: 'string' }, nodeId: { type: 'string' }, page: { type: 'number' },
6911
7246
  name: { type: 'string', description: '导出目录名,默认 jianying' },
6912
7247
  draft: { type: 'boolean', description: '另写一份 draft_content.json(剪映草稿结构,未在剪映里实测过)' },
6913
- subtitles: { type: 'json', description: '字幕数组 [{startFrame,endFrame,text}];工程 timeline 里有 subtitles 就不用传' },
7248
+ subtitles: {
7249
+ type: 'array',
7250
+ items: {
7251
+ type: 'object',
7252
+ additionalProperties: true,
7253
+ properties: { startFrame: { type: 'number' }, endFrame: { type: 'number' }, text: { type: 'string' } },
7254
+ },
7255
+ description: '字幕数组 [{startFrame,endFrame,text}];工程 timeline 里有 subtitles 就不用传',
7256
+ },
6914
7257
  file: { type: 'string', description: '当成片对照的 mp4,默认 .out/film.mp4' },
6915
7258
  },
6916
7259
  output: { schema: { type: 'object', additionalProperties: false, properties: { summary: { type: 'string', required: true }, details: { type: 'string', required: true } } },
@@ -6928,7 +7271,7 @@ function sanitizeNewName(raw) {
6928
7271
  if (policyFor(scope.sessionId)?.mode === 'read-only') throw new Error('当前会话为只读')
6929
7272
  const timeline = readTimeline(dir)
6930
7273
  const result = await exportPackage({ dir, timeline, name: args.name || 'jianying', draft: Boolean(args.draft),
6931
- subtitles: args.subtitles, film: args.file ? workspacePath(scope, isAbsolute(args.file) ? args.file : joinPath(dir, args.file)) : undefined })
7274
+ subtitles: jsonValue(args.subtitles), film: args.file ? workspacePath(scope, isAbsolute(args.file) ? args.file : joinPath(dir, args.file)) : undefined })
6932
7275
  return { summary: '已导出 ' + result.shots.length + ' 镜 / ' + result.stems.length + ' 条音轨到 ' + result.folder
6933
7276
  + (result.subtitles ? '(含字幕 ' + result.subtitles + ' 条)' : '')
6934
7277
  + (result.missing.length ? ',缺 ' + result.missing.length + ' 个音频文件' : ''),
@@ -6956,7 +7299,7 @@ function sanitizeNewName(raw) {
6956
7299
  }
6957
7300
 
6958
7301
  // motion_assets 真素材进工程(原字节 + 尺寸 + 色板),场景用 staticFile 取
6959
- ctx.effect(() => ctx.tools.register(defineTool({
7302
+ ctx.effect(() => registerHostTool(defineTool({
6960
7303
  name: 'motion_assets',
6961
7304
  description: '视频工程素材:list 查看工程素材;library 按 query(如 纸张、颗粒、钢琴)或 category 搜索插件自带纹理及乐器采样;use 用 assetId 按需导入,返回 file,图片用 staticFile(file),音频须另用 motion_audio add 上时间轴。素材库是可选来源,用户自带素材优先,采样是单音而非完整配乐;使用前查看许可。gather 按站点收集品牌素材、配色与字体,collect 扫单页;capture 收本地素材;fetch 下载原图;crop 裁切;page 截网页;palette 读色板。',
6962
7305
  parameters: {
@@ -7049,7 +7392,7 @@ function sanitizeNewName(raw) {
7049
7392
  })))
7050
7393
 
7051
7394
  // motion_sfx 音效采购:Freesound 搜索/下载与素材索引
7052
- ctx.effect(() => ctx.tools.register(defineTool({
7395
+ ctx.effect(() => registerHostTool(defineTool({
7053
7396
  name: 'motion_sfx',
7054
7397
  description: '音效采购(Freesound)。search 按英文关键词搜,返回时长与作者;get 按 id 下载 HQ mp3 预览到 <工程>/assets/sfx/,量峰值与有效区间,维护一份 assets/audio-manifest.json;list 看已下载的音效。下载完用 motion_audio {action:"add", file:"sfx/xxx.mp3", role:"sfx"} 挂到轴上。',
7055
7398
  parameters: {
@@ -7060,7 +7403,7 @@ function sanitizeNewName(raw) {
7060
7403
  minDuration: { type: 'number', description: 'search:最短秒数,默认 0' },
7061
7404
  maxDuration: { type: 'number', description: 'search:最长秒数' },
7062
7405
  limit: { type: 'number', description: 'search:最多返回几条,默认 8' },
7063
- ids: { type: 'json', description: 'get:音效 id 数组,例如 ["fs:12345"]' },
7406
+ ids: { type: 'array', items: { type: 'string' }, description: 'get:音效 id 数组,例如 ["fs:12345"]' },
7064
7407
  },
7065
7408
  output: { schema: { type: 'object', additionalProperties: false, properties: { summary: { type: 'string', required: true }, details: { type: 'string', required: true } } },
7066
7409
  render: (_args, value) => [{ type: 'text', text: value.summary + '\n' + value.details }] },
@@ -7076,7 +7419,7 @@ function sanitizeNewName(raw) {
7076
7419
  details: JSON.stringify({ ...found, next: '挑一条再 motion_sfx {action:"get", ids:["fs:xxxx"]};想先听可以按 pageUrl 打开' }) }
7077
7420
  }
7078
7421
  if (policyFor(scope.sessionId)?.mode === 'read-only') throw new Error('当前会话为只读')
7079
- const result = await getSfx(dir, args.ids)
7422
+ const result = await getSfx(dir, jsonArray(args.ids))
7080
7423
  const ok = result.results.filter((item) => item.ok)
7081
7424
  const bad = result.results.filter((item) => !item.ok)
7082
7425
  return { summary: '下载 ' + ok.length + ' 条' + (bad.length ? ',失败 ' + bad.length + ' 条' : ''),
@@ -7091,7 +7434,7 @@ function sanitizeNewName(raw) {
7091
7434
  })))
7092
7435
 
7093
7436
  // motion_audio 音频轴:加/删/列音轨,量节拍,把切镜和目标音钉到拍子上
7094
- ctx.effect(() => ctx.tools.register(defineTool({
7437
+ ctx.effect(() => registerHostTool(defineTool({
7095
7438
  name: 'motion_audio',
7096
7439
  description: '音频轴工具。list 列出时间轴上的音轨;add 把一条音频放到指定秒(默认写进 <工程>/assets/,素材没进工程会自动捕获);remove 按 cueId 删;analyze 量 BPM/网格/重音(返回 bpm、grid 残差、hits 三分类、peakLag);apply 写 beats.json 并把切点吸到拍子上,给 cueId 时改把这条音轨钉到拍子(自动扣掉音源峰值滞后与输出偏移)。bpm 为 null 说明量不出稳定网格——不要硬对。',
7097
7440
  parameters: {
@@ -7223,13 +7566,37 @@ function sanitizeNewName(raw) {
7223
7566
  },
7224
7567
  })))
7225
7568
 
7226
- ctx.effect(() => ctx.tools.register(defineTool({
7569
+ ctx.effect(() => registerHostTool(defineTool({
7227
7570
  name: 'motion_sync',
7228
7571
  description: '音乐拍点→切镜/动作→音效峰值统一绑定。bind 替换绑定表并分析音乐;list 查看解析结果;clear 解除绑定、保留当前排期。anchors:[{id,type:"cut"|"action",shotId,beat?:0起拍号,at?:源音乐秒,name?:动作名,visual?:{metric:"change"|"brightness",roi:{x,y,w,h}},sfx?:[{cueId,peakSeconds:素材源峰值秒}]}]。beat/at 二选一;动作场景接收 anchors,使用 anchors.<name> 本镜局部帧写关键帧。动作验收要选 ROI/metric,切镜默认测帧差。绑定后 motion_audio_verify 对照成片。音乐需要非循环音轨,音乐 from/trimStart 参与换算。',
7229
7572
  parameters: {
7230
7573
  action: {type:'string',enum:['bind','list','clear'],required:true},
7231
7574
  nodeId: {type:'string',required:true}, path:{type:'string'},page:{type:'number'},
7232
- musicCueId:{type:'string'},anchors:{type:'json'},
7575
+ musicCueId:{type:'string'},
7576
+ anchors:{
7577
+ type:'array',
7578
+ items:{
7579
+ type:'object',
7580
+ additionalProperties:true,
7581
+ properties:{
7582
+ id:{type:'string'},
7583
+ type:{type:'string',enum:['cut','action']},
7584
+ shotId:{type:'string'},
7585
+ beat:{type:'number'},
7586
+ at:{type:'number'},
7587
+ name:{type:'string'},
7588
+ visual:{
7589
+ type:'object',
7590
+ additionalProperties:true,
7591
+ properties:{
7592
+ metric:{type:'string',enum:['change','brightness']},
7593
+ roi:{type:'object',additionalProperties:false,properties:{x:{type:'number'},y:{type:'number'},w:{type:'number'},h:{type:'number'}}},
7594
+ },
7595
+ },
7596
+ sfx:{type:'array',items:{type:'object',additionalProperties:true,properties:{cueId:{type:'string'},peakSeconds:{type:'number'}}}},
7597
+ },
7598
+ },
7599
+ },
7233
7600
  outputOffsetSeconds:{type:'number',description:'具体渲染管线实测音轨延迟(秒),默认0;验收报告给出多点测得的偏移参考'},
7234
7601
  toleranceFrames:{type:'number',description:'音画及音效误差容差,默认2帧,最大3帧'},
7235
7602
  },
@@ -7249,7 +7616,7 @@ function sanitizeNewName(raw) {
7249
7616
  const analysis=await analyzeBeats(assetPath(dir,cue.file))
7250
7617
  const tolerance=args.toleranceFrames??2
7251
7618
  if(!Number.isFinite(tolerance)||tolerance<0||tolerance>3)throw new Error('toleranceFrames 必须在0–3内')
7252
- timeline.sync={version:1,musicCueId:cue.id,analysis,analysisSourceHash:filmHash(assetPath(dir,cue.file)),anchors:args.anchors,outputOffsetSeconds:args.outputOffsetSeconds??0,toleranceFrames:tolerance}
7619
+ timeline.sync={version:1,musicCueId:cue.id,analysis,analysisSourceHash:filmHash(assetPath(dir,cue.file)),anchors:jsonArray(args.anchors),outputOffsetSeconds:args.outputOffsetSeconds??0,toleranceFrames:tolerance}
7253
7620
  timeline=resolveSync(timeline)
7254
7621
  }
7255
7622
  writeTimeline(dir,timeline)
@@ -7258,7 +7625,7 @@ function sanitizeNewName(raw) {
7258
7625
  })))
7259
7626
 
7260
7627
  // motion_audio verify 渲后回测:成片里的声音到底偏了多少帧
7261
- ctx.effect(() => ctx.tools.register(defineTool({
7628
+ ctx.effect(() => registerHostTool(defineTool({
7262
7629
  name: 'motion_audio_verify',
7263
7630
  description: '音画对照验收:按 motion_sync 锚点,从成片匹配源音乐和音效波形、测切镜或动作ROI的实际变化帧,逐项报告音画及音效误差。无证据标 unverified,超容差 fail。报告写入 .out/qa/sync/report.json。',
7264
7631
  parameters: {
@@ -7288,7 +7655,7 @@ function sanitizeNewName(raw) {
7288
7655
  // 为什么时长由旁白说了算:旁白是"内容的长度",画面是"内容的形状"。
7289
7656
  // 先按字速估、合成后用真实时长再重排一次,镜头跟着句子走 —— 反过来做
7290
7657
  // (先定死镜头再塞旁白)只能靠变速或砍句,两个都难看。
7291
- ctx.effect(() => ctx.tools.register(defineTool({
7658
+ ctx.effect(() => registerHostTool(defineTool({
7292
7659
  name: 'motion_voice',
7293
7660
  description: '旁白(TTS):把 voice.script.md(## shot-N 分节、一句一行)变成 voice 音轨 + 字幕,并让镜头时长跟着句子走。action=list 看现状/音色;plan 只估算与重排(不合成,先看排期是否合理);say 真合成(edge-tts,拿不到就退 SAPI)+ 用真实时长重排;align 已有音频只按真实时长重排。合成结果按台词哈希缓存,改一句只重合成一句。字幕写进 timeline.subtitles,导出 SRT / 剪映字幕轨零改动。',
7294
7661
  parameters: {
@@ -7368,7 +7735,7 @@ function sanitizeNewName(raw) {
7368
7735
  },
7369
7736
  })))
7370
7737
 
7371
- ctx.effect(() => ctx.tools.register({ ...applyTool, execute: async (args, exec) => {
7738
+ ctx.effect(() => registerHostTool({ ...applyTool, execute: async (args, exec) => {
7372
7739
  const sessionId = sessionIdOf(exec)
7373
7740
  const path = typeof args.path === 'string' && args.path ? args.path : focusedPathFor(sessionId) || DEFAULT_PATH
7374
7741
  const target = await resolveTarget(path, sessionId)
@@ -7378,24 +7745,34 @@ function sanitizeNewName(raw) {
7378
7745
  })
7379
7746
  } }))
7380
7747
 
7381
- // 把"怎么用这张画布"注册成**内嵌技能**(见 SKILL_BODY 的注释):
7748
+ // 注册段收尾:一行汇总(成功几个、失败哪几个)。远端出问题时先看这条,
7749
+ // 再看 DIAGNOSTICS_PATH 的 JSON —— 两者对不上就说明注册完又被回收了。
7750
+ reportToolRegistrations()
7751
+
7752
+ // 把"怎么用这张画布"注册成**内嵌技能**(见 SKILL_DEFINITIONS 的注释):
7382
7753
  // 换了电脑、只装了插件(没有源码/README/tools)时,模型依然知道该怎么操作。
7383
7754
  //
7384
7755
  // `skills` 是**可选**依赖:没装 skill 注册表的部署里照样能用(工具描述本身已经够启动),
7385
7756
  // 所以走 ctx.get 而不是写进 inject —— 缺它不该让整个插件停在等待态。
7386
7757
  const skills = typeof ctx.get === 'function' ? ctx.get('skills') : undefined
7387
7758
  if (skills !== undefined && skills !== null && typeof skills.register === 'function') {
7388
- ctx.effect(() =>
7389
- skills.register({
7390
- name: SKILL_SUMMARY.name,
7391
- description: SKILL_SUMMARY.description,
7392
- whenToUse: SKILL_SUMMARY.whenToUse,
7393
- // `source: 'runtime'` 不能省:注册时只校验 name/description/invocation,
7394
- // 但**取全文**时注册表会再跑一次 validateDefinition,那里要求 source 是字符串
7395
- // (少了它,目录里看得见、一 load 就抛 "source must be a string")。
7396
- source: 'runtime',
7397
- content: SKILL_BODY,
7398
- }),
7399
- )
7759
+ ctx.effect(() => {
7760
+ const disposers = SKILL_DEFINITIONS.map((definition) =>
7761
+ skills.register({
7762
+ name: definition.name,
7763
+ description: definition.description,
7764
+ whenToUse: definition.whenToUse,
7765
+ // `source: 'runtime'` 不能省:注册时只校验 name/description/invocation,
7766
+ // 但**取全文**时注册表会再跑一次 validateDefinition,那里要求 source 是字符串
7767
+ // (少了它,目录里看得见、一 load 就抛 "source must be a string")。
7768
+ source: 'runtime',
7769
+ content: definition.content,
7770
+ }),
7771
+ )
7772
+ // 两条注册各返回一个 disposer;合成一个交回 Cordis,卸载时一并撤掉。
7773
+ return () => {
7774
+ for (const dispose of disposers) if (typeof dispose === 'function') dispose()
7775
+ }
7776
+ })
7400
7777
  }
7401
7778
  }