Compare commits
2 Commits
dd32db7677
...
87ceaad92b
| Author | SHA1 | Date | |
|---|---|---|---|
| 87ceaad92b | |||
| 09654b7d4b |
228
CLAUDE.md
Normal file
228
CLAUDE.md
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
多用户 AI Agent 系统,为每个登录用户提供独立的 Docker 容器环境。支持终端交互、文件操作、对话管理和实时监控。主要用于学习和实验"智能体 + 真实 Dev Workflow",代码大量由 AI 生成。
|
||||||
|
|
||||||
|
## Core Architecture
|
||||||
|
|
||||||
|
### Entry Points
|
||||||
|
- **CLI模式**: `python main.py` - 启动命令行交互式 Agent
|
||||||
|
- **Web模式**: `python web_server.py` - 启动 Flask + Socket.IO Web 服务器 (默认端口 8091)
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
|
||||||
|
**core/**
|
||||||
|
- `MainTerminal` - CLI 主终端,处理用户输入和 AI 对话循环
|
||||||
|
- `WebTerminal` - Web 终端,继承 MainTerminal,增加实时推送和对话持久化
|
||||||
|
- `tool_config.py` - 定义所有工具的配置和分类
|
||||||
|
|
||||||
|
**modules/**
|
||||||
|
- `user_container_manager.py` - 单用户-单容器管理,负责容器生命周期
|
||||||
|
- `container_file_proxy.py` - 容器内文件代理,通过 `docker exec` 沙箱化文件操作
|
||||||
|
- `terminal_manager.py` - 管理多个持久化终端会话
|
||||||
|
- `file_manager.py` - 文件 CRUD,包含搜索、替换和 append/modify streaming 功能
|
||||||
|
- `conversation_manager.py` - 对话历史持久化
|
||||||
|
- `context_manager.py` - 上下文构建、token统计、对话管理
|
||||||
|
- `memory_manager.py` - 长期记忆管理
|
||||||
|
- `todo_manager.py` - 待办事项管理
|
||||||
|
- `sub_agent_manager.py` - 子智能体任务调度
|
||||||
|
- `user_manager.py` - 用户认证和工作区管理
|
||||||
|
- `upload_security.py` - 上传文件隔离扫描
|
||||||
|
|
||||||
|
**utils/**
|
||||||
|
- `api_client.py` - 与 OpenAI-compatible API 交互,支持 thinking mode(首次思考,后续快速)
|
||||||
|
- `terminal_factory.py` - 终端类型工厂
|
||||||
|
|
||||||
|
**static/**
|
||||||
|
- Vue + Socket.IO 前端,包含对话流、终端输出、资源监控面板
|
||||||
|
|
||||||
|
**docker/**
|
||||||
|
- `terminal.Dockerfile` - 终端容器镜像构建配置
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Build & Run
|
||||||
|
```bash
|
||||||
|
# 安装依赖(需要在项目根目录创建 requirements.txt)
|
||||||
|
pip install flask flask-socketio flask-cors openai python-dotenv tiktoken lxml
|
||||||
|
|
||||||
|
# 构建终端镜像
|
||||||
|
docker build -f docker/terminal.Dockerfile -t my-agent-shell:latest .
|
||||||
|
|
||||||
|
# CLI 模式
|
||||||
|
python main.py
|
||||||
|
|
||||||
|
# Web 模式
|
||||||
|
python web_server.py
|
||||||
|
# 或指定端口和思考模式
|
||||||
|
python web_server.py --port 8091 --thinking-mode
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing & Debugging
|
||||||
|
```bash
|
||||||
|
# 查看容器状态
|
||||||
|
docker ps -a | grep agent-term
|
||||||
|
|
||||||
|
# 查看调试日志
|
||||||
|
tail -f logs/debug_stream.log
|
||||||
|
|
||||||
|
# 查看容器统计
|
||||||
|
tail -f logs/container_stats.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
主要配置文件:`.env`(复制 `.env.example`)
|
||||||
|
|
||||||
|
**必填变量**:
|
||||||
|
- `AGENT_API_BASE_URL` - OpenAI-compatible API 端点
|
||||||
|
- `AGENT_API_KEY` - API 密钥
|
||||||
|
- `AGENT_MODEL_ID` - 模型 ID (例如 deepseek-chat)
|
||||||
|
- `WEB_SECRET_KEY` - Flask session 密钥
|
||||||
|
|
||||||
|
**容器配置** (`config/terminal.py`):
|
||||||
|
- `TERMINAL_SANDBOX_MODE` - docker/host
|
||||||
|
- `TERMINAL_SANDBOX_IMAGE` - 容器镜像 (默认 python:3.11-slim)
|
||||||
|
- `TERMINAL_SANDBOX_NETWORK` - 网络模式 (bridge/none/host)
|
||||||
|
- `TERMINAL_SANDBOX_CPUS` - CPU 限制
|
||||||
|
- `TERMINAL_SANDBOX_MEMORY` - 内存限制
|
||||||
|
|
||||||
|
**资源限制**:
|
||||||
|
- `PROJECT_MAX_STORAGE_MB` - 单用户磁盘配额 (默认 2048MB)
|
||||||
|
- `MAX_ACTIVE_USER_CONTAINERS` - 并发容器数量 (默认 8)
|
||||||
|
|
||||||
|
## Important Implementation Details
|
||||||
|
|
||||||
|
### File Operations with Streaming
|
||||||
|
|
||||||
|
**append_to_file** 和 **modify_file** 使用特殊的流式输出格式:
|
||||||
|
|
||||||
|
```
|
||||||
|
<<<APPEND:path/to/file>>>
|
||||||
|
文件内容...
|
||||||
|
<<<END_APPEND>>>
|
||||||
|
|
||||||
|
<<<MODIFY:path/to/file>>>
|
||||||
|
[replace:1]
|
||||||
|
<<OLD>>原文内容<<END>>
|
||||||
|
<<NEW>>新内容<<END>>
|
||||||
|
[/replace]
|
||||||
|
<<<END_MODIFY>>>
|
||||||
|
```
|
||||||
|
|
||||||
|
处理逻辑在 `web_server.py` 的 `handle_task_with_sender` 中,通过检测标记并在流式输出中即时执行。
|
||||||
|
|
||||||
|
### Container Architecture
|
||||||
|
|
||||||
|
每个用户登录时:
|
||||||
|
1. `UserContainerManager` 创建专属容器
|
||||||
|
2. 容器挂载 `users/<username>/project/` 到 `/workspace`
|
||||||
|
3. 所有文件操作通过 `ContainerFileProxy` 在容器内执行
|
||||||
|
4. 终端会话通过 `docker exec` 运行
|
||||||
|
5. 空闲超时或登出后自动销毁容器
|
||||||
|
|
||||||
|
### Context Management
|
||||||
|
|
||||||
|
`ContextManager` 负责:
|
||||||
|
- 动态构建上下文(系统提示 + 工具 + 对话历史 + 聚焦文件 + 记忆)
|
||||||
|
- Token 统计和限制检查
|
||||||
|
- 对话历史保存和加载
|
||||||
|
- 自动保存机制(每次 API 调用后增量保存)
|
||||||
|
|
||||||
|
### Thinking Mode
|
||||||
|
|
||||||
|
支持"思考模式"(需配置 `AGENT_THINKING_MODEL_ID`):
|
||||||
|
- 首次调用使用 reasoning 模型(如 deepseek-reasoner)
|
||||||
|
- 后续调用使用快速模型
|
||||||
|
- 可通过 `THINKING_FAST_INTERVAL` 控制切换频率
|
||||||
|
- 失败时自动回退到思考模式
|
||||||
|
|
||||||
|
### Web Socket Events
|
||||||
|
|
||||||
|
主要事件:
|
||||||
|
- `send_message` - 发送用户消息
|
||||||
|
- `thinking_start/chunk/end` - 思考过程
|
||||||
|
- `text_start/chunk/end` - 文本响应
|
||||||
|
- `tool_preparing/start/update_action` - 工具执行状态
|
||||||
|
- `conversation_list_update` - 对话列表更新
|
||||||
|
- `terminal_output` - 终端输出
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Adding New Tools
|
||||||
|
|
||||||
|
1. 在 `MainTerminal` 添加 `handle_<tool_name>` 方法
|
||||||
|
2. 在 `core/tool_config.py` 的 `define_tools()` 中注册工具
|
||||||
|
3. 如果需要在流式输出中处理,在 `web_server.py` 添加特殊逻辑
|
||||||
|
|
||||||
|
### Working with Containers
|
||||||
|
|
||||||
|
使用 `ContainerHandle`:
|
||||||
|
```python
|
||||||
|
# 文件操作
|
||||||
|
result = container_handle.execute_file_op("read", path="/workspace/file.txt")
|
||||||
|
|
||||||
|
# 执行命令
|
||||||
|
result = container_handle.run_command("ls -la /workspace")
|
||||||
|
|
||||||
|
# 终端会话
|
||||||
|
result = container_handle.start_terminal(session_name="main", working_dir="/workspace")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Managing Conversations
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 创建新对话
|
||||||
|
terminal.create_new_conversation(thinking_mode=True)
|
||||||
|
|
||||||
|
# 加载对话
|
||||||
|
terminal.load_conversation(conversation_id)
|
||||||
|
|
||||||
|
# 搜索对话
|
||||||
|
terminal.search_conversations(query="bug fix")
|
||||||
|
|
||||||
|
# 删除对话
|
||||||
|
terminal.delete_conversation(conversation_id)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- 所有用户操作在独立容器中执行,与宿主机隔离
|
||||||
|
- 文件操作路径验证在 `FileManager._validate_path`
|
||||||
|
- 上传文件经过 `UploadQuarantineManager` 隔离和扫描
|
||||||
|
- CSRF 保护在 Web 端启用
|
||||||
|
- 登录失败次数限制和临时锁定
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
- 代码大量由 AI 生成,未达到生产级别
|
||||||
|
- 容器资源配额依赖 Docker cgroups
|
||||||
|
- 前端处于改造中,部分功能可能不稳定
|
||||||
|
- Token 统计基于 tiktoken,可能与实际 API 有偏差
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
agents/
|
||||||
|
├── core/ # 核心终端和工具配置
|
||||||
|
├── modules/ # 独立功能模块
|
||||||
|
├── utils/ # 辅助工具
|
||||||
|
├── config/ # 配置文件
|
||||||
|
├── static/ # Web 前端
|
||||||
|
├── docker/ # 容器镜像
|
||||||
|
├── prompts/ # 系统提示词
|
||||||
|
├── users/ # 用户工作区
|
||||||
|
├── data/ # 全局数据
|
||||||
|
└── logs/ # 日志文件
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Tips
|
||||||
|
|
||||||
|
- 修改系统提示词:编辑 `prompts/main_system.txt`
|
||||||
|
- 调整工具限制:修改 `config/limits.py`
|
||||||
|
- 容器镜像定制:编辑 `docker/terminal.Dockerfile` 并重新构建
|
||||||
|
- 前端开发:`npm install && npm run dev` (在 `static/` 目录)
|
||||||
|
- 查看详细执行流程:监控 `logs/debug_stream.log`
|
||||||
@ -69,6 +69,7 @@
|
|||||||
@toggle-panel-menu="togglePanelMenu"
|
@toggle-panel-menu="togglePanelMenu"
|
||||||
@select-panel="selectPanelMode"
|
@select-panel="selectPanelMode"
|
||||||
@open-file-manager="openGuiFileManager"
|
@open-file-manager="openGuiFileManager"
|
||||||
|
@toggle-thinking-mode="handleQuickModeToggle"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -96,6 +96,14 @@ if (window.visualViewport) {
|
|||||||
window.visualViewport.addEventListener('scroll', updateViewportHeightVar);
|
window.visualViewport.addEventListener('scroll', updateViewportHeightVar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ENABLE_APP_DEBUG_LOGS = false;
|
||||||
|
function debugLog(...args) {
|
||||||
|
if (!ENABLE_APP_DEBUG_LOGS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debugLog(...args);
|
||||||
|
}
|
||||||
|
|
||||||
const appOptions = {
|
const appOptions = {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -149,7 +157,7 @@ const appOptions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
console.log('Vue应用已挂载');
|
debugLog('Vue应用已挂载');
|
||||||
if (window.ensureCsrfToken) {
|
if (window.ensureCsrfToken) {
|
||||||
window.ensureCsrfToken().catch((err) => {
|
window.ensureCsrfToken().catch((err) => {
|
||||||
console.warn('CSRF token 初始化失败:', err);
|
console.warn('CSRF token 初始化失败:', err);
|
||||||
@ -289,7 +297,7 @@ const appOptions = {
|
|||||||
currentConversationId: {
|
currentConversationId: {
|
||||||
immediate: false,
|
immediate: false,
|
||||||
handler(newValue, oldValue) {
|
handler(newValue, oldValue) {
|
||||||
console.log('currentConversationId 变化', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload });
|
debugLog('currentConversationId 变化', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload });
|
||||||
this.logMessageState('watch:currentConversationId', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload });
|
this.logMessageState('watch:currentConversationId', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload });
|
||||||
if (!newValue || typeof newValue !== 'string' || newValue.startsWith('temp_')) {
|
if (!newValue || typeof newValue !== 'string' || newValue.startsWith('temp_')) {
|
||||||
return;
|
return;
|
||||||
@ -593,7 +601,7 @@ const appOptions = {
|
|||||||
},
|
},
|
||||||
logMessageState(action, extra = {}) {
|
logMessageState(action, extra = {}) {
|
||||||
const count = Array.isArray(this.messages) ? this.messages.length : 'N/A';
|
const count = Array.isArray(this.messages) ? this.messages.length : 'N/A';
|
||||||
console.log('[Messages]', {
|
debugLog('[Messages]', {
|
||||||
action,
|
action,
|
||||||
count,
|
count,
|
||||||
conversationId: this.currentConversationId,
|
conversationId: this.currentConversationId,
|
||||||
@ -913,9 +921,49 @@ const appOptions = {
|
|||||||
this.toolActionIndex.clear();
|
this.toolActionIndex.clear();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hasPendingToolActions() {
|
||||||
|
const mapHasEntries = map => map && typeof map.size === 'number' && map.size > 0;
|
||||||
|
if (mapHasEntries(this.preparingTools) || mapHasEntries(this.activeTools)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!Array.isArray(this.messages)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.messages.some(msg => {
|
||||||
|
if (!msg || msg.role !== 'assistant' || !Array.isArray(msg.actions)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return msg.actions.some(action => {
|
||||||
|
if (!action || action.type !== 'tool' || !action.tool) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (action.tool.awaiting_content) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const status = typeof action.tool.status === 'string'
|
||||||
|
? action.tool.status.toLowerCase()
|
||||||
|
: '';
|
||||||
|
return !status || ['preparing', 'running', 'pending', 'queued'].includes(status);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
maybeResetStreamingState(reason = 'unspecified') {
|
||||||
|
if (!this.streamingMessage) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.hasPendingToolActions()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.streamingMessage = false;
|
||||||
|
this.stopRequested = false;
|
||||||
|
debugLog('流式状态已结束', { reason });
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
// 完整重置所有状态
|
// 完整重置所有状态
|
||||||
resetAllStates(reason = 'unspecified') {
|
resetAllStates(reason = 'unspecified') {
|
||||||
console.log('重置所有前端状态', { reason, conversationId: this.currentConversationId });
|
debugLog('重置所有前端状态', { reason, conversationId: this.currentConversationId });
|
||||||
this.logMessageState('resetAllStates:before-cleanup', { reason });
|
this.logMessageState('resetAllStates:before-cleanup', { reason });
|
||||||
this.fileHideContextMenu();
|
this.fileHideContextMenu();
|
||||||
|
|
||||||
@ -960,7 +1008,7 @@ const appOptions = {
|
|||||||
this.toolSetSettingsLoading(false);
|
this.toolSetSettingsLoading(false);
|
||||||
this.toolSetSettings([]);
|
this.toolSetSettings([]);
|
||||||
|
|
||||||
console.log('前端状态重置完成');
|
debugLog('前端状态重置完成');
|
||||||
this._scrollListenerReady = false;
|
this._scrollListenerReady = false;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.ensureScrollListener();
|
this.ensureScrollListener();
|
||||||
@ -983,7 +1031,7 @@ const appOptions = {
|
|||||||
|
|
||||||
async loadInitialData() {
|
async loadInitialData() {
|
||||||
try {
|
try {
|
||||||
console.log('加载初始数据...');
|
debugLog('加载初始数据...');
|
||||||
|
|
||||||
await this.fileFetchTree();
|
await this.fileFetchTree();
|
||||||
await this.focusFetchFiles();
|
await this.focusFetchFiles();
|
||||||
@ -1003,26 +1051,28 @@ const appOptions = {
|
|||||||
if (!this.currentConversationId) {
|
if (!this.currentConversationId) {
|
||||||
this.skipConversationHistoryReload = true;
|
this.skipConversationHistoryReload = true;
|
||||||
this.currentConversationId = statusConversationId;
|
this.currentConversationId = statusConversationId;
|
||||||
}
|
|
||||||
// 如果有当前对话,尝试获取标题和Token统计
|
// 如果有当前对话,尝试获取标题和历史
|
||||||
try {
|
try {
|
||||||
const convResponse = await fetch(`/api/conversations/current`);
|
const convResponse = await fetch(`/api/conversations/current`);
|
||||||
const convData = await convResponse.json();
|
const convData = await convResponse.json();
|
||||||
if (convData.success && convData.data) {
|
if (convData.success && convData.data) {
|
||||||
this.currentConversationTitle = convData.data.title;
|
this.currentConversationTitle = convData.data.title;
|
||||||
|
}
|
||||||
|
// 初始化时调用一次,因为 skipConversationHistoryReload 会阻止 watch 触发
|
||||||
|
await this.fetchAndDisplayHistory();
|
||||||
|
// 获取当前对话的Token统计
|
||||||
|
this.fetchConversationTokenStatistics();
|
||||||
|
this.updateCurrentContextTokens();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('获取当前对话标题失败:', e);
|
||||||
}
|
}
|
||||||
await this.fetchAndDisplayHistory();
|
|
||||||
// 获取当前对话的Token统计
|
|
||||||
this.fetchConversationTokenStatistics();
|
|
||||||
this.updateCurrentContextTokens();
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('获取当前对话标题失败:', e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.loadToolSettings(true);
|
await this.loadToolSettings(true);
|
||||||
|
|
||||||
console.log('初始数据加载完成');
|
debugLog('初始数据加载完成');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载初始数据失败:', error);
|
console.error('加载初始数据失败:', error);
|
||||||
}
|
}
|
||||||
@ -1108,7 +1158,7 @@ const appOptions = {
|
|||||||
this.promoteConversationToTop(this.currentConversationId);
|
this.promoteConversationToTop(this.currentConversationId);
|
||||||
}
|
}
|
||||||
this.hasMoreConversations = data.data.has_more;
|
this.hasMoreConversations = data.data.has_more;
|
||||||
console.log(`已加载 ${this.conversations.length} 个对话`);
|
debugLog(`已加载 ${this.conversations.length} 个对话`);
|
||||||
|
|
||||||
if (this.conversationsOffset === 0 && !this.currentConversationId && this.conversations.length > 0) {
|
if (this.conversationsOffset === 0 && !this.currentConversationId && this.conversations.length > 0) {
|
||||||
const latestConversation = this.conversations[0];
|
const latestConversation = this.conversations[0];
|
||||||
@ -1136,11 +1186,11 @@ const appOptions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async loadConversation(conversationId) {
|
async loadConversation(conversationId) {
|
||||||
console.log('加载对话:', conversationId);
|
debugLog('加载对话:', conversationId);
|
||||||
this.logMessageState('loadConversation:start', { conversationId });
|
this.logMessageState('loadConversation:start', { conversationId });
|
||||||
|
|
||||||
if (conversationId === this.currentConversationId) {
|
if (conversationId === this.currentConversationId) {
|
||||||
console.log('已是当前对话,跳过加载');
|
debugLog('已是当前对话,跳过加载');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1152,7 +1202,7 @@ const appOptions = {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log('对话加载API成功:', result);
|
debugLog('对话加载API成功:', result);
|
||||||
|
|
||||||
// 2. 更新当前对话信息
|
// 2. 更新当前对话信息
|
||||||
this.skipConversationHistoryReload = true;
|
this.skipConversationHistoryReload = true;
|
||||||
@ -1166,17 +1216,13 @@ const appOptions = {
|
|||||||
this.resetAllStates(`loadConversation:${conversationId}`);
|
this.resetAllStates(`loadConversation:${conversationId}`);
|
||||||
this.subAgentFetch();
|
this.subAgentFetch();
|
||||||
this.fetchTodoList();
|
this.fetchTodoList();
|
||||||
|
|
||||||
// 4. 延迟获取并显示历史对话内容(关键功能)
|
// 4. 历史对话内容和Token统计由后端的 conversation_loaded 事件触发
|
||||||
setTimeout(() => {
|
// 不在此处重复调用,避免双重加载
|
||||||
this.fetchAndDisplayHistory();
|
// Socket.IO 的 conversation_loaded 事件会处理:
|
||||||
}, 300);
|
// - fetchAndDisplayHistory()
|
||||||
|
// - fetchConversationTokenStatistics()
|
||||||
// 5. 获取Token统计(重点:加载历史累计统计+当前上下文)
|
// - updateCurrentContextTokens()
|
||||||
setTimeout(() => {
|
|
||||||
this.fetchConversationTokenStatistics();
|
|
||||||
this.updateCurrentContextTokens();
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.error('对话加载失败:', result.message);
|
console.error('对话加载失败:', result.message);
|
||||||
@ -1212,16 +1258,16 @@ const appOptions = {
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
async fetchAndDisplayHistory() {
|
async fetchAndDisplayHistory() {
|
||||||
if (this.historyLoading) {
|
if (this.historyLoading) {
|
||||||
console.log('历史消息正在加载,跳过重复请求');
|
debugLog('历史消息正在加载,跳过重复请求');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.historyLoading = true;
|
this.historyLoading = true;
|
||||||
try {
|
try {
|
||||||
console.log('开始获取历史对话内容...');
|
debugLog('开始获取历史对话内容...');
|
||||||
this.logMessageState('fetchAndDisplayHistory:start', { conversationId: this.currentConversationId });
|
this.logMessageState('fetchAndDisplayHistory:start', { conversationId: this.currentConversationId });
|
||||||
|
|
||||||
if (!this.currentConversationId || this.currentConversationId.startsWith('temp_')) {
|
if (!this.currentConversationId || this.currentConversationId.startsWith('temp_')) {
|
||||||
console.log('没有当前对话ID,跳过历史加载');
|
debugLog('没有当前对话ID,跳过历史加载');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1234,7 +1280,7 @@ const appOptions = {
|
|||||||
// 备用方案:通过状态API获取
|
// 备用方案:通过状态API获取
|
||||||
const statusResponse = await fetch('/api/status');
|
const statusResponse = await fetch('/api/status');
|
||||||
const status = await statusResponse.json();
|
const status = await statusResponse.json();
|
||||||
console.log('系统状态:', status);
|
debugLog('系统状态:', status);
|
||||||
this.applyStatusSnapshot(status);
|
this.applyStatusSnapshot(status);
|
||||||
|
|
||||||
// 如果状态中有对话历史字段
|
// 如果状态中有对话历史字段
|
||||||
@ -1243,16 +1289,16 @@ const appOptions = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('备用方案也无法获取历史消息');
|
debugLog('备用方案也无法获取历史消息');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messagesData = await messagesResponse.json();
|
const messagesData = await messagesResponse.json();
|
||||||
console.log('获取到消息数据:', messagesData);
|
debugLog('获取到消息数据:', messagesData);
|
||||||
|
|
||||||
if (messagesData.success && messagesData.data && messagesData.data.messages) {
|
if (messagesData.success && messagesData.data && messagesData.data.messages) {
|
||||||
const messages = messagesData.data.messages;
|
const messages = messagesData.data.messages;
|
||||||
console.log(`发现 ${messages.length} 条历史消息`);
|
debugLog(`发现 ${messages.length} 条历史消息`);
|
||||||
|
|
||||||
if (messages.length > 0) {
|
if (messages.length > 0) {
|
||||||
// 清空当前显示的消息
|
// 清空当前显示的消息
|
||||||
@ -1268,15 +1314,15 @@ const appOptions = {
|
|||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('历史对话内容显示完成');
|
debugLog('历史对话内容显示完成');
|
||||||
} else {
|
} else {
|
||||||
console.log('对话存在但没有历史消息');
|
debugLog('对话存在但没有历史消息');
|
||||||
this.logMessageState('fetchAndDisplayHistory:no-history-clear');
|
this.logMessageState('fetchAndDisplayHistory:no-history-clear');
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
this.logMessageState('fetchAndDisplayHistory:no-history-cleared');
|
this.logMessageState('fetchAndDisplayHistory:no-history-cleared');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('消息数据格式不正确:', messagesData);
|
debugLog('消息数据格式不正确:', messagesData);
|
||||||
this.logMessageState('fetchAndDisplayHistory:invalid-data-clear');
|
this.logMessageState('fetchAndDisplayHistory:invalid-data-clear');
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
this.logMessageState('fetchAndDisplayHistory:invalid-data-cleared');
|
this.logMessageState('fetchAndDisplayHistory:invalid-data-cleared');
|
||||||
@ -1284,7 +1330,7 @@ const appOptions = {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取历史对话失败:', error);
|
console.error('获取历史对话失败:', error);
|
||||||
console.log('尝试不显示错误弹窗,仅在控制台记录');
|
debugLog('尝试不显示错误弹窗,仅在控制台记录');
|
||||||
// 不显示alert,避免打断用户体验
|
// 不显示alert,避免打断用户体验
|
||||||
this.logMessageState('fetchAndDisplayHistory:error-clear', { error: error?.message || String(error) });
|
this.logMessageState('fetchAndDisplayHistory:error-clear', { error: error?.message || String(error) });
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
@ -1299,8 +1345,8 @@ const appOptions = {
|
|||||||
// 关键功能:渲染历史消息
|
// 关键功能:渲染历史消息
|
||||||
// ==========================================
|
// ==========================================
|
||||||
renderHistoryMessages(historyMessages) {
|
renderHistoryMessages(historyMessages) {
|
||||||
console.log('开始渲染历史消息...', historyMessages);
|
debugLog('开始渲染历史消息...', historyMessages);
|
||||||
console.log('历史消息数量:', historyMessages.length);
|
debugLog('历史消息数量:', historyMessages.length);
|
||||||
this.logMessageState('renderHistoryMessages:start', { historyCount: historyMessages.length });
|
this.logMessageState('renderHistoryMessages:start', { historyCount: historyMessages.length });
|
||||||
|
|
||||||
if (!Array.isArray(historyMessages)) {
|
if (!Array.isArray(historyMessages)) {
|
||||||
@ -1311,7 +1357,7 @@ const appOptions = {
|
|||||||
let currentAssistantMessage = null;
|
let currentAssistantMessage = null;
|
||||||
|
|
||||||
historyMessages.forEach((message, index) => {
|
historyMessages.forEach((message, index) => {
|
||||||
console.log(`处理消息 ${index + 1}/${historyMessages.length}:`, message.role, message);
|
debugLog(`处理消息 ${index + 1}/${historyMessages.length}:`, message.role, message);
|
||||||
|
|
||||||
if (message.role === 'user') {
|
if (message.role === 'user') {
|
||||||
// 用户消息 - 先结束之前的assistant消息
|
// 用户消息 - 先结束之前的assistant消息
|
||||||
@ -1324,7 +1370,7 @@ const appOptions = {
|
|||||||
role: 'user',
|
role: 'user',
|
||||||
content: message.content || ''
|
content: message.content || ''
|
||||||
});
|
});
|
||||||
console.log('添加用户消息:', message.content?.substring(0, 50) + '...');
|
debugLog('添加用户消息:', message.content?.substring(0, 50) + '...');
|
||||||
|
|
||||||
} else if (message.role === 'assistant') {
|
} else if (message.role === 'assistant') {
|
||||||
// AI消息 - 如果没有当前assistant消息,创建一个
|
// AI消息 - 如果没有当前assistant消息,创建一个
|
||||||
@ -1335,7 +1381,9 @@ const appOptions = {
|
|||||||
streamingThinking: '',
|
streamingThinking: '',
|
||||||
streamingText: '',
|
streamingText: '',
|
||||||
currentStreamingType: null,
|
currentStreamingType: null,
|
||||||
activeThinkingId: null
|
activeThinkingId: null,
|
||||||
|
awaitingFirstContent: false,
|
||||||
|
generatingLabel: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1368,7 +1416,7 @@ const appOptions = {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
blockId
|
blockId
|
||||||
});
|
});
|
||||||
console.log('添加思考内容:', reasoningText.substring(0, 50) + '...');
|
debugLog('添加思考内容:', reasoningText.substring(0, 50) + '...');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理普通文本内容(移除思考标签后的内容)
|
// 处理普通文本内容(移除思考标签后的内容)
|
||||||
@ -1402,7 +1450,7 @@ const appOptions = {
|
|||||||
},
|
},
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
});
|
});
|
||||||
console.log('添加append占位信息:', appendPayloadMeta.path);
|
debugLog('添加append占位信息:', appendPayloadMeta.path);
|
||||||
} else if (modifyPayloadMeta) {
|
} else if (modifyPayloadMeta) {
|
||||||
currentAssistantMessage.actions.push({
|
currentAssistantMessage.actions.push({
|
||||||
id: `history-modify-payload-${Date.now()}-${Math.random()}`,
|
id: `history-modify-payload-${Date.now()}-${Math.random()}`,
|
||||||
@ -1417,7 +1465,7 @@ const appOptions = {
|
|||||||
},
|
},
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
});
|
});
|
||||||
console.log('添加modify占位信息:', modifyPayloadMeta.path);
|
debugLog('添加modify占位信息:', modifyPayloadMeta.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (textContent && !appendPayloadMeta && !modifyPayloadMeta && !isAppendMessage && !isModifyMessage && !containsAppendMarkers) {
|
if (textContent && !appendPayloadMeta && !modifyPayloadMeta && !isAppendMessage && !isModifyMessage && !containsAppendMarkers) {
|
||||||
@ -1428,7 +1476,7 @@ const appOptions = {
|
|||||||
streaming: false,
|
streaming: false,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
});
|
});
|
||||||
console.log('添加文本内容:', textContent.substring(0, 50) + '...');
|
debugLog('添加文本内容:', textContent.substring(0, 50) + '...');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理工具调用
|
// 处理工具调用
|
||||||
@ -1456,7 +1504,7 @@ const appOptions = {
|
|||||||
},
|
},
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
});
|
});
|
||||||
console.log('添加工具调用:', toolCall.function.name);
|
debugLog('添加工具调用:', toolCall.function.name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1509,7 +1557,7 @@ const appOptions = {
|
|||||||
if (message.name === 'append_to_file' && result && result.message) {
|
if (message.name === 'append_to_file' && result && result.message) {
|
||||||
toolAction.tool.message = result.message;
|
toolAction.tool.message = result.message;
|
||||||
}
|
}
|
||||||
console.log(`更新工具结果: ${message.name} -> ${message.content?.substring(0, 50)}...`);
|
debugLog(`更新工具结果: ${message.name} -> ${message.content?.substring(0, 50)}...`);
|
||||||
|
|
||||||
// append_to_file 的摘要在 append_payload 占位中呈现,此处无需重复
|
// append_to_file 的摘要在 append_payload 占位中呈现,此处无需重复
|
||||||
} else {
|
} else {
|
||||||
@ -1524,7 +1572,7 @@ const appOptions = {
|
|||||||
currentAssistantMessage = null;
|
currentAssistantMessage = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('处理其他类型消息:', message.role);
|
debugLog('处理其他类型消息:', message.role);
|
||||||
this.messages.push({
|
this.messages.push({
|
||||||
role: message.role,
|
role: message.role,
|
||||||
content: message.content || ''
|
content: message.content || ''
|
||||||
@ -1537,7 +1585,7 @@ const appOptions = {
|
|||||||
this.messages.push(currentAssistantMessage);
|
this.messages.push(currentAssistantMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`历史消息渲染完成,共 ${this.messages.length} 条消息`);
|
debugLog(`历史消息渲染完成,共 ${this.messages.length} 条消息`);
|
||||||
this.logMessageState('renderHistoryMessages:after-render');
|
this.logMessageState('renderHistoryMessages:after-render');
|
||||||
|
|
||||||
// 强制更新视图
|
// 强制更新视图
|
||||||
@ -1550,7 +1598,7 @@ const appOptions = {
|
|||||||
const blockCount = this.$el && this.$el.querySelectorAll
|
const blockCount = this.$el && this.$el.querySelectorAll
|
||||||
? this.$el.querySelectorAll('.message-block').length
|
? this.$el.querySelectorAll('.message-block').length
|
||||||
: 'N/A';
|
: 'N/A';
|
||||||
console.log('[Messages] DOM 渲染统计', {
|
debugLog('[Messages] DOM 渲染统计', {
|
||||||
blocks: blockCount,
|
blocks: blockCount,
|
||||||
conversationId: this.currentConversationId
|
conversationId: this.currentConversationId
|
||||||
});
|
});
|
||||||
@ -1559,7 +1607,7 @@ const appOptions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async createNewConversation() {
|
async createNewConversation() {
|
||||||
console.log('创建新对话...');
|
debugLog('创建新对话...');
|
||||||
this.logMessageState('createNewConversation:start');
|
this.logMessageState('createNewConversation:start');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -1576,7 +1624,7 @@ const appOptions = {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log('新对话创建成功:', result.conversation_id);
|
debugLog('新对话创建成功:', result.conversation_id);
|
||||||
|
|
||||||
// 清空当前消息
|
// 清空当前消息
|
||||||
this.logMessageState('createNewConversation:before-clear');
|
this.logMessageState('createNewConversation:before-clear');
|
||||||
@ -1625,7 +1673,7 @@ const appOptions = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('删除对话:', conversationId);
|
debugLog('删除对话:', conversationId);
|
||||||
this.logMessageState('deleteConversation:start', { conversationId });
|
this.logMessageState('deleteConversation:start', { conversationId });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -1636,7 +1684,7 @@ const appOptions = {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log('对话删除成功');
|
debugLog('对话删除成功');
|
||||||
|
|
||||||
// 如果删除的是当前对话,清空界面
|
// 如果删除的是当前对话,清空界面
|
||||||
if (conversationId === this.currentConversationId) {
|
if (conversationId === this.currentConversationId) {
|
||||||
@ -1673,7 +1721,7 @@ const appOptions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async duplicateConversation(conversationId) {
|
async duplicateConversation(conversationId) {
|
||||||
console.log('复制对话:', conversationId);
|
debugLog('复制对话:', conversationId);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/conversations/${conversationId}/duplicate`, {
|
const response = await fetch(`/api/conversations/${conversationId}/duplicate`, {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
@ -1715,7 +1763,7 @@ const appOptions = {
|
|||||||
|
|
||||||
this.searchTimer = setTimeout(() => {
|
this.searchTimer = setTimeout(() => {
|
||||||
if (this.searchQuery.trim()) {
|
if (this.searchQuery.trim()) {
|
||||||
console.log('搜索对话:', this.searchQuery);
|
debugLog('搜索对话:', this.searchQuery);
|
||||||
// TODO: 实现搜索API调用
|
// TODO: 实现搜索API调用
|
||||||
// this.searchConversationsAPI(this.searchQuery);
|
// this.searchConversationsAPI(this.searchQuery);
|
||||||
} else {
|
} else {
|
||||||
@ -1865,7 +1913,7 @@ const appOptions = {
|
|||||||
if (this.streamingMessage && !this.stopRequested) {
|
if (this.streamingMessage && !this.stopRequested) {
|
||||||
this.socket.emit('stop_task');
|
this.socket.emit('stop_task');
|
||||||
this.stopRequested = true;
|
this.stopRequested = true;
|
||||||
console.log('发送停止请求');
|
debugLog('发送停止请求');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1919,7 +1967,7 @@ const appOptions = {
|
|||||||
if (newId) {
|
if (newId) {
|
||||||
this.currentConversationId = newId;
|
this.currentConversationId = newId;
|
||||||
}
|
}
|
||||||
console.log('对话压缩完成:', result);
|
debugLog('对话压缩完成:', result);
|
||||||
} else {
|
} else {
|
||||||
const message = result.message || result.error || '压缩失败';
|
const message = result.message || result.error || '压缩失败';
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
@ -2082,7 +2130,7 @@ const appOptions = {
|
|||||||
enabled: !!item.enabled,
|
enabled: !!item.enabled,
|
||||||
tools: Array.isArray(item.tools) ? item.tools : []
|
tools: Array.isArray(item.tools) ? item.tools : []
|
||||||
}));
|
}));
|
||||||
console.log('[ToolSettings] Snapshot applied', {
|
debugLog('[ToolSettings] Snapshot applied', {
|
||||||
received: categories.length,
|
received: categories.length,
|
||||||
normalized,
|
normalized,
|
||||||
anyEnabled: normalized.some(cat => cat.enabled),
|
anyEnabled: normalized.some(cat => cat.enabled),
|
||||||
@ -2094,23 +2142,23 @@ const appOptions = {
|
|||||||
|
|
||||||
async loadToolSettings(force = false) {
|
async loadToolSettings(force = false) {
|
||||||
if (!this.isConnected && !force) {
|
if (!this.isConnected && !force) {
|
||||||
console.log('[ToolSettings] Skip load: disconnected & not forced');
|
debugLog('[ToolSettings] Skip load: disconnected & not forced');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.toolSettingsLoading) {
|
if (this.toolSettingsLoading) {
|
||||||
console.log('[ToolSettings] Skip load: already loading');
|
debugLog('[ToolSettings] Skip load: already loading');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!force && this.toolSettings.length > 0) {
|
if (!force && this.toolSettings.length > 0) {
|
||||||
console.log('[ToolSettings] Skip load: already have settings');
|
debugLog('[ToolSettings] Skip load: already have settings');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('[ToolSettings] Fetch start', { force, hasConnection: this.isConnected });
|
debugLog('[ToolSettings] Fetch start', { force, hasConnection: this.isConnected });
|
||||||
this.toolSetSettingsLoading(true);
|
this.toolSetSettingsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/tool-settings');
|
const response = await fetch('/api/tool-settings');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('[ToolSettings] Fetch response', { status: response.status, data });
|
debugLog('[ToolSettings] Fetch response', { status: response.status, data });
|
||||||
if (response.ok && data.success && Array.isArray(data.categories)) {
|
if (response.ok && data.success && Array.isArray(data.categories)) {
|
||||||
this.applyToolSettingsSnapshot(data.categories);
|
this.applyToolSettingsSnapshot(data.categories);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -14,6 +14,27 @@
|
|||||||
<span class="icon icon-sm" :style="iconStyleSafe('bot')" aria-hidden="true"></span>
|
<span class="icon icon-sm" :style="iconStyleSafe('bot')" aria-hidden="true"></span>
|
||||||
<span>AI Assistant</span>
|
<span>AI Assistant</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="msg.awaitingFirstContent"
|
||||||
|
class="action-item streaming-content immediate-show assistant-generating-block"
|
||||||
|
>
|
||||||
|
<div class="text-output">
|
||||||
|
<div
|
||||||
|
class="text-content assistant-generating-placeholder"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-for="(letter, letterIndex) in getGeneratingLetters(msg)"
|
||||||
|
:key="letterIndex"
|
||||||
|
class="assistant-generating-letter"
|
||||||
|
:style="{ animationDelay: `${letterIndex * 0.08}s` }"
|
||||||
|
>
|
||||||
|
{{ letter }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="(action, actionIndex) in msg.actions || []"
|
v-for="(action, actionIndex) in msg.actions || []"
|
||||||
:key="action.id || `${index}-${actionIndex}`"
|
:key="action.id || `${index}-${actionIndex}`"
|
||||||
@ -181,6 +202,7 @@ const props = defineProps<{
|
|||||||
formatSearchTime: (filters: Record<string, any>) => string;
|
formatSearchTime: (filters: Record<string, any>) => string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const DEFAULT_GENERATING_TEXT = '生成中…';
|
||||||
const rootEl = ref<HTMLElement | null>(null);
|
const rootEl = ref<HTMLElement | null>(null);
|
||||||
const thinkingRefs = new Map<string, HTMLElement | null>();
|
const thinkingRefs = new Map<string, HTMLElement | null>();
|
||||||
|
|
||||||
@ -203,6 +225,14 @@ function iconStyleSafe(key: string, size?: string) {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getGeneratingLetters(message: any) {
|
||||||
|
const label =
|
||||||
|
typeof message?.generatingLabel === 'string' && message.generatingLabel.trim()
|
||||||
|
? message.generatingLabel.trim()
|
||||||
|
: DEFAULT_GENERATING_TEXT;
|
||||||
|
return Array.from(label);
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
rootEl,
|
rootEl,
|
||||||
getThinkingRef
|
getThinkingRef
|
||||||
|
|||||||
@ -11,7 +11,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-indicators">
|
<div class="status-indicators">
|
||||||
<span class="mode-indicator" :class="{ thinking: thinkingMode, fast: !thinkingMode }">
|
<button
|
||||||
|
type="button"
|
||||||
|
class="mode-indicator"
|
||||||
|
:class="{ thinking: thinkingMode, fast: !thinkingMode }"
|
||||||
|
:title="thinkingMode ? '思考模式(点击切换)' : '快速模式(点击切换)'"
|
||||||
|
@click="$emit('toggle-thinking-mode')"
|
||||||
|
>
|
||||||
<transition name="mode-icon" mode="out-in">
|
<transition name="mode-icon" mode="out-in">
|
||||||
<span
|
<span
|
||||||
class="icon icon-sm"
|
class="icon icon-sm"
|
||||||
@ -20,7 +26,7 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></span>
|
></span>
|
||||||
</transition>
|
</transition>
|
||||||
</span>
|
</button>
|
||||||
<span class="connection-dot" :class="{ active: isConnected }" :title="isConnected ? '已连接' : '未连接'"></span>
|
<span class="connection-dot" :class="{ active: isConnected }" :title="isConnected ? '已连接' : '未连接'"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -145,6 +151,7 @@ defineEmits<{
|
|||||||
(event: 'toggle-panel-menu'): void;
|
(event: 'toggle-panel-menu'): void;
|
||||||
(event: 'select-panel', mode: 'files' | 'todo' | 'subAgents'): void;
|
(event: 'select-panel', mode: 'files' | 'todo' | 'subAgents'): void;
|
||||||
(event: 'open-file-manager'): void;
|
(event: 'open-file-manager'): void;
|
||||||
|
(event: 'toggle-thinking-mode'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const panelMenuWrapper = ref<HTMLElement | null>(null);
|
const panelMenuWrapper = ref<HTMLElement | null>(null);
|
||||||
|
|||||||
@ -4,7 +4,15 @@ import { renderLatexInRealtime } from './useMarkdownRenderer';
|
|||||||
|
|
||||||
export async function initializeLegacySocket(ctx: any) {
|
export async function initializeLegacySocket(ctx: any) {
|
||||||
try {
|
try {
|
||||||
console.log('初始化WebSocket连接...');
|
const SOCKET_DEBUG_LOGS_ENABLED = false;
|
||||||
|
const socketLog = (...args: any[]) => {
|
||||||
|
if (!SOCKET_DEBUG_LOGS_ENABLED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
socketLog('初始化WebSocket连接...');
|
||||||
|
|
||||||
const socketOptions = {
|
const socketOptions = {
|
||||||
transports: ['websocket', 'polling'],
|
transports: ['websocket', 'polling'],
|
||||||
@ -15,7 +23,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
|
|
||||||
const STREAMING_CHAR_DELAY = 22;
|
const STREAMING_CHAR_DELAY = 22;
|
||||||
const STREAMING_FINALIZE_DELAY = 1000;
|
const STREAMING_FINALIZE_DELAY = 1000;
|
||||||
const STREAMING_DEBUG = true;
|
const STREAMING_DEBUG = false;
|
||||||
const STREAMING_DEBUG_HISTORY_LIMIT = 2000;
|
const STREAMING_DEBUG_HISTORY_LIMIT = 2000;
|
||||||
const streamingState = {
|
const streamingState = {
|
||||||
buffer: [] as string[],
|
buffer: [] as string[],
|
||||||
@ -169,6 +177,35 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const markStreamingIdleIfPossible = (source: string) => {
|
||||||
|
try {
|
||||||
|
if (!ctx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof ctx.maybeResetStreamingState === 'function') {
|
||||||
|
const reset = ctx.maybeResetStreamingState(source);
|
||||||
|
if (reset) {
|
||||||
|
logStreamingDebug('streaming_idle_reset', { source });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ctx.streamingMessage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hasPending =
|
||||||
|
typeof ctx.hasPendingToolActions === 'function'
|
||||||
|
? ctx.hasPendingToolActions()
|
||||||
|
: false;
|
||||||
|
if (!hasPending) {
|
||||||
|
ctx.streamingMessage = false;
|
||||||
|
ctx.stopRequested = false;
|
||||||
|
logStreamingDebug('streaming_idle_reset:fallback', { source });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('自动结束流式状态失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const stopStreamingTimer = () => {
|
const stopStreamingTimer = () => {
|
||||||
if (streamingState.timer !== null) {
|
if (streamingState.timer !== null) {
|
||||||
clearTimeout(streamingState.timer);
|
clearTimeout(streamingState.timer);
|
||||||
@ -242,10 +279,12 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
ctx.chatCompleteTextAction(finalText || '');
|
ctx.chatCompleteTextAction(finalText || '');
|
||||||
completeActiveTextAction(finalText || '');
|
completeActiveTextAction(finalText || '');
|
||||||
ctx.$forceUpdate();
|
ctx.$forceUpdate();
|
||||||
ctx.streamingMessage = false;
|
// 注意:不在这里重置 streamingMessage,因为可能还有工具调用在进行
|
||||||
|
// streamingMessage 只在 task_complete 事件时重置
|
||||||
logStreamingDebug('finalizeStreamingText:complete', snapshotStreamingState());
|
logStreamingDebug('finalizeStreamingText:complete', snapshotStreamingState());
|
||||||
streamingState.activeMessageIndex = null;
|
streamingState.activeMessageIndex = null;
|
||||||
streamingState.activeTextAction = null;
|
streamingState.activeTextAction = null;
|
||||||
|
markStreamingIdleIfPossible('finalizeStreamingText');
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -400,7 +439,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
// 连接事件
|
// 连接事件
|
||||||
ctx.socket.on('connect', () => {
|
ctx.socket.on('connect', () => {
|
||||||
ctx.isConnected = true;
|
ctx.isConnected = true;
|
||||||
console.log('WebSocket已连接');
|
socketLog('WebSocket已连接');
|
||||||
// 连接时重置所有状态并刷新当前对话
|
// 连接时重置所有状态并刷新当前对话
|
||||||
ctx.resetAllStates('socket:connect');
|
ctx.resetAllStates('socket:connect');
|
||||||
scheduleHistoryReload(200);
|
scheduleHistoryReload(200);
|
||||||
@ -408,7 +447,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
|
|
||||||
ctx.socket.on('disconnect', () => {
|
ctx.socket.on('disconnect', () => {
|
||||||
ctx.isConnected = false;
|
ctx.isConnected = false;
|
||||||
console.log('WebSocket已断开');
|
socketLog('WebSocket已断开');
|
||||||
// 断线时也重置状态,防止状态混乱
|
// 断线时也重置状态,防止状态混乱
|
||||||
ctx.resetAllStates('socket:disconnect');
|
ctx.resetAllStates('socket:disconnect');
|
||||||
});
|
});
|
||||||
@ -451,7 +490,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
ctx.socket.on('token_update', (data) => {
|
ctx.socket.on('token_update', (data) => {
|
||||||
console.log('收到token更新事件:', data);
|
socketLog('收到token更新事件:', data);
|
||||||
|
|
||||||
// 只处理当前对话的token更新
|
// 只处理当前对话的token更新
|
||||||
if (data.conversation_id === ctx.currentConversationId) {
|
if (data.conversation_id === ctx.currentConversationId) {
|
||||||
@ -460,7 +499,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
ctx.currentConversationTokens.cumulative_output_tokens = data.cumulative_output_tokens || 0;
|
ctx.currentConversationTokens.cumulative_output_tokens = data.cumulative_output_tokens || 0;
|
||||||
ctx.currentConversationTokens.cumulative_total_tokens = data.cumulative_total_tokens || 0;
|
ctx.currentConversationTokens.cumulative_total_tokens = data.cumulative_total_tokens || 0;
|
||||||
|
|
||||||
console.log(`累计Token统计更新: 输入=${data.cumulative_input_tokens}, 输出=${data.cumulative_output_tokens}, 总计=${data.cumulative_total_tokens}`);
|
socketLog(`累计Token统计更新: 输入=${data.cumulative_input_tokens}, 输出=${data.cumulative_output_tokens}, 总计=${data.cumulative_total_tokens}`);
|
||||||
|
|
||||||
const hasContextTokens = typeof data.current_context_tokens === 'number';
|
const hasContextTokens = typeof data.current_context_tokens === 'number';
|
||||||
if (hasContextTokens && typeof ctx.resourceSetCurrentContextTokens === 'function') {
|
if (hasContextTokens && typeof ctx.resourceSetCurrentContextTokens === 'function') {
|
||||||
@ -475,7 +514,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ctx.socket.on('todo_updated', (data) => {
|
ctx.socket.on('todo_updated', (data) => {
|
||||||
console.log('收到todo更新事件:', data);
|
socketLog('收到todo更新事件:', data);
|
||||||
if (data && data.conversation_id) {
|
if (data && data.conversation_id) {
|
||||||
ctx.currentConversationId = data.conversation_id;
|
ctx.currentConversationId = data.conversation_id;
|
||||||
}
|
}
|
||||||
@ -487,14 +526,14 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
ctx.projectPath = data.project_path || '';
|
ctx.projectPath = data.project_path || '';
|
||||||
ctx.agentVersion = data.version || ctx.agentVersion;
|
ctx.agentVersion = data.version || ctx.agentVersion;
|
||||||
ctx.thinkingMode = !!data.thinking_mode;
|
ctx.thinkingMode = !!data.thinking_mode;
|
||||||
console.log('系统就绪:', data);
|
socketLog('系统就绪:', data);
|
||||||
|
|
||||||
// 系统就绪后立即加载对话列表
|
// 系统就绪后立即加载对话列表
|
||||||
ctx.loadConversationsList();
|
ctx.loadConversationsList();
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.socket.on('tool_settings_updated', (data) => {
|
ctx.socket.on('tool_settings_updated', (data) => {
|
||||||
console.log('收到工具设置更新:', data);
|
socketLog('收到工具设置更新:', data);
|
||||||
if (data && Array.isArray(data.categories)) {
|
if (data && Array.isArray(data.categories)) {
|
||||||
ctx.applyToolSettingsSnapshot(data.categories);
|
ctx.applyToolSettingsSnapshot(data.categories);
|
||||||
}
|
}
|
||||||
@ -506,7 +545,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
|
|
||||||
// 监听对话变更事件
|
// 监听对话变更事件
|
||||||
ctx.socket.on('conversation_changed', (data) => {
|
ctx.socket.on('conversation_changed', (data) => {
|
||||||
console.log('对话已切换:', data);
|
socketLog('对话已切换:', data);
|
||||||
ctx.currentConversationId = data.conversation_id;
|
ctx.currentConversationId = data.conversation_id;
|
||||||
ctx.currentConversationTitle = data.title || '';
|
ctx.currentConversationTitle = data.title || '';
|
||||||
ctx.promoteConversationToTop(data.conversation_id);
|
ctx.promoteConversationToTop(data.conversation_id);
|
||||||
@ -550,10 +589,10 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
|
|
||||||
// 监听对话加载事件
|
// 监听对话加载事件
|
||||||
ctx.socket.on('conversation_loaded', (data) => {
|
ctx.socket.on('conversation_loaded', (data) => {
|
||||||
console.log('对话已加载:', data);
|
socketLog('对话已加载:', data);
|
||||||
if (ctx.skipConversationLoadedEvent) {
|
if (ctx.skipConversationLoadedEvent) {
|
||||||
ctx.skipConversationLoadedEvent = false;
|
ctx.skipConversationLoadedEvent = false;
|
||||||
console.log('跳过重复的 conversation_loaded 处理');
|
socketLog('跳过重复的 conversation_loaded 处理');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (data.clear_ui) {
|
if (data.clear_ui) {
|
||||||
@ -576,7 +615,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
|
|
||||||
// 监听对话列表更新事件
|
// 监听对话列表更新事件
|
||||||
ctx.socket.on('conversation_list_update', (data) => {
|
ctx.socket.on('conversation_list_update', (data) => {
|
||||||
console.log('对话列表已更新:', data);
|
socketLog('对话列表已更新:', data);
|
||||||
// 刷新对话列表
|
// 刷新对话列表
|
||||||
ctx.loadConversationsList();
|
ctx.loadConversationsList();
|
||||||
});
|
});
|
||||||
@ -594,7 +633,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
|
|
||||||
// AI消息开始
|
// AI消息开始
|
||||||
ctx.socket.on('ai_message_start', () => {
|
ctx.socket.on('ai_message_start', () => {
|
||||||
console.log('AI消息开始');
|
socketLog('AI消息开始');
|
||||||
logStreamingDebug('socket:ai_message_start');
|
logStreamingDebug('socket:ai_message_start');
|
||||||
finalizeStreamingText({ force: true });
|
finalizeStreamingText({ force: true });
|
||||||
resetStreamingBuffer();
|
resetStreamingBuffer();
|
||||||
@ -604,13 +643,14 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
typeof ctx.currentMessageIndex === 'number' ? ctx.currentMessageIndex : null;
|
typeof ctx.currentMessageIndex === 'number' ? ctx.currentMessageIndex : null;
|
||||||
streamingState.activeTextAction = null;
|
streamingState.activeTextAction = null;
|
||||||
ctx.stopRequested = false;
|
ctx.stopRequested = false;
|
||||||
|
ctx.streamingMessage = true; // 确保设置为流式状态
|
||||||
ctx.chatEnableAutoScroll();
|
ctx.chatEnableAutoScroll();
|
||||||
ctx.scrollToBottom();
|
ctx.scrollToBottom();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 思考流开始
|
// 思考流开始
|
||||||
ctx.socket.on('thinking_start', () => {
|
ctx.socket.on('thinking_start', () => {
|
||||||
console.log('思考开始');
|
socketLog('思考开始');
|
||||||
const result = ctx.chatStartThinkingAction();
|
const result = ctx.chatStartThinkingAction();
|
||||||
if (result && result.blockId) {
|
if (result && result.blockId) {
|
||||||
const blockId = result.blockId;
|
const blockId = result.blockId;
|
||||||
@ -639,7 +679,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
|
|
||||||
// 思考结束
|
// 思考结束
|
||||||
ctx.socket.on('thinking_end', (data) => {
|
ctx.socket.on('thinking_end', (data) => {
|
||||||
console.log('思考结束');
|
socketLog('思考结束');
|
||||||
const blockId = ctx.chatCompleteThinkingAction(data.full_content);
|
const blockId = ctx.chatCompleteThinkingAction(data.full_content);
|
||||||
if (blockId) {
|
if (blockId) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -654,7 +694,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
|
|
||||||
// 文本流开始
|
// 文本流开始
|
||||||
ctx.socket.on('text_start', () => {
|
ctx.socket.on('text_start', () => {
|
||||||
console.log('文本开始');
|
socketLog('文本开始');
|
||||||
logStreamingDebug('socket:text_start');
|
logStreamingDebug('socket:text_start');
|
||||||
finalizeStreamingText({ force: true });
|
finalizeStreamingText({ force: true });
|
||||||
resetStreamingBuffer();
|
resetStreamingBuffer();
|
||||||
@ -692,7 +732,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
|
|
||||||
// 文本结束
|
// 文本结束
|
||||||
ctx.socket.on('text_end', (data) => {
|
ctx.socket.on('text_end', (data) => {
|
||||||
console.log('文本结束');
|
socketLog('文本结束');
|
||||||
logStreamingDebug('socket:text_end', {
|
logStreamingDebug('socket:text_end', {
|
||||||
finalLength: (data?.full_content || '').length,
|
finalLength: (data?.full_content || '').length,
|
||||||
snapshot: snapshotStreamingState()
|
snapshot: snapshotStreamingState()
|
||||||
@ -708,13 +748,13 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
|
|
||||||
// 工具提示事件(可选)
|
// 工具提示事件(可选)
|
||||||
ctx.socket.on('tool_hint', (data) => {
|
ctx.socket.on('tool_hint', (data) => {
|
||||||
console.log('工具提示:', data.name);
|
socketLog('工具提示:', data.name);
|
||||||
// 可以在这里添加提示UI
|
// 可以在这里添加提示UI
|
||||||
});
|
});
|
||||||
|
|
||||||
// 工具准备中事件 - 实时显示
|
// 工具准备中事件 - 实时显示
|
||||||
ctx.socket.on('tool_preparing', (data) => {
|
ctx.socket.on('tool_preparing', (data) => {
|
||||||
console.log('工具准备中:', data.name);
|
socketLog('工具准备中:', data.name);
|
||||||
const msg = ctx.chatEnsureAssistantMessage();
|
const msg = ctx.chatEnsureAssistantMessage();
|
||||||
if (!msg) {
|
if (!msg) {
|
||||||
return;
|
return;
|
||||||
@ -744,7 +784,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
|
|
||||||
// 工具状态更新事件 - 实时显示详细状态
|
// 工具状态更新事件 - 实时显示详细状态
|
||||||
ctx.socket.on('tool_status', (data) => {
|
ctx.socket.on('tool_status', (data) => {
|
||||||
console.log('工具状态:', data);
|
socketLog('工具状态:', data);
|
||||||
const target = ctx.toolFindAction(data.id, data.preparing_id, data.execution_id);
|
const target = ctx.toolFindAction(data.id, data.preparing_id, data.execution_id);
|
||||||
if (target) {
|
if (target) {
|
||||||
target.tool.statusDetail = data.detail;
|
target.tool.statusDetail = data.detail;
|
||||||
@ -763,7 +803,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
|
|
||||||
// 工具开始(从准备转为执行)
|
// 工具开始(从准备转为执行)
|
||||||
ctx.socket.on('tool_start', (data) => {
|
ctx.socket.on('tool_start', (data) => {
|
||||||
console.log('工具开始执行:', data.name);
|
socketLog('工具开始执行:', data.name);
|
||||||
let action = null;
|
let action = null;
|
||||||
if (data.preparing_id && ctx.preparingTools.has(data.preparing_id)) {
|
if (data.preparing_id && ctx.preparingTools.has(data.preparing_id)) {
|
||||||
action = ctx.preparingTools.get(data.preparing_id);
|
action = ctx.preparingTools.get(data.preparing_id);
|
||||||
@ -807,7 +847,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
|
|
||||||
// 更新action(工具完成)
|
// 更新action(工具完成)
|
||||||
ctx.socket.on('update_action', (data) => {
|
ctx.socket.on('update_action', (data) => {
|
||||||
console.log('更新action:', data.id, 'status:', data.status);
|
socketLog('更新action:', data.id, 'status:', data.status);
|
||||||
let targetAction = ctx.toolFindAction(data.id, data.preparing_id, data.execution_id);
|
let targetAction = ctx.toolFindAction(data.id, data.preparing_id, data.execution_id);
|
||||||
if (!targetAction && data.preparing_id && ctx.preparingTools.has(data.preparing_id)) {
|
if (!targetAction && data.preparing_id && ctx.preparingTools.has(data.preparing_id)) {
|
||||||
targetAction = ctx.preparingTools.get(data.preparing_id);
|
targetAction = ctx.preparingTools.get(data.preparing_id);
|
||||||
@ -870,18 +910,19 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
}
|
}
|
||||||
ctx.$forceUpdate();
|
ctx.$forceUpdate();
|
||||||
ctx.conditionalScrollToBottom();
|
ctx.conditionalScrollToBottom();
|
||||||
|
markStreamingIdleIfPossible('update_action');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关键修复:每个工具完成后都更新当前上下文Token
|
// 关键修复:每个工具完成后都更新当前上下文Token
|
||||||
if (data.status === 'completed') {
|
if (data.status === 'completed') {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
ctx.updateCurrentContextTokens();
|
ctx.updateCurrentContextTokens();
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.socket.on('append_payload', (data) => {
|
ctx.socket.on('append_payload', (data) => {
|
||||||
console.log('收到append_payload事件:', data);
|
socketLog('收到append_payload事件:', data);
|
||||||
ctx.chatAddAppendPayloadAction({
|
ctx.chatAddAppendPayloadAction({
|
||||||
path: data.path || '未知文件',
|
path: data.path || '未知文件',
|
||||||
forced: !!data.forced,
|
forced: !!data.forced,
|
||||||
@ -894,7 +935,7 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ctx.socket.on('modify_payload', (data) => {
|
ctx.socket.on('modify_payload', (data) => {
|
||||||
console.log('收到modify_payload事件:', data);
|
socketLog('收到modify_payload事件:', data);
|
||||||
ctx.chatAddModifyPayloadAction({
|
ctx.chatAddModifyPayloadAction({
|
||||||
path: data.path || '未知文件',
|
path: data.path || '未知文件',
|
||||||
total: data.total ?? null,
|
total: data.total ?? null,
|
||||||
@ -908,19 +949,19 @@ export async function initializeLegacySocket(ctx: any) {
|
|||||||
|
|
||||||
// 停止请求确认
|
// 停止请求确认
|
||||||
ctx.socket.on('stop_requested', (data) => {
|
ctx.socket.on('stop_requested', (data) => {
|
||||||
console.log('停止请求已接收:', data.message);
|
socketLog('停止请求已接收:', data.message);
|
||||||
// 可以显示提示信息
|
// 可以显示提示信息
|
||||||
});
|
});
|
||||||
|
|
||||||
// 任务停止
|
// 任务停止
|
||||||
ctx.socket.on('task_stopped', (data) => {
|
ctx.socket.on('task_stopped', (data) => {
|
||||||
console.log('任务已停止:', data.message);
|
socketLog('任务已停止:', data.message);
|
||||||
ctx.resetAllStates('socket:task_stopped');
|
ctx.resetAllStates('socket:task_stopped');
|
||||||
});
|
});
|
||||||
|
|
||||||
// 任务完成(重点:更新Token统计)
|
// 任务完成(重点:更新Token统计)
|
||||||
ctx.socket.on('task_complete', (data) => {
|
ctx.socket.on('task_complete', (data) => {
|
||||||
console.log('任务完成', data);
|
socketLog('任务完成', data);
|
||||||
ctx.resetAllStates('socket:task_complete');
|
ctx.resetAllStates('socket:task_complete');
|
||||||
|
|
||||||
// 任务完成后立即更新Token统计(关键修复)
|
// 任务完成后立即更新Token统计(关键修复)
|
||||||
|
|||||||
@ -15,6 +15,31 @@ interface ChatState {
|
|||||||
thinkingScrollLocks: Map<string, boolean>;
|
thinkingScrollLocks: Map<string, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GENERATING_LABELS = [
|
||||||
|
'正在构思…',
|
||||||
|
'稍候,AI 正在准备',
|
||||||
|
'准备工具中',
|
||||||
|
'容我三思…',
|
||||||
|
'答案马上就来',
|
||||||
|
'灵感加载中',
|
||||||
|
'思路拼装中',
|
||||||
|
'琢磨最佳方案',
|
||||||
|
'脑内开会中',
|
||||||
|
'整理资料中',
|
||||||
|
'润色回复中',
|
||||||
|
'调配上下文',
|
||||||
|
'搜刮记忆中',
|
||||||
|
'快敲完了,别急'
|
||||||
|
];
|
||||||
|
|
||||||
|
function randomGeneratingLabel() {
|
||||||
|
if (!GENERATING_LABELS.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const index = Math.floor(Math.random() * GENERATING_LABELS.length);
|
||||||
|
return GENERATING_LABELS[index];
|
||||||
|
}
|
||||||
|
|
||||||
function createAssistantMessage() {
|
function createAssistantMessage() {
|
||||||
return {
|
return {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
@ -22,7 +47,9 @@ function createAssistantMessage() {
|
|||||||
streamingThinking: '',
|
streamingThinking: '',
|
||||||
streamingText: '',
|
streamingText: '',
|
||||||
currentStreamingType: null,
|
currentStreamingType: null,
|
||||||
activeThinkingId: null
|
activeThinkingId: null,
|
||||||
|
awaitingFirstContent: false,
|
||||||
|
generatingLabel: randomGeneratingLabel()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,6 +65,12 @@ function cloneMap<K, V>(source: Map<K, V>) {
|
|||||||
return new Map<K, V>(Array.from(source.entries()));
|
return new Map<K, V>(Array.from(source.entries()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearAwaitingFirstContent(message: any) {
|
||||||
|
if (message && message.awaitingFirstContent) {
|
||||||
|
message.awaitingFirstContent = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const useChatStore = defineStore('chat', {
|
export const useChatStore = defineStore('chat', {
|
||||||
state: (): ChatState => ({
|
state: (): ChatState => ({
|
||||||
messages: [],
|
messages: [],
|
||||||
@ -142,10 +175,12 @@ export const useChatStore = defineStore('chat', {
|
|||||||
this.messages.push(message);
|
this.messages.push(message);
|
||||||
this.currentMessageIndex = this.messages.length - 1;
|
this.currentMessageIndex = this.messages.length - 1;
|
||||||
this.streamingMessage = true;
|
this.streamingMessage = true;
|
||||||
|
message.awaitingFirstContent = true;
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
startThinkingAction() {
|
startThinkingAction() {
|
||||||
const msg = this.ensureAssistantMessage();
|
const msg = this.ensureAssistantMessage();
|
||||||
|
clearAwaitingFirstContent(msg);
|
||||||
msg.streamingThinking = '';
|
msg.streamingThinking = '';
|
||||||
msg.currentStreamingType = 'thinking';
|
msg.currentStreamingType = 'thinking';
|
||||||
const actionId = randomId('thinking');
|
const actionId = randomId('thinking');
|
||||||
@ -189,6 +224,10 @@ export const useChatStore = defineStore('chat', {
|
|||||||
},
|
},
|
||||||
startTextAction() {
|
startTextAction() {
|
||||||
const msg = this.ensureAssistantMessage();
|
const msg = this.ensureAssistantMessage();
|
||||||
|
if (!msg) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
clearAwaitingFirstContent(msg);
|
||||||
msg.streamingText = '';
|
msg.streamingText = '';
|
||||||
msg.currentStreamingType = 'text';
|
msg.currentStreamingType = 'text';
|
||||||
const action = {
|
const action = {
|
||||||
@ -204,6 +243,12 @@ export const useChatStore = defineStore('chat', {
|
|||||||
appendTextChunk(content: string) {
|
appendTextChunk(content: string) {
|
||||||
if (this.currentMessageIndex < 0) return null;
|
if (this.currentMessageIndex < 0) return null;
|
||||||
const msg = this.messages[this.currentMessageIndex];
|
const msg = this.messages[this.currentMessageIndex];
|
||||||
|
if (!msg) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof msg.streamingText !== 'string') {
|
||||||
|
msg.streamingText = '';
|
||||||
|
}
|
||||||
msg.streamingText += content;
|
msg.streamingText += content;
|
||||||
const lastAction = msg.actions[msg.actions.length - 1];
|
const lastAction = msg.actions[msg.actions.length - 1];
|
||||||
if (lastAction && lastAction.type === 'text') {
|
if (lastAction && lastAction.type === 'text') {
|
||||||
@ -228,6 +273,7 @@ export const useChatStore = defineStore('chat', {
|
|||||||
},
|
},
|
||||||
addSystemMessage(content: string) {
|
addSystemMessage(content: string) {
|
||||||
const msg = this.ensureAssistantMessage();
|
const msg = this.ensureAssistantMessage();
|
||||||
|
clearAwaitingFirstContent(msg);
|
||||||
msg.actions.push({
|
msg.actions.push({
|
||||||
id: randomId('system'),
|
id: randomId('system'),
|
||||||
type: 'system',
|
type: 'system',
|
||||||
@ -237,6 +283,7 @@ export const useChatStore = defineStore('chat', {
|
|||||||
},
|
},
|
||||||
addAppendPayloadAction(data: any) {
|
addAppendPayloadAction(data: any) {
|
||||||
const msg = this.ensureAssistantMessage();
|
const msg = this.ensureAssistantMessage();
|
||||||
|
clearAwaitingFirstContent(msg);
|
||||||
msg.actions.push({
|
msg.actions.push({
|
||||||
id: `append-payload-${Date.now()}-${Math.random()}`,
|
id: `append-payload-${Date.now()}-${Math.random()}`,
|
||||||
type: 'append_payload',
|
type: 'append_payload',
|
||||||
@ -246,6 +293,7 @@ export const useChatStore = defineStore('chat', {
|
|||||||
},
|
},
|
||||||
addModifyPayloadAction(data: any) {
|
addModifyPayloadAction(data: any) {
|
||||||
const msg = this.ensureAssistantMessage();
|
const msg = this.ensureAssistantMessage();
|
||||||
|
clearAwaitingFirstContent(msg);
|
||||||
msg.actions.push({
|
msg.actions.push({
|
||||||
id: `modify-payload-${Date.now()}-${Math.random()}`,
|
id: `modify-payload-${Date.now()}-${Math.random()}`,
|
||||||
type: 'modify_payload',
|
type: 'modify_payload',
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
const FILE_STORE_DEBUG_LOGS = false;
|
||||||
|
function fileDebugLog(...args: unknown[]) {
|
||||||
|
if (!FILE_STORE_DEBUG_LOGS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(...args);
|
||||||
|
}
|
||||||
|
|
||||||
interface FileNode {
|
interface FileNode {
|
||||||
type: 'folder' | 'file';
|
type: 'folder' | 'file';
|
||||||
name: string;
|
name: string;
|
||||||
@ -80,7 +88,7 @@ export const useFileStore = defineStore('file', {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('/api/files');
|
const response = await fetch('/api/files');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('[FileTree] fetch result', data);
|
fileDebugLog('[FileTree] fetch result', data);
|
||||||
this.setFileTreeFromResponse(data);
|
this.setFileTreeFromResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取文件树失败:', error);
|
console.error('获取文件树失败:', error);
|
||||||
@ -127,7 +135,7 @@ export const useFileStore = defineStore('file', {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const current = !!this.expandedFolders[path];
|
const current = !!this.expandedFolders[path];
|
||||||
console.log('[FileTree] toggle folder', path, '=>', !current);
|
fileDebugLog('[FileTree] toggle folder', path, '=>', !current);
|
||||||
this.expandedFolders = {
|
this.expandedFolders = {
|
||||||
...this.expandedFolders,
|
...this.expandedFolders,
|
||||||
[path]: !current
|
[path]: !current
|
||||||
|
|||||||
@ -147,6 +147,49 @@
|
|||||||
border-left: 4px solid var(--claude-accent);
|
border-left: 4px solid var(--claude-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assistant-generating-block {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-content.assistant-generating-placeholder {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.08em;
|
||||||
|
padding: 8px 0 16px;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--claude-text);
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-generating-letter {
|
||||||
|
display: inline-block;
|
||||||
|
opacity: 0.35;
|
||||||
|
transform: translateY(0);
|
||||||
|
animation: assistant-generating-wave 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes assistant-generating-wave {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.35;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-3px) scale(1.05);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
opacity: 0.65;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.thinking-content {
|
.thinking-content {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
@ -322,6 +365,34 @@
|
|||||||
padding: 0 20px 0 15px;
|
padding: 0 20px 0 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-output .text-content table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 16px 0;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 8px 20px rgba(61, 57, 41, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-output .text-content thead {
|
||||||
|
background: rgba(218, 119, 86, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-output .text-content th,
|
||||||
|
.text-output .text-content td {
|
||||||
|
border: 1px solid rgba(118, 103, 84, 0.18);
|
||||||
|
padding: 10px 14px;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-output .text-content th {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--claude-text);
|
||||||
|
}
|
||||||
|
|
||||||
.system-action {
|
.system-action {
|
||||||
margin: 12px 0;
|
margin: 12px 0;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
|
|||||||
@ -108,6 +108,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mode-indicator {
|
.mode-indicator {
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
padding: 0;
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
@ -120,6 +124,14 @@
|
|||||||
transition: background 0.25s ease, box-shadow 0.25s ease, transform 0.25s ease;
|
transition: background 0.25s ease, box-shadow 0.25s ease, transform 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mode-indicator:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-indicator:focus-visible {
|
||||||
|
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8), 0 8px 20px rgba(189, 93, 58, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
.mode-indicator.fast {
|
.mode-indicator.fast {
|
||||||
background: #ffcc4d;
|
background: #ffcc4d;
|
||||||
box-shadow: 0 8px 20px rgba(255, 204, 77, 0.35);
|
box-shadow: 0 8px 20px rgba(255, 204, 77, 0.35);
|
||||||
|
|||||||
@ -561,14 +561,7 @@
|
|||||||
|
|
||||||
// 初始化WebSocket连接
|
// 初始化WebSocket连接
|
||||||
async function initWebSocket() {
|
async function initWebSocket() {
|
||||||
const usePollingOnly = window.location.hostname !== 'localhost' &&
|
const socketOptions = {
|
||||||
window.location.hostname !== '127.0.0.1';
|
|
||||||
|
|
||||||
const socketOptions = usePollingOnly ? {
|
|
||||||
transports: ['polling'],
|
|
||||||
upgrade: false,
|
|
||||||
autoConnect: false
|
|
||||||
} : {
|
|
||||||
transports: ['websocket', 'polling'],
|
transports: ['websocket', 'polling'],
|
||||||
autoConnect: false
|
autoConnect: false
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user