@xiaobuyu/nodesign 0.0.1 → 0.0.2

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 (282) hide show
  1. package/README.md +156 -106
  2. package/bin/nodesign.js +0 -0
  3. package/package.json +1 -1
  4. package/server/api/README.md +45 -45
  5. package/server/api/_guard.js +75 -75
  6. package/server/api/admin.js +187 -187
  7. package/server/api/assets/docx-page.js +103 -103
  8. package/server/api/assets/helpers.js +22 -22
  9. package/server/api/assets/notes.js +144 -144
  10. package/server/api/assets.js +896 -896
  11. package/server/api/board.js +57 -57
  12. package/server/api/browse.js +138 -138
  13. package/server/api/canvas.js +444 -444
  14. package/server/api/chatai.js +324 -324
  15. package/server/api/exports/build-standalone.js +980 -980
  16. package/server/api/exports/cards.js +118 -118
  17. package/server/api/exports/docx-pdf.js +46 -46
  18. package/server/api/exports/export-page.js +142 -142
  19. package/server/api/exports/handoff.js +147 -147
  20. package/server/api/exports.js +690 -690
  21. package/server/api/instruction.js +75 -75
  22. package/server/api/me.js +116 -116
  23. package/server/api/memory.js +185 -185
  24. package/server/api/pending-changes.js +366 -366
  25. package/server/api/plugins.js +164 -164
  26. package/server/api/projects.js +220 -220
  27. package/server/api/publish.js +64 -64
  28. package/server/api/recent.js +73 -73
  29. package/server/api/sessions.js +589 -589
  30. package/server/api/skills.js +77 -77
  31. package/server/api/standalone-fit.js +168 -168
  32. package/server/api/turn-compose.js +180 -180
  33. package/server/api/turn-inflight.js +44 -44
  34. package/server/auth/README.md +17 -17
  35. package/server/auth/middleware.js +138 -138
  36. package/server/auth/origin-guard.js +71 -71
  37. package/server/auth/session.js +108 -108
  38. package/server/auth/tier.js +113 -113
  39. package/server/auth/users-store.js +348 -348
  40. package/server/edit/README.md +29 -29
  41. package/server/engine/README.md +113 -113
  42. package/server/engine/agent/agent-shared.js +550 -550
  43. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
  44. package/server/engine/agent/auto-mode-rules.js +78 -78
  45. package/server/engine/agent/context.js +418 -418
  46. package/server/engine/agent/events.js +464 -464
  47. package/server/engine/agent/hooks/canvas-validate.js +199 -199
  48. package/server/engine/agent/hooks/failure.js +120 -120
  49. package/server/engine/agent/hooks/file-events.js +75 -75
  50. package/server/engine/agent/hooks/lifecycle.js +233 -233
  51. package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
  52. package/server/engine/agent/hooks/post-guidance.js +206 -206
  53. package/server/engine/agent/hooks/post-subagent-report.js +104 -104
  54. package/server/engine/agent/hooks/post-trim.js +58 -58
  55. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
  56. package/server/engine/agent/hooks/pre-defaults.js +84 -84
  57. package/server/engine/agent/hooks/pre-injectors.js +277 -277
  58. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
  59. package/server/engine/agent/hooks/pre-starter-files.js +61 -61
  60. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
  61. package/server/engine/agent/hooks/site-validate.js +94 -94
  62. package/server/engine/agent/hooks/tool-prompts.js +31 -31
  63. package/server/engine/agent/hooks/turn-state-memory.js +56 -56
  64. package/server/engine/agent/hooks/user-prompt-submit.js +215 -215
  65. package/server/engine/agent/hooks.js +354 -354
  66. package/server/engine/agent/init-contract.js +69 -69
  67. package/server/engine/agent/isolation.js +159 -159
  68. package/server/engine/agent/model-table.js +292 -292
  69. package/server/engine/agent/plugin-loader.js +266 -266
  70. package/server/engine/agent/prompts/nodesign-prelude.md +336 -336
  71. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
  72. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
  73. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
  74. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
  75. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
  76. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
  77. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
  78. package/server/engine/agent/prompts/tools/site-reference.md +106 -106
  79. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
  80. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
  81. package/server/engine/agent/session-loop.js +1024 -1024
  82. package/server/engine/agent/session-model.js +164 -164
  83. package/server/engine/agent/skill.js +204 -204
  84. package/server/engine/agent/task-events.js +78 -78
  85. package/server/engine/agents/ds-extractor.md +132 -132
  86. package/server/engine/agents/explorer.md +190 -190
  87. package/server/engine/agents/index.js +230 -230
  88. package/server/engine/agents/schemas/design-system.json +156 -156
  89. package/server/engine/agents/schemas/tweak-schema.json +113 -113
  90. package/server/engine/agents/tweak-proposer.md +127 -127
  91. package/server/engine/agents/vision-checker.md +254 -254
  92. package/server/engine/browse/capture.js +422 -422
  93. package/server/engine/browse/card.js +137 -137
  94. package/server/engine/browse/handover.js +59 -59
  95. package/server/engine/browse/page-digest.js +119 -119
  96. package/server/engine/browse/refs.js +196 -196
  97. package/server/engine/browse/registry.js +307 -307
  98. package/server/engine/browse/screencast.js +170 -170
  99. package/server/engine/browse/state.js +121 -121
  100. package/server/engine/chatai/chat-log.js +79 -79
  101. package/server/engine/chatai/index.js +193 -193
  102. package/server/engine/chatai/openai-compat.js +152 -152
  103. package/server/engine/chatai/orchestrate.js +309 -309
  104. package/server/engine/chatai/perform.js +40 -40
  105. package/server/engine/chatai/summarize.js +79 -79
  106. package/server/engine/mcp/capability-gate.js +52 -52
  107. package/server/engine/mcp/index.js +339 -339
  108. package/server/engine/mcp/param-sanitizer.js +142 -142
  109. package/server/engine/mcp/tools/arrange-on-board.js +124 -124
  110. package/server/engine/mcp/tools/artifact-session.js +278 -278
  111. package/server/engine/mcp/tools/browse-computer.js +421 -421
  112. package/server/engine/mcp/tools/browse-find-batch.js +183 -183
  113. package/server/engine/mcp/tools/browse-screenshot.js +157 -157
  114. package/server/engine/mcp/tools/browse.js +480 -480
  115. package/server/engine/mcp/tools/build-docx.js +101 -101
  116. package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
  117. package/server/engine/mcp/tools/create-on-board.js +155 -155
  118. package/server/engine/mcp/tools/crystallize-skill.js +168 -168
  119. package/server/engine/mcp/tools/explain-style.js +237 -237
  120. package/server/engine/mcp/tools/export-handoff.js +133 -133
  121. package/server/engine/mcp/tools/expose-tweaks.js +149 -149
  122. package/server/engine/mcp/tools/generate-image.js +842 -842
  123. package/server/engine/mcp/tools/get-computed-styles.js +164 -164
  124. package/server/engine/mcp/tools/get-pending-changes.js +205 -205
  125. package/server/engine/mcp/tools/h3box-ssh.js +84 -84
  126. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
  127. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
  128. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
  129. package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
  130. package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
  131. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
  132. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
  133. package/server/engine/mcp/tools/highlight.js +68 -68
  134. package/server/engine/mcp/tools/list-pages.js +222 -222
  135. package/server/engine/mcp/tools/lookup-tags.js +59 -59
  136. package/server/engine/mcp/tools/navigate-to-page.js +60 -60
  137. package/server/engine/mcp/tools/organize-board.js +64 -64
  138. package/server/engine/mcp/tools/paint-still.js +434 -434
  139. package/server/engine/mcp/tools/pin-to-board.js +130 -130
  140. package/server/engine/mcp/tools/preview-deck.js +130 -130
  141. package/server/engine/mcp/tools/profile-scroll.js +305 -305
  142. package/server/engine/mcp/tools/publish-site.js +102 -102
  143. package/server/engine/mcp/tools/query-elements.js +195 -195
  144. package/server/engine/mcp/tools/read-board.js +103 -103
  145. package/server/engine/mcp/tools/read-document.js +93 -93
  146. package/server/engine/mcp/tools/read-page.js +264 -264
  147. package/server/engine/mcp/tools/read-tavern-json.js +143 -143
  148. package/server/engine/mcp/tools/record-decision.js +140 -140
  149. package/server/engine/mcp/tools/relate-on-board.js +110 -110
  150. package/server/engine/mcp/tools/report-issue.js +115 -115
  151. package/server/engine/mcp/tools/roll-film.js +266 -266
  152. package/server/engine/mcp/tools/screenshot-docx.js +138 -138
  153. package/server/engine/mcp/tools/screenshot-url.js +195 -195
  154. package/server/engine/mcp/tools/screenshot.js +583 -583
  155. package/server/engine/mcp/tools/tier-gate.js +96 -96
  156. package/server/engine/mcp/tools/trace-motion.js +215 -215
  157. package/server/engine/mcp/tools/web-search.js +548 -548
  158. package/server/engine/motion/inventory.js +349 -349
  159. package/server/engine/perception/session.js +235 -235
  160. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
  161. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -192
  162. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
  163. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
  164. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
  165. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
  166. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
  167. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
  168. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
  169. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -162
  170. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
  171. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
  172. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -228
  173. package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -78
  174. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
  175. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -323
  176. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
  177. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
  178. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
  179. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
  180. package/server/engine/runs/active-runs.js +698 -698
  181. package/server/engine/runs/live-turn.js +252 -252
  182. package/server/engine/runs/store.js +364 -364
  183. package/server/engine/runs/turn-relay.js +217 -217
  184. package/server/engine/runtime/workspace.js +164 -164
  185. package/server/lib/artifact-file-path.js +80 -80
  186. package/server/lib/artifact-target.js +284 -284
  187. package/server/lib/asset-refs.js +239 -239
  188. package/server/lib/async-queue.js +104 -104
  189. package/server/lib/auto-relations.js +130 -130
  190. package/server/lib/binding-types.js +60 -60
  191. package/server/lib/board-kind-sizes.js +62 -62
  192. package/server/lib/board-relations.js +133 -133
  193. package/server/lib/browse-proxy.js +180 -180
  194. package/server/lib/canvas-id.js +36 -36
  195. package/server/lib/cover.js +227 -227
  196. package/server/lib/danbooru-tags.js +291 -291
  197. package/server/lib/doc-extract.js +156 -156
  198. package/server/lib/docx/build-from-source.js +260 -260
  199. package/server/lib/docx/build.js +490 -490
  200. package/server/lib/docx/dump-styles.js +287 -287
  201. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
  202. package/server/lib/docx/merge-runs.js +129 -129
  203. package/server/lib/docx/numbering.js +218 -218
  204. package/server/lib/docx/order.js +152 -152
  205. package/server/lib/docx/rawzip.js +172 -172
  206. package/server/lib/docx/render.js +92 -92
  207. package/server/lib/docx/text-lint.js +225 -225
  208. package/server/lib/docx/tokens.js +335 -335
  209. package/server/lib/docx/units.js +29 -29
  210. package/server/lib/docx/xml.js +291 -291
  211. package/server/lib/docx-pages.js +187 -187
  212. package/server/lib/export-collect.js +289 -289
  213. package/server/lib/export-package.js +210 -210
  214. package/server/lib/html-srcset.js +145 -145
  215. package/server/lib/image-variant.js +460 -460
  216. package/server/lib/ingress/forward-openai-chat.js +305 -305
  217. package/server/lib/ingress/openai-chat.js +491 -491
  218. package/server/lib/ingress/session-notice.js +66 -66
  219. package/server/lib/ingress/session-routes.js +68 -68
  220. package/server/lib/ingress/slot-probe.js +130 -130
  221. package/server/lib/ingress/upstream-billing.js +52 -52
  222. package/server/lib/ingress/upstream-fail-streak.js +72 -72
  223. package/server/lib/ingress/upstream-truncation.js +48 -48
  224. package/server/lib/issues-store.js +182 -182
  225. package/server/lib/kinds/deck.js +72 -72
  226. package/server/lib/kinds/docx.js +283 -283
  227. package/server/lib/kinds/file-kinds.js +113 -113
  228. package/server/lib/kinds/index.js +202 -202
  229. package/server/lib/kinds/site.js +161 -161
  230. package/server/lib/model-ingress.js +543 -543
  231. package/server/lib/moderation.js +255 -255
  232. package/server/lib/notice-store.js +103 -103
  233. package/server/lib/plugin-install.js +104 -104
  234. package/server/lib/plugin-validator.js +721 -721
  235. package/server/lib/publish-store.js +115 -115
  236. package/server/lib/quick-summary.js +52 -52
  237. package/server/lib/quota.js +270 -270
  238. package/server/lib/rate-window.js +29 -29
  239. package/server/lib/reference-assets.js +92 -92
  240. package/server/lib/region-shot.js +135 -135
  241. package/server/lib/safe-path.js +73 -73
  242. package/server/lib/sdk-session.js +33 -33
  243. package/server/lib/showcase-store.js +92 -92
  244. package/server/lib/site-publish.js +465 -465
  245. package/server/lib/ssrf-guard.js +432 -432
  246. package/server/lib/task-scan.js +158 -158
  247. package/server/lib/tavern-json.js +154 -154
  248. package/server/lib/video-variant.js +237 -237
  249. package/server/lib/workspace-path.js +33 -33
  250. package/server/ops/fix-sdk-musl.mjs +40 -40
  251. package/server/ops/install-macos-fonts.sh +114 -114
  252. package/server/ops/macos-fonts.conf +336 -336
  253. package/server/ops/sandbox-shim/bwrap +57 -57
  254. package/server/projects/assets-summary.js +119 -119
  255. package/server/projects/auto-name.js +53 -53
  256. package/server/projects/board-store.js +640 -640
  257. package/server/projects/move-entry.js +103 -103
  258. package/server/projects/store.js +270 -270
  259. package/server/projects/ui-config.js +54 -54
  260. package/server/projects/workspace-templates.js +53 -53
  261. package/server/projects/workspace.js +1025 -1025
  262. package/server/runtime/local-config.js +177 -177
  263. package/server/runtime/profile.js +72 -72
  264. package/server/services/rembg-service.py +334 -334
  265. package/server/shared/README.md +25 -25
  266. package/server/shared/deck.js +54 -54
  267. package/server/shared/time.js +49 -49
  268. package/server/style-pipeline/README.md +53 -53
  269. package/server/ws/broker.js +36 -36
  270. package/server/ws/browse-channel.js +177 -177
  271. package/server/ws/index.js +386 -386
  272. package/web/README.md +89 -89
  273. package/web/dist/assets/BrowserWindow-BYCgizHw.js +11 -0
  274. package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-pNydEM4L.js} +1 -1
  275. package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-PPgzh3Gq.js} +1 -1
  276. package/web/dist/assets/{SiteWindow-D6eS-2gM.js → SiteWindow-DM0LQPCO.js} +1 -1
  277. package/web/dist/assets/{index-Cbl3ATvp.js → index-BHTBJlpY.js} +32 -32
  278. package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-C0I13CbD.js} +1 -1
  279. package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-B1UQ8Hld.js} +1 -1
  280. package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-8lrgqmfv.js} +1 -1
  281. package/web/dist/index.html +17 -17
  282. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
