init_project.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 网文项目初始化脚本
  5. 目标:
  6. - 生成可运行的项目结构(webnovel-project)
  7. - 创建/更新 .webnovel/state.json(运行时真相)
  8. - 生成基础设定集与大纲模板文件(供 /webnovel-plan 与 /webnovel-write 使用)
  9. 说明:
  10. - 该脚本是命令 /webnovel-init 的“唯一允许的文件生成入口”(与命令文档保持一致)。
  11. - 生成的内容以“模板骨架”为主,便于 AI/作者后续补全;但保证所有关键文件存在。
  12. """
  13. from __future__ import annotations
  14. import argparse
  15. import json
  16. import subprocess
  17. import sys
  18. from datetime import datetime
  19. from pathlib import Path
  20. from typing import Any, Dict, List
  21. import re
  22. # 安全修复:导入安全工具函数
  23. from security_utils import sanitize_commit_message, atomic_write_json, is_git_available
  24. # Windows 编码兼容性修复
  25. if sys.platform == "win32":
  26. import io
  27. sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
  28. sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
  29. def _read_text_if_exists(path: Path) -> str:
  30. if not path.exists():
  31. return ""
  32. return path.read_text(encoding="utf-8")
  33. def _write_text_if_missing(path: Path, content: str) -> None:
  34. path.parent.mkdir(parents=True, exist_ok=True)
  35. if path.exists():
  36. return
  37. path.write_text(content, encoding="utf-8")
  38. def _split_genre_keys(genre: str) -> list[str]:
  39. raw = (genre or "").strip()
  40. if not raw:
  41. return []
  42. # 支持复合题材:A+B / A+B / A、B / A与B
  43. raw = re.sub(r"[+/、]", "+", raw)
  44. raw = raw.replace("与", "+")
  45. parts = [p.strip() for p in raw.split("+") if p.strip()]
  46. return parts or [raw]
  47. def _normalize_genre_key(key: str) -> str:
  48. aliases = {
  49. "修仙/玄幻": "修仙",
  50. "玄幻修仙": "修仙",
  51. "玄幻": "修仙",
  52. "修真": "修仙",
  53. "都市修真": "都市异能",
  54. "都市高武": "高武",
  55. "都市奇闻": "都市脑洞",
  56. "古言脑洞": "古言",
  57. }
  58. return aliases.get(key, key)
  59. def _apply_label_replacements(text: str, replacements: Dict[str, str]) -> str:
  60. if not text or not replacements:
  61. return text
  62. lines = text.splitlines()
  63. for i, line in enumerate(lines):
  64. stripped = line.lstrip()
  65. for label, value in replacements.items():
  66. if not value:
  67. continue
  68. prefix = f"- {label}:"
  69. if stripped.startswith(prefix):
  70. leading = line[: len(line) - len(stripped)]
  71. lines[i] = f"{leading}{prefix}{value}"
  72. return "\n".join(lines)
  73. def _parse_tier_map(raw: str) -> Dict[str, str]:
  74. result: Dict[str, str] = {}
  75. if not raw:
  76. return result
  77. for part in raw.split(";"):
  78. part = part.strip()
  79. if not part:
  80. continue
  81. if ":" in part:
  82. key, val = part.split(":", 1)
  83. result[key.strip()] = val.strip()
  84. return result
  85. def _render_team_rows(names: List[str], roles: List[str]) -> List[str]:
  86. rows = []
  87. for idx, name in enumerate(names):
  88. role = roles[idx] if idx < len(roles) else ""
  89. rows.append(f"| {name} | {role or '主线/副线'} | | | |")
  90. return rows
  91. def _ensure_state_schema(state: Dict[str, Any]) -> Dict[str, Any]:
  92. """确保 state.json 具备 v5.1 架构所需的字段集合(v5.4 沿用)。
  93. v5.1 变更:
  94. - entities_v3 和 alias_index 已迁移到 index.db,不再存储在 state.json
  95. - structured_relationships 已迁移到 index.db relationships 表
  96. - state.json 保持精简 (< 5KB)
  97. """
  98. state.setdefault("project_info", {})
  99. state.setdefault("progress", {})
  100. state.setdefault("protagonist_state", {})
  101. state.setdefault("relationships", {}) # update_state.py 需要此字段
  102. state.setdefault("disambiguation_warnings", [])
  103. state.setdefault("disambiguation_pending", [])
  104. state.setdefault("world_settings", {"power_system": [], "factions": [], "locations": []})
  105. state.setdefault("plot_threads", {"active_threads": [], "foreshadowing": []})
  106. state.setdefault("review_checkpoints", [])
  107. state.setdefault("chapter_meta", {})
  108. state.setdefault(
  109. "strand_tracker",
  110. {
  111. "last_quest_chapter": 0,
  112. "last_fire_chapter": 0,
  113. "last_constellation_chapter": 0,
  114. "current_dominant": "quest",
  115. "chapters_since_switch": 0,
  116. "history": [],
  117. },
  118. )
  119. # v5.1: entities_v3, alias_index, structured_relationships 已迁移到 index.db
  120. # 不再在 state.json 中初始化这些字段
  121. # progress schema evolution
  122. state["progress"].setdefault("current_chapter", 0)
  123. state["progress"].setdefault("total_words", 0)
  124. state["progress"].setdefault("last_updated", datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
  125. state["progress"].setdefault("volumes_completed", [])
  126. state["progress"].setdefault("current_volume", 1)
  127. state["progress"].setdefault("volumes_planned", [])
  128. # protagonist schema evolution
  129. ps = state["protagonist_state"]
  130. ps.setdefault("name", "")
  131. ps.setdefault("power", {"realm": "", "layer": 1, "bottleneck": ""})
  132. ps.setdefault("location", {"current": "", "last_chapter": 0})
  133. ps.setdefault("golden_finger", {"name": "", "level": 1, "cooldown": 0, "skills": []})
  134. ps.setdefault("attributes", {})
  135. return state
  136. def _build_master_outline(target_chapters: int, *, chapters_per_volume: int = 50) -> str:
  137. volumes = (target_chapters - 1) // chapters_per_volume + 1 if target_chapters > 0 else 1
  138. lines: list[str] = [
  139. "# 总纲",
  140. "",
  141. "> 本文件为“总纲骨架”,用于 /webnovel-plan 细化为卷大纲与章纲。",
  142. "",
  143. "## 卷结构",
  144. "",
  145. ]
  146. for v in range(1, volumes + 1):
  147. start = (v - 1) * chapters_per_volume + 1
  148. end = min(v * chapters_per_volume, target_chapters)
  149. lines.extend(
  150. [
  151. f"### 第{v}卷(第{start}-{end}章)",
  152. "- 核心冲突:",
  153. "- 关键爽点:",
  154. "- 卷末高潮:",
  155. "- 主要登场角色:",
  156. "- 关键伏笔(埋/收):",
  157. "",
  158. ]
  159. )
  160. return "\n".join(lines).rstrip() + "\n"
  161. def _inject_volume_rows(template_text: str, target_chapters: int, *, chapters_per_volume: int = 50) -> str:
  162. """在总纲模板的卷表中注入卷行(若存在表头)。"""
  163. lines = template_text.splitlines()
  164. header_idx = None
  165. for i, line in enumerate(lines):
  166. if line.strip().startswith("| 卷号"):
  167. header_idx = i
  168. break
  169. if header_idx is None:
  170. return template_text
  171. insert_idx = header_idx + 2 if header_idx + 1 < len(lines) else len(lines)
  172. volumes = (target_chapters - 1) // chapters_per_volume + 1 if target_chapters > 0 else 1
  173. rows = []
  174. for v in range(1, volumes + 1):
  175. start = (v - 1) * chapters_per_volume + 1
  176. end = min(v * chapters_per_volume, target_chapters)
  177. rows.append(f"| {v} | | 第{start}-{end}章 | | |")
  178. # 避免重复插入(若模板已有数据行)
  179. existing = {line.strip() for line in lines}
  180. rows = [r for r in rows if r.strip() not in existing]
  181. return "\n".join(lines[:insert_idx] + rows + lines[insert_idx:])
  182. def init_project(
  183. project_dir: str,
  184. title: str,
  185. genre: str,
  186. *,
  187. protagonist_name: str = "",
  188. target_words: int = 2_000_000,
  189. target_chapters: int = 600,
  190. golden_finger_name: str = "",
  191. golden_finger_type: str = "",
  192. golden_finger_style: str = "",
  193. core_selling_points: str = "",
  194. protagonist_structure: str = "",
  195. heroine_config: str = "",
  196. heroine_names: str = "",
  197. heroine_role: str = "",
  198. co_protagonists: str = "",
  199. co_protagonist_roles: str = "",
  200. antagonist_tiers: str = "",
  201. world_scale: str = "",
  202. factions: str = "",
  203. power_system_type: str = "",
  204. social_class: str = "",
  205. resource_distribution: str = "",
  206. gf_visibility: str = "",
  207. gf_irreversible_cost: str = "",
  208. protagonist_desire: str = "",
  209. protagonist_flaw: str = "",
  210. protagonist_archetype: str = "",
  211. antagonist_level: str = "",
  212. target_reader: str = "",
  213. platform: str = "",
  214. currency_system: str = "",
  215. currency_exchange: str = "",
  216. sect_hierarchy: str = "",
  217. cultivation_chain: str = "",
  218. cultivation_subtiers: str = "",
  219. ) -> None:
  220. project_path = Path(project_dir).expanduser().resolve()
  221. if ".claude" in project_path.parts:
  222. raise SystemExit("Refusing to initialize a project inside .claude. Choose a different directory.")
  223. project_path.mkdir(parents=True, exist_ok=True)
  224. # 目录结构(同时兼容“卷目录”与后续扩展)
  225. directories = [
  226. ".webnovel/backups",
  227. ".webnovel/archive",
  228. ".webnovel/summaries",
  229. "设定集/角色库/主要角色",
  230. "设定集/角色库/次要角色",
  231. "设定集/角色库/反派角色",
  232. "设定集/物品库",
  233. "设定集/其他设定",
  234. "大纲",
  235. "正文/第1卷",
  236. "审查报告",
  237. ]
  238. for dir_path in directories:
  239. (project_path / dir_path).mkdir(parents=True, exist_ok=True)
  240. # state.json(创建或增量补齐)
  241. state_path = project_path / ".webnovel" / "state.json"
  242. if state_path.exists():
  243. try:
  244. state: Dict[str, Any] = json.loads(state_path.read_text(encoding="utf-8"))
  245. except json.JSONDecodeError:
  246. state = {}
  247. else:
  248. state = {}
  249. state = _ensure_state_schema(state)
  250. created_at = state.get("project_info", {}).get("created_at") or datetime.now().strftime("%Y-%m-%d")
  251. state["project_info"].update(
  252. {
  253. "title": title,
  254. "genre": genre,
  255. "created_at": created_at,
  256. "target_words": int(target_words),
  257. "target_chapters": int(target_chapters),
  258. # 下面字段属于“初始化元信息”,不影响运行时脚本
  259. "golden_finger_name": golden_finger_name,
  260. "golden_finger_type": golden_finger_type,
  261. "golden_finger_style": golden_finger_style,
  262. "core_selling_points": core_selling_points,
  263. "protagonist_structure": protagonist_structure,
  264. "heroine_config": heroine_config,
  265. "heroine_names": heroine_names,
  266. "heroine_role": heroine_role,
  267. "co_protagonists": co_protagonists,
  268. "co_protagonist_roles": co_protagonist_roles,
  269. "antagonist_tiers": antagonist_tiers,
  270. "world_scale": world_scale,
  271. "factions": factions,
  272. "power_system_type": power_system_type,
  273. "social_class": social_class,
  274. "resource_distribution": resource_distribution,
  275. "gf_visibility": gf_visibility,
  276. "gf_irreversible_cost": gf_irreversible_cost,
  277. "target_reader": target_reader,
  278. "platform": platform,
  279. "currency_system": currency_system,
  280. "currency_exchange": currency_exchange,
  281. "sect_hierarchy": sect_hierarchy,
  282. "cultivation_chain": cultivation_chain,
  283. "cultivation_subtiers": cultivation_subtiers,
  284. }
  285. )
  286. if protagonist_name:
  287. state["protagonist_state"]["name"] = protagonist_name
  288. gf_type_norm = (golden_finger_type or "").strip()
  289. if gf_type_norm in {"无", "无金手指", "none"}:
  290. state["protagonist_state"]["golden_finger"]["name"] = "无金手指"
  291. state["protagonist_state"]["golden_finger"]["level"] = 0
  292. state["protagonist_state"]["golden_finger"]["cooldown"] = 0
  293. elif golden_finger_name:
  294. state["protagonist_state"]["golden_finger"]["name"] = golden_finger_name
  295. # 确保 golden_finger 字段存在且可编辑
  296. if not state["protagonist_state"]["golden_finger"].get("name"):
  297. state["protagonist_state"]["golden_finger"]["name"] = "未命名金手指"
  298. state["progress"]["last_updated"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  299. state_path.parent.mkdir(parents=True, exist_ok=True)
  300. # 使用原子化写入(初始化不需要备份旧文件)
  301. atomic_write_json(state_path, state, use_lock=True, backup=False)
  302. # 读取内置模板(可选)
  303. script_dir = Path(__file__).resolve().parent
  304. templates_dir = script_dir.parent / "templates"
  305. output_templates_dir = templates_dir / "output"
  306. genre_key = (genre or "").strip()
  307. genre_keys = [_normalize_genre_key(k) for k in _split_genre_keys(genre_key)]
  308. genre_templates = []
  309. seen = set()
  310. for key in genre_keys:
  311. if not key or key in seen:
  312. continue
  313. seen.add(key)
  314. template_text = _read_text_if_exists(templates_dir / "genres" / f"{key}.md")
  315. if template_text:
  316. genre_templates.append(template_text.strip())
  317. genre_template = "\n\n---\n\n".join(genre_templates)
  318. golden_finger_templates = _read_text_if_exists(templates_dir / "golden-finger-templates.md")
  319. output_worldview = _read_text_if_exists(output_templates_dir / "设定集-世界观.md")
  320. output_power = _read_text_if_exists(output_templates_dir / "设定集-力量体系.md")
  321. output_protagonist = _read_text_if_exists(output_templates_dir / "设定集-主角卡.md")
  322. output_heroine = _read_text_if_exists(output_templates_dir / "设定集-女主卡.md")
  323. output_team = _read_text_if_exists(output_templates_dir / "设定集-主角组.md")
  324. output_golden_finger = _read_text_if_exists(output_templates_dir / "设定集-金手指.md")
  325. output_outline = _read_text_if_exists(output_templates_dir / "大纲-总纲.md")
  326. output_fusion = _read_text_if_exists(output_templates_dir / "复合题材-融合逻辑.md")
  327. output_antagonist = _read_text_if_exists(output_templates_dir / "设定集-反派设计.md")
  328. # 基础文件(只在缺失时生成,避免覆盖已有内容)
  329. now = datetime.now().strftime("%Y-%m-%d")
  330. worldview_content = output_worldview.strip() if output_worldview else ""
  331. if not worldview_content:
  332. worldview_content = "\n".join(
  333. [
  334. "# 世界观",
  335. "",
  336. f"> 项目:{title}|题材:{genre}|创建:{now}",
  337. "",
  338. "## 一句话世界观",
  339. "- (用一句话说明世界的核心规则与卖点)",
  340. "",
  341. "## 核心规则(设定即物理)",
  342. "- 规则1:",
  343. "- 规则2:",
  344. "- 规则3:",
  345. "",
  346. "## 势力与地理(简版)",
  347. "- 主要势力:",
  348. "- 关键地点:",
  349. "",
  350. "## 参考题材模板(可删/可改)",
  351. "",
  352. (genre_template.strip() + "\n") if genre_template else "(未找到对应题材模板,可自行补充)\n",
  353. ]
  354. ).rstrip() + "\n"
  355. else:
  356. worldview_content = _apply_label_replacements(
  357. worldview_content,
  358. {
  359. "大陆/位面数量": world_scale,
  360. "核心势力": factions,
  361. "社会阶层": social_class,
  362. "资源分配规则": resource_distribution,
  363. "宗门/组织层级": sect_hierarchy,
  364. "货币体系": currency_system,
  365. "兑换规则": currency_exchange,
  366. },
  367. )
  368. _write_text_if_missing(
  369. project_path / "设定集" / "世界观.md",
  370. worldview_content,
  371. )
  372. power_content = output_power.strip() if output_power else ""
  373. if not power_content:
  374. power_content = "\n".join(
  375. [
  376. "# 力量体系",
  377. "",
  378. f"> 项目:{title}|题材:{genre}|创建:{now}",
  379. "",
  380. "## 等级/境界划分",
  381. "- (列出从弱到强的等级,含突破条件与代价)",
  382. "",
  383. "## 技能/招式规则",
  384. "- 获得方式:",
  385. "- 成本与副作用:",
  386. "- 进阶与组合:",
  387. "",
  388. "## 禁止事项(防崩坏)",
  389. "- 未达等级不得使用高阶能力(设定即物理)",
  390. "- 新增能力必须申报并入库(发明需申报)",
  391. "",
  392. ]
  393. ).rstrip() + "\n"
  394. else:
  395. power_content = _apply_label_replacements(
  396. power_content,
  397. {
  398. "体系类型": power_system_type,
  399. "典型境界链(可选)": cultivation_chain,
  400. "小境界划分": cultivation_subtiers,
  401. },
  402. )
  403. _write_text_if_missing(
  404. project_path / "设定集" / "力量体系.md",
  405. power_content,
  406. )
  407. protagonist_content = output_protagonist.strip() if output_protagonist else ""
  408. if not protagonist_content:
  409. protagonist_content = "\n".join(
  410. [
  411. "# 主角卡",
  412. "",
  413. f"> 主角:{protagonist_name or '(待填写)'}|项目:{title}|创建:{now}",
  414. "",
  415. "## 三要素",
  416. f"- 欲望:{protagonist_desire or '(待填写)'}",
  417. f"- 弱点:{protagonist_flaw or '(待填写)'}",
  418. f"- 人设类型:{protagonist_archetype or '(待填写)'}",
  419. "",
  420. "## 初始状态(开局)",
  421. "- 身份:",
  422. "- 资源:",
  423. "- 约束:",
  424. "",
  425. "## 金手指概览",
  426. f"- 称呼:{golden_finger_name or '(待填写)'}",
  427. f"- 类型:{golden_finger_type or '(待填写)'}",
  428. f"- 风格:{golden_finger_style or '(待填写)'}",
  429. "- 成长曲线:",
  430. "",
  431. ]
  432. ).rstrip() + "\n"
  433. else:
  434. protagonist_content = _apply_label_replacements(
  435. protagonist_content,
  436. {
  437. "姓名": protagonist_name,
  438. "真正渴望(可能不自知)": protagonist_desire,
  439. "性格缺陷": protagonist_flaw,
  440. },
  441. )
  442. _write_text_if_missing(
  443. project_path / "设定集" / "主角卡.md",
  444. protagonist_content,
  445. )
  446. heroine_content = output_heroine.strip() if output_heroine else ""
  447. if heroine_content:
  448. heroine_content = _apply_label_replacements(
  449. heroine_content,
  450. {
  451. "姓名": heroine_names,
  452. "与主角关系定位(对手/盟友/共谋/牵制)": heroine_role,
  453. },
  454. )
  455. _write_text_if_missing(project_path / "设定集" / "女主卡.md", heroine_content)
  456. team_content = output_team.strip() if output_team else ""
  457. if team_content:
  458. names = [n.strip() for n in co_protagonists.split(",") if n.strip()] if co_protagonists else []
  459. roles = [r.strip() for r in co_protagonist_roles.split(",") if r.strip()] if co_protagonist_roles else []
  460. if names:
  461. lines = team_content.splitlines()
  462. new_rows = _render_team_rows(names, roles)
  463. replaced = False
  464. out_lines: List[str] = []
  465. for line in lines:
  466. if line.strip().startswith("| 主角A"):
  467. out_lines.extend(new_rows)
  468. replaced = True
  469. continue
  470. if replaced and line.strip().startswith("| 主角"):
  471. continue
  472. out_lines.append(line)
  473. team_content = "\n".join(out_lines)
  474. _write_text_if_missing(
  475. project_path / "设定集" / "主角组.md",
  476. team_content,
  477. )
  478. golden_finger_content = output_golden_finger.strip() if output_golden_finger else ""
  479. if not golden_finger_content:
  480. golden_finger_content = "\n".join(
  481. [
  482. "# 金手指设计",
  483. "",
  484. f"> 项目:{title}|题材:{genre}|创建:{now}",
  485. "",
  486. "## 选型",
  487. f"- 称呼:{golden_finger_name or '(待填写)'}",
  488. f"- 类型:{golden_finger_type or '(待填写)'}",
  489. f"- 风格:{golden_finger_style or '(待填写)'}",
  490. "",
  491. "## 规则(必须写清)",
  492. "- 触发条件:",
  493. "- 冷却/代价:",
  494. "- 上限:",
  495. "- 反噬/风险:",
  496. "",
  497. "## 成长曲线(章节规划)",
  498. "- Lv1:",
  499. "- Lv2:",
  500. "- Lv3:",
  501. "",
  502. "## 模板参考(可删/可改)",
  503. "",
  504. (golden_finger_templates.strip() + "\n") if golden_finger_templates else "(未找到金手指模板库)\n",
  505. ]
  506. ).rstrip() + "\n"
  507. else:
  508. golden_finger_content = _apply_label_replacements(
  509. golden_finger_content,
  510. {
  511. "类型": golden_finger_type,
  512. "读者可见度": gf_visibility,
  513. "不可逆代价": gf_irreversible_cost,
  514. },
  515. )
  516. _write_text_if_missing(
  517. project_path / "设定集" / "金手指设计.md",
  518. golden_finger_content,
  519. )
  520. fusion_content = output_fusion.strip() if output_fusion else ""
  521. if fusion_content:
  522. _write_text_if_missing(
  523. project_path / "设定集" / "复合题材-融合逻辑.md",
  524. fusion_content,
  525. )
  526. antagonist_content = output_antagonist.strip() if output_antagonist else ""
  527. if not antagonist_content:
  528. antagonist_content = "\n".join(
  529. [
  530. "# 反派设计",
  531. "",
  532. f"> 项目:{title}|创建:{now}",
  533. "",
  534. f"- 反派等级:{antagonist_level or '(待填写)'}",
  535. "- 动机:",
  536. "- 资源/势力:",
  537. "- 与主角的镜像关系:",
  538. "- 终局:",
  539. "",
  540. ]
  541. ).rstrip() + "\n"
  542. else:
  543. tier_map = _parse_tier_map(antagonist_tiers)
  544. if tier_map:
  545. lines = antagonist_content.splitlines()
  546. out_lines = []
  547. for line in lines:
  548. if line.strip().startswith("| 小反派"):
  549. name = tier_map.get("小反派", "")
  550. out_lines.append(f"| 小反派 | {name} | 前期 | | |")
  551. continue
  552. if line.strip().startswith("| 中反派"):
  553. name = tier_map.get("中反派", "")
  554. out_lines.append(f"| 中反派 | {name} | 中期 | | |")
  555. continue
  556. if line.strip().startswith("| 大反派"):
  557. name = tier_map.get("大反派", "")
  558. out_lines.append(f"| 大反派 | {name} | 后期 | | |")
  559. continue
  560. out_lines.append(line)
  561. antagonist_content = "\n".join(out_lines)
  562. _write_text_if_missing(project_path / "设定集" / "反派设计.md", antagonist_content)
  563. outline_content = output_outline.strip() if output_outline else ""
  564. if outline_content:
  565. outline_content = _inject_volume_rows(outline_content, int(target_chapters)).rstrip() + "\n"
  566. else:
  567. outline_content = _build_master_outline(int(target_chapters))
  568. _write_text_if_missing(project_path / "大纲" / "总纲.md", outline_content)
  569. _write_text_if_missing(
  570. project_path / "大纲" / "爽点规划.md",
  571. "\n".join(
  572. [
  573. "# 爽点规划",
  574. "",
  575. f"> 项目:{title}|题材:{genre}|创建:{now}",
  576. "",
  577. "## 核心卖点(来自初始化输入)",
  578. f"- {core_selling_points or '(待填写,建议 1-3 条,用逗号分隔)'}",
  579. "",
  580. "## 密度目标(建议)",
  581. "- 每章至少 1 个小爽点",
  582. "- 每 5 章至少 1 个大爽点",
  583. "",
  584. "## 分布表(示例,可改)",
  585. "",
  586. "| 章节范围 | 主导爽点类型 | 备注 |",
  587. "|---|---|---|",
  588. "| 1-5 | 金手指/打脸/反转 | 开篇钩子 + 立人设 |",
  589. "| 6-10 | 升级/收获 | 进入主线节奏 |",
  590. "",
  591. ]
  592. ),
  593. )
  594. # Git 初始化(仅当项目目录内尚无 .git 且 Git 可用)
  595. git_dir = project_path / ".git"
  596. if not git_dir.exists():
  597. if not is_git_available():
  598. print("\n⚠️ Git 不可用,跳过版本控制初始化")
  599. print("💡 如需启用 Git 版本控制,请安装 Git: https://git-scm.com/")
  600. else:
  601. print("\nInitializing Git repository...")
  602. try:
  603. subprocess.run(["git", "init"], cwd=project_path, check=True, capture_output=True, text=True)
  604. gitignore_file = project_path / ".gitignore"
  605. if not gitignore_file.exists():
  606. gitignore_file.write_text(
  607. """# Python
  608. __pycache__/
  609. *.py[cod]
  610. *.so
  611. # Temporary files
  612. *.tmp
  613. *.bak
  614. .DS_Store
  615. # IDE
  616. .vscode/
  617. .idea/
  618. # Don't ignore .webnovel (we need to track state.json)
  619. # But ignore cache files
  620. .webnovel/context_cache.json
  621. .webnovel/*.lock
  622. .webnovel/*.bak
  623. """,
  624. encoding="utf-8",
  625. )
  626. subprocess.run(["git", "add", "."], cwd=project_path, check=True, capture_output=True)
  627. # 安全修复:清理 title 防止命令注入
  628. safe_title = sanitize_commit_message(title)
  629. subprocess.run(
  630. ["git", "commit", "-m", f"初始化网文项目:{safe_title}"],
  631. cwd=project_path,
  632. check=True,
  633. capture_output=True,
  634. )
  635. print("Git initialized.")
  636. except subprocess.CalledProcessError as e:
  637. print(f"Git init failed (non-fatal): {e}")
  638. print(f"\nProject initialized at: {project_path}")
  639. print("Key files:")
  640. print(" - .webnovel/state.json")
  641. print(" - 设定集/世界观.md")
  642. print(" - 设定集/力量体系.md")
  643. print(" - 设定集/主角卡.md")
  644. print(" - 设定集/金手指设计.md")
  645. print(" - 大纲/总纲.md")
  646. print(" - 大纲/爽点规划.md")
  647. def main() -> None:
  648. parser = argparse.ArgumentParser(description="网文项目初始化脚本(生成项目结构 + state.json + 基础模板)")
  649. parser.add_argument("project_dir", help="项目目录(建议 ./webnovel-project)")
  650. parser.add_argument("title", help="小说标题")
  651. parser.add_argument(
  652. "genre",
  653. help="题材类型(可用“+”组合,如:都市脑洞+规则怪谈;示例:修仙/系统流/都市异能/古言/现实题材)",
  654. )
  655. parser.add_argument("--protagonist-name", default="", help="主角姓名")
  656. parser.add_argument("--target-words", type=int, default=2_000_000, help="目标总字数(默认 2000000)")
  657. parser.add_argument("--target-chapters", type=int, default=600, help="目标总章节数(默认 600)")
  658. parser.add_argument("--golden-finger-name", default="", help="金手指称呼/系统名(建议读者可见的代号)")
  659. parser.add_argument("--golden-finger-type", default="", help="金手指类型(如 系统流/鉴定流/签到流)")
  660. parser.add_argument("--golden-finger-style", default="", help="金手指风格(如 冷漠工具型/毒舌吐槽型)")
  661. parser.add_argument("--core-selling-points", default="", help="核心卖点(逗号分隔)")
  662. parser.add_argument("--protagonist-structure", default="", help="主角结构(单主角/多主角)")
  663. parser.add_argument("--heroine-config", default="", help="女主配置(无女主/单女主/多女主)")
  664. parser.add_argument("--heroine-names", default="", help="女主姓名(多个用逗号分隔)")
  665. parser.add_argument("--heroine-role", default="", help="女主定位(事业线/情感线/对抗线)")
  666. parser.add_argument("--co-protagonists", default="", help="多主角姓名(逗号分隔)")
  667. parser.add_argument("--co-protagonist-roles", default="", help="多主角定位(逗号分隔)")
  668. parser.add_argument("--antagonist-tiers", default="", help="反派分层(如 小反派:张三;中反派:李四;大反派:王五)")
  669. parser.add_argument("--world-scale", default="", help="世界规模")
  670. parser.add_argument("--factions", default="", help="势力格局/核心势力")
  671. parser.add_argument("--power-system-type", default="", help="力量体系类型")
  672. parser.add_argument("--social-class", default="", help="社会阶层")
  673. parser.add_argument("--resource-distribution", default="", help="资源分配")
  674. parser.add_argument("--gf-visibility", default="", help="金手指可见度(明牌/半明牌/暗牌)")
  675. parser.add_argument("--gf-irreversible-cost", default="", help="金手指不可逆代价")
  676. parser.add_argument("--currency-system", default="", help="货币体系")
  677. parser.add_argument("--currency-exchange", default="", help="货币兑换/面值规则")
  678. parser.add_argument("--sect-hierarchy", default="", help="宗门/组织层级")
  679. parser.add_argument("--cultivation-chain", default="", help="典型境界链")
  680. parser.add_argument("--cultivation-subtiers", default="", help="小境界划分(初/中/后/巅 等)")
  681. # 深度模式可选参数(用于预填模板)
  682. parser.add_argument("--protagonist-desire", default="", help="主角核心欲望(深度模式)")
  683. parser.add_argument("--protagonist-flaw", default="", help="主角性格弱点(深度模式)")
  684. parser.add_argument("--protagonist-archetype", default="", help="主角人设类型(深度模式)")
  685. parser.add_argument("--antagonist-level", default="", help="反派等级(深度模式)")
  686. parser.add_argument("--target-reader", default="", help="目标读者(深度模式)")
  687. parser.add_argument("--platform", default="", help="发布平台(深度模式)")
  688. args = parser.parse_args()
  689. init_project(
  690. args.project_dir,
  691. args.title,
  692. args.genre,
  693. protagonist_name=args.protagonist_name,
  694. target_words=args.target_words,
  695. target_chapters=args.target_chapters,
  696. golden_finger_name=args.golden_finger_name,
  697. golden_finger_type=args.golden_finger_type,
  698. golden_finger_style=args.golden_finger_style,
  699. core_selling_points=args.core_selling_points,
  700. protagonist_structure=args.protagonist_structure,
  701. heroine_config=args.heroine_config,
  702. heroine_names=args.heroine_names,
  703. heroine_role=args.heroine_role,
  704. co_protagonists=args.co_protagonists,
  705. co_protagonist_roles=args.co_protagonist_roles,
  706. antagonist_tiers=args.antagonist_tiers,
  707. world_scale=args.world_scale,
  708. factions=args.factions,
  709. power_system_type=args.power_system_type,
  710. social_class=args.social_class,
  711. resource_distribution=args.resource_distribution,
  712. gf_visibility=args.gf_visibility,
  713. gf_irreversible_cost=args.gf_irreversible_cost,
  714. protagonist_desire=args.protagonist_desire,
  715. protagonist_flaw=args.protagonist_flaw,
  716. protagonist_archetype=args.protagonist_archetype,
  717. antagonist_level=args.antagonist_level,
  718. target_reader=args.target_reader,
  719. platform=args.platform,
  720. currency_system=args.currency_system,
  721. currency_exchange=args.currency_exchange,
  722. sect_hierarchy=args.sect_hierarchy,
  723. cultivation_chain=args.cultivation_chain,
  724. cultivation_subtiers=args.cultivation_subtiers,
  725. )
  726. if __name__ == "__main__":
  727. main()