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.
- package/LICENSE +33 -0
- package/README.md +26 -2
- package/client.js +2650 -0
- package/cordis.patch.yml +106 -0
- package/data.js +354 -0
- package/flow.js +338 -0
- package/icon.svg +13 -0
- package/index.js +234 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +50 -4
- package/stills/A1.jpg +0 -0
- package/stills/A2.jpg +0 -0
- package/stills/A3.jpg +0 -0
- package/stills/A4.jpg +0 -0
- package/stills/A5.jpg +0 -0
- package/stills/B1.jpg +0 -0
- package/stills/B2.jpg +0 -0
- package/stills/B3.jpg +0 -0
- package/stills/B4.jpg +0 -0
- package/stills/B5.jpg +0 -0
- package/stills/C1.jpg +0 -0
- package/stills/C2.jpg +0 -0
- package/stills/C3.jpg +0 -0
- package/stills/C4.jpg +0 -0
- package/stills/D1.jpg +0 -0
- package/stills/D2.jpg +0 -0
- package/stills/D3.jpg +0 -0
- package/stills/D4.jpg +0 -0
- package/stills/E1.jpg +0 -0
- package/stills/E2.jpg +0 -0
- package/stills/E3.jpg +0 -0
- package/stills/F1.jpg +0 -0
- package/stills/F2.jpg +0 -0
- package/stills/F3.jpg +0 -0
- package/stills/G1.jpg +0 -0
- package/stills/G2.jpg +0 -0
- package/stills/H1.jpg +0 -0
- package/stills/H10.jpg +0 -0
- package/stills/H11.jpg +0 -0
- package/stills/H12.jpg +0 -0
- package/stills/H2.jpg +0 -0
- package/stills/H3.jpg +0 -0
- package/stills/H4.jpg +0 -0
- package/stills/H5.jpg +0 -0
- package/stills/H6.jpg +0 -0
- package/stills/H7.jpg +0 -0
- package/stills/H8.jpg +0 -0
- package/stills/H9.jpg +0 -0
- package/stills/K1.jpg +0 -0
- package/stills/K10.jpg +0 -0
- package/stills/K11.jpg +0 -0
- package/stills/K12.jpg +0 -0
- package/stills/K13.jpg +0 -0
- package/stills/K14.jpg +0 -0
- package/stills/K15.jpg +0 -0
- package/stills/K16.jpg +0 -0
- package/stills/K17.jpg +0 -0
- package/stills/K18.jpg +0 -0
- package/stills/K19.jpg +0 -0
- package/stills/K2.jpg +0 -0
- package/stills/K20.jpg +0 -0
- package/stills/K21.jpg +0 -0
- package/stills/K22.jpg +0 -0
- package/stills/K23.jpg +0 -0
- package/stills/K24.jpg +0 -0
- package/stills/K25.jpg +0 -0
- package/stills/K26.jpg +0 -0
- package/stills/K27.jpg +0 -0
- package/stills/K28.jpg +0 -0
- package/stills/K29.jpg +0 -0
- package/stills/K3.jpg +0 -0
- package/stills/K30.jpg +0 -0
- package/stills/K31.jpg +0 -0
- package/stills/K32.jpg +0 -0
- package/stills/K33.jpg +0 -0
- package/stills/K34.jpg +0 -0
- package/stills/K35.jpg +0 -0
- package/stills/K36.jpg +0 -0
- package/stills/K37.jpg +0 -0
- package/stills/K38.jpg +0 -0
- package/stills/K4.jpg +0 -0
- package/stills/K5.jpg +0 -0
- package/stills/K6.jpg +0 -0
- package/stills/K7.jpg +0 -0
- package/stills/K8.jpg +0 -0
- package/stills/K9.jpg +0 -0
- package/stills/L1.jpg +0 -0
- package/stills/L10.jpg +0 -0
- package/stills/L11.jpg +0 -0
- package/stills/L12.jpg +0 -0
- package/stills/L13.jpg +0 -0
- package/stills/L14.jpg +0 -0
- package/stills/L15.jpg +0 -0
- package/stills/L16.jpg +0 -0
- package/stills/L17.jpg +0 -0
- package/stills/L18.jpg +0 -0
- package/stills/L19.jpg +0 -0
- package/stills/L2.jpg +0 -0
- package/stills/L20.jpg +0 -0
- package/stills/L21.jpg +0 -0
- package/stills/L22.jpg +0 -0
- package/stills/L23.jpg +0 -0
- package/stills/L24.jpg +0 -0
- package/stills/L25.jpg +0 -0
- package/stills/L26.jpg +0 -0
- package/stills/L27.jpg +0 -0
- package/stills/L28.jpg +0 -0
- package/stills/L29.jpg +0 -0
- package/stills/L3.jpg +0 -0
- package/stills/L30.jpg +0 -0
- package/stills/L31.jpg +0 -0
- package/stills/L32.jpg +0 -0
- package/stills/L33.jpg +0 -0
- package/stills/L34.jpg +0 -0
- package/stills/L35.jpg +0 -0
- package/stills/L36.jpg +0 -0
- package/stills/L37.jpg +0 -0
- package/stills/L38.jpg +0 -0
- package/stills/L39.jpg +0 -0
- package/stills/L4.jpg +0 -0
- package/stills/L40.jpg +0 -0
- package/stills/L41.jpg +0 -0
- package/stills/L42.jpg +0 -0
- package/stills/L43.jpg +0 -0
- package/stills/L44.jpg +0 -0
- package/stills/L45.jpg +0 -0
- package/stills/L46.jpg +0 -0
- package/stills/L47.jpg +0 -0
- package/stills/L48.jpg +0 -0
- package/stills/L49.jpg +0 -0
- package/stills/L5.jpg +0 -0
- package/stills/L50.jpg +0 -0
- package/stills/L51.jpg +0 -0
- package/stills/L52.jpg +0 -0
- package/stills/L53.jpg +0 -0
- package/stills/L54.jpg +0 -0
- package/stills/L55.jpg +0 -0
- package/stills/L56.jpg +0 -0
- package/stills/L57.jpg +0 -0
- package/stills/L58.jpg +0 -0
- package/stills/L59.jpg +0 -0
- package/stills/L6.jpg +0 -0
- package/stills/L7.jpg +0 -0
- package/stills/L8.jpg +0 -0
- package/stills/L9.jpg +0 -0
- package/stills/M1.jpg +0 -0
- package/stills/M10.jpg +0 -0
- package/stills/M11.jpg +0 -0
- package/stills/M12.jpg +0 -0
- package/stills/M13.jpg +0 -0
- package/stills/M14.jpg +0 -0
- package/stills/M15.jpg +0 -0
- package/stills/M16.jpg +0 -0
- package/stills/M17.jpg +0 -0
- package/stills/M18.jpg +0 -0
- package/stills/M19.jpg +0 -0
- package/stills/M2.jpg +0 -0
- package/stills/M20.jpg +0 -0
- package/stills/M21.jpg +0 -0
- package/stills/M22.jpg +0 -0
- package/stills/M23.jpg +0 -0
- package/stills/M24.jpg +0 -0
- package/stills/M25.jpg +0 -0
- package/stills/M26.jpg +0 -0
- package/stills/M27.jpg +0 -0
- package/stills/M28.jpg +0 -0
- package/stills/M29.jpg +0 -0
- package/stills/M3.jpg +0 -0
- package/stills/M30.jpg +0 -0
- package/stills/M31.jpg +0 -0
- package/stills/M32.jpg +0 -0
- package/stills/M33.jpg +0 -0
- package/stills/M34.jpg +0 -0
- package/stills/M35.jpg +0 -0
- package/stills/M36.jpg +0 -0
- package/stills/M37.jpg +0 -0
- package/stills/M38.jpg +0 -0
- package/stills/M39.jpg +0 -0
- package/stills/M4.jpg +0 -0
- package/stills/M40.jpg +0 -0
- package/stills/M5.jpg +0 -0
- package/stills/M6.jpg +0 -0
- package/stills/M7.jpg +0 -0
- package/stills/M8.jpg +0 -0
- package/stills/M9.jpg +0 -0
- package/stills/O1.jpg +0 -0
- package/stills/O10.jpg +0 -0
- package/stills/O11.jpg +0 -0
- package/stills/O12.jpg +0 -0
- package/stills/O13.jpg +0 -0
- package/stills/O14.jpg +0 -0
- package/stills/O15.jpg +0 -0
- package/stills/O16.jpg +0 -0
- package/stills/O17.jpg +0 -0
- package/stills/O18.jpg +0 -0
- package/stills/O19.jpg +0 -0
- package/stills/O2.jpg +0 -0
- package/stills/O20.jpg +0 -0
- package/stills/O21.jpg +0 -0
- package/stills/O22.jpg +0 -0
- package/stills/O23.jpg +0 -0
- package/stills/O24.jpg +0 -0
- package/stills/O25.jpg +0 -0
- package/stills/O26.jpg +0 -0
- package/stills/O27.jpg +0 -0
- package/stills/O28.jpg +0 -0
- package/stills/O29.jpg +0 -0
- package/stills/O3.jpg +0 -0
- package/stills/O30.jpg +0 -0
- package/stills/O31.jpg +0 -0
- package/stills/O32.jpg +0 -0
- package/stills/O33.jpg +0 -0
- package/stills/O34.jpg +0 -0
- package/stills/O35.jpg +0 -0
- package/stills/O36.jpg +0 -0
- package/stills/O37.jpg +0 -0
- package/stills/O38.jpg +0 -0
- package/stills/O39.jpg +0 -0
- package/stills/O4.jpg +0 -0
- package/stills/O40.jpg +0 -0
- package/stills/O41.jpg +0 -0
- package/stills/O42.jpg +0 -0
- package/stills/O43.jpg +0 -0
- package/stills/O44.jpg +0 -0
- package/stills/O45.jpg +0 -0
- package/stills/O46.jpg +0 -0
- package/stills/O47.jpg +0 -0
- package/stills/O48.jpg +0 -0
- package/stills/O49.jpg +0 -0
- package/stills/O5.jpg +0 -0
- package/stills/O50.jpg +0 -0
- package/stills/O51.jpg +0 -0
- package/stills/O52.jpg +0 -0
- package/stills/O53.jpg +0 -0
- package/stills/O54.jpg +0 -0
- package/stills/O55.jpg +0 -0
- package/stills/O56.jpg +0 -0
- package/stills/O57.jpg +0 -0
- package/stills/O58.jpg +0 -0
- package/stills/O59.jpg +0 -0
- package/stills/O6.jpg +0 -0
- package/stills/O60.jpg +0 -0
- package/stills/O61.jpg +0 -0
- package/stills/O62.jpg +0 -0
- package/stills/O63.jpg +0 -0
- package/stills/O64.jpg +0 -0
- package/stills/O65.jpg +0 -0
- package/stills/O66.jpg +0 -0
- package/stills/O67.jpg +0 -0
- package/stills/O68.jpg +0 -0
- package/stills/O69.jpg +0 -0
- package/stills/O7.jpg +0 -0
- package/stills/O70.jpg +0 -0
- package/stills/O71.jpg +0 -0
- package/stills/O72.jpg +0 -0
- package/stills/O73.jpg +0 -0
- package/stills/O74.jpg +0 -0
- package/stills/O75.jpg +0 -0
- package/stills/O76.jpg +0 -0
- package/stills/O77.jpg +0 -0
- package/stills/O78.jpg +0 -0
- package/stills/O79.jpg +0 -0
- package/stills/O8.jpg +0 -0
- package/stills/O80.jpg +0 -0
- package/stills/O81.jpg +0 -0
- package/stills/O82.jpg +0 -0
- package/stills/O83.jpg +0 -0
- package/stills/O84.jpg +0 -0
- package/stills/O85.jpg +0 -0
- package/stills/O86.jpg +0 -0
- package/stills/O87.jpg +0 -0
- package/stills/O88.jpg +0 -0
- package/stills/O89.jpg +0 -0
- package/stills/O9.jpg +0 -0
- package/stills/O90.jpg +0 -0
- package/stills/O91.jpg +0 -0
- package/stills/O92.jpg +0 -0
- package/stills/O93.jpg +0 -0
- package/stills/O94.jpg +0 -0
- package/stills/O95.jpg +0 -0
- package/stills/O96.jpg +0 -0
- package/stills/O97.jpg +0 -0
- package/stills/O98.jpg +0 -0
- package/stills/R1.jpg +0 -0
- package/stills/R2.jpg +0 -0
- package/stills/R3.jpg +0 -0
- package/stills/R4.jpg +0 -0
- package/stills/R5.jpg +0 -0
- package/stills/R6.jpg +0 -0
- package/stills/R7.jpg +0 -0
- package/stills/V1.jpg +0 -0
- package/stills/V10.jpg +0 -0
- package/stills/V11.jpg +0 -0
- package/stills/V12.jpg +0 -0
- package/stills/V13.jpg +0 -0
- package/stills/V14.jpg +0 -0
- package/stills/V15.jpg +0 -0
- package/stills/V2.jpg +0 -0
- package/stills/V3.jpg +0 -0
- package/stills/V4.jpg +0 -0
- package/stills/V5.jpg +0 -0
- package/stills/V6.jpg +0 -0
- package/stills/V7.jpg +0 -0
- package/stills/V8.jpg +0 -0
- package/stills/V9.jpg +0 -0
- package/stills/X1.jpg +0 -0
- package/stills/X10.jpg +0 -0
- package/stills/X11.jpg +0 -0
- package/stills/X12.jpg +0 -0
- package/stills/X13.jpg +0 -0
- package/stills/X14.jpg +0 -0
- package/stills/X15.jpg +0 -0
- package/stills/X16.jpg +0 -0
- package/stills/X17.jpg +0 -0
- package/stills/X18.jpg +0 -0
- package/stills/X19.jpg +0 -0
- package/stills/X2.jpg +0 -0
- package/stills/X20.jpg +0 -0
- package/stills/X21.jpg +0 -0
- package/stills/X22.jpg +0 -0
- package/stills/X23.jpg +0 -0
- package/stills/X24.jpg +0 -0
- package/stills/X25.jpg +0 -0
- package/stills/X26.jpg +0 -0
- package/stills/X27.jpg +0 -0
- package/stills/X28.jpg +0 -0
- package/stills/X29.jpg +0 -0
- package/stills/X3.jpg +0 -0
- package/stills/X30.jpg +0 -0
- package/stills/X31.jpg +0 -0
- package/stills/X4.jpg +0 -0
- package/stills/X5.jpg +0 -0
- package/stills/X6.jpg +0 -0
- package/stills/X7.jpg +0 -0
- package/stills/X8.jpg +0 -0
- package/stills/X9.jpg +0 -0
- package/stills/Z1.jpg +0 -0
- package/stills/Z10.jpg +0 -0
- package/stills/Z11.jpg +0 -0
- package/stills/Z12.jpg +0 -0
- package/stills/Z13.jpg +0 -0
- package/stills/Z14.jpg +0 -0
- package/stills/Z15.jpg +0 -0
- package/stills/Z2.jpg +0 -0
- package/stills/Z3.jpg +0 -0
- package/stills/Z4.jpg +0 -0
- package/stills/Z5.jpg +0 -0
- package/stills/Z6.jpg +0 -0
- package/stills/Z7.jpg +0 -0
- package/stills/Z8.jpg +0 -0
- package/stills/Z9.jpg +0 -0
- package/stills/approved.json +198 -0
- package/stills/index.json +1 -0
- package/styles.json +5090 -0
- package/tools.js +788 -0
package/cordis.patch.yml
ADDED
|
@@ -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
|
+
}
|