mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
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:
@@ -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>
|
||||
```
|
||||
|
||||
前端检测到此标签后,自动打开画布并流式显示内容。
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @file WriteFile 模块导出
|
||||
* @description 导出 WriteFile 解析器相关功能
|
||||
* @module lib/writeFile
|
||||
*/
|
||||
|
||||
export { WriteFileParser } from "./parser";
|
||||
export type { WriteFileBlock, WriteFileParseResult } from "./parser";
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user