@@ -1,158 +1,158 @@
1
- /**
2
- * task-scan.js — 任务目录的统一扫描规则(2026-07-29)
3
- *
4
- * 站点接进真实工程形态(构建步骤 / node_modules / 源目录)后,「扫任务目录」
5
- * 这件事在五个地方各写各的:页面清单、整站 zip、断链检测、导出勾选、素材墙。
6
- * 五份跳过规则各自为政的结果是:node_modules 被整站 zip 原样打包、构建缓存
7
- * 触发刷新记账 —— 而且每一处都是静默的。所以收拢到这一个模块。
8
- *
9
- * 两层规则:
10
- * ① 硬清单 —— 永远不看的目录(依赖 / 构建缓存 / VCS)。不可配置,
11
- * 因为「不小心把 node_modules 打进 zip」不该是一个能配出来的状态。
12
- * ② `.ndignore` —— 任务根下的忽略档,gitignore 语法子集,agent 自己写。
13
- * 支持:# 注释、尾 `/`(只匹配目录)、头 `/`(锚定任务根)、`*` `?` `**`。
14
- * 不支持 `!` 反选(要精细控制就少写点通配)。
15
- */
16
-
17
- import path from 'node:path';
18
- import fs from 'node:fs/promises';
19
-
20
- /**
21
- * 工作区里**不是产物**的那些名字。形态扫描(deck / site)一律跳过。
22
- *
23
- * 2026-08-07 扁平化之后才需要这张表:以前形态扫描的根是 `tasks/<任务>/`,
24
- * 一个只装产物的干净目录;现在根是项目工作区,产物跟基础设施住在一起。
25
- * 不挡的话有两个实测会发生的假产物:
26
- * - `canvas.template.html` / `site.template.html`(skill 拷进 cwd 的起手
27
- * 模板)会各自变成一张 deck 卡 —— deck 认"顶层任意 .html";
28
- * - `exports/` 里导出过一个站就会多出一张站点卡 —— 站点认"子目录里有
29
- * index.html",而导出包正好长这样。
30
- *
31
- * 点开头的(.claude / .nd / .git)扫描器本来就跳,不用列。
32
- */
33
- // `agent-memory` 是老符号链接时代的残骸目录名(正规位置是 .claude/agent-memory/),
34
- // 留在这里是为了它万一还在也不会作为一张文件夹卡出现在桌面上。
35
- export const RESERVED_DIRS = new Set(['assets', 'exports', 'notes', 'node_modules', 'agent-memory']);
36
-
37
- /**
38
- * 不当产物看的文件:起手模板(拷进来是给 agent 抄/改的,不是成品)。
39
- * 2026-08-15 加 js/mjs —— RP 的管线模块 `演出.template.js` 走同一条起手文件路,
40
- * 规则只认 html/css 的话它会当成一张产物卡上墙。
41
- */
42
- export function isReservedFile(name) {
43
- return /\.template\.(html?|css|jsx?|mjs)$/i.test(name) || RESERVED_FILES.has(name);
44
- }
45
-
46
- /**
47
- * 基础设施文件:住在工作区里,但**不是用户的东西**,不该在桌面上出现。
48
- *
49
- * 2026-08-08 加。在这之前只挡起手模板,于是 `board.json`(画布自己的布局档)
50
- * 会作为一张 .json 文件卡上墙 —— 画布把自己的存档画在自己身上。
51
- */
52
- export const RESERVED_FILES = new Set([
53
- 'board.json', // 画布布局,画布自己的存档
54
- '.nd-project.json', // 产物标记(形态兜底)
55
- '.gitignore',
56
- '.ndignore',
57
- ]);
58
-
59
- /** 永远不扫的目录名(任何深度命中即剪枝) */
60
- export const HARD_IGNORE_DIRS = new Set([
61
- 'node_modules', '.git', '.wrangler', '.cache', '.next', '.astro', '.nuxt',
62
- '.svelte-kit', '.output', '.parcel-cache', '.turbo', '.vite',
63
- 'venv', '.venv', '__pycache__', 'coverage',
64
- ]);
65
-
66
- /** 展品目录:站点的试作放这里,单独渲染成卡,不算站点页面、不进 zip */
67
- export const DRAFTS_DIR = '_drafts';
68
-
69
- export const NDIGNORE_FILE = '.ndignore';
70
-
71
- /** gitignore 子集 → RegExp。认不出的行按字面量处理(转义后精确匹配)。 */
72
- function compilePattern(line) {
73
- let pat = line.trim();
74
- if (!pat || pat.startsWith('#')) return null;
75
- const dirOnly = pat.endsWith('/');
76
- if (dirOnly) pat = pat.slice(0, -1);
77
- const anchored = pat.startsWith('/');
78
- if (anchored) pat = pat.slice(1);
79
- // 转义正则元字符,再还原通配语义(** → 任意层,* → 段内任意,? → 段内单字符)
80
- let re = pat
81
- .replace(/[.+^${}()|[\]\\]/g, '\\$&')
82
- .replace(/\*\*/g, '\uE000')
83
- .replace(/\*/g, '[^/]*')
84
- .replace(/\?/g, '[^/]')
85
- .replace(/\uE000/g, '.*');
86
- // 无斜杠的裸模式(`*.bak`)匹配任何一层的条目名;带斜杠 / 锚定的按路径匹配
87
- const body = (!anchored && !pat.includes('/')) ? `(?:^|/)${re}` : `^${re}`;
88
- return { re: new RegExp(`${body}(?:/|$)`), dirOnly };
89
- }
90
-
91
- /**
92
- * 读任务的 `.ndignore`,返回匹配器 `(rel, isDir) => boolean`。
93
- * rel 相对任务根、'/' 分隔、不带前导斜杠。没有忽略档 → 只有硬清单生效。
94
- */
95
- export async function loadIgnore(taskDir) {
96
- let rules = [];
97
- try {
98
- const raw = await fs.readFile(path.join(taskDir, NDIGNORE_FILE), 'utf8');
99
- rules = raw.split('\n').map(compilePattern).filter(Boolean);
100
- } catch { /* 没有 .ndignore:正常 */ }
101
- return (rel, isDir = false) => {
102
- const segs = rel.split('/');
103
- // 硬清单:路径里任何一段命中都算(父目录被剪枝时子路径根本走不到这,
104
- // 这里再查一遍是给「直接拿 rel 来问」的调用方兜底)
105
- if (segs.some(s => HARD_IGNORE_DIRS.has(s))) return true;
106
- for (const r of rules) {
107
- if (r.dirOnly && !isDir && !r.re.test(rel + '/')) continue;
108
- if (r.re.test(rel)) return true;
109
- }
110
- return false;
111
- };
112
- }
113
-
114
- /**
115
- * 递归列任务目录下的文件(应用硬清单 + .ndignore + 跳 dot 条目)。
116
- *
117
- * @param {string} rootDir 绝对路径(任务根或产物根)
118
- * @param {object} [opts]
119
- * @param {number} [opts.maxDepth=6]
120
- * @param {Function} [opts.ignore] loadIgnore 的返回值;不传就现读
121
- * @param {string} [opts.ignoreBase] ignore 规则的基准目录(缺省 = rootDir)。
122
- * 产物根是子目录时,.ndignore 在任务根,
123
- * rel 要先换算回任务根视角再问匹配器。
124
- * @param {boolean} [opts.includeDrafts=false] 是否进 `_drafts/`
125
- * @returns {Promise<Array<{rel: string, abs: string, name: string, size: number}>>}
126
- */
127
- export async function walkTaskFiles(rootDir, opts = {}) {
128
- const { maxDepth = 6, includeDrafts = false } = opts;
129
- const ignoreBase = opts.ignoreBase || rootDir;
130
- const ignore = opts.ignore || await loadIgnore(ignoreBase);
131
- const out = [];
132
- const toIgnoreRel = (abs) => path.relative(ignoreBase, abs).split(path.sep).join('/');
133
-
134
- const walk = async (dir, prefix, depth) => {
135
- if (depth > maxDepth) return;
136
- let entries;
137
- try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { return; }
138
- for (const e of entries) {
139
- if (e.name.startsWith('.')) continue;
140
- const abs = path.join(dir, e.name);
141
- const rel = prefix ? `${prefix}/${e.name}` : e.name;
142
- if (e.isDirectory()) {
143
- if (HARD_IGNORE_DIRS.has(e.name)) continue;
144
- if (!includeDrafts && e.name === DRAFTS_DIR) continue;
145
- if (ignore(toIgnoreRel(abs), true)) continue;
146
- await walk(abs, rel, depth + 1);
147
- continue;
148
- }
149
- if (!e.isFile()) continue;
150
- if (ignore(toIgnoreRel(abs), false)) continue;
151
- let size = 0;
152
- try { size = (await fs.stat(abs)).size; } catch { continue; }
153
- out.push({ rel, abs, name: e.name, size });
154
- }
155
- };
156
- await walk(rootDir, '', 1);
157
- return out;
158
- }
1
+ /**
2
+ * task-scan.js — 任务目录的统一扫描规则(2026-07-29)
3
+ *
4
+ * 站点接进真实工程形态(构建步骤 / node_modules / 源目录)后,「扫任务目录」
5
+ * 这件事在五个地方各写各的:页面清单、整站 zip、断链检测、导出勾选、素材墙。
6
+ * 五份跳过规则各自为政的结果是:node_modules 被整站 zip 原样打包、构建缓存
7
+ * 触发刷新记账 —— 而且每一处都是静默的。所以收拢到这一个模块。
8
+ *
9
+ * 两层规则:
10
+ * ① 硬清单 —— 永远不看的目录(依赖 / 构建缓存 / VCS)。不可配置,
11
+ * 因为「不小心把 node_modules 打进 zip」不该是一个能配出来的状态。
12
+ * ② `.ndignore` —— 任务根下的忽略档,gitignore 语法子集,agent 自己写。
13
+ * 支持:# 注释、尾 `/`(只匹配目录)、头 `/`(锚定任务根)、`*` `?` `**`。
14
+ * 不支持 `!` 反选(要精细控制就少写点通配)。
15
+ */
16
+
17
+ import path from 'node:path';
18
+ import fs from 'node:fs/promises';
19
+
20
+ /**
21
+ * 工作区里**不是产物**的那些名字。形态扫描(deck / site)一律跳过。
22
+ *
23
+ * 2026-08-07 扁平化之后才需要这张表:以前形态扫描的根是 `tasks/<任务>/`,
24
+ * 一个只装产物的干净目录;现在根是项目工作区,产物跟基础设施住在一起。
25
+ * 不挡的话有两个实测会发生的假产物:
26
+ * - `canvas.template.html` / `site.template.html`(skill 拷进 cwd 的起手
27
+ * 模板)会各自变成一张 deck 卡 —— deck 认"顶层任意 .html";
28
+ * - `exports/` 里导出过一个站就会多出一张站点卡 —— 站点认"子目录里有
29
+ * index.html",而导出包正好长这样。
30
+ *
31
+ * 点开头的(.claude / .nd / .git)扫描器本来就跳,不用列。
32
+ */
33
+ // `agent-memory` 是老符号链接时代的残骸目录名(正规位置是 .claude/agent-memory/),
34
+ // 留在这里是为了它万一还在也不会作为一张文件夹卡出现在桌面上。
35
+ export const RESERVED_DIRS = new Set(['assets', 'exports', 'notes', 'node_modules', 'agent-memory']);
36
+
37
+ /**
38
+ * 不当产物看的文件:起手模板(拷进来是给 agent 抄/改的,不是成品)。
39
+ * 2026-08-15 加 js/mjs —— RP 的管线模块 `演出.template.js` 走同一条起手文件路,
40
+ * 规则只认 html/css 的话它会当成一张产物卡上墙。
41
+ */
42
+ export function isReservedFile(name) {
43
+ return /\.template\.(html?|css|jsx?|mjs)$/i.test(name) || RESERVED_FILES.has(name);
44
+ }
45
+
46
+ /**
47
+ * 基础设施文件:住在工作区里,但**不是用户的东西**,不该在桌面上出现。
48
+ *
49
+ * 2026-08-08 加。在这之前只挡起手模板,于是 `board.json`(画布自己的布局档)
50
+ * 会作为一张 .json 文件卡上墙 —— 画布把自己的存档画在自己身上。
51
+ */
52
+ export const RESERVED_FILES = new Set([
53
+ 'board.json', // 画布布局,画布自己的存档
54
+ '.nd-project.json', // 产物标记(形态兜底)
55
+ '.gitignore',
56
+ '.ndignore',
57
+ ]);
58
+
59
+ /** 永远不扫的目录名(任何深度命中即剪枝) */
60
+ export const HARD_IGNORE_DIRS = new Set([
61
+ 'node_modules', '.git', '.wrangler', '.cache', '.next', '.astro', '.nuxt',
62
+ '.svelte-kit', '.output', '.parcel-cache', '.turbo', '.vite',
63
+ 'venv', '.venv', '__pycache__', 'coverage',
64
+ ]);
65
+
66
+ /** 展品目录:站点的试作放这里,单独渲染成卡,不算站点页面、不进 zip */
67
+ export const DRAFTS_DIR = '_drafts';
68
+
69
+ export const NDIGNORE_FILE = '.ndignore';
70
+
71
+ /** gitignore 子集 → RegExp。认不出的行按字面量处理(转义后精确匹配)。 */
72
+ function compilePattern(line) {
73
+ let pat = line.trim();
74
+ if (!pat || pat.startsWith('#')) return null;
75
+ const dirOnly = pat.endsWith('/');
76
+ if (dirOnly) pat = pat.slice(0, -1);
77
+ const anchored = pat.startsWith('/');
78
+ if (anchored) pat = pat.slice(1);
79
+ // 转义正则元字符,再还原通配语义(** → 任意层,* → 段内任意,? → 段内单字符)
80
+ let re = pat
81
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
82
+ .replace(/\*\*/g, '\uE000')
83
+ .replace(/\*/g, '[^/]*')
84
+ .replace(/\?/g, '[^/]')
85
+ .replace(/\uE000/g, '.*');
86
+ // 无斜杠的裸模式(`*.bak`)匹配任何一层的条目名;带斜杠 / 锚定的按路径匹配
87
+ const body = (!anchored && !pat.includes('/')) ? `(?:^|/)${re}` : `^${re}`;
88
+ return { re: new RegExp(`${body}(?:/|$)`), dirOnly };
89
+ }
90
+
91
+ /**
92
+ * 读任务的 `.ndignore`,返回匹配器 `(rel, isDir) => boolean`。
93
+ * rel 相对任务根、'/' 分隔、不带前导斜杠。没有忽略档 → 只有硬清单生效。
94
+ */
95
+ export async function loadIgnore(taskDir) {
96
+ let rules = [];
97
+ try {
98
+ const raw = await fs.readFile(path.join(taskDir, NDIGNORE_FILE), 'utf8');
99
+ rules = raw.split('\n').map(compilePattern).filter(Boolean);
100
+ } catch { /* 没有 .ndignore:正常 */ }
101
+ return (rel, isDir = false) => {
102
+ const segs = rel.split('/');
103
+ // 硬清单:路径里任何一段命中都算(父目录被剪枝时子路径根本走不到这,
104
+ // 这里再查一遍是给「直接拿 rel 来问」的调用方兜底)
105
+ if (segs.some(s => HARD_IGNORE_DIRS.has(s))) return true;
106
+ for (const r of rules) {
107
+ if (r.dirOnly && !isDir && !r.re.test(rel + '/')) continue;
108
+ if (r.re.test(rel)) return true;
109
+ }
110
+ return false;
111
+ };
112
+ }
113
+
114
+ /**
115
+ * 递归列任务目录下的文件(应用硬清单 + .ndignore + 跳 dot 条目)。
116
+ *
117
+ * @param {string} rootDir 绝对路径(任务根或产物根)
118
+ * @param {object} [opts]
119
+ * @param {number} [opts.maxDepth=6]
120
+ * @param {Function} [opts.ignore] loadIgnore 的返回值;不传就现读
121
+ * @param {string} [opts.ignoreBase] ignore 规则的基准目录(缺省 = rootDir)。
122
+ * 产物根是子目录时,.ndignore 在任务根,
123
+ * rel 要先换算回任务根视角再问匹配器。
124
+ * @param {boolean} [opts.includeDrafts=false] 是否进 `_drafts/`
125
+ * @returns {Promise<Array<{rel: string, abs: string, name: string, size: number}>>}
126
+ */
127
+ export async function walkTaskFiles(rootDir, opts = {}) {
128
+ const { maxDepth = 6, includeDrafts = false } = opts;
129
+ const ignoreBase = opts.ignoreBase || rootDir;
130
+ const ignore = opts.ignore || await loadIgnore(ignoreBase);
131
+ const out = [];
132
+ const toIgnoreRel = (abs) => path.relative(ignoreBase, abs).split(path.sep).join('/');
133
+
134
+ const walk = async (dir, prefix, depth) => {
135
+ if (depth > maxDepth) return;
136
+ let entries;
137
+ try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { return; }
138
+ for (const e of entries) {
139
+ if (e.name.startsWith('.')) continue;
140
+ const abs = path.join(dir, e.name);
141
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
142
+ if (e.isDirectory()) {
143
+ if (HARD_IGNORE_DIRS.has(e.name)) continue;
144
+ if (!includeDrafts && e.name === DRAFTS_DIR) continue;
145
+ if (ignore(toIgnoreRel(abs), true)) continue;
146
+ await walk(abs, rel, depth + 1);
147
+ continue;
148
+ }
149
+ if (!e.isFile()) continue;
150
+ if (ignore(toIgnoreRel(abs), false)) continue;
151
+ let size = 0;
152
+ try { size = (await fs.stat(abs)).size; } catch { continue; }
153
+ out.push({ rel, abs, name: e.name, size });
154
+ }
155
+ };
156
+ await walk(rootDir, '', 1);
157
+ return out;
158
+ }
@@ -1,154 +1,154 @@
1
- /**
2
- * tavern-json —— 酒馆(SillyTavern)导出 JSON 的解析与摘要(2026-08-15)
3
- *
4
- * 起因是一份真文件:`Izumi 0814.json`,464KB、210 条提示词,而**启用的只有 56 条**
5
- * (合计约 9.8k 字符)。agent 拿 Read 去读它 = 十几万 token 进上下文换来一堆停用
6
- * 的备选条目 —— 这不是"贵一点",是把上下文烧光还看不清结构。所以搬酒馆的东西
7
- * 进来必须先过这一层:**先给结构摘要,再按需取正文**。
8
- *
9
- * 认三种形态(酒馆导出的 JSON 就这几类):
10
- * preset Chat Completion 预设 —— `prompts[]` + `prompt_order[]`。管的是
11
- * "每次请求怎么拼上下文 + 采样参数",跟我们的 编排.yaml 同类。
12
- * 条目里那些 0 字的是 **marker**(角色描述/性格/场景/Chat History…),
13
- * 运行时由酒馆填真内容,搬过来一律丢掉 —— 我们由 编排.yaml 自己管。
14
- * card 角色卡 V2(`spec: chara_card_v2`)或 V1。人设、开场白、示例对话,
15
- * 可能内嵌世界书(character_book)。
16
- * lorebook 世界书单独导出 —— `entries`,每条带 keys(触发词)与 constant(常驻)。
17
- *
18
- * 只读不写:转成 编排.yaml 和设定文件是 agent 的活,它得按题材决定怎么分组、
19
- * 哪些不要(破甲话术在我们这条通路上没意义)。
20
- */
21
-
22
- /** 摘要里每条正文最多带多少字的引子 */
23
- const PEEK = 60;
24
-
25
- export function detectKind(doc) {
26
- if (!doc || typeof doc !== 'object') return null;
27
- if (Array.isArray(doc.prompts) && Array.isArray(doc.prompt_order)) return 'preset';
28
- if (doc.spec === 'chara_card_v2' || doc.spec === 'chara_card_v3') return 'card';
29
- if (doc.data && typeof doc.data === 'object' && typeof doc.data.first_mes === 'string') return 'card';
30
- if (typeof doc.first_mes === 'string' && typeof doc.description === 'string') return 'card';
31
- if (doc.entries && typeof doc.entries === 'object') return 'lorebook';
32
- return null;
33
- }
34
-
35
- const 引 = (s) => String(s || '').replace(/\s+/g, ' ').trim().slice(0, PEEK);
36
- const 字 = (s) => String(s || '').length;
37
-
38
- /** 预设:启用的条目按 order 顺序排开,停用的只报个数 */
39
- export function digestPreset(doc) {
40
- const byId = new Map((doc.prompts || []).map(p => [p.identifier, p]));
41
- const order = (doc.prompt_order?.[0]?.order) || [];
42
- const 启用 = [];
43
- const 停用 = [];
44
- for (const o of order) {
45
- const p = byId.get(o.identifier);
46
- if (!p) continue;
47
- const 条 = {
48
- id: p.identifier,
49
- 名字: p.name || p.identifier,
50
- 角色: p.role || 'system',
51
- 字数: 字(p.content),
52
- 占位: p.marker === true, // 运行时由酒馆填(角色卡/历史)
53
- 分隔: p.marker !== true && 字(p.content) === 0, // 纯分节标题("可选功能开始"这种)
54
- 深度: p.injection_position === 1 ? (p.injection_depth ?? 0) : null,
55
- 引子: 引(p.content),
56
- };
57
- (o.enabled ? 启用 : 停用).push(条);
58
- }
59
- const 实条 = 启用.filter(e => e.字数 > 0);
60
- return {
61
- 形态: 'preset',
62
- 参数: {
63
- temperature: doc.temperature, top_p: doc.top_p, top_k: doc.top_k,
64
- frequency_penalty: doc.frequency_penalty, presence_penalty: doc.presence_penalty,
65
- 最大上下文: doc.openai_max_context, 最大输出: doc.openai_max_tokens,
66
- reasoning_effort: doc.reasoning_effort, squash_system_messages: doc.squash_system_messages,
67
- },
68
- 启用, 停用,
69
- 合计字数: 实条.reduce((n, e) => n + e.字数, 0),
70
- 占位条目: 启用.filter(e => e.占位).map(e => e.名字),
71
- 分隔条目: 启用.filter(e => e.分隔).map(e => e.名字),
72
- };
73
- }
74
-
75
- /** 角色卡:字段清单 + 内嵌世界书条目表 */
76
- export function digestCard(doc) {
77
- const d = doc.data && typeof doc.data === 'object' ? doc.data : doc;
78
- const 字段 = ['name', 'description', 'personality', 'scenario', 'first_mes', 'mes_example',
79
- 'system_prompt', 'post_history_instructions', 'creator_notes']
80
- .map(k => ({ 字段: k, 字数: 字(d[k]), 引子: 引(d[k]) }))
81
- .filter(x => x.字数 > 0);
82
- const book = d.character_book || doc.character_book || null;
83
- return {
84
- 形态: 'card',
85
- 名字: d.name || '(无名)',
86
- 字段,
87
- 开场白备选: Array.isArray(d.alternate_greetings) ? d.alternate_greetings.length : 0,
88
- 世界书: book ? digestLorebook(book).条目 : [],
89
- };
90
- }
91
-
92
- /** 世界书:每条的触发词、常驻与否、字数 */
93
- export function digestLorebook(doc) {
94
- const raw = doc.entries;
95
- const list = Array.isArray(raw) ? raw : Object.values(raw || {});
96
- const 条目 = list.map((e, i) => ({
97
- id: String(e.uid ?? e.id ?? i),
98
- 名字: e.comment || e.name || (Array.isArray(e.key) ? e.key.join('/') : '') || `条目${i + 1}`,
99
- 触发: Array.isArray(e.key) ? e.key : (Array.isArray(e.keys) ? e.keys : []),
100
- 常驻: !!(e.constant),
101
- 停用: e.enabled === false || e.disable === true,
102
- 字数: 字(e.content),
103
- 引子: 引(e.content),
104
- }));
105
- return { 形态: 'lorebook', 条目 };
106
- }
107
-
108
- export function digest(doc) {
109
- const kind = detectKind(doc);
110
- if (kind === 'preset') return digestPreset(doc);
111
- if (kind === 'card') return digestCard(doc);
112
- if (kind === 'lorebook') return digestLorebook(doc);
113
- return null;
114
- }
115
-
116
- /**
117
- * 按名字/id 取正文。名字支持部分匹配(酒馆的条目名带 emoji,让 agent 原样抄
118
- * 一串 emoji 当参数是给自己找麻烦)。
119
- * @returns {{名字: string, 角色?: string, 正文: string}[]}
120
- */
121
- export function fetchEntries(doc, 选) {
122
- const want = (选 || []).map(s => String(s).trim()).filter(Boolean);
123
- if (!want.length) return [];
124
- const kind = detectKind(doc);
125
- let 池 = [];
126
- if (kind === 'preset') {
127
- 池 = (doc.prompts || []).map(p => ({ id: p.identifier, 名字: p.name || p.identifier, 角色: p.role || 'system', 正文: String(p.content || '') }));
128
- } else if (kind === 'lorebook' || (kind === 'card' && (doc.data?.character_book || doc.character_book))) {
129
- const book = kind === 'lorebook' ? doc : (doc.data?.character_book || doc.character_book);
130
- const list = Array.isArray(book.entries) ? book.entries : Object.values(book.entries || {});
131
- 池 = list.map((e, i) => ({
132
- id: String(e.uid ?? e.id ?? i),
133
- 名字: e.comment || e.name || (Array.isArray(e.key) ? e.key.join('/') : `条目${i + 1}`),
134
- 正文: String(e.content || ''),
135
- }));
136
- }
137
- if (kind === 'card') {
138
- const d = doc.data && typeof doc.data === 'object' ? doc.data : doc;
139
- for (const k of ['description', 'personality', 'scenario', 'first_mes', 'mes_example',
140
- 'system_prompt', 'post_history_instructions']) {
141
- if (d[k]) 池.push({ id: k, 名字: k, 正文: String(d[k]) });
142
- }
143
- if (Array.isArray(d.alternate_greetings)) {
144
- d.alternate_greetings.forEach((g, i) => 池.push({ id: `alt_greeting_${i}`, 名字: `alternate_greetings[${i}]`, 正文: String(g || '') }));
145
- }
146
- }
147
- const 出 = [];
148
- const 见 = new Set();
149
- for (const w of want) {
150
- const hit = 池.find(x => x.id === w) || 池.find(x => x.名字 === w) || 池.find(x => x.名字.includes(w));
151
- if (hit && !见.has(hit.id)) { 见.add(hit.id); 出.push(hit); }
152
- }
153
- return 出;
154
- }
1
+ /**
2
+ * tavern-json —— 酒馆(SillyTavern)导出 JSON 的解析与摘要(2026-08-15)
3
+ *
4
+ * 起因是一份真文件:`Izumi 0814.json`,464KB、210 条提示词,而**启用的只有 56 条**
5
+ * (合计约 9.8k 字符)。agent 拿 Read 去读它 = 十几万 token 进上下文换来一堆停用
6
+ * 的备选条目 —— 这不是"贵一点",是把上下文烧光还看不清结构。所以搬酒馆的东西
7
+ * 进来必须先过这一层:**先给结构摘要,再按需取正文**。
8
+ *
9
+ * 认三种形态(酒馆导出的 JSON 就这几类):
10
+ * preset Chat Completion 预设 —— `prompts[]` + `prompt_order[]`。管的是
11
+ * "每次请求怎么拼上下文 + 采样参数",跟我们的 编排.yaml 同类。
12
+ * 条目里那些 0 字的是 **marker**(角色描述/性格/场景/Chat History…),
13
+ * 运行时由酒馆填真内容,搬过来一律丢掉 —— 我们由 编排.yaml 自己管。
14
+ * card 角色卡 V2(`spec: chara_card_v2`)或 V1。人设、开场白、示例对话,
15
+ * 可能内嵌世界书(character_book)。
16
+ * lorebook 世界书单独导出 —— `entries`,每条带 keys(触发词)与 constant(常驻)。
17
+ *
18
+ * 只读不写:转成 编排.yaml 和设定文件是 agent 的活,它得按题材决定怎么分组、
19
+ * 哪些不要(破甲话术在我们这条通路上没意义)。
20
+ */
21
+
22
+ /** 摘要里每条正文最多带多少字的引子 */
23
+ const PEEK = 60;
24
+
25
+ export function detectKind(doc) {
26
+ if (!doc || typeof doc !== 'object') return null;
27
+ if (Array.isArray(doc.prompts) && Array.isArray(doc.prompt_order)) return 'preset';
28
+ if (doc.spec === 'chara_card_v2' || doc.spec === 'chara_card_v3') return 'card';
29
+ if (doc.data && typeof doc.data === 'object' && typeof doc.data.first_mes === 'string') return 'card';
30
+ if (typeof doc.first_mes === 'string' && typeof doc.description === 'string') return 'card';
31
+ if (doc.entries && typeof doc.entries === 'object') return 'lorebook';
32
+ return null;
33
+ }
34
+
35
+ const 引 = (s) => String(s || '').replace(/\s+/g, ' ').trim().slice(0, PEEK);
36
+ const 字 = (s) => String(s || '').length;
37
+
38
+ /** 预设:启用的条目按 order 顺序排开,停用的只报个数 */
39
+ export function digestPreset(doc) {
40
+ const byId = new Map((doc.prompts || []).map(p => [p.identifier, p]));
41
+ const order = (doc.prompt_order?.[0]?.order) || [];
42
+ const 启用 = [];
43
+ const 停用 = [];
44
+ for (const o of order) {
45
+ const p = byId.get(o.identifier);
46
+ if (!p) continue;
47
+ const 条 = {
48
+ id: p.identifier,
49
+ 名字: p.name || p.identifier,
50
+ 角色: p.role || 'system',
51
+ 字数: 字(p.content),
52
+ 占位: p.marker === true, // 运行时由酒馆填(角色卡/历史)
53
+ 分隔: p.marker !== true && 字(p.content) === 0, // 纯分节标题("可选功能开始"这种)
54
+ 深度: p.injection_position === 1 ? (p.injection_depth ?? 0) : null,
55
+ 引子: 引(p.content),
56
+ };
57
+ (o.enabled ? 启用 : 停用).push(条);
58
+ }
59
+ const 实条 = 启用.filter(e => e.字数 > 0);
60
+ return {
61
+ 形态: 'preset',
62
+ 参数: {
63
+ temperature: doc.temperature, top_p: doc.top_p, top_k: doc.top_k,
64
+ frequency_penalty: doc.frequency_penalty, presence_penalty: doc.presence_penalty,
65
+ 最大上下文: doc.openai_max_context, 最大输出: doc.openai_max_tokens,
66
+ reasoning_effort: doc.reasoning_effort, squash_system_messages: doc.squash_system_messages,
67
+ },
68
+ 启用, 停用,
69
+ 合计字数: 实条.reduce((n, e) => n + e.字数, 0),
70
+ 占位条目: 启用.filter(e => e.占位).map(e => e.名字),
71
+ 分隔条目: 启用.filter(e => e.分隔).map(e => e.名字),
72
+ };
73
+ }
74
+
75
+ /** 角色卡:字段清单 + 内嵌世界书条目表 */
76
+ export function digestCard(doc) {
77
+ const d = doc.data && typeof doc.data === 'object' ? doc.data : doc;
78
+ const 字段 = ['name', 'description', 'personality', 'scenario', 'first_mes', 'mes_example',
79
+ 'system_prompt', 'post_history_instructions', 'creator_notes']
80
+ .map(k => ({ 字段: k, 字数: 字(d[k]), 引子: 引(d[k]) }))
81
+ .filter(x => x.字数 > 0);
82
+ const book = d.character_book || doc.character_book || null;
83
+ return {
84
+ 形态: 'card',
85
+ 名字: d.name || '(无名)',
86
+ 字段,
87
+ 开场白备选: Array.isArray(d.alternate_greetings) ? d.alternate_greetings.length : 0,
88
+ 世界书: book ? digestLorebook(book).条目 : [],
89
+ };
90
+ }
91
+
92
+ /** 世界书:每条的触发词、常驻与否、字数 */
93
+ export function digestLorebook(doc) {
94
+ const raw = doc.entries;
95
+ const list = Array.isArray(raw) ? raw : Object.values(raw || {});
96
+ const 条目 = list.map((e, i) => ({
97
+ id: String(e.uid ?? e.id ?? i),
98
+ 名字: e.comment || e.name || (Array.isArray(e.key) ? e.key.join('/') : '') || `条目${i + 1}`,
99
+ 触发: Array.isArray(e.key) ? e.key : (Array.isArray(e.keys) ? e.keys : []),
100
+ 常驻: !!(e.constant),
101
+ 停用: e.enabled === false || e.disable === true,
102
+ 字数: 字(e.content),
103
+ 引子: 引(e.content),
104
+ }));
105
+ return { 形态: 'lorebook', 条目 };
106
+ }
107
+
108
+ export function digest(doc) {
109
+ const kind = detectKind(doc);
110
+ if (kind === 'preset') return digestPreset(doc);
111
+ if (kind === 'card') return digestCard(doc);
112
+ if (kind === 'lorebook') return digestLorebook(doc);
113
+ return null;
114
+ }
115
+
116
+ /**
117
+ * 按名字/id 取正文。名字支持部分匹配(酒馆的条目名带 emoji,让 agent 原样抄
118
+ * 一串 emoji 当参数是给自己找麻烦)。
119
+ * @returns {{名字: string, 角色?: string, 正文: string}[]}
120
+ */
121
+ export function fetchEntries(doc, 选) {
122
+ const want = (选 || []).map(s => String(s).trim()).filter(Boolean);
123
+ if (!want.length) return [];
124
+ const kind = detectKind(doc);
125
+ let 池 = [];
126
+ if (kind === 'preset') {
127
+ 池 = (doc.prompts || []).map(p => ({ id: p.identifier, 名字: p.name || p.identifier, 角色: p.role || 'system', 正文: String(p.content || '') }));
128
+ } else if (kind === 'lorebook' || (kind === 'card' && (doc.data?.character_book || doc.character_book))) {
129
+ const book = kind === 'lorebook' ? doc : (doc.data?.character_book || doc.character_book);
130
+ const list = Array.isArray(book.entries) ? book.entries : Object.values(book.entries || {});
131
+ 池 = list.map((e, i) => ({
132
+ id: String(e.uid ?? e.id ?? i),
133
+ 名字: e.comment || e.name || (Array.isArray(e.key) ? e.key.join('/') : `条目${i + 1}`),
134
+ 正文: String(e.content || ''),
135
+ }));
136
+ }
137
+ if (kind === 'card') {
138
+ const d = doc.data && typeof doc.data === 'object' ? doc.data : doc;
139
+ for (const k of ['description', 'personality', 'scenario', 'first_mes', 'mes_example',
140
+ 'system_prompt', 'post_history_instructions']) {
141
+ if (d[k]) 池.push({ id: k, 名字: k, 正文: String(d[k]) });
142
+ }
143
+ if (Array.isArray(d.alternate_greetings)) {
144
+ d.alternate_greetings.forEach((g, i) => 池.push({ id: `alt_greeting_${i}`, 名字: `alternate_greetings[${i}]`, 正文: String(g || '') }));
145
+ }
146
+ }
147
+ const 出 = [];
148
+ const 见 = new Set();
149
+ for (const w of want) {
150
+ const hit = 池.find(x => x.id === w) || 池.find(x => x.名字 === w) || 池.find(x => x.名字.includes(w));
151
+ if (hit && !见.has(hit.id)) { 见.add(hit.id); 出.push(hit); }
152
+ }
153
+ return 出;
154
+ }