feat: v0.53.0 - 统一对话系统与内容创作增强

Features

统一对话系统
- 新增 useUnifiedChat Hook 支持多种对话模式 (Agent/General/Creator)
- 新增统一对话 API 封装 (unified-chat.ts)
- 新增 Chat 类型定义系统 (chat.ts)
- 支持流式响应和工具调用

内容创作增强
- 新增 Content Creator 文档和系统提示词
- 新增 writeFile 解析器支持画布输出
- 优化 General Chat 组件支持内容创作模式
- 支持多种创作主题 (社交媒体/海报/音乐/知识/规划/文档/视频/小说)

会话管理优化
- 增强 Aster Agent 状态管理
- 新增统一会话命令 (unified_chat_cmd)
- 新增 Chat DAO 数据访问层
- 优化数据库迁移脚本

Bug Fixes
- 修复 ESLint 警告和未使用变量问题
- 修复 React Hooks 依赖警告

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
coso
2026-02-03 00:01:34 +08:00
co-authored by Claude Opus 4.5
parent be4c4641c2
commit 91fe7ff481
37 changed files with 3645 additions and 97 deletions
+67
View File
@@ -0,0 +1,67 @@
# WriteFile 解析器模块
## 概述
本模块提供 `<write_file>` 标签的流式解析功能,用于从 AI 响应中提取文件内容并实时显示在画布中。
## 文件索引
| 文件 | 说明 |
|------|------|
| `parser.ts` | WriteFile 标签解析器实现 |
| `index.ts` | 模块导出 |
## 核心功能
### WriteFileParser
流式解析 `<write_file path="xxx.md">...</write_file>` 标签。
```typescript
import { WriteFileParser } from "@/lib/writeFile";
// 静态方法:一次性解析
const result = WriteFileParser.parse(text);
// 实例方法:流式解析
const parser = new WriteFileParser();
const result1 = parser.parse(partialText);
const result2 = parser.parse(moreText);
```
### 解析结果
```typescript
interface WriteFileParseResult {
blocks: WriteFileBlock[]; // 解析出的文件块
plainText: string; // 去除标签后的纯文本
hasStreamingBlock: boolean; // 是否有正在解析的块
}
interface WriteFileBlock {
path: string; // 文件路径
content: string; // 文件内容
isComplete: boolean; // 是否解析完成
startIndex: number; // 起始位置
endIndex: number; // 结束位置
}
```
## 使用场景
1. **流式画布更新**:AI 输出 `<write_file>` 标签时,实时解析并更新画布内容
2. **内容分离**:将文件内容从聊天消息中分离,分别显示在画布和聊天区域
## 与系统提示词配合
系统提示词 (`systemPrompt.ts`) 指导 AI 使用 `<write_file>` 标签输出长文内容:
```markdown
<write_file path="draft.md">
# 文章标题
内容...
</write_file>
```
前端检测到此标签后,自动打开画布并流式显示内容。
+8
View File
@@ -0,0 +1,8 @@
/**
* @file WriteFile 模块导出
* @description 导出 WriteFile 解析器相关功能
* @module lib/writeFile
*/
export { WriteFileParser } from "./parser";
export type { WriteFileBlock, WriteFileParseResult } from "./parser";
+205
View File
@@ -0,0 +1,205 @@
/**
* @file WriteFile 标签解析器
* @description 从 AI 响应中流式解析 <write_file path="xxx.md"> 标签
* @module lib/writeFile/parser
*
* 支持流式解析,实时提取文件内容用于画布显示
*/
/**
* WriteFile 解析结果
*/
export interface WriteFileBlock {
/** 文件路径 */
path: string;
/** 文件内容 */
content: string;
/** 是否解析完成 */
isComplete: boolean;
/** 原始标签在文本中的起始位置 */
startIndex: number;
/** 原始标签在文本中的结束位置 */
endIndex: number;
}
/**
* 解析结果
*/
export interface WriteFileParseResult {
/** 解析出的文件块列表 */
blocks: WriteFileBlock[];
/** 去除 write_file 标签后的纯文本 */
plainText: string;
/** 是否有正在解析中的块 */
hasStreamingBlock: boolean;
}
/**
* 解析器状态
*/
interface ParserState {
/** 当前正在解析的块 */
currentBlock: WriteFileBlock | null;
/** 已完成的块 */
completedBlocks: WriteFileBlock[];
/** 纯文本部分 */
plainTextParts: string[];
/** 上次处理的位置 */
lastProcessedIndex: number;
}
/**
* WriteFile 标签解析器
* 支持流式解析 <write_file path="xxx.md">...</write_file> 标签
*/
export class WriteFileParser {
private state: ParserState;
constructor() {
this.state = {
currentBlock: null,
completedBlocks: [],
plainTextParts: [],
lastProcessedIndex: 0,
};
}
/**
* 重置解析器状态
*/
reset(): void {
this.state = {
currentBlock: null,
completedBlocks: [],
plainTextParts: [],
lastProcessedIndex: 0,
};
}
/**
* 解析文本内容(支持流式调用)
* @param text 完整的文本内容(包含之前的内容)
* @returns 解析结果
*/
parse(text: string): WriteFileParseResult {
// 重置状态,重新解析整个文本
this.reset();
let currentIndex = 0;
const openTagRegex = /<write_file\s+path\s*=\s*["']([^"']+)["']\s*>/gi;
const closeTag = "</write_file>";
while (currentIndex < text.length) {
// 如果当前在块内,查找结束标签
if (this.state.currentBlock) {
const closeIndex = text
.toLowerCase()
.indexOf(closeTag.toLowerCase(), currentIndex);
if (closeIndex !== -1) {
// 找到结束标签,完成当前块
this.state.currentBlock.content = text.slice(
this.state.currentBlock.startIndex +
this.getOpenTagLength(text, this.state.currentBlock.startIndex),
closeIndex,
);
this.state.currentBlock.isComplete = true;
this.state.currentBlock.endIndex = closeIndex + closeTag.length;
this.state.completedBlocks.push(this.state.currentBlock);
this.state.currentBlock = null;
currentIndex = closeIndex + closeTag.length;
} else {
// 没有找到结束标签,内容还在流式传输中
this.state.currentBlock.content = text.slice(
this.state.currentBlock.startIndex +
this.getOpenTagLength(text, this.state.currentBlock.startIndex),
);
this.state.currentBlock.endIndex = text.length;
break;
}
} else {
// 查找开始标签
openTagRegex.lastIndex = currentIndex;
const match = openTagRegex.exec(text);
if (match) {
// 保存开始标签之前的纯文本
if (match.index > currentIndex) {
this.state.plainTextParts.push(
text.slice(currentIndex, match.index),
);
}
// 创建新的块
this.state.currentBlock = {
path: match[1],
content: "",
isComplete: false,
startIndex: match.index,
endIndex: match.index + match[0].length,
};
currentIndex = match.index + match[0].length;
} else {
// 没有找到开始标签,剩余都是纯文本
this.state.plainTextParts.push(text.slice(currentIndex));
break;
}
}
}
return this.getResult();
}
/**
* 获取开始标签的长度
*/
private getOpenTagLength(text: string, startIndex: number): number {
const openTagRegex = /<write_file\s+path\s*=\s*["'][^"']+["']\s*>/gi;
openTagRegex.lastIndex = startIndex;
const match = openTagRegex.exec(text);
return match ? match[0].length : 0;
}
/**
* 获取解析结果
*/
private getResult(): WriteFileParseResult {
const blocks = [...this.state.completedBlocks];
// 如果有正在解析的块,也加入结果
if (this.state.currentBlock) {
blocks.push(this.state.currentBlock);
}
return {
blocks,
plainText: this.state.plainTextParts.join(""),
hasStreamingBlock: this.state.currentBlock !== null,
};
}
/**
* 静态方法:一次性解析完整文本
*/
static parse(text: string): WriteFileParseResult {
const parser = new WriteFileParser();
return parser.parse(text);
}
/**
* 检查文本是否包含 write_file 标签
*/
static hasWriteFileTag(text: string): boolean {
return /<write_file\s+path\s*=\s*["'][^"']+["']\s*>/i.test(text);
}
/**
* 获取第一个 write_file 块(用于快速检测)
*/
static getFirstBlock(text: string): WriteFileBlock | null {
const result = WriteFileParser.parse(text);
return result.blocks.length > 0 ? result.blocks[0] : null;
}
}
export default WriteFileParser;