@cloud411716/fancy-webnovel 1.0.46 → 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/client.js +369 -0
- package/cordis.patch.yml +4 -13
- package/docs/douyin-plugin.md +118 -0
- package/docs/material-library/README.md +404 -0
- package/docs/material-library/import.js +437 -0
- package/docs/material-library/prompt.md +370 -0
- package/docs/material-library/queries.surql +94 -0
- package/docs/material-library/sample.jsonl +14 -0
- package/docs/material-library/schema.surql +46 -0
- package/docs/mcp-profile.example.yml +24 -0
- package/docs/story-graph-design.md +602 -0
- package/docs/story-graph-validation.md +81 -0
- package/docs/workspace-mcp.md +284 -0
- package/index.js +26 -38
- package/lib/atomic-write.js +109 -0
- package/lib/chapter-order.js +100 -0
- package/lib/chinese-num.js +111 -0
- package/lib/confirmation.js +80 -0
- package/lib/db-init.js +123 -0
- package/lib/deployed-state.js +141 -0
- package/lib/direction-labels.js +265 -0
- package/lib/error-codes.js +104 -0
- package/lib/infra.js +594 -0
- package/lib/llm-wiki.js +109 -0
- package/lib/material-db.js +149 -0
- package/lib/path-guard.js +150 -0
- package/lib/projects.js +400 -0
- package/lib/scraper-utils.js +305 -0
- package/lib/surreal-server.js +338 -0
- package/lib/topic-decision.js +860 -0
- package/lib/topic-fields.js +251 -0
- package/lib/topic-schema.js +225 -0
- package/lib/workspace.js +501 -0
- package/package.json +62 -5
- package/plugins/fancy-analyze/analyze-definitions.md +519 -0
- package/plugins/fancy-analyze/command-input.js +65 -0
- package/plugins/fancy-analyze/index.js +2024 -0
- package/plugins/fancy-analyze/ingest-coverage.js +107 -0
- package/plugins/fancy-analyze/schema-analyze.md +627 -0
- package/plugins/fancy-analyze/schema-write.md +178 -0
- package/plugins/fancy-analyze/scripts/append-summary-chapter.js +612 -0
- package/plugins/fancy-analyze/scripts/split_source.js +292 -0
- package/plugins/fancy-analyze/summary-progress.js +495 -0
- package/plugins/fancy-analyze/templates.js +1179 -0
- package/plugins/fancy-bootstrap/index.js +313 -0
- package/plugins/fancy-bootstrap/scripts/init.js +177 -0
- package/plugins/fancy-bootstrap/scripts/path-utils.js +21 -0
- package/plugins/fancy-bootstrap/templates.js +71 -0
- package/plugins/fancy-db/index.js +172 -0
- package/plugins/fancy-douyin/deploy.js +134 -0
- package/plugins/fancy-douyin/index.js +551 -0
- package/plugins/fancy-douyin/prompts/01-analysis-prompt.md +84 -0
- package/plugins/fancy-douyin/scripts/crawl-note.cjs +194 -0
- package/plugins/fancy-douyin/scripts/lib.cjs +197 -0
- package/plugins/fancy-douyin/scripts/scan-account.cjs +128 -0
- package/plugins/fancy-douyin/scripts/start-chrome.cjs +203 -0
- package/plugins/fancy-douyin/state.js +197 -0
- package/plugins/fancy-douyin/templates.js +127 -0
- package/plugins/fancy-export/exporter.js +805 -0
- package/plugins/fancy-export/index.js +63 -0
- package/plugins/fancy-import/import-core.js +1118 -0
- package/plugins/fancy-import/import-driver.js +604 -0
- package/plugins/fancy-import/index.js +738 -0
- package/plugins/fancy-import/prompts/01-import-prompt.md +131 -0
- package/plugins/fancy-import/templates.js +58 -0
- package/plugins/fancy-material/exporter.js +153 -0
- package/plugins/fancy-material/genres.js +256 -0
- package/plugins/fancy-material/index.js +835 -0
- package/plugins/fancy-material/inventory.js +143 -0
- package/plugins/fancy-material/kinds.js +313 -0
- package/plugins/fancy-material/queue.js +55 -0
- package/plugins/fancy-material/scripts/query.js +254 -0
- package/plugins/fancy-material/templates.js +258 -0
- package/plugins/fancy-material/tool-deploy.js +138 -0
- package/plugins/fancy-material/validate.js +149 -0
- package/plugins/fancy-project/index.js +133 -0
- package/plugins/fancy-scan/existing-block.js +100 -0
- package/plugins/fancy-scan/index.js +708 -0
- package/plugins/fancy-scan/prompts/01-analysis-prompt.md +302 -0
- package/plugins/fancy-scan/scripts/append-block.js +725 -0
- package/plugins/fancy-scan/scripts/report-validate.js +824 -0
- package/plugins/fancy-scan/scripts/run-scan.js +667 -0
- package/plugins/fancy-scan/scripts/scrapers/fanqie-rank-scraper.js +923 -0
- package/plugins/fancy-scan/scripts/scrapers/jjwxc-rank-scraper.js +469 -0
- package/plugins/fancy-scan/scripts/scrapers/qimao-rank-scraper.js +629 -0
- package/plugins/fancy-scan/templates/blocks.js +155 -0
- package/plugins/fancy-scan/templates/platforms.js +372 -0
- package/plugins/fancy-scan/templates/report-schema.js +90 -0
- package/plugins/fancy-scan/templates.js +694 -0
- package/plugins/fancy-scan/tool-deploy.js +335 -0
- package/plugins/fancy-scan/wait-turn.js +46 -0
- package/plugins/fancy-story-builder/db/README.md +106 -0
- package/plugins/fancy-story-builder/db/client.js +228 -0
- package/plugins/fancy-story-builder/db/commands.js +145 -0
- package/plugins/fancy-story-builder/db/ids.js +39 -0
- package/plugins/fancy-story-builder/db/import.js +798 -0
- package/plugins/fancy-story-builder/db/project.js +299 -0
- package/plugins/fancy-story-builder/db/query.js +361 -0
- package/plugins/fancy-story-builder/db/schema.js +417 -0
- package/plugins/fancy-story-builder/db/stage-sync.js +238 -0
- package/plugins/fancy-story-builder/deploy-manifest.js +239 -0
- package/plugins/fancy-story-builder/deslop-driver.js +821 -0
- package/plugins/fancy-story-builder/flesh-output.js +168 -0
- package/plugins/fancy-story-builder/index.js +6861 -0
- package/plugins/fancy-story-builder/patch-driver.js +390 -0
- package/plugins/fancy-story-builder/prompts/01-skeleton-prompt.md +563 -0
- package/plugins/fancy-story-builder/prompts/02-flesh-prompt.md +602 -0
- package/plugins/fancy-story-builder/prompts/03-rhythm-prompt.md +539 -0
- package/plugins/fancy-story-builder/prompts/04-writing-prompt.md +225 -0
- package/plugins/fancy-story-builder/prompts/05-deslop-prompt.md +96 -0
- package/plugins/fancy-story-builder/purpose-template.md +83 -0
- package/plugins/fancy-story-builder/references/formulas-rhythm.json +302 -0
- package/plugins/fancy-story-builder/references/formulas-skeleton.json +402 -0
- package/plugins/fancy-story-builder/references/genre-profiles.json +3514 -0
- package/plugins/fancy-story-builder/references/glossary.md +594 -0
- package/plugins/fancy-story-builder/references/index.md +581 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/00-index.md +174 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/01-scene.md +150 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/02-chapter-workflow.md +99 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/03-opening-chapter.md +104 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/04-dialogue-mastery.md +274 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/05-genre-formulas.md +432 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/06-tone-ensemble.md +259 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/07-ledger-audit.md +222 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/08-diagnosis-revision.md +205 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/09-reading-power.md +135 -0
- package/plugins/fancy-story-builder/references//345/206/231/344/275/234/346/214/207/345/257/274/10-deslop.md +129 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/00-index.md +98 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/01-upgrade-rhythm.md +300 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/02-arc-rhythm.md +168 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/03-highpoint-hooks.md +189 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/04-satisfaction.md +226 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/05-foreshadow-density.md +195 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/06-chapter-outline.md +226 -0
- package/plugins/fancy-story-builder/references//350/212/202/345/245/217/346/214/207/345/257/274/07-audit.md +181 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/00-index.md +100 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/01-character.md +243 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/02-worldbuilding.md +245 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/03-relations.md +185 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/04-emotion-methods.md +286 -0
- package/plugins/fancy-story-builder/references//350/241/200/350/202/211/346/214/207/345/257/274/05-audit.md +187 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/00-index.md +91 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/01-positioning.md +209 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/02-packaging.md +135 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/03-entities.md +305 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/04-conflict.md +328 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/05-quota.md +187 -0
- package/plugins/fancy-story-builder/references//351/252/250/346/236/266/346/214/207/345/257/274/06-audit.md +160 -0
- package/plugins/fancy-story-builder/rhythm-output.js +140 -0
- package/plugins/fancy-story-builder/schema-setting.json +1100 -0
- package/plugins/fancy-story-builder/schema-setting.md +232 -0
- package/plugins/fancy-story-builder/scripts/ai-patterns.js +619 -0
- package/plugins/fancy-story-builder/scripts/brief-validate.js +164 -0
- package/plugins/fancy-story-builder/scripts/check-guide-compliance.js +289 -0
- package/plugins/fancy-story-builder/scripts/content-quality.js +416 -0
- package/plugins/fancy-story-builder/scripts/deslop-validate.js +143 -0
- package/plugins/fancy-story-builder/scripts/error-classes.js +112 -0
- package/plugins/fancy-story-builder/scripts/flesh-validate.js +1312 -0
- package/plugins/fancy-story-builder/scripts/formula-engine.js +112 -0
- package/plugins/fancy-story-builder/scripts/graph-utils.js +1484 -0
- package/plugins/fancy-story-builder/scripts/label-coverage.js +171 -0
- package/plugins/fancy-story-builder/scripts/prompt-drift-check.js +441 -0
- package/plugins/fancy-story-builder/scripts/reading-power.js +298 -0
- package/plugins/fancy-story-builder/scripts/reference-log.js +110 -0
- package/plugins/fancy-story-builder/scripts/rhythm-validate.js +1312 -0
- package/plugins/fancy-story-builder/scripts/schema-pages.js +2668 -0
- package/plugins/fancy-story-builder/scripts/semantic-lint.js +381 -0
- package/plugins/fancy-story-builder/scripts/skeleton-quota.js +145 -0
- package/plugins/fancy-story-builder/scripts/skeleton-validate.js +1973 -0
- package/plugins/fancy-story-builder/scripts/stage-batches.js +1405 -0
- package/plugins/fancy-story-builder/scripts/stage-contract.js +429 -0
- package/plugins/fancy-story-builder/scripts/story-artifacts.js +933 -0
- package/plugins/fancy-story-builder/scripts/upstream-query.js +198 -0
- package/plugins/fancy-story-builder/scripts/v2/baseline.js +112 -0
- package/plugins/fancy-story-builder/scripts/v2/cast.js +517 -0
- package/plugins/fancy-story-builder/scripts/v2/context.js +434 -0
- package/plugins/fancy-story-builder/scripts/v2/contracts.js +493 -0
- package/plugins/fancy-story-builder/scripts/v2/core.js +419 -0
- package/plugins/fancy-story-builder/scripts/v2/engine.js +1068 -0
- package/plugins/fancy-story-builder/scripts/v2/export-markdown.js +794 -0
- package/plugins/fancy-story-builder/scripts/v2/genre.js +195 -0
- package/plugins/fancy-story-builder/scripts/v2/knowledge.js +321 -0
- package/plugins/fancy-story-builder/scripts/v2/macro.js +409 -0
- package/plugins/fancy-story-builder/scripts/v2/manifest.js +538 -0
- package/plugins/fancy-story-builder/scripts/v2/migrate.js +137 -0
- package/plugins/fancy-story-builder/scripts/v2/prompts.js +1335 -0
- package/plugins/fancy-story-builder/scripts/v2/rhythm.js +618 -0
- package/plugins/fancy-story-builder/scripts/v2/scale.js +843 -0
- package/plugins/fancy-story-builder/scripts/v2/snapshot.js +121 -0
- package/plugins/fancy-story-builder/scripts/v2/stages.js +273 -0
- package/plugins/fancy-story-builder/scripts/v2/writing.js +216 -0
- package/plugins/fancy-story-builder/scripts/writing-rules.js +167 -0
- package/plugins/fancy-story-builder/scripts/writing-validate.js +682 -0
- package/plugins/fancy-story-builder/skeleton-output.js +199 -0
- package/plugins/fancy-story-builder/stage-driver.js +1049 -0
- package/plugins/fancy-story-builder/storydb/README.md +271 -0
- package/plugins/fancy-story-builder/storydb/cli.js +458 -0
- package/plugins/fancy-story-builder/storydb/client.js +351 -0
- package/plugins/fancy-story-builder/storydb/compile.js +452 -0
- package/plugins/fancy-story-builder/storydb/context.js +288 -0
- package/plugins/fancy-story-builder/storydb/definitions.js +699 -0
- package/plugins/fancy-story-builder/storydb/driver.js +106 -0
- package/plugins/fancy-story-builder/storydb/dsh-tools.js +313 -0
- package/plugins/fancy-story-builder/storydb/export.js +175 -0
- package/plugins/fancy-story-builder/storydb/ids.js +31 -0
- package/plugins/fancy-story-builder/storydb/impact.js +170 -0
- package/plugins/fancy-story-builder/storydb/import.js +559 -0
- package/plugins/fancy-story-builder/storydb/mcp.js +99 -0
- package/plugins/fancy-story-builder/storydb/prompts.js +952 -0
- package/plugins/fancy-story-builder/storydb/stage-command.js +379 -0
- package/plugins/fancy-story-builder/storydb/stage.js +426 -0
- package/plugins/fancy-story-builder/storydb/store.js +891 -0
- package/plugins/fancy-story-builder/storydb/tools.js +592 -0
- package/plugins/fancy-story-builder/storydb/validation.js +183 -0
- package/plugins/fancy-story-builder/templates/v2/10-scale/scale-template.json +144 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/emotion-template.json +15 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/endings-template.json +16 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/lines-template.json +28 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/roles-template.json +10 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/story-template.json +19 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/volumes-template.json +24 -0
- package/plugins/fancy-story-builder/templates/v2/20-macro/world-template.json +24 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/characters-template.json +49 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/climaxes-template.json +14 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/conflicts-template.json +30 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/factions-template.json +18 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/foreshadows-template.json +29 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/information-template.json +29 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/items-template.json +13 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/locations-template.json +12 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/payoffs-template.json +16 -0
- package/plugins/fancy-story-builder/templates/v2/30-cast/relations-template.json +44 -0
- package/plugins/fancy-story-builder/templates/v2/40-rhythm/chapter-map-template.json +81 -0
- package/plugins/fancy-story-builder/templates/v2/40-rhythm/outline-template.json +162 -0
- package/plugins/fancy-story-builder/templates/v2/50-writing/ledger-template.json +46 -0
- package/plugins/fancy-story-builder/templates/v2/60-deslop/report-template.json +10 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//346/227/266/345/272/217/346/230/240/345/260/204/346/250/241/346/235/277.json +427 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//347/253/240/350/212/202/347/273/206/347/272/262/346/250/241/346/235/277.json +34 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//347/253/240/350/212/202/350/247/204/345/210/222/346/250/241/346/235/277.json +24 -0
- package/plugins/fancy-story-builder/templates//350/212/202/345/245/217//350/212/202/345/245/217/345/217/202/346/225/260/346/250/241/346/235/277.json +27 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//344/270/226/347/225/214/350/247/202/350/247/204/345/210/231/346/250/241/346/235/277.json +168 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/263/351/224/256/351/201/223/345/205/267/346/250/241/346/235/277.json +184 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/205/266/344/273/226/345/256/236/344/275/223/346/250/241/346/235/277.json +62 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/206/262/347/252/201/346/250/241/346/235/277.json +131 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/211/247/346/203/205/346/250/241/346/235/277.json +85 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/212/277/345/212/233/346/250/241/346/235/277.json +103 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//345/234/260/347/202/271/346/250/241/346/235/277.json +68 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//347/273/223/345/261/200/346/250/241/346/235/277.json +142 -0
- package/plugins/fancy-story-builder/templates//350/241/200/350/202/211//350/247/222/350/211/262/346/250/241/346/235/277.json +292 -0
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/205/263/347/263/273/346/250/241/346/235/277.json +266 -0
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/256/236/344/275/223/346/250/241/346/235/277.json +336 -0
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//345/261/202/347/272/247/346/250/241/346/235/277.json +65 -0
- package/plugins/fancy-story-builder/templates//351/252/250/346/236/266//346/241/206/346/236/266/346/250/241/346/235/277.json +308 -0
- package/plugins/fancy-story-builder/templates.js +1446 -0
- package/plugins/fancy-story-builder/wiki-references.js +540 -0
- package/plugins/fancy-story-builder/writing-driver.js +1388 -0
- package/plugins/fancy-topic/index.js +1057 -0
- package/plugins/fancy-topic/prompts/01-exploration-prompt.md +115 -0
- package/plugins/fancy-topic/scripts/save-suggestion.js +219 -0
- package/plugins/fancy-topic/scripts/select-topic.js +317 -0
- package/plugins/fancy-topic/templates.js +476 -0
- package/plugins/fancy-topic/tool-deploy.js +200 -0
- package/commands/bootstrap/index.js +0 -164
- package/commands/bootstrap/init-project.js +0 -121
- package/commands/scan/index.js +0 -283
- package/commands/scan/platforms/fanqie.js +0 -146
- package/commands/scan/platforms/jinjiang.js +0 -143
- package/commands/scan/platforms/qidian.js +0 -102
- package/commands/scan/platforms/qimao.js +0 -127
- package/commands/scan/scraper.js +0 -195
- package/infra.js +0 -160
package/lib/llm-wiki.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* llm_wiki 客户端的共享工具。
|
|
3
|
+
*
|
|
4
|
+
* 两个插件接的是同一个 llm_wiki 服务:
|
|
5
|
+
* plugins/fancy-analyze —— 拆书(产出到 对标/)
|
|
6
|
+
* plugins/fancy-story-builder —— 故事圣经(产出到 设定/)
|
|
7
|
+
*
|
|
8
|
+
* 它们踩过同一批坑,所以解法收敛到这里,而不是各写一份:
|
|
9
|
+
* 1. fetch 失败时 Node 只抛一句 `TypeError: fetch failed`,
|
|
10
|
+
* 真正的原因(ECONNREFUSED / ENOTFOUND / 超时)藏在 err.cause 里。
|
|
11
|
+
* 直接报 err.message,用户既不知道是服务没启动、地址写错,还是超时。
|
|
12
|
+
* 2. HTTP 4xx/5xx 与响应体里的 { ok:false, error } 是**两种**失败,
|
|
13
|
+
* 只看其中一种就会漏 —— 漏掉的后果是"失败了却报成功"。
|
|
14
|
+
* 3. 服务端可能返回非 JSON(例如网关的 HTML 错误页),
|
|
15
|
+
* response.json() 会抛 SyntaxError,把真正的原因盖掉。
|
|
16
|
+
*
|
|
17
|
+
* @module lib/llm-wiki
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 把 fetch 抛出的异常翻译成人话。
|
|
22
|
+
*
|
|
23
|
+
* @param {Error} err - fetch 抛出的异常
|
|
24
|
+
* @param {number} [timeoutMs] - 本次请求的超时设置(用于生成超时文案)
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export function describeFetchError(err, timeoutMs = 30000) {
|
|
28
|
+
if (!err) return '未知错误';
|
|
29
|
+
if (err.name === 'AbortError' || err.name === 'TimeoutError') {
|
|
30
|
+
return `连接 llm_wiki 超时(${Math.round(timeoutMs / 1000)}s 没有响应)——`
|
|
31
|
+
+ '请确认服务已启动,且 .fancy-deployed 里的 llm_wiki_url 指向正确地址';
|
|
32
|
+
}
|
|
33
|
+
// 多地址(IPv4/IPv6)时 undici 会抛 AggregateError
|
|
34
|
+
const cause = err.cause || {};
|
|
35
|
+
const first = (cause.errors && cause.errors.length) ? cause.errors[0] : cause;
|
|
36
|
+
const code = first.code || cause.code || '';
|
|
37
|
+
const addr = first.address || cause.address;
|
|
38
|
+
const port = first.port || cause.port;
|
|
39
|
+
const where = addr ? `${addr}:${port}` : '';
|
|
40
|
+
|
|
41
|
+
if (code === 'ECONNREFUSED') {
|
|
42
|
+
return `连不上 llm_wiki(${where} 拒绝连接)——服务很可能没启动;`
|
|
43
|
+
+ '请确认服务已启动,且 .fancy-deployed 里的 llm_wiki_url 指向正确地址';
|
|
44
|
+
}
|
|
45
|
+
if (code === 'ENOTFOUND' || code === 'EAI_AGAIN') {
|
|
46
|
+
return `llm_wiki 地址解析失败(${first.hostname || cause.hostname || '未知主机'})`
|
|
47
|
+
+ '——请检查 .fancy-deployed 里的 llm_wiki_url';
|
|
48
|
+
}
|
|
49
|
+
if (code === 'ECONNRESET' || code === 'EPIPE') {
|
|
50
|
+
return `llm_wiki 连接被中断(${code})——服务可能正在重启`;
|
|
51
|
+
}
|
|
52
|
+
if (code === 'ETIMEDOUT') {
|
|
53
|
+
return `连接 llm_wiki 超时(${code})——服务无响应`;
|
|
54
|
+
}
|
|
55
|
+
if (code) {
|
|
56
|
+
return `${err.message}(${code}${where ? ' ' + where : ''})`;
|
|
57
|
+
}
|
|
58
|
+
return err.message || String(err);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 安全地读 JSON 响应体:不是 JSON 就返回 null,不抛异常。
|
|
63
|
+
*
|
|
64
|
+
* 服务端返回 HTML 错误页时,response.json() 会抛
|
|
65
|
+
* `SyntaxError: Unexpected token '<'`,把真正的 HTTP 状态盖掉。
|
|
66
|
+
*
|
|
67
|
+
* @param {Response} response - fetch 的 Response
|
|
68
|
+
* @returns {Promise<object|null>}
|
|
69
|
+
*/
|
|
70
|
+
export async function readJsonSafe(response) {
|
|
71
|
+
try {
|
|
72
|
+
return await response.json();
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 把 llm_wiki 的响应翻译成一句人话;成功返回 null。
|
|
80
|
+
*
|
|
81
|
+
* 必须同时看 HTTP 状态码和响应体 —— 只看其中一个都会漏:
|
|
82
|
+
* - 只看 body:llm_wiki 返回 502/401 时 body 里可能没有 error 字段
|
|
83
|
+
* - 只看 status:llm_wiki 的错误约定是 HTTP 200 + { ok:false, error }
|
|
84
|
+
*
|
|
85
|
+
* @param {object} result - { status, data }
|
|
86
|
+
* @returns {string|null} 错误描述;null 表示成功
|
|
87
|
+
*/
|
|
88
|
+
export function llmWikiError(result) {
|
|
89
|
+
const status = result && result.status;
|
|
90
|
+
const data = result && result.data;
|
|
91
|
+
const apiErr = data && data.error ? String(data.error) : '';
|
|
92
|
+
|
|
93
|
+
if (status !== undefined && (status < 200 || status >= 300)) {
|
|
94
|
+
if (status === 401 || status === 403) {
|
|
95
|
+
// 401/403 几乎必然是 token 的问题,比服务端那句泛泛的
|
|
96
|
+
// error 更有指向性,所以鉴权提示在前。
|
|
97
|
+
return `鉴权失败(HTTP ${status}),请检查 .fancy-deployed 里的 llm_wiki_token`
|
|
98
|
+
+ (apiErr ? `(服务端返回:${apiErr})` : '');
|
|
99
|
+
}
|
|
100
|
+
if (!data) {
|
|
101
|
+
// 响应体不是 JSON —— 多半是网关/反代的错误页
|
|
102
|
+
return `HTTP ${status}(服务端返回的不是 JSON,可能是网关错误页)`;
|
|
103
|
+
}
|
|
104
|
+
return apiErr || `HTTP ${status}`;
|
|
105
|
+
}
|
|
106
|
+
if (apiErr) return apiErr;
|
|
107
|
+
if (data && data.ok === false) return '接口返回 ok=false';
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared material library database.
|
|
3
|
+
*
|
|
4
|
+
* The material library is workspace-level: one database per workspace
|
|
5
|
+
* (`material_library`), shared by every project. It lives on the same
|
|
6
|
+
* SurrealDB server as the registry and project databases.
|
|
7
|
+
*
|
|
8
|
+
* The schema is kept in `docs/material-library/schema.surql` so the standalone
|
|
9
|
+
* docs/import tooling and the plugin initialize exactly the same tables.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
import { Surreal, createRemoteEngines } from 'surrealdb';
|
|
15
|
+
import { MATERIAL_KINDS } from '../plugins/fancy-material/kinds.js';
|
|
16
|
+
import { readSecrets, readWorkspace } from './workspace.js';
|
|
17
|
+
import { rpcUrl, startServer } from './surreal-server.js';
|
|
18
|
+
|
|
19
|
+
export const MATERIAL_DATABASE = 'material_library';
|
|
20
|
+
export const MATERIAL_SCHEMA_VERSION = 6;
|
|
21
|
+
|
|
22
|
+
const SCHEMA_FILE = fileURLToPath(new URL('../docs/material-library/schema.surql', import.meta.url));
|
|
23
|
+
|
|
24
|
+
export function materialDatabaseName(workspace) {
|
|
25
|
+
return (workspace && workspace.surreal && workspace.surreal.material_database) || MATERIAL_DATABASE;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function materialSchemaText() {
|
|
29
|
+
return fs.readFileSync(SCHEMA_FILE, 'utf8');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Connect as the workspace root user to the material database.
|
|
34
|
+
*
|
|
35
|
+
* `startServer` verifies the workspace credentials and may move the workspace
|
|
36
|
+
* to a free port when another SurrealDB is squatting on the configured one.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} root workspace directory
|
|
39
|
+
* @returns {Promise<{db: Surreal, workspace: object, secrets: object, namespace: string, database: string}>}
|
|
40
|
+
*/
|
|
41
|
+
export async function connectMaterial(root) {
|
|
42
|
+
await startServer(root);
|
|
43
|
+
const workspace = readWorkspace(root);
|
|
44
|
+
const secrets = readSecrets(root);
|
|
45
|
+
const db = new Surreal({ engines: { ...createRemoteEngines() } });
|
|
46
|
+
await db.connect(rpcUrl(workspace));
|
|
47
|
+
await db.signin({
|
|
48
|
+
username: secrets.surreal?.username || 'root',
|
|
49
|
+
password: secrets.surreal?.password,
|
|
50
|
+
});
|
|
51
|
+
const namespace = workspace.surreal?.namespace || 'fancy';
|
|
52
|
+
const database = materialDatabaseName(workspace);
|
|
53
|
+
await db.use({ namespace, database });
|
|
54
|
+
return { db, workspace, secrets, namespace, database };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Ensure the material database and its schema exist. Idempotent.
|
|
59
|
+
*
|
|
60
|
+
* Schema v6 adds `source` (builtin/other) and narrows builtin kinds to the current 54-kind catalog. Before applying the
|
|
61
|
+
* new ASSERT, rows belonging to retired kinds are removed; otherwise SurrealDB
|
|
62
|
+
* refuses to overwrite the field because existing records no longer conform.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} root workspace directory
|
|
65
|
+
* @returns {Promise<{ok: boolean, namespace: string, database: string, schema_version: number, fields: number, indexes: string[], pruned: number}>}
|
|
66
|
+
*/
|
|
67
|
+
export async function ensureMaterialDatabase(root) {
|
|
68
|
+
const { db, namespace, database } = await connectMaterial(root);
|
|
69
|
+
try {
|
|
70
|
+
let pruned = 0;
|
|
71
|
+
const info = await db.query('INFO FOR DB;');
|
|
72
|
+
const dbInfo = Array.isArray(info) && info[0] ? info[0] : info;
|
|
73
|
+
const tables = (dbInfo && dbInfo.tables) || {};
|
|
74
|
+
if (tables.material) {
|
|
75
|
+
// Old databases may have rows created before these fields existed.
|
|
76
|
+
// SCHEMAFULL tables reject writes to undefined fields, so add them
|
|
77
|
+
// before backfilling.
|
|
78
|
+
await db.query('DEFINE FIELD IF NOT EXISTS created_at ON material TYPE datetime DEFAULT time::now();');
|
|
79
|
+
await db.query('DEFINE FIELD IF NOT EXISTS updated_at ON material TYPE datetime VALUE time::now();');
|
|
80
|
+
await db.query("DEFINE FIELD IF NOT EXISTS source ON material TYPE string DEFAULT 'builtin';");
|
|
81
|
+
await db.query('UPDATE material SET created_at = time::now() WHERE created_at = NONE;');
|
|
82
|
+
await db.query('UPDATE material SET updated_at = time::now() WHERE updated_at = NONE;');
|
|
83
|
+
await db.query("UPDATE material SET source = 'builtin' WHERE source = NONE OR source NOT IN ['builtin', 'other'];");
|
|
84
|
+
// Builtin rows cannot use retired kinds, so remove them before the
|
|
85
|
+
// DEFINE FIELD OVERWRITE. Rows with source='other' may use any kind and
|
|
86
|
+
// are deliberately kept.
|
|
87
|
+
const invalid = await db.query(
|
|
88
|
+
"SELECT count() AS n FROM material WHERE source = 'builtin' AND kind NOT IN $kinds GROUP ALL;",
|
|
89
|
+
{ kinds: MATERIAL_KINDS },
|
|
90
|
+
);
|
|
91
|
+
const invalidRow = Array.isArray(invalid) && Array.isArray(invalid[0]) ? invalid[0][0] : null;
|
|
92
|
+
pruned = invalidRow ? Number(invalidRow.n) || 0 : 0;
|
|
93
|
+
if (pruned > 0) {
|
|
94
|
+
await db.query("DELETE material WHERE source = 'builtin' AND kind NOT IN $kinds;", { kinds: MATERIAL_KINDS });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
await db.query(materialSchemaText());
|
|
98
|
+
const tableInfo = await db.query('INFO FOR TABLE material;');
|
|
99
|
+
const table = Array.isArray(tableInfo) && tableInfo[0] ? tableInfo[0] : {};
|
|
100
|
+
if (!table.fields || !table.fields.source || !table.fields.kind || !table.fields.name || !table.fields.tags) {
|
|
101
|
+
throw new Error('素材库 schema 初始化后仍未找到 material.kind/name/tags 字段。');
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
ok: true,
|
|
105
|
+
namespace,
|
|
106
|
+
database,
|
|
107
|
+
schema_version: MATERIAL_SCHEMA_VERSION,
|
|
108
|
+
fields: Object.keys(table.fields || {}).length,
|
|
109
|
+
indexes: Object.keys(table.indexes || {}),
|
|
110
|
+
pruned,
|
|
111
|
+
};
|
|
112
|
+
} finally {
|
|
113
|
+
try { await db.close(); } catch { /* ignore */ }
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Remove the workspace material database.
|
|
119
|
+
*
|
|
120
|
+
* Connect through the registry database (never through the target database)
|
|
121
|
+
* and issue `REMOVE DATABASE IF EXISTS`.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} root workspace directory
|
|
124
|
+
* @returns {Promise<{removed: boolean, database: string}>}
|
|
125
|
+
*/
|
|
126
|
+
export async function removeMaterialDatabase(root) {
|
|
127
|
+
await startServer(root);
|
|
128
|
+
const workspace = readWorkspace(root);
|
|
129
|
+
const secrets = readSecrets(root);
|
|
130
|
+
const namespace = workspace.surreal?.namespace || 'fancy';
|
|
131
|
+
const database = materialDatabaseName(workspace);
|
|
132
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(database)) {
|
|
133
|
+
throw new Error('素材库数据库名不合法:' + database);
|
|
134
|
+
}
|
|
135
|
+
const registry = workspace.surreal?.registry_database || 'fancy_registry';
|
|
136
|
+
const db = new Surreal({ engines: { ...createRemoteEngines() } });
|
|
137
|
+
try {
|
|
138
|
+
await db.connect(rpcUrl(workspace));
|
|
139
|
+
await db.signin({
|
|
140
|
+
username: secrets.surreal?.username || 'root',
|
|
141
|
+
password: secrets.surreal?.password,
|
|
142
|
+
});
|
|
143
|
+
await db.use({ namespace, database: registry });
|
|
144
|
+
await db.query('REMOVE DATABASE IF EXISTS ' + database + ';');
|
|
145
|
+
return { removed: true, database };
|
|
146
|
+
} finally {
|
|
147
|
+
try { await db.close(); } catch { /* ignore */ }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* path-guard.js — 项目路径校验(bootstrap / scan / topic 共用一套)
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么改成白名单(2026-10-29)
|
|
5
|
+
*
|
|
6
|
+
* 之前用的是黑名单:禁掉 `< > : " ' | ? *` 和控制字符,其余放行。
|
|
7
|
+
* 但项目路径会被**原样拼进提示词里的 shell 命令**:
|
|
8
|
+
*
|
|
9
|
+
* cat >> "${topicFile}" <<'FANCY_SCAN_EOF'
|
|
10
|
+
*
|
|
11
|
+
* 双引号**挡不住**命令替换 —— `$()` 和反引号在双引号里照样展开。
|
|
12
|
+
* 于是 `/tmp/$(sh${IFS}/tmp/pwn.sh)` 这种目录名能通过校验:
|
|
13
|
+
* 它不含空格、不含 `..`、不含任何被禁字符。
|
|
14
|
+
*
|
|
15
|
+
* 黑名单永远列不全(要挡 `$(`、`` ` ``、`;`、`&`、`|`…),
|
|
16
|
+
* 所以改成白名单:**只放行确实需要的字符**。
|
|
17
|
+
*
|
|
18
|
+
* ## 为什么放行空格和括号
|
|
19
|
+
*
|
|
20
|
+
* 路径在提示词里是**单引号赋值 + 双引号引用**:
|
|
21
|
+
*
|
|
22
|
+
* TOPIC_FILE='/path/带 空格/项目(测试)'
|
|
23
|
+
* cat >> "$TOPIC_FILE" <<'FANCY_SCAN_EOF'
|
|
24
|
+
*
|
|
25
|
+
* 单引号内不做任何展开,`$TOPIC_FILE` 展开成字面路径后**不再二次展开**,
|
|
26
|
+
* 所以空格、括号、花括号在引号内都是安全的 —— 没必要为此把用户挡在门外。
|
|
27
|
+
*
|
|
28
|
+
* 真正危险的是 `$`、`` ` ``、`;`、`&`、`|`、`<`、`>` 和两种引号本身 ——
|
|
29
|
+
* 它们能让路径跳出引号,或者让 shell 在路径里执行命令。这些一律拒绝。
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 敏感系统路径前缀(黑名单)。
|
|
34
|
+
*
|
|
35
|
+
* **只是额外一层提示,不是安全边界** —— 安全边界是上面的白名单。
|
|
36
|
+
* 攻击者可以自己造一个不在名单里的敏感路径(/home/x/.ssh 等)。
|
|
37
|
+
*/
|
|
38
|
+
import os from 'os';
|
|
39
|
+
|
|
40
|
+
export const SENSITIVE_PATH_PREFIXES = [
|
|
41
|
+
'/etc', '/proc', '/sys', '/dev', '/root', '/boot',
|
|
42
|
+
'/usr', '/bin', '/sbin', '/lib', '/lib64', '/opt', '/var', '/run', '/snap',
|
|
43
|
+
'/System', '/Library', '/private',
|
|
44
|
+
'C:/Windows', 'C:/Program Files', 'C:/Program Files (x86)',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** Home-relative directories that must never become a workspace. */
|
|
48
|
+
const HOME_SENSITIVE_DIRS = [
|
|
49
|
+
'.ssh', '.gnupg', '.aws', '.config', '.local', '.cache', '.kube',
|
|
50
|
+
'.docker', '.npm', '.cargo', '.rustup', '.password-store',
|
|
51
|
+
'.netrc', '.git-credentials', '.mozilla', '.thunderbird',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 明确拒绝的字符。
|
|
56
|
+
*
|
|
57
|
+
* `$` `` ` `` → 命令替换
|
|
58
|
+
* `;` `&` `|` → 命令分隔
|
|
59
|
+
* `<` `>` → 重定向
|
|
60
|
+
* `"` `'` → 提前闭合引号
|
|
61
|
+
* `!` `#` `?` `*` `%` `^` → 交互式历史展开 / 注释 / 通配 / 其它 shell 语义
|
|
62
|
+
* 控制字符 → 不可见、会被各种工具截断
|
|
63
|
+
*/
|
|
64
|
+
const FORBIDDEN_CHARS = /[$\u0060;&|<>"'!?#*%^]/;
|
|
65
|
+
|
|
66
|
+
function normalizePathForGuard(p) {
|
|
67
|
+
return String(p || '').replace(/\\/g, '/');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Return the sensitive prefix that matches, or null. Prefixes are normalized
|
|
72
|
+
* so Windows-style input and the home-directory exclusions use one rule.
|
|
73
|
+
*/
|
|
74
|
+
function sensitivePrefixOf(p) {
|
|
75
|
+
if (!p || typeof p !== 'string') return null;
|
|
76
|
+
const normalized = normalizePathForGuard(p);
|
|
77
|
+
if (normalized === '/') return '/';
|
|
78
|
+
const home = normalizePathForGuard(os.homedir()).replace(/\/+$/, '');
|
|
79
|
+
for (const name of HOME_SENSITIVE_DIRS) {
|
|
80
|
+
const prefix = home + '/' + name;
|
|
81
|
+
if (normalized === prefix || normalized.startsWith(prefix + '/')) return prefix;
|
|
82
|
+
}
|
|
83
|
+
for (const prefix of SENSITIVE_PATH_PREFIXES) {
|
|
84
|
+
const normalizedPrefix = normalizePathForGuard(prefix);
|
|
85
|
+
if (normalized === normalizedPrefix || normalized.startsWith(normalizedPrefix + '/')) return normalizedPrefix;
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 路径是否可用作项目根。
|
|
92
|
+
*
|
|
93
|
+
* @param {string} p
|
|
94
|
+
* @returns {boolean}
|
|
95
|
+
*/
|
|
96
|
+
export function isValidProjectPath(p) {
|
|
97
|
+
if (!p || typeof p !== 'string') return false;
|
|
98
|
+
if (p.includes('..')) return false;
|
|
99
|
+
if (FORBIDDEN_CHARS.test(p)) return false;
|
|
100
|
+
// 控制字符(含换行 / 制表符)
|
|
101
|
+
if (/[\x00-\x1f\x7f]/.test(p)) return false;
|
|
102
|
+
// 敏感系统路径
|
|
103
|
+
if (sensitivePrefixOf(p)) return false;
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 路径是不是敏感系统目录(单独拎出来,因为错误码不一样)。
|
|
109
|
+
*
|
|
110
|
+
* @param {string} p
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
export function isSensitiveProjectPath(p) {
|
|
114
|
+
return sensitivePrefixOf(p) !== null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 路径不可用时,给用户看的原因(人话)。
|
|
119
|
+
*
|
|
120
|
+
* @param {string} p
|
|
121
|
+
* @returns {string|null} null 表示路径可用
|
|
122
|
+
*/
|
|
123
|
+
export function projectPathProblem(p) {
|
|
124
|
+
if (!p || typeof p !== 'string') return '路径不能为空。';
|
|
125
|
+
if (p.includes('..')) return '路径中不能包含 ".."。';
|
|
126
|
+
if (FORBIDDEN_CHARS.test(p)) {
|
|
127
|
+
return '路径中不能包含以下字符:$ ` ; & | < > " \' ! # ? * % ^'
|
|
128
|
+
+ '(它们在 shell 命令里会有特殊含义,路径必须能被安全引用)';
|
|
129
|
+
}
|
|
130
|
+
if (/[\x00-\x1f\x7f]/.test(p)) return '路径中不能包含控制字符。';
|
|
131
|
+
const sensitive = sensitivePrefixOf(p);
|
|
132
|
+
if (sensitive) {
|
|
133
|
+
if (sensitive === '/') return '不能将根目录作为工作区目录。';
|
|
134
|
+
return `不能将系统受保护目录(${sensitive})作为工作区目录。`;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 把路径安全地写进 shell 的单引号字符串。
|
|
141
|
+
*
|
|
142
|
+
* 单引号内不做任何展开;路径里的单引号用 `'\''` 断开再接上。
|
|
143
|
+
* 本插件的路径校验本来就禁止单引号,这里是双保险。
|
|
144
|
+
*
|
|
145
|
+
* @param {string} p
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
export function shellSingleQuote(p) {
|
|
149
|
+
return "'" + String(p == null ? '' : p).split("'").join("'\\''") + "'";
|
|
150
|
+
}
|