@waterwx/dsh-novel-forge 1.3.1 → 1.3.3
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 +2 -2
- package/lib/client.js +251 -301
- package/lib/client.js.map +1 -1
- package/lib/index.js +22 -13
- package/lib/index.js.map +1 -1
- package/lib/types/bookshelf.d.ts +1 -1
- package/lib/types/index.d.ts +1 -1
- package/package.json +2 -2
- package/src/bookshelf.ts +2 -2
- package/src/client/panel/NovelPanel.tsx +6 -35
- package/src/engine.ts +2 -2
- package/src/index.ts +7 -5
- package/src/routes.ts +2 -2
- package/scripts/assistant-frames.txt +0 -8
- package/scripts/diagnose-adapter.mts +0 -81
- package/scripts/diagnose-assistant.mjs +0 -58
- package/scripts/diagnose-bible.mjs +0 -72
- package/scripts/diagnose-harness.mts +0 -75
- package/scripts/novel-forge-restart.log +0 -10
- package/scripts/novel-forge-web.stderr.log +0 -2
- package/scripts/novel-forge-web.stdout.log +0 -1
- package/scripts/outline-sample.txt +0 -449
- package/scripts/probe.mjs +0 -6
- package/scripts/restart-web.ps1 +0 -88
- package/scripts/smoke.mts +0 -72
- package/scripts/upload-github.ps1 +0 -51
package/lib/types/bookshelf.d.ts
CHANGED
|
@@ -32,6 +32,6 @@ export declare function activateBook(id: string): BookEntry | undefined;
|
|
|
32
32
|
export declare function removeBook(id: string): boolean;
|
|
33
33
|
/** 当前书输出目录(无书架则 undefined,回退 settings)。 */
|
|
34
34
|
export declare function activeBookOutputDir(): string | undefined;
|
|
35
|
-
/**
|
|
35
|
+
/** 默认输出目录推断:~/.dsh/novels/书名。 */
|
|
36
36
|
export declare function defaultOutputDirFor(bookName: string): string;
|
|
37
37
|
export {};
|
package/lib/types/index.d.ts
CHANGED
|
@@ -56,7 +56,7 @@ export interface Config {
|
|
|
56
56
|
}
|
|
57
57
|
export declare const Config: z<Config>;
|
|
58
58
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
59
|
-
export declare const NOVEL_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-novel-forge \u63D2\u4EF6\uFF08AI \u7F16\u8BD1\u5C0F\u8BF4\u5DE5\u4F5C\u53F0\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300C\u5C0F\u8BF4\u5DE5\u574A\u300D\u5165\u53E3\u3002\u80FD\u529B\uFF1A\u8BFB\u53D6 docx \u5927\u7EB2\
|
|
59
|
+
export declare const NOVEL_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-novel-forge \u63D2\u4EF6\uFF08AI \u7F16\u8BD1\u5C0F\u8BF4\u5DE5\u4F5C\u53F0\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300C\u5C0F\u8BF4\u5DE5\u574A\u300D\u5165\u53E3\u3002\u80FD\u529B\uFF1A\u8BFB\u53D6 docx \u5927\u7EB2\u6216\u7C98\u8D34\u5927\u7EB2\u6587\u672C\uFF1B\u7528 LLM \u63D0\u70BC\u9053\u85CF\uFF08\u4EBA\u8BBE/\u4E16\u754C\u89C2/\u91D1\u624B\u6307\u89C4\u5219/\u5199\u4F5C\u7EA2\u7EBF\uFF0C\u5373\u8BBE\u5B9A\u5723\u7ECF\uFF09\uFF1B\u751F\u6210\u5377\u8BA1\u5212\u4E0E\u7AE0\u8282\u8BA1\u5212\uFF1B\u9010\u7AE0\u8C03\u7528 LLM \u751F\u6210 3000-4000 \u5B57\u6B63\u6587\u5E76\u4FDD\u5B58\u4E3A Markdown\uFF08\u9ED8\u8BA4\u8F93\u51FA\u5230\u7528\u6237\u4E3B\u76EE\u5F55 ~/.dsh/novels\uFF09\uFF1B\u6BCF\u7AE0\u81EA\u52A8\u751F\u6210\u6458\u8981\uFF08\u53D9\u4E8B\u8BB0\u5FC6\uFF09\u3001\u81EA\u52A8 AI \u5BA1\u7A3F\uFF08\u4EBA\u8BBE/\u8BBE\u5B9A/\u7EA2\u7EBF/\u6587\u7B14/\u723D\u70B9/\u903B\u8F91\uFF09\uFF0C\u652F\u6301\u6309\u5BA1\u7A3F\u610F\u89C1\u91CD\u5199\u3001\u53BB AI \u5473\u6DA6\u8272\u3001\u6697\u7EBF\uFF08\u4F0F\u7B14\uFF09\u7BA1\u7406\u3001\u6279\u91CF\u8FDE\u5199\u4E0E\u5168\u672C\u5BFC\u51FA\uFF08txt/md\uFF09\u3002\u9650\u5236\uFF1A\u751F\u6210\u6D88\u8017 LLM API \u989D\u5EA6\uFF1B\u8F93\u51FA\u76EE\u5F55\u4E0E\u6A21\u578B\u53EF\u5728\u63D2\u4EF6\u8BBE\u7F6E\u4E2D\u4FEE\u6539\uFF1B\u7AE0\u8282\u6B63\u6587\u8D28\u91CF\u53D6\u51B3\u4E8E\u5927\u7EB2\u5B8C\u6574\u5EA6\u3002\u7528\u6237\u63D0\u5230\u300C\u5C0F\u8BF4 / \u5927\u7EB2 / \u5199\u5C0F\u8BF4 / \u7AE0\u8282 / \u5BA1\u7A3F / \u6DA6\u8272\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002";
|
|
60
60
|
/** Resolve a config-like value into the full runtime config. */
|
|
61
61
|
export declare function resolveConfig(value: Partial<Config> | undefined): NovelConfig;
|
|
62
62
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waterwx/dsh-novel-forge",
|
|
3
3
|
"description": "AI 编译小说工作台 for the dsh web GUI: import a novel outline (docx/text), plan chapters with LLM, generate chapter by chapter (3000-4000 chars each), and save every chapter as Markdown into your chosen output folder.",
|
|
4
|
-
"version": "1.3.
|
|
4
|
+
"version": "1.3.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"lib/**/*.d.ts",
|
|
82
82
|
"lib/**/*.d.ts.map",
|
|
83
83
|
"src",
|
|
84
|
-
"scripts",
|
|
84
|
+
"scripts/patch-schemastery.mjs",
|
|
85
85
|
"cordis.patch.yml",
|
|
86
86
|
"README.md"
|
|
87
87
|
],
|
package/src/bookshelf.ts
CHANGED
|
@@ -144,8 +144,8 @@ export function activeBookOutputDir(): string | undefined {
|
|
|
144
144
|
return book?.outputDir
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
/**
|
|
147
|
+
/** 默认输出目录推断:~/.dsh/novels/书名。 */
|
|
148
148
|
export function defaultOutputDirFor(bookName: string): string {
|
|
149
149
|
const clean = bookName.replace(/[\\/:*?"<>|]/g, '').trim().slice(0, 40) || '未命名小说'
|
|
150
|
-
return join('
|
|
150
|
+
return join(homedir(), '.dsh', 'novels', clean)
|
|
151
151
|
}
|
|
@@ -1204,24 +1204,6 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
1204
1204
|
}
|
|
1205
1205
|
}
|
|
1206
1206
|
|
|
1207
|
-
/** 更新角色漫画重要性(main/support/extra)。 */
|
|
1208
|
-
const handleRoleImportance = async (role: RoleRecord, importance: RoleRecord['importance']): Promise<void> => {
|
|
1209
|
-
setBusy(true)
|
|
1210
|
-
setBusyLabel(`更新「${role.name}」重要性…`)
|
|
1211
|
-
setError('')
|
|
1212
|
-
try {
|
|
1213
|
-
await api.roles({ op: 'update', role: { ...role, importance } })
|
|
1214
|
-
await refresh(false)
|
|
1215
|
-
pushProgress(`已更新「${role.name}」重要性`, 'done')
|
|
1216
|
-
} catch (err) {
|
|
1217
|
-
setError((err as Error).message)
|
|
1218
|
-
pushProgress(`更新「${role.name}」重要性失败:${(err as Error).message}`, 'error')
|
|
1219
|
-
} finally {
|
|
1220
|
-
setBusy(false)
|
|
1221
|
-
setBusyLabel('')
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
1207
|
/** 作者复盘补跑:全书缺失章节(流式)。 */
|
|
1226
1208
|
const handleAuthorBackfillAll = async (): Promise<void> => {
|
|
1227
1209
|
const missing = chapters.filter(c => c.status !== 'pending' && c.status !== 'generating' && c.status !== 'error' && c.authorReview === undefined).length
|
|
@@ -3818,7 +3800,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
3818
3800
|
<option value="0">关(默认)</option>
|
|
3819
3801
|
<option value="1">开</option>
|
|
3820
3802
|
</select>
|
|
3821
|
-
<span className={css.meta}
|
|
3803
|
+
<span className={css.meta}>开启后角色详情显示「豆包生成」参考图按钮</span>
|
|
3822
3804
|
</div>
|
|
3823
3805
|
</div>
|
|
3824
3806
|
<div className={css.row}>
|
|
@@ -4396,7 +4378,7 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
4396
4378
|
<div className={css.card} style={{ flex: 1, minHeight: 0 }}>
|
|
4397
4379
|
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
4398
4380
|
<span className={css.cardTitle} style={{ fontSize: 17, fontWeight: 700 }}>🖼️ 角色形象</span>
|
|
4399
|
-
<span className={css.meta}>角色形象锚点 /
|
|
4381
|
+
<span className={css.meta}>角色形象锚点 / 生图提示词包 / 参考图集——复制提示词即可在即梦、豆包等工具出图</span>
|
|
4400
4382
|
</div>
|
|
4401
4383
|
|
|
4402
4384
|
<input
|
|
@@ -4663,10 +4645,10 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
4663
4645
|
for (const e of kit.expressions) blocks.push({ key: 'exp-' + e.name, title: `表情·${e.name}`, zh: e.zh, en: e.en })
|
|
4664
4646
|
blocks.push({ key: 'details', title: '细节', zh: kit.details.zh, en: kit.details.en })
|
|
4665
4647
|
} else {
|
|
4666
|
-
blocks.push({ key: 'portrait', title: '立绘', zh: anchor.zh + '
|
|
4667
|
-
blocks.push({ key: 'sheet', title: '四视图', zh: anchor.zh + '
|
|
4668
|
-
for (const n of expressions) blocks.push({ key: 'exp-' + n, title: `表情·${expName(n)}`, zh: anchor.zh + `。表情:${expName(n)}
|
|
4669
|
-
blocks.push({ key: 'details', title: '细节', zh:
|
|
4648
|
+
blocks.push({ key: 'portrait', title: '立绘', zh: '正面站立全身人像,3D动漫,超精细建模,纯白纯色背景,' + anchor.zh + '。角色设计稿,服装磨损、标志物细节完整展示,无多余杂物,全身完整无裁切。' + rulesZh, en: anchor.en + ', full body, front view, plain white background, 3d render, character design sheet' })
|
|
4649
|
+
blocks.push({ key: 'sheet', title: '四视图', zh: '角色设定四视图,3D动漫,超精细建模,纯白纯色背景,' + anchor.zh + '。同一角色正面/左侧面/右侧面/背面,四个视角分别描述,全身完整,服装与标志物细节完整展示,无多余杂物。' + rulesZh, en: anchor.en + ', character sheet, front view, left side view, right side view, back view, full body, plain white background, 3d render' })
|
|
4650
|
+
for (const n of expressions) blocks.push({ key: 'exp-' + n, title: `表情·${expName(n)}`, zh: '脸部特写(头部到锁骨),3D动漫,超精细建模,纯白纯色背景,' + anchor.zh + `。表情:${expName(n)},五官与角色定稿完全一致,皮肤纹理细节完整,无多余杂物。` + rulesZh, en: anchor.en + `, facial close-up, head to collarbone, expression: ${expName(n)}, plain white background, 3d render, consistent with character design, detailed skin texture, no extra objects` })
|
|
4651
|
+
blocks.push({ key: 'details', title: '细节', zh: `多组局部细节集合参考图,3D动漫,超精细建模,纯白纯色背景:${anchor.tags.map(t => t + '特写').join(';')}。细节清晰锐利,角色细节参考稿,无多余杂物。` + rulesZh, en: anchor.tags.join(', ') + ', multi-panel detail reference sheet, plain white background, 3d render, macro close-up, sharp details, character detail sheet, no extra objects' })
|
|
4670
4652
|
}
|
|
4671
4653
|
return (
|
|
4672
4654
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 8 }}>
|
|
@@ -4718,17 +4700,6 @@ export function NovelPanel({ controller, api }: NovelPanelProps) {
|
|
|
4718
4700
|
onChange={e => { setDetailUploadLabel(e.target.value) }}
|
|
4719
4701
|
/>
|
|
4720
4702
|
<button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={busy} onClick={() => { uploadInDetail(detailRole) }}>📤 上传图</button>
|
|
4721
|
-
<select
|
|
4722
|
-
className={css.input}
|
|
4723
|
-
style={{ fontSize: 12, padding: '3px 4px', width: 90 }}
|
|
4724
|
-
value={detailRole.importance ?? 'support'}
|
|
4725
|
-
onChange={e => { void handleRoleImportance(detailRole, e.target.value as RoleRecord['importance']) }}
|
|
4726
|
-
title="漫剧重要性"
|
|
4727
|
-
>
|
|
4728
|
-
<option value="main">重要</option>
|
|
4729
|
-
<option value="support">配角</option>
|
|
4730
|
-
<option value="extra">路人</option>
|
|
4731
|
-
</select>
|
|
4732
4703
|
</div>
|
|
4733
4704
|
</div>
|
|
4734
4705
|
</div>
|
package/src/engine.ts
CHANGED
|
@@ -1333,7 +1333,7 @@ export async function extractRoleVisual(
|
|
|
1333
1333
|
'- source:说明依据(如"第1章/第8章外貌描写;瞳色未明确")。',
|
|
1334
1334
|
'- expressions:该角色在本书剧情中需要的情绪表情清单数组,6-12 个(如 ["疲惫","麻木","压抑悲伤","皱眉","紧绷","放空"]),依据正文情绪描写与角色处境推断。',
|
|
1335
1335
|
'- promptKit:四类精修提示词(字段化+出图约束,参考示例结构):',
|
|
1336
|
-
' · portrait
|
|
1336
|
+
' · portrait 立绘:严格按字段顺序写(参考示例结构):开头构图定位(正面站立全身人像)→ 风格(3D动漫,超精细建模)→ 背景(纯白纯色背景)→ 身份(男子,{角色名},外表{年龄},男性)→ 发型发色 → 胡茬 → 眼眸 → 面部 → 气质 → 上身服装分件(颜色款式+磨损细节)→ 下身 → 鞋 → 标志物(标签/印记,含细节如洇暗翘边)→ 收尾(角色设计稿,细节完整展示,无多余杂物,全身完整无裁切)。',
|
|
1337
1337
|
' · sheet 四视图:同一角色的 正面/左侧面/右侧面/背面 四视图设定表(character sheet),纯白背景,全身完整,四个视角分别描述,服装与标志物细节完整展示。',
|
|
1338
1338
|
' · expressions 表情:每个表情一段,脸部特写(头部到锁骨),纯白背景,五官与角色定稿完全一致,只换情绪表达(眼神/嘴角/眉),皮肤纹理细节完整,无多余杂物。',
|
|
1339
1339
|
' · details 细节:多组局部细节集合参考图(一张图多个局部框),纯白背景;把该角色全部标志物逐项列出(如标签/印记/工牌/袖口磨损/鞋),每项一句特写描述;细节清晰锐利,角色细节参考稿,无多余杂物。',
|
|
@@ -1407,7 +1407,7 @@ export async function generateRolePromptKit(
|
|
|
1407
1407
|
const system = [
|
|
1408
1408
|
'你是一位 AI 绘图提示词工程师。基于给定角色的形象锚点、表情清单与本书视觉规则,输出四类生图提示词(每类 zh+en 各一段)。',
|
|
1409
1409
|
'四类:',
|
|
1410
|
-
'1. portrait
|
|
1410
|
+
'1. portrait 立绘:正面站立全身人像,3D动漫超精细建模,纯白纯色背景;按字段流组织:身份(男子,角色名,外表年龄)→ 发型发色 → 胡茬 → 眼眸 → 面部 → 气质 → 上身服装分件(含磨损)→ 下身 → 鞋 → 标志物(含细节)→ 收尾(角色设计稿,细节完整展示,无多余杂物,全身完整无裁切)。',
|
|
1411
1411
|
'2. sheet 四视图:同一角色的 正面/左侧面/右侧面/背面 四视图设定表(character sheet),纯白背景,四个视角分别描述。',
|
|
1412
1412
|
'3. expressions 表情:每个表情一段,脸部特写(头部到锁骨),纯白背景,五官与角色定稿完全一致,只换情绪表达(眼神/嘴角/眉),皮肤纹理细节完整,无多余杂物。',
|
|
1413
1413
|
'4. details 细节:多组局部细节集合参考图(一张图多个局部框),纯白背景;把该角色全部标志物逐项列出,每项一句特写描述;细节清晰锐利,角色细节参考稿,无多余杂物。',
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* changes.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { homedir } from 'node:os'
|
|
11
|
+
import { join } from 'node:path'
|
|
10
12
|
import type { Context } from '@deepseek-ai/cordis'
|
|
11
13
|
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
12
14
|
import z from 'schemastery'
|
|
@@ -69,8 +71,8 @@ export interface Config {
|
|
|
69
71
|
export const Config: z<Config> = z.object({
|
|
70
72
|
announceToAgent: z.boolean().default(true),
|
|
71
73
|
enabled: z.boolean().default(true),
|
|
72
|
-
outlinePath: z.string().default('
|
|
73
|
-
outputDir: z.string().default('
|
|
74
|
+
outlinePath: z.string().default(''),
|
|
75
|
+
outputDir: z.string().default(join(homedir(), '.dsh', 'novels')),
|
|
74
76
|
provider: z.string().default('deepseek-official'),
|
|
75
77
|
model: z.string().default('deepseek-v4-flash'),
|
|
76
78
|
reasoningEffort: z.union(['off', 'low', 'high', 'max']).default('off'),
|
|
@@ -87,8 +89,8 @@ export const Config: z<Config> = z.object({
|
|
|
87
89
|
|
|
88
90
|
/** Schema defaults, re-read for hand-built test contexts. */
|
|
89
91
|
const DEFAULT_ANNOUNCE = true
|
|
90
|
-
const DEFAULT_OUTLINE_PATH = '
|
|
91
|
-
const DEFAULT_OUTPUT_DIR = '
|
|
92
|
+
const DEFAULT_OUTLINE_PATH = ''
|
|
93
|
+
const DEFAULT_OUTPUT_DIR = join(homedir(), '.dsh', 'novels')
|
|
92
94
|
const DEFAULT_PROVIDER = 'deepseek-official'
|
|
93
95
|
const DEFAULT_MODEL = 'deepseek-v4-flash'
|
|
94
96
|
const DEFAULT_REASONING_EFFORT = 'off' as const
|
|
@@ -103,7 +105,7 @@ const DEFAULT_AUTO_REVIEW_AFTER_REVISE = true
|
|
|
103
105
|
const SECTION_ORDER = 160
|
|
104
106
|
|
|
105
107
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
106
|
-
export const NOVEL_GUIDANCE = '本机已安装 dsh-novel-forge 插件(AI 编译小说工作台):侧边栏「小说工坊」入口。能力:读取 docx
|
|
108
|
+
export const NOVEL_GUIDANCE = '本机已安装 dsh-novel-forge 插件(AI 编译小说工作台):侧边栏「小说工坊」入口。能力:读取 docx 大纲或粘贴大纲文本;用 LLM 提炼道藏(人设/世界观/金手指规则/写作红线,即设定圣经);生成卷计划与章节计划;逐章调用 LLM 生成 3000-4000 字正文并保存为 Markdown(默认输出到用户主目录 ~/.dsh/novels);每章自动生成摘要(叙事记忆)、自动 AI 审稿(人设/设定/红线/文笔/爽点/逻辑),支持按审稿意见重写、去 AI 味润色、暗线(伏笔)管理、批量连写与全本导出(txt/md)。限制:生成消耗 LLM API 额度;输出目录与模型可在插件设置中修改;章节正文质量取决于大纲完整度。用户提到「小说 / 大纲 / 写小说 / 章节 / 审稿 / 润色」时即指本插件,请据此协作。'
|
|
107
109
|
|
|
108
110
|
/** Resolve a config-like value into the full runtime config. */
|
|
109
111
|
export function resolveConfig(value: Partial<Config> | undefined): NovelConfig {
|
package/src/routes.ts
CHANGED
|
@@ -1699,7 +1699,7 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
1699
1699
|
}
|
|
1700
1700
|
const role = project.roles.find(r => r.name === name)
|
|
1701
1701
|
if (role === undefined) {
|
|
1702
|
-
writeJson(res, 404, { error:
|
|
1702
|
+
writeJson(res, 404, { error: `当前激活《${project.bookName}》,角色 ${name} 不存在——若期望的书不对,请先在书架切换该书并刷新` })
|
|
1703
1703
|
return
|
|
1704
1704
|
}
|
|
1705
1705
|
const label = body?.label?.trim() ?? ''
|
|
@@ -2079,7 +2079,7 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
2079
2079
|
}
|
|
2080
2080
|
const scene = project.scenes.find(x => x.name === name)
|
|
2081
2081
|
if (scene === undefined) {
|
|
2082
|
-
writeJson(res, 404, { error:
|
|
2082
|
+
writeJson(res, 404, { error: `当前激活《${project.bookName}》,场景 ${name} 不存在——若期望的书不对,请先在书架切换该书并刷新` })
|
|
2083
2083
|
return
|
|
2084
2084
|
}
|
|
2085
2085
|
const label = body?.label?.trim() ?? '全景'
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
{"type":"tool","name":"outline_replace","status":"start"}
|
|
2
|
-
{"type":"tool","name":"outline_replace","status":"error","detail":"大纲中未找到片段「第1卷《杂役墟醒》:开局底层发育,穿越与古玉认主落地,墟境规则逐步展开;宗门受欺…」"}
|
|
3
|
-
{"type":"tool","name":"outline_text","status":"start"}
|
|
4
|
-
{"type":"tool","name":"outline_text","status":"done","detail":"《归墟玉主》\n200 万字完整版 · 官方全书大纲(最终定稿)\n一、全书基础档案\n书名:归墟玉主\n题材:穿越修仙、苟道发育、秘境冒险、收集养成、炼丹炼器、渔翁得利爽文\n总规划字数:200 万字\n单章篇幅:3000–4000 字\n总章节区间:520–550 章\n核心对标风格\n资源闭环玩法:原材料采集 → 成品制作 → 神鼎升华,资源复利滚雪球成长\n长线收集体系:古玉残片解锁墟境功能,持续开放新玩法\n"}
|
|
5
|
-
{"type":"tool","name":"outline_replace","status":"start"}
|
|
6
|
-
{"type":"tool","name":"outline_replace","status":"done","detail":"大纲已修改:替换了 19 字符的片段。"}
|
|
7
|
-
{"type":"delta","text":"第一卷大纲已更新完毕。改动位置在\"第一卷:杂役墟醒 → 主线剧情 → 开篇铺垫\"这一行:\n\n- **原文**:开篇铺垫:外卖穿越、古玉碎护魂完整背景\n- **改后**:开篇铺垫:暴雨送餐夜拾回祖传古玉碎块、碎玉暗藏\"界\"字与低语、穿越时碎玉护魂,完整交代古玉来历并埋设守玉人伏笔\n\n这样\"暴雨天捡到古玉\"的伏笔正式进入大纲,与既有的\"穿越时碎玉护魂\"、\"残缺的界字、守玉人低语\"设定形成三层递进:**雨夜拾玉(现实伏笔)→ 碎玉护魂(穿越金手指)→ 界字低语(上界崩塌秘辛)**,后续第四、五卷回收时逻辑更顺。\n\n需要我接着在正文里落实这个伏笔吗?可以:\n\n1. **检查第1章《暴雨穿界》**——看现有正文是否已有\"雨天拾玉\"桥段,若没有就补一小段自然嵌入;\n2. **调整第2章《滴血认主》**——确保\"界字低语\"在滴血认主时第一次闪现,与大纲呼应。\n\n你希望先做哪一个?或者直接让我先调出第1章正文看看目前写法。"}
|
|
8
|
-
{"type":"done"}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Diagnose the adapter directly: instantiate DeepSeekAdapter with resolved
|
|
3
|
-
* connection facts (key from ~/.dsh/.credentials.yaml) and stream the bible
|
|
4
|
-
* prompt, printing every chunk. This isolates the adapter from cordis.
|
|
5
|
-
* Run: node scripts/diagnose-adapter.mjs (from novel-forge dir, using the
|
|
6
|
-
* profile's node_modules via the harness loader).
|
|
7
|
-
*/
|
|
8
|
-
import { readFileSync } from 'node:fs'
|
|
9
|
-
import { homedir } from 'node:os'
|
|
10
|
-
import { join } from 'node:path'
|
|
11
|
-
import { DeepSeekAdapter } from '@deepseek-ai/dsh-llm-deepseek'
|
|
12
|
-
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
13
|
-
|
|
14
|
-
const creds = readFileSync(join(homedir(), '.dsh', '.credentials.yaml'), 'utf8')
|
|
15
|
-
const m = /DEEPSEEK_API_KEY:\s*(.+)/.exec(creds)
|
|
16
|
-
if (!m) { console.error('no key found'); process.exit(1) }
|
|
17
|
-
const apiKey = m[1].trim()
|
|
18
|
-
|
|
19
|
-
const outline = readFileSync('D:\\ryan work\\harness\\plugins\\novel-forge\\scripts\\outline-sample.txt', 'utf8')
|
|
20
|
-
|
|
21
|
-
const system = [
|
|
22
|
-
'你是一位资深网文编辑兼设定架构师。你会收到一份小说大纲,请把它提炼成结构化的「设定圣经」(Story Bible),供后续写作时严格引用。',
|
|
23
|
-
'要求:',
|
|
24
|
-
'1. 忠于大纲,不自行发明大纲之外的设定。',
|
|
25
|
-
'2. 角色卡覆盖大纲明确出现的角色(主角必含),每个角色给出性格标签、目标、关键关系。',
|
|
26
|
-
'3. 世界规则覆盖力量体系、金手指机制、势力、地理等所有硬性规则,逐条列出。',
|
|
27
|
-
'4. 红线列出大纲中明确禁止的内容(如无后宫、不圣母、无无脑碾压等)。',
|
|
28
|
-
'5. 风格列出叙事基调、节奏、POV 等写作风格要点。',
|
|
29
|
-
'输出必须是合法 JSON 对象,不要输出任何其他文字或 Markdown 代码块标记。',
|
|
30
|
-
'重要:所有字符串值内部不得包含换行符(不要用多行字符串),JSON 必须在一段内完整结束。',
|
|
31
|
-
'JSON 结构:',
|
|
32
|
-
'{"genre": "题材与基调一句话", "worldRules": ["规则1", "规则2", ...], "characters": [{"name": "角色名", "role": "protagonist|supporting|antagonist|other", "traits": ["标签1", ...], "goals": "目标与动机", "relations": "关键关系"}], "redLines": ["红线1", ...], "style": ["风格1", ...]}',
|
|
33
|
-
].join('\n')
|
|
34
|
-
|
|
35
|
-
const adapter = new DeepSeekAdapter({
|
|
36
|
-
options: () => ({
|
|
37
|
-
baseURL: 'https://api.deepseek.com',
|
|
38
|
-
apiKeyEnv: 'DEEPSEEK_API_KEY',
|
|
39
|
-
defaultContextWindow: 1000000,
|
|
40
|
-
maxTokens: 256000,
|
|
41
|
-
streamIdleTimeoutMs: 300000,
|
|
42
|
-
thinking: 'enabled',
|
|
43
|
-
reasoningEffort: 'high',
|
|
44
|
-
models: [],
|
|
45
|
-
}),
|
|
46
|
-
resolveApiKey: async () => apiKey,
|
|
47
|
-
resolveUserId: () => ({ id: 'diagnose' }),
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
const messages = [createUserMessage({
|
|
51
|
-
content: [{ type: 'text', text: `请为下面这部小说提炼设定圣经:\n\n${outline}` }],
|
|
52
|
-
source: { kind: 'plugin', plugin: 'dsh-novel-forge' },
|
|
53
|
-
})]
|
|
54
|
-
|
|
55
|
-
console.log('adapter.stream()...')
|
|
56
|
-
let textLen = 0
|
|
57
|
-
let reasoningLen = 0
|
|
58
|
-
let blocks = 0
|
|
59
|
-
let finish: unknown
|
|
60
|
-
try {
|
|
61
|
-
for await (const chunk of adapter.stream({
|
|
62
|
-
provider: 'deepseek-official',
|
|
63
|
-
model: 'deepseek-v4-flash',
|
|
64
|
-
messages: messages as never,
|
|
65
|
-
system,
|
|
66
|
-
maxTokens: 16000,
|
|
67
|
-
temperature: 0.4,
|
|
68
|
-
} as never)) {
|
|
69
|
-
if (chunk.type === 'text-delta') textLen += chunk.text.length
|
|
70
|
-
else if (chunk.type === 'reasoning-delta') reasoningLen += chunk.text.length
|
|
71
|
-
else if (chunk.type === 'block-end') blocks++
|
|
72
|
-
else if (chunk.type === 'finish') finish = chunk.reason
|
|
73
|
-
}
|
|
74
|
-
console.log('text chars:', textLen)
|
|
75
|
-
console.log('reasoning chars:', reasoningLen)
|
|
76
|
-
console.log('blocks:', blocks)
|
|
77
|
-
console.log('finish:', JSON.stringify(finish))
|
|
78
|
-
} catch (error) {
|
|
79
|
-
console.error('stream threw:', (error as Error).message)
|
|
80
|
-
}
|
|
81
|
-
process.exit(0)
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Diagnose the assistant stream with node: POST a message, save all frames
|
|
3
|
-
* to a file, print a summary (frame types + sample texts).
|
|
4
|
-
*/
|
|
5
|
-
import { writeFileSync } from 'node:fs'
|
|
6
|
-
|
|
7
|
-
const BASE = 'http://127.0.0.1:3080'
|
|
8
|
-
|
|
9
|
-
async function main() {
|
|
10
|
-
const message = process.argv[2] ?? '把大纲第一段的剧情要点改一下:加一个下雨天主角捡到古玉的伏笔'
|
|
11
|
-
console.log('message:', message)
|
|
12
|
-
|
|
13
|
-
const response = await fetch(BASE + '/api/dsh-novel-forge/assistant', {
|
|
14
|
-
method: 'POST',
|
|
15
|
-
headers: { 'content-type': 'application/json' },
|
|
16
|
-
body: JSON.stringify({ message }),
|
|
17
|
-
})
|
|
18
|
-
console.log('HTTP', response.status)
|
|
19
|
-
const text = await response.text()
|
|
20
|
-
writeFileSync('D:\\ryan work\\harness\\plugins\\novel-forge\\scripts\\assistant-frames.txt', text, 'utf8')
|
|
21
|
-
|
|
22
|
-
const frames = text.split('\n').filter(l => l.trim() !== '')
|
|
23
|
-
console.log('frames:', frames.length)
|
|
24
|
-
let deltas = 0
|
|
25
|
-
let toolDeltas = 0
|
|
26
|
-
let tools = 0
|
|
27
|
-
let done = 0
|
|
28
|
-
let errors = 0
|
|
29
|
-
let toolDeltaChars = 0
|
|
30
|
-
for (const line of frames) {
|
|
31
|
-
try {
|
|
32
|
-
const f = JSON.parse(line)
|
|
33
|
-
if (f.type === 'delta') deltas++
|
|
34
|
-
else if (f.type === 'toolDelta') { toolDeltas++; toolDeltaChars += f.text.length }
|
|
35
|
-
else if (f.type === 'tool') { tools++; if (tools <= 6) console.log(' tool:', f.status, f.name, String(f.detail ?? '').slice(0, 120)) }
|
|
36
|
-
else if (f.type === 'done') done++
|
|
37
|
-
else if (f.type === 'error') { errors++; console.log(' ERROR:', f.message) }
|
|
38
|
-
} catch { /* skip */ }
|
|
39
|
-
}
|
|
40
|
-
console.log('delta frames:', deltas)
|
|
41
|
-
console.log('toolDelta frames:', toolDeltas, '(', toolDeltaChars, 'chars )')
|
|
42
|
-
console.log('tool frames:', tools)
|
|
43
|
-
console.log('done:', done, 'errors:', errors)
|
|
44
|
-
|
|
45
|
-
// Print the first delta frame head and any action tag seen in deltas.
|
|
46
|
-
for (const line of frames) {
|
|
47
|
-
try {
|
|
48
|
-
const f = JSON.parse(line)
|
|
49
|
-
if (f.type === 'delta' && f.text.includes('<dsh-action')) {
|
|
50
|
-
console.log('--- delta containing action tag ---')
|
|
51
|
-
console.log(f.text.slice(0, 400))
|
|
52
|
-
break
|
|
53
|
-
}
|
|
54
|
-
} catch { /* skip */ }
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
main().catch(e => { console.error('FAIL:', e.message); process.exit(1) })
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Diagnostic: call the DeepSeek API directly with the exact bible prompt to
|
|
3
|
-
* see what the model actually returns (content? reasoning-only? empty?).
|
|
4
|
-
* Run: node scripts/diagnose-bible.mjs
|
|
5
|
-
*/
|
|
6
|
-
import { readFileSync } from 'node:fs'
|
|
7
|
-
import { homedir } from 'node:os'
|
|
8
|
-
import { join } from 'node:path'
|
|
9
|
-
|
|
10
|
-
// Read the API key from ~/.dsh/.credentials.yaml (simple line parse).
|
|
11
|
-
const creds = readFileSync(join(homedir(), '.dsh', '.credentials.yaml'), 'utf8')
|
|
12
|
-
const m = /DEEPSEEK_API_KEY:\s*(.+)/.exec(creds)
|
|
13
|
-
if (!m) { console.error('no key found'); process.exit(1) }
|
|
14
|
-
const apiKey = m[1].trim()
|
|
15
|
-
|
|
16
|
-
const outline = readFileSync('D:\\ryan work\\harness\\plugins\\novel-forge\\scripts\\outline-sample.txt', 'utf8')
|
|
17
|
-
|
|
18
|
-
const system = [
|
|
19
|
-
'你是一位资深网文编辑兼设定架构师。你会收到一份小说大纲,请把它提炼成结构化的「设定圣经」(Story Bible),供后续写作时严格引用。',
|
|
20
|
-
'要求:',
|
|
21
|
-
'1. 忠于大纲,不自行发明大纲之外的设定。',
|
|
22
|
-
'2. 角色卡覆盖大纲明确出现的角色(主角必含),每个角色给出性格标签、目标、关键关系。',
|
|
23
|
-
'3. 世界规则覆盖力量体系、金手指机制、势力、地理等所有硬性规则,逐条列出。',
|
|
24
|
-
'4. 红线列出大纲中明确禁止的内容(如无后宫、不圣母、无无脑碾压等)。',
|
|
25
|
-
'5. 风格列出叙事基调、节奏、POV 等写作风格要点。',
|
|
26
|
-
'输出必须是合法 JSON 对象,不要输出任何其他文字或 Markdown 代码块标记。',
|
|
27
|
-
'重要:所有字符串值内部不得包含换行符(不要用多行字符串),JSON 必须在一段内完整结束。',
|
|
28
|
-
'JSON 结构:',
|
|
29
|
-
'{"genre": "题材与基调一句话", "worldRules": ["规则1", "规则2", ...], "characters": [{"name": "角色名", "role": "protagonist|supporting|antagonist|other", "traits": ["标签1", ...], "goals": "目标与动机", "relations": "关键关系"}], "redLines": ["红线1", ...], "style": ["风格1", ...]}',
|
|
30
|
-
].join('\n')
|
|
31
|
-
|
|
32
|
-
const body = {
|
|
33
|
-
model: 'deepseek-v4-flash',
|
|
34
|
-
messages: [
|
|
35
|
-
{ role: 'system', content: system },
|
|
36
|
-
{ role: 'user', content: `请为下面这部小说提炼设定圣经:\n\n${outline}` },
|
|
37
|
-
],
|
|
38
|
-
max_tokens: 16000,
|
|
39
|
-
temperature: 0.4,
|
|
40
|
-
stream: false,
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
console.log('request: model=deepseek-v4-flash max_tokens=16000')
|
|
44
|
-
const t0 = Date.now()
|
|
45
|
-
const response = await fetch('https://api.deepseek.com/chat/completions', {
|
|
46
|
-
method: 'POST',
|
|
47
|
-
headers: {
|
|
48
|
-
'content-type': 'application/json',
|
|
49
|
-
authorization: `Bearer ${apiKey}`,
|
|
50
|
-
},
|
|
51
|
-
body: JSON.stringify(body),
|
|
52
|
-
})
|
|
53
|
-
console.log(`HTTP ${response.status} in ${Date.now() - t0}ms`)
|
|
54
|
-
if (!response.ok) {
|
|
55
|
-
console.log(await response.text())
|
|
56
|
-
process.exit(1)
|
|
57
|
-
}
|
|
58
|
-
const data = await response.json()
|
|
59
|
-
const choice = data.choices?.[0]
|
|
60
|
-
console.log('finish_reason:', choice?.finish_reason)
|
|
61
|
-
console.log('message keys:', Object.keys(choice?.message ?? {}))
|
|
62
|
-
const content = choice?.message?.content ?? ''
|
|
63
|
-
const reasoning = choice?.message?.reasoning_content ?? ''
|
|
64
|
-
console.log('content length:', content.length)
|
|
65
|
-
console.log('reasoning length:', reasoning.length)
|
|
66
|
-
console.log('--- content head ---')
|
|
67
|
-
console.log(content.slice(0, 800))
|
|
68
|
-
if (reasoning.length > 0) {
|
|
69
|
-
console.log('--- reasoning head ---')
|
|
70
|
-
console.log(reasoning.slice(0, 400))
|
|
71
|
-
}
|
|
72
|
-
console.log('--- usage ---', JSON.stringify(data.usage))
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Diagnose the harness LLM path properly: build a cordis context, mount
|
|
3
|
-
* LlmRuntime + deepseek plugin, and check what ctx.llm.stream returns.
|
|
4
|
-
* Run from deepseek-harness-master (has tsx).
|
|
5
|
-
*/
|
|
6
|
-
import { Context } from '@deepseek-ai/cordis'
|
|
7
|
-
import LlmRuntime from '@deepseek-ai/dsh-llm'
|
|
8
|
-
import { createUserMessage, BlockAssembler } from '@deepseek-ai/dsh-llm'
|
|
9
|
-
import * as DeepSeekPlugin from '@deepseek-ai/dsh-llm-deepseek'
|
|
10
|
-
import { readFileSync } from 'node:fs'
|
|
11
|
-
|
|
12
|
-
const outline = readFileSync('D:\\ryan work\\harness\\plugins\\novel-forge\\scripts\\outline-sample.txt', 'utf8')
|
|
13
|
-
|
|
14
|
-
const system = [
|
|
15
|
-
'你是一位资深网文编辑兼设定架构师。你会收到一份小说大纲,请把它提炼成结构化的「设定圣经」(Story Bible),供后续写作时严格引用。',
|
|
16
|
-
'1. 忠于大纲,不自行发明大纲之外的设定。',
|
|
17
|
-
'输出必须是合法 JSON 对象,不要输出任何其他文字。',
|
|
18
|
-
'JSON 结构:',
|
|
19
|
-
'{"genre": "题材与基调一句话", "worldRules": ["规则1"], "characters": [], "redLines": [], "style": []}',
|
|
20
|
-
].join('\n')
|
|
21
|
-
|
|
22
|
-
const ctx = new Context()
|
|
23
|
-
console.log('before plugin, ctx.llm =', typeof ctx.llm)
|
|
24
|
-
ctx.plugin(LlmRuntime)
|
|
25
|
-
console.log('after LlmRuntime, ctx.llm =', typeof ctx.llm)
|
|
26
|
-
try {
|
|
27
|
-
ctx.plugin(DeepSeekPlugin as never, { reasoningEffort: 'high' } as never)
|
|
28
|
-
console.log('deepseek plugin applied')
|
|
29
|
-
} catch (error) {
|
|
30
|
-
console.error('deepseek plugin failed:', (error as Error).message)
|
|
31
|
-
}
|
|
32
|
-
console.log('providers:', JSON.stringify(ctx.llm?.listProviders() ?? 'NO-LLM'))
|
|
33
|
-
|
|
34
|
-
const messages = [createUserMessage({
|
|
35
|
-
content: [{ type: 'text', text: `请为下面这部小说提炼设定圣经:\n\n${outline}` }],
|
|
36
|
-
source: { kind: 'plugin', plugin: 'dsh-novel-forge' },
|
|
37
|
-
})]
|
|
38
|
-
|
|
39
|
-
const request = {
|
|
40
|
-
provider: 'deepseek-official',
|
|
41
|
-
model: 'deepseek-v4-flash',
|
|
42
|
-
messages,
|
|
43
|
-
system,
|
|
44
|
-
maxTokens: 16000,
|
|
45
|
-
temperature: 0.4,
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
console.log('calling ctx.llm.stream...')
|
|
49
|
-
const assembler = new BlockAssembler()
|
|
50
|
-
let textLen = 0
|
|
51
|
-
let reasoningLen = 0
|
|
52
|
-
let sawFinish = false
|
|
53
|
-
try {
|
|
54
|
-
for await (const chunk of ctx.llm.stream(request as never)) {
|
|
55
|
-
assembler.push(chunk)
|
|
56
|
-
if (chunk.type === 'text-delta') textLen += chunk.text.length
|
|
57
|
-
else if (chunk.type === 'reasoning-delta') reasoningLen += chunk.text.length
|
|
58
|
-
else if (chunk.type === 'finish') sawFinish = true
|
|
59
|
-
}
|
|
60
|
-
const blocks = assembler.blocks()
|
|
61
|
-
console.log('text chars:', textLen)
|
|
62
|
-
console.log('reasoning chars:', reasoningLen)
|
|
63
|
-
console.log('assembled blocks:', blocks.map(b => b.type).join(','))
|
|
64
|
-
console.log('finish:', JSON.stringify(assembler.finish))
|
|
65
|
-
const text = blocks
|
|
66
|
-
.filter((block): block is Extract<typeof block, { type: 'text' }> => block.type === 'text')
|
|
67
|
-
.map(block => block.text)
|
|
68
|
-
.join('\n')
|
|
69
|
-
.trim()
|
|
70
|
-
console.log('complete() would return chars:', text.length)
|
|
71
|
-
console.log('head:', text.slice(0, 200))
|
|
72
|
-
} catch (error) {
|
|
73
|
-
console.error('stream threw:', (error as Error).message)
|
|
74
|
-
}
|
|
75
|
-
process.exit(0)
|