dsh-creation-mode 0.0.0-stage → 3.0.0

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 (356) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +26 -2
  3. package/client.js +2650 -0
  4. package/cordis.patch.yml +106 -0
  5. package/data.js +354 -0
  6. package/flow.js +338 -0
  7. package/icon.svg +13 -0
  8. package/index.js +234 -0
  9. package/locale/en.json +6 -0
  10. package/locale/zh.json +6 -0
  11. package/package.json +50 -4
  12. package/stills/A1.jpg +0 -0
  13. package/stills/A2.jpg +0 -0
  14. package/stills/A3.jpg +0 -0
  15. package/stills/A4.jpg +0 -0
  16. package/stills/A5.jpg +0 -0
  17. package/stills/B1.jpg +0 -0
  18. package/stills/B2.jpg +0 -0
  19. package/stills/B3.jpg +0 -0
  20. package/stills/B4.jpg +0 -0
  21. package/stills/B5.jpg +0 -0
  22. package/stills/C1.jpg +0 -0
  23. package/stills/C2.jpg +0 -0
  24. package/stills/C3.jpg +0 -0
  25. package/stills/C4.jpg +0 -0
  26. package/stills/D1.jpg +0 -0
  27. package/stills/D2.jpg +0 -0
  28. package/stills/D3.jpg +0 -0
  29. package/stills/D4.jpg +0 -0
  30. package/stills/E1.jpg +0 -0
  31. package/stills/E2.jpg +0 -0
  32. package/stills/E3.jpg +0 -0
  33. package/stills/F1.jpg +0 -0
  34. package/stills/F2.jpg +0 -0
  35. package/stills/F3.jpg +0 -0
  36. package/stills/G1.jpg +0 -0
  37. package/stills/G2.jpg +0 -0
  38. package/stills/H1.jpg +0 -0
  39. package/stills/H10.jpg +0 -0
  40. package/stills/H11.jpg +0 -0
  41. package/stills/H12.jpg +0 -0
  42. package/stills/H2.jpg +0 -0
  43. package/stills/H3.jpg +0 -0
  44. package/stills/H4.jpg +0 -0
  45. package/stills/H5.jpg +0 -0
  46. package/stills/H6.jpg +0 -0
  47. package/stills/H7.jpg +0 -0
  48. package/stills/H8.jpg +0 -0
  49. package/stills/H9.jpg +0 -0
  50. package/stills/K1.jpg +0 -0
  51. package/stills/K10.jpg +0 -0
  52. package/stills/K11.jpg +0 -0
  53. package/stills/K12.jpg +0 -0
  54. package/stills/K13.jpg +0 -0
  55. package/stills/K14.jpg +0 -0
  56. package/stills/K15.jpg +0 -0
  57. package/stills/K16.jpg +0 -0
  58. package/stills/K17.jpg +0 -0
  59. package/stills/K18.jpg +0 -0
  60. package/stills/K19.jpg +0 -0
  61. package/stills/K2.jpg +0 -0
  62. package/stills/K20.jpg +0 -0
  63. package/stills/K21.jpg +0 -0
  64. package/stills/K22.jpg +0 -0
  65. package/stills/K23.jpg +0 -0
  66. package/stills/K24.jpg +0 -0
  67. package/stills/K25.jpg +0 -0
  68. package/stills/K26.jpg +0 -0
  69. package/stills/K27.jpg +0 -0
  70. package/stills/K28.jpg +0 -0
  71. package/stills/K29.jpg +0 -0
  72. package/stills/K3.jpg +0 -0
  73. package/stills/K30.jpg +0 -0
  74. package/stills/K31.jpg +0 -0
  75. package/stills/K32.jpg +0 -0
  76. package/stills/K33.jpg +0 -0
  77. package/stills/K34.jpg +0 -0
  78. package/stills/K35.jpg +0 -0
  79. package/stills/K36.jpg +0 -0
  80. package/stills/K37.jpg +0 -0
  81. package/stills/K38.jpg +0 -0
  82. package/stills/K4.jpg +0 -0
  83. package/stills/K5.jpg +0 -0
  84. package/stills/K6.jpg +0 -0
  85. package/stills/K7.jpg +0 -0
  86. package/stills/K8.jpg +0 -0
  87. package/stills/K9.jpg +0 -0
  88. package/stills/L1.jpg +0 -0
  89. package/stills/L10.jpg +0 -0
  90. package/stills/L11.jpg +0 -0
  91. package/stills/L12.jpg +0 -0
  92. package/stills/L13.jpg +0 -0
  93. package/stills/L14.jpg +0 -0
  94. package/stills/L15.jpg +0 -0
  95. package/stills/L16.jpg +0 -0
  96. package/stills/L17.jpg +0 -0
  97. package/stills/L18.jpg +0 -0
  98. package/stills/L19.jpg +0 -0
  99. package/stills/L2.jpg +0 -0
  100. package/stills/L20.jpg +0 -0
  101. package/stills/L21.jpg +0 -0
  102. package/stills/L22.jpg +0 -0
  103. package/stills/L23.jpg +0 -0
  104. package/stills/L24.jpg +0 -0
  105. package/stills/L25.jpg +0 -0
  106. package/stills/L26.jpg +0 -0
  107. package/stills/L27.jpg +0 -0
  108. package/stills/L28.jpg +0 -0
  109. package/stills/L29.jpg +0 -0
  110. package/stills/L3.jpg +0 -0
  111. package/stills/L30.jpg +0 -0
  112. package/stills/L31.jpg +0 -0
  113. package/stills/L32.jpg +0 -0
  114. package/stills/L33.jpg +0 -0
  115. package/stills/L34.jpg +0 -0
  116. package/stills/L35.jpg +0 -0
  117. package/stills/L36.jpg +0 -0
  118. package/stills/L37.jpg +0 -0
  119. package/stills/L38.jpg +0 -0
  120. package/stills/L39.jpg +0 -0
  121. package/stills/L4.jpg +0 -0
  122. package/stills/L40.jpg +0 -0
  123. package/stills/L41.jpg +0 -0
  124. package/stills/L42.jpg +0 -0
  125. package/stills/L43.jpg +0 -0
  126. package/stills/L44.jpg +0 -0
  127. package/stills/L45.jpg +0 -0
  128. package/stills/L46.jpg +0 -0
  129. package/stills/L47.jpg +0 -0
  130. package/stills/L48.jpg +0 -0
  131. package/stills/L49.jpg +0 -0
  132. package/stills/L5.jpg +0 -0
  133. package/stills/L50.jpg +0 -0
  134. package/stills/L51.jpg +0 -0
  135. package/stills/L52.jpg +0 -0
  136. package/stills/L53.jpg +0 -0
  137. package/stills/L54.jpg +0 -0
  138. package/stills/L55.jpg +0 -0
  139. package/stills/L56.jpg +0 -0
  140. package/stills/L57.jpg +0 -0
  141. package/stills/L58.jpg +0 -0
  142. package/stills/L59.jpg +0 -0
  143. package/stills/L6.jpg +0 -0
  144. package/stills/L7.jpg +0 -0
  145. package/stills/L8.jpg +0 -0
  146. package/stills/L9.jpg +0 -0
  147. package/stills/M1.jpg +0 -0
  148. package/stills/M10.jpg +0 -0
  149. package/stills/M11.jpg +0 -0
  150. package/stills/M12.jpg +0 -0
  151. package/stills/M13.jpg +0 -0
  152. package/stills/M14.jpg +0 -0
  153. package/stills/M15.jpg +0 -0
  154. package/stills/M16.jpg +0 -0
  155. package/stills/M17.jpg +0 -0
  156. package/stills/M18.jpg +0 -0
  157. package/stills/M19.jpg +0 -0
  158. package/stills/M2.jpg +0 -0
  159. package/stills/M20.jpg +0 -0
  160. package/stills/M21.jpg +0 -0
  161. package/stills/M22.jpg +0 -0
  162. package/stills/M23.jpg +0 -0
  163. package/stills/M24.jpg +0 -0
  164. package/stills/M25.jpg +0 -0
  165. package/stills/M26.jpg +0 -0
  166. package/stills/M27.jpg +0 -0
  167. package/stills/M28.jpg +0 -0
  168. package/stills/M29.jpg +0 -0
  169. package/stills/M3.jpg +0 -0
  170. package/stills/M30.jpg +0 -0
  171. package/stills/M31.jpg +0 -0
  172. package/stills/M32.jpg +0 -0
  173. package/stills/M33.jpg +0 -0
  174. package/stills/M34.jpg +0 -0
  175. package/stills/M35.jpg +0 -0
  176. package/stills/M36.jpg +0 -0
  177. package/stills/M37.jpg +0 -0
  178. package/stills/M38.jpg +0 -0
  179. package/stills/M39.jpg +0 -0
  180. package/stills/M4.jpg +0 -0
  181. package/stills/M40.jpg +0 -0
  182. package/stills/M5.jpg +0 -0
  183. package/stills/M6.jpg +0 -0
  184. package/stills/M7.jpg +0 -0
  185. package/stills/M8.jpg +0 -0
  186. package/stills/M9.jpg +0 -0
  187. package/stills/O1.jpg +0 -0
  188. package/stills/O10.jpg +0 -0
  189. package/stills/O11.jpg +0 -0
  190. package/stills/O12.jpg +0 -0
  191. package/stills/O13.jpg +0 -0
  192. package/stills/O14.jpg +0 -0
  193. package/stills/O15.jpg +0 -0
  194. package/stills/O16.jpg +0 -0
  195. package/stills/O17.jpg +0 -0
  196. package/stills/O18.jpg +0 -0
  197. package/stills/O19.jpg +0 -0
  198. package/stills/O2.jpg +0 -0
  199. package/stills/O20.jpg +0 -0
  200. package/stills/O21.jpg +0 -0
  201. package/stills/O22.jpg +0 -0
  202. package/stills/O23.jpg +0 -0
  203. package/stills/O24.jpg +0 -0
  204. package/stills/O25.jpg +0 -0
  205. package/stills/O26.jpg +0 -0
  206. package/stills/O27.jpg +0 -0
  207. package/stills/O28.jpg +0 -0
  208. package/stills/O29.jpg +0 -0
  209. package/stills/O3.jpg +0 -0
  210. package/stills/O30.jpg +0 -0
  211. package/stills/O31.jpg +0 -0
  212. package/stills/O32.jpg +0 -0
  213. package/stills/O33.jpg +0 -0
  214. package/stills/O34.jpg +0 -0
  215. package/stills/O35.jpg +0 -0
  216. package/stills/O36.jpg +0 -0
  217. package/stills/O37.jpg +0 -0
  218. package/stills/O38.jpg +0 -0
  219. package/stills/O39.jpg +0 -0
  220. package/stills/O4.jpg +0 -0
  221. package/stills/O40.jpg +0 -0
  222. package/stills/O41.jpg +0 -0
  223. package/stills/O42.jpg +0 -0
  224. package/stills/O43.jpg +0 -0
  225. package/stills/O44.jpg +0 -0
  226. package/stills/O45.jpg +0 -0
  227. package/stills/O46.jpg +0 -0
  228. package/stills/O47.jpg +0 -0
  229. package/stills/O48.jpg +0 -0
  230. package/stills/O49.jpg +0 -0
  231. package/stills/O5.jpg +0 -0
  232. package/stills/O50.jpg +0 -0
  233. package/stills/O51.jpg +0 -0
  234. package/stills/O52.jpg +0 -0
  235. package/stills/O53.jpg +0 -0
  236. package/stills/O54.jpg +0 -0
  237. package/stills/O55.jpg +0 -0
  238. package/stills/O56.jpg +0 -0
  239. package/stills/O57.jpg +0 -0
  240. package/stills/O58.jpg +0 -0
  241. package/stills/O59.jpg +0 -0
  242. package/stills/O6.jpg +0 -0
  243. package/stills/O60.jpg +0 -0
  244. package/stills/O61.jpg +0 -0
  245. package/stills/O62.jpg +0 -0
  246. package/stills/O63.jpg +0 -0
  247. package/stills/O64.jpg +0 -0
  248. package/stills/O65.jpg +0 -0
  249. package/stills/O66.jpg +0 -0
  250. package/stills/O67.jpg +0 -0
  251. package/stills/O68.jpg +0 -0
  252. package/stills/O69.jpg +0 -0
  253. package/stills/O7.jpg +0 -0
  254. package/stills/O70.jpg +0 -0
  255. package/stills/O71.jpg +0 -0
  256. package/stills/O72.jpg +0 -0
  257. package/stills/O73.jpg +0 -0
  258. package/stills/O74.jpg +0 -0
  259. package/stills/O75.jpg +0 -0
  260. package/stills/O76.jpg +0 -0
  261. package/stills/O77.jpg +0 -0
  262. package/stills/O78.jpg +0 -0
  263. package/stills/O79.jpg +0 -0
  264. package/stills/O8.jpg +0 -0
  265. package/stills/O80.jpg +0 -0
  266. package/stills/O81.jpg +0 -0
  267. package/stills/O82.jpg +0 -0
  268. package/stills/O83.jpg +0 -0
  269. package/stills/O84.jpg +0 -0
  270. package/stills/O85.jpg +0 -0
  271. package/stills/O86.jpg +0 -0
  272. package/stills/O87.jpg +0 -0
  273. package/stills/O88.jpg +0 -0
  274. package/stills/O89.jpg +0 -0
  275. package/stills/O9.jpg +0 -0
  276. package/stills/O90.jpg +0 -0
  277. package/stills/O91.jpg +0 -0
  278. package/stills/O92.jpg +0 -0
  279. package/stills/O93.jpg +0 -0
  280. package/stills/O94.jpg +0 -0
  281. package/stills/O95.jpg +0 -0
  282. package/stills/O96.jpg +0 -0
  283. package/stills/O97.jpg +0 -0
  284. package/stills/O98.jpg +0 -0
  285. package/stills/R1.jpg +0 -0
  286. package/stills/R2.jpg +0 -0
  287. package/stills/R3.jpg +0 -0
  288. package/stills/R4.jpg +0 -0
  289. package/stills/R5.jpg +0 -0
  290. package/stills/R6.jpg +0 -0
  291. package/stills/R7.jpg +0 -0
  292. package/stills/V1.jpg +0 -0
  293. package/stills/V10.jpg +0 -0
  294. package/stills/V11.jpg +0 -0
  295. package/stills/V12.jpg +0 -0
  296. package/stills/V13.jpg +0 -0
  297. package/stills/V14.jpg +0 -0
  298. package/stills/V15.jpg +0 -0
  299. package/stills/V2.jpg +0 -0
  300. package/stills/V3.jpg +0 -0
  301. package/stills/V4.jpg +0 -0
  302. package/stills/V5.jpg +0 -0
  303. package/stills/V6.jpg +0 -0
  304. package/stills/V7.jpg +0 -0
  305. package/stills/V8.jpg +0 -0
  306. package/stills/V9.jpg +0 -0
  307. package/stills/X1.jpg +0 -0
  308. package/stills/X10.jpg +0 -0
  309. package/stills/X11.jpg +0 -0
  310. package/stills/X12.jpg +0 -0
  311. package/stills/X13.jpg +0 -0
  312. package/stills/X14.jpg +0 -0
  313. package/stills/X15.jpg +0 -0
  314. package/stills/X16.jpg +0 -0
  315. package/stills/X17.jpg +0 -0
  316. package/stills/X18.jpg +0 -0
  317. package/stills/X19.jpg +0 -0
  318. package/stills/X2.jpg +0 -0
  319. package/stills/X20.jpg +0 -0
  320. package/stills/X21.jpg +0 -0
  321. package/stills/X22.jpg +0 -0
  322. package/stills/X23.jpg +0 -0
  323. package/stills/X24.jpg +0 -0
  324. package/stills/X25.jpg +0 -0
  325. package/stills/X26.jpg +0 -0
  326. package/stills/X27.jpg +0 -0
  327. package/stills/X28.jpg +0 -0
  328. package/stills/X29.jpg +0 -0
  329. package/stills/X3.jpg +0 -0
  330. package/stills/X30.jpg +0 -0
  331. package/stills/X31.jpg +0 -0
  332. package/stills/X4.jpg +0 -0
  333. package/stills/X5.jpg +0 -0
  334. package/stills/X6.jpg +0 -0
  335. package/stills/X7.jpg +0 -0
  336. package/stills/X8.jpg +0 -0
  337. package/stills/X9.jpg +0 -0
  338. package/stills/Z1.jpg +0 -0
  339. package/stills/Z10.jpg +0 -0
  340. package/stills/Z11.jpg +0 -0
  341. package/stills/Z12.jpg +0 -0
  342. package/stills/Z13.jpg +0 -0
  343. package/stills/Z14.jpg +0 -0
  344. package/stills/Z15.jpg +0 -0
  345. package/stills/Z2.jpg +0 -0
  346. package/stills/Z3.jpg +0 -0
  347. package/stills/Z4.jpg +0 -0
  348. package/stills/Z5.jpg +0 -0
  349. package/stills/Z6.jpg +0 -0
  350. package/stills/Z7.jpg +0 -0
  351. package/stills/Z8.jpg +0 -0
  352. package/stills/Z9.jpg +0 -0
  353. package/stills/approved.json +198 -0
  354. package/stills/index.json +1 -0
  355. package/styles.json +5090 -0
  356. package/tools.js +788 -0
