2026-03-03 16:57:49 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 故事数据管理器
|
|
|
|
|
|
*/
|
2026-03-09 14:15:00 +08:00
|
|
|
|
import { get, post, request } from '../utils/http';
|
2026-03-03 16:57:49 +08:00
|
|
|
|
|
|
|
|
|
|
export default class StoryManager {
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
this.storyList = [];
|
|
|
|
|
|
this.currentStory = null;
|
|
|
|
|
|
this.currentNodeKey = 'start';
|
|
|
|
|
|
this.categories = [];
|
2026-03-06 13:16:54 +08:00
|
|
|
|
this.pathHistory = []; // 记录用户走过的路径
|
2026-03-03 16:57:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 加载故事列表
|
|
|
|
|
|
*/
|
|
|
|
|
|
async loadStoryList(options = {}) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.storyList = await get('/stories', options);
|
|
|
|
|
|
return this.storyList;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载故事列表失败:', error);
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 加载热门故事
|
|
|
|
|
|
*/
|
|
|
|
|
|
async loadHotStories(limit = 10) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return await get('/stories/hot', { limit });
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载热门故事失败:', error);
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 加载分类列表
|
|
|
|
|
|
*/
|
|
|
|
|
|
async loadCategories() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.categories = await get('/stories/categories');
|
|
|
|
|
|
return this.categories;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载分类失败:', error);
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 加载故事详情
|
|
|
|
|
|
*/
|
|
|
|
|
|
async loadStoryDetail(storyId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.currentStory = await get(`/stories/${storyId}`);
|
|
|
|
|
|
this.currentNodeKey = 'start';
|
2026-03-06 13:16:54 +08:00
|
|
|
|
this.pathHistory = []; // 重置路径历史
|
2026-03-03 16:57:49 +08:00
|
|
|
|
|
|
|
|
|
|
// 记录游玩次数
|
|
|
|
|
|
await post(`/stories/${storyId}/play`);
|
|
|
|
|
|
|
|
|
|
|
|
return this.currentStory;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载故事详情失败:', error);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取当前节点
|
|
|
|
|
|
*/
|
|
|
|
|
|
getCurrentNode() {
|
|
|
|
|
|
if (!this.currentStory || !this.currentStory.nodes) return null;
|
|
|
|
|
|
return this.currentStory.nodes[this.currentNodeKey];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 选择选项,前进到下一个节点
|
|
|
|
|
|
*/
|
|
|
|
|
|
selectChoice(choiceIndex) {
|
|
|
|
|
|
const currentNode = this.getCurrentNode();
|
|
|
|
|
|
if (!currentNode || !currentNode.choices || !currentNode.choices[choiceIndex]) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const choice = currentNode.choices[choiceIndex];
|
2026-03-06 13:16:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 记录路径历史
|
|
|
|
|
|
this.pathHistory.push({
|
|
|
|
|
|
nodeKey: this.currentNodeKey,
|
|
|
|
|
|
content: currentNode.content,
|
|
|
|
|
|
choice: choice.text
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-03 16:57:49 +08:00
|
|
|
|
this.currentNodeKey = choice.nextNodeKey;
|
|
|
|
|
|
|
|
|
|
|
|
return this.getCurrentNode();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查当前节点是否为结局
|
|
|
|
|
|
*/
|
|
|
|
|
|
isEnding() {
|
|
|
|
|
|
const currentNode = this.getCurrentNode();
|
|
|
|
|
|
return currentNode && currentNode.is_ending;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取结局信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
getEndingInfo() {
|
|
|
|
|
|
const currentNode = this.getCurrentNode();
|
|
|
|
|
|
if (!currentNode || !currentNode.is_ending) return null;
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
name: currentNode.ending_name,
|
|
|
|
|
|
score: currentNode.ending_score,
|
|
|
|
|
|
type: currentNode.ending_type,
|
|
|
|
|
|
content: currentNode.content
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 重置故事进度
|
|
|
|
|
|
*/
|
|
|
|
|
|
resetStory() {
|
|
|
|
|
|
this.currentNodeKey = 'start';
|
2026-03-09 14:15:00 +08:00
|
|
|
|
this.pathHistory = []; // 清空路径历史
|
2026-03-03 16:57:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 点赞故事
|
|
|
|
|
|
*/
|
|
|
|
|
|
async likeStory(like = true) {
|
|
|
|
|
|
if (!this.currentStory) return;
|
|
|
|
|
|
await post(`/stories/${this.currentStory.id}/like`, { like });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* AI改写结局
|
|
|
|
|
|
*/
|
|
|
|
|
|
async rewriteEnding(storyId, ending, prompt) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await post(`/stories/${storyId}/rewrite`, {
|
|
|
|
|
|
ending_name: ending?.name,
|
|
|
|
|
|
ending_content: ending?.content,
|
|
|
|
|
|
prompt: prompt
|
2026-03-05 15:57:51 +08:00
|
|
|
|
}, { timeout: 60000 });
|
2026-03-03 16:57:49 +08:00
|
|
|
|
return result;
|
|
|
|
|
|
} catch (error) {
|
2026-03-05 15:57:51 +08:00
|
|
|
|
console.error('AI改写失败:', error?.errMsg || error?.message || JSON.stringify(error));
|
2026-03-03 16:57:49 +08:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-03 17:06:08 +08:00
|
|
|
|
|
2026-03-09 23:00:15 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* AI改写结局,异步提交到草稿箱
|
|
|
|
|
|
*/
|
|
|
|
|
|
async rewriteEndingAsync(storyId, ending, prompt, userId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 先标记之前的未读草稿为已读
|
|
|
|
|
|
await this.markAllDraftsRead(userId);
|
|
|
|
|
|
|
|
|
|
|
|
const result = await post(`/drafts/ending`, {
|
|
|
|
|
|
userId: userId,
|
|
|
|
|
|
storyId: storyId,
|
|
|
|
|
|
endingName: ending?.name || '未知结局',
|
|
|
|
|
|
endingContent: ending?.content || '',
|
|
|
|
|
|
prompt: prompt
|
|
|
|
|
|
}, { timeout: 30000 });
|
|
|
|
|
|
|
|
|
|
|
|
if (result && result.draftId) {
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('AI改写结局提交失败:', error?.errMsg || error?.message || JSON.stringify(error));
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* AI续写结局,异步提交到草稿箱
|
|
|
|
|
|
*/
|
|
|
|
|
|
async continueEndingAsync(storyId, ending, prompt, userId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 先标记之前的未读草稿为已读
|
|
|
|
|
|
await this.markAllDraftsRead(userId);
|
|
|
|
|
|
|
|
|
|
|
|
const result = await post(`/drafts/continue-ending`, {
|
|
|
|
|
|
userId: userId,
|
|
|
|
|
|
storyId: storyId,
|
|
|
|
|
|
endingName: ending?.name || '未知结局',
|
|
|
|
|
|
endingContent: ending?.content || '',
|
|
|
|
|
|
prompt: prompt
|
|
|
|
|
|
}, { timeout: 30000 });
|
|
|
|
|
|
|
|
|
|
|
|
if (result && result.draftId) {
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('AI续写结局提交失败:', error?.errMsg || error?.message || JSON.stringify(error));
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-06 13:16:54 +08:00
|
|
|
|
/**
|
2026-03-09 14:15:00 +08:00
|
|
|
|
* AI改写中间章节,异步提交到草稿箱
|
|
|
|
|
|
* @returns {Object|null} 成功返回草稿ID,失败返回 null
|
2026-03-06 13:16:54 +08:00
|
|
|
|
*/
|
2026-03-09 14:15:00 +08:00
|
|
|
|
async rewriteBranchAsync(storyId, prompt, userId) {
|
2026-03-06 13:16:54 +08:00
|
|
|
|
try {
|
2026-03-09 14:15:00 +08:00
|
|
|
|
// 先标记之前的未读草稿为已读,避免轮询弹出之前的通知
|
|
|
|
|
|
await this.markAllDraftsRead(userId);
|
|
|
|
|
|
|
2026-03-06 13:16:54 +08:00
|
|
|
|
const currentNode = this.getCurrentNode();
|
2026-03-09 14:15:00 +08:00
|
|
|
|
const result = await post(`/drafts`, {
|
2026-03-06 13:16:54 +08:00
|
|
|
|
userId: userId,
|
2026-03-09 14:15:00 +08:00
|
|
|
|
storyId: storyId,
|
2026-03-06 13:16:54 +08:00
|
|
|
|
currentNodeKey: this.currentNodeKey,
|
|
|
|
|
|
pathHistory: this.pathHistory,
|
|
|
|
|
|
currentContent: currentNode?.content || '',
|
|
|
|
|
|
prompt: prompt
|
2026-03-09 14:15:00 +08:00
|
|
|
|
}, { timeout: 30000 });
|
2026-03-06 13:16:54 +08:00
|
|
|
|
|
2026-03-09 14:15:00 +08:00
|
|
|
|
if (result && result.draftId) {
|
|
|
|
|
|
return result;
|
2026-03-06 13:16:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
} catch (error) {
|
2026-03-09 14:15:00 +08:00
|
|
|
|
console.error('AI改写提交失败:', error?.errMsg || error?.message || JSON.stringify(error));
|
2026-03-06 13:16:54 +08:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 14:15:00 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取用户草稿列表
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getDrafts(userId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await get(`/drafts?userId=${userId}`);
|
|
|
|
|
|
return result || [];
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取草稿列表失败:', error);
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 检查是否有新完成的草稿
|
|
|
|
|
|
*/
|
|
|
|
|
|
async checkNewDrafts(userId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await get(`/drafts/check-new?userId=${userId}`);
|
|
|
|
|
|
return result || { hasNew: false, count: 0, drafts: [] };
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('检查新草稿失败:', error);
|
|
|
|
|
|
return { hasNew: false, count: 0, drafts: [] };
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 批量标记所有未读草稿为已读
|
|
|
|
|
|
*/
|
|
|
|
|
|
async markAllDraftsRead(userId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await request({ url: `/drafts/batch-read?userId=${userId}`, method: 'PUT' });
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('批量标记已读失败:', error);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取草稿详情
|
|
|
|
|
|
*/
|
|
|
|
|
|
async getDraftDetail(draftId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await get(`/drafts/${draftId}`);
|
|
|
|
|
|
return result;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取草稿详情失败:', error);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 删除草稿
|
|
|
|
|
|
*/
|
|
|
|
|
|
async deleteDraft(draftId, userId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await request({
|
|
|
|
|
|
url: `/drafts/${draftId}?userId=${userId}`,
|
|
|
|
|
|
method: 'DELETE'
|
|
|
|
|
|
});
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('删除草稿失败:', error);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从草稿加载并播放 AI 生成的内容
|
|
|
|
|
|
*/
|
|
|
|
|
|
loadDraftContent(draft) {
|
|
|
|
|
|
if (!draft || !draft.aiNodes) return null;
|
|
|
|
|
|
|
|
|
|
|
|
// 将 AI 生成的节点合并到当前故事
|
|
|
|
|
|
if (this.currentStory) {
|
|
|
|
|
|
Object.assign(this.currentStory.nodes, draft.aiNodes);
|
|
|
|
|
|
this.currentNodeKey = draft.entryNodeKey || 'branch_1';
|
|
|
|
|
|
return this.getCurrentNode();
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-03 17:06:08 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* AI续写故事
|
|
|
|
|
|
*/
|
|
|
|
|
|
async continueStory(storyId, prompt) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await post(`/stories/${storyId}/continue`, {
|
|
|
|
|
|
current_node_key: this.currentNodeKey,
|
|
|
|
|
|
prompt: prompt
|
|
|
|
|
|
});
|
|
|
|
|
|
return result;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('AI续写失败:', error);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* AI创作新故事
|
|
|
|
|
|
*/
|
|
|
|
|
|
async createStory(params) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const result = await post('/stories/ai-create', {
|
|
|
|
|
|
genre: params.genre,
|
|
|
|
|
|
keywords: params.keywords,
|
|
|
|
|
|
protagonist: params.protagonist,
|
|
|
|
|
|
conflict: params.conflict
|
|
|
|
|
|
});
|
|
|
|
|
|
return result;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('AI创作失败:', error);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-03 16:57:49 +08:00
|
|
|
|
}
|