novelws 5.2.0 → 5.4.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 +51 -0
- package/README.md +53 -0
- 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 +51 -0
- package/dist/commands/dashboard.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +26 -0
- 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 +6 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +8 -0
- 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/package.json +10 -3
- package/templates/commands/analyze.md +9 -1
- package/templates/commands/expand.md +19 -3
- package/templates/commands/write.md +23 -7
- package/templates/dashboard/assets/ChaptersView-CkYRzkTt.css +1 -0
- package/templates/dashboard/assets/CharactersView-BuH15mT-.css +1 -0
- package/templates/dashboard/assets/DashboardView-qD5yG8Hj.css +1 -0
- package/templates/dashboard/assets/PlotsView-Cnx9_j0t.css +1 -0
- package/templates/dashboard/assets/ProtagonistView-DfWCFC3K.css +1 -0
- package/templates/dashboard/assets/RelationshipsView-DMtu4xH0.css +1 -0
- package/templates/dashboard/assets/TimelineView-CovxzAeu.css +1 -0
- package/templates/dashboard/assets/index-nD7kmLMb.css +1 -0
- package/templates/dashboard/index.html +13 -0
- package/templates/dot-claude/CLAUDE.md +27 -0
- 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
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""db_volume_switch.py — 从 DB 生成 volume-summary.md
|
|
3
|
+
|
|
4
|
+
用法:
|
|
5
|
+
python scripts/db_volume_switch.py --vol 2 # 为 vol-002 生成 volume-summary
|
|
6
|
+
python scripts/db_volume_switch.py --vol 2 --dry # 预览输出,不写入文件
|
|
7
|
+
|
|
8
|
+
从 DB 查询上一卷的角色状态、伏笔、关系、时间线,生成新卷的 volume-summary.md。
|
|
9
|
+
替代原有的"读取4个 tracking JSON"卷切换流程。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
import psycopg2
|
|
17
|
+
|
|
18
|
+
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
19
|
+
SCHEMA = "novelws"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_config():
|
|
23
|
+
config_path = os.path.join(BASE, "resources", "config.json")
|
|
24
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
25
|
+
return json.load(f)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_db_config(config):
|
|
29
|
+
db = config.get("database", {})
|
|
30
|
+
return {
|
|
31
|
+
"host": db.get("host", "127.0.0.1"),
|
|
32
|
+
"port": db.get("port", 5432),
|
|
33
|
+
"dbname": db.get("dbname", "postgres"),
|
|
34
|
+
"user": db.get("user", "postgres"),
|
|
35
|
+
"password": db.get("password", ""),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_story_path(config):
|
|
40
|
+
story_name = config.get("story", "")
|
|
41
|
+
if not story_name:
|
|
42
|
+
stories_dir = os.path.join(BASE, "stories")
|
|
43
|
+
if os.path.isdir(stories_dir):
|
|
44
|
+
dirs = [d for d in os.listdir(stories_dir)
|
|
45
|
+
if os.path.isdir(os.path.join(stories_dir, d))]
|
|
46
|
+
if dirs:
|
|
47
|
+
story_name = dirs[0]
|
|
48
|
+
return os.path.join(BASE, "stories", story_name)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def generate_volume_summary(cur, target_vol):
|
|
52
|
+
"""从 DB 生成 volume-summary.md 内容"""
|
|
53
|
+
prev_vol = target_vol - 1
|
|
54
|
+
lines = []
|
|
55
|
+
|
|
56
|
+
# 获取目标卷信息
|
|
57
|
+
cur.execute(f"""
|
|
58
|
+
SELECT v.title, p.phase_number, p.title as phase_title
|
|
59
|
+
FROM {SCHEMA}.volumes v JOIN {SCHEMA}.phases p ON v.phase_id = p.id
|
|
60
|
+
WHERE v.vol_number = %s
|
|
61
|
+
""", (target_vol,))
|
|
62
|
+
target_info = cur.fetchone()
|
|
63
|
+
target_title = target_info[0] if target_info else f"vol-{target_vol:03d}"
|
|
64
|
+
phase_num = target_info[1] if target_info else "?"
|
|
65
|
+
phase_title = target_info[2] if target_info else "?"
|
|
66
|
+
|
|
67
|
+
lines.append(f"# Volume Summary — vol-{target_vol:03d} {target_title}")
|
|
68
|
+
lines.append(f"纪元{phase_num} [{phase_title}]")
|
|
69
|
+
lines.append("")
|
|
70
|
+
|
|
71
|
+
# 1. 故事进度
|
|
72
|
+
lines.append("## 故事进度")
|
|
73
|
+
cur.execute(f"""
|
|
74
|
+
SELECT v.vol_number, v.title,
|
|
75
|
+
COUNT(ch.id) as total_ch,
|
|
76
|
+
COUNT(CASE WHEN ch.status IN ('drafted','final') THEN 1 END) as written_ch,
|
|
77
|
+
COALESCE(SUM(ch.word_count), 0) as words
|
|
78
|
+
FROM {SCHEMA}.volumes v
|
|
79
|
+
LEFT JOIN {SCHEMA}.chapters ch ON ch.volume_id = v.id
|
|
80
|
+
WHERE v.vol_number <= %s
|
|
81
|
+
GROUP BY v.vol_number, v.title
|
|
82
|
+
ORDER BY v.vol_number
|
|
83
|
+
""", (prev_vol,))
|
|
84
|
+
rows = cur.fetchall()
|
|
85
|
+
for r in rows:
|
|
86
|
+
status = "已完成" if r[3] == r[2] and r[2] > 0 else f"进行中({r[3]}/{r[2]}章)"
|
|
87
|
+
lines.append(f"- vol-{r[0]:03d} {r[1]}: {status}, {r[4]:,}字")
|
|
88
|
+
lines.append("")
|
|
89
|
+
|
|
90
|
+
# 2. 活跃角色状态(从最近2卷提取)
|
|
91
|
+
lines.append("## 活跃角色状态")
|
|
92
|
+
cur.execute(f"""
|
|
93
|
+
SELECT DISTINCT ON (c.name)
|
|
94
|
+
c.name, c.role_type, c.status, cs.location, cs.state_summary, cs.last_appearance,
|
|
95
|
+
v.vol_number
|
|
96
|
+
FROM {SCHEMA}.characters c
|
|
97
|
+
JOIN {SCHEMA}.character_states cs ON c.id = cs.character_id
|
|
98
|
+
JOIN {SCHEMA}.volumes v ON cs.volume_id = v.id
|
|
99
|
+
WHERE v.vol_number BETWEEN %s AND %s AND c.status != 'dead'
|
|
100
|
+
ORDER BY c.name, v.vol_number DESC, cs.last_appearance DESC
|
|
101
|
+
""", (max(1, prev_vol - 1), prev_vol))
|
|
102
|
+
rows = cur.fetchall()
|
|
103
|
+
if rows:
|
|
104
|
+
for r in rows:
|
|
105
|
+
summary = r[4] or "-"
|
|
106
|
+
if len(summary) > 100:
|
|
107
|
+
summary = summary[:100] + "…"
|
|
108
|
+
lines.append(f"- **{r[0]}** ({r[1]}, {r[2]}): {r[3] or '?'} — {summary} [vol-{r[6]:03d} ch-{r[5]}]")
|
|
109
|
+
else:
|
|
110
|
+
lines.append("(无角色数据)")
|
|
111
|
+
lines.append("")
|
|
112
|
+
|
|
113
|
+
# 已故角色(单独列出)
|
|
114
|
+
cur.execute(f"""
|
|
115
|
+
SELECT c.name, cs.state_summary, cs.last_appearance, v.vol_number
|
|
116
|
+
FROM {SCHEMA}.characters c
|
|
117
|
+
JOIN {SCHEMA}.character_states cs ON c.id = cs.character_id
|
|
118
|
+
JOIN {SCHEMA}.volumes v ON cs.volume_id = v.id
|
|
119
|
+
WHERE c.status = 'dead' AND v.vol_number <= %s
|
|
120
|
+
ORDER BY cs.last_appearance DESC
|
|
121
|
+
""", (prev_vol,))
|
|
122
|
+
dead_rows = cur.fetchall()
|
|
123
|
+
if dead_rows:
|
|
124
|
+
lines.append("### 已故/退场角色")
|
|
125
|
+
for r in dead_rows:
|
|
126
|
+
summary = r[1] or "-"
|
|
127
|
+
if len(summary) > 80:
|
|
128
|
+
summary = summary[:80] + "…"
|
|
129
|
+
lines.append(f"- {r[0]}: {summary} [vol-{r[3]:03d} ch-{r[2]}]")
|
|
130
|
+
lines.append("")
|
|
131
|
+
|
|
132
|
+
# 3. 活跃伏笔
|
|
133
|
+
lines.append("## 活跃伏笔")
|
|
134
|
+
cur.execute(f"""
|
|
135
|
+
SELECT f.code, f.description, f.status, ch.chapter_number, v.vol_number, f.note
|
|
136
|
+
FROM {SCHEMA}.foreshadowing f
|
|
137
|
+
JOIN {SCHEMA}.chapters ch ON f.planted_chapter_id = ch.id
|
|
138
|
+
JOIN {SCHEMA}.volumes v ON ch.volume_id = v.id
|
|
139
|
+
WHERE f.status IN ('planted', 'hinted', 'partially_resolved')
|
|
140
|
+
ORDER BY v.vol_number, ch.chapter_number
|
|
141
|
+
""")
|
|
142
|
+
rows = cur.fetchall()
|
|
143
|
+
if rows:
|
|
144
|
+
lines.append(f"共 {len(rows)} 条未解决:")
|
|
145
|
+
for r in rows:
|
|
146
|
+
note = f" ({r[5]})" if r[5] else ""
|
|
147
|
+
lines.append(f"- [{r[0]}] vol-{r[4]:03d} ch-{r[3]:03d} [{r[2]}]: {r[1]}{note}")
|
|
148
|
+
else:
|
|
149
|
+
lines.append("无活跃伏笔")
|
|
150
|
+
lines.append("")
|
|
151
|
+
|
|
152
|
+
# 4. 关键关系
|
|
153
|
+
lines.append("## 关键关系")
|
|
154
|
+
cur.execute(f"""
|
|
155
|
+
SELECT ca.name, cb.name, r.relationship_type, r.current_status
|
|
156
|
+
FROM {SCHEMA}.relationships r
|
|
157
|
+
JOIN {SCHEMA}.characters ca ON r.character_a_id = ca.id
|
|
158
|
+
JOIN {SCHEMA}.characters cb ON r.character_b_id = cb.id
|
|
159
|
+
JOIN {SCHEMA}.volumes v ON r.volume_id = v.id
|
|
160
|
+
WHERE v.vol_number = %s
|
|
161
|
+
ORDER BY r.last_update_chapter DESC
|
|
162
|
+
""", (prev_vol,))
|
|
163
|
+
rows = cur.fetchall()
|
|
164
|
+
if rows:
|
|
165
|
+
for r in rows:
|
|
166
|
+
status = r[3] or "-"
|
|
167
|
+
if len(status) > 80:
|
|
168
|
+
status = status[:80] + "…"
|
|
169
|
+
lines.append(f"- {r[0]} → {r[1]} [{r[2]}]: {status}")
|
|
170
|
+
else:
|
|
171
|
+
lines.append("(无关系数据)")
|
|
172
|
+
lines.append("")
|
|
173
|
+
|
|
174
|
+
# 5. 待续悬念(上一卷末章钩子)
|
|
175
|
+
lines.append("## 待续悬念")
|
|
176
|
+
cur.execute(f"""
|
|
177
|
+
SELECT ch.chapter_number, te.event_description
|
|
178
|
+
FROM {SCHEMA}.timeline_events te
|
|
179
|
+
JOIN {SCHEMA}.chapters ch ON te.chapter_id = ch.id
|
|
180
|
+
JOIN {SCHEMA}.volumes v ON te.volume_id = v.id
|
|
181
|
+
WHERE v.vol_number = %s
|
|
182
|
+
ORDER BY ch.chapter_number DESC
|
|
183
|
+
LIMIT 3
|
|
184
|
+
""", (prev_vol,))
|
|
185
|
+
rows = cur.fetchall()
|
|
186
|
+
if rows:
|
|
187
|
+
lines.append("上一卷末尾事件:")
|
|
188
|
+
for r in reversed(rows):
|
|
189
|
+
lines.append(f"- ch-{r[0]:03d}: {r[1]}")
|
|
190
|
+
else:
|
|
191
|
+
lines.append("(无上一卷数据)")
|
|
192
|
+
lines.append("")
|
|
193
|
+
|
|
194
|
+
# 6. 活跃情节线
|
|
195
|
+
lines.append("## 活跃情节线")
|
|
196
|
+
cur.execute(f"""
|
|
197
|
+
SELECT name, status, description
|
|
198
|
+
FROM {SCHEMA}.plot_threads
|
|
199
|
+
WHERE status NOT IN ('已完成')
|
|
200
|
+
ORDER BY id
|
|
201
|
+
""")
|
|
202
|
+
rows = cur.fetchall()
|
|
203
|
+
if rows:
|
|
204
|
+
for r in rows:
|
|
205
|
+
desc = r[2] or "-"
|
|
206
|
+
if len(desc) > 100:
|
|
207
|
+
desc = desc[:100] + "…"
|
|
208
|
+
lines.append(f"- **{r[0]}** [{r[1]}]: {desc}")
|
|
209
|
+
else:
|
|
210
|
+
lines.append("无活跃情节线")
|
|
211
|
+
lines.append("")
|
|
212
|
+
|
|
213
|
+
# 7. 主角状态
|
|
214
|
+
lines.append("## 主角状态")
|
|
215
|
+
cur.execute(f"""
|
|
216
|
+
SELECT level, progress_pct FROM {SCHEMA}.cultivation_curve
|
|
217
|
+
ORDER BY chapter_number DESC LIMIT 1
|
|
218
|
+
""")
|
|
219
|
+
cult = cur.fetchone()
|
|
220
|
+
if cult:
|
|
221
|
+
lines.append(f"- 修炼: {cult[0]} ({cult[1]}%)")
|
|
222
|
+
|
|
223
|
+
cur.execute(f"""
|
|
224
|
+
SELECT skill_name, skill_category FROM {SCHEMA}.skill_overview
|
|
225
|
+
WHERE status = 'active' ORDER BY acquired_chapter
|
|
226
|
+
""")
|
|
227
|
+
skills = cur.fetchall()
|
|
228
|
+
if skills:
|
|
229
|
+
skill_strs = [f"{s[0]}[{s[1]}]" for s in skills]
|
|
230
|
+
lines.append(f"- 技能清单: {', '.join(skill_strs)}")
|
|
231
|
+
|
|
232
|
+
cur.execute(f"""
|
|
233
|
+
SELECT item_name, quantity FROM {SCHEMA}.current_inventory
|
|
234
|
+
ORDER BY item_type
|
|
235
|
+
""")
|
|
236
|
+
items = cur.fetchall()
|
|
237
|
+
if items:
|
|
238
|
+
item_strs = [f"{r[0]}×{r[1]}" if r[1] > 1 else r[0] for r in items]
|
|
239
|
+
lines.append(f"- 关键道具: {'、'.join(item_strs)}")
|
|
240
|
+
lines.append("")
|
|
241
|
+
|
|
242
|
+
return "\n".join(lines)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def main():
|
|
246
|
+
parser = argparse.ArgumentParser(description="从 DB 生成 volume-summary.md")
|
|
247
|
+
parser.add_argument("--vol", type=int, required=True, help="目标卷号")
|
|
248
|
+
parser.add_argument("--dry", action="store_true", help="预览输出,不写入文件")
|
|
249
|
+
args = parser.parse_args()
|
|
250
|
+
|
|
251
|
+
config = load_config()
|
|
252
|
+
db_config = get_db_config(config)
|
|
253
|
+
story_path = get_story_path(config)
|
|
254
|
+
volumes_dir = os.path.join(story_path, "volumes")
|
|
255
|
+
|
|
256
|
+
conn = psycopg2.connect(**db_config)
|
|
257
|
+
cur = conn.cursor()
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
content = generate_volume_summary(cur, args.vol)
|
|
261
|
+
|
|
262
|
+
if args.dry:
|
|
263
|
+
print(content)
|
|
264
|
+
else:
|
|
265
|
+
vol_dir = os.path.join(volumes_dir, f"vol-{args.vol:03d}")
|
|
266
|
+
os.makedirs(vol_dir, exist_ok=True)
|
|
267
|
+
out_path = os.path.join(vol_dir, "volume-summary.md")
|
|
268
|
+
with open(out_path, "w", encoding="utf-8") as f:
|
|
269
|
+
f.write(content)
|
|
270
|
+
print(f"已生成: {out_path}")
|
|
271
|
+
print(f"字数: {len(content)} 字符")
|
|
272
|
+
finally:
|
|
273
|
+
cur.close()
|
|
274
|
+
conn.close()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__":
|
|
278
|
+
main()
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Phase A: 小说数据库初始化 — 建表 + 视图
|
|
3
|
+
|
|
4
|
+
用法:
|
|
5
|
+
python scripts/phase_a_init_db.py # 创建 schema + 表 + 视图
|
|
6
|
+
python scripts/phase_a_init_db.py --seed # 建表后运行 db_sync.py --all 导入数据
|
|
7
|
+
|
|
8
|
+
依赖: pip install -r scripts/requirements.txt
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import psycopg2
|
|
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
|
+
# ─────────────────────────────────────────────
|
|
39
|
+
# DDL: 建 schema + 20 张核心表
|
|
40
|
+
# ─────────────────────────────────────────────
|
|
41
|
+
DDL = f"""
|
|
42
|
+
DROP SCHEMA IF EXISTS {SCHEMA} CASCADE;
|
|
43
|
+
CREATE SCHEMA {SCHEMA};
|
|
44
|
+
|
|
45
|
+
-- 1) phases: 纪元
|
|
46
|
+
CREATE TABLE {SCHEMA}.phases (
|
|
47
|
+
id SERIAL PRIMARY KEY,
|
|
48
|
+
phase_number INT NOT NULL UNIQUE,
|
|
49
|
+
title VARCHAR(100) NOT NULL,
|
|
50
|
+
theme VARCHAR(200),
|
|
51
|
+
vol_start INT NOT NULL,
|
|
52
|
+
vol_end INT NOT NULL,
|
|
53
|
+
protagonist_level_start VARCHAR(50),
|
|
54
|
+
protagonist_level_end VARCHAR(50),
|
|
55
|
+
stage_level VARCHAR(50),
|
|
56
|
+
growth_keyword VARCHAR(200),
|
|
57
|
+
core_conflict TEXT,
|
|
58
|
+
status VARCHAR(20) DEFAULT 'planned'
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
-- 2) volumes: 卷
|
|
62
|
+
CREATE TABLE {SCHEMA}.volumes (
|
|
63
|
+
id SERIAL PRIMARY KEY,
|
|
64
|
+
phase_id INT NOT NULL REFERENCES {SCHEMA}.phases(id),
|
|
65
|
+
vol_number INT NOT NULL UNIQUE,
|
|
66
|
+
title VARCHAR(200) NOT NULL,
|
|
67
|
+
arc_type VARCHAR(50),
|
|
68
|
+
instance_name VARCHAR(100),
|
|
69
|
+
instance_type VARCHAR(100),
|
|
70
|
+
instance_level VARCHAR(50),
|
|
71
|
+
protagonist_level_range VARCHAR(100),
|
|
72
|
+
chapter_count INT DEFAULT 100,
|
|
73
|
+
outline_path TEXT,
|
|
74
|
+
summary_path TEXT,
|
|
75
|
+
status VARCHAR(20) DEFAULT 'planned'
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
-- 3) chapters: 章节
|
|
79
|
+
CREATE TABLE {SCHEMA}.chapters (
|
|
80
|
+
id SERIAL PRIMARY KEY,
|
|
81
|
+
volume_id INT NOT NULL REFERENCES {SCHEMA}.volumes(id),
|
|
82
|
+
chapter_number INT NOT NULL,
|
|
83
|
+
global_chapter_number INT NOT NULL UNIQUE,
|
|
84
|
+
title VARCHAR(300),
|
|
85
|
+
synopsis_path TEXT,
|
|
86
|
+
content_path TEXT,
|
|
87
|
+
synopsis_summary TEXT,
|
|
88
|
+
synopsis_keywords TEXT[],
|
|
89
|
+
status VARCHAR(20) DEFAULT 'planned',
|
|
90
|
+
word_count INT DEFAULT 0,
|
|
91
|
+
scene_location VARCHAR(200),
|
|
92
|
+
time_in_story VARCHAR(200),
|
|
93
|
+
pov_character VARCHAR(100) DEFAULT '主角'
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
-- 4) characters: 角色主表
|
|
97
|
+
CREATE TABLE {SCHEMA}.characters (
|
|
98
|
+
id SERIAL PRIMARY KEY,
|
|
99
|
+
name VARCHAR(100) NOT NULL UNIQUE,
|
|
100
|
+
aliases TEXT[],
|
|
101
|
+
role_type VARCHAR(50),
|
|
102
|
+
first_appearance_vol INT,
|
|
103
|
+
first_appearance_ch INT,
|
|
104
|
+
cultivation_level VARCHAR(100),
|
|
105
|
+
faction VARCHAR(200),
|
|
106
|
+
status VARCHAR(20) DEFAULT 'active'
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
-- 5) character_states: 角色状态快照(按卷)
|
|
110
|
+
CREATE TABLE {SCHEMA}.character_states (
|
|
111
|
+
id SERIAL PRIMARY KEY,
|
|
112
|
+
character_id INT NOT NULL REFERENCES {SCHEMA}.characters(id),
|
|
113
|
+
volume_id INT NOT NULL REFERENCES {SCHEMA}.volumes(id),
|
|
114
|
+
cultivation_level VARCHAR(100),
|
|
115
|
+
location VARCHAR(200),
|
|
116
|
+
state_summary TEXT,
|
|
117
|
+
last_appearance INT,
|
|
118
|
+
UNIQUE(character_id, volume_id)
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
-- 6) plot_threads: 情节线
|
|
122
|
+
CREATE TABLE {SCHEMA}.plot_threads (
|
|
123
|
+
id SERIAL PRIMARY KEY,
|
|
124
|
+
name VARCHAR(200) NOT NULL,
|
|
125
|
+
thread_type VARCHAR(50),
|
|
126
|
+
status VARCHAR(30) DEFAULT 'active',
|
|
127
|
+
description TEXT,
|
|
128
|
+
start_volume_id INT REFERENCES {SCHEMA}.volumes(id),
|
|
129
|
+
volume_id INT REFERENCES {SCHEMA}.volumes(id),
|
|
130
|
+
key_events JSONB DEFAULT '[]'::jsonb
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
-- 7) foreshadowing: 伏笔
|
|
134
|
+
CREATE TABLE {SCHEMA}.foreshadowing (
|
|
135
|
+
id SERIAL PRIMARY KEY,
|
|
136
|
+
code VARCHAR(20) UNIQUE,
|
|
137
|
+
plot_thread_id INT REFERENCES {SCHEMA}.plot_threads(id),
|
|
138
|
+
description TEXT NOT NULL,
|
|
139
|
+
planted_chapter_id INT REFERENCES {SCHEMA}.chapters(id),
|
|
140
|
+
hinted_chapter_id INT REFERENCES {SCHEMA}.chapters(id),
|
|
141
|
+
resolved_chapter_id INT REFERENCES {SCHEMA}.chapters(id),
|
|
142
|
+
status VARCHAR(30) DEFAULT 'planted',
|
|
143
|
+
importance VARCHAR(20) DEFAULT 'major',
|
|
144
|
+
note TEXT
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
-- 8) chapter_foreshadowing: 伏笔×章节关联
|
|
148
|
+
CREATE TABLE {SCHEMA}.chapter_foreshadowing (
|
|
149
|
+
id SERIAL PRIMARY KEY,
|
|
150
|
+
foreshadowing_id INT NOT NULL REFERENCES {SCHEMA}.foreshadowing(id),
|
|
151
|
+
chapter_id INT NOT NULL REFERENCES {SCHEMA}.chapters(id),
|
|
152
|
+
action_type VARCHAR(20) NOT NULL,
|
|
153
|
+
UNIQUE(foreshadowing_id, chapter_id, action_type)
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
-- 9) chapter_participants: 章节×角色关联
|
|
157
|
+
CREATE TABLE {SCHEMA}.chapter_participants (
|
|
158
|
+
id SERIAL PRIMARY KEY,
|
|
159
|
+
chapter_id INT NOT NULL REFERENCES {SCHEMA}.chapters(id),
|
|
160
|
+
character_id INT NOT NULL REFERENCES {SCHEMA}.characters(id),
|
|
161
|
+
role_in_chapter VARCHAR(50),
|
|
162
|
+
UNIQUE(chapter_id, character_id)
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
-- 10) relationships: 角色关系
|
|
166
|
+
CREATE TABLE {SCHEMA}.relationships (
|
|
167
|
+
id SERIAL PRIMARY KEY,
|
|
168
|
+
character_a_id INT NOT NULL REFERENCES {SCHEMA}.characters(id),
|
|
169
|
+
character_b_id INT NOT NULL REFERENCES {SCHEMA}.characters(id),
|
|
170
|
+
relationship_type VARCHAR(100),
|
|
171
|
+
current_status TEXT,
|
|
172
|
+
note TEXT,
|
|
173
|
+
last_update_chapter INT,
|
|
174
|
+
volume_id INT REFERENCES {SCHEMA}.volumes(id),
|
|
175
|
+
UNIQUE(character_a_id, character_b_id, volume_id)
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
-- 11) timeline_events: 时间线
|
|
179
|
+
CREATE TABLE {SCHEMA}.timeline_events (
|
|
180
|
+
id SERIAL PRIMARY KEY,
|
|
181
|
+
chapter_id INT NOT NULL REFERENCES {SCHEMA}.chapters(id),
|
|
182
|
+
event_description TEXT NOT NULL,
|
|
183
|
+
story_time VARCHAR(200),
|
|
184
|
+
location VARCHAR(200),
|
|
185
|
+
tags TEXT[] DEFAULT '{{}}',
|
|
186
|
+
volume_id INT NOT NULL REFERENCES {SCHEMA}.volumes(id)
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
-- 12) cross_volume_refs: 跨卷引用
|
|
190
|
+
CREATE TABLE {SCHEMA}.cross_volume_refs (
|
|
191
|
+
id SERIAL PRIMARY KEY,
|
|
192
|
+
source_chapter_id INT NOT NULL REFERENCES {SCHEMA}.chapters(id),
|
|
193
|
+
target_chapter_id INT NOT NULL REFERENCES {SCHEMA}.chapters(id),
|
|
194
|
+
ref_type VARCHAR(50),
|
|
195
|
+
description TEXT
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
-- 13) character_key_changes: 角色章级变更历史
|
|
199
|
+
CREATE TABLE {SCHEMA}.character_key_changes (
|
|
200
|
+
id SERIAL PRIMARY KEY,
|
|
201
|
+
character_id INT NOT NULL REFERENCES {SCHEMA}.characters(id),
|
|
202
|
+
chapter_number INT NOT NULL,
|
|
203
|
+
change_desc TEXT NOT NULL,
|
|
204
|
+
volume_id INT NOT NULL REFERENCES {SCHEMA}.volumes(id)
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
-- 14) relationship_history: 关系变迁历史
|
|
208
|
+
CREATE TABLE {SCHEMA}.relationship_history (
|
|
209
|
+
id SERIAL PRIMARY KEY,
|
|
210
|
+
relationship_id INT NOT NULL REFERENCES {SCHEMA}.relationships(id),
|
|
211
|
+
chapter_number INT NOT NULL,
|
|
212
|
+
status_desc TEXT NOT NULL,
|
|
213
|
+
volume_id INT NOT NULL REFERENCES {SCHEMA}.volumes(id)
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
-- 15) plot_thread_events: 情节线关键事件(展开表)
|
|
217
|
+
CREATE TABLE {SCHEMA}.plot_thread_events (
|
|
218
|
+
id SERIAL PRIMARY KEY,
|
|
219
|
+
plot_thread_id INT NOT NULL REFERENCES {SCHEMA}.plot_threads(id),
|
|
220
|
+
chapter_number INT NOT NULL,
|
|
221
|
+
event_desc TEXT NOT NULL,
|
|
222
|
+
volume_id INT NOT NULL REFERENCES {SCHEMA}.volumes(id)
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
-- 索引
|
|
226
|
+
CREATE INDEX idx_chapters_volume ON {SCHEMA}.chapters(volume_id);
|
|
227
|
+
CREATE INDEX idx_chapters_global ON {SCHEMA}.chapters(global_chapter_number);
|
|
228
|
+
CREATE INDEX idx_char_states_vol ON {SCHEMA}.character_states(volume_id);
|
|
229
|
+
CREATE INDEX idx_foreshadowing_status ON {SCHEMA}.foreshadowing(status);
|
|
230
|
+
CREATE INDEX idx_timeline_chapter ON {SCHEMA}.timeline_events(chapter_id);
|
|
231
|
+
CREATE INDEX idx_timeline_tags ON {SCHEMA}.timeline_events USING GIN(tags);
|
|
232
|
+
CREATE INDEX idx_plot_threads_status ON {SCHEMA}.plot_threads(status);
|
|
233
|
+
CREATE INDEX idx_plot_threads_vol ON {SCHEMA}.plot_threads(volume_id);
|
|
234
|
+
CREATE INDEX idx_char_key_changes_char ON {SCHEMA}.character_key_changes(character_id);
|
|
235
|
+
CREATE INDEX idx_char_key_changes_vol ON {SCHEMA}.character_key_changes(volume_id);
|
|
236
|
+
CREATE INDEX idx_rel_history_rel ON {SCHEMA}.relationship_history(relationship_id);
|
|
237
|
+
CREATE INDEX idx_rel_history_vol ON {SCHEMA}.relationship_history(volume_id);
|
|
238
|
+
CREATE INDEX idx_plot_events_thread ON {SCHEMA}.plot_thread_events(plot_thread_id);
|
|
239
|
+
CREATE INDEX idx_plot_events_vol ON {SCHEMA}.plot_thread_events(volume_id);
|
|
240
|
+
|
|
241
|
+
-- 16) protagonist_skills: 主角技能主表
|
|
242
|
+
CREATE TABLE {SCHEMA}.protagonist_skills (
|
|
243
|
+
id SERIAL PRIMARY KEY,
|
|
244
|
+
skill_name VARCHAR(100) NOT NULL UNIQUE,
|
|
245
|
+
skill_category VARCHAR(20) NOT NULL,
|
|
246
|
+
skill_level VARCHAR(50),
|
|
247
|
+
parent_skill_id INT REFERENCES {SCHEMA}.protagonist_skills(id),
|
|
248
|
+
description TEXT,
|
|
249
|
+
acquired_chapter INT NOT NULL,
|
|
250
|
+
acquired_method VARCHAR(200),
|
|
251
|
+
last_used_chapter INT,
|
|
252
|
+
use_count INT DEFAULT 0,
|
|
253
|
+
status VARCHAR(20) DEFAULT 'active'
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
-- 17) protagonist_skill_events: 技能时序表
|
|
257
|
+
CREATE TABLE {SCHEMA}.protagonist_skill_events (
|
|
258
|
+
id SERIAL PRIMARY KEY,
|
|
259
|
+
skill_id INT NOT NULL REFERENCES {SCHEMA}.protagonist_skills(id),
|
|
260
|
+
chapter_number INT NOT NULL,
|
|
261
|
+
event_type VARCHAR(30) NOT NULL,
|
|
262
|
+
detail TEXT,
|
|
263
|
+
volume_id INT NOT NULL REFERENCES {SCHEMA}.volumes(id)
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
-- 18) protagonist_inventory: 装备/道具主表
|
|
267
|
+
CREATE TABLE {SCHEMA}.protagonist_inventory (
|
|
268
|
+
id SERIAL PRIMARY KEY,
|
|
269
|
+
item_name VARCHAR(100) NOT NULL,
|
|
270
|
+
item_type VARCHAR(30) NOT NULL,
|
|
271
|
+
quantity INT DEFAULT 1,
|
|
272
|
+
quality VARCHAR(30),
|
|
273
|
+
description TEXT,
|
|
274
|
+
acquired_chapter INT NOT NULL,
|
|
275
|
+
acquired_method VARCHAR(200),
|
|
276
|
+
status VARCHAR(20) DEFAULT 'held',
|
|
277
|
+
UNIQUE(item_name, acquired_chapter)
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
-- 19) protagonist_inventory_events: 道具时序表
|
|
281
|
+
CREATE TABLE {SCHEMA}.protagonist_inventory_events (
|
|
282
|
+
id SERIAL PRIMARY KEY,
|
|
283
|
+
item_id INT NOT NULL REFERENCES {SCHEMA}.protagonist_inventory(id),
|
|
284
|
+
chapter_number INT NOT NULL,
|
|
285
|
+
event_type VARCHAR(30) NOT NULL,
|
|
286
|
+
quantity_change INT DEFAULT 0,
|
|
287
|
+
detail TEXT,
|
|
288
|
+
volume_id INT NOT NULL REFERENCES {SCHEMA}.volumes(id)
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
-- 20) protagonist_cultivation: 修炼进度表
|
|
292
|
+
CREATE TABLE {SCHEMA}.protagonist_cultivation (
|
|
293
|
+
id SERIAL PRIMARY KEY,
|
|
294
|
+
chapter_number INT NOT NULL,
|
|
295
|
+
level VARCHAR(50) NOT NULL,
|
|
296
|
+
progress_pct DECIMAL(5,1),
|
|
297
|
+
breakthrough_type VARCHAR(20),
|
|
298
|
+
trigger VARCHAR(200),
|
|
299
|
+
detail TEXT,
|
|
300
|
+
volume_id INT NOT NULL REFERENCES {SCHEMA}.volumes(id)
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
-- protagonist 索引
|
|
304
|
+
CREATE INDEX idx_skills_category ON {SCHEMA}.protagonist_skills(skill_category);
|
|
305
|
+
CREATE INDEX idx_skills_status ON {SCHEMA}.protagonist_skills(status);
|
|
306
|
+
CREATE INDEX idx_skill_events_skill ON {SCHEMA}.protagonist_skill_events(skill_id);
|
|
307
|
+
CREATE INDEX idx_skill_events_vol ON {SCHEMA}.protagonist_skill_events(volume_id);
|
|
308
|
+
CREATE INDEX idx_skill_events_type ON {SCHEMA}.protagonist_skill_events(event_type);
|
|
309
|
+
CREATE INDEX idx_inventory_type ON {SCHEMA}.protagonist_inventory(item_type);
|
|
310
|
+
CREATE INDEX idx_inventory_status ON {SCHEMA}.protagonist_inventory(status);
|
|
311
|
+
CREATE INDEX idx_inv_events_item ON {SCHEMA}.protagonist_inventory_events(item_id);
|
|
312
|
+
CREATE INDEX idx_inv_events_vol ON {SCHEMA}.protagonist_inventory_events(volume_id);
|
|
313
|
+
CREATE INDEX idx_cultivation_vol ON {SCHEMA}.protagonist_cultivation(volume_id);
|
|
314
|
+
CREATE INDEX idx_cultivation_ch ON {SCHEMA}.protagonist_cultivation(chapter_number);
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
VIEWS = f"""
|
|
318
|
+
CREATE OR REPLACE VIEW {SCHEMA}.writing_dashboard AS
|
|
319
|
+
SELECT
|
|
320
|
+
p.phase_number,
|
|
321
|
+
p.title AS phase_title,
|
|
322
|
+
COUNT(DISTINCT v.id) AS total_volumes,
|
|
323
|
+
COUNT(DISTINCT v.id) FILTER (WHERE v.status = 'completed') AS completed_volumes,
|
|
324
|
+
COUNT(DISTINCT ch.id) AS total_chapters,
|
|
325
|
+
COUNT(DISTINCT ch.id) FILTER (WHERE ch.status = 'final') AS final_chapters,
|
|
326
|
+
COUNT(DISTINCT ch.id) FILTER (WHERE ch.status = 'synopsis') AS synopsis_chapters,
|
|
327
|
+
SUM(ch.word_count) AS total_words
|
|
328
|
+
FROM {SCHEMA}.phases p
|
|
329
|
+
LEFT JOIN {SCHEMA}.volumes v ON v.phase_id = p.id
|
|
330
|
+
LEFT JOIN {SCHEMA}.chapters ch ON ch.volume_id = v.id
|
|
331
|
+
GROUP BY p.id, p.phase_number, p.title
|
|
332
|
+
ORDER BY p.phase_number;
|
|
333
|
+
|
|
334
|
+
CREATE OR REPLACE VIEW {SCHEMA}.open_foreshadowing AS
|
|
335
|
+
SELECT
|
|
336
|
+
f.code, f.description, f.status,
|
|
337
|
+
ch.chapter_number AS planted_at,
|
|
338
|
+
ch.time_in_story AS planted_time,
|
|
339
|
+
f.note
|
|
340
|
+
FROM {SCHEMA}.foreshadowing f
|
|
341
|
+
JOIN {SCHEMA}.chapters ch ON f.planted_chapter_id = ch.id
|
|
342
|
+
WHERE f.status IN ('planted', 'hinted', 'partially_resolved')
|
|
343
|
+
ORDER BY ch.chapter_number;
|
|
344
|
+
|
|
345
|
+
CREATE OR REPLACE VIEW {SCHEMA}.character_overview AS
|
|
346
|
+
SELECT
|
|
347
|
+
c.name, c.role_type, c.status,
|
|
348
|
+
cs.location, cs.state_summary, cs.last_appearance,
|
|
349
|
+
v.vol_number, v.title AS volume_title
|
|
350
|
+
FROM {SCHEMA}.characters c
|
|
351
|
+
LEFT JOIN {SCHEMA}.character_states cs ON c.id = cs.character_id
|
|
352
|
+
LEFT JOIN {SCHEMA}.volumes v ON cs.volume_id = v.id
|
|
353
|
+
ORDER BY c.name, v.vol_number;
|
|
354
|
+
|
|
355
|
+
CREATE OR REPLACE VIEW {SCHEMA}.current_inventory AS
|
|
356
|
+
SELECT item_name, item_type, quantity, quality, description, acquired_chapter
|
|
357
|
+
FROM {SCHEMA}.protagonist_inventory
|
|
358
|
+
WHERE status = 'held'
|
|
359
|
+
ORDER BY item_type, item_name;
|
|
360
|
+
|
|
361
|
+
CREATE OR REPLACE VIEW {SCHEMA}.skill_overview AS
|
|
362
|
+
SELECT s.skill_name, s.skill_category, s.skill_level, s.status,
|
|
363
|
+
s.acquired_chapter, s.use_count,
|
|
364
|
+
COUNT(e.id) AS total_events
|
|
365
|
+
FROM {SCHEMA}.protagonist_skills s
|
|
366
|
+
LEFT JOIN {SCHEMA}.protagonist_skill_events e ON s.id = e.skill_id
|
|
367
|
+
GROUP BY s.id
|
|
368
|
+
ORDER BY s.skill_category, s.acquired_chapter;
|
|
369
|
+
|
|
370
|
+
CREATE OR REPLACE VIEW {SCHEMA}.cultivation_curve AS
|
|
371
|
+
SELECT chapter_number, level, progress_pct, breakthrough_type, trigger
|
|
372
|
+
FROM {SCHEMA}.protagonist_cultivation
|
|
373
|
+
ORDER BY chapter_number;
|
|
374
|
+
"""
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def run():
|
|
378
|
+
config = load_config()
|
|
379
|
+
db_config = get_db_config(config)
|
|
380
|
+
conn = psycopg2.connect(**db_config)
|
|
381
|
+
conn.autocommit = False
|
|
382
|
+
cur = conn.cursor()
|
|
383
|
+
|
|
384
|
+
# ── Step 1: 建表 ──
|
|
385
|
+
print("[1/3] 创建 schema + 20 张表...")
|
|
386
|
+
cur.execute(DDL)
|
|
387
|
+
conn.commit()
|
|
388
|
+
print(" ✓ 表结构创建完成")
|
|
389
|
+
|
|
390
|
+
# ── Step 2: 创建视图 ──
|
|
391
|
+
print("[2/3] 创建常用视图...")
|
|
392
|
+
cur.execute(VIEWS)
|
|
393
|
+
conn.commit()
|
|
394
|
+
print(" ✓ 6 个视图创建完成")
|
|
395
|
+
|
|
396
|
+
# ── Step 3: 验证 ──
|
|
397
|
+
print("\n[3/3] 验证表结构...")
|
|
398
|
+
cur.execute(f"""
|
|
399
|
+
SELECT table_name FROM information_schema.tables
|
|
400
|
+
WHERE table_schema = '{SCHEMA}' AND table_type = 'BASE TABLE'
|
|
401
|
+
ORDER BY table_name
|
|
402
|
+
""")
|
|
403
|
+
tables = [row[0] for row in cur.fetchall()]
|
|
404
|
+
print(f" ✓ {len(tables)} 张表创建成功: {', '.join(tables)}")
|
|
405
|
+
|
|
406
|
+
cur.close()
|
|
407
|
+
conn.close()
|
|
408
|
+
|
|
409
|
+
print(f"\n✅ 数据库初始化完成 (schema: {SCHEMA})")
|
|
410
|
+
print(" 下一步: 运行 python scripts/db_sync.py --all 导入 tracking 数据")
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def main():
|
|
414
|
+
import argparse
|
|
415
|
+
parser = argparse.ArgumentParser(description="小说数据库初始化 — 建表 + 视图")
|
|
416
|
+
parser.add_argument("--seed", action="store_true", help="建表后自动运行 db_sync.py --all")
|
|
417
|
+
args = parser.parse_args()
|
|
418
|
+
|
|
419
|
+
run()
|
|
420
|
+
|
|
421
|
+
if args.seed:
|
|
422
|
+
print("\n--- 自动导入数据 ---")
|
|
423
|
+
sync_script = os.path.join(BASE, "scripts", "db_sync.py")
|
|
424
|
+
subprocess.run([sys.executable, sync_script, "--all"], check=True)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
if __name__ == "__main__":
|
|
428
|
+
main()
|