@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.
- package/README.md +156 -106
- package/bin/nodesign.js +0 -0
- package/package.json +1 -1
- package/server/api/README.md +45 -45
- package/server/api/_guard.js +75 -75
- package/server/api/admin.js +187 -187
- package/server/api/assets/docx-page.js +103 -103
- package/server/api/assets/helpers.js +22 -22
- package/server/api/assets/notes.js +144 -144
- package/server/api/assets.js +896 -896
- package/server/api/board.js +57 -57
- package/server/api/browse.js +138 -138
- package/server/api/canvas.js +444 -444
- package/server/api/chatai.js +324 -324
- package/server/api/exports/build-standalone.js +980 -980
- package/server/api/exports/cards.js +118 -118
- package/server/api/exports/docx-pdf.js +46 -46
- package/server/api/exports/export-page.js +142 -142
- package/server/api/exports/handoff.js +147 -147
- package/server/api/exports.js +690 -690
- package/server/api/instruction.js +75 -75
- package/server/api/me.js +116 -116
- package/server/api/memory.js +185 -185
- package/server/api/pending-changes.js +366 -366
- package/server/api/plugins.js +164 -164
- package/server/api/projects.js +220 -220
- package/server/api/publish.js +64 -64
- package/server/api/recent.js +73 -73
- package/server/api/sessions.js +589 -589
- package/server/api/skills.js +77 -77
- package/server/api/standalone-fit.js +168 -168
- package/server/api/turn-compose.js +180 -180
- package/server/api/turn-inflight.js +44 -44
- package/server/auth/README.md +17 -17
- package/server/auth/middleware.js +138 -138
- package/server/auth/origin-guard.js +71 -71
- package/server/auth/session.js +108 -108
- package/server/auth/tier.js +113 -113
- package/server/auth/users-store.js +348 -348
- package/server/edit/README.md +29 -29
- package/server/engine/README.md +113 -113
- package/server/engine/agent/agent-shared.js +550 -550
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
- package/server/engine/agent/auto-mode-rules.js +78 -78
- package/server/engine/agent/context.js +418 -418
- package/server/engine/agent/events.js +464 -464
- package/server/engine/agent/hooks/canvas-validate.js +199 -199
- package/server/engine/agent/hooks/failure.js +120 -120
- package/server/engine/agent/hooks/file-events.js +75 -75
- package/server/engine/agent/hooks/lifecycle.js +233 -233
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
- package/server/engine/agent/hooks/post-guidance.js +206 -206
- package/server/engine/agent/hooks/post-subagent-report.js +104 -104
- package/server/engine/agent/hooks/post-trim.js +58 -58
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
- package/server/engine/agent/hooks/pre-defaults.js +84 -84
- package/server/engine/agent/hooks/pre-injectors.js +277 -277
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
- package/server/engine/agent/hooks/pre-starter-files.js +61 -61
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
- package/server/engine/agent/hooks/site-validate.js +94 -94
- package/server/engine/agent/hooks/tool-prompts.js +31 -31
- package/server/engine/agent/hooks/turn-state-memory.js +56 -56
- package/server/engine/agent/hooks/user-prompt-submit.js +215 -215
- package/server/engine/agent/hooks.js +354 -354
- package/server/engine/agent/init-contract.js +69 -69
- package/server/engine/agent/isolation.js +159 -159
- package/server/engine/agent/model-table.js +292 -292
- package/server/engine/agent/plugin-loader.js +266 -266
- package/server/engine/agent/prompts/nodesign-prelude.md +336 -336
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
- package/server/engine/agent/prompts/tools/site-reference.md +106 -106
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
- package/server/engine/agent/session-loop.js +1024 -1024
- package/server/engine/agent/session-model.js +164 -164
- package/server/engine/agent/skill.js +204 -204
- package/server/engine/agent/task-events.js +78 -78
- package/server/engine/agents/ds-extractor.md +132 -132
- package/server/engine/agents/explorer.md +190 -190
- package/server/engine/agents/index.js +230 -230
- package/server/engine/agents/schemas/design-system.json +156 -156
- package/server/engine/agents/schemas/tweak-schema.json +113 -113
- package/server/engine/agents/tweak-proposer.md +127 -127
- package/server/engine/agents/vision-checker.md +254 -254
- package/server/engine/browse/capture.js +422 -422
- package/server/engine/browse/card.js +137 -137
- package/server/engine/browse/handover.js +59 -59
- package/server/engine/browse/page-digest.js +119 -119
- package/server/engine/browse/refs.js +196 -196
- package/server/engine/browse/registry.js +307 -307
- package/server/engine/browse/screencast.js +170 -170
- package/server/engine/browse/state.js +121 -121
- package/server/engine/chatai/chat-log.js +79 -79
- package/server/engine/chatai/index.js +193 -193
- package/server/engine/chatai/openai-compat.js +152 -152
- package/server/engine/chatai/orchestrate.js +309 -309
- package/server/engine/chatai/perform.js +40 -40
- package/server/engine/chatai/summarize.js +79 -79
- package/server/engine/mcp/capability-gate.js +52 -52
- package/server/engine/mcp/index.js +339 -339
- package/server/engine/mcp/param-sanitizer.js +142 -142
- package/server/engine/mcp/tools/arrange-on-board.js +124 -124
- package/server/engine/mcp/tools/artifact-session.js +278 -278
- package/server/engine/mcp/tools/browse-computer.js +421 -421
- package/server/engine/mcp/tools/browse-find-batch.js +183 -183
- package/server/engine/mcp/tools/browse-screenshot.js +157 -157
- package/server/engine/mcp/tools/browse.js +480 -480
- package/server/engine/mcp/tools/build-docx.js +101 -101
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
- package/server/engine/mcp/tools/create-on-board.js +155 -155
- package/server/engine/mcp/tools/crystallize-skill.js +168 -168
- package/server/engine/mcp/tools/explain-style.js +237 -237
- package/server/engine/mcp/tools/export-handoff.js +133 -133
- package/server/engine/mcp/tools/expose-tweaks.js +149 -149
- package/server/engine/mcp/tools/generate-image.js +842 -842
- package/server/engine/mcp/tools/get-computed-styles.js +164 -164
- package/server/engine/mcp/tools/get-pending-changes.js +205 -205
- package/server/engine/mcp/tools/h3box-ssh.js +84 -84
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
- package/server/engine/mcp/tools/highlight.js +68 -68
- package/server/engine/mcp/tools/list-pages.js +222 -222
- package/server/engine/mcp/tools/lookup-tags.js +59 -59
- package/server/engine/mcp/tools/navigate-to-page.js +60 -60
- package/server/engine/mcp/tools/organize-board.js +64 -64
- package/server/engine/mcp/tools/paint-still.js +434 -434
- package/server/engine/mcp/tools/pin-to-board.js +130 -130
- package/server/engine/mcp/tools/preview-deck.js +130 -130
- package/server/engine/mcp/tools/profile-scroll.js +305 -305
- package/server/engine/mcp/tools/publish-site.js +102 -102
- package/server/engine/mcp/tools/query-elements.js +195 -195
- package/server/engine/mcp/tools/read-board.js +103 -103
- package/server/engine/mcp/tools/read-document.js +93 -93
- package/server/engine/mcp/tools/read-page.js +264 -264
- package/server/engine/mcp/tools/read-tavern-json.js +143 -143
- package/server/engine/mcp/tools/record-decision.js +140 -140
- package/server/engine/mcp/tools/relate-on-board.js +110 -110
- package/server/engine/mcp/tools/report-issue.js +115 -115
- package/server/engine/mcp/tools/roll-film.js +266 -266
- package/server/engine/mcp/tools/screenshot-docx.js +138 -138
- package/server/engine/mcp/tools/screenshot-url.js +195 -195
- package/server/engine/mcp/tools/screenshot.js +583 -583
- package/server/engine/mcp/tools/tier-gate.js +96 -96
- package/server/engine/mcp/tools/trace-motion.js +215 -215
- package/server/engine/mcp/tools/web-search.js +548 -548
- package/server/engine/motion/inventory.js +349 -349
- package/server/engine/perception/session.js +235 -235
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -192
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -162
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -228
- 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
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -323
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
- package/server/engine/runs/active-runs.js +698 -698
- package/server/engine/runs/live-turn.js +252 -252
- package/server/engine/runs/store.js +364 -364
- package/server/engine/runs/turn-relay.js +217 -217
- package/server/engine/runtime/workspace.js +164 -164
- package/server/lib/artifact-file-path.js +80 -80
- package/server/lib/artifact-target.js +284 -284
- package/server/lib/asset-refs.js +239 -239
- package/server/lib/async-queue.js +104 -104
- package/server/lib/auto-relations.js +130 -130
- package/server/lib/binding-types.js +60 -60
- package/server/lib/board-kind-sizes.js +62 -62
- package/server/lib/board-relations.js +133 -133
- package/server/lib/browse-proxy.js +180 -180
- package/server/lib/canvas-id.js +36 -36
- package/server/lib/cover.js +227 -227
- package/server/lib/danbooru-tags.js +291 -291
- package/server/lib/doc-extract.js +156 -156
- package/server/lib/docx/build-from-source.js +260 -260
- package/server/lib/docx/build.js +490 -490
- package/server/lib/docx/dump-styles.js +287 -287
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
- package/server/lib/docx/merge-runs.js +129 -129
- package/server/lib/docx/numbering.js +218 -218
- package/server/lib/docx/order.js +152 -152
- package/server/lib/docx/rawzip.js +172 -172
- package/server/lib/docx/render.js +92 -92
- package/server/lib/docx/text-lint.js +225 -225
- package/server/lib/docx/tokens.js +335 -335
- package/server/lib/docx/units.js +29 -29
- package/server/lib/docx/xml.js +291 -291
- package/server/lib/docx-pages.js +187 -187
- package/server/lib/export-collect.js +289 -289
- package/server/lib/export-package.js +210 -210
- package/server/lib/html-srcset.js +145 -145
- package/server/lib/image-variant.js +460 -460
- package/server/lib/ingress/forward-openai-chat.js +305 -305
- package/server/lib/ingress/openai-chat.js +491 -491
- package/server/lib/ingress/session-notice.js +66 -66
- package/server/lib/ingress/session-routes.js +68 -68
- package/server/lib/ingress/slot-probe.js +130 -130
- package/server/lib/ingress/upstream-billing.js +52 -52
- package/server/lib/ingress/upstream-fail-streak.js +72 -72
- package/server/lib/ingress/upstream-truncation.js +48 -48
- package/server/lib/issues-store.js +182 -182
- package/server/lib/kinds/deck.js +72 -72
- package/server/lib/kinds/docx.js +283 -283
- package/server/lib/kinds/file-kinds.js +113 -113
- package/server/lib/kinds/index.js +202 -202
- package/server/lib/kinds/site.js +161 -161
- package/server/lib/model-ingress.js +543 -543
- package/server/lib/moderation.js +255 -255
- package/server/lib/notice-store.js +103 -103
- package/server/lib/plugin-install.js +104 -104
- package/server/lib/plugin-validator.js +721 -721
- package/server/lib/publish-store.js +115 -115
- package/server/lib/quick-summary.js +52 -52
- package/server/lib/quota.js +270 -270
- package/server/lib/rate-window.js +29 -29
- package/server/lib/reference-assets.js +92 -92
- package/server/lib/region-shot.js +135 -135
- package/server/lib/safe-path.js +73 -73
- package/server/lib/sdk-session.js +33 -33
- package/server/lib/showcase-store.js +92 -92
- package/server/lib/site-publish.js +465 -465
- package/server/lib/ssrf-guard.js +432 -432
- package/server/lib/task-scan.js +158 -158
- package/server/lib/tavern-json.js +154 -154
- package/server/lib/video-variant.js +237 -237
- package/server/lib/workspace-path.js +33 -33
- package/server/ops/fix-sdk-musl.mjs +40 -40
- package/server/ops/install-macos-fonts.sh +114 -114
- package/server/ops/macos-fonts.conf +336 -336
- package/server/ops/sandbox-shim/bwrap +57 -57
- package/server/projects/assets-summary.js +119 -119
- package/server/projects/auto-name.js +53 -53
- package/server/projects/board-store.js +640 -640
- package/server/projects/move-entry.js +103 -103
- package/server/projects/store.js +270 -270
- package/server/projects/ui-config.js +54 -54
- package/server/projects/workspace-templates.js +53 -53
- package/server/projects/workspace.js +1025 -1025
- package/server/runtime/local-config.js +177 -177
- package/server/runtime/profile.js +72 -72
- package/server/services/rembg-service.py +334 -334
- package/server/shared/README.md +25 -25
- package/server/shared/deck.js +54 -54
- package/server/shared/time.js +49 -49
- package/server/style-pipeline/README.md +53 -53
- package/server/ws/broker.js +36 -36
- package/server/ws/browse-channel.js +177 -177
- package/server/ws/index.js +386 -386
- package/web/README.md +89 -89
- package/web/dist/assets/BrowserWindow-BYCgizHw.js +11 -0
- package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-pNydEM4L.js} +1 -1
- package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-PPgzh3Gq.js} +1 -1
- package/web/dist/assets/{SiteWindow-D6eS-2gM.js → SiteWindow-DM0LQPCO.js} +1 -1
- package/web/dist/assets/{index-Cbl3ATvp.js → index-BHTBJlpY.js} +32 -32
- package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-C0I13CbD.js} +1 -1
- package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-B1UQ8Hld.js} +1 -1
- package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-8lrgqmfv.js} +1 -1
- package/web/dist/index.html +17 -17
- package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
package/server/lib/task-scan.js
CHANGED
|
@@ -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
|
+
}
|