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,343 @@
1
+ #!/usr/bin/env python3
2
+ """db_init_protagonist.py — 主角数据表初始化 + 首次导入
3
+
4
+ 用法:
5
+ python scripts/db_init_protagonist.py # 建表 + 导入 vol-001~003
6
+ python scripts/db_init_protagonist.py --ddl # 仅建表
7
+ """
8
+
9
+ import argparse
10
+ import json
11
+ import os
12
+ import sys
13
+ import psycopg2
14
+
15
+ sys.stdout.reconfigure(encoding='utf-8')
16
+
17
+ BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
18
+ SCHEMA = "novelws"
19
+
20
+
21
+ def load_config():
22
+ config_path = os.path.join(BASE, "resources", "config.json")
23
+ with open(config_path, "r", encoding="utf-8") as f:
24
+ return json.load(f)
25
+
26
+
27
+ def get_db_config(config):
28
+ db = config.get("database", {})
29
+ return {
30
+ "host": db.get("host", "127.0.0.1"),
31
+ "port": db.get("port", 5432),
32
+ "dbname": db.get("dbname", "postgres"),
33
+ "user": db.get("user", "postgres"),
34
+ "password": db.get("password", ""),
35
+ }
36
+
37
+
38
+ DDL = f"""
39
+ -- 主角技能主表
40
+ CREATE TABLE IF NOT EXISTS {SCHEMA}.protagonist_skills (
41
+ id SERIAL PRIMARY KEY,
42
+ skill_name VARCHAR(100) NOT NULL UNIQUE,
43
+ skill_category VARCHAR(20) NOT NULL,
44
+ skill_level VARCHAR(50),
45
+ parent_skill_id INT REFERENCES {SCHEMA}.protagonist_skills(id),
46
+ description TEXT,
47
+ acquired_chapter INT NOT NULL,
48
+ acquired_method VARCHAR(200),
49
+ last_used_chapter INT,
50
+ use_count INT DEFAULT 0,
51
+ status VARCHAR(20) DEFAULT 'active'
52
+ );
53
+
54
+ -- 技能时序表
55
+ CREATE TABLE IF NOT EXISTS {SCHEMA}.protagonist_skill_events (
56
+ id SERIAL PRIMARY KEY,
57
+ skill_id INT NOT NULL REFERENCES {SCHEMA}.protagonist_skills(id),
58
+ chapter_number INT NOT NULL,
59
+ event_type VARCHAR(30) NOT NULL,
60
+ detail TEXT,
61
+ volume_id INT NOT NULL REFERENCES {SCHEMA}.volumes(id)
62
+ );
63
+
64
+ -- 装备/道具主表
65
+ CREATE TABLE IF NOT EXISTS {SCHEMA}.protagonist_inventory (
66
+ id SERIAL PRIMARY KEY,
67
+ item_name VARCHAR(100) NOT NULL,
68
+ item_type VARCHAR(30) NOT NULL,
69
+ quantity INT DEFAULT 1,
70
+ quality VARCHAR(30),
71
+ description TEXT,
72
+ acquired_chapter INT NOT NULL,
73
+ acquired_method VARCHAR(200),
74
+ status VARCHAR(20) DEFAULT 'held',
75
+ UNIQUE(item_name, acquired_chapter)
76
+ );
77
+
78
+ -- 道具时序表
79
+ CREATE TABLE IF NOT EXISTS {SCHEMA}.protagonist_inventory_events (
80
+ id SERIAL PRIMARY KEY,
81
+ item_id INT NOT NULL REFERENCES {SCHEMA}.protagonist_inventory(id),
82
+ chapter_number INT NOT NULL,
83
+ event_type VARCHAR(30) NOT NULL,
84
+ quantity_change INT DEFAULT 0,
85
+ detail TEXT,
86
+ volume_id INT NOT NULL REFERENCES {SCHEMA}.volumes(id)
87
+ );
88
+
89
+ -- 修炼进度表
90
+ CREATE TABLE IF NOT EXISTS {SCHEMA}.protagonist_cultivation (
91
+ id SERIAL PRIMARY KEY,
92
+ chapter_number INT NOT NULL,
93
+ level VARCHAR(50) NOT NULL,
94
+ progress_pct DECIMAL(5,1),
95
+ breakthrough_type VARCHAR(20),
96
+ trigger VARCHAR(200),
97
+ detail TEXT,
98
+ volume_id INT NOT NULL REFERENCES {SCHEMA}.volumes(id)
99
+ );
100
+
101
+ -- 索引
102
+ CREATE INDEX IF NOT EXISTS idx_skills_category ON {SCHEMA}.protagonist_skills(skill_category);
103
+ CREATE INDEX IF NOT EXISTS idx_skills_status ON {SCHEMA}.protagonist_skills(status);
104
+ CREATE INDEX IF NOT EXISTS idx_skill_events_skill ON {SCHEMA}.protagonist_skill_events(skill_id);
105
+ CREATE INDEX IF NOT EXISTS idx_skill_events_vol ON {SCHEMA}.protagonist_skill_events(volume_id);
106
+ CREATE INDEX IF NOT EXISTS idx_skill_events_type ON {SCHEMA}.protagonist_skill_events(event_type);
107
+ CREATE INDEX IF NOT EXISTS idx_inventory_type ON {SCHEMA}.protagonist_inventory(item_type);
108
+ CREATE INDEX IF NOT EXISTS idx_inventory_status ON {SCHEMA}.protagonist_inventory(status);
109
+ CREATE INDEX IF NOT EXISTS idx_inv_events_item ON {SCHEMA}.protagonist_inventory_events(item_id);
110
+ CREATE INDEX IF NOT EXISTS idx_inv_events_vol ON {SCHEMA}.protagonist_inventory_events(volume_id);
111
+ CREATE INDEX IF NOT EXISTS idx_cultivation_vol ON {SCHEMA}.protagonist_cultivation(volume_id);
112
+ CREATE INDEX IF NOT EXISTS idx_cultivation_ch ON {SCHEMA}.protagonist_cultivation(chapter_number);
113
+
114
+ -- 视图
115
+ CREATE OR REPLACE VIEW {SCHEMA}.current_inventory AS
116
+ SELECT item_name, item_type, quantity, quality, description, acquired_chapter
117
+ FROM {SCHEMA}.protagonist_inventory
118
+ WHERE status = 'held'
119
+ ORDER BY item_type, item_name;
120
+
121
+ CREATE OR REPLACE VIEW {SCHEMA}.skill_overview AS
122
+ SELECT s.skill_name, s.skill_category, s.skill_level, s.status,
123
+ s.acquired_chapter, s.use_count,
124
+ COUNT(e.id) AS total_events
125
+ FROM {SCHEMA}.protagonist_skills s
126
+ LEFT JOIN {SCHEMA}.protagonist_skill_events e ON s.id = e.skill_id
127
+ GROUP BY s.id
128
+ ORDER BY s.skill_category, s.acquired_chapter;
129
+
130
+ CREATE OR REPLACE VIEW {SCHEMA}.cultivation_curve AS
131
+ SELECT chapter_number, level, progress_pct, breakthrough_type, trigger
132
+ FROM {SCHEMA}.protagonist_cultivation
133
+ ORDER BY chapter_number;
134
+ """
135
+
136
+ # ── 技能数据(vol-001~003) ──
137
+ # (skill_name, skill_category, skill_level, parent_skill_id, description,
138
+ # acquired_chapter, acquired_method, status)
139
+ SKILLS_DATA = [
140
+ # vol-001
141
+ ("账本脑", "账本脑", "入门", None, "数据化思维,信息分类归档", 1, "铜盘觉醒", "active"),
142
+ ("分解功能", "账本脑", "入门", None, "铜盘分解灵材", 40, "铜盘解锁", "active"),
143
+ ("灵气感知", "被动", "入门", None, "灵气初步感知能力", 49, "自悟", "active"),
144
+ ("定身符", "符", "入门", None, "3秒静止效果", 68, "修炼突破", "active"),
145
+ # vol-002
146
+ ("P0轮回仓应急", "账本脑", "入门", None, "72小时冷却,副本内有效", 108, "铜盘解锁", "active"),
147
+ ("灵气内敛", "被动", "入门", None, "压制灵气外泄,5分钟持续", 129, "修炼突破", "active"),
148
+ ("伪装术", "被动", "固化", None, "禁忌乡伪装术固化为永久技能", 139, "副本掉落", "active"),
149
+ ("假身符", "符", "入门", None, "8秒维持时间", 178, "修炼突破", "active"),
150
+ # vol-003
151
+ ("镜面系统", "阵", "入门", None, "镜面反射间接观察,规避视线类铁律", 215, "副本自创", "active"),
152
+ ("Build雏形", "符", "入门", None, "陷阱链+符毒联动+镜面系统+假身符四系统联动", 240, "副本设计", "active"),
153
+ ("镜杀阵", "阵", "入门", None, "用镜片反射诅咒实体目光回自身,以敌人规则反杀", 262, "副本实战", "active"),
154
+ ]
155
+
156
+ # ── 修炼进度数据 ──
157
+ # (chapter_number, level, progress_pct, breakthrough_type, trigger, detail, vol_number)
158
+ CULTIVATION_DATA = [
159
+ # vol-001
160
+ (1, "段0", 0.0, "major", "铜盘觉醒", "铜盘激活,灵气初灌", 1),
161
+ (49, "段0", 80.0, None, "修炼", "首次主动感知灵气", 1),
162
+ (68, "段1·炼气前期", 0.0, "major", "修炼突破", "突破段1,画出第一张有共振的符纸", 1),
163
+ # vol-002
164
+ (154, "段1·炼气前期", 25.0, None, "修炼", "段1修炼进度达25%", 2),
165
+ (182, "段1·炼气前期", 30.0, None, "画符修炼", "画符训练效率是纯打坐的三倍", 2),
166
+ # vol-003
167
+ (272, "段1·炼气前期", 35.0, None, "副本战斗", "超标副本高压环境促进修为", 3),
168
+ (300, "段1·炼气前期", 37.8, None, "修炼", "卷3完结状态", 3),
169
+ ]
170
+
171
+ # ── 道具数据 ──
172
+ # (item_name, item_type, quantity, quality, description,
173
+ # acquired_chapter, acquired_method, status)
174
+ INVENTORY_DATA = [
175
+ # vol-001
176
+ ("短刃", "装备", 1, "普通", "来自前探索者补给点", 30, "拾取", "held"),
177
+ ("铁丝绊索", "工具", 1, "普通", "陷阱材料", 30, "拾取", "held"),
178
+ ("迷香", "消耗品", 1, "普通", "来自前探索者补给点", 30, "拾取", "consumed"),
179
+ ("兽血", "材料", 1, "普通", "来自前探索者补给点", 30, "拾取", "consumed"),
180
+ ("灵气结晶", "材料", 3, "普通", "蜂巢副本掉落", 40, "副本掉落", "consumed"),
181
+ ("定身符纸成品", "消耗品", 6, "普通", "3秒静止效果", 68, "制作", "held"),
182
+ # vol-002
183
+ ("假身符纸成品", "消耗品", 2, "普通", "假身符成品(卷2末剩余)", 181, "制作", "held"),
184
+ ("阴气结晶", "材料", 1, "稀有", "禁忌乡副本掉落", 139, "副本掉落", "held"),
185
+ ("灵魂印记碎片", "材料", 1, "稀有", "用途待解锁", 139, "副本掉落", "held"),
186
+ ("灵墨", "材料", 1, "普通", "画符用灵墨", 165, "交易", "held"),
187
+ ("空白符纸", "材料", 10, "普通", "画符用空白符纸", 68, "交易", "held"),
188
+ ("灵气压制符", "消耗品", 1, "精良", "压制灵气波动约六小时", 191, "交易", "consumed"),
189
+ # vol-003
190
+ ("镜面符材", "材料", 5, "稀有", "可制作反射观察符,规避视线类铁律", 268, "副本掉落", "held"),
191
+ ("诅咒封存体(林安)", "材料", 1, "古纪级", "林安灵魂封存在轮回盘中,用途待解锁", 268, "副本掉落", "held"),
192
+ ("灵魂印记碎片", "材料", 3, "稀有", "副本掉落累计(卷3新增)", 268, "副本掉落", "held"),
193
+ ]
194
+
195
+
196
+ def get_vol_id(cur, vol_number):
197
+ cur.execute(f"SELECT id FROM {SCHEMA}.volumes WHERE vol_number = %s", (vol_number,))
198
+ row = cur.fetchone()
199
+ if not row:
200
+ raise ValueError(f"卷 {vol_number} 不存在于数据库中")
201
+ return row[0]
202
+
203
+
204
+ def import_skills(cur):
205
+ count = 0
206
+ for s in SKILLS_DATA:
207
+ name, cat, level, parent, desc, ch, method, status = s
208
+ cur.execute(f"""
209
+ INSERT INTO {SCHEMA}.protagonist_skills
210
+ (skill_name, skill_category, skill_level, parent_skill_id,
211
+ description, acquired_chapter, acquired_method, status)
212
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
213
+ ON CONFLICT (skill_name) DO UPDATE SET
214
+ skill_level = EXCLUDED.skill_level,
215
+ status = EXCLUDED.status
216
+ RETURNING id
217
+ """, s)
218
+ skill_id = cur.fetchone()[0]
219
+
220
+ vol_num = 1 if ch <= 100 else (2 if ch <= 200 else 3)
221
+ vol_id = get_vol_id(cur, vol_num)
222
+ cur.execute(f"""
223
+ INSERT INTO {SCHEMA}.protagonist_skill_events
224
+ (skill_id, chapter_number, event_type, detail, volume_id)
225
+ VALUES (%s, %s, 'acquired', %s, %s)
226
+ """, (skill_id, ch, f"获得技能:{name}", vol_id))
227
+ count += 1
228
+ return count
229
+
230
+
231
+ def import_cultivation(cur):
232
+ count = 0
233
+ for c in CULTIVATION_DATA:
234
+ ch, level, pct, bt, trigger, detail, vol_num = c
235
+ vol_id = get_vol_id(cur, vol_num)
236
+ cur.execute(f"""
237
+ INSERT INTO {SCHEMA}.protagonist_cultivation
238
+ (chapter_number, level, progress_pct, breakthrough_type,
239
+ trigger, detail, volume_id)
240
+ VALUES (%s, %s, %s, %s, %s, %s, %s)
241
+ """, (ch, level, pct, bt, trigger, detail, vol_id))
242
+ count += 1
243
+ return count
244
+
245
+
246
+ def import_inventory(cur):
247
+ count = 0
248
+ for item in INVENTORY_DATA:
249
+ name, itype, qty, quality, desc, ch, method, status = item
250
+ vol_num = 1 if ch <= 100 else (2 if ch <= 200 else 3)
251
+ vol_id = get_vol_id(cur, vol_num)
252
+ cur.execute(f"""
253
+ INSERT INTO {SCHEMA}.protagonist_inventory
254
+ (item_name, item_type, quantity, quality, description,
255
+ acquired_chapter, acquired_method, status)
256
+ VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
257
+ ON CONFLICT (item_name, acquired_chapter) DO UPDATE SET
258
+ quantity = EXCLUDED.quantity,
259
+ status = EXCLUDED.status
260
+ RETURNING id
261
+ """, item)
262
+ item_id = cur.fetchone()[0]
263
+
264
+ cur.execute(f"""
265
+ INSERT INTO {SCHEMA}.protagonist_inventory_events
266
+ (item_id, chapter_number, event_type, quantity_change, detail, volume_id)
267
+ VALUES (%s, %s, 'acquired', %s, %s, %s)
268
+ """, (item_id, ch, qty, f"获得:{name}", vol_id))
269
+ count += 1
270
+ return count
271
+
272
+
273
+ def run():
274
+ parser = argparse.ArgumentParser(description="主角数据表初始化")
275
+ parser.add_argument("--ddl", action="store_true", help="仅建表,不导入数据")
276
+ args = parser.parse_args()
277
+
278
+ config = load_config()
279
+ db_config = get_db_config(config)
280
+ conn = psycopg2.connect(**db_config)
281
+ conn.autocommit = False
282
+ cur = conn.cursor()
283
+
284
+ try:
285
+ print("[1/4] 创建主角数据表...")
286
+ cur.execute(DDL)
287
+ conn.commit()
288
+ print(" 5张表 + 索引 + 视图创建完成")
289
+
290
+ if args.ddl:
291
+ print("\nDDL 完成(仅建表模式)")
292
+ return
293
+
294
+ # 清空旧数据(幂等)
295
+ print("[1.5] 清空旧数据(幂等重导)...")
296
+ cur.execute(f"DELETE FROM {SCHEMA}.protagonist_skill_events")
297
+ cur.execute(f"DELETE FROM {SCHEMA}.protagonist_inventory_events")
298
+ cur.execute(f"DELETE FROM {SCHEMA}.protagonist_skills")
299
+ cur.execute(f"DELETE FROM {SCHEMA}.protagonist_inventory")
300
+ cur.execute(f"DELETE FROM {SCHEMA}.protagonist_cultivation")
301
+ conn.commit()
302
+
303
+ print("[2/4] 导入技能数据...")
304
+ n = import_skills(cur)
305
+ conn.commit()
306
+ print(f" {n} 条技能导入完成")
307
+
308
+ print("[3/4] 导入修炼进度...")
309
+ n = import_cultivation(cur)
310
+ conn.commit()
311
+ print(f" {n} 条修炼记录导入完成")
312
+
313
+ print("[4/4] 导入道具数据...")
314
+ n = import_inventory(cur)
315
+ conn.commit()
316
+ print(f" {n} 条道具导入完成")
317
+
318
+ # 验证
319
+ print("\n--- 数据验证 ---")
320
+ for table, label in [
321
+ (f"{SCHEMA}.protagonist_skills", "技能"),
322
+ (f"{SCHEMA}.protagonist_skill_events", "技能事件"),
323
+ (f"{SCHEMA}.protagonist_inventory", "道具"),
324
+ (f"{SCHEMA}.protagonist_inventory_events", "道具事件"),
325
+ (f"{SCHEMA}.protagonist_cultivation", "修炼进度"),
326
+ ]:
327
+ cur.execute(f"SELECT COUNT(*) FROM {table}")
328
+ count = cur.fetchone()[0]
329
+ print(f" {label:10s}: {count} 条")
330
+
331
+ print("\n主角数据初始化完成")
332
+
333
+ except Exception as e:
334
+ conn.rollback()
335
+ print(f"\n!!! 失败: {e}")
336
+ raise
337
+ finally:
338
+ cur.close()
339
+ conn.close()
340
+
341
+
342
+ if __name__ == "__main__":
343
+ run()