| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059 |
- /**
- * 小说上下文管理器 - 工具函数
- * Novel Context Manager - Utility Functions
- *
- * 注意:此文件为客户端/服务端通用工具,不可包含 Node.js 专用模块(如 fs、path)。
- * 服务端专用函数请放在 novel-server-utils.ts。
- */
- import { WorldStateFile, WorldLoreFile, NarrativeHook, NovelType } from './novel-types';
- // ─────────────────────────────────────────────
- // 1. generateChapterFileName
- // ─────────────────────────────────────────────
- /**
- * 生成章节文件名
- * @example generateChapterFileName(1, "序章") → "chapter_1_序章.md"
- */
- export function generateChapterFileName(index: number, title: string): string {
- // Sanitize title: remove characters illegal in Windows/Unix filenames
- const safe = title
- .replace(/[<>:"/\\|?*\x00-\x1f]/g, '-')
- .replace(/-+/g, '-')
- .replace(/^-|-$/g, '')
- .trim()
- || 'untitled';
- return `chapter_${index}_${safe}.md`;
- }
- // ─────────────────────────────────────────────
- // 2. isValidProject (已移至 novel-server-utils.ts)
- // ─────────────────────────────────────────────
- // isValidProject 使用 Node.js fs 模块,仅可在服务端使用。
- // 请从 '@/app/lib/novel-server-utils' 导入。
- // ─────────────────────────────────────────────
- // 3. buildContextFormat
- // ─────────────────────────────────────────────
- /**
- * 将世界状态文件、世界观设定文件、近期章节拼接为 AI 上下文提示词前缀,
- * 最后追加用户原始 instruction(保持不变)。
- *
- * 跳过 enabled: false 或 content 为空的文件。
- */
- export function buildContextFormat(
- worldStateFiles: WorldStateFile[],
- loreFiles: WorldLoreFile[],
- instruction: string,
- recentChapters?: string[]
- ): string {
- const parts: string[] = [];
- // 世界状态文件
- const enabledWorldState = worldStateFiles.filter((f) => f.enabled && f.content.trim() !== '');
- if (enabledWorldState.length > 0) {
- parts.push('[小说世界状态]');
- for (const file of enabledWorldState) {
- parts.push(`=== ${file.fileName} ===`);
- parts.push(file.content);
- }
- }
- // 世界观设定文件
- const enabledLore = loreFiles.filter((f) => f.enabled && f.content.trim() !== '');
- if (enabledLore.length > 0) {
- parts.push('[世界观设定]');
- for (const file of enabledLore) {
- parts.push(`=== ${file.fileName} ===`);
- parts.push(file.content);
- }
- }
- // 上下文结束标记(只有在有世界状态或世界观内容时才添加)
- if (enabledWorldState.length > 0 || enabledLore.length > 0) {
- parts.push('[上下文结束]');
- }
- // 近期章节
- if (recentChapters && recentChapters.length > 0) {
- parts.push('[近期章节]');
- recentChapters.forEach((chapterContent, i) => {
- parts.push(`=== 第${i + 1}章 ===`);
- parts.push(chapterContent);
- });
- parts.push('[近期章节结束]');
- }
- // 用户原始指令(保持不变)
- parts.push(instruction);
- return parts.join('\n');
- }
- // ─────────────────────────────────────────────
- // 4. getDefaultTemplate
- // ─────────────────────────────────────────────
- /**
- * 根据文件名返回对应的默认模板内容(中文字段说明)
- */
- export function getDefaultTemplate(fileName: string): string {
- switch (fileName) {
- case 'plot_summary.md':
- return `# 剧情梗概
- > **文件说明**:本文件**仅**记录已发生的剧情梗概,按章节/时间线整理。
- > **禁止写入**:角色属性数值、物品道具、好感度数值、世界观设定——这些内容属于其他专属文件。
- ## 当前章节
- (当前正在写第几章,简要说明本章主要事件)
- ## 时间线
- - 第1章:(事件摘要)
- - 第2章:(事件摘要)
- ## 主线剧情
- (主线故事的核心走向与目标)
- ## 近期剧情节点
- (最近几章的关键转折点或重要事件)
- ## 待解决的剧情线索
- (尚未展开或需要后续推进的剧情)
- `;
- case 'protagonist_stats.md':
- return `# 主角属性
- > **文件说明**:本文件**仅**记录主角的可量化属性数值、当前位置和故事内时间。
- > **禁止写入**:剧情事件、物品道具、角色关系、好感度——这些内容属于其他专属文件。
- ## 基本信息
- - **姓名**:(主角姓名)
- - **年龄**:(当前年龄)
- - **境界/等级**:(修炼境界或能力等级)
- ## 生命值与状态
- - **生命值**:(当前/最大)
- - **精神力**:(当前/最大)
- - **当前状态**:(正常/受伤/特殊状态)
- ## 位置与时间
- - **当前位置**:(所在地点)
- - **当前时间**:(故事内时间,如"第三年春季")
- ## 核心能力
- - (能力1):(简要说明)
- - (能力2):(简要说明)
- ## 成长记录
- (近期获得的重要能力或突破)
- `;
- case 'protagonist_inventory.md':
- return `# 物品栏
- > **文件说明**:本文件**仅**记录主角持有的道具、装备及数量。
- > **禁止写入**:属性数值、剧情事件、角色关系——这些内容属于其他专属文件。
- ## 随身携带
- | 物品名称 | 数量 | 说明 |
- |---------|------|------|
- | (物品名) | 1 | (用途/来源) |
- ## 储物空间
- | 物品名称 | 数量 | 说明 |
- |---------|------|------|
- | (物品名) | 1 | (用途/来源) |
- ## 重要道具
- (对剧情有重要意义的特殊物品)
- ## 货币/资源
- - **金币/灵石**:(数量)
- - **其他资源**:(说明)
- `;
- case 'character_relations.md':
- return `# 角色关系
- > **文件说明**:本文件**仅**记录角色间的关系类型和好感度数值。
- > **禁止写入**:角色背景故事、剧情事件、属性数值——角色背景请写入 lore/characters_lore.md,剧情请写入 plot_summary.md。
- ## 主要角色
- ### (角色名1)
- - **关系**:(与主角的关系,如"师父"、"挚友")
- - **当前状态**:(在世/离开/未知)
- - **当前位置**:(所在地)
- - **关系备注**:(近期互动或关系变化)
- ### (角色名2)
- - **关系**:(与主角的关系)
- - **当前状态**:(在世/离开/未知)
- - **当前位置**:(所在地)
- - **关系备注**:(近期互动或关系变化)
- ## 敌对势力
- (与主角存在冲突的角色或组织)
- ## 关系变化记录
- (近期发生的重要关系变化)
- `;
- case 'world_notes.md':
- return `# 世界笔记
- > **文件说明**:本文件**仅**记录不属于其他专属文件的可量化世界信息(如阵营声望值、资源储量、特殊规则数值等)。
- > **禁止写入**:剧情梗概、主角属性、物品栏、角色关系、地理/历史/文化/经济设定——这些均有专属文件。
- ## 重要地点
- - **(地点名)**:(简要描述,当前剧情意义)
- ## 重要事件记录
- (已发生的对世界观有影响的重大事件)
- ## 规则与设定备忘
- (需要保持一致的世界规则,如修炼体系、魔法规则等)
- ## 未解之谜
- (故事中尚未揭晓的谜题或伏笔)
- ## 其他备注
- (其他需要记录的世界细节)
- `;
- case 'narrative_hooks.json':
- return JSON.stringify({ version: 1, hooks: [] }, null, 2);
- case 'writing_style.md':
- return `# 文风档案
- > **文件说明**:本文件**仅**记录叙事风格、语言特点、节奏偏好等文风设定,由初始化向导生成后仅供用户手动修改。
- > **禁止写入**:剧情内容、角色属性、世界设定——这些内容属于其他专属文件。
- ## 整体风格
- (描述本作品的整体写作风格,如"硬核爽文"、"细腻情感流"、"幽默轻松"等)
- ## 叙事视角
- (第一人称/第三人称限知/第三人称全知)
- ## 语言特点
- - **句式风格**:(长句为主/短句为主/长短结合)
- - **用词偏好**:(文言色彩/现代口语/专业术语)
- - **描写侧重**:(动作/心理/环境/对话)
- ## 节奏控制
- (快节奏爽文/慢热情感/张弛有度)
- ## 禁忌与避免
- (需要避免的写作习惯或表达方式)
- ## 参考风格
- (参考的作品或作者风格)
- `;
- case 'worldbuilding.md':
- return `# 世界观总纲
- > **文件说明**:本文件**仅**记录宇宙规则、力量体系、世界起源等核心设定。
- > **禁止写入**:具体地理信息、历史事件、势力详情、人物志——这些均有专属文件(geography.md / history.md / factions.md / characters_lore.md)。
- ## 世界背景
- (本故事发生的世界基本设定,时代背景,世界类型)
- ## 核心规则
- (这个世界运行的基本规律,如修炼体系、魔法体系、科技水平等)
- ## 世界格局
- (当前世界的政治、势力格局概览)
- ## 特殊设定
- (区别于其他作品的独特世界观元素)
- ## 世界历史简述
- (影响当前故事的重要历史背景)
- `;
- case 'geography.md':
- return `# 地理信息
- > **文件说明**:本文件**仅**记录大陆/区域地图描述、地名、地形、气候。
- > **禁止写入**:历史事件、势力关系、人物信息、文化习俗——这些均有专属文件。
- ## 主要地区
- ### (地区名1)
- - **类型**:(城市/山脉/秘境/国家等)
- - **特点**:(地理特征、气候、资源)
- - **剧情意义**:(在故事中的作用)
- ### (地区名2)
- - **类型**:(城市/山脉/秘境/国家等)
- - **特点**:(地理特征、气候、资源)
- - **剧情意义**:(在故事中的作用)
- ## 主角活动范围
- (主角目前活动的主要地理区域)
- ## 地理关系
- (各地区之间的距离、交通方式、政治关系)
- `;
- case 'factions.md':
- return `# 势力组织
- > **文件说明**:本文件**仅**记录势力与组织的基本信息、势力范围及相互关系。
- > **禁止写入**:具体历史事件、个人人物志、地理描述——历史请写入 history.md,人物请写入 characters_lore.md。
- ## 主要势力
- ### (势力名1)
- - **类型**:(门派/国家/商会/秘密组织等)
- - **实力**:(强/中/弱,或具体描述)
- - **立场**:(友好/中立/敌对)
- - **核心人物**:(领导者或关键人物)
- - **与主角关系**:(当前关系状态)
- ### (势力名2)
- - **类型**:(门派/国家/商会/秘密组织等)
- - **实力**:(强/中/弱,或具体描述)
- - **立场**:(友好/中立/敌对)
- - **核心人物**:(领导者或关键人物)
- - **与主角关系**:(当前关系状态)
- ## 势力格局变化
- (近期发生的势力变动或冲突)
- `;
- case 'history.md':
- return `# 历史事件
- > **文件说明**:本文件**仅**记录重大历史事件、传说、古代战争和时间线。
- > **禁止写入**:当前剧情进展、现存势力的实时状态、主角当前属性——当前剧情请写入 plot_summary.md。
- ## 世界重大历史
- ### (历史事件1)
- - **时间**:(故事内时间或"N年前")
- - **事件**:(简要描述)
- - **影响**:(对当前世界的影响)
- ### (历史事件2)
- - **时间**:(故事内时间或"N年前")
- - **事件**:(简要描述)
- - **影响**:(对当前世界的影响)
- ## 主角相关历史
- (主角的过去经历中对当前剧情有影响的事件)
- ## 未解历史谜题
- (故事中涉及但尚未揭晓的历史秘密)
- `;
- case 'culture.md':
- return `# 文化习俗
- > **文件说明**:本文件**仅**记录礼仪、节日、宗教信仰、社会阶层等文化习俗。
- > **禁止写入**:地理信息、历史事件、经济数据、势力关系——这些均有专属文件。
- ## 社会结构
- (社会阶层划分,等级制度)
- ## 主要文化习俗
- - **(习俗名1)**:(描述及意义)
- - **(习俗名2)**:(描述及意义)
- ## 宗教与信仰
- (主要宗教或信仰体系)
- ## 禁忌
- (社会禁忌,违反的后果)
- ## 节日与仪式
- (重要节日或仪式,对剧情可能有影响的)
- ## 语言与文字
- (世界中的语言体系,是否有特殊文字)
- `;
- case 'economy.md':
- return `# 经济体系
- > **文件说明**:本文件**仅**记录货币体系、贸易规则、资源分布和物价参考。
- > **禁止写入**:势力政治关系、文化习俗、地理描述——这些均有专属文件。
- ## 货币体系
- (流通货币种类,换算关系)
- ## 主要产业
- - **(产业名1)**:(描述)
- - **(产业名2)**:(描述)
- ## 贸易体系
- (主要贸易路线,重要商品)
- ## 经济格局
- (各势力的经济实力对比)
- ## 特殊资源
- (世界中的稀缺资源,对修炼/战斗有重要意义的物资)
- ## 主角经济状况
- (主角当前的财富状况和经济来源)
- `;
- case 'characters_lore.md':
- return `# 重要人物志
- > **文件说明**:本文件**仅**记录非主角的重要 NPC 背景、身份和势力归属。
- > **禁止写入**:主角属性数值(写入 protagonist_stats.md)、角色好感度数值(写入 character_relations.md)、剧情梗概(写入 plot_summary.md)。
- ## 重要配角
- ### (配角姓名1)
- - **身份**:(在故事中的角色定位)
- - **背景**:(简要背景)
- - **性格**:(核心性格特点)
- - **势力归属**:(所属门派/国家/组织)
- - **隐藏信息**:(读者/主角尚不知晓的信息)
- ### (配角姓名2)
- - **身份**:(在故事中的角色定位)
- - **背景**:(简要背景)
- - **性格**:(核心性格特点)
- - **势力归属**:(所属门派/国家/组织)
- - **隐藏信息**:(读者/主角尚不知晓的信息)
- ## 反派/对立角色
- ### (反派姓名)
- - **身份**:(在故事中的角色定位)
- - **动机**:(行动的核心动机)
- - **能力**:(主要能力或威胁)
- - **弱点**:(已知或潜在弱点)
- `;
- default:
- return `# ${fileName}\n\n(暂无内容)\n`;
- }
- }
- // ─────────────────────────────────────────────
- // 4b. getNovelTypeTemplate
- // ─────────────────────────────────────────────
- /**
- * 根据小说类型返回差异化的 protagonist_stats.md 初始模板内容。
- * 需求 1.10:选择 Novel_Type 后,将对应默认属性值填充到相关 World_State_File。
- */
- export function getNovelTypeTemplate(novelType: NovelType): Record<string, string> {
- const templates: Record<string, string> = {};
- switch (novelType) {
- case 'xianxia':
- templates['protagonist_stats.md'] = `# 主角属性
- > **文件说明**:本文件**仅**记录主角的可量化属性数值、当前位置和故事内时间。
- > **禁止写入**:剧情事件、物品道具、角色关系、好感度——这些内容属于其他专属文件。
- ## 基本信息
- - **姓名**:(主角姓名)
- - **年龄**:(当前年龄)
- - **境界**:炼气期一层
- - **修炼功法**:(功法名称)
- ## 修炼资质
- - **灵根**:(单灵根/双灵根/杂灵根)
- - **灵根属性**:(金/木/水/火/土)
- - **天赋评级**:(上/中/下)
- ## 生命值与状态
- - **生命值**:100/100
- - **灵力**:50/50
- - **当前状态**:正常
- ## 位置与时间
- - **当前位置**:(所在地点)
- - **当前时间**:(故事内时间,如"天历三年春")
- ## 核心能力
- - (技能1):(简要说明)
- - (技能2):(简要说明)
- ## 成长记录
- (近期获得的重要能力或突破)
- `;
- templates['protagonist_inventory.md'] = `# 物品栏
- > **文件说明**:本文件**仅**记录主角持有的道具、装备及数量。
- > **禁止写入**:属性数值、剧情事件、角色关系——这些内容属于其他专属文件。
- ## 随身携带
- | 物品名称 | 数量 | 说明 |
- |---------|------|------|
- | 低阶灵石 | 10 | 修炼用 |
- | 聚气丹 | 3 | 辅助修炼 |
- ## 储物袋
- | 物品名称 | 数量 | 说明 |
- |---------|------|------|
- | (物品名) | 1 | (用途/来源) |
- ## 重要道具
- (对剧情有重要意义的特殊法宝或道具)
- ## 货币/资源
- - **灵石**:10 块(低阶)
- - **其他资源**:(说明)
- `;
- break;
- case 'farming':
- templates['protagonist_stats.md'] = `# 主角属性
- > **文件说明**:本文件**仅**记录主角的可量化属性数值、当前位置和故事内时间。
- > **禁止写入**:剧情事件、物品道具、角色关系、好感度——这些内容属于其他专属文件。
- ## 基本信息
- - **姓名**:(主角姓名)
- - **年龄**:(当前年龄)
- - **职业**:农场主
- - **所在地**:(农场/村庄名称)
- ## 农场状态
- - **农场等级**:1 级
- - **土地面积**:5 亩
- - **作物槽位**:10 个
- - **牲畜槽位**:5 个
- ## 技能
- - **种植**:Lv.1
- - **烹饪**:Lv.1
- - **手工**:Lv.1
- ## 生命值与状态
- - **体力**:100/100
- - **心情**:愉快
- - **当前状态**:正常
- ## 位置与时间
- - **当前位置**:(所在地点)
- - **当前时间**:(故事内时间,如"第一年春季")
- ## 成长记录
- (近期解锁的技能或农场升级记录)
- `;
- templates['protagonist_inventory.md'] = `# 物品栏
- > **文件说明**:本文件**仅**记录主角持有的道具、装备及数量。
- > **禁止写入**:属性数值、剧情事件、角色关系——这些内容属于其他专属文件。
- ## 随身携带
- | 物品名称 | 数量 | 说明 |
- |---------|------|------|
- | 种子袋 | 1 | 含各类蔬菜种子 |
- | 农具 | 1 套 | 锄头、镰刀等 |
- ## 仓库
- | 物品名称 | 数量 | 说明 |
- |---------|------|------|
- | 小麦 | 50 斤 | 自产 |
- | 蔬菜 | 20 斤 | 自产 |
- ## 重要道具
- (对剧情有重要意义的特殊物品)
- ## 货币/资源
- - **金币**:100
- - **木材**:20 根
- - **石料**:10 块
- `;
- break;
- case 'survival':
- templates['protagonist_stats.md'] = `# 主角属性
- > **文件说明**:本文件**仅**记录主角的可量化属性数值、当前位置和故事内时间。
- > **禁止写入**:剧情事件、物品道具、角色关系、好感度——这些内容属于其他专属文件。
- ## 基本信息
- - **姓名**:(主角姓名)
- - **年龄**:(当前年龄)
- - **职业背景**:(末世前职业)
- - **当前身份**:幸存者
- ## 生存状态
- - **生命值**:100/100
- - **饥饿度**:80/100(100 为饱腹)
- - **口渴度**:75/100(100 为解渴)
- - **疲劳度**:20/100(0 为精力充沛)
- - **感染度**:0%
- ## 技能
- - **近战**:Lv.1
- - **射击**:Lv.0
- - **医疗**:Lv.0
- - **制造**:Lv.1
- ## 位置与时间
- - **当前位置**:(所在地点)
- - **当前时间**:(末世后第X天)
- ## 成长记录
- (近期习得的生存技能或重要经历)
- `;
- templates['protagonist_inventory.md'] = `# 物品栏
- > **文件说明**:本文件**仅**记录主角持有的道具、装备及数量。
- > **禁止写入**:属性数值、剧情事件、角色关系——这些内容属于其他专属文件。
- ## 随身背包(负重:15/30 kg)
- | 物品名称 | 数量 | 重量 | 说明 |
- |---------|------|------|------|
- | 水(瓶装) | 2 瓶 | 1 kg | 饮用水 |
- | 压缩饼干 | 5 包 | 0.5 kg | 应急食物 |
- | 急救包 | 1 个 | 0.5 kg | 基础医疗 |
- | 匕首 | 1 把 | 0.3 kg | 近战武器 |
- ## 据点储备
- | 物品名称 | 数量 | 说明 |
- |---------|------|------|
- | (物品名) | 1 | (用途) |
- ## 货币/资源
- - **物资积分**:0
- - **燃料**:5 升
- `;
- break;
- case 'building':
- templates['protagonist_stats.md'] = `# 主角属性
- > **文件说明**:本文件**仅**记录主角的可量化属性数值、当前位置和故事内时间。
- > **禁止写入**:剧情事件、物品道具、角色关系、好感度——这些内容属于其他专属文件。
- ## 基本信息
- - **姓名**:(主角姓名)
- - **年龄**:(当前年龄)
- - **职位**:领主/城主
- - **领地名称**:(领地名称)
- ## 领地状态
- - **领地等级**:1 级(村庄)
- - **人口**:50 人
- - **幸福度**:70/100
- - **防御值**:10
- ## 资源储备
- - **木材**:200
- - **石料**:100
- - **食物**:500
- - **金币**:1000
- ## 技能
- - **建筑**:Lv.1
- - **管理**:Lv.1
- - **外交**:Lv.0
- ## 位置与时间
- - **当前位置**:(领地所在地)
- - **当前时间**:(故事内时间)
- ## 成长记录
- (近期完成的建筑或领地升级)
- `;
- templates['protagonist_inventory.md'] = `# 物品栏
- > **文件说明**:本文件**仅**记录主角持有的道具、装备及数量。
- > **禁止写入**:属性数值、剧情事件、角色关系——这些内容属于其他专属文件。
- ## 随身携带
- | 物品名称 | 数量 | 说明 |
- |---------|------|------|
- | 领主令牌 | 1 | 身份证明 |
- | 建筑图纸 | 3 张 | 可建造建筑 |
- ## 领地仓库
- | 物品名称 | 数量 | 说明 |
- |---------|------|------|
- | 木材 | 200 | 建筑材料 |
- | 石料 | 100 | 建筑材料 |
- | 食物 | 500 | 居民口粮 |
- ## 货币/资源
- - **金币**:1000
- - **特殊资源**:(说明)
- `;
- break;
- case 'urban':
- templates['protagonist_stats.md'] = `# 主角属性
- > **文件说明**:本文件**仅**记录主角的可量化属性数值、当前位置和故事内时间。
- > **禁止写入**:剧情事件、物品道具、角色关系、好感度——这些内容属于其他专属文件。
- ## 基本信息
- - **姓名**:(主角姓名)
- - **年龄**:(当前年龄)
- - **职业**:(当前职业)
- - **社会地位**:普通人
- ## 能力值
- - **体质**:普通
- - **智力**:普通
- - **魅力**:普通
- - **财富**:(贫困/普通/小康/富裕)
- ## 特殊能力
- - (异能/系统/金手指):(描述)
- ## 生命值与状态
- - **生命值**:100/100
- - **精神力**:100/100
- - **当前状态**:正常
- ## 位置与时间
- - **当前位置**:(城市/地区)
- - **当前时间**:(现实时间,如"2024年春")
- ## 成长记录
- (近期获得的能力提升或重要经历)
- `;
- templates['protagonist_inventory.md'] = `# 物品栏
- > **文件说明**:本文件**仅**记录主角持有的道具、装备及数量。
- > **禁止写入**:属性数值、剧情事件、角色关系——这些内容属于其他专属文件。
- ## 随身携带
- | 物品名称 | 数量 | 说明 |
- |---------|------|------|
- | 手机 | 1 | 通讯工具 |
- | 钱包 | 1 | 含身份证、银行卡 |
- ## 家中储备
- | 物品名称 | 数量 | 说明 |
- |---------|------|------|
- | (物品名) | 1 | (用途) |
- ## 货币/资源
- - **现金**:(金额)
- - **银行存款**:(金额)
- - **其他资产**:(说明)
- `;
- break;
- case 'fantasy':
- templates['protagonist_stats.md'] = `# 主角属性
- > **文件说明**:本文件**仅**记录主角的可量化属性数值、当前位置和故事内时间。
- > **禁止写入**:剧情事件、物品道具、角色关系、好感度——这些内容属于其他专属文件。
- ## 基本信息
- - **姓名**:(主角姓名)
- - **年龄**:(当前年龄)
- - **种族**:人类
- - **职业**:(战士/法师/盗贼/牧师等)
- - **等级**:Lv.1
- ## 属性值
- - **力量**:10
- - **敏捷**:10
- - **体质**:10
- - **智力**:10
- - **感知**:10
- - **魅力**:10
- ## 生命值与状态
- - **HP**:50/50
- - **MP**:30/30
- - **当前状态**:正常
- ## 技能
- - (技能1):(说明)
- - (技能2):(说明)
- ## 位置与时间
- - **当前位置**:(所在地点)
- - **当前时间**:(故事内时间)
- ## 成长记录
- (近期升级或习得的技能)
- `;
- templates['protagonist_inventory.md'] = `# 物品栏
- > **文件说明**:本文件**仅**记录主角持有的道具、装备及数量。
- > **禁止写入**:属性数值、剧情事件、角色关系——这些内容属于其他专属文件。
- ## 装备栏
- | 部位 | 装备名称 | 属性 |
- |------|---------|------|
- | 武器 | 铁剑 | 攻击+5 |
- | 护甲 | 皮甲 | 防御+3 |
- | 饰品 | — | — |
- ## 背包(10/20 格)
- | 物品名称 | 数量 | 说明 |
- |---------|------|------|
- | 初级治疗药水 | 3 瓶 | 恢复 50 HP |
- | 面包 | 5 个 | 食物 |
- ## 货币/资源
- - **金币**:50
- - **银币**:20
- - **铜币**:0
- `;
- break;
- case 'custom':
- default:
- // 自定义类型使用通用模板,不覆盖
- break;
- }
- return templates;
- }
- // ─────────────────────────────────────────────
- // 5. parseNarrativeHooks
- // ─────────────────────────────────────────────
- // 类型映射:中文 → 英文
- const TYPE_ZH_TO_EN: Record<string, NarrativeHook['type']> = {
- 悬念: 'suspense',
- 伏笔: 'foreshadowing',
- 作者读者信息差: 'author_reader_gap',
- 读者主角信息差: 'reader_protagonist_gap',
- };
- // 类型映射:英文 → 中文
- const TYPE_EN_TO_ZH: Record<NarrativeHook['type'], string> = {
- suspense: '悬念',
- foreshadowing: '伏笔',
- author_reader_gap: '作者读者信息差',
- reader_protagonist_gap: '读者主角信息差',
- };
- /**
- * 解析 narrative_hooks.md 的 Markdown 内容,返回 NarrativeHook 数组。
- *
- * 每个钩子格式:
- * ```markdown
- * ## {id}
- * - **类型**: {type}
- * - **描述**: {description}
- * - **埋入章节**: {introducedInChapter}
- * - **状态**: open
- * ```
- */
- export function parseNarrativeHooks(markdown: string): NarrativeHook[] {
- const hooks: NarrativeHook[] = [];
- // 按 ## 标题分割(跳过第一个空块)
- const sections = markdown.split(/^## /m).slice(1);
- for (const section of sections) {
- try {
- const lines = section.split('\n');
- const id = lines[0].trim();
- if (!id) continue;
- let type: NarrativeHook['type'] | undefined;
- let description = '';
- let introducedInChapter = 0;
- let status: NarrativeHook['status'] = 'open';
- let resolvedInChapter: number | undefined;
- let resolvedAt: string | undefined;
- for (const line of lines.slice(1)) {
- const trimmed = line.trim();
- // 匹配 - **字段**: 值
- const fieldMatch = trimmed.match(/^-\s+\*\*(.+?)\*\*:\s*(.*)$/);
- if (!fieldMatch) continue;
- const [, fieldName, fieldValue] = fieldMatch;
- const value = fieldValue.trim();
- switch (fieldName) {
- case '类型': {
- const mapped = TYPE_ZH_TO_EN[value];
- if (mapped) type = mapped;
- break;
- }
- case '描述':
- description = value;
- break;
- case '埋入章节': {
- const num = parseInt(value, 10);
- if (!isNaN(num)) introducedInChapter = num;
- break;
- }
- case '状态':
- if (value === 'open' || value === 'resolved') status = value;
- break;
- case '解决章节': {
- const num = parseInt(value, 10);
- if (!isNaN(num)) resolvedInChapter = num;
- break;
- }
- case '解决时间':
- resolvedAt = value;
- break;
- }
- }
- // 必填字段校验(introducedInChapter 允许为 0,表示手动添加时章节未知)
- if (!id || !type || !description) continue;
- const hook: NarrativeHook = {
- id,
- type,
- description,
- introducedInChapter,
- status,
- };
- if (resolvedInChapter !== undefined) hook.resolvedInChapter = resolvedInChapter;
- if (resolvedAt !== undefined) hook.resolvedAt = resolvedAt;
- hooks.push(hook);
- } catch {
- // 解析失败的条目跳过
- continue;
- }
- }
- return hooks;
- }
- // ─────────────────────────────────────────────
- // 6. serializeNarrativeHook
- // ─────────────────────────────────────────────
- /**
- * 将 NarrativeHook 序列化为 Markdown 格式(与 parseNarrativeHooks 格式一致)
- */
- export function serializeNarrativeHook(hook: NarrativeHook): string {
- const typeZh = TYPE_EN_TO_ZH[hook.type];
- const lines: string[] = [
- `## ${hook.id}`,
- `- **类型**: ${typeZh}`,
- `- **描述**: ${hook.description}`,
- `- **埋入章节**: ${hook.introducedInChapter}`,
- `- **状态**: ${hook.status}`,
- ];
- if (hook.resolvedInChapter !== undefined) {
- lines.push(`- **解决章节**: ${hook.resolvedInChapter}`);
- }
- if (hook.resolvedAt !== undefined) {
- lines.push(`- **解决时间**: ${hook.resolvedAt}`);
- }
- return lines.join('\n') + '\n';
- }
- /**
- * 将钩子数组序列化为 JSON 格式
- */
- export function serializeHooksToJson(hooks: NarrativeHook[]): string {
- return JSON.stringify({ hooks, version: 1 }, null, 2);
- }
- /**
- * 从 JSON 格式解析钩子数组
- */
- export function parseHooksFromJson(json: string): NarrativeHook[] {
- try {
- const data = JSON.parse(json);
- if (data && Array.isArray(data.hooks)) {
- return data.hooks as NarrativeHook[];
- }
- return [];
- } catch {
- return [];
- }
- }
- // ─────────────────────────────────────────────
- // 7. resolveHook
- /**
- * 将指定钩子标记为已解决状态(JSON格式):
- * - 将钩子的 status 设为 resolved
- * - 记录解决章节和时间戳
- * - 保留钩子在数组中(不移除)
- *
- * @param hooksJsonContent - narrative_hooks.json 的当前内容
- * @param hookId - 要解决的钩子 ID
- * @param chapterIndex - 解决该钩子的章节序号
- * @returns updatedHooksJson - 更新后的 JSON 内容
- */
- export function resolveHook(
- hooksJsonContent: string,
- hookId: string,
- chapterIndex: number
- ): { updatedHooksFile: string; resolvedEntry: string } {
- const hooks = parseHooksFromJson(hooksJsonContent);
- const hookIndex = hooks.findIndex((h) => h.id === hookId);
- if (hookIndex === -1) {
- return { updatedHooksFile: hooksJsonContent, resolvedEntry: '' };
- }
- const resolvedHook = hooks[hookIndex];
- hooks[hookIndex] = {
- ...resolvedHook,
- status: 'resolved',
- resolvedInChapter: chapterIndex,
- resolvedAt: new Date().toISOString(),
- };
- const resolvedEntry = [
- `## ${resolvedHook.id}`,
- `- **类型**: ${resolvedHook.type}`,
- `- **描述**: ${resolvedHook.description}`,
- `- **埋入章节**: ${resolvedHook.introducedInChapter}`,
- `- **解决章节**: ${chapterIndex}`,
- `- **状态**: resolved`,
- ].join('\n');
- return {
- updatedHooksFile: serializeHooksToJson(hooks),
- resolvedEntry,
- };
- }
- /**
- * 从 JSON 格式中删除指定钩子
- *
- * @param hooksJsonContent - narrative_hooks.json 的当前内容
- * @param hookId - 要删除的钩子 ID
- * @returns updatedHooksJson - 更新后的 JSON 内容
- */
- export function deleteHook(
- hooksJsonContent: string,
- hookId: string
- ): string {
- const hooks = parseHooksFromJson(hooksJsonContent);
- const filteredHooks = hooks.filter((h) => h.id !== hookId);
- return serializeHooksToJson(filteredHooks);
- }
- /**
- * 向 JSON 格式的钩子文件中添加新钩子
- *
- * @param hooksJsonContent - narrative_hooks.json 的当前内容(空字符串表示无文件)
- * @param newHooks - 要添加的新钩子数组
- * @returns updatedHooksJson - 更新后的 JSON 内容
- */
- export function addHooks(
- hooksJsonContent: string,
- newHooks: NarrativeHook[]
- ): string {
- const existingHooks = hooksJsonContent ? parseHooksFromJson(hooksJsonContent) : [];
- const allHooks = [...existingHooks, ...newHooks];
- return serializeHooksToJson(allHooks);
- }
|