merge: feat/deep-compression-inplace

This commit is contained in:
JOJO 2026-05-30 22:19:51 +08:00
commit 72c2b2af2a
17 changed files with 588 additions and 197 deletions

3
.gitignore vendored
View File

@ -42,3 +42,6 @@ doc/
# Host workspace config (local, use .example) # Host workspace config (local, use .example)
config/host_workspaces.json config/host_workspaces.json
config/custom_models.json config/custom_models.json
# Claude Code local artifacts (settings, worktrees)
.claude/

View File

@ -5,6 +5,8 @@
这份文档基于当前仓库实际代码重写。若与未来代码冲突,以代码为准并及时更新本文档。 这份文档基于当前仓库实际代码重写。若与未来代码冲突,以代码为准并及时更新本文档。
> 和用户交流时默认使用中文。
## 1) 当前项目结构(按代码现状) ## 1) 当前项目结构(按代码现状)
- **后端入口** - **后端入口**

136
CLAUDE.md
View File

@ -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 壳版本时,必须同步完成:

View File

@ -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]:

View File

@ -162,6 +162,7 @@ class MainTerminalToolsDefinitionMixin:
# 更新分类为空列表,避免旧缓存 # 更新分类为空列表,避免旧缓存
if "custom" in self.tool_categories_map: if "custom" in self.tool_categories_map:
self.tool_categories_map["custom"].tools = [] self.tool_categories_map["custom"].tools = []
self._refresh_disabled_tools()
return [] return []
tools: List[Dict] = [] tools: List[Dict] = []
@ -194,6 +195,7 @@ class MainTerminalToolsDefinitionMixin:
# 覆盖 custom 分类的工具列表 # 覆盖 custom 分类的工具列表
if "custom" in self.tool_categories_map: if "custom" in self.tool_categories_map:
self.tool_categories_map["custom"].tools = tool_ids self.tool_categories_map["custom"].tools = tool_ids
self._refresh_disabled_tools()
return tools return tools

View File

@ -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"),

View File

@ -63,6 +63,19 @@ def _set_terminal_workspace_label(terminal: WebTerminal, label: Optional[str]) -
pass pass
def _apply_workspace_personalization_preferences(terminal: WebTerminal, workspace) -> None:
"""Apply persisted workspace personalization after policy/workspace resolution."""
try:
config = load_personalization_config(workspace.data_dir)
terminal.apply_personalization_preferences(config)
if has_request_context():
session["run_mode"] = getattr(terminal, "run_mode", session.get("run_mode"))
session["thinking_mode"] = getattr(terminal, "thinking_mode", session.get("thinking_mode"))
session["model_key"] = getattr(terminal, "model_key", session.get("model_key"))
except Exception as exc:
debug_log(f"[Personalization] 应用工作区偏好失败: {exc}")
def _ensure_workspace_skills_synced(terminal: WebTerminal, workspace) -> None: def _ensure_workspace_skills_synced(terminal: WebTerminal, workspace) -> None:
""" """
确保工作区 skills 已按当前个性化配置完成同步 确保工作区 skills 已按当前个性化配置完成同步
@ -300,6 +313,7 @@ def get_user_resources(username: Optional[str] = None, workspace_id: Optional[st
except Exception as exc: except Exception as exc:
debug_log(f"[admin_policy][host_mode] 应用失败: {exc}") debug_log(f"[admin_policy][host_mode] 应用失败: {exc}")
_apply_workspace_personalization_preferences(terminal, workspace)
_ensure_workspace_skills_synced(terminal, workspace) _ensure_workspace_skills_synced(terminal, workspace)
write_host_workspace_debug( write_host_workspace_debug(
"context.get_user_resources.host.return", "context.get_user_resources.host.return",
@ -437,6 +451,7 @@ def get_user_resources(username: Optional[str] = None, workspace_id: Optional[st
except Exception as exc: except Exception as exc:
debug_log(f"[admin_policy] 应用失败: {exc}") debug_log(f"[admin_policy] 应用失败: {exc}")
_apply_workspace_personalization_preferences(terminal, workspace)
_ensure_workspace_skills_synced(terminal, workspace) _ensure_workspace_skills_synced(terminal, workspace)
return terminal, workspace return terminal, workspace

View File

@ -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

View File

@ -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 也会被忽略——我们只取 contentprompt 已要求其直接输出总结。
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,
} }

View File

@ -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
}); });

