novel-utils.ts 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059
  1. /**
  2. * 小说上下文管理器 - 工具函数
  3. * Novel Context Manager - Utility Functions
  4. *
  5. * 注意:此文件为客户端/服务端通用工具,不可包含 Node.js 专用模块(如 fs、path)。
  6. * 服务端专用函数请放在 novel-server-utils.ts。
  7. */
  8. import { WorldStateFile, WorldLoreFile, NarrativeHook, NovelType } from './novel-types';
  9. // ─────────────────────────────────────────────
  10. // 1. generateChapterFileName
  11. // ─────────────────────────────────────────────
  12. /**
  13. * 生成章节文件名
  14. * @example generateChapterFileName(1, "序章") → "chapter_1_序章.md"
  15. */
  16. export function generateChapterFileName(index: number, title: string): string {
  17. // Sanitize title: remove characters illegal in Windows/Unix filenames
  18. const safe = title
  19. .replace(/[<>:"/\\|?*\x00-\x1f]/g, '-')
  20. .replace(/-+/g, '-')
  21. .replace(/^-|-$/g, '')
  22. .trim()
  23. || 'untitled';
  24. return `chapter_${index}_${safe}.md`;
  25. }
  26. // ─────────────────────────────────────────────
  27. // 2. isValidProject (已移至 novel-server-utils.ts)
  28. // ─────────────────────────────────────────────
  29. // isValidProject 使用 Node.js fs 模块,仅可在服务端使用。
  30. // 请从 '@/app/lib/novel-server-utils' 导入。
  31. // ─────────────────────────────────────────────
  32. // 3. buildContextFormat
  33. // ─────────────────────────────────────────────
  34. /**
  35. * 将世界状态文件、世界观设定文件、近期章节拼接为 AI 上下文提示词前缀,
  36. * 最后追加用户原始 instruction(保持不变)。
  37. *
  38. * 跳过 enabled: false 或 content 为空的文件。
  39. */
  40. export function buildContextFormat(
  41. worldStateFiles: WorldStateFile[],
  42. loreFiles: WorldLoreFile[],
  43. instruction: string,
  44. recentChapters?: string[]
  45. ): string {
  46. const parts: string[] = [];
  47. // 世界状态文件
  48. const enabledWorldState = worldStateFiles.filter((f) => f.enabled && f.content.trim() !== '');
  49. if (enabledWorldState.length > 0) {
  50. parts.push('[小说世界状态]');
  51. for (const file of enabledWorldState) {
  52. parts.push(`=== ${file.fileName} ===`);
  53. parts.push(file.content);
  54. }
  55. }
  56. // 世界观设定文件
  57. const enabledLore = loreFiles.filter((f) => f.enabled && f.content.trim() !== '');
  58. if (enabledLore.length > 0) {
  59. parts.push('[世界观设定]');
  60. for (const file of enabledLore) {
  61. parts.push(`=== ${file.fileName} ===`);
  62. parts.push(file.content);
  63. }
  64. }
  65. // 上下文结束标记(只有在有世界状态或世界观内容时才添加)
  66. if (enabledWorldState.length > 0 || enabledLore.length > 0) {
  67. parts.push('[上下文结束]');
  68. }
  69. // 近期章节
  70. if (recentChapters && recentChapters.length > 0) {
  71. parts.push('[近期章节]');
  72. recentChapters.forEach((chapterContent, i) => {
  73. parts.push(`=== 第${i + 1}章 ===`);
  74. parts.push(chapterContent);
  75. });
  76. parts.push('[近期章节结束]');
  77. }
  78. // 用户原始指令(保持不变)
  79. parts.push(instruction);
  80. return parts.join('\n');
  81. }
  82. // ─────────────────────────────────────────────
  83. // 4. getDefaultTemplate
  84. // ─────────────────────────────────────────────
  85. /**
  86. * 根据文件名返回对应的默认模板内容(中文字段说明)
  87. */
  88. export function getDefaultTemplate(fileName: string): string {
  89. switch (fileName) {
  90. case 'plot_summary.md':
  91. return `# 剧情梗概
  92. > **文件说明**:本文件**仅**记录已发生的剧情梗概,按章节/时间线整理。
  93. > **禁止写入**:角色属性数值、物品道具、好感度数值、世界观设定——这些内容属于其他专属文件。
  94. ## 当前章节
  95. (当前正在写第几章,简要说明本章主要事件)
  96. ## 时间线
  97. - 第1章:(事件摘要)
  98. - 第2章:(事件摘要)
  99. ## 主线剧情
  100. (主线故事的核心走向与目标)
  101. ## 近期剧情节点
  102. (最近几章的关键转折点或重要事件)
  103. ## 待解决的剧情线索
  104. (尚未展开或需要后续推进的剧情)
  105. `;
  106. case 'protagonist_stats.md':
  107. return `# 主角属性
  108. > **文件说明**:本文件**仅**记录主角的可量化属性数值、当前位置和故事内时间。
  109. > **禁止写入**:剧情事件、物品道具、角色关系、好感度——这些内容属于其他专属文件。
  110. ## 基本信息
  111. - **姓名**:(主角姓名)
  112. - **年龄**:(当前年龄)
  113. - **境界/等级**:(修炼境界或能力等级)
  114. ## 生命值与状态
  115. - **生命值**:(当前/最大)
  116. - **精神力**:(当前/最大)
  117. - **当前状态**:(正常/受伤/特殊状态)
  118. ## 位置与时间
  119. - **当前位置**:(所在地点)
  120. - **当前时间**:(故事内时间,如"第三年春季")
  121. ## 核心能力
  122. - (能力1):(简要说明)
  123. - (能力2):(简要说明)
  124. ## 成长记录
  125. (近期获得的重要能力或突破)
  126. `;
  127. case 'protagonist_inventory.md':
  128. return `# 物品栏
  129. > **文件说明**:本文件**仅**记录主角持有的道具、装备及数量。
  130. > **禁止写入**:属性数值、剧情事件、角色关系——这些内容属于其他专属文件。
  131. ## 随身携带
  132. | 物品名称 | 数量 | 说明 |
  133. |---------|------|------|
  134. | (物品名) | 1 | (用途/来源) |
  135. ## 储物空间
  136. | 物品名称 | 数量 | 说明 |
  137. |---------|------|------|
  138. | (物品名) | 1 | (用途/来源) |
  139. ## 重要道具
  140. (对剧情有重要意义的特殊物品)
  141. ## 货币/资源
  142. - **金币/灵石**:(数量)
  143. - **其他资源**:(说明)
  144. `;
  145. case 'character_relations.md':
  146. return `# 角色关系
  147. > **文件说明**:本文件**仅**记录角色间的关系类型和好感度数值。
  148. > **禁止写入**:角色背景故事、剧情事件、属性数值——角色背景请写入 lore/characters_lore.md,剧情请写入 plot_summary.md。
  149. ## 主要角色
  150. ### (角色名1)
  151. - **关系**:(与主角的关系,如"师父"、"挚友")
  152. - **当前状态**:(在世/离开/未知)
  153. - **当前位置**:(所在地)
  154. - **关系备注**:(近期互动或关系变化)
  155. ### (角色名2)
  156. - **关系**:(与主角的关系)
  157. - **当前状态**:(在世/离开/未知)
  158. - **当前位置**:(所在地)
  159. - **关系备注**:(近期互动或关系变化)
  160. ## 敌对势力
  161. (与主角存在冲突的角色或组织)
  162. ## 关系变化记录
  163. (近期发生的重要关系变化)
  164. `;
  165. case 'world_notes.md':
  166. return `# 世界笔记
  167. > **文件说明**:本文件**仅**记录不属于其他专属文件的可量化世界信息(如阵营声望值、资源储量、特殊规则数值等)。
  168. > **禁止写入**:剧情梗概、主角属性、物品栏、角色关系、地理/历史/文化/经济设定——这些均有专属文件。
  169. ## 重要地点
  170. - **(地点名)**:(简要描述,当前剧情意义)
  171. ## 重要事件记录
  172. (已发生的对世界观有影响的重大事件)
  173. ## 规则与设定备忘
  174. (需要保持一致的世界规则,如修炼体系、魔法规则等)
  175. ## 未解之谜
  176. (故事中尚未揭晓的谜题或伏笔)
  177. ## 其他备注
  178. (其他需要记录的世界细节)
  179. `;
  180. case 'narrative_hooks.json':
  181. return JSON.stringify({ version: 1, hooks: [] }, null, 2);
  182. case 'writing_style.md':
  183. return `# 文风档案
  184. > **文件说明**:本文件**仅**记录叙事风格、语言特点、节奏偏好等文风设定,由初始化向导生成后仅供用户手动修改。
  185. > **禁止写入**:剧情内容、角色属性、世界设定——这些内容属于其他专属文件。
  186. ## 整体风格
  187. (描述本作品的整体写作风格,如"硬核爽文"、"细腻情感流"、"幽默轻松"等)
  188. ## 叙事视角
  189. (第一人称/第三人称限知/第三人称全知)
  190. ## 语言特点
  191. - **句式风格**:(长句为主/短句为主/长短结合)
  192. - **用词偏好**:(文言色彩/现代口语/专业术语)
  193. - **描写侧重**:(动作/心理/环境/对话)
  194. ## 节奏控制
  195. (快节奏爽文/慢热情感/张弛有度)
  196. ## 禁忌与避免
  197. (需要避免的写作习惯或表达方式)
  198. ## 参考风格
  199. (参考的作品或作者风格)
  200. `;
  201. case 'worldbuilding.md':
  202. return `# 世界观总纲
  203. > **文件说明**:本文件**仅**记录宇宙规则、力量体系、世界起源等核心设定。
  204. > **禁止写入**:具体地理信息、历史事件、势力详情、人物志——这些均有专属文件(geography.md / history.md / factions.md / characters_lore.md)。
  205. ## 世界背景
  206. (本故事发生的世界基本设定,时代背景,世界类型)
  207. ## 核心规则
  208. (这个世界运行的基本规律,如修炼体系、魔法体系、科技水平等)
  209. ## 世界格局
  210. (当前世界的政治、势力格局概览)
  211. ## 特殊设定
  212. (区别于其他作品的独特世界观元素)
  213. ## 世界历史简述
  214. (影响当前故事的重要历史背景)
  215. `;
  216. case 'geography.md':
  217. return `# 地理信息
  218. > **文件说明**:本文件**仅**记录大陆/区域地图描述、地名、地形、气候。
  219. > **禁止写入**:历史事件、势力关系、人物信息、文化习俗——这些均有专属文件。
  220. ## 主要地区
  221. ### (地区名1)
  222. - **类型**:(城市/山脉/秘境/国家等)
  223. - **特点**:(地理特征、气候、资源)
  224. - **剧情意义**:(在故事中的作用)
  225. ### (地区名2)
  226. - **类型**:(城市/山脉/秘境/国家等)
  227. - **特点**:(地理特征、气候、资源)
  228. - **剧情意义**:(在故事中的作用)
  229. ## 主角活动范围
  230. (主角目前活动的主要地理区域)
  231. ## 地理关系
  232. (各地区之间的距离、交通方式、政治关系)
  233. `;
  234. case 'factions.md':
  235. return `# 势力组织
  236. > **文件说明**:本文件**仅**记录势力与组织的基本信息、势力范围及相互关系。
  237. > **禁止写入**:具体历史事件、个人人物志、地理描述——历史请写入 history.md,人物请写入 characters_lore.md。
  238. ## 主要势力
  239. ### (势力名1)
  240. - **类型**:(门派/国家/商会/秘密组织等)
  241. - **实力**:(强/中/弱,或具体描述)
  242. - **立场**:(友好/中立/敌对)
  243. - **核心人物**:(领导者或关键人物)
  244. - **与主角关系**:(当前关系状态)
  245. ### (势力名2)
  246. - **类型**:(门派/国家/商会/秘密组织等)
  247. - **实力**:(强/中/弱,或具体描述)
  248. - **立场**:(友好/中立/敌对)
  249. - **核心人物**:(领导者或关键人物)
  250. - **与主角关系**:(当前关系状态)
  251. ## 势力格局变化
  252. (近期发生的势力变动或冲突)
  253. `;
  254. case 'history.md':
  255. return `# 历史事件
  256. > **文件说明**:本文件**仅**记录重大历史事件、传说、古代战争和时间线。
  257. > **禁止写入**:当前剧情进展、现存势力的实时状态、主角当前属性——当前剧情请写入 plot_summary.md。
  258. ## 世界重大历史
  259. ### (历史事件1)
  260. - **时间**:(故事内时间或"N年前")
  261. - **事件**:(简要描述)
  262. - **影响**:(对当前世界的影响)
  263. ### (历史事件2)
  264. - **时间**:(故事内时间或"N年前")
  265. - **事件**:(简要描述)
  266. - **影响**:(对当前世界的影响)
  267. ## 主角相关历史
  268. (主角的过去经历中对当前剧情有影响的事件)
  269. ## 未解历史谜题
  270. (故事中涉及但尚未揭晓的历史秘密)
  271. `;
  272. case 'culture.md':
  273. return `# 文化习俗
  274. > **文件说明**:本文件**仅**记录礼仪、节日、宗教信仰、社会阶层等文化习俗。
  275. > **禁止写入**:地理信息、历史事件、经济数据、势力关系——这些均有专属文件。
  276. ## 社会结构
  277. (社会阶层划分,等级制度)
  278. ## 主要文化习俗
  279. - **(习俗名1)**:(描述及意义)
  280. - **(习俗名2)**:(描述及意义)
  281. ## 宗教与信仰
  282. (主要宗教或信仰体系)
  283. ## 禁忌
  284. (社会禁忌,违反的后果)
  285. ## 节日与仪式
  286. (重要节日或仪式,对剧情可能有影响的)
  287. ## 语言与文字
  288. (世界中的语言体系,是否有特殊文字)
  289. `;
  290. case 'economy.md':
  291. return `# 经济体系
  292. > **文件说明**:本文件**仅**记录货币体系、贸易规则、资源分布和物价参考。
  293. > **禁止写入**:势力政治关系、文化习俗、地理描述——这些均有专属文件。
  294. ## 货币体系
  295. (流通货币种类,换算关系)
  296. ## 主要产业
  297. - **(产业名1)**:(描述)
  298. - **(产业名2)**:(描述)
  299. ## 贸易体系
  300. (主要贸易路线,重要商品)
  301. ## 经济格局
  302. (各势力的经济实力对比)
  303. ## 特殊资源
  304. (世界中的稀缺资源,对修炼/战斗有重要意义的物资)
  305. ## 主角经济状况
  306. (主角当前的财富状况和经济来源)
  307. `;
  308. case 'characters_lore.md':
  309. return `# 重要人物志
  310. > **文件说明**:本文件**仅**记录非主角的重要 NPC 背景、身份和势力归属。
  311. > **禁止写入**:主角属性数值(写入 protagonist_stats.md)、角色好感度数值(写入 character_relations.md)、剧情梗概(写入 plot_summary.md)。
  312. ## 重要配角
  313. ### (配角姓名1)
  314. - **身份**:(在故事中的角色定位)
  315. - **背景**:(简要背景)
  316. - **性格**:(核心性格特点)
  317. - **势力归属**:(所属门派/国家/组织)
  318. - **隐藏信息**:(读者/主角尚不知晓的信息)
  319. ### (配角姓名2)
  320. - **身份**:(在故事中的角色定位)
  321. - **背景**:(简要背景)
  322. - **性格**:(核心性格特点)
  323. - **势力归属**:(所属门派/国家/组织)
  324. - **隐藏信息**:(读者/主角尚不知晓的信息)
  325. ## 反派/对立角色
  326. ### (反派姓名)
  327. - **身份**:(在故事中的角色定位)
  328. - **动机**:(行动的核心动机)
  329. - **能力**:(主要能力或威胁)
  330. - **弱点**:(已知或潜在弱点)
  331. `;
  332. default:
  333. return `# ${fileName}\n\n(暂无内容)\n`;
  334. }
  335. }
  336. // ─────────────────────────────────────────────
  337. // 4b. getNovelTypeTemplate
  338. // ─────────────────────────────────────────────
  339. /**
  340. * 根据小说类型返回差异化的 protagonist_stats.md 初始模板内容。
  341. * 需求 1.10:选择 Novel_Type 后,将对应默认属性值填充到相关 World_State_File。
  342. */
  343. export function getNovelTypeTemplate(novelType: NovelType): Record<string, string> {
  344. const templates: Record<string, string> = {};
  345. switch (novelType) {
  346. case 'xianxia':
  347. templates['protagonist_stats.md'] = `# 主角属性
  348. > **文件说明**:本文件**仅**记录主角的可量化属性数值、当前位置和故事内时间。
  349. > **禁止写入**:剧情事件、物品道具、角色关系、好感度——这些内容属于其他专属文件。
  350. ## 基本信息
  351. - **姓名**:(主角姓名)
  352. - **年龄**:(当前年龄)
  353. - **境界**:炼气期一层
  354. - **修炼功法**:(功法名称)
  355. ## 修炼资质
  356. - **灵根**:(单灵根/双灵根/杂灵根)
  357. - **灵根属性**:(金/木/水/火/土)
  358. - **天赋评级**:(上/中/下)
  359. ## 生命值与状态
  360. - **生命值**:100/100
  361. - **灵力**:50/50
  362. - **当前状态**:正常
  363. ## 位置与时间
  364. - **当前位置**:(所在地点)
  365. - **当前时间**:(故事内时间,如"天历三年春")
  366. ## 核心能力
  367. - (技能1):(简要说明)
  368. - (技能2):(简要说明)
  369. ## 成长记录
  370. (近期获得的重要能力或突破)
  371. `;
  372. templates['protagonist_inventory.md'] = `# 物品栏
  373. > **文件说明**:本文件**仅**记录主角持有的道具、装备及数量。
  374. > **禁止写入**:属性数值、剧情事件、角色关系——这些内容属于其他专属文件。
  375. ## 随身携带
  376. | 物品名称 | 数量 | 说明 |
  377. |---------|------|------|
  378. | 低阶灵石 | 10 | 修炼用 |
  379. | 聚气丹 | 3 | 辅助修炼 |
  380. ## 储物袋
  381. | 物品名称 | 数量 | 说明 |
  382. |---------|------|------|
  383. | (物品名) | 1 | (用途/来源) |
  384. ## 重要道具
  385. (对剧情有重要意义的特殊法宝或道具)
  386. ## 货币/资源
  387. - **灵石**:10 块(低阶)
  388. - **其他资源**:(说明)
  389. `;
  390. break;
  391. case 'farming':
  392. templates['protagonist_stats.md'] = `# 主角属性
  393. > **文件说明**:本文件**仅**记录主角的可量化属性数值、当前位置和故事内时间。
  394. > **禁止写入**:剧情事件、物品道具、角色关系、好感度——这些内容属于其他专属文件。
  395. ## 基本信息
  396. - **姓名**:(主角姓名)
  397. - **年龄**:(当前年龄)
  398. - **职业**:农场主
  399. - **所在地**:(农场/村庄名称)
  400. ## 农场状态
  401. - **农场等级**:1 级
  402. - **土地面积**:5 亩
  403. - **作物槽位**:10 个
  404. - **牲畜槽位**:5 个
  405. ## 技能
  406. - **种植**:Lv.1
  407. - **烹饪**:Lv.1
  408. - **手工**:Lv.1
  409. ## 生命值与状态
  410. - **体力**:100/100
  411. - **心情**:愉快
  412. - **当前状态**:正常
  413. ## 位置与时间
  414. - **当前位置**:(所在地点)
  415. - **当前时间**:(故事内时间,如"第一年春季")
  416. ## 成长记录
  417. (近期解锁的技能或农场升级记录)
  418. `;
  419. templates['protagonist_inventory.md'] = `# 物品栏
  420. > **文件说明**:本文件**仅**记录主角持有的道具、装备及数量。
  421. > **禁止写入**:属性数值、剧情事件、角色关系——这些内容属于其他专属文件。
  422. ## 随身携带
  423. | 物品名称 | 数量 | 说明 |
  424. |---------|------|------|
  425. | 种子袋 | 1 | 含各类蔬菜种子 |
  426. | 农具 | 1 套 | 锄头、镰刀等 |
  427. ## 仓库
  428. | 物品名称 | 数量 | 说明 |
  429. |---------|------|------|
  430. | 小麦 | 50 斤 | 自产 |
  431. | 蔬菜 | 20 斤 | 自产 |
  432. ## 重要道具
  433. (对剧情有重要意义的特殊物品)
  434. ## 货币/资源
  435. - **金币**:100
  436. - **木材**:20 根
  437. - **石料**:10 块
  438. `;
  439. break;
  440. case 'survival':
  441. templates['protagonist_stats.md'] = `# 主角属性
  442. > **文件说明**:本文件**仅**记录主角的可量化属性数值、当前位置和故事内时间。
  443. > **禁止写入**:剧情事件、物品道具、角色关系、好感度——这些内容属于其他专属文件。
  444. ## 基本信息
  445. - **姓名**:(主角姓名)
  446. - **年龄**:(当前年龄)
  447. - **职业背景**:(末世前职业)
  448. - **当前身份**:幸存者
  449. ## 生存状态
  450. - **生命值**:100/100
  451. - **饥饿度**:80/100(100 为饱腹)
  452. - **口渴度**:75/100(100 为解渴)
  453. - **疲劳度**:20/100(0 为精力充沛)
  454. - **感染度**:0%
  455. ## 技能
  456. - **近战**:Lv.1
  457. - **射击**:Lv.0
  458. - **医疗**:Lv.0
  459. - **制造**:Lv.1
  460. ## 位置与时间
  461. - **当前位置**:(所在地点)
  462. - **当前时间**:(末世后第X天)
  463. ## 成长记录
  464. (近期习得的生存技能或重要经历)
  465. `;
  466. templates['protagonist_inventory.md'] = `# 物品栏
  467. > **文件说明**:本文件**仅**记录主角持有的道具、装备及数量。
  468. > **禁止写入**:属性数值、剧情事件、角色关系——这些内容属于其他专属文件。
  469. ## 随身背包(负重:15/30 kg)
  470. | 物品名称 | 数量 | 重量 | 说明 |
  471. |---------|------|------|------|
  472. | 水(瓶装) | 2 瓶 | 1 kg | 饮用水 |
  473. | 压缩饼干 | 5 包 | 0.5 kg | 应急食物 |
  474. | 急救包 | 1 个 | 0.5 kg | 基础医疗 |
  475. | 匕首 | 1 把 | 0.3 kg | 近战武器 |
  476. ## 据点储备
  477. | 物品名称 | 数量 | 说明 |
  478. |---------|------|------|
  479. | (物品名) | 1 | (用途) |
  480. ## 货币/资源
  481. - **物资积分**:0
  482. - **燃料**:5 升
  483. `;
  484. break;
  485. case 'building':
  486. templates['protagonist_stats.md'] = `# 主角属性
  487. > **文件说明**:本文件**仅**记录主角的可量化属性数值、当前位置和故事内时间。
  488. > **禁止写入**:剧情事件、物品道具、角色关系、好感度——这些内容属于其他专属文件。
  489. ## 基本信息
  490. - **姓名**:(主角姓名)
  491. - **年龄**:(当前年龄)
  492. - **职位**:领主/城主
  493. - **领地名称**:(领地名称)
  494. ## 领地状态
  495. - **领地等级**:1 级(村庄)
  496. - **人口**:50 人
  497. - **幸福度**:70/100
  498. - **防御值**:10
  499. ## 资源储备
  500. - **木材**:200
  501. - **石料**:100
  502. - **食物**:500
  503. - **金币**:1000
  504. ## 技能
  505. - **建筑**:Lv.1
  506. - **管理**:Lv.1
  507. - **外交**:Lv.0
  508. ## 位置与时间
  509. - **当前位置**:(领地所在地)
  510. - **当前时间**:(故事内时间)
  511. ## 成长记录
  512. (近期完成的建筑或领地升级)
  513. `;
  514. templates['protagonist_inventory.md'] = `# 物品栏
  515. > **文件说明**:本文件**仅**记录主角持有的道具、装备及数量。
  516. > **禁止写入**:属性数值、剧情事件、角色关系——这些内容属于其他专属文件。
  517. ## 随身携带
  518. | 物品名称 | 数量 | 说明 |
  519. |---------|------|------|
  520. | 领主令牌 | 1 | 身份证明 |
  521. | 建筑图纸 | 3 张 | 可建造建筑 |
  522. ## 领地仓库
  523. | 物品名称 | 数量 | 说明 |
  524. |---------|------|------|
  525. | 木材 | 200 | 建筑材料 |
  526. | 石料 | 100 | 建筑材料 |
  527. | 食物 | 500 | 居民口粮 |
  528. ## 货币/资源
  529. - **金币**:1000
  530. - **特殊资源**:(说明)
  531. `;
  532. break;
  533. case 'urban':
  534. templates['protagonist_stats.md'] = `# 主角属性
  535. > **文件说明**:本文件**仅**记录主角的可量化属性数值、当前位置和故事内时间。
  536. > **禁止写入**:剧情事件、物品道具、角色关系、好感度——这些内容属于其他专属文件。
  537. ## 基本信息
  538. - **姓名**:(主角姓名)
  539. - **年龄**:(当前年龄)
  540. - **职业**:(当前职业)
  541. - **社会地位**:普通人
  542. ## 能力值
  543. - **体质**:普通
  544. - **智力**:普通
  545. - **魅力**:普通
  546. - **财富**:(贫困/普通/小康/富裕)
  547. ## 特殊能力
  548. - (异能/系统/金手指):(描述)
  549. ## 生命值与状态
  550. - **生命值**:100/100
  551. - **精神力**:100/100
  552. - **当前状态**:正常
  553. ## 位置与时间
  554. - **当前位置**:(城市/地区)
  555. - **当前时间**:(现实时间,如"2024年春")
  556. ## 成长记录
  557. (近期获得的能力提升或重要经历)
  558. `;
  559. templates['protagonist_inventory.md'] = `# 物品栏
  560. > **文件说明**:本文件**仅**记录主角持有的道具、装备及数量。
  561. > **禁止写入**:属性数值、剧情事件、角色关系——这些内容属于其他专属文件。
  562. ## 随身携带
  563. | 物品名称 | 数量 | 说明 |
  564. |---------|------|------|
  565. | 手机 | 1 | 通讯工具 |
  566. | 钱包 | 1 | 含身份证、银行卡 |
  567. ## 家中储备
  568. | 物品名称 | 数量 | 说明 |
  569. |---------|------|------|
  570. | (物品名) | 1 | (用途) |
  571. ## 货币/资源
  572. - **现金**:(金额)
  573. - **银行存款**:(金额)
  574. - **其他资产**:(说明)
  575. `;
  576. break;
  577. case 'fantasy':
  578. templates['protagonist_stats.md'] = `# 主角属性
  579. > **文件说明**:本文件**仅**记录主角的可量化属性数值、当前位置和故事内时间。
  580. > **禁止写入**:剧情事件、物品道具、角色关系、好感度——这些内容属于其他专属文件。
  581. ## 基本信息
  582. - **姓名**:(主角姓名)
  583. - **年龄**:(当前年龄)
  584. - **种族**:人类
  585. - **职业**:(战士/法师/盗贼/牧师等)
  586. - **等级**:Lv.1
  587. ## 属性值
  588. - **力量**:10
  589. - **敏捷**:10
  590. - **体质**:10
  591. - **智力**:10
  592. - **感知**:10
  593. - **魅力**:10
  594. ## 生命值与状态
  595. - **HP**:50/50
  596. - **MP**:30/30
  597. - **当前状态**:正常
  598. ## 技能
  599. - (技能1):(说明)
  600. - (技能2):(说明)
  601. ## 位置与时间
  602. - **当前位置**:(所在地点)
  603. - **当前时间**:(故事内时间)
  604. ## 成长记录
  605. (近期升级或习得的技能)
  606. `;
  607. templates['protagonist_inventory.md'] = `# 物品栏
  608. > **文件说明**:本文件**仅**记录主角持有的道具、装备及数量。
  609. > **禁止写入**:属性数值、剧情事件、角色关系——这些内容属于其他专属文件。
  610. ## 装备栏
  611. | 部位 | 装备名称 | 属性 |
  612. |------|---------|------|
  613. | 武器 | 铁剑 | 攻击+5 |
  614. | 护甲 | 皮甲 | 防御+3 |
  615. | 饰品 | — | — |
  616. ## 背包(10/20 格)
  617. | 物品名称 | 数量 | 说明 |
  618. |---------|------|------|
  619. | 初级治疗药水 | 3 瓶 | 恢复 50 HP |
  620. | 面包 | 5 个 | 食物 |
  621. ## 货币/资源
  622. - **金币**:50
  623. - **银币**:20
  624. - **铜币**:0
  625. `;
  626. break;
  627. case 'custom':
  628. default:
  629. // 自定义类型使用通用模板,不覆盖
  630. break;
  631. }
  632. return templates;
  633. }
  634. // ─────────────────────────────────────────────
  635. // 5. parseNarrativeHooks
  636. // ─────────────────────────────────────────────
  637. // 类型映射:中文 → 英文
  638. const TYPE_ZH_TO_EN: Record<string, NarrativeHook['type']> = {
  639. 悬念: 'suspense',
  640. 伏笔: 'foreshadowing',
  641. 作者读者信息差: 'author_reader_gap',
  642. 读者主角信息差: 'reader_protagonist_gap',
  643. };
  644. // 类型映射:英文 → 中文
  645. const TYPE_EN_TO_ZH: Record<NarrativeHook['type'], string> = {
  646. suspense: '悬念',
  647. foreshadowing: '伏笔',
  648. author_reader_gap: '作者读者信息差',
  649. reader_protagonist_gap: '读者主角信息差',
  650. };
  651. /**
  652. * 解析 narrative_hooks.md 的 Markdown 内容,返回 NarrativeHook 数组。
  653. *
  654. * 每个钩子格式:
  655. * ```markdown
  656. * ## {id}
  657. * - **类型**: {type}
  658. * - **描述**: {description}
  659. * - **埋入章节**: {introducedInChapter}
  660. * - **状态**: open
  661. * ```
  662. */
  663. export function parseNarrativeHooks(markdown: string): NarrativeHook[] {
  664. const hooks: NarrativeHook[] = [];
  665. // 按 ## 标题分割(跳过第一个空块)
  666. const sections = markdown.split(/^## /m).slice(1);
  667. for (const section of sections) {
  668. try {
  669. const lines = section.split('\n');
  670. const id = lines[0].trim();
  671. if (!id) continue;
  672. let type: NarrativeHook['type'] | undefined;
  673. let description = '';
  674. let introducedInChapter = 0;
  675. let status: NarrativeHook['status'] = 'open';
  676. let resolvedInChapter: number | undefined;
  677. let resolvedAt: string | undefined;
  678. for (const line of lines.slice(1)) {
  679. const trimmed = line.trim();
  680. // 匹配 - **字段**: 值
  681. const fieldMatch = trimmed.match(/^-\s+\*\*(.+?)\*\*:\s*(.*)$/);
  682. if (!fieldMatch) continue;
  683. const [, fieldName, fieldValue] = fieldMatch;
  684. const value = fieldValue.trim();
  685. switch (fieldName) {
  686. case '类型': {
  687. const mapped = TYPE_ZH_TO_EN[value];
  688. if (mapped) type = mapped;
  689. break;
  690. }
  691. case '描述':
  692. description = value;
  693. break;
  694. case '埋入章节': {
  695. const num = parseInt(value, 10);
  696. if (!isNaN(num)) introducedInChapter = num;
  697. break;
  698. }
  699. case '状态':
  700. if (value === 'open' || value === 'resolved') status = value;
  701. break;
  702. case '解决章节': {
  703. const num = parseInt(value, 10);
  704. if (!isNaN(num)) resolvedInChapter = num;
  705. break;
  706. }
  707. case '解决时间':
  708. resolvedAt = value;
  709. break;
  710. }
  711. }
  712. // 必填字段校验(introducedInChapter 允许为 0,表示手动添加时章节未知)
  713. if (!id || !type || !description) continue;
  714. const hook: NarrativeHook = {
  715. id,
  716. type,
  717. description,
  718. introducedInChapter,
  719. status,
  720. };
  721. if (resolvedInChapter !== undefined) hook.resolvedInChapter = resolvedInChapter;
  722. if (resolvedAt !== undefined) hook.resolvedAt = resolvedAt;
  723. hooks.push(hook);
  724. } catch {
  725. // 解析失败的条目跳过
  726. continue;
  727. }
  728. }
  729. return hooks;
  730. }
  731. // ─────────────────────────────────────────────
  732. // 6. serializeNarrativeHook
  733. // ─────────────────────────────────────────────
  734. /**
  735. * 将 NarrativeHook 序列化为 Markdown 格式(与 parseNarrativeHooks 格式一致)
  736. */
  737. export function serializeNarrativeHook(hook: NarrativeHook): string {
  738. const typeZh = TYPE_EN_TO_ZH[hook.type];
  739. const lines: string[] = [
  740. `## ${hook.id}`,
  741. `- **类型**: ${typeZh}`,
  742. `- **描述**: ${hook.description}`,
  743. `- **埋入章节**: ${hook.introducedInChapter}`,
  744. `- **状态**: ${hook.status}`,
  745. ];
  746. if (hook.resolvedInChapter !== undefined) {
  747. lines.push(`- **解决章节**: ${hook.resolvedInChapter}`);
  748. }
  749. if (hook.resolvedAt !== undefined) {
  750. lines.push(`- **解决时间**: ${hook.resolvedAt}`);
  751. }
  752. return lines.join('\n') + '\n';
  753. }
  754. /**
  755. * 将钩子数组序列化为 JSON 格式
  756. */
  757. export function serializeHooksToJson(hooks: NarrativeHook[]): string {
  758. return JSON.stringify({ hooks, version: 1 }, null, 2);
  759. }
  760. /**
  761. * 从 JSON 格式解析钩子数组
  762. */
  763. export function parseHooksFromJson(json: string): NarrativeHook[] {
  764. try {
  765. const data = JSON.parse(json);
  766. if (data && Array.isArray(data.hooks)) {
  767. return data.hooks as NarrativeHook[];
  768. }
  769. return [];
  770. } catch {
  771. return [];
  772. }
  773. }
  774. // ─────────────────────────────────────────────
  775. // 7. resolveHook
  776. /**
  777. * 将指定钩子标记为已解决状态(JSON格式):
  778. * - 将钩子的 status 设为 resolved
  779. * - 记录解决章节和时间戳
  780. * - 保留钩子在数组中(不移除)
  781. *
  782. * @param hooksJsonContent - narrative_hooks.json 的当前内容
  783. * @param hookId - 要解决的钩子 ID
  784. * @param chapterIndex - 解决该钩子的章节序号
  785. * @returns updatedHooksJson - 更新后的 JSON 内容
  786. */
  787. export function resolveHook(
  788. hooksJsonContent: string,
  789. hookId: string,
  790. chapterIndex: number
  791. ): { updatedHooksFile: string; resolvedEntry: string } {
  792. const hooks = parseHooksFromJson(hooksJsonContent);
  793. const hookIndex = hooks.findIndex((h) => h.id === hookId);
  794. if (hookIndex === -1) {
  795. return { updatedHooksFile: hooksJsonContent, resolvedEntry: '' };
  796. }
  797. const resolvedHook = hooks[hookIndex];
  798. hooks[hookIndex] = {
  799. ...resolvedHook,
  800. status: 'resolved',
  801. resolvedInChapter: chapterIndex,
  802. resolvedAt: new Date().toISOString(),
  803. };
  804. const resolvedEntry = [
  805. `## ${resolvedHook.id}`,
  806. `- **类型**: ${resolvedHook.type}`,
  807. `- **描述**: ${resolvedHook.description}`,
  808. `- **埋入章节**: ${resolvedHook.introducedInChapter}`,
  809. `- **解决章节**: ${chapterIndex}`,
  810. `- **状态**: resolved`,
  811. ].join('\n');
  812. return {
  813. updatedHooksFile: serializeHooksToJson(hooks),
  814. resolvedEntry,
  815. };
  816. }
  817. /**
  818. * 从 JSON 格式中删除指定钩子
  819. *
  820. * @param hooksJsonContent - narrative_hooks.json 的当前内容
  821. * @param hookId - 要删除的钩子 ID
  822. * @returns updatedHooksJson - 更新后的 JSON 内容
  823. */
  824. export function deleteHook(
  825. hooksJsonContent: string,
  826. hookId: string
  827. ): string {
  828. const hooks = parseHooksFromJson(hooksJsonContent);
  829. const filteredHooks = hooks.filter((h) => h.id !== hookId);
  830. return serializeHooksToJson(filteredHooks);
  831. }
  832. /**
  833. * 向 JSON 格式的钩子文件中添加新钩子
  834. *
  835. * @param hooksJsonContent - narrative_hooks.json 的当前内容(空字符串表示无文件)
  836. * @param newHooks - 要添加的新钩子数组
  837. * @returns updatedHooksJson - 更新后的 JSON 内容
  838. */
  839. export function addHooks(
  840. hooksJsonContent: string,
  841. newHooks: NarrativeHook[]
  842. ): string {
  843. const existingHooks = hooksJsonContent ? parseHooksFromJson(hooksJsonContent) : [];
  844. const allHooks = [...existingHooks, ...newHooks];
  845. return serializeHooksToJson(allHooks);
  846. }