novelws 5.2.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +51 -0
  3. package/dist/cli.js +3 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/dashboard.d.ts +3 -0
  6. package/dist/commands/dashboard.d.ts.map +1 -0
  7. package/dist/commands/dashboard.js +42 -0
  8. package/dist/commands/dashboard.js.map +1 -0
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/init.js +19 -0
  11. package/dist/commands/init.js.map +1 -1
  12. package/dist/commands/upgrade.d.ts.map +1 -1
  13. package/dist/commands/upgrade.js +23 -0
  14. package/dist/commands/upgrade.js.map +1 -1
  15. package/dist/core/config.d.ts +3 -0
  16. package/dist/core/config.d.ts.map +1 -1
  17. package/dist/core/config.js +4 -0
  18. package/dist/core/config.js.map +1 -1
  19. package/dist/server/datasource/db.d.ts +38 -0
  20. package/dist/server/datasource/db.d.ts.map +1 -0
  21. package/dist/server/datasource/db.js +323 -0
  22. package/dist/server/datasource/db.js.map +1 -0
  23. package/dist/server/datasource/fs.d.ts +30 -0
  24. package/dist/server/datasource/fs.d.ts.map +1 -0
  25. package/dist/server/datasource/fs.js +308 -0
  26. package/dist/server/datasource/fs.js.map +1 -0
  27. package/dist/server/datasource/index.d.ts +11 -0
  28. package/dist/server/datasource/index.d.ts.map +1 -0
  29. package/dist/server/datasource/index.js +33 -0
  30. package/dist/server/datasource/index.js.map +1 -0
  31. package/dist/server/index.d.ts +12 -0
  32. package/dist/server/index.d.ts.map +1 -0
  33. package/dist/server/index.js +69 -0
  34. package/dist/server/index.js.map +1 -0
  35. package/dist/server/routes/characters.d.ts +4 -0
  36. package/dist/server/routes/characters.d.ts.map +1 -0
  37. package/dist/server/routes/characters.js +27 -0
  38. package/dist/server/routes/characters.js.map +1 -0
  39. package/dist/server/routes/plots.d.ts +4 -0
  40. package/dist/server/routes/plots.d.ts.map +1 -0
  41. package/dist/server/routes/plots.js +36 -0
  42. package/dist/server/routes/plots.js.map +1 -0
  43. package/dist/server/routes/protagonist.d.ts +4 -0
  44. package/dist/server/routes/protagonist.d.ts.map +1 -0
  45. package/dist/server/routes/protagonist.js +46 -0
  46. package/dist/server/routes/protagonist.js.map +1 -0
  47. package/dist/server/routes/relationships.d.ts +4 -0
  48. package/dist/server/routes/relationships.d.ts.map +1 -0
  49. package/dist/server/routes/relationships.js +27 -0
  50. package/dist/server/routes/relationships.js.map +1 -0
  51. package/dist/server/routes/stats.d.ts +4 -0
  52. package/dist/server/routes/stats.d.ts.map +1 -0
  53. package/dist/server/routes/stats.js +21 -0
  54. package/dist/server/routes/stats.js.map +1 -0
  55. package/dist/server/routes/stories.d.ts +4 -0
  56. package/dist/server/routes/stories.d.ts.map +1 -0
  57. package/dist/server/routes/stories.js +80 -0
  58. package/dist/server/routes/stories.js.map +1 -0
  59. package/dist/server/routes/timeline.d.ts +4 -0
  60. package/dist/server/routes/timeline.d.ts.map +1 -0
  61. package/dist/server/routes/timeline.js +17 -0
  62. package/dist/server/routes/timeline.js.map +1 -0
  63. package/dist/server/types.d.ts +199 -0
  64. package/dist/server/types.d.ts.map +1 -0
  65. package/dist/server/types.js +2 -0
  66. package/dist/server/types.js.map +1 -0
  67. package/package.json +13 -3
  68. package/templates/commands/analyze.md +9 -1
  69. package/templates/commands/expand.md +19 -3
  70. package/templates/commands/write.md +23 -7
  71. package/templates/dot-claude/CLAUDE.md +27 -0
  72. package/templates/scripts/db_context.py +609 -0
  73. package/templates/scripts/db_init_protagonist.py +343 -0
  74. package/templates/scripts/db_sync.py +611 -0
  75. package/templates/scripts/db_volume_switch.py +278 -0
  76. package/templates/scripts/phase_a_init_db.py +428 -0
  77. package/templates/scripts/requirements.txt +1 -0
  78. package/templates/tracking/character-state.json +1 -3
  79. package/templates/tracking/plot-tracker.json +1 -5
  80. package/templates/tracking/relationships.json +1 -3
  81. package/templates/tracking/timeline.json +1 -3
  82. package/templates/volume-outline.md +31 -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()