feat(compression): 深压缩改为 in-place 标记前缀,不再切换新对话
深压缩从"建新对话+切过去"重构为标记当前对话历史前缀(deep_compacted): 原文保留并照常显示,仅在 build_messages 构建请求时排除。conversation_id 不变,避免任务状态/目标模式/前端对话切换的大量适配带来的 bug。 - deep_compression.py: _mark_history_compacted 打标 + 重置 current_context_tokens 防自动续接死循环;总结请求传入正常 tools 以 100% 命中前缀缓存 - context.py build_messages: 跳过 deep_compacted 消息 - conversation.py /compress: 去掉切对话,按 compress_behavior 决定续接/等待 - 新增个性化设置 deep_compress_form(file/inject) 与 deep_compress_behavior(continue/wait,仅手动压缩生效) - 前端去掉强制切换对话,改为重载当前对话刷新展示 - AGENTS.md/CLAUDE.md: 补充默认中文交流约定等说明 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f5e4541d83
commit
3fd2214da0
@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
这份文档基于当前仓库实际代码重写。若与未来代码冲突,以代码为准并及时更新本文档。
|
这份文档基于当前仓库实际代码重写。若与未来代码冲突,以代码为准并及时更新本文档。
|
||||||
|
|
||||||
|
> 和用户交流时默认使用中文。
|
||||||
|
|
||||||
## 1) 当前项目结构(按代码现状)
|
## 1) 当前项目结构(按代码现状)
|
||||||
|
|
||||||
- **后端入口**
|
- **后端入口**
|
||||||
|
|||||||
136
CLAUDE.md
136
CLAUDE.md
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
> 和用户交流时默认使用中文。
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
多用户 AI Agent 系统,为每个登录用户提供独立的 Docker 容器环境。支持终端交互、文件操作、对话管理和实时监控。主要用于学习和实验"智能体 + 真实 Dev Workflow",代码大量由 AI 生成。
|
多用户 AI Agent 系统,为每个登录用户提供独立的 Docker 容器环境。支持终端交互、文件操作、对话管理和实时监控。主要用于学习和实验"智能体 + 真实 Dev Workflow",代码大量由 AI 生成。
|
||||||
@ -9,14 +11,22 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
## Core Architecture
|
## Core Architecture
|
||||||
|
|
||||||
### Entry Points
|
### Entry Points
|
||||||
- **CLI模式**: `python main.py` - 启动命令行交互式 Agent
|
- **统一入口**: `python main.py` - 当前实现默认进入 Web 启动流程(thinking_mode=True)
|
||||||
- **Web模式**: `python web_server.py` - 启动 Flask Web 服务器(主链路为 REST 任务轮询,默认端口 8091)
|
- **推荐 Web 入口**: `python -m server.app` - 封装并转发到 `server/app_legacy.py`,主链路为 REST 任务轮询,默认端口 8091
|
||||||
|
- **兼容入口**: `python web_server.py` - 已标记 deprecated,但仍可启动
|
||||||
|
|
||||||
### Key Components
|
### Key Components
|
||||||
|
|
||||||
|
**server/** (Flask 业务主线,chat/task 以 REST 任务轮询为主,Socket.IO 用于兼容与实时辅助通道)
|
||||||
|
- `app.py` / `app_legacy.py` - Web 服务入口与遗留实现
|
||||||
|
- `chat.py` / `chat_flow*.py` - 对话与任务流编排(runner / tool_loop / stream_loop / task_main 等拆分)
|
||||||
|
- `goal_flow.py` - 自主目标循环
|
||||||
|
- `deep_compression.py` - 上下文深度压缩
|
||||||
|
- `tasks.py` / `monitor.py` / `conversation.py` / `files.py` / `auth.py` / `admin.py` - 任务、监控、对话、文件、认证、管理接口
|
||||||
|
|
||||||
**core/**
|
**core/**
|
||||||
- `MainTerminal` - CLI 主终端,处理用户输入和 AI 对话循环
|
- `MainTerminal` (`main_terminal.py`) - CLI 主终端,处理用户输入和 AI 对话循环;逻辑拆分到 `core/main_terminal_parts/*`(commands / context / tools / tools_execution / tools_policy / tools_read 等)
|
||||||
- `WebTerminal` - Web 终端,继承 MainTerminal,支持轮询任务与实时通道兼容能力
|
- `WebTerminal` (`web_terminal.py`) - Web 终端,继承 MainTerminal,支持轮询任务与实时通道兼容能力
|
||||||
- `tool_config.py` - 定义所有工具的配置和分类
|
- `tool_config.py` - 定义所有工具的配置和分类
|
||||||
|
|
||||||
**modules/**
|
**modules/**
|
||||||
@ -37,7 +47,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
- `terminal_factory.py` - 终端类型工厂
|
- `terminal_factory.py` - 终端类型工厂
|
||||||
|
|
||||||
**static/**
|
**static/**
|
||||||
- Vue 前端(聊天主链路使用 REST 任务轮询,保留 Socket.IO 兼容能力),包含对话流、终端输出、资源监控面板
|
- Vue 3 + TS 前端(`static/src/`,分层为 `app/` `stores/` `components/` `composables/`),聊天主链路使用 REST 任务轮询,保留 Socket.IO 兼容能力,包含对话流、终端输出、资源监控面板
|
||||||
|
- 监控动画核心文件:`static/src/components/chat/monitor/MonitorDirector.ts`(动画与场景执行)、`static/src/stores/monitor.ts`(事件队列与状态机)
|
||||||
|
|
||||||
|
**cli/**
|
||||||
|
- React 19 + Ink 6 + TypeScript CLI 前端(正在重写中),连接现有本地 Web API(默认 `127.0.0.1:8091`),不是独立 agent runtime
|
||||||
|
- 关键文件:`cli/src/App.tsx`、`cli/src/components.tsx`、`cli/src/eventMapper.ts`、`cli/src/api.ts`
|
||||||
|
|
||||||
**docker/**
|
**docker/**
|
||||||
- `terminal.Dockerfile` - 终端容器镜像构建配置
|
- `terminal.Dockerfile` - 终端容器镜像构建配置
|
||||||
@ -46,22 +61,62 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
### Build & Run
|
### Build & Run
|
||||||
```bash
|
```bash
|
||||||
# 安装依赖(需要在项目根目录创建 requirements.txt)
|
# 安装 Python 依赖
|
||||||
pip install flask flask-socketio flask-cors openai python-dotenv tiktoken lxml
|
pip install -r requirements.txt
|
||||||
|
|
||||||
# 构建终端镜像
|
# 构建终端镜像
|
||||||
docker build -f docker/terminal.Dockerfile -t my-agent-shell:latest .
|
docker build -f docker/terminal.Dockerfile -t my-agent-shell:latest .
|
||||||
|
|
||||||
# CLI 模式
|
# 统一入口(默认进入 Web 启动流程)
|
||||||
python main.py
|
python main.py
|
||||||
|
|
||||||
# Web 模式
|
# 推荐 Web 启动
|
||||||
|
python -m server.app
|
||||||
|
# 可选参数
|
||||||
|
python -m server.app --path ./project --port 8091 --debug --thinking-mode
|
||||||
|
|
||||||
|
# 兼容启动方式(deprecated)
|
||||||
python web_server.py
|
python web_server.py
|
||||||
# 或指定端口和思考模式
|
|
||||||
python web_server.py --port 8091 --thinking-mode
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing & Debugging
|
### Frontend (根目录 Vue)
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
# 构建(默认只看最后 5 行,减少 Vite 构建资源列表等无关输出)
|
||||||
|
npm run build --silent 2>&1 | tail -n 5
|
||||||
|
# 开发监听(当前脚本是 build watch)
|
||||||
|
npm run dev
|
||||||
|
# Lint
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### CLI (React / Ink)
|
||||||
|
```bash
|
||||||
|
npm --prefix cli install
|
||||||
|
# 开发启动
|
||||||
|
npm run cli # 或 npm --prefix cli run dev
|
||||||
|
# 构建
|
||||||
|
npm run cli:build
|
||||||
|
# 类型检查
|
||||||
|
npm run cli:typecheck
|
||||||
|
# 可执行命令名(构建后):agents / agents-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# 主要离线冒烟用例(unittest)
|
||||||
|
python -m unittest test.test_server_refactor_smoke
|
||||||
|
|
||||||
|
# 改动后端接口适配(如 server/chat.py)时补充
|
||||||
|
python3 -m py_compile server/chat.py
|
||||||
|
|
||||||
|
# CLI 最小可复现验证
|
||||||
|
npm --prefix cli run typecheck
|
||||||
|
npm --prefix cli run build
|
||||||
|
```
|
||||||
|
> 仓库未发现 `pytest.ini`/`pyproject.toml`/`tox.ini`,不要默认要求 `pytest` 作为唯一入口。`test/test_system_message.py` 依赖外部 `MOONSHOT_API_KEY` 与网络,不属于离线稳定用例。
|
||||||
|
|
||||||
|
### Debugging
|
||||||
```bash
|
```bash
|
||||||
# 查看容器状态
|
# 查看容器状态
|
||||||
docker ps -a | grep agent-term
|
docker ps -a | grep agent-term
|
||||||
@ -139,9 +194,18 @@ tail -f logs/container_stats.log
|
|||||||
|
|
||||||
### Adding New Tools
|
### Adding New Tools
|
||||||
|
|
||||||
1. 在 `MainTerminal` 添加 `handle_<tool_name>` 方法
|
1. 在 `core/main_terminal_parts/tools_execution.py`(及相关 `tools_*.py`)实现执行逻辑
|
||||||
2. 在 `core/tool_config.py` 的 `define_tools()` 中注册工具
|
2. 在 `core/tool_config.py` / `core/main_terminal_parts/tools_definition.py` 中注册工具定义
|
||||||
3. 如果需要在流式输出中处理,在 `web_server.py` 添加特殊逻辑
|
3. 如需在任务流/流式输出中特殊处理,在 `server/chat_flow*.py` 对应环节添加逻辑
|
||||||
|
|
||||||
|
### 代码修改约定
|
||||||
|
|
||||||
|
- **最小改动原则**:只改与需求直接相关文件,避免顺手重构。
|
||||||
|
- **文件编辑方式**:优先使用原生文件编辑工具,尽量不要用 `bash`/`python` 脚本批量改文件。
|
||||||
|
- **后端改动优先级**:先改 `modules/`、`server/` 内对应模块,最后才动入口。
|
||||||
|
- **前端改动优先级**:按 `static/src` 现有分层改(`app/` `stores/` `components/` `composables/`)。
|
||||||
|
- **CLI 改动优先级**:优先在 `cli/src/App.tsx`、`cli/src/components.tsx`、`cli/src/eventMapper.ts`、`cli/src/api.ts` 内做最小闭环修改。
|
||||||
|
- 涉及 monitor 动画/事件联动时,至少同步检查 `MonitorDirector.ts` 与 `stores/monitor.ts`。
|
||||||
|
|
||||||
### Working with Containers
|
### Working with Containers
|
||||||
|
|
||||||
@ -192,16 +256,21 @@ terminal.delete_conversation(conversation_id)
|
|||||||
|
|
||||||
```
|
```
|
||||||
agents/
|
agents/
|
||||||
├── core/ # 核心终端和工具配置
|
├── server/ # Flask 业务主线(chat/task/goal/monitor/auth ...)
|
||||||
|
├── core/ # 核心终端与工具编排(main_terminal_parts/*)
|
||||||
├── modules/ # 独立功能模块
|
├── modules/ # 独立功能模块
|
||||||
├── utils/ # 辅助工具
|
├── utils/ # 辅助工具
|
||||||
├── config/ # 配置文件
|
├── config/ # 配置拆分(api/limits/terminal/paths ...,由 __init__ 聚合)
|
||||||
├── static/ # Web 前端
|
├── static/ # Vue 3 + TS Web 前端
|
||||||
|
├── cli/ # React + Ink CLI 前端(重写中)
|
||||||
├── docker/ # 容器镜像
|
├── docker/ # 容器镜像
|
||||||
├── prompts/ # 系统提示词
|
├── prompts/ # 系统提示词
|
||||||
├── users/ # 用户工作区
|
├── android-webview-app/ # Android WebView 客户端工程
|
||||||
├── data/ # 全局数据
|
├── test/ # 测试用例
|
||||||
└── logs/ # 日志文件
|
├── docs/ / doc/ # 设计与发布文档
|
||||||
|
├── users/ # 用户工作区(运行态,gitignored)
|
||||||
|
├── data/ # 全局数据(运行态,gitignored)
|
||||||
|
└── logs/ # 日志文件(运行态,gitignored)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development Tips
|
## Development Tips
|
||||||
@ -209,9 +278,32 @@ agents/
|
|||||||
- 修改系统提示词:编辑 `prompts/main_system.txt`
|
- 修改系统提示词:编辑 `prompts/main_system.txt`
|
||||||
- 调整工具限制:修改 `config/limits.py`
|
- 调整工具限制:修改 `config/limits.py`
|
||||||
- 容器镜像定制:编辑 `docker/terminal.Dockerfile` 并重新构建
|
- 容器镜像定制:编辑 `docker/terminal.Dockerfile` 并重新构建
|
||||||
- 前端开发:`npm install && npm run dev` (在 `static/` 目录)
|
- 前端开发:在项目根目录运行 `npm install && npm run dev`(build watch)
|
||||||
- 查看详细执行流程:监控 `logs/debug_stream.log`
|
- 查看详细执行流程:监控 `logs/debug_stream.log`
|
||||||
|
|
||||||
|
## Permission Mode & Sandbox Execution (2026-05)
|
||||||
|
|
||||||
|
完整说明文档:`docs/host_sandbox_and_permission_model.md`
|
||||||
|
|
||||||
|
系统有两套独立但叠加的控制:
|
||||||
|
|
||||||
|
### 权限模式 (Permission Mode)
|
||||||
|
- `readonly` - `run_command` 在只读沙箱执行;`write_file`/`edit_file` 等写入类工具直接拒绝
|
||||||
|
- `approval` - `run_command` 先走只读沙箱,权限拒绝(如 `Operation not permitted`/`Permission denied`)时触发前端审批,通过后仅该次命令以可写沙箱重试
|
||||||
|
- `auto_approval` - 工作区内写入直接执行,工作区外写入进入审批;`run_command` 权限拒绝由后台审批智能体自动审核
|
||||||
|
- `unrestricted` - 不做权限模式拦截,是否沙箱由执行环境决定
|
||||||
|
|
||||||
|
### 执行环境 (Execution Mode)
|
||||||
|
- `sandbox`(默认)- OS 沙箱执行(macOS: `sandbox-exec`,Linux: `bwrap + seccomp`,Windows: `WSL2`),支持会话级 TTL 自动回退 `direct -> sandbox`
|
||||||
|
- `direct` - 宿主机直接执行(高风险,仅建议短时使用)
|
||||||
|
|
||||||
|
### 路径授权语义
|
||||||
|
- 前端路径授权分「可读可写」与「仅可读」:可读集合 = 可读可写 + 仅可读,可写集合 = 可读可写
|
||||||
|
|
||||||
|
### 自动审批智能体
|
||||||
|
- 配置文件:`config/auto_approval.json`(`name`/`url`/`key`/`model`/`extra_params`,及 `timeout_seconds`/`max_rounds`/`max_command_timeout`)
|
||||||
|
- 调试开关:`modules/approval_agent.py` 中 `DEBUG_SAVE_APPROVAL_AGENT_TRANSCRIPT`,开启后写入 `logs/approval_agent/`
|
||||||
|
|
||||||
## Android Release Notes (Required for frontend update)
|
## Android Release Notes (Required for frontend update)
|
||||||
|
|
||||||
当改动前端并准备发 Android WebView 壳版本时,必须同步完成:
|
当改动前端并准备发 Android WebView 壳版本时,必须同步完成:
|
||||||
|
|||||||
@ -445,8 +445,15 @@ class MainTerminalContextMixin:
|
|||||||
# 添加对话历史(保留完整结构,包括tool_calls和tool消息)
|
# 添加对话历史(保留完整结构,包括tool_calls和tool消息)
|
||||||
conversation = context["conversation"]
|
conversation = context["conversation"]
|
||||||
replaced_tool_count = 0
|
replaced_tool_count = 0
|
||||||
|
deep_compacted_skipped = 0
|
||||||
for idx, conv in enumerate(conversation):
|
for idx, conv in enumerate(conversation):
|
||||||
metadata = conv.get("metadata") or {}
|
metadata = conv.get("metadata") or {}
|
||||||
|
# 深压缩:被标记为 deep_compacted 的消息(整段已压缩前缀)原文保留在历史中用于展示,
|
||||||
|
# 但构建请求时整体跳过,不纳入上下文。由于按连续前缀整体标记,
|
||||||
|
# 不会出现 assistant.tool_calls 与其 tool 响应被拆散导致的配对悬空。
|
||||||
|
if metadata.get("deep_compacted"):
|
||||||
|
deep_compacted_skipped += 1
|
||||||
|
continue
|
||||||
if conv["role"] == "assistant":
|
if conv["role"] == "assistant":
|
||||||
# Assistant消息可能包含工具调用
|
# Assistant消息可能包含工具调用
|
||||||
message = {
|
message = {
|
||||||
@ -523,6 +530,8 @@ class MainTerminalContextMixin:
|
|||||||
# 当前用户输入已经在conversation中了,不需要重复添加
|
# 当前用户输入已经在conversation中了,不需要重复添加
|
||||||
if shallow_replace_enabled:
|
if shallow_replace_enabled:
|
||||||
print(f"[ContextCompression] build_messages 替换tool占位符: {replaced_tool_count} 条")
|
print(f"[ContextCompression] build_messages 替换tool占位符: {replaced_tool_count} 条")
|
||||||
|
if deep_compacted_skipped:
|
||||||
|
print(f"[ContextCompression] build_messages 跳过已深压缩消息: {deep_compacted_skipped} 条")
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
def _load_agents_md_content(self) -> Optional[str]:
|
def _load_agents_md_content(self) -> Optional[str]:
|
||||||
|
|||||||
@ -87,6 +87,8 @@ DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = {
|
|||||||
"shallow_compress_trigger_tool_calls_interval": None,
|
"shallow_compress_trigger_tool_calls_interval": None,
|
||||||
"shallow_compress_keep_user_turn_tools": None, # 保留最近N次用户输入后的工具不压缩(默认3)
|
"shallow_compress_keep_user_turn_tools": None, # 保留最近N次用户输入后的工具不压缩(默认3)
|
||||||
"deep_compress_trigger_tokens": None,
|
"deep_compress_trigger_tokens": None,
|
||||||
|
"deep_compress_form": "file", # 深压缩形式:file-生成文件 / inject-直接注入文件全文
|
||||||
|
"deep_compress_behavior": "continue", # 手动压缩行为:continue-注入并触发请求 / wait-仅插入等待用户
|
||||||
"silent_tool_disable": True, # 禁用工具时不向模型插入提示(默认开启)
|
"silent_tool_disable": True, # 禁用工具时不向模型插入提示(默认开启)
|
||||||
"enhanced_tool_display": True, # 增强工具显示
|
"enhanced_tool_display": True, # 增强工具显示
|
||||||
"versioning_restore_mode": "overwrite", # 版本回溯模式固定为 overwrite
|
"versioning_restore_mode": "overwrite", # 版本回溯模式固定为 overwrite
|
||||||
@ -371,6 +373,20 @@ def sanitize_personalization_payload(
|
|||||||
max_value=MAX_COMPRESSION_TRIGGER_TOKENS,
|
max_value=MAX_COMPRESSION_TRIGGER_TOKENS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 深压缩形式:file(生成文件,引导语提示位置)/ inject(直接注入文件全文)
|
||||||
|
deep_form = data.get("deep_compress_form", base.get("deep_compress_form"))
|
||||||
|
if isinstance(deep_form, str) and deep_form.strip().lower() in ("file", "inject"):
|
||||||
|
base["deep_compress_form"] = deep_form.strip().lower()
|
||||||
|
else:
|
||||||
|
base["deep_compress_form"] = "file"
|
||||||
|
|
||||||
|
# 手动压缩行为:continue(注入并触发请求)/ wait(仅插入等待用户)
|
||||||
|
deep_behavior = data.get("deep_compress_behavior", base.get("deep_compress_behavior"))
|
||||||
|
if isinstance(deep_behavior, str) and deep_behavior.strip().lower() in ("continue", "wait"):
|
||||||
|
base["deep_compress_behavior"] = deep_behavior.strip().lower()
|
||||||
|
else:
|
||||||
|
base["deep_compress_behavior"] = "continue"
|
||||||
|
|
||||||
if "shallow_compress_trigger_tool_calls_interval" in data:
|
if "shallow_compress_trigger_tool_calls_interval" in data:
|
||||||
base["shallow_compress_trigger_tool_calls_interval"] = _sanitize_optional_int(
|
base["shallow_compress_trigger_tool_calls_interval"] = _sanitize_optional_int(
|
||||||
data.get("shallow_compress_trigger_tool_calls_interval"),
|
data.get("shallow_compress_trigger_tool_calls_interval"),
|
||||||
|
|||||||
@ -1147,7 +1147,7 @@ def restore_conversation_versioning_checkpoint(conversation_id, terminal: WebTer
|
|||||||
@api_login_required
|
@api_login_required
|
||||||
@with_terminal
|
@with_terminal
|
||||||
def compress_conversation(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
def compress_conversation(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||||
"""深层压缩指定对话,生成 compact 文件并切换到新对话。"""
|
"""深层压缩指定对话(in-place):生成 compact 文件、标记历史前缀为已压缩,按设置决定是否续接。"""
|
||||||
try:
|
try:
|
||||||
policy = resolve_admin_policy(get_current_user_record())
|
policy = resolve_admin_policy(get_current_user_record())
|
||||||
if policy.get("ui_blocks", {}).get("block_compress_conversation"):
|
if policy.get("ui_blocks", {}).get("block_compress_conversation"):
|
||||||
@ -1167,36 +1167,34 @@ def compress_conversation(conversation_id, terminal: WebTerminal, workspace: Use
|
|||||||
status_code = 404 if "不存在" in result.get("error", "") else (409 if result.get("in_progress") else 400)
|
status_code = 404 if "不存在" in result.get("error", "") else (409 if result.get("in_progress") else 400)
|
||||||
return jsonify(result), status_code
|
return jsonify(result), status_code
|
||||||
|
|
||||||
new_conversation_id = result["compressed_conversation_id"]
|
# in-place 压缩:对话 id 不变。通知前端当前对话内容已变化(历史前缀被标记),刷新展示。
|
||||||
|
load_result = terminal.load_conversation(normalized_id)
|
||||||
load_result = terminal.load_conversation(new_conversation_id)
|
|
||||||
|
|
||||||
if load_result.get("success"):
|
if load_result.get("success"):
|
||||||
socketio.emit('conversation_list_update', {
|
socketio.emit('conversation_list_update', {
|
||||||
'action': 'compressed',
|
'action': 'compressed',
|
||||||
'conversation_id': new_conversation_id
|
'conversation_id': normalized_id
|
||||||
}, room=f"user_{username}")
|
|
||||||
socketio.emit('conversation_changed', {
|
|
||||||
'conversation_id': new_conversation_id,
|
|
||||||
'title': load_result.get('title', '压缩后的对话'),
|
|
||||||
'messages_count': load_result.get('messages_count', 0)
|
|
||||||
}, room=f"user_{username}")
|
}, room=f"user_{username}")
|
||||||
socketio.emit('conversation_loaded', {
|
socketio.emit('conversation_loaded', {
|
||||||
'conversation_id': new_conversation_id,
|
'conversation_id': normalized_id,
|
||||||
'clear_ui': True
|
'clear_ui': True
|
||||||
}, room=f"user_{username}")
|
}, room=f"user_{username}")
|
||||||
|
|
||||||
|
compress_behavior = str(result.get("compress_behavior") or "continue").strip().lower()
|
||||||
response_payload = {
|
response_payload = {
|
||||||
"success": True,
|
"success": True,
|
||||||
"compressed_conversation_id": new_conversation_id,
|
"in_place": True,
|
||||||
|
"compressed_conversation_id": normalized_id,
|
||||||
"compact_file": result.get("compact_file"),
|
"compact_file": result.get("compact_file"),
|
||||||
"summary_failed": result.get("summary_failed", False),
|
"summary_failed": result.get("summary_failed", False),
|
||||||
"guide_message": result.get("guide_message"),
|
"guide_message": result.get("guide_message"),
|
||||||
|
"compress_form": result.get("compress_form"),
|
||||||
|
"compress_behavior": compress_behavior,
|
||||||
"load_result": load_result
|
"load_result": load_result
|
||||||
}
|
}
|
||||||
|
|
||||||
guide_message = (result.get("guide_message") or "").strip()
|
guide_message = (result.get("guide_message") or "").strip()
|
||||||
if guide_message:
|
if guide_message and compress_behavior == "continue":
|
||||||
|
# 继续工作:创建续接任务,自动发送引导语并触发请求(同一对话)。
|
||||||
try:
|
try:
|
||||||
from .tasks import task_manager
|
from .tasks import task_manager
|
||||||
workspace_id = session.get("workspace_id") or "default"
|
workspace_id = session.get("workspace_id") or "default"
|
||||||
@ -1205,7 +1203,7 @@ def compress_conversation(conversation_id, terminal: WebTerminal, workspace: Use
|
|||||||
workspace_id,
|
workspace_id,
|
||||||
guide_message,
|
guide_message,
|
||||||
images=[],
|
images=[],
|
||||||
conversation_id=new_conversation_id,
|
conversation_id=normalized_id,
|
||||||
videos=[],
|
videos=[],
|
||||||
message_source="compression_handoff",
|
message_source="compression_handoff",
|
||||||
)
|
)
|
||||||
@ -1217,6 +1215,21 @@ def compress_conversation(conversation_id, terminal: WebTerminal, workspace: Use
|
|||||||
debug_log(f"[Compression] 自动发送引导消息失败: {exc}")
|
debug_log(f"[Compression] 自动发送引导消息失败: {exc}")
|
||||||
response_payload["auto_task_started"] = False
|
response_payload["auto_task_started"] = False
|
||||||
response_payload["auto_task_error"] = str(exc)
|
response_payload["auto_task_error"] = str(exc)
|
||||||
|
elif guide_message and compress_behavior == "wait":
|
||||||
|
# 等待用户:只把引导语作为 user 消息追加进历史,不触发请求。
|
||||||
|
try:
|
||||||
|
terminal.context_manager.add_conversation(
|
||||||
|
role="user",
|
||||||
|
content=guide_message,
|
||||||
|
metadata={"message_source": "compression_handoff"},
|
||||||
|
)
|
||||||
|
response_payload["auto_task_started"] = False
|
||||||
|
response_payload["guide_inserted"] = True
|
||||||
|
except Exception as exc:
|
||||||
|
debug_log(f"[Compression] 追加引导语消息失败: {exc}")
|
||||||
|
response_payload["auto_task_started"] = False
|
||||||
|
response_payload["guide_inserted"] = False
|
||||||
|
response_payload["guide_insert_error"] = str(exc)
|
||||||
else:
|
else:
|
||||||
response_payload["auto_task_started"] = False
|
response_payload["auto_task_started"] = False
|
||||||
|
|
||||||
|
|||||||
@ -6,8 +6,6 @@ from datetime import datetime
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from modules.versioning_manager import ConversationVersioningManager
|
|
||||||
|
|
||||||
|
|
||||||
SUMMARY_PROMPT = (
|
SUMMARY_PROMPT = (
|
||||||
"由于当前对话过长,系统正在自动压缩。请你基于已有上下文输出一份可继续执行的工作总结,要求:\n"
|
"由于当前对话过长,系统正在自动压缩。请你基于已有上下文输出一份可继续执行的工作总结,要求:\n"
|
||||||
@ -18,7 +16,9 @@ SUMMARY_PROMPT = (
|
|||||||
"5) 工具调用中的重要结果/错误与修复\n"
|
"5) 工具调用中的重要结果/错误与修复\n"
|
||||||
"6) 当前未完成事项与下一步计划(可直接执行)\n"
|
"6) 当前未完成事项与下一步计划(可直接执行)\n"
|
||||||
"7) 风险与注意事项\n"
|
"7) 风险与注意事项\n"
|
||||||
"请使用中文,结构清晰,尽量具体,不要省略关键上下文。"
|
"请使用中文,结构清晰,尽量具体,不要省略关键上下文。\n"
|
||||||
|
"不要考虑过往对话,只考虑当前对话任务。\n"
|
||||||
|
"禁止调用任何工具,必须直接输出总结内容。"
|
||||||
)
|
)
|
||||||
|
|
||||||
GUIDE_USER_MESSAGE_TEMPLATE = "当前对话已经被自动压缩(第{count}次)。请阅读{path}并继续工作"
|
GUIDE_USER_MESSAGE_TEMPLATE = "当前对话已经被自动压缩(第{count}次)。请阅读{path}并继续工作"
|
||||||
@ -68,6 +68,7 @@ def _normalize_deep_compression_records(metadata: Dict[str, Any]) -> List[Dict[s
|
|||||||
|
|
||||||
|
|
||||||
def _build_guide_message(*, compression_index: int, compact_file: str, previous_records: List[Dict[str, Any]]) -> str:
|
def _build_guide_message(*, compression_index: int, compact_file: str, previous_records: List[Dict[str, Any]]) -> str:
|
||||||
|
"""生成文件模式的引导语:仅提示压缩文件位置,由模型自行阅读。"""
|
||||||
base = GUIDE_USER_MESSAGE_TEMPLATE.format(count=compression_index, path=compact_file)
|
base = GUIDE_USER_MESSAGE_TEMPLATE.format(count=compression_index, path=compact_file)
|
||||||
if not previous_records:
|
if not previous_records:
|
||||||
return base
|
return base
|
||||||
@ -81,6 +82,41 @@ def _build_guide_message(*, compression_index: int, compact_file: str, previous_
|
|||||||
return "\n".join(lines).strip()
|
return "\n".join(lines).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _read_compact_file_content(project_path: Path, relative_path: str) -> str:
|
||||||
|
"""读取 compact 文件全文,读取失败时返回空串。"""
|
||||||
|
rel = str(relative_path or "").strip()
|
||||||
|
if not rel:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
target = (Path(project_path) / rel).resolve()
|
||||||
|
return target.read_text(encoding="utf-8").strip()
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _build_inject_guide_message(
|
||||||
|
*,
|
||||||
|
project_path: Path,
|
||||||
|
compression_index: int,
|
||||||
|
current_record: Dict[str, Any],
|
||||||
|
previous_records: List[Dict[str, Any]],
|
||||||
|
) -> str:
|
||||||
|
"""生成直接注入模式的引导语:把历次压缩文件全文按顺序拼入正文,不提及文件位置。"""
|
||||||
|
lines: List[str] = [f"当前对话已被第{compression_index}次压缩,以下为历次压缩的完整工作总结,请据此继续工作。"]
|
||||||
|
all_records = list(previous_records) + [current_record]
|
||||||
|
for rec in all_records:
|
||||||
|
count = int(rec.get("count") or 0)
|
||||||
|
rel_path = str(rec.get("compact_file") or "").strip()
|
||||||
|
if count <= 0 or not rel_path:
|
||||||
|
continue
|
||||||
|
content = _read_compact_file_content(project_path, rel_path)
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"第{count}次压缩:")
|
||||||
|
lines.append(content or "(压缩文件内容读取失败)")
|
||||||
|
return "\n".join(lines).strip()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_text_only(content: Any) -> str:
|
def _extract_text_only(content: Any) -> str:
|
||||||
if content is None:
|
if content is None:
|
||||||
return ""
|
return ""
|
||||||
@ -162,10 +198,14 @@ async def _generate_summary(web_terminal, prompt: str, retries: int = 5) -> Tupl
|
|||||||
# build_messages 不会自动附加 user_input,压缩总结必须显式注入
|
# build_messages 不会自动附加 user_input,压缩总结必须显式注入
|
||||||
if prompt and isinstance(prompt, str):
|
if prompt and isinstance(prompt, str):
|
||||||
messages = list(messages) + [{"role": "user", "content": prompt}]
|
messages = list(messages) + [{"role": "user", "content": prompt}]
|
||||||
|
# 关键:总结请求必须与"用户正常发一条消息"完全无差别,才能 100% 命中前缀缓存。
|
||||||
|
# 因此 tools 必须照常传入(缺失 tools 字段会改变请求前缀、破坏缓存)。
|
||||||
|
# 模型即便返回 tool_calls 也会被忽略——我们只取 content;prompt 已要求其直接输出总结。
|
||||||
|
tools = web_terminal.define_tools()
|
||||||
for _ in range(max(1, retries)):
|
for _ in range(max(1, retries)):
|
||||||
try:
|
try:
|
||||||
response_text = ""
|
response_text = ""
|
||||||
async for chunk in web_terminal.api_client.chat(messages, tools=None, stream=False):
|
async for chunk in web_terminal.api_client.chat(messages, tools=tools, stream=False):
|
||||||
if not isinstance(chunk, dict):
|
if not isinstance(chunk, dict):
|
||||||
continue
|
continue
|
||||||
choices = chunk.get("choices") or []
|
choices = chunk.get("choices") or []
|
||||||
@ -236,6 +276,33 @@ def _write_compact_file(
|
|||||||
return str(file_path.relative_to(project_path))
|
return str(file_path.relative_to(project_path))
|
||||||
|
|
||||||
|
|
||||||
|
def _mark_history_compacted(history: List[Dict[str, Any]], *, round_index: int, now: str) -> int:
|
||||||
|
"""把当前对话历史中所有尚未标记的消息打上 deep_compacted 标记(in-place)。
|
||||||
|
|
||||||
|
深压缩按"整段前缀"处理:本次压缩时,历史里所有还没被标记的消息整体视为已压缩前缀。
|
||||||
|
这天然保证 assistant.tool_calls 与其 tool 响应被一并标记/排除,不会出现配对悬空。
|
||||||
|
原文保留在 metadata 中,持久化与重新加载时照常显示,仅在 build_messages 构建请求时被跳过。
|
||||||
|
返回本次新标记的消息条数。
|
||||||
|
"""
|
||||||
|
if not isinstance(history, list):
|
||||||
|
return 0
|
||||||
|
marked = 0
|
||||||
|
for msg in history:
|
||||||
|
if not isinstance(msg, dict):
|
||||||
|
continue
|
||||||
|
metadata = msg.get("metadata")
|
||||||
|
if not isinstance(metadata, dict):
|
||||||
|
metadata = {}
|
||||||
|
if metadata.get("deep_compacted"):
|
||||||
|
continue
|
||||||
|
metadata["deep_compacted"] = True
|
||||||
|
metadata["deep_compacted_round"] = round_index
|
||||||
|
metadata["deep_compacted_at"] = now
|
||||||
|
msg["metadata"] = metadata
|
||||||
|
marked += 1
|
||||||
|
return marked
|
||||||
|
|
||||||
|
|
||||||
async def run_deep_compression(
|
async def run_deep_compression(
|
||||||
*,
|
*,
|
||||||
web_terminal,
|
web_terminal,
|
||||||
@ -253,12 +320,33 @@ async def run_deep_compression(
|
|||||||
if metadata.get("compression_in_progress"):
|
if metadata.get("compression_in_progress"):
|
||||||
return {"success": False, "error": "对话正在压缩中", "in_progress": True}
|
return {"success": False, "error": "对话正在压缩中", "in_progress": True}
|
||||||
|
|
||||||
old_title = conv_data.get("title") or "未命名"
|
# 读取个性化压缩设置:
|
||||||
source_versioning = metadata.get("versioning") if isinstance(metadata.get("versioning"), dict) else {}
|
# - compress_form: file(生成文件,引导语提示位置) / inject(把历次压缩文件全文注入引导语)
|
||||||
source_versioning_enabled = bool(source_versioning.get("enabled"))
|
# - compress_behavior: continue(注入引导语并触发请求) / wait(仅注入引导语,等待用户)
|
||||||
source_tracking_mode = ConversationVersioningManager.normalize_tracking_mode(
|
# compress_behavior 仅作用于手动压缩;自动深压缩永远继续工作。
|
||||||
source_versioning.get("tracking_mode")
|
try:
|
||||||
)
|
from modules.personalization_manager import load_personalization_config
|
||||||
|
pconfig = load_personalization_config(workspace.data_dir) or {}
|
||||||
|
except Exception:
|
||||||
|
pconfig = {}
|
||||||
|
compress_form = str(pconfig.get("deep_compress_form") or "file").strip().lower()
|
||||||
|
if compress_form not in ("file", "inject"):
|
||||||
|
compress_form = "file"
|
||||||
|
compress_behavior = str(pconfig.get("deep_compress_behavior") or "continue").strip().lower()
|
||||||
|
if compress_behavior not in ("continue", "wait"):
|
||||||
|
compress_behavior = "continue"
|
||||||
|
if mode != "manual":
|
||||||
|
compress_behavior = "continue"
|
||||||
|
|
||||||
|
# in-place 压缩全程操作"当前对话"的内存历史:总结、标记、状态写入都依赖
|
||||||
|
# cm.conversation_history / cm.conversation_metadata。若当前对话不是目标对话,
|
||||||
|
# 必须先切换过去,否则总结会基于错误历史、压缩状态也会写错对话。
|
||||||
|
if getattr(cm, "current_conversation_id", None) != conversation_id:
|
||||||
|
try:
|
||||||
|
web_terminal.load_conversation(conversation_id)
|
||||||
|
except Exception as exc:
|
||||||
|
return {"success": False, "error": f"加载目标对话失败: {exc}"}
|
||||||
|
|
||||||
compression_count = int(metadata.get("compression_count", 0) or 0)
|
compression_count = int(metadata.get("compression_count", 0) or 0)
|
||||||
previous_records = _normalize_deep_compression_records(metadata)
|
previous_records = _normalize_deep_compression_records(metadata)
|
||||||
previous_max_count = max([int(item.get("count") or 0) for item in previous_records], default=0)
|
previous_max_count = max([int(item.get("count") or 0) for item in previous_records], default=0)
|
||||||
@ -313,124 +401,102 @@ async def run_deep_compression(
|
|||||||
cm.set_compression_state(
|
cm.set_compression_state(
|
||||||
in_progress=True,
|
in_progress=True,
|
||||||
mode=mode,
|
mode=mode,
|
||||||
stage="creating_new_conversation",
|
stage="marking_history",
|
||||||
job_id=job_id,
|
job_id=job_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
new_conv_id = cm.conversation_manager.create_conversation(
|
# === in-place 压缩:不创建/切换新对话,只把当前对话历史前缀打上 deep_compacted 标记 ===
|
||||||
project_path=str(workspace.project_path),
|
# 当前对话已在函数开头对齐为目标对话,这里直接对内存历史打标。
|
||||||
thinking_mode=bool(metadata.get("thinking_mode", False)),
|
now_iso = datetime.now().isoformat()
|
||||||
run_mode=metadata.get("run_mode") or ("thinking" if metadata.get("thinking_mode") else "fast"),
|
marked_count = _mark_history_compacted(
|
||||||
initial_messages=[],
|
cm.conversation_history or [],
|
||||||
model_key=metadata.get("model_key"),
|
round_index=target_count,
|
||||||
has_images=False,
|
now=now_iso,
|
||||||
has_videos=False,
|
|
||||||
metadata_overrides={
|
|
||||||
"compression_count": target_count,
|
|
||||||
"title_locked": True,
|
|
||||||
"skip_auto_title_generation": True,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
# 标记后立即持久化历史(标记写在每条消息的 metadata 中)。
|
||||||
|
try:
|
||||||
|
cm.save_current_conversation()
|
||||||
|
except Exception as exc:
|
||||||
|
_emit(sender, "system_message", {"content": f"压缩标记保存失败:{exc}"})
|
||||||
|
|
||||||
|
# 关键:重置 current_context_tokens,避免自动压缩续接后阈值判断仍读到压缩前的大值而陷入死循环。
|
||||||
|
# 真实上下文长度会在下一次 API 响应后被重新写入。
|
||||||
|
try:
|
||||||
|
cm.conversation_manager.update_token_statistics(
|
||||||
|
conversation_id,
|
||||||
|
input_tokens=0,
|
||||||
|
output_tokens=0,
|
||||||
|
total_tokens=0,
|
||||||
|
current_context_tokens=0,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
_emit(sender, "system_message", {"content": f"压缩后重置上下文统计失败:{exc}"})
|
||||||
|
|
||||||
current_record = {
|
current_record = {
|
||||||
"count": target_count,
|
"count": target_count,
|
||||||
"compact_file": relative_compact_path,
|
"compact_file": relative_compact_path,
|
||||||
"created_at": datetime.now().isoformat(),
|
"created_at": now_iso,
|
||||||
"source_conversation_id": conversation_id,
|
"source_conversation_id": conversation_id,
|
||||||
"compressed_conversation_id": new_conv_id,
|
"compressed_conversation_id": conversation_id,
|
||||||
}
|
}
|
||||||
all_records = previous_records + [current_record]
|
all_records = previous_records + [current_record]
|
||||||
cm.conversation_manager.update_conversation_metadata(
|
|
||||||
new_conv_id,
|
|
||||||
{
|
|
||||||
"compression_count": target_count,
|
|
||||||
"deep_compression_records": all_records,
|
|
||||||
"last_deep_compression_record": current_record,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if source_versioning_enabled:
|
# 构建引导语(按压缩形式)。inject 模式读取历次压缩文件全文,文件仍会生成,只是不提及位置。
|
||||||
try:
|
if compress_form == "inject":
|
||||||
vm = ConversationVersioningManager(
|
guide_message = _build_inject_guide_message(
|
||||||
project_path=workspace.project_path,
|
project_path=Path(workspace.project_path),
|
||||||
data_dir=workspace.data_dir,
|
compression_index=target_count,
|
||||||
conversation_id=new_conv_id,
|
current_record=current_record,
|
||||||
)
|
previous_records=previous_records,
|
||||||
vm_meta = vm.set_enabled(
|
)
|
||||||
enabled=True,
|
else:
|
||||||
mode="overwrite",
|
guide_message = _build_guide_message(
|
||||||
tracking_mode=source_tracking_mode,
|
compression_index=target_count,
|
||||||
)
|
compact_file=relative_compact_path,
|
||||||
new_conv_data = cm.conversation_manager.load_conversation(new_conv_id) or {}
|
previous_records=previous_records,
|
||||||
snapshot_payload = {
|
)
|
||||||
"conversation_id": new_conv_id,
|
|
||||||
"title": new_conv_data.get("title"),
|
|
||||||
"metadata": new_conv_data.get("metadata") or {},
|
|
||||||
"messages": new_conv_data.get("messages") or [],
|
|
||||||
"message_index": -1,
|
|
||||||
"run_status": "initial",
|
|
||||||
}
|
|
||||||
init_result = vm.ensure_initial_checkpoint(
|
|
||||||
workspace_path=str(workspace.project_path),
|
|
||||||
conversation_snapshot=snapshot_payload,
|
|
||||||
tracking_mode=source_tracking_mode,
|
|
||||||
)
|
|
||||||
init_row = init_result.get("row") or {}
|
|
||||||
last_commit = init_row.get("commit") or vm_meta.get("last_commit")
|
|
||||||
cm.conversation_manager.update_conversation_metadata(
|
|
||||||
new_conv_id,
|
|
||||||
{
|
|
||||||
"versioning": {
|
|
||||||
"enabled": True,
|
|
||||||
"mode": "overwrite",
|
|
||||||
"tracking_mode": source_tracking_mode,
|
|
||||||
"updated_at": datetime.now().isoformat(),
|
|
||||||
"last_commit": last_commit,
|
|
||||||
"last_input_seq": int(vm_meta.get("last_input_seq") or 0),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
cm.conversation_manager.update_conversation_title(new_conv_id, f"{old_title}对话 压缩后")
|
# 更新对话 metadata:压缩记录 + 清理压缩状态标记(同一对话,无切换)。
|
||||||
web_terminal.load_conversation(new_conv_id)
|
meta_updates = {
|
||||||
guide_message = _build_guide_message(
|
"compression_count": target_count,
|
||||||
compression_index=target_count,
|
"deep_compression_records": all_records,
|
||||||
compact_file=relative_compact_path,
|
"last_deep_compression_record": current_record,
|
||||||
previous_records=previous_records,
|
"compression_in_progress": False,
|
||||||
)
|
"compression_mode": None,
|
||||||
|
"compression_stage": None,
|
||||||
|
"compression_job_id": None,
|
||||||
|
"compression_error": summary_fail_reason,
|
||||||
|
"compression_resume_payload": None,
|
||||||
|
"is_ultra_long_conversation": False,
|
||||||
|
}
|
||||||
|
cm.conversation_manager.update_conversation_metadata(conversation_id, meta_updates)
|
||||||
|
# 同步内存中的 metadata,保证后续在同一对话内的判断读到最新值。
|
||||||
|
try:
|
||||||
|
if getattr(cm, "current_conversation_id", None) == conversation_id and isinstance(cm.conversation_metadata, dict):
|
||||||
|
cm.conversation_metadata.update(meta_updates)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# 清理旧对话压缩状态
|
|
||||||
cm.conversation_manager.update_conversation_metadata(
|
|
||||||
conversation_id,
|
|
||||||
{
|
|
||||||
"compression_in_progress": False,
|
|
||||||
"compression_mode": None,
|
|
||||||
"compression_stage": None,
|
|
||||||
"compression_job_id": None,
|
|
||||||
"compression_error": summary_fail_reason,
|
|
||||||
"compression_resume_payload": None,
|
|
||||||
"is_ultra_long_conversation": True,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
_emit(sender, "conversation_resolved", {
|
|
||||||
"source_conversation_id": conversation_id,
|
|
||||||
"conversation_id": new_conv_id,
|
|
||||||
"title": f"{old_title}对话 压缩后",
|
|
||||||
"created": True,
|
|
||||||
})
|
|
||||||
_emit(sender, "compression_finished", {
|
_emit(sender, "compression_finished", {
|
||||||
"source_conversation_id": conversation_id,
|
"source_conversation_id": conversation_id,
|
||||||
"conversation_id": new_conv_id,
|
"conversation_id": conversation_id,
|
||||||
"in_progress": False,
|
"in_progress": False,
|
||||||
"compact_file": relative_compact_path,
|
"compact_file": relative_compact_path,
|
||||||
|
"marked_count": marked_count,
|
||||||
|
"compress_form": compress_form,
|
||||||
|
"compress_behavior": compress_behavior,
|
||||||
"job_id": job_id,
|
"job_id": job_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"compressed_conversation_id": new_conv_id,
|
"in_place": True,
|
||||||
|
"compressed_conversation_id": conversation_id,
|
||||||
"compact_file": relative_compact_path,
|
"compact_file": relative_compact_path,
|
||||||
|
"marked_count": marked_count,
|
||||||
|
"compress_form": compress_form,
|
||||||
|
"compress_behavior": compress_behavior,
|
||||||
"summary_failed": bool(summary_fail_reason),
|
"summary_failed": bool(summary_fail_reason),
|
||||||
"guide_message": guide_message,
|
"guide_message": guide_message,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1057,6 +1057,7 @@ export const messageMethods = {
|
|||||||
|
|
||||||
if (response.ok && result.success) {
|
if (response.ok && result.success) {
|
||||||
this.compressionStage = 'switching';
|
this.compressionStage = 'switching';
|
||||||
|
// in-place 压缩:对话 id 不变,重新加载以刷新“已压缩历史”展示。
|
||||||
const newId = result.compressed_conversation_id;
|
const newId = result.compressed_conversation_id;
|
||||||
if (newId) {
|
if (newId) {
|
||||||
await this.loadConversation(newId, { force: true });
|
await this.loadConversation(newId, { force: true });
|
||||||
@ -1064,6 +1065,7 @@ export const messageMethods = {
|
|||||||
const guideMessage = (result.guide_message || '').trim();
|
const guideMessage = (result.guide_message || '').trim();
|
||||||
const autoTaskStarted = !!result.auto_task_started;
|
const autoTaskStarted = !!result.auto_task_started;
|
||||||
const autoTaskId = result.auto_task_id;
|
const autoTaskId = result.auto_task_id;
|
||||||
|
const compressBehavior = String(result.compress_behavior || 'continue');
|
||||||
if (autoTaskStarted && autoTaskId) {
|
if (autoTaskStarted && autoTaskId) {
|
||||||
const { useTaskStore } = await import('../../stores/task');
|
const { useTaskStore } = await import('../../stores/task');
|
||||||
const taskStore = useTaskStore();
|
const taskStore = useTaskStore();
|
||||||
@ -1079,6 +1081,11 @@ export const messageMethods = {
|
|||||||
if (typeof this.monitorShowPendingReply === 'function') {
|
if (typeof this.monitorShowPendingReply === 'function') {
|
||||||
this.monitorShowPendingReply();
|
this.monitorShowPendingReply();
|
||||||
}
|
}
|
||||||
|
} else if (compressBehavior === 'wait') {
|
||||||
|
// 等待用户:后端已把引导语作为 user 消息追加进历史,这里仅刷新展示,不触发请求。
|
||||||
|
if (newId) {
|
||||||
|
await this.loadConversation(newId, { force: true });
|
||||||
|
}
|
||||||
} else if (guideMessage) {
|
} else if (guideMessage) {
|
||||||
await this.sendAutoUserMessage(guideMessage);
|
await this.sendAutoUserMessage(guideMessage);
|
||||||
}
|
}
|
||||||
@ -1089,7 +1096,7 @@ export const messageMethods = {
|
|||||||
debugLog('对话压缩完成:', result);
|
debugLog('对话压缩完成:', result);
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
title: '压缩完成',
|
title: '压缩完成',
|
||||||
message: '已生成压缩后的新对话',
|
message: '已压缩较早的对话内容',
|
||||||
type: 'success',
|
type: 'success',
|
||||||
duration: 2200
|
duration: 2200
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2427,8 +2427,10 @@ export const taskPollingMethods = {
|
|||||||
this.compressionMode = '';
|
this.compressionMode = '';
|
||||||
this.compressionStage = '';
|
this.compressionStage = '';
|
||||||
this.compressionError = '';
|
this.compressionError = '';
|
||||||
|
// in-place 压缩:对话 id 不变。重新加载该对话以刷新“已压缩历史”的展示;
|
||||||
|
// 若返回的 id 与当前不一致(兼容旧行为)也按其加载。
|
||||||
const newId = data?.conversation_id;
|
const newId = data?.conversation_id;
|
||||||
if (newId && newId !== this.currentConversationId) {
|
if (newId) {
|
||||||
await this.loadConversation(newId, { force: true });
|
await this.loadConversation(newId, { force: true });
|
||||||
}
|
}
|
||||||
this.conversationsOffset = 0;
|
this.conversationsOffset = 0;
|
||||||
@ -2436,13 +2438,11 @@ export const taskPollingMethods = {
|
|||||||
await this.loadConversationsList();
|
await this.loadConversationsList();
|
||||||
}
|
}
|
||||||
await this.refreshRunningWorkspaceTasks?.();
|
await this.refreshRunningWorkspaceTasks?.();
|
||||||
// 压缩会把同一个后台任务从旧 conversation 迁移到新 conversation。
|
// 续接任务仍在同一对话内运行,恢复轮询以持续显示进度。
|
||||||
// 切换对话时本地轮询会被清理,这里必须立即按新会话恢复轮询,
|
|
||||||
// 否则后续进度需要手动刷新页面才会继续显示。
|
|
||||||
await this.restoreTaskState?.();
|
await this.restoreTaskState?.();
|
||||||
this.uiPushToast({
|
this.uiPushToast({
|
||||||
title: '压缩完成',
|
title: '压缩完成',
|
||||||
message: '已自动切换到压缩后的新对话',
|
message: '已压缩较早的对话内容',
|
||||||
type: 'success',
|
type: 'success',
|
||||||
duration: 2400
|
duration: 2400
|
||||||
});
|
});
|
||||||
|
|||||||
@ -995,6 +995,50 @@
|
|||||||
"
|
"
|
||||||
/></label>
|
/></label>
|
||||||
</div>
|
</div>
|
||||||
|
<label class="settings-toggle-row inner"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">深压缩直接注入</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>关闭:生成压缩文件,引导语提示文件位置由模型自行阅读;开启:把历次压缩文件全文直接注入引导语(文件仍会生成)。</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.deep_compress_form === 'inject'"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'deep_compress_form',
|
||||||
|
value: $event.target.checked ? 'inject' : 'file'
|
||||||
|
})
|
||||||
|
" /><span class="fancy-check" aria-hidden="true"
|
||||||
|
><svg viewBox="0 0 64 64">
|
||||||
|
<path
|
||||||
|
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
|
||||||
|
pathLength="575.0541381835938"
|
||||||
|
class="fancy-path"
|
||||||
|
></path></svg></span
|
||||||
|
></label>
|
||||||
|
<label class="settings-toggle-row inner"
|
||||||
|
><span class="settings-row-copy"
|
||||||
|
><span class="settings-row-title">手动压缩后等待用户</span
|
||||||
|
><span class="settings-row-desc"
|
||||||
|
>仅作用于手动压缩。关闭:压缩后自动注入引导语并继续工作;开启:压缩后只插入引导语,等待你下一步输入。自动深压缩始终继续工作。</span
|
||||||
|
></span
|
||||||
|
><input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="form.deep_compress_behavior === 'wait'"
|
||||||
|
@change="
|
||||||
|
personalization.updateField({
|
||||||
|
key: 'deep_compress_behavior',
|
||||||
|
value: $event.target.checked ? 'wait' : 'continue'
|
||||||
|
})
|
||||||
|
" /><span class="fancy-check" aria-hidden="true"
|
||||||
|
><svg viewBox="0 0 64 64">
|
||||||
|
<path
|
||||||
|
d="M 0 16 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 16 L 32 48 L 64 16 V 8 A 8 8 90 0 0 56 0 H 8 A 8 8 90 0 0 0 8 V 56 A 8 8 90 0 0 8 64 H 56 A 8 8 90 0 0 64 56 V 16"
|
||||||
|
pathLength="575.0541381835938"
|
||||||
|
class="fancy-path"
|
||||||
|
></path></svg></span
|
||||||
|
></label>
|
||||||
<div class="settings-inline-actions right">
|
<div class="settings-inline-actions right">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -43,6 +43,8 @@ interface PersonalForm {
|
|||||||
shallow_compress_max_replace_per_round: number | null;
|
shallow_compress_max_replace_per_round: number | null;
|
||||||
shallow_compress_trigger_tool_calls_interval: number | null;
|
shallow_compress_trigger_tool_calls_interval: number | null;
|
||||||
deep_compress_trigger_tokens: number | null;
|
deep_compress_trigger_tokens: number | null;
|
||||||
|
deep_compress_form: 'file' | 'inject';
|
||||||
|
deep_compress_behavior: 'continue' | 'wait';
|
||||||
agents_md_auto_inject: boolean;
|
agents_md_auto_inject: boolean;
|
||||||
allow_root_file_creation: boolean;
|
allow_root_file_creation: boolean;
|
||||||
theme: 'classic' | 'light' | 'dark';
|
theme: 'classic' | 'light' | 'dark';
|
||||||
@ -134,6 +136,8 @@ const defaultForm = (): PersonalForm => ({
|
|||||||
shallow_compress_max_replace_per_round: null,
|
shallow_compress_max_replace_per_round: null,
|
||||||
shallow_compress_trigger_tool_calls_interval: null,
|
shallow_compress_trigger_tool_calls_interval: null,
|
||||||
deep_compress_trigger_tokens: null,
|
deep_compress_trigger_tokens: null,
|
||||||
|
deep_compress_form: 'file',
|
||||||
|
deep_compress_behavior: 'continue',
|
||||||
agents_md_auto_inject: false,
|
agents_md_auto_inject: false,
|
||||||
allow_root_file_creation: false,
|
allow_root_file_creation: false,
|
||||||
theme: loadCachedTheme(),
|
theme: loadCachedTheme(),
|
||||||
@ -339,6 +343,8 @@ export const usePersonalizationStore = defineStore('personalization', {
|
|||||||
deep_compress_trigger_tokens: this.normalizeCompressionNumber(
|
deep_compress_trigger_tokens: this.normalizeCompressionNumber(
|
||||||
data.deep_compress_trigger_tokens
|
data.deep_compress_trigger_tokens
|
||||||
),
|
),
|
||||||
|
deep_compress_form: data.deep_compress_form === 'inject' ? 'inject' : 'file',
|
||||||
|
deep_compress_behavior: data.deep_compress_behavior === 'wait' ? 'wait' : 'continue',
|
||||||
agents_md_auto_inject: !!data.agents_md_auto_inject,
|
agents_md_auto_inject: !!data.agents_md_auto_inject,
|
||||||
allow_root_file_creation: !!data.allow_root_file_creation,
|
allow_root_file_creation: !!data.allow_root_file_creation,
|
||||||
theme: ['classic', 'light', 'dark'].includes(data.theme) ? data.theme : fallbackTheme,
|
theme: ['classic', 'light', 'dark'].includes(data.theme) ? data.theme : fallbackTheme,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user