@@ -0,0 +1,106 @@
1
+ # 创作模式 (Creation Mode)
2
+ #
3
+ # 一个 bundle 插两行:
4
+ # - profile 根行负责用 HTTP 提供风格目录与生成好的动画;
5
+ # - 预设行声明「创作模式」这个 Agent 模式, 并挂载创作工具。
6
+ # 路由留在根行, 因为它是 profile 级的: 每个预设各挂一份会在同一个前缀上冲突。
7
+ - insert:
8
+ - id: code-animation-studio
9
+ name: dsh-creation-mode
10
+ config:
11
+ tools: false
12
+ routes: true
13
+ - id: preset-code-animation
14
+ name: '@deepseek-ai/dsh-agent-preset'
15
+ config:
16
+ id: code-animation
17
+ name: 创作模式
18
+ description: 用 193 种代码动画风格直接在对话里出片:选风格、写动画、预览、导出视频。
19
+ order: 5
20
+ plugins:
21
+ - id: code-animation-studio
22
+ name: dsh-creation-mode
23
+ config:
24
+ tools: true
25
+ routes: false
26
+ - id: persona
27
+ name: '@deepseek-ai/dsh-persona'
28
+ config:
29
+ prefix: >-
30
+ You are a coding agent powered by the {{model}} model.
31
+ - id: agent-instructions
32
+ name: '@deepseek-ai/dsh-agent-instructions'
33
+ config:
34
+ maxBytes: 65536
35
+ - id: time-context
36
+ name: '@deepseek-ai/dsh-time-context'
37
+ - id: tool-bash
38
+ name: '@deepseek-ai/dsh-tool-bash'
39
+ disabled: !!js process.platform === 'win32'
40
+ - id: tool-pwsh
41
+ name: '@deepseek-ai/dsh-tool-pwsh'
42
+ disabled: !!js process.platform !== 'win32'
43
+ - id: tool-fs
44
+ name: '@deepseek-ai/dsh-tool-fs'
45
+ - id: tool-fs-search
46
+ name: '@deepseek-ai/dsh-tool-fs-search'
47
+ config:
48
+ sampleOverCapGlobResults: false
49
+ - id: tool-jobs
50
+ name: '@deepseek-ai/dsh-tool-jobs'
51
+ - id: tool-schedule
52
+ name: '@deepseek-ai/dsh-tool-schedule'
53
+ - id: skill-filesystem
54
+ name: '@deepseek-ai/dsh-skill-filesystem'
55
+ - id: tool-skill
56
+ name: '@deepseek-ai/dsh-tool-skill'
57
+ - id: command-goal
58
+ name: '@deepseek-ai/dsh-command-goal'
59
+ - id: tool-goal
60
+ name: '@deepseek-ai/dsh-tool-goal'
61
+ - id: compaction
62
+ name: cordis:group
63
+ group: true
64
+ isolate:
65
+ compaction: true
66
+ toolResultPruner: true
67
+ config:
68
+ - id: compaction-basic
69
+ name: '@deepseek-ai/dsh-compaction-basic'
70
+ - id: command-compact
71
+ name: '@deepseek-ai/dsh-command-compact'
72
+ - id: tool-result-pruner
73
+ name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
74
+ config:
75
+ thresholdChars: 8192
76
+ headChars: 4096
77
+ tailChars: 1024
78
+ - id: delegation
79
+ name: cordis:group
80
+ group: true
81
+ isolate:
82
+ workflowEngine: true
83
+ config:
84
+ - id: tool-subagent-control
85
+ name: '@deepseek-ai/dsh-tool-subagent-control'
86
+ - id: tool-subagent-list-agents
87
+ name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
88
+ - id: tool-subagent
89
+ name: '@deepseek-ai/dsh-tool-subagent'
90
+ config:
91
+ provider: spawn
92
+ toolName: subagent
93
+ modelSelectionSettings: true
94
+ - id: tool-subagent-fork
95
+ name: '@deepseek-ai/dsh-tool-subagent'
96
+ config:
97
+ provider: fork
98
+ toolName: subagent_fork
99
+ - id: tool-ask-user
100
+ name: '@deepseek-ai/dsh-tool-ask-user'
101
+ - id: tool-todo
102
+ name: '@deepseek-ai/dsh-tool-todo'
103
+ config:
104
+ allowParallelInProgress: true
105
+ - id: present
106
+ name: '@deepseek-ai/dsh-tool-present'
package/data.js ADDED
@@ -0,0 +1,354 @@
1
+ /**
2
+ * Shared catalog helpers for both halves of the bundle.
3
+ *
4
+ * The 341 style entries ship beside this module as `styles.json`; their reference
5
+ * stills live under `stills/`. Both the Host bundle (routes) and the creation-mode
6
+ * tools (search) import from here so there is one source of truth.
7
+ *
8
+ * Only styles listed in `stills/approved.json` are published. The rest keep their
9
+ * files on disk but stay out of the catalog until a usable still is added and its id
10
+ * is appended to that list.
11
+ */
12
+ import { readFileSync } from 'node:fs';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const HERE = new URL('./', import.meta.url);
16
+
17
+ export const STYLES_FILE = fileURLToPath(new URL('./styles.json', HERE));
18
+ export const STILLS_DIR = fileURLToPath(new URL('./stills/', HERE));
19
+ /** Stills cleared for publication, one approved style id per entry. */
20
+ export const APPROVED_FILE = fileURLToPath(new URL('./stills/approved.json', HERE));
21
+ /** Where generated animations are written, one HTML file per creation. */
22
+ export const OUTPUT_DIR = fileURLToPath(new URL('./created/', HERE));
23
+ /** Subdirectory an export is written into, under the session directory. */
24
+ export const EXPORT_DIR_NAME = 'code-animation';
25
+
26
+ /**
27
+ * Build marker surfaced by the health route.
28
+ *
29
+ * Bump it whenever tool names or their result shape change: a preset mounts this
30
+ * module once at process start, so a running host can still serve an older revision
31
+ * than the one on disk, and this is how that shows up.
32
+ */
33
+ export const BUILD_REV = 8;
34
+
35
+ /** Tool names this revision registers, in the order they are declared. */
36
+ export const TOOL_NAMES = ['list_styles', 'show_stills', 'get_style', 'save_storyboard', 'save_animation'];
37
+
38
+ let cached;
39
+ let approvedCache;
40
+
41
+ /**
42
+ * Style ids whose still was reviewed and published.
43
+ *
44
+ * The list is read from disk on first use and cached; an absent or unreadable file
45
+ * publishes nothing, which fails closed rather than shipping unvetted artwork.
46
+ */
47
+ export function approvedIds() {
48
+ if (approvedCache) return approvedCache;
49
+ try {
50
+ const raw = JSON.parse(readFileSync(APPROVED_FILE, 'utf8'));
51
+ const list = Array.isArray(raw) ? raw : raw.approved;
52
+ approvedCache = new Set(
53
+ (Array.isArray(list) ? list : []).map((id) => String(id).trim().toUpperCase()).filter(Boolean),
54
+ );
55
+ } catch {
56
+ approvedCache = new Set();
57
+ }
58
+ return approvedCache;
59
+ }
60
+
61
+ /** Whether one style's still is published, so the style may appear at all. */
62
+ export function isApproved(style) {
63
+ return approvedIds().has(String(style?.id || '').toUpperCase());
64
+ }
65
+
66
+ /**
67
+ * Load the catalog once, keeping only published styles.
68
+ *
69
+ * Filtering here rather than at each call site means search, `get_style`, the still
70
+ * route and the card renderers cannot disagree about what is on offer.
71
+ */
72
+ export function catalog() {
73
+ if (cached) return cached;
74
+ const raw = JSON.parse(readFileSync(STYLES_FILE, 'utf8'));
75
+ const approved = approvedIds();
76
+ const styles = raw.styles
77
+ .filter((style) => approved.has(String(style.id).toUpperCase()))
78
+ .map((style) => ({ ...style, still: true }));
79
+ cached = { series: raw.series, styles };
80
+ return cached;
81
+ }
82
+
83
+ /** Field weights: how much a hit in one field says about the intended style. */
84
+ const FIELD_WEIGHTS = {
85
+ id: 120,
86
+ name: 60,
87
+ en: 40,
88
+ tags: 34,
89
+ category: 22,
90
+ seriesName: 18,
91
+ fit: 16,
92
+ desc: 14,
93
+ // Prompts describe technique ("±1 像素", "24 fps"), so a hit there is weak evidence
94
+ // and never counts toward term coverage; it only orders otherwise-equal candidates.
95
+ prompt: 2,
96
+ };
97
+
98
+ /** Fields whose hit proves the style is about the term, used for coverage. */
99
+ const CORE_FIELDS = ['name', 'en', 'tags', 'category', 'seriesName', 'fit', 'desc'];
100
+
101
+ /** Split a query into searchable terms, keeping CJK runs together. */
102
+ export function tokenize(query) {
103
+ const raw = String(query || '').toLowerCase().trim();
104
+ if (!raw) return [];
105
+ // Latin words, numbers, and CJK runs; everything else separates terms.
106
+ const parts = raw.match(/[\u4e00-\u9fff]+|[a-z0-9][a-z0-9+#.-]*/g) || [];
107
+ const terms = [];
108
+ const seen = new Set();
109
+ const push = (term, weight) => {
110
+ const key = `${term}\u0000${weight}`;
111
+ if (term && !seen.has(key)) {
112
+ seen.add(key);
113
+ terms.push({ term, weight });
114
+ }
115
+ };
116
+ for (const part of parts) {
117
+ push(part, 1);
118
+ // A long CJK run is one concept in prose but several in a catalog entry, so
119
+ // its bigrams raise recall for queries like "水墨晕染" against "水墨".
120
+ if (/^[\u4e00-\u9fff]+$/.test(part) && part.length > 2) {
121
+ for (let i = 0; i < part.length - 1; i += 1) push(part.slice(i, i + 2), 0.5);
122
+ }
123
+ }
124
+ return terms;
125
+ }
126
+
127
+ /** One lowercased field value, with array fields flattened. */
128
+ function fieldText(style, field) {
129
+ const value = style[field];
130
+ if (Array.isArray(value)) return value.join(' ').toLowerCase();
131
+ return String(value ?? '').toLowerCase();
132
+ }
133
+
134
+ /** Score one term against one style across every weighted field. */
135
+ function scoreTerm(style, term) {
136
+ let best = 0;
137
+ if (style.id.toLowerCase() === term) best = Math.max(best, FIELD_WEIGHTS.id);
138
+ else if (style.id.toLowerCase().startsWith(term)) best = Math.max(best, FIELD_WEIGHTS.id * 0.6);
139
+ for (const field of [...CORE_FIELDS, 'prompt']) {
140
+ if (fieldText(style, field).includes(term)) best = Math.max(best, FIELD_WEIGHTS[field]);
141
+ }
142
+ return best;
143
+ }
144
+
145
+ /**
146
+ * Whether a hit on this term came from a field that describes the style itself.
147
+ *
148
+ * Coverage counts only these, so a prompt's incidental jargon cannot make an
149
+ * unrelated style look like a multi-term match.
150
+ */
151
+ function coreHit(style, term) {
152
+ return CORE_FIELDS.some((field) => fieldText(style, field).includes(term));
153
+ }
154
+
155
+ /**
156
+ * Whether a query names catalog ids outright, such as "B3" or "L49 O96".
157
+ *
158
+ * Prompts carry hex colours like `#4b3326`, so a bare `b3` substring also matches
159
+ * unrelated entries; an id-shaped query is answered from the id field alone.
160
+ */
161
+ export function looksLikeIds(query) {
162
+ const raw = String(query || '').trim();
163
+ if (!raw) return false;
164
+ const parts = raw.split(/[\s,,、]+/).filter(Boolean);
165
+ return parts.length > 0 && parts.every((part) => /^[a-z]{1,2}\d{1,3}$/i.test(part));
166
+ }
167
+
168
+ /** Split an id-shaped query into its lowercase ids. */
169
+ function idParts(query) {
170
+ return String(query || '').trim().toLowerCase().split(/[\s,,、]+/).filter(Boolean);
171
+ }
172
+
173
+ /**
174
+ * Nearest catalog entries to one misspelled id.
175
+ *
176
+ * Distance is the letter edit distance plus the numeric gap, so `B33` lands on `B3`
177
+ * and `L9` on `L8`/`L10` rather than on an arbitrary entry.
178
+ */
179
+ export function nearestIds(id, limit = 3) {
180
+ const { styles } = catalog();
181
+ const target = /^([a-z]{1,2})(\d{1,3})$/.exec(String(id || '').trim().toLowerCase());
182
+ if (!target) return [];
183
+ const [, letter, digits] = target;
184
+ const number = Number(digits);
185
+ const ranked = [];
186
+ styles.forEach((style, index) => {
187
+ const match = /^([a-z]{1,2})(\d{1,3})$/.exec(style.id.toLowerCase());
188
+ if (!match) return;
189
+ const [, otherLetter, otherDigits] = match;
190
+ const letterGap = letter === otherLetter ? 0 : letterGap_(letter, otherLetter);
191
+ // Rank by how close the ids read, not by raw difference: "B33" is a typo of "B3",
192
+ // and 33 is nearer in written form to 3 than to 5 or 4.
193
+ const numberGap = digitGap(digits, otherDigits);
194
+ ranked.push({ style, cost: letterGap * 10 + numberGap, index });
195
+ });
196
+ ranked.sort((a, b) => (a.cost - b.cost) || (a.index - b.index));
197
+ return ranked.slice(0, limit).map((entry) => entry.style);
198
+ }
199
+
200
+ /**
201
+ * How far two id numbers read apart, counting a shared prefix cheaply.
202
+ *
203
+ * "33" is one insertion away from "3" and one from "34", but two from "5", which
204
+ * keeps a dropped or doubled digit ranking above an unrelated neighbour.
205
+ */
206
+ function digitGap(a, b) {
207
+ const [short, long] = a.length <= b.length ? [a, b] : [b, a];
208
+ let shared = 0;
209
+ while (shared < short.length && short[shared] === long[shared]) shared += 1;
210
+ const extra = long.length - shared;
211
+ const tailGap = Number(long.slice(shared) || 0) - Number(short.slice(shared) || 0);
212
+ return extra + Math.abs(tailGap) / 100;
213
+ }
214
+
215
+ /** How many letters separate two series prefixes; 1 for neighbouring series. */
216
+ function letterGap_(a, b) {
217
+ const first = a.charCodeAt(0);
218
+ const second = b.charCodeAt(0);
219
+ return Math.abs(first - second) + Math.abs(a.length - b.length);
220
+ }
221
+
222
+ /**
223
+ * Case-insensitive match over every weighted field.
224
+ *
225
+ * A multi-term query matches when any term matches, so a descriptive phrase never
226
+ * collapses to zero results; ranking then prefers the styles matching more terms.
227
+ */
228
+ export function matches(style, query) {
229
+ if (looksLikeIds(query)) {
230
+ // Prefix match keeps a near-miss id addressable for the caller's suggestion path.
231
+ const id = String(style.id).toLowerCase();
232
+ return idParts(query).some((part) => id.startsWith(part));
233
+ }
234
+ const terms = tokenize(query);
235
+ if (!terms.length) return true;
236
+ return terms.some(({ term }) => scoreTerm(style, term) > 0);
237
+ }
238
+
239
+ /**
240
+ * Rank styles against a query.
241
+ *
242
+ * Each term scores against weighted fields; the total is summed and a coverage bonus
243
+ * rewards styles that satisfy every term, so "8-bit 复古 街机" ranks the styles that
244
+ * carry all three above those that carry one. Ties keep catalog order for stability.
245
+ */
246
+ export function searchStyles(query, options = {}) {
247
+ const { styles } = catalog();
248
+ const limit = Math.max(1, Math.min(Number(options.limit) || 12, 50));
249
+ const series = options.series ? String(options.series).toUpperCase() : '';
250
+
251
+ // An id-shaped query never leaves the id field: prompts carry hex colours such as
252
+ // `#4b3326`, and a full-text fallback would answer `B3` with unrelated entries.
253
+ if (looksLikeIds(query)) {
254
+ const wanted = idParts(query);
255
+ const exact = [];
256
+ const near = [];
257
+ wanted.forEach((part) => {
258
+ const hit = styles.find((style) => style.id.toLowerCase() === part);
259
+ if (hit) exact.push(hit);
260
+ else styles.forEach((style) => {
261
+ const id = style.id.toLowerCase();
262
+ if (id.startsWith(part) && !near.includes(style)) near.push(style);
263
+ });
264
+ });
265
+ if (exact.length || near.length) return [...exact, ...near].slice(0, limit);
266
+ // A misspelled id ("B33", "L9" when that is absent) still deserves a neighbour,
267
+ // which the caller reports as the near-miss suggestion.
268
+ return nearestIds(wanted[0], limit);
269
+ }
270
+
271
+ const terms = tokenize(query);
272
+ const scored = [];
273
+ styles.forEach((style, index) => {
274
+ if (series && String(style.series).toUpperCase() !== series) return;
275
+ if (!terms.length) {
276
+ scored.push({ style, score: 0, covered: 0, index });
277
+ return;
278
+ }
279
+ let score = 0;
280
+ let covered = 0;
281
+ let weightMatched = 0;
282
+ let weightTotal = 0;
283
+ for (const { term, weight } of terms) {
284
+ weightTotal += weight;
285
+ const hit = scoreTerm(style, term);
286
+ if (hit > 0) {
287
+ // A prompt-only hit still scores, but only a core-field hit proves the style
288
+ // is about the term, so only that counts toward coverage and its bonus.
289
+ if (coreHit(style, term)) {
290
+ covered += 1;
291
+ weightMatched += weight;
292
+ }
293
+ score += hit * weight;
294
+ }
295
+ }
296
+ if (covered === 0) return;
297
+ // Coverage dominates: a style matching every term outranks one matching a single
298
+ // term in a heavy field, which is what a multi-word query actually means.
299
+ const ratio = weightTotal > 0 ? weightMatched / weightTotal : 0;
300
+ scored.push({ style, score: score + ratio * 400, covered, index });
301
+ });
302
+ scored.sort((a, b) => (b.score - a.score) || (b.covered - a.covered) || (a.index - b.index));
303
+ return scored.slice(0, limit).map((entry) => entry.style);
304
+ }
305
+
306
+ /** Look one style up by its exact catalog id, ignoring case. */
307
+ export function styleById(id) {
308
+ const { styles } = catalog();
309
+ const key = String(id || '').trim().toUpperCase();
310
+ return styles.find((style) => style.id === key);
311
+ }
312
+
313
+ /** Project a catalog entry onto what a caller actually needs to see. */
314
+ export function describe(style) {
315
+ return {
316
+ id: style.id,
317
+ name: style.name,
318
+ series: style.series,
319
+ seriesName: style.seriesName,
320
+ category: style.category,
321
+ description: style.desc,
322
+ fit: style.fit,
323
+ tags: style.tags || [],
324
+ hasStill: Boolean(style.still),
325
+ // Empty string rather than null: the tool schema dialect has no nullable union.
326
+ stillUrl: style.still ? `/code-animation-api/still/${style.id}` : '',
327
+ prompt: style.prompt,
328
+ promptChars: (style.prompt || '').length,
329
+ music: defaultMusicFor(style),
330
+ };
331
+ }
332
+
333
+ /**
334
+ * Pick a score for a style.
335
+ *
336
+ * The series carries most of the signal (a Chinese-ink piece and a cyberpunk piece
337
+ * should not get the same bed), with tags refined on top: 3D leans cinematic, and a
338
+ * monochrome plate wants something sparser.
339
+ */
340
+ export function defaultMusicFor(style) {
341
+ const series = String(style?.series || '').toUpperCase();
342
+ const tags = (style?.tags || []).join(' ');
343
+ const text = `${style?.name || ''} ${style?.desc || ''} ${style?.fit || ''}`;
344
+
345
+ if (series === 'Z') return 'guofeng';
346
+ if (/中国风|水墨|敦煌|京剧|皮影|剪纸|年画|青花|书法/.test(text)) return 'guofeng';
347
+ if (/赛博|霓虹|cyber|朋克|末日|故障|合成波|CRT|glitch/i.test(text)) return 'synth';
348
+ if (/B&W|黑白/.test(tags) || /默片|银版|黑白/.test(text)) return 'ambient';
349
+ if (series === 'O' || series === 'L') return 'epic';
350
+ if (/\b3D\b/.test(tags)) return 'epic';
351
+ if (series === 'K' || series === 'M') return 'bright';
352
+ if (series === 'R') return 'synth';
353
+ return 'pulse';
354
+ }