View File

@ -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
}); });

View File

@ -8,8 +8,10 @@
:class="{ :class="{
'goal-mode-banner--running': goalRunning, 'goal-mode-banner--running': goalRunning,
'goal-mode-banner--done': goalCompleted && !goalRunning, 'goal-mode-banner--done': goalCompleted && !goalRunning,
'goal-mode-banner--clickable': goalRunning || goalCompleted 'goal-mode-banner--clickable': goalRunning || goalCompleted,
'goal-mode-banner--collapsed': goalBannerCollapsed
}" }"
:title="goalBannerCollapsed ? goalBannerTitle : undefined"
@click.stop="goalRunning || goalCompleted ? $emit('open-goal-dialog') : null" @click.stop="goalRunning || goalCompleted ? $emit('open-goal-dialog') : null"
> >
<span class="goal-mode-banner__dot" aria-hidden="true"></span> <span class="goal-mode-banner__dot" aria-hidden="true"></span>
@ -1040,6 +1042,18 @@ const hasRuntimeLayoutExpansion = computed(() => {
const goalCompleted = computed(() => String(props.goalProgress?.status || '').toLowerCase() === 'done'); const goalCompleted = computed(() => String(props.goalProgress?.status || '').toLowerCase() === 'done');
// skill
// / skill
const goalBannerCollapsed = computed(
() => runtimeQueuedMessagesForRender.value.length > 0 || skillSlashMenuOpen.value
);
const goalBannerTitle = computed(() => {
if (props.goalRunning) return '目标模式运行中';
if (goalCompleted.value) return '目标模式完成';
return '目标模式已就绪';
});
const collectComposerVisualHeight = () => { const collectComposerVisualHeight = () => {
const root = inputAreaRoot.value; const root = inputAreaRoot.value;
const shell = compactInputShell.value; const shell = compactInputShell.value;
@ -1049,7 +1063,9 @@ const collectComposerVisualHeight = () => {
const shellRect = shell.getBoundingClientRect(); const shellRect = shell.getBoundingClientRect();
let top = shellRect.top; let top = shellRect.top;
let bottom = shellRect.bottom; let bottom = shellRect.bottom;
const nodes = root.querySelectorAll('.runtime-queue-list:not(.runtime-queue-list--empty), .skill-slash-menu, .goal-mode-banner'); //
// .goal-mode-banner--collapsed
const nodes = root.querySelectorAll('.runtime-queue-list:not(.runtime-queue-list--empty), .skill-slash-menu, .goal-mode-banner:not(.goal-mode-banner--collapsed)');
nodes.forEach((node) => { nodes.forEach((node) => {
if (!(node instanceof HTMLElement)) return; if (!(node instanceof HTMLElement)) return;
const rect = node.getBoundingClientRect(); const rect = node.getBoundingClientRect();
@ -1258,6 +1274,13 @@ watch(
} }
); );
// skill /
//
watch(goalBannerCollapsed, async () => {
await nextTick();
emitComposerHeight();
});
onMounted(() => { onMounted(() => {
if (hasSkillMarkdownLink.value) { if (hasSkillMarkdownLink.value) {
renderRichEditor(); renderRichEditor();
@ -1321,6 +1344,30 @@ onBeforeUnmount(() => {
background: color-mix(in srgb, var(--theme-surface-soft, #fffaf0) 92%, var(--claude-text-tertiary, #a59a86) 8%); background: color-mix(in srgb, var(--theme-surface-soft, #fffaf0) 92%, var(--claude-text-tertiary, #a59a86) 8%);
border: 1px solid var(--theme-chip-border, rgba(118, 103, 84, 0.2)); border: 1px solid var(--theme-chip-border, rgba(118, 103, 84, 0.2));
user-select: none; user-select: none;
transition: gap 0.2s ease, padding 0.2s ease;
}
/* padding
关键切到 absolute 脱离文档流避免横幅占用一行流高度把队列/skill 菜单顶起来
锚定在输入框上方 6px 4px与展开时的视觉位置一致圆点位置不变 */
.goal-mode-banner--collapsed {
position: absolute;
left: 4px;
bottom: calc(100% + 6px);
z-index: 2;
margin: 0;
gap: 0;
padding-right: 10px;
}
.goal-mode-banner__text {
overflow: hidden;
white-space: nowrap;
max-width: 200px;
opacity: 1;
transition: max-width 0.2s ease, opacity 0.18s ease;
}
.goal-mode-banner--collapsed .goal-mode-banner__text {
max-width: 0;
opacity: 0;
} }
.goal-mode-banner--running { .goal-mode-banner--running {
color: var(--claude-accent, #da7756); color: var(--claude-accent, #da7756);

View File

@ -5,7 +5,7 @@
<div class="exp-header"> <div class="exp-header">
<div> <div>
<div class="title">第一次使用选择适合您的模式</div> <div class="title">第一次使用选择适合您的模式</div>
<div class="subtitle">速度联网与工具范围各不相同随时可在个人空间切换</div> <div class="subtitle">速度联网与工具范围各不相同随时可在个人空间切换</div>
</div> </div>
<button class="confirm-btn" type="button" @click="confirmSelection">确认并开始</button> <button class="confirm-btn" type="button" @click="confirmSelection">确认并开始</button>
</div> </div>
@ -57,28 +57,28 @@ const modes = [
id: 'fast', id: 'fast',
badge: '极速', badge: '极速',
title: '快速回答', title: '快速回答',
desc: '快速回答您的问题,会上网搜索,不会写文件', desc: '快速回答您的问题,会上网搜索,不会写文件',
icon: `<svg viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2"><path d="M13 2 5 14h6l-2 8 10-12h-6z"/></svg>` icon: `<svg viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2"><path d="M13 2 5 14h6l-2 8 10-12h-6z"/></svg>`
}, },
{ {
id: 'thinking', id: 'thinking',
badge: '思考', badge: '思考',
title: '思考后再答', title: '思考后再答',
desc: '经过思考后完成任务,会上网搜索,不会写文件,回复时间较长', desc: '经过思考后完成任务,会上网搜索,不会写文件,回复时间较长',
icon: `<svg viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2"><path d="M12 18V5"/><path d="M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4"/><path d="M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5"/><path d="M17.997 5.125a4 4 0 0 1 2.526 5.77"/><path d="M18 18a4 4 0 0 0 2-7.464"/><path d="M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517"/><path d="M6 18a4 4 0 0 1-2-7.464"/><path d="M6.003 5.125a4 4 0 0 0-2.526 5.77"/></svg>` icon: `<svg viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2"><path d="M12 18V5"/><path d="M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4"/><path d="M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5"/><path d="M17.997 5.125a4 4 0 0 1 2.526 5.77"/><path d="M18 18a4 4 0 0 0 2-7.464"/><path d="M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517"/><path d="M6 18a4 4 0 0 1-2-7.464"/><path d="M6.003 5.125a4 4 0 0 0-2.526 5.77"/></svg>`
}, },
{ {
id: 'research', id: 'research',
badge: '研究', badge: '研究',
title: '多步骤研究', title: '多步骤研究',
desc: '多步骤思考与 20+ 工具完成复杂任务,回复时间可能很长', desc: '多步骤思考与 20+ 工具完成复杂任务,回复时间可能很长',
icon: `<svg viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2"><path d="M9 3v5l-4 9a2 2 0 0 0 1.8 3h10.4A2 2 0 0 0 19 17l-4-9V3"/><path d="M9 8h6"/></svg>` icon: `<svg viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2"><path d="M9 3v5l-4 9a2 2 0 0 0 1.8 3h10.4A2 2 0 0 0 19 17l-4-9V3"/><path d="M9 8h6"/></svg>`
}, },
{ {
id: 'expert', id: 'expert',
badge: '专家', badge: '专家',
title: '全功能探索', title: '全功能探索',
desc: '自由探索所有配置选项,适合智能体专家与从业者', desc: '自由探索所有配置选项,适合智能体专家与从业者',
icon: `<svg viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2"><path d="M3 9 12 5l9 4-9 4-9-4Z"/><path d="M6 10v5c0 .6.4 1.2 1 1.4l5 2.1 5-2.1c.6-.2 1-.8 1-1.4v-5"/><path d="M12 13v6"/></svg>` icon: `<svg viewBox="0 0 24 24" stroke="currentColor" fill="none" stroke-width="2"><path d="M3 9 12 5l9 4-9 4-9-4Z"/><path d="M6 10v5c0 .6.4 1.2 1 1.4l5 2.1 5-2.1c.6-.2 1-.8 1-1.4v-5"/><path d="M12 13v6"/></svg>`
} }
] as const; ] as const;

View File

@ -90,7 +90,7 @@
<span class="settings-row-copy"> <span class="settings-row-copy">
<span class="settings-row-title">自动生成对话标题</span> <span class="settings-row-title">自动生成对话标题</span>
<span class="settings-row-desc" <span class="settings-row-desc"
>默认开启关闭后标题将沿用首条消息</span >默认开启关闭后标题将沿用首条消息</span
> >
</span> </span>
<input <input
@ -116,7 +116,7 @@
<span class="settings-row-copy"> <span class="settings-row-copy">
<span class="settings-row-title">新手教程</span> <span class="settings-row-title">新手教程</span>
<span class="settings-row-desc" <span class="settings-row-desc"
>通过高亮与说明窗了解主要功能入口和常用设置</span >通过高亮与说明窗了解主要功能入口和常用设置</span
> >
</span> </span>
<button <button
@ -160,7 +160,7 @@
<div class="settings-action-row danger-zone"> <div class="settings-action-row danger-zone">
<span class="settings-row-copy"> <span class="settings-row-copy">
<span class="settings-row-title">退出登录</span> <span class="settings-row-title">退出登录</span>
<span class="settings-row-desc">结束当前账号会话</span> <span class="settings-row-desc">结束当前账号会话</span>
</span> </span>
<button <button
type="button" type="button"
@ -181,7 +181,7 @@
<label class="settings-toggle-row"> <label class="settings-toggle-row">
<span class="settings-row-copy"> <span class="settings-row-copy">
<span class="settings-row-title">启用个性化提示</span> <span class="settings-row-title">启用个性化提示</span>
<span class="settings-row-desc">开启后才会注入您的偏好</span> <span class="settings-row-desc">开启后才会注入您的偏好</span>
</span> </span>
<input <input
type="checkbox" type="checkbox"
@ -278,7 +278,7 @@
<span class="settings-row-copy"> <span class="settings-row-copy">
<span class="settings-row-title">回答时必须考虑的信息</span> <span class="settings-row-title">回答时必须考虑的信息</span>
<span class="settings-row-desc" <span class="settings-row-desc"
>最多 {{ maxConsiderations }} 可拖动排序</span >最多 {{ maxConsiderations }} 可拖动排序</span
> >
</span> </span>
<div class="settings-input-stack wide"> <div class="settings-input-stack wide">
@ -326,7 +326,7 @@
<div class="settings-select-row"> <div class="settings-select-row">
<span class="settings-row-copy"> <span class="settings-row-copy">
<span class="settings-row-title">智能体交流风格</span> <span class="settings-row-title">智能体交流风格</span>
<span class="settings-row-desc">智能体和您交流时使用何种形式</span> <span class="settings-row-desc">智能体和您交流时使用何种形式</span>
</span> </span>
<div <div
class="settings-select-wrap" class="settings-select-wrap"
@ -351,7 +351,7 @@
:class="{ selected: form.communication_style === 'default' }" :class="{ selected: form.communication_style === 'default' }"
@click="selectCommunicationStyle('default')" @click="selectCommunicationStyle('default')"
> >
<strong>默认</strong><span>标准 AI 风格</span <strong>默认</strong><span>标准 AI 风格</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg> ><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button> </button>
<button <button
@ -360,7 +360,7 @@
:class="{ selected: form.communication_style === 'human_like' }" :class="{ selected: form.communication_style === 'human_like' }"
@click="selectCommunicationStyle('human_like')" @click="selectCommunicationStyle('human_like')"
> >
<strong>拟人</strong><span>像聊天一样模仿人类语言风格</span <strong>拟人</strong><span>像聊天一样模仿人类语言风格</span
><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg> ><svg viewBox="0 0 24 24"><path d="M5 12.5 9.5 17 19 7" /></svg>
</button> </button>
</div> </div>
@ -378,7 +378,7 @@
<span class="settings-row-copy" <span class="settings-row-copy"
><span class="settings-row-title">默认模型</span ><span class="settings-row-title">默认模型</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>为新对话/登录后首次请求选择首选模型</span >为新对话/登录后首次请求选择首选模型</span
></span ></span
> >
<div <div
@ -421,7 +421,7 @@
<span class="settings-row-copy" <span class="settings-row-copy"
><span class="settings-row-title">默认运行模式</span ><span class="settings-row-title">默认运行模式</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>登录或新建任务时的初始运行模式</span >登录或新建任务时的初始运行模式</span
></span ></span
> >
<div <div
@ -460,7 +460,7 @@
<span class="settings-row-copy" <span class="settings-row-copy"
><span class="settings-row-title">思考频率</span ><span class="settings-row-title">思考频率</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>连续快速模式运行多少轮后系统强制切换到思考模型</span >连续快速模式运行多少轮后系统强制切换到思考模型</span
></span ></span
> >
<div class="settings-input-stack wide"> <div class="settings-input-stack wide">
@ -501,7 +501,7 @@
<div class="settings-select-row"> <div class="settings-select-row">
<span class="settings-row-copy" <span class="settings-row-copy"
><span class="settings-row-title">主题切换</span ><span class="settings-row-title">主题切换</span
><span class="settings-row-desc">在经典明亮和夜间之间切换</span></span ><span class="settings-row-desc">在经典明亮和夜间之间切换</span></span
> >
<div <div
class="settings-select-wrap" class="settings-select-wrap"
@ -538,7 +538,7 @@
><span class="settings-row-copy" ><span class="settings-row-copy"
><span class="settings-row-title">使用自定义称呼</span ><span class="settings-row-title">使用自定义称呼</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>对话区域使用个性化设置中的自称和称呼</span >对话区域使用个性化设置中的自称和称呼</span
></span ></span
><input ><input
type="checkbox" type="checkbox"
@ -560,7 +560,7 @@
><span class="settings-row-copy" ><span class="settings-row-copy"
><span class="settings-row-title">增强工具显示</span ><span class="settings-row-title">增强工具显示</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>工具块显示格式化内容关闭则显示原始 JSON</span >工具块显示格式化内容关闭则显示原始 JSON</span
></span ></span
><input ><input
type="checkbox" type="checkbox"
@ -581,7 +581,7 @@
<div class="settings-select-row"> <div class="settings-select-row">
<span class="settings-row-copy" <span class="settings-row-copy"
><span class="settings-row-title">堆叠块显示模式</span ><span class="settings-row-title">堆叠块显示模式</span
><span class="settings-row-desc">选择思考/工具块的显示方式</span></span ><span class="settings-row-desc">选择思考/工具块的显示方式</span></span
> >
<div <div
class="settings-select-wrap" class="settings-select-wrap"
@ -619,7 +619,7 @@
<span class="settings-row-copy" <span class="settings-row-copy"
><span class="settings-row-title">简略消息显示</span ><span class="settings-row-title">简略消息显示</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>审核子智能体等紧凑系统消息的显示形式</span >审核子智能体等紧凑系统消息的显示形式</span
></span ></span
> >
<div <div
@ -665,7 +665,7 @@
<div class="settings-select-row"> <div class="settings-select-row">
<span class="settings-row-copy" <span class="settings-row-copy"
><span class="settings-row-title">默认权限</span ><span class="settings-row-title">默认权限</span
><span class="settings-row-desc">新建对话时的默认权限模式</span></span ><span class="settings-row-desc">新建对话时的默认权限模式</span></span
> >
<div <div
class="settings-select-wrap" class="settings-select-wrap"
@ -703,7 +703,7 @@
><span class="settings-row-copy" ><span class="settings-row-copy"
><span class="settings-row-title">允许根目录创建文件</span ><span class="settings-row-title">允许根目录创建文件</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>关闭则禁止在工作区根目录创建文件</span >关闭则禁止在工作区根目录创建文件</span
></span ></span
><input ><input
type="checkbox" type="checkbox"
@ -725,7 +725,7 @@
><span class="settings-row-copy" ><span class="settings-row-copy"
><span class="settings-row-title">AGENTS.md 自动注入</span ><span class="settings-row-title">AGENTS.md 自动注入</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>工作区根目录存在 AGENTS.md 时自动注入系统提示词</span >工作区根目录存在 AGENTS.md 时自动注入系统提示词</span
></span ></span
><input ><input
type="checkbox" type="checkbox"
@ -752,7 +752,7 @@
><span class="settings-row-copy" ><span class="settings-row-copy"
><span class="settings-row-title">审核智能体主动取证</span ><span class="settings-row-title">审核智能体主动取证</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>开启后审核智能体可运行只读命令查看文件跑测试来核实目标是否达成关闭则仅依据对话内容判断</span >开启后可运行只读命令核实目标关闭则仅依据对话判断</span
></span ></span
><input ><input
type="checkbox" type="checkbox"
@ -775,7 +775,7 @@
<span class="settings-row-copy" <span class="settings-row-copy"
><span class="settings-row-title">最大自动续命轮数</span ><span class="settings-row-title">最大自动续命轮数</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>目标模式下主模型停下后最多自动继续的轮数1-100</span >主模型停下后最多自动继续 1-100 </span
></span ></span
> >
<input <input
@ -797,7 +797,7 @@
><span class="settings-row-copy" ><span class="settings-row-copy"
><span class="settings-row-title">启用累计 token 上限</span ><span class="settings-row-title">启用累计 token 上限</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>开启后目标模式累计消耗输入+输出超过下方上限即停止</span >累计输入输出超过上限时停止目标模式</span
></span ></span
><input ><input
type="checkbox" type="checkbox"
@ -816,7 +816,7 @@
<div v-if="goalTokenLimitEnabled" class="settings-select-row"> <div v-if="goalTokenLimitEnabled" class="settings-select-row">
<span class="settings-row-copy" <span class="settings-row-copy"
><span class="settings-row-title">累计 token 上限</span ><span class="settings-row-title">累计 token 上限</span
><span class="settings-row-desc">达到该累计消耗即停止目标模式</span></span ><span class="settings-row-desc">达到该累计消耗即停止目标模式</span></span
> >
<input <input
type="number" type="number"
@ -843,7 +843,7 @@
><span class="settings-row-copy" ><span class="settings-row-copy"
><span class="settings-row-title">最近对话提示</span ><span class="settings-row-title">最近对话提示</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>把当前工作区最近若干个非空对话注入系统提示词</span >把当前工作区最近若干个非空对话注入系统提示词</span
></span ></span
><input ><input
type="checkbox" type="checkbox"
@ -883,7 +883,7 @@
<div class="settings-group-title"> <div class="settings-group-title">
<span class="settings-row-title">上下文压缩策略</span <span class="settings-row-title">上下文压缩策略</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>可分别控制自动浅层压缩与自动深层压缩</span >可分别控制自动浅层压缩与自动深层压缩</span
> >
</div> </div>
<label class="settings-toggle-row inner" <label class="settings-toggle-row inner"
@ -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"
@ -1012,7 +1056,7 @@
><span class="settings-row-copy" ><span class="settings-row-copy"
><span class="settings-row-title">静默禁用工具</span ><span class="settings-row-title">静默禁用工具</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>禁用工具时不再注入提示消息模型不会感知被禁用项</span >禁用工具时不再注入提示消息模型不会感知被禁用项</span
></span ></span
><input ><input
type="checkbox" type="checkbox"
@ -1034,7 +1078,7 @@
><span class="settings-row-copy" ><span class="settings-row-copy"
><span class="settings-row-title">工具意图提示</span ><span class="settings-row-title">工具意图提示</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>调用工具时先用简短文字告诉你要做什么</span >调用工具时先用简短文字告诉你要做什么</span
></span ></span
><input ><input
type="checkbox" type="checkbox"
@ -1056,7 +1100,7 @@
><span class="settings-row-copy" ><span class="settings-row-copy"
><span class="settings-row-title">Skill 提示系统</span ><span class="settings-row-title">Skill 提示系统</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>检测到特定关键词时自动提示模型阅读相关 skill</span >检测到特定关键词时自动提示模型阅读相关 skill</span
></span ></span
><input ><input
type="checkbox" type="checkbox"
@ -1078,7 +1122,7 @@
<div class="settings-group-title"> <div class="settings-group-title">
<span class="settings-row-title">强约束系统</span <span class="settings-row-title">强约束系统</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>仅对个人空间中已启用的 skill 生效</span >仅对个人空间中已启用的 skill 生效</span
> >
</div> </div>
<label class="settings-toggle-row inner" <label class="settings-toggle-row inner"
@ -1158,7 +1202,7 @@
<div class="settings-group-title"> <div class="settings-group-title">
<span class="settings-row-title">可用 Skills</span <span class="settings-row-title">可用 Skills</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>勾选后会注入 system prompt并同步到工作区的 skills/ 目录</span >勾选后会注入 system prompt并同步到工作区的 skills/ 目录</span
> >
</div> </div>
<div class="settings-check-grid"> <div class="settings-check-grid">
@ -1186,7 +1230,7 @@
<div class="settings-group-title"> <div class="settings-group-title">
<span class="settings-row-title">默认禁用工具类别</span <span class="settings-row-title">默认禁用工具类别</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>选择后这些类别在新任务中保持关闭</span >选择后这些类别在新任务中保持关闭</span
> >
</div> </div>
<div class="settings-check-grid"> <div class="settings-check-grid">
@ -1222,7 +1266,7 @@
<span class="settings-row-copy" <span class="settings-row-copy"
><span class="settings-row-title">图片压缩</span ><span class="settings-row-title">图片压缩</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>发送图片或调用 view_image 时自动等比缩放</span >发送图片或调用 view_image 时自动等比缩放</span
></span ></span
> >
<div <div
@ -1271,7 +1315,7 @@
<p class="usage-summary-eyebrow">对话用量</p> <p class="usage-summary-eyebrow">对话用量</p>
<h3>用量统计</h3> <h3>用量统计</h3>
<p class="usage-summary-desc"> <p class="usage-summary-desc">
累计统计所有对话的输入/输出 Token对话数量用户消息与工具调用 累计统计所有对话的输入/输出 Token对话数量用户消息与工具调用
</p> </p>
</div> </div>
</div> </div>
@ -1334,7 +1378,7 @@
<span class="settings-row-copy" <span class="settings-row-copy"
><span class="settings-row-title">MCP 服务器管理</span ><span class="settings-row-title">MCP 服务器管理</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>统一配置 MCP Server集中管理工具扩展能力</span >统一配置 MCP Server集中管理工具扩展能力</span
></span ></span
><button ><button
type="button" type="button"
@ -1348,7 +1392,7 @@
<span class="settings-row-copy" <span class="settings-row-copy"
><span class="settings-row-title">管理员监控入口</span ><span class="settings-row-title">管理员监控入口</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>展示配额趋势容器运行项目存储与上传安全</span >展示配额趋势容器运行项目存储与上传安全</span
></span ></span
><button ><button
type="button" type="button"
@ -1361,7 +1405,7 @@
<div class="settings-action-row"> <div class="settings-action-row">
<span class="settings-row-copy" <span class="settings-row-copy"
><span class="settings-row-title">API 管理</span ><span class="settings-row-title">API 管理</span
><span class="settings-row-desc">管理 API 调用与账户</span></span ><span class="settings-row-desc">管理 API 调用与账户</span></span
><button ><button
type="button" type="button"
class="settings-secondary-button" class="settings-secondary-button"
@ -1374,7 +1418,7 @@
<span class="settings-row-copy" <span class="settings-row-copy"
><span class="settings-row-title">自定义工具管理</span ><span class="settings-row-title">自定义工具管理</span
><span class="settings-row-desc" ><span class="settings-row-desc"
>在线创建/编辑自定义工具文件</span >在线创建/编辑自定义工具文件</span
></span ></span
><button ><button
type="button" type="button"
@ -1615,14 +1659,14 @@ const permissionModeOptions: Array<{
label: string; label: string;
desc: string; desc: string;
}> = [ }> = [
{ id: 'readonly', label: '只读', desc: '仅允许读取/检索类工具,修改操作将被拒绝' }, { id: 'readonly', label: '只读', desc: '仅允许读取/检索类工具,修改操作将被拒绝' },
{ id: 'approval', label: '批准', desc: '对工作区文件进行修改的工具需人工批准后执行' }, { id: 'approval', label: '批准', desc: '对工作区文件进行修改的工具需人工批准后执行' },
{ {
id: 'auto_approval', id: 'auto_approval',
label: '自动审核', label: '自动审核',
desc: '工作区内写入直通;高风险操作由后台审核智能体自动审批' desc: '工作区内写入直通;高风险操作由后台审核智能体自动审批'
}, },
{ id: 'unrestricted', label: '无限制', desc: '工具按常规流程直接执行' } { id: 'unrestricted', label: '无限制', desc: '工具按常规流程直接执行' }
]; ];
const policyStore = usePolicyStore(); const policyStore = usePolicyStore();
@ -2143,7 +2187,7 @@ const compactMessageDisplayOptions = [
{ {
id: 'brief', id: 'brief',
label: '简略信息', label: '简略信息',
desc: '仅以一行横线概要替代(如「目标审核完成」)', desc: '用一行横线概要替代系统消息',
value: 'brief' as const value: 'brief' as const
} }
]; ];

View File

@ -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,

View File

@ -51,6 +51,75 @@ class ServerRefactorSmokeTest(unittest.TestCase):
self.assertTrue(callable(conversation.compute_workspace_storage)) self.assertTrue(callable(conversation.compute_workspace_storage))
self.assertTrue(callable(conversation.collect_upload_events)) self.assertTrue(callable(conversation.collect_upload_events))
def test_custom_tools_refresh_disabled_after_dynamic_load(self):
from core.main_terminal_parts.tools_definition import MainTerminalToolsDefinitionMixin
class Registry:
def reload(self):
return [
{
"id": "demo_tool",
"description": "Demo tool",
"parameters": {"type": "object", "properties": {}},
}
]
class Terminal(MainTerminalToolsDefinitionMixin):
custom_tools_enabled = True
user_role = "admin"
custom_tool_registry = Registry()
def __init__(self):
self.tool_categories_map = {
"custom": SimpleNamespace(
tools=[],
default_enabled=True,
silent_when_disabled=False,
)
}
self.tool_category_states = {"custom": False}
self.admin_forced_category_states = {}
self.disabled_tools = set()
def _refresh_disabled_tools(self):
disabled = set()
for key, category in self.tool_categories_map.items():
if not self.tool_category_states.get(key, category.default_enabled):
disabled.update(category.tools)
self.disabled_tools = disabled
terminal = Terminal()
custom_tools = terminal._build_custom_tools()
self.assertEqual(custom_tools[0]["function"]["name"], "demo_tool")
self.assertEqual(terminal.tool_categories_map["custom"].tools, ["demo_tool"])
self.assertIn("demo_tool", terminal.disabled_tools)
def test_context_applies_workspace_personalization_preferences(self):
import server.context as context
calls = []
class Terminal:
run_mode = "fast"
thinking_mode = False
model_key = None
def apply_personalization_preferences(self, config):
calls.append(config)
workspace = SimpleNamespace(data_dir="/tmp/workspace-data")
original_loader = context.load_personalization_config
try:
context.load_personalization_config = lambda data_dir: {
"disabled_tool_categories": ["custom"]
}
context._apply_workspace_personalization_preferences(Terminal(), workspace)
finally:
context.load_personalization_config = original_loader
self.assertEqual(calls, [{"disabled_tool_categories": ["custom"]}])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()