novelws 5.1.0 → 5.3.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/CHANGELOG.md +52 -0
- package/README.md +54 -3
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/dashboard.d.ts +3 -0
- package/dist/commands/dashboard.d.ts.map +1 -0
- package/dist/commands/dashboard.js +42 -0
- package/dist/commands/dashboard.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +18 -4
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +23 -0
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/core/config.d.ts +4 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +5 -2
- package/dist/core/config.js.map +1 -1
- package/dist/server/datasource/db.d.ts +38 -0
- package/dist/server/datasource/db.d.ts.map +1 -0
- package/dist/server/datasource/db.js +323 -0
- package/dist/server/datasource/db.js.map +1 -0
- package/dist/server/datasource/fs.d.ts +30 -0
- package/dist/server/datasource/fs.d.ts.map +1 -0
- package/dist/server/datasource/fs.js +308 -0
- package/dist/server/datasource/fs.js.map +1 -0
- package/dist/server/datasource/index.d.ts +11 -0
- package/dist/server/datasource/index.d.ts.map +1 -0
- package/dist/server/datasource/index.js +33 -0
- package/dist/server/datasource/index.js.map +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +69 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/routes/characters.d.ts +4 -0
- package/dist/server/routes/characters.d.ts.map +1 -0
- package/dist/server/routes/characters.js +27 -0
- package/dist/server/routes/characters.js.map +1 -0
- package/dist/server/routes/plots.d.ts +4 -0
- package/dist/server/routes/plots.d.ts.map +1 -0
- package/dist/server/routes/plots.js +36 -0
- package/dist/server/routes/plots.js.map +1 -0
- package/dist/server/routes/protagonist.d.ts +4 -0
- package/dist/server/routes/protagonist.d.ts.map +1 -0
- package/dist/server/routes/protagonist.js +46 -0
- package/dist/server/routes/protagonist.js.map +1 -0
- package/dist/server/routes/relationships.d.ts +4 -0
- package/dist/server/routes/relationships.d.ts.map +1 -0
- package/dist/server/routes/relationships.js +27 -0
- package/dist/server/routes/relationships.js.map +1 -0
- package/dist/server/routes/stats.d.ts +4 -0
- package/dist/server/routes/stats.d.ts.map +1 -0
- package/dist/server/routes/stats.js +21 -0
- package/dist/server/routes/stats.js.map +1 -0
- package/dist/server/routes/stories.d.ts +4 -0
- package/dist/server/routes/stories.d.ts.map +1 -0
- package/dist/server/routes/stories.js +80 -0
- package/dist/server/routes/stories.js.map +1 -0
- package/dist/server/routes/timeline.d.ts +4 -0
- package/dist/server/routes/timeline.d.ts.map +1 -0
- package/dist/server/routes/timeline.js +17 -0
- package/dist/server/routes/timeline.js.map +1 -0
- package/dist/server/types.d.ts +199 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +2 -0
- package/dist/server/types.js.map +1 -0
- package/dist/utils/diagnostics.d.ts +2 -2
- package/dist/utils/diagnostics.d.ts.map +1 -1
- package/dist/utils/diagnostics.js +46 -59
- package/dist/utils/diagnostics.js.map +1 -1
- package/package.json +13 -3
- package/templates/commands/analyze.md +20 -8
- package/templates/commands/expand.md +46 -20
- package/templates/commands/plan.md +3 -2
- package/templates/commands/specify.md +6 -2
- package/templates/commands/write.md +77 -24
- package/templates/dot-claude/CLAUDE.md +48 -8
- package/templates/resources/anti-ai.md +92 -13
- package/templates/resources/constitution.md +83 -140
- package/templates/resources/style-reference.md +30 -6
- package/templates/scripts/db_context.py +609 -0
- package/templates/scripts/db_init_protagonist.py +343 -0
- package/templates/scripts/db_sync.py +611 -0
- package/templates/scripts/db_volume_switch.py +278 -0
- package/templates/scripts/phase_a_init_db.py +428 -0
- package/templates/scripts/requirements.txt +1 -0
- package/templates/tracking/character-state.json +1 -3
- package/templates/tracking/plot-tracker.json +1 -5
- package/templates/tracking/relationships.json +1 -3
- package/templates/tracking/timeline.json +1 -3
- package/templates/volume-outline.md +31 -0
- package/templates/volume-summary.md +20 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""db_context.py — 从 DB 生成精确最小上下文
|
|
3
|
+
|
|
4
|
+
用法:
|
|
5
|
+
python scripts/db_context.py --chapter 42 --mode write
|
|
6
|
+
python scripts/db_context.py --chapter 42 --mode expand
|
|
7
|
+
python scripts/db_context.py --chapter 42 --mode analyze
|
|
8
|
+
python scripts/db_context.py --dashboard
|
|
9
|
+
|
|
10
|
+
输出 markdown 格式的上下文摘要到 stdout,可重定向到文件供 AI 读取。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import psycopg2
|
|
18
|
+
|
|
19
|
+
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
20
|
+
SCHEMA = "novelws"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_config():
|
|
24
|
+
config_path = os.path.join(BASE, "resources", "config.json")
|
|
25
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
26
|
+
return json.load(f)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_db_config(config):
|
|
30
|
+
db = config.get("database", {})
|
|
31
|
+
return {
|
|
32
|
+
"host": db.get("host", "127.0.0.1"),
|
|
33
|
+
"port": db.get("port", 5432),
|
|
34
|
+
"dbname": db.get("dbname", "postgres"),
|
|
35
|
+
"user": db.get("user", "postgres"),
|
|
36
|
+
"password": db.get("password", ""),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_conn():
|
|
41
|
+
config = load_config()
|
|
42
|
+
return psycopg2.connect(**get_db_config(config))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_chapter_info(cur, global_ch):
|
|
46
|
+
"""获取章节基本信息和所属卷"""
|
|
47
|
+
cur.execute(f"""
|
|
48
|
+
SELECT ch.id, ch.chapter_number, ch.time_in_story, ch.status,
|
|
49
|
+
v.id as vol_id, v.vol_number, v.title as vol_title,
|
|
50
|
+
p.phase_number, p.title as phase_title
|
|
51
|
+
FROM {SCHEMA}.chapters ch
|
|
52
|
+
JOIN {SCHEMA}.volumes v ON ch.volume_id = v.id
|
|
53
|
+
JOIN {SCHEMA}.phases p ON v.phase_id = p.id
|
|
54
|
+
WHERE ch.global_chapter_number = %s
|
|
55
|
+
""", (global_ch,))
|
|
56
|
+
return cur.fetchone()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def context_write(cur, global_ch):
|
|
60
|
+
"""生成 /write 模式的上下文"""
|
|
61
|
+
info = get_chapter_info(cur, global_ch)
|
|
62
|
+
if not info:
|
|
63
|
+
print(f"## 错误: 章节 {global_ch} 不存在于数据库中")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
ch_id, ch_num, time_in_story, ch_status, vol_id, vol_num, vol_title, phase_num, phase_title = info
|
|
67
|
+
|
|
68
|
+
print(f"# /write 上下文 — 第{global_ch}章 (vol-{vol_num:03d} ch-{ch_num:03d})")
|
|
69
|
+
print(f"纪元{phase_num} [{phase_title}] · {vol_title}")
|
|
70
|
+
print()
|
|
71
|
+
|
|
72
|
+
# 1. 当前卷活跃角色
|
|
73
|
+
print("## 活跃角色")
|
|
74
|
+
cur.execute(f"""
|
|
75
|
+
SELECT c.name, c.role_type, cs.location, cs.state_summary, cs.last_appearance
|
|
76
|
+
FROM {SCHEMA}.characters c
|
|
77
|
+
JOIN {SCHEMA}.character_states cs ON c.id = cs.character_id
|
|
78
|
+
WHERE cs.volume_id = %s
|
|
79
|
+
ORDER BY cs.last_appearance DESC
|
|
80
|
+
""", (vol_id,))
|
|
81
|
+
rows = cur.fetchall()
|
|
82
|
+
if rows:
|
|
83
|
+
print("| 角色 | 身份 | 位置 | 状态 | 最后出场 |")
|
|
84
|
+
print("|------|------|------|------|---------|")
|
|
85
|
+
for r in rows:
|
|
86
|
+
print(f"| {r[0]} | {r[1]} | {r[2] or '-'} | {_truncate(r[3], 60)} | ch-{r[4]} |")
|
|
87
|
+
print()
|
|
88
|
+
|
|
89
|
+
# 2. 未解决伏笔
|
|
90
|
+
print("## 未解决伏笔")
|
|
91
|
+
cur.execute(f"""
|
|
92
|
+
SELECT f.code, f.description, f.status, ch.chapter_number as planted_at, f.note
|
|
93
|
+
FROM {SCHEMA}.foreshadowing f
|
|
94
|
+
JOIN {SCHEMA}.chapters ch ON f.planted_chapter_id = ch.id
|
|
95
|
+
WHERE f.status IN ('planted', 'hinted', 'partially_resolved')
|
|
96
|
+
AND ch.volume_id = %s
|
|
97
|
+
ORDER BY ch.chapter_number
|
|
98
|
+
""", (vol_id,))
|
|
99
|
+
rows = cur.fetchall()
|
|
100
|
+
if rows:
|
|
101
|
+
print(f"共 {len(rows)} 条:")
|
|
102
|
+
for r in rows:
|
|
103
|
+
note = f" ({r[4]})" if r[4] else ""
|
|
104
|
+
print(f"- [{r[0]}] ch-{r[3]:03d} [{r[2]}]: {r[1]}{note}")
|
|
105
|
+
else:
|
|
106
|
+
print("无")
|
|
107
|
+
print()
|
|
108
|
+
|
|
109
|
+
# 3. 活跃情节线
|
|
110
|
+
print("## 活跃情节线")
|
|
111
|
+
cur.execute(f"""
|
|
112
|
+
SELECT name, status, description
|
|
113
|
+
FROM {SCHEMA}.plot_threads
|
|
114
|
+
WHERE status NOT IN ('已完成')
|
|
115
|
+
ORDER BY id
|
|
116
|
+
""")
|
|
117
|
+
rows = cur.fetchall()
|
|
118
|
+
if rows:
|
|
119
|
+
for r in rows:
|
|
120
|
+
print(f"- **{r[0]}** [{r[1]}]: {_truncate(r[2], 80)}")
|
|
121
|
+
print()
|
|
122
|
+
|
|
123
|
+
# 4. 前5章概要摘要
|
|
124
|
+
print("## 前序章节")
|
|
125
|
+
start_ch = max(1, ch_num - 5)
|
|
126
|
+
cur.execute(f"""
|
|
127
|
+
SELECT chapter_number, time_in_story, synopsis_summary
|
|
128
|
+
FROM {SCHEMA}.chapters
|
|
129
|
+
WHERE volume_id = %s AND chapter_number >= %s AND chapter_number < %s
|
|
130
|
+
ORDER BY chapter_number
|
|
131
|
+
""", (vol_id, start_ch, ch_num))
|
|
132
|
+
rows = cur.fetchall()
|
|
133
|
+
if rows:
|
|
134
|
+
for r in rows:
|
|
135
|
+
print(f"- ch-{r[0]:03d} [{r[1] or '?'}]: {r[2] or '(无摘要)'}")
|
|
136
|
+
else:
|
|
137
|
+
print("无前序章节")
|
|
138
|
+
print()
|
|
139
|
+
|
|
140
|
+
# 5. 角色关系
|
|
141
|
+
print("## 关键关系")
|
|
142
|
+
cur.execute(f"""
|
|
143
|
+
SELECT ca.name, cb.name, r.relationship_type, r.current_status
|
|
144
|
+
FROM {SCHEMA}.relationships r
|
|
145
|
+
JOIN {SCHEMA}.characters ca ON r.character_a_id = ca.id
|
|
146
|
+
JOIN {SCHEMA}.characters cb ON r.character_b_id = cb.id
|
|
147
|
+
WHERE r.volume_id = %s
|
|
148
|
+
ORDER BY r.last_update_chapter DESC
|
|
149
|
+
LIMIT 10
|
|
150
|
+
""", (vol_id,))
|
|
151
|
+
rows = cur.fetchall()
|
|
152
|
+
if rows:
|
|
153
|
+
for r in rows:
|
|
154
|
+
print(f"- {r[0]} → {r[1]} [{r[2]}]: {_truncate(r[3], 60)}")
|
|
155
|
+
print()
|
|
156
|
+
|
|
157
|
+
# 6. 时间线(最近5条)
|
|
158
|
+
print("## 最近时间线")
|
|
159
|
+
cur.execute(f"""
|
|
160
|
+
SELECT ch.chapter_number, te.story_time, te.event_description
|
|
161
|
+
FROM {SCHEMA}.timeline_events te
|
|
162
|
+
JOIN {SCHEMA}.chapters ch ON te.chapter_id = ch.id
|
|
163
|
+
WHERE te.volume_id = %s AND ch.chapter_number < %s
|
|
164
|
+
ORDER BY ch.chapter_number DESC
|
|
165
|
+
LIMIT 5
|
|
166
|
+
""", (vol_id, ch_num))
|
|
167
|
+
rows = cur.fetchall()
|
|
168
|
+
if rows:
|
|
169
|
+
for r in reversed(rows):
|
|
170
|
+
print(f"- ch-{r[0]:03d} [{r[1]}]: {r[2]}")
|
|
171
|
+
print()
|
|
172
|
+
|
|
173
|
+
# 7. 主角状态
|
|
174
|
+
print("## 主角状态")
|
|
175
|
+
cur.execute(f"""
|
|
176
|
+
SELECT level, progress_pct FROM {SCHEMA}.cultivation_curve
|
|
177
|
+
ORDER BY chapter_number DESC LIMIT 1
|
|
178
|
+
""")
|
|
179
|
+
cult = cur.fetchone()
|
|
180
|
+
if cult:
|
|
181
|
+
print(f"- 修炼: {cult[0]} ({cult[1]}%)")
|
|
182
|
+
|
|
183
|
+
cur.execute(f"""
|
|
184
|
+
SELECT skill_name FROM {SCHEMA}.skill_overview
|
|
185
|
+
WHERE status = 'active' ORDER BY acquired_chapter
|
|
186
|
+
""")
|
|
187
|
+
skills = cur.fetchall()
|
|
188
|
+
if skills:
|
|
189
|
+
names = '、'.join(r[0] for r in skills[:4])
|
|
190
|
+
extra = f" 等 (共{len(skills)}项)" if len(skills) > 4 else ""
|
|
191
|
+
print(f"- 活跃技能: {names}{extra}")
|
|
192
|
+
|
|
193
|
+
cur.execute(f"""
|
|
194
|
+
SELECT item_name, quantity FROM {SCHEMA}.current_inventory
|
|
195
|
+
ORDER BY item_type LIMIT 5
|
|
196
|
+
""")
|
|
197
|
+
items = cur.fetchall()
|
|
198
|
+
if items:
|
|
199
|
+
item_strs = [f"{r[0]}×{r[1]}" if r[1] > 1 else r[0] for r in items]
|
|
200
|
+
extra = " 等" if len(items) >= 5 else ""
|
|
201
|
+
print(f"- 关键道具: {'、'.join(item_strs)}{extra}")
|
|
202
|
+
print()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def context_expand(cur, global_ch):
|
|
206
|
+
"""生成 /expand 模式的上下文 — 更精确,只加载本章相关数据"""
|
|
207
|
+
info = get_chapter_info(cur, global_ch)
|
|
208
|
+
if not info:
|
|
209
|
+
print(f"## 错误: 章节 {global_ch} 不存在于数据库中")
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
ch_id, ch_num, time_in_story, ch_status, vol_id, vol_num, vol_title, phase_num, phase_title = info
|
|
213
|
+
|
|
214
|
+
print(f"# /expand 上下文 — 第{global_ch}章 (vol-{vol_num:03d} ch-{ch_num:03d})")
|
|
215
|
+
print(f"纪元{phase_num} [{phase_title}] · {vol_title}")
|
|
216
|
+
print(f"故事时间: {time_in_story or '未知'}")
|
|
217
|
+
print()
|
|
218
|
+
|
|
219
|
+
# 1. 本章关联的伏笔
|
|
220
|
+
print("## 本章伏笔")
|
|
221
|
+
cur.execute(f"""
|
|
222
|
+
SELECT f.code, f.description, f.status, cf.action_type,
|
|
223
|
+
planted_ch.chapter_number as planted_at
|
|
224
|
+
FROM {SCHEMA}.chapter_foreshadowing cf
|
|
225
|
+
JOIN {SCHEMA}.foreshadowing f ON cf.foreshadowing_id = f.id
|
|
226
|
+
LEFT JOIN {SCHEMA}.chapters planted_ch ON f.planted_chapter_id = planted_ch.id
|
|
227
|
+
WHERE cf.chapter_id = %s
|
|
228
|
+
ORDER BY cf.action_type
|
|
229
|
+
""", (ch_id,))
|
|
230
|
+
rows = cur.fetchall()
|
|
231
|
+
if rows:
|
|
232
|
+
for r in rows:
|
|
233
|
+
print(f"- [{r[0]}] {r[3]}: {r[1]} (埋设于ch-{r[4]:03d}, 状态:{r[2]})")
|
|
234
|
+
else:
|
|
235
|
+
# 回退:显示当前卷所有未解决伏笔
|
|
236
|
+
print("(本章无直接关联伏笔,显示当前卷未解决伏笔)")
|
|
237
|
+
cur.execute(f"""
|
|
238
|
+
SELECT f.code, f.description, f.status, ch.chapter_number
|
|
239
|
+
FROM {SCHEMA}.foreshadowing f
|
|
240
|
+
JOIN {SCHEMA}.chapters ch ON f.planted_chapter_id = ch.id
|
|
241
|
+
WHERE f.status IN ('planted', 'hinted', 'partially_resolved')
|
|
242
|
+
AND ch.volume_id = %s
|
|
243
|
+
ORDER BY ch.chapter_number
|
|
244
|
+
""", (vol_id,))
|
|
245
|
+
rows = cur.fetchall()
|
|
246
|
+
for r in rows:
|
|
247
|
+
print(f"- [{r[0]}] ch-{r[3]:03d} [{r[2]}]: {r[1]}")
|
|
248
|
+
print()
|
|
249
|
+
|
|
250
|
+
# 2. 当前卷活跃角色(精简版)
|
|
251
|
+
print("## 活跃角色状态")
|
|
252
|
+
cur.execute(f"""
|
|
253
|
+
SELECT c.name, c.role_type, cs.location, cs.state_summary, cs.last_appearance
|
|
254
|
+
FROM {SCHEMA}.characters c
|
|
255
|
+
JOIN {SCHEMA}.character_states cs ON c.id = cs.character_id
|
|
256
|
+
WHERE cs.volume_id = %s
|
|
257
|
+
ORDER BY cs.last_appearance DESC
|
|
258
|
+
""", (vol_id,))
|
|
259
|
+
rows = cur.fetchall()
|
|
260
|
+
if rows:
|
|
261
|
+
for r in rows:
|
|
262
|
+
print(f"- **{r[0]}** ({r[1]}): {r[2] or '?'} — {_truncate(r[3], 80)} [ch-{r[4]}]")
|
|
263
|
+
print()
|
|
264
|
+
|
|
265
|
+
# 3. 相关角色关系
|
|
266
|
+
print("## 角色关系")
|
|
267
|
+
cur.execute(f"""
|
|
268
|
+
SELECT ca.name, cb.name, r.relationship_type, r.current_status
|
|
269
|
+
FROM {SCHEMA}.relationships r
|
|
270
|
+
JOIN {SCHEMA}.characters ca ON r.character_a_id = ca.id
|
|
271
|
+
JOIN {SCHEMA}.characters cb ON r.character_b_id = cb.id
|
|
272
|
+
WHERE r.volume_id = %s
|
|
273
|
+
ORDER BY r.last_update_chapter DESC
|
|
274
|
+
""", (vol_id,))
|
|
275
|
+
rows = cur.fetchall()
|
|
276
|
+
if rows:
|
|
277
|
+
for r in rows:
|
|
278
|
+
print(f"- {r[0]} → {r[1]} [{r[2]}]: {_truncate(r[3], 60)}")
|
|
279
|
+
print()
|
|
280
|
+
|
|
281
|
+
# 4. 前2章时间线(衔接用)
|
|
282
|
+
print("## 前序衔接")
|
|
283
|
+
cur.execute(f"""
|
|
284
|
+
SELECT ch.chapter_number, te.story_time, te.event_description
|
|
285
|
+
FROM {SCHEMA}.timeline_events te
|
|
286
|
+
JOIN {SCHEMA}.chapters ch ON te.chapter_id = ch.id
|
|
287
|
+
WHERE te.volume_id = %s AND ch.chapter_number < %s
|
|
288
|
+
ORDER BY ch.chapter_number DESC
|
|
289
|
+
LIMIT 3
|
|
290
|
+
""", (vol_id, ch_num))
|
|
291
|
+
rows = cur.fetchall()
|
|
292
|
+
if rows:
|
|
293
|
+
for r in reversed(rows):
|
|
294
|
+
print(f"- ch-{r[0]:03d} [{r[1]}]: {r[2]}")
|
|
295
|
+
print()
|
|
296
|
+
|
|
297
|
+
# 5. 主角能力
|
|
298
|
+
print("## 主角能力")
|
|
299
|
+
cur.execute(f"""
|
|
300
|
+
SELECT level, progress_pct FROM {SCHEMA}.cultivation_curve
|
|
301
|
+
ORDER BY chapter_number DESC LIMIT 1
|
|
302
|
+
""")
|
|
303
|
+
cult = cur.fetchone()
|
|
304
|
+
if cult:
|
|
305
|
+
print(f"- 当前修为: {cult[0]} ({cult[1]}%)")
|
|
306
|
+
|
|
307
|
+
cur.execute(f"""
|
|
308
|
+
SELECT skill_name, skill_category, description FROM {SCHEMA}.skill_overview
|
|
309
|
+
WHERE status = 'active' ORDER BY acquired_chapter
|
|
310
|
+
""")
|
|
311
|
+
skills = cur.fetchall()
|
|
312
|
+
if skills:
|
|
313
|
+
print("- 可用技能:")
|
|
314
|
+
for s in skills:
|
|
315
|
+
print(f" - {s[0]} [{s[1]}]: {_truncate(s[2], 40)}")
|
|
316
|
+
|
|
317
|
+
cur.execute(f"""
|
|
318
|
+
SELECT item_name, quantity FROM {SCHEMA}.current_inventory
|
|
319
|
+
ORDER BY item_type
|
|
320
|
+
""")
|
|
321
|
+
items = cur.fetchall()
|
|
322
|
+
if items:
|
|
323
|
+
print("- 可用道具:")
|
|
324
|
+
for it in items:
|
|
325
|
+
qty = f" ×{it[1]}" if it[1] > 1 else ""
|
|
326
|
+
print(f" - {it[0]}{qty}")
|
|
327
|
+
print()
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def context_analyze(cur, global_ch):
|
|
331
|
+
"""生成 /analyze 模式的上下文 — 一致性校验数据"""
|
|
332
|
+
info = get_chapter_info(cur, global_ch)
|
|
333
|
+
if not info:
|
|
334
|
+
print(f"## 错误: 章节 {global_ch} 不存在于数据库中")
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
ch_id, ch_num, time_in_story, ch_status, vol_id, vol_num, vol_title, phase_num, phase_title = info
|
|
338
|
+
|
|
339
|
+
print(f"# /analyze 上下文 — 第{global_ch}章 (vol-{vol_num:03d} ch-{ch_num:03d})")
|
|
340
|
+
print()
|
|
341
|
+
|
|
342
|
+
# 1. 本章应有的伏笔
|
|
343
|
+
print("## 本章伏笔检查清单")
|
|
344
|
+
cur.execute(f"""
|
|
345
|
+
SELECT f.code, f.description, cf.action_type
|
|
346
|
+
FROM {SCHEMA}.chapter_foreshadowing cf
|
|
347
|
+
JOIN {SCHEMA}.foreshadowing f ON cf.foreshadowing_id = f.id
|
|
348
|
+
WHERE cf.chapter_id = %s
|
|
349
|
+
""", (ch_id,))
|
|
350
|
+
rows = cur.fetchall()
|
|
351
|
+
if rows:
|
|
352
|
+
for r in rows:
|
|
353
|
+
print(f"- [{r[0]}] 应{r[2]}: {r[1]}")
|
|
354
|
+
else:
|
|
355
|
+
print("无直接关联伏笔")
|
|
356
|
+
print()
|
|
357
|
+
|
|
358
|
+
# 2. 角色状态(用于一致性对比)
|
|
359
|
+
print("## 角色状态基准")
|
|
360
|
+
cur.execute(f"""
|
|
361
|
+
SELECT c.name, cs.location, cs.state_summary, cs.last_appearance
|
|
362
|
+
FROM {SCHEMA}.characters c
|
|
363
|
+
JOIN {SCHEMA}.character_states cs ON c.id = cs.character_id
|
|
364
|
+
WHERE cs.volume_id = %s
|
|
365
|
+
ORDER BY c.name
|
|
366
|
+
""", (vol_id,))
|
|
367
|
+
rows = cur.fetchall()
|
|
368
|
+
for r in rows:
|
|
369
|
+
print(f"- {r[0]}: {r[1]} — {_truncate(r[2], 60)} [最后ch-{r[3]}]")
|
|
370
|
+
print()
|
|
371
|
+
|
|
372
|
+
# 3. 时间线连续性
|
|
373
|
+
print("## 时间线连续性")
|
|
374
|
+
cur.execute(f"""
|
|
375
|
+
SELECT ch.chapter_number, te.story_time, te.event_description
|
|
376
|
+
FROM {SCHEMA}.timeline_events te
|
|
377
|
+
JOIN {SCHEMA}.chapters ch ON te.chapter_id = ch.id
|
|
378
|
+
WHERE te.volume_id = %s AND ch.chapter_number BETWEEN %s AND %s
|
|
379
|
+
ORDER BY ch.chapter_number
|
|
380
|
+
""", (vol_id, max(1, ch_num - 2), ch_num + 1))
|
|
381
|
+
rows = cur.fetchall()
|
|
382
|
+
for r in rows:
|
|
383
|
+
marker = " <<<" if r[0] == ch_num else ""
|
|
384
|
+
print(f"- ch-{r[0]:03d} [{r[1]}]: {r[2]}{marker}")
|
|
385
|
+
print()
|
|
386
|
+
|
|
387
|
+
# 4. 全局一致性警告
|
|
388
|
+
print("## 一致性警告")
|
|
389
|
+
# 检测遗忘伏笔(埋设超过50章未解决)
|
|
390
|
+
cur.execute(f"""
|
|
391
|
+
SELECT f.code, f.description, ch.chapter_number as planted_at
|
|
392
|
+
FROM {SCHEMA}.foreshadowing f
|
|
393
|
+
JOIN {SCHEMA}.chapters ch ON f.planted_chapter_id = ch.id
|
|
394
|
+
WHERE f.status = 'planted' AND ch.chapter_number < %s - 50
|
|
395
|
+
ORDER BY ch.chapter_number
|
|
396
|
+
""", (ch_num,))
|
|
397
|
+
rows = cur.fetchall()
|
|
398
|
+
if rows:
|
|
399
|
+
print("### 可能遗忘的伏笔(埋设超过50章):")
|
|
400
|
+
for r in rows:
|
|
401
|
+
print(f"- [{r[0]}] 埋设于ch-{r[2]:03d}: {r[1]}")
|
|
402
|
+
else:
|
|
403
|
+
print("无一致性警告")
|
|
404
|
+
print()
|
|
405
|
+
|
|
406
|
+
# 5. 主角能力基准
|
|
407
|
+
print("## 主角能力基准")
|
|
408
|
+
cur.execute(f"""
|
|
409
|
+
SELECT level FROM {SCHEMA}.cultivation_curve
|
|
410
|
+
ORDER BY chapter_number DESC LIMIT 1
|
|
411
|
+
""")
|
|
412
|
+
cult = cur.fetchone()
|
|
413
|
+
if cult:
|
|
414
|
+
print(f"- 修炼等级: {cult[0]}")
|
|
415
|
+
|
|
416
|
+
cur.execute(f"""
|
|
417
|
+
SELECT skill_name, acquired_chapter FROM {SCHEMA}.skill_overview
|
|
418
|
+
WHERE status = 'active' ORDER BY acquired_chapter
|
|
419
|
+
""")
|
|
420
|
+
skills = cur.fetchall()
|
|
421
|
+
if skills:
|
|
422
|
+
print("- 已习得技能:")
|
|
423
|
+
for s in skills:
|
|
424
|
+
print(f" - {s[0]} (ch-{s[1]:03d})")
|
|
425
|
+
|
|
426
|
+
cur.execute(f"""
|
|
427
|
+
SELECT item_name, quantity FROM {SCHEMA}.current_inventory
|
|
428
|
+
ORDER BY item_type
|
|
429
|
+
""")
|
|
430
|
+
items = cur.fetchall()
|
|
431
|
+
if items:
|
|
432
|
+
print("- 持有道具:")
|
|
433
|
+
for it in items:
|
|
434
|
+
qty = f" ×{it[1]}" if it[1] > 1 else ""
|
|
435
|
+
print(f" - {it[0]}{qty}")
|
|
436
|
+
print()
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def dashboard(cur):
|
|
440
|
+
"""输出创作进度仪表盘"""
|
|
441
|
+
print("# 创作进度仪表盘")
|
|
442
|
+
print()
|
|
443
|
+
|
|
444
|
+
cur.execute(f"SELECT * FROM {SCHEMA}.writing_dashboard")
|
|
445
|
+
rows = cur.fetchall()
|
|
446
|
+
|
|
447
|
+
print("## 纪元进度")
|
|
448
|
+
print("| 纪 | 标题 | 总卷 | 完成卷 | 总章 | 终稿章 | 概要章 | 总字数 |")
|
|
449
|
+
print("|----|------|------|--------|------|--------|--------|--------|")
|
|
450
|
+
for r in rows:
|
|
451
|
+
words = f"{r[7]:,}" if r[7] else "0"
|
|
452
|
+
print(f"| {r[0]} | {r[1]} | {r[2]} | {r[3]} | {r[4]} | {r[5]} | {r[6]} | {words} |")
|
|
453
|
+
print()
|
|
454
|
+
|
|
455
|
+
# 伏笔统计
|
|
456
|
+
print("## 伏笔统计")
|
|
457
|
+
cur.execute(f"""
|
|
458
|
+
SELECT status, COUNT(*) FROM {SCHEMA}.foreshadowing GROUP BY status ORDER BY status
|
|
459
|
+
""")
|
|
460
|
+
rows = cur.fetchall()
|
|
461
|
+
for r in rows:
|
|
462
|
+
print(f"- {r[0]}: {r[1]} 条")
|
|
463
|
+
print()
|
|
464
|
+
|
|
465
|
+
# 角色统计
|
|
466
|
+
print("## 角色统计")
|
|
467
|
+
cur.execute(f"""
|
|
468
|
+
SELECT status, COUNT(*) FROM {SCHEMA}.characters GROUP BY status ORDER BY status
|
|
469
|
+
""")
|
|
470
|
+
rows = cur.fetchall()
|
|
471
|
+
for r in rows:
|
|
472
|
+
print(f"- {r[0]}: {r[1]} 个")
|
|
473
|
+
print()
|
|
474
|
+
|
|
475
|
+
# 情节线统计
|
|
476
|
+
print("## 情节线统计")
|
|
477
|
+
cur.execute(f"""
|
|
478
|
+
SELECT status, COUNT(*) FROM {SCHEMA}.plot_threads GROUP BY status ORDER BY status
|
|
479
|
+
""")
|
|
480
|
+
rows = cur.fetchall()
|
|
481
|
+
for r in rows:
|
|
482
|
+
print(f"- {r[0]}: {r[1]} 条")
|
|
483
|
+
print()
|
|
484
|
+
|
|
485
|
+
# 遗忘伏笔检测
|
|
486
|
+
print("## 遗忘伏笔检测")
|
|
487
|
+
cur.execute(f"""
|
|
488
|
+
SELECT f.code, f.description, ch.chapter_number, v.vol_number
|
|
489
|
+
FROM {SCHEMA}.foreshadowing f
|
|
490
|
+
JOIN {SCHEMA}.chapters ch ON f.planted_chapter_id = ch.id
|
|
491
|
+
JOIN {SCHEMA}.volumes v ON ch.volume_id = v.id
|
|
492
|
+
WHERE f.status = 'planted'
|
|
493
|
+
ORDER BY ch.chapter_number
|
|
494
|
+
""")
|
|
495
|
+
rows = cur.fetchall()
|
|
496
|
+
# 获取最大章节号作为当前进度
|
|
497
|
+
cur.execute(f"SELECT MAX(chapter_number) FROM {SCHEMA}.chapters WHERE synopsis_summary IS NOT NULL")
|
|
498
|
+
max_ch = cur.fetchone()[0] or 0
|
|
499
|
+
|
|
500
|
+
warn_count = 0
|
|
501
|
+
crit_count = 0
|
|
502
|
+
if rows:
|
|
503
|
+
for r in rows:
|
|
504
|
+
age = max_ch - r[2]
|
|
505
|
+
if age > 50:
|
|
506
|
+
crit_count += 1
|
|
507
|
+
print(f"- ❌ [{r[0]}] vol-{r[3]:03d} ch-{r[2]:03d} (已过{age}章): {_truncate(r[1], 60)}")
|
|
508
|
+
elif age > 30:
|
|
509
|
+
warn_count += 1
|
|
510
|
+
print(f"- ⚠️ [{r[0]}] vol-{r[3]:03d} ch-{r[2]:03d} (已过{age}章): {_truncate(r[1], 60)}")
|
|
511
|
+
if warn_count == 0 and crit_count == 0:
|
|
512
|
+
print("✅ 无超期伏笔")
|
|
513
|
+
else:
|
|
514
|
+
print(f"\n合计: ⚠️ 待推进 {warn_count} 条, ❌ 严重遗忘 {crit_count} 条")
|
|
515
|
+
else:
|
|
516
|
+
print("无 planted 伏笔")
|
|
517
|
+
print()
|
|
518
|
+
|
|
519
|
+
# 角色活跃度
|
|
520
|
+
print("## 角色活跃度")
|
|
521
|
+
cur.execute(f"""
|
|
522
|
+
SELECT DISTINCT ON (c.name)
|
|
523
|
+
c.name, c.role_type, cs.last_appearance, v.vol_number, c.status
|
|
524
|
+
FROM {SCHEMA}.characters c
|
|
525
|
+
JOIN {SCHEMA}.character_states cs ON c.id = cs.character_id
|
|
526
|
+
JOIN {SCHEMA}.volumes v ON cs.volume_id = v.id
|
|
527
|
+
ORDER BY c.name, v.vol_number DESC, cs.last_appearance DESC
|
|
528
|
+
""")
|
|
529
|
+
rows = cur.fetchall()
|
|
530
|
+
if rows:
|
|
531
|
+
print("| 角色 | 类型 | 最后出场 | 状态 |")
|
|
532
|
+
print("|------|------|---------|------|")
|
|
533
|
+
for r in sorted(rows, key=lambda x: x[2] or 0, reverse=True):
|
|
534
|
+
print(f"| {r[0]} | {r[1]} | vol-{r[3]:03d} ch-{r[2] or '?'} | {r[4]} |")
|
|
535
|
+
else:
|
|
536
|
+
print("(无角色数据)")
|
|
537
|
+
print()
|
|
538
|
+
|
|
539
|
+
# 主角成长
|
|
540
|
+
print("## 主角成长")
|
|
541
|
+
cur.execute(f"""
|
|
542
|
+
SELECT COUNT(*) FILTER (WHERE status = 'active'),
|
|
543
|
+
COUNT(*) FILTER (WHERE status = 'sealed'),
|
|
544
|
+
COUNT(*)
|
|
545
|
+
FROM {SCHEMA}.protagonist_skills
|
|
546
|
+
""")
|
|
547
|
+
sk = cur.fetchone()
|
|
548
|
+
if sk:
|
|
549
|
+
print(f"- 技能: {sk[2]} 项 (活跃 {sk[0]} / 封印 {sk[1]})")
|
|
550
|
+
|
|
551
|
+
cur.execute(f"""
|
|
552
|
+
SELECT level, progress_pct FROM {SCHEMA}.cultivation_curve
|
|
553
|
+
ORDER BY chapter_number DESC LIMIT 1
|
|
554
|
+
""")
|
|
555
|
+
cult = cur.fetchone()
|
|
556
|
+
if cult:
|
|
557
|
+
print(f"- 修炼: {cult[0]} ({cult[1]}%)")
|
|
558
|
+
|
|
559
|
+
cur.execute(f"""
|
|
560
|
+
SELECT COUNT(*) FROM {SCHEMA}.current_inventory
|
|
561
|
+
""")
|
|
562
|
+
inv = cur.fetchone()
|
|
563
|
+
if inv:
|
|
564
|
+
print(f"- 道具: {inv[0]} 件持有")
|
|
565
|
+
print()
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _truncate(text, max_len=80):
|
|
569
|
+
"""截断文本"""
|
|
570
|
+
if not text:
|
|
571
|
+
return "-"
|
|
572
|
+
text = text.replace("\n", " ")
|
|
573
|
+
if len(text) > max_len:
|
|
574
|
+
return text[:max_len] + "..."
|
|
575
|
+
return text
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def main():
|
|
579
|
+
parser = argparse.ArgumentParser(description="DB -> 精确上下文生成器")
|
|
580
|
+
parser.add_argument("--chapter", type=int, help="目标章节号(全局)")
|
|
581
|
+
parser.add_argument("--mode", choices=["write", "expand", "analyze"], default="write",
|
|
582
|
+
help="上下文模式: write/expand/analyze")
|
|
583
|
+
parser.add_argument("--dashboard", action="store_true", help="输出创作进度仪表盘")
|
|
584
|
+
args = parser.parse_args()
|
|
585
|
+
|
|
586
|
+
if not args.chapter and not args.dashboard:
|
|
587
|
+
print("用法: python db_context.py --chapter 42 --mode write")
|
|
588
|
+
print(" python db_context.py --dashboard")
|
|
589
|
+
sys.exit(1)
|
|
590
|
+
|
|
591
|
+
conn = get_conn()
|
|
592
|
+
cur = conn.cursor()
|
|
593
|
+
|
|
594
|
+
try:
|
|
595
|
+
if args.dashboard:
|
|
596
|
+
dashboard(cur)
|
|
597
|
+
elif args.mode == "write":
|
|
598
|
+
context_write(cur, args.chapter)
|
|
599
|
+
elif args.mode == "expand":
|
|
600
|
+
context_expand(cur, args.chapter)
|
|
601
|
+
elif args.mode == "analyze":
|
|
602
|
+
context_analyze(cur, args.chapter)
|
|
603
|
+
finally:
|
|
604
|
+
cur.close()
|
|
605
|
+
conn.close()
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
if __name__ == "__main__":
|
|
609
|
+
main()
|