@waterwx/dsh-novel-forge 1.3.0 → 1.3.1
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/package.json +2 -1
- package/scripts/assistant-frames.txt +8 -0
- package/scripts/diagnose-adapter.mts +81 -0
- package/scripts/diagnose-assistant.mjs +58 -0
- package/scripts/diagnose-bible.mjs +72 -0
- package/scripts/diagnose-harness.mts +75 -0
- package/scripts/novel-forge-restart.log +10 -0
- package/scripts/novel-forge-web.stderr.log +2 -0
- package/scripts/novel-forge-web.stdout.log +1 -0
- package/scripts/outline-sample.txt +449 -0
- package/scripts/patch-schemastery.mjs +47 -0
- package/scripts/probe.mjs +6 -0
- package/scripts/restart-web.ps1 +88 -0
- package/scripts/smoke.mts +72 -0
- package/scripts/upload-github.ps1 +51 -0
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.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
"lib/**/*.d.ts",
|
|
82
82
|
"lib/**/*.d.ts.map",
|
|
83
83
|
"src",
|
|
84
|
+
"scripts",
|
|
84
85
|
"cordis.patch.yml",
|
|
85
86
|
"README.md"
|
|
86
87
|
],
|
|
@@ -0,0 +1,8 @@
|
|
|
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"}
|
|
@@ -0,0 +1,81 @@
|
|
|
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)
|
|
@@ -0,0 +1,58 @@
|
|
|
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) })
|
|
@@ -0,0 +1,72 @@
|
|
|
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))
|
|
@@ -0,0 +1,75 @@
|
|
|
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)
|