merge: feat/deep-compression-inplace
This commit is contained in:
commit
72c2b2af2a
3
.gitignore
vendored
3
.gitignore
vendored
@ -42,3 +42,6 @@ doc/
|
||||
# Host workspace config (local, use .example)
|
||||
config/host_workspaces.json
|
||||
config/custom_models.json
|
||||
|
||||
# Claude Code local artifacts (settings, worktrees)
|
||||
.claude/
|
||||
|
||||
@ -5,6 +5,8 @@
|
||||
|
||||
这份文档基于当前仓库实际代码重写。若与未来代码冲突,以代码为准并及时更新本文档。
|
||||
|
||||
> 和用户交流时默认使用中文。
|
||||
|
||||
## 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.
|
||||
|
||||
> 和用户交流时默认使用中文。
|
||||
|
||||
## Project Overview
|
||||
|
||||
多用户 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
|
||||
|
||||
### Entry Points
|
||||
- **CLI模式**: `python main.py` - 启动命令行交互式 Agent
|
||||
- **Web模式**: `python web_server.py` - 启动 Flask Web 服务器(主链路为 REST 任务轮询,默认端口 8091)
|
||||
- **统一入口**: `python main.py` - 当前实现默认进入 Web 启动流程(thinking_mode=True)
|
||||
- **推荐 Web 入口**: `python -m server.app` - 封装并转发到 `server/app_legacy.py`,主链路为 REST 任务轮询,默认端口 8091
|
||||
- **兼容入口**: `python web_server.py` - 已标记 deprecated,但仍可启动
|
||||
|
||||
### 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/**
|
||||
- `MainTerminal` - CLI 主终端,处理用户输入和 AI 对话循环
|
||||
- `WebTerminal` - Web 终端,继承 MainTerminal,支持轮询任务与实时通道兼容能力
|
||||
- `MainTerminal` (`main_terminal.py`) - CLI 主终端,处理用户输入和 AI 对话循环;逻辑拆分到 `core/main_terminal_parts/*`(commands / context / tools / tools_execution / tools_policy / tools_read 等)
|
||||
- `WebTerminal` (`web_terminal.py`) - Web 终端,继承 MainTerminal,支持轮询任务与实时通道兼容能力
|
||||
- `tool_config.py` - 定义所有工具的配置和分类
|
||||
|
||||
**modules/**
|
||||
@ -37,7 +47,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
- `terminal_factory.py` - 终端类型工厂
|
||||
|
||||
**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/**
|
||||
- `terminal.Dockerfile` - 终端容器镜像构建配置
|
||||
@ -46,22 +61,62 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
### Build & Run
|
||||
```bash
|
||||
# 安装依赖(需要在项目根目录创建 requirements.txt)
|
||||
pip install flask flask-socketio flask-cors openai python-dotenv tiktoken lxml
|
||||
# 安装 Python 依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 构建终端镜像
|
||||
docker build -f docker/terminal.Dockerfile -t my-agent-shell:latest .
|
||||
|
||||
# CLI 模式
|
||||
# 统一入口(默认进入 Web 启动流程)
|
||||
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 --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
|
||||
# 查看容器状态
|
||||
docker ps -a | grep agent-term
|
||||
@ -139,9 +194,18 @@ tail -f logs/container_stats.log
|
||||
|
||||
### Adding New Tools
|
||||
|
||||
1. 在 `MainTerminal` 添加 `handle_<tool_name>` 方法
|
||||
2. 在 `core/tool_config.py` 的 `define_tools()` 中注册工具
|
||||
3. 如果需要在流式输出中处理,在 `web_server.py` 添加特殊逻辑
|
||||
1. 在 `core/main_terminal_parts/tools_execution.py`(及相关 `tools_*.py`)实现执行逻辑
|
||||
2. 在 `core/tool_config.py` / `core/main_terminal_parts/tools_definition.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
|
||||
|
||||
@ -192,16 +256,21 @@ terminal.delete_conversation(conversation_id)
|
||||
|
||||
```
|
||||
agents/
|
||||
├── core/ # 核心终端和工具配置
|
||||
├── server/ # Flask 业务主线(chat/task/goal/monitor/auth ...)
|
||||
├── core/ # 核心终端与工具编排(main_terminal_parts/*)
|
||||
├── modules/ # 独立功能模块
|
||||
├── utils/ # 辅助工具
|
||||
├── config/ # 配置文件
|
||||
├── static/ # Web 前端
|
||||
├── config/ # 配置拆分(api/limits/terminal/paths ...,由 __init__ 聚合)
|
||||
├── static/ # Vue 3 + TS Web 前端
|
||||
├── cli/ # React + Ink CLI 前端(重写中)
|
||||
├── docker/ # 容器镜像
|
||||
├── prompts/ # 系统提示词
|
||||
├── users/ # 用户工作区
|
||||
├── data/ # 全局数据
|
||||
└── logs/ # 日志文件
|
||||
├── android-webview-app/ # Android WebView 客户端工程
|
||||
├── test/ # 测试用例
|
||||
├── docs/ / doc/ # 设计与发布文档
|
||||
├── users/ # 用户工作区(运行态,gitignored)
|
||||
├── data/ # 全局数据(运行态,gitignored)
|
||||
└── logs/ # 日志文件(运行态,gitignored)
|
||||
```
|
||||
|
||||
## Development Tips
|
||||
@ -209,9 +278,32 @@ agents/
|
||||
- 修改系统提示词:编辑 `prompts/main_system.txt`
|
||||
- 调整工具限制:修改 `config/limits.py`
|
||||
- 容器镜像定制:编辑 `docker/terminal.Dockerfile` 并重新构建
|
||||
- 前端开发:`npm install && npm run dev` (在 `static/` 目录)
|
||||
- 前端开发:在项目根目录运行 `npm install && npm run dev`(build watch)
|
||||
- 查看详细执行流程:监控 `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 WebView 壳版本时,必须同步完成:
|
||||
|
||||
@ -445,8 +445,15 @@ class MainTerminalContextMixin:
|
||||
# 添加对话历史(保留完整结构,包括tool_calls和tool消息)
|
||||
conversation = context["conversation"]
|
||||
replaced_tool_count = 0
|
||||
deep_compacted_skipped = 0
|
||||
for idx, conv in enumerate(conversation):
|
||||
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":
|
||||
# Assistant消息可能包含工具调用
|
||||
message = {
|
||||
@ -523,6 +530,8 @@ class MainTerminalContextMixin:
|
||||
# 当前用户输入已经在conversation中了,不需要重复添加
|
||||
if shallow_replace_enabled:
|
||||
print(f"[ContextCompression] build_messages 替换tool占位符: {replaced_tool_count} 条")
|
||||
if deep_compacted_skipped:
|
||||
print(f"[ContextCompression] build_messages 跳过已深压缩消息: {deep_compacted_skipped} 条")
|
||||
return messages
|
||||
|
||||
def _load_agents_md_content(self) -> Optional[str]:
|
||||
|
||||
@ -162,6 +162,7 @@ class MainTerminalToolsDefinitionMixin:
|
||||
# 更新分类为空列表,避免旧缓存
|
||||
if "custom" in self.tool_categories_map:
|
||||
self.tool_categories_map["custom"].tools = []
|
||||
self._refresh_disabled_tools()
|
||||
return []
|
||||
|
||||
tools: List[Dict] = []
|
||||
@ -194,6 +195,7 @@ class MainTerminalToolsDefinitionMixin:
|
||||
# 覆盖 custom 分类的工具列表
|
||||
if "custom" in self.tool_categories_map:
|
||||
self.tool_categories_map["custom"].tools = tool_ids
|
||||
self._refresh_disabled_tools()
|
||||
|
||||
return tools
|
||||
|
||||
|
||||
@ -87,6 +87,8 @@ DEFAULT_PERSONALIZATION_CONFIG: Dict[str, Any] = {
|
||||
"shallow_compress_trigger_tool_calls_interval": None,
|
||||
"shallow_compress_keep_user_turn_tools": None, # 保留最近N次用户输入后的工具不压缩(默认3)
|
||||
"deep_compress_trigger_tokens": None,
|
||||
"deep_compress_form": "file", # 深压缩形式:file-生成文件 / inject-直接注入文件全文
|
||||
"deep_compress_behavior": "continue", # 手动压缩行为:continue-注入并触发请求 / wait-仅插入等待用户
|
||||
"silent_tool_disable": True, # 禁用工具时不向模型插入提示(默认开启)
|
||||
"enhanced_tool_display": True, # 增强工具显示
|
||||
"versioning_restore_mode": "overwrite", # 版本回溯模式固定为 overwrite
|
||||
@ -371,6 +373,20 @@ def sanitize_personalization_payload(
|
||||
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:
|
||||
base["shallow_compress_trigger_tool_calls_interval"] = _sanitize_optional_int(
|
||||
data.get("shallow_compress_trigger_tool_calls_interval"),
|
||||
|
||||
@ -63,6 +63,19 @@ def _set_terminal_workspace_label(terminal: WebTerminal, label: Optional[str]) -
|
||||
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:
|
||||
"""
|
||||
确保工作区 skills 已按当前个性化配置完成同步。
|
||||
@ -300,6 +313,7 @@ def get_user_resources(username: Optional[str] = None, workspace_id: Optional[st
|
||||
except Exception as exc:
|
||||
debug_log(f"[admin_policy][host_mode] 应用失败: {exc}")
|
||||
|
||||
_apply_workspace_personalization_preferences(terminal, workspace)
|
||||
_ensure_workspace_skills_synced(terminal, workspace)
|
||||
write_host_workspace_debug(
|
||||
"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:
|
||||
debug_log(f"[admin_policy] 应用失败: {exc}")
|
||||
|
||||
_apply_workspace_personalization_preferences(terminal, workspace)
|
||||
_ensure_workspace_skills_synced(terminal, workspace)
|
||||
return terminal, workspace
|
||||
|
||||
|
||||
@ -1147,7 +1147,7 @@ def restore_conversation_versioning_checkpoint(conversation_id, terminal: WebTer
|
||||
@api_login_required
|
||||
@with_terminal
|
||||
def compress_conversation(conversation_id, terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
||||
"""深层压缩指定对话,生成 compact 文件并切换到新对话。"""
|
||||
"""深层压缩指定对话(in-place):生成 compact 文件、标记历史前缀为已压缩,按设置决定是否续接。"""
|
||||
try:
|
||||
policy = resolve_admin_policy(get_current_user_record())
|
||||
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)
|
||||
return jsonify(result), status_code
|
||||
|
||||
new_conversation_id = result["compressed_conversation_id"]
|
||||
|
||||
load_result = terminal.load_conversation(new_conversation_id)
|
||||
|
||||
# in-place 压缩:对话 id 不变。通知前端当前对话内容已变化(历史前缀被标记),刷新展示。
|
||||
load_result = terminal.load_conversation(normalized_id)
|
||||
if load_result.get("success"):
|
||||
socketio.emit('conversation_list_update', {
|
||||
'action': 'compressed',
|
||||
'conversation_id': new_conversation_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)
|
||||
'conversation_id': normalized_id
|
||||
}, room=f"user_{username}")
|
||||
socketio.emit('conversation_loaded', {
|
||||
'conversation_id': new_conversation_id,
|
||||
'conversation_id': normalized_id,
|
||||
'clear_ui': True
|
||||
}, room=f"user_{username}")
|
||||
|
||||
compress_behavior = str(result.get("compress_behavior") or "continue").strip().lower()
|
||||
response_payload = {
|
||||
"success": True,
|
||||
"compressed_conversation_id": new_conversation_id,
|
||||
"in_place": True,
|
||||
"compressed_conversation_id": normalized_id,
|
||||
"compact_file": result.get("compact_file"),
|
||||
"summary_failed": result.get("summary_failed", False),
|
||||
"guide_message": result.get("guide_message"),
|
||||
"compress_form": result.get("compress_form"),
|
||||
"compress_behavior": compress_behavior,
|
||||
"load_result": load_result
|
||||
}
|
||||
|
||||
guide_message = (result.get("guide_message") or "").strip()
|
||||
if guide_message:
|
||||
if guide_message and compress_behavior == "continue":
|
||||
# 继续工作:创建续接任务,自动发送引导语并触发请求(同一对话)。
|
||||
try:
|
||||
from .tasks import task_manager
|
||||
workspace_id = session.get("workspace_id") or "default"
|
||||
@ -1205,7 +1203,7 @@ def compress_conversation(conversation_id, terminal: WebTerminal, workspace: Use
|
||||
workspace_id,
|
||||
guide_message,
|
||||
images=[],
|
||||
conversation_id=new_conversation_id,
|
||||
conversation_id=normalized_id,
|
||||
videos=[],
|
||||
message_source="compression_handoff",
|
||||
)
|
||||
@ -1217,6 +1215,21 @@ def compress_conversation(conversation_id, terminal: WebTerminal, workspace: Use
|
||||
debug_log(f"[Compression] 自动发送引导消息失败: {exc}")
|
||||
response_payload["auto_task_started"] = False
|
||||
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:
|
||||
response_payload["auto_task_started"] = False
|
||||
|
||||
|
||||
@ -6,8 +6,6 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from modules.versioning_manager import ConversationVersioningManager
|
||||
|
||||
|
||||
SUMMARY_PROMPT = (
|
||||
"由于当前对话过长,系统正在自动压缩。请你基于已有上下文输出一份可继续执行的工作总结,要求:\n"
|
||||
@ -18,7 +16,9 @@ SUMMARY_PROMPT = (
|
||||
"5) 工具调用中的重要结果/错误与修复\n"
|
||||
"6) 当前未完成事项与下一步计划(可直接执行)\n"
|
||||
"7) 风险与注意事项\n"
|
||||
"请使用中文,结构清晰,尽量具体,不要省略关键上下文。"
|
||||
"请使用中文,结构清晰,尽量具体,不要省略关键上下文。\n"
|
||||
"不要考虑过往对话,只考虑当前对话任务。\n"
|
||||
"禁止调用任何工具,必须直接输出总结内容。"
|
||||
)
|
||||
|
||||
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:
|
||||
"""生成文件模式的引导语:仅提示压缩文件位置,由模型自行阅读。"""
|
||||
base = GUIDE_USER_MESSAGE_TEMPLATE.format(count=compression_index, path=compact_file)
|
||||
if not previous_records:
|
||||
return base
|
||||
@ -81,6 +82,41 @@ def _build_guide_message(*, compression_index: int, compact_file: str, previous_
|
||||
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:
|
||||
if content is None:
|
||||
return ""
|
||||
@ -162,10 +198,14 @@ async def _generate_summary(web_terminal, prompt: str, retries: int = 5) -> Tupl
|
||||
# build_messages 不会自动附加 user_input,压缩总结必须显式注入
|
||||
if prompt and isinstance(prompt, str):
|
||||
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)):
|
||||
try:
|
||||
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):
|
||||
continue
|
||||
choices = chunk.get("choices") or []
|
||||
@ -236,6 +276,33 @@ def _write_compact_file(
|
||||
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(
|
||||
*,
|
||||
web_terminal,
|
||||
@ -253,12 +320,33 @@ async def run_deep_compression(
|
||||
if metadata.get("compression_in_progress"):
|
||||
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 {}
|
||||
source_versioning_enabled = bool(source_versioning.get("enabled"))
|
||||
source_tracking_mode = ConversationVersioningManager.normalize_tracking_mode(
|
||||
source_versioning.get("tracking_mode")
|
||||
)
|
||||
# 读取个性化压缩设置:
|
||||
# - compress_form: file(生成文件,引导语提示位置) / inject(把历次压缩文件全文注入引导语)
|
||||
# - compress_behavior: continue(注入引导语并触发请求) / wait(仅注入引导语,等待用户)
|
||||
# compress_behavior 仅作用于手动压缩;自动深压缩永远继续工作。
|
||||
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)
|
||||
previous_records = _normalize_deep_compression_records(metadata)
|
||||
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(
|
||||
in_progress=True,
|
||||
mode=mode,
|
||||
stage="creating_new_conversation",
|
||||
stage="marking_history",
|
||||
job_id=job_id,
|
||||
)
|
||||
|
||||
new_conv_id = cm.conversation_manager.create_conversation(
|
||||
project_path=str(workspace.project_path),
|
||||
thinking_mode=bool(metadata.get("thinking_mode", False)),
|
||||
run_mode=metadata.get("run_mode") or ("thinking" if metadata.get("thinking_mode") else "fast"),
|
||||
initial_messages=[],
|
||||
model_key=metadata.get("model_key"),
|
||||
has_images=False,
|
||||
has_videos=False,
|
||||
metadata_overrides={
|
||||
"compression_count": target_count,
|
||||
"title_locked": True,
|
||||
"skip_auto_title_generation": True,
|
||||
}
|
||||
# === in-place 压缩:不创建/切换新对话,只把当前对话历史前缀打上 deep_compacted 标记 ===
|
||||
# 当前对话已在函数开头对齐为目标对话,这里直接对内存历史打标。
|
||||
now_iso = datetime.now().isoformat()
|
||||
marked_count = _mark_history_compacted(
|
||||
cm.conversation_history or [],
|
||||
round_index=target_count,
|
||||
now=now_iso,
|
||||
)
|
||||
# 标记后立即持久化历史(标记写在每条消息的 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 = {
|
||||
"count": target_count,
|
||||
"compact_file": relative_compact_path,
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"created_at": now_iso,
|
||||
"source_conversation_id": conversation_id,
|
||||
"compressed_conversation_id": new_conv_id,
|
||||
"compressed_conversation_id": conversation_id,
|
||||
}
|
||||
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:
|
||||
try:
|
||||
vm = ConversationVersioningManager(
|
||||
project_path=workspace.project_path,
|
||||
data_dir=workspace.data_dir,
|
||||
conversation_id=new_conv_id,
|
||||
)
|
||||
vm_meta = vm.set_enabled(
|
||||
enabled=True,
|
||||
mode="overwrite",
|
||||
tracking_mode=source_tracking_mode,
|
||||
)
|
||||
new_conv_data = cm.conversation_manager.load_conversation(new_conv_id) or {}
|
||||
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
|
||||
# 构建引导语(按压缩形式)。inject 模式读取历次压缩文件全文,文件仍会生成,只是不提及位置。
|
||||
if compress_form == "inject":
|
||||
guide_message = _build_inject_guide_message(
|
||||
project_path=Path(workspace.project_path),
|
||||
compression_index=target_count,
|
||||
current_record=current_record,
|
||||
previous_records=previous_records,
|
||||
)
|
||||
else:
|
||||
guide_message = _build_guide_message(
|
||||
compression_index=target_count,
|
||||
compact_file=relative_compact_path,
|
||||
previous_records=previous_records,
|
||||
)
|
||||
|
||||
cm.conversation_manager.update_conversation_title(new_conv_id, f"{old_title}对话 压缩后")
|
||||
web_terminal.load_conversation(new_conv_id)
|
||||
guide_message = _build_guide_message(
|
||||
compression_index=target_count,
|
||||
compact_file=relative_compact_path,
|
||||
previous_records=previous_records,
|
||||
)
|
||||
# 更新对话 metadata:压缩记录 + 清理压缩状态标记(同一对话,无切换)。
|
||||
meta_updates = {
|
||||
"compression_count": target_count,
|
||||
"deep_compression_records": all_records,
|
||||
"last_deep_compression_record": current_record,
|
||||
"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", {
|
||||
"source_conversation_id": conversation_id,
|
||||
"conversation_id": new_conv_id,
|
||||
"conversation_id": conversation_id,
|
||||
"in_progress": False,
|
||||
"compact_file": relative_compact_path,
|
||||
"marked_count": marked_count,
|
||||
"compress_form": compress_form,
|
||||
"compress_behavior": compress_behavior,
|
||||
"job_id": job_id,
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"compressed_conversation_id": new_conv_id,
|
||||
"in_place": True,
|
||||
"compressed_conversation_id": conversation_id,
|
||||
"compact_file": relative_compact_path,
|
||||
"marked_count": marked_count,
|
||||
"compress_form": compress_form,
|
||||
"compress_behavior": compress_behavior,
|
||||
"summary_failed": bool(summary_fail_reason),
|
||||
"guide_message": guide_message,
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@ -1057,6 +1057,7 @@ export const messageMethods = {
|
||||
|
||||
if (response.ok && result.success) {
|
||||
this.compressionStage = 'switching';
|
||||
// in-place 压缩:对话 id 不变,重新加载以刷新“已压缩历史”展示。
|
||||
const newId = result.compressed_conversation_id;
|
||||
if (newId) {
|
||||
await this.loadConversation(newId, { force: true });
|
||||
@ -1064,6 +1065,7 @@ export const messageMethods = {
|
||||
const guideMessage = (result.guide_message || '').trim();
|
||||
const autoTaskStarted = !!result.auto_task_started;
|
||||
const autoTaskId = result.auto_task_id;
|
||||
const compressBehavior = String(result.compress_behavior || 'continue');
|
||||
if (autoTaskStarted && autoTaskId) {
|
||||
const { useTaskStore } = await import('../../stores/task');
|
||||
const taskStore = useTaskStore();
|
||||
@ -1079,6 +1081,11 @@ export const messageMethods = {
|
||||
if (typeof this.monitorShowPendingReply === 'function') {
|
||||
this.monitorShowPendingReply();
|
||||
}
|
||||
} else if (compressBehavior === 'wait') {
|
||||
// 等待用户:后端已把引导语作为 user 消息追加进历史,这里仅刷新展示,不触发请求。
|
||||
if (newId) {
|
||||
await this.loadConversation(newId, { force: true });
|
||||
}
|
||||
} else if (guideMessage) {
|
||||
await this.sendAutoUserMessage(guideMessage);
|
||||
}
|
||||
@ -1089,7 +1096,7 @@ export const messageMethods = {
|
||||
debugLog('对话压缩完成:', result);
|
||||
this.uiPushToast({
|
||||
title: '压缩完成',
|
||||
message: '已生成压缩后的新对话',
|
||||
message: '已压缩较早的对话内容',
|
||||
type: 'success',
|
||||
duration: 2200
|
||||
});
|
||||
|
||||
@ -2427,8 +2427,10 @@ export const taskPollingMethods = {
|
||||
this.compressionMode = '';
|
||||
this.compressionStage = '';
|
||||
this.compressionError = '';
|
||||
// in-place 压缩:对话 id 不变。重新加载该对话以刷新“已压缩历史”的展示;
|
||||
// 若返回的 id 与当前不一致(兼容旧行为)也按其加载。
|
||||
const newId = data?.conversation_id;
|
||||
if (newId && newId !== this.currentConversationId) {
|
||||
if (newId) {
|
||||
await this.loadConversation(newId, { force: true });
|
||||
}
|
||||
this.conversationsOffset = 0;
|
||||
@ -2436,13 +2438,11 @@ export const taskPollingMethods = {
|
||||
await this.loadConversationsList();
|
||||
}
|
||||
await this.refreshRunningWorkspaceTasks?.();
|
||||
// 压缩会把同一个后台任务从旧 conversation 迁移到新 conversation。
|
||||
// 切换对话时本地轮询会被清理,这里必须立即按新会话恢复轮询,
|
||||
// 否则后续进度需要手动刷新页面才会继续显示。
|
||||
// 续接任务仍在同一对话内运行,恢复轮询以持续显示进度。
|
||||
await this.restoreTaskState?.();
|
||||
this.uiPushToast({
|
||||
title: '压缩完成',
|
||||
message: '已自动切换到压缩后的新对话',
|
||||
message: '已压缩较早的对话内容',
|
||||
type: 'success',
|
||||
duration: 2400
|
||||
});
|
||||
|
||||
@ -8,8 +8,10 @@
|
||||
:class="{
|
||||
'goal-mode-banner--running': 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"
|
||||
>
|
||||
<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');
|
||||
|
||||
// 当消息队列非空或 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 root = inputAreaRoot.value;
|
||||
const shell = compactInputShell.value;
|
||||
@ -1049,7 +1063,9 @@ const collectComposerVisualHeight = () => {
|
||||
const shellRect = shell.getBoundingClientRect();
|
||||
let top = shellRect.top;
|
||||
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) => {
|
||||
if (!(node instanceof HTMLElement)) return;
|
||||
const rect = node.getBoundingClientRect();
|
||||
@ -1258,6 +1274,13 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
// skill 菜单开关会切换目标横幅收起/展开态,需重新上报高度,
|
||||
// 否则展开回来时聊天区预留高度不会同步恢复。
|
||||
watch(goalBannerCollapsed, async () => {
|
||||
await nextTick();
|
||||
emitComposerHeight();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (hasSkillMarkdownLink.value) {
|
||||
renderRichEditor();
|
||||
@ -1321,6 +1344,30 @@ onBeforeUnmount(() => {
|
||||
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));
|
||||
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 {
|
||||
color: var(--claude-accent, #da7756);
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<div class="exp-header">
|
||||
<div>
|
||||
<div class="title">第一次使用?选择适合您的模式</div>
|
||||
<div class="subtitle">速度、联网与工具范围各不相同,随时可在个人空间切换。</div>
|
||||
<div class="subtitle">速度、联网与工具范围各不相同,随时可在个人空间切换</div>
|
||||
</div>
|
||||
<button class="confirm-btn" type="button" @click="confirmSelection">确认并开始</button>
|
||||
</div>
|
||||
@ -57,28 +57,28 @@ const modes = [
|
||||
id: 'fast',
|
||||
badge: '极速',
|
||||
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>`
|
||||
},
|
||||
{
|
||||
id: 'thinking',
|
||||
badge: '思考',
|
||||
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>`
|
||||
},
|
||||
{
|
||||
id: 'research',
|
||||
badge: '研究',
|
||||
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>`
|
||||
},
|
||||
{
|
||||
id: 'expert',
|
||||
badge: '专家',
|
||||
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>`
|
||||
}
|
||||
] as const;
|
||||
|
||||
@ -90,7 +90,7 @@
|
||||
<span class="settings-row-copy">
|
||||
<span class="settings-row-title">自动生成对话标题</span>
|
||||
<span class="settings-row-desc"
|
||||
>默认开启;关闭后标题将沿用首条消息。</span
|
||||
>默认开启;关闭后标题将沿用首条消息</span
|
||||
>
|
||||
</span>
|
||||
<input
|
||||
@ -116,7 +116,7 @@
|
||||
<span class="settings-row-copy">
|
||||
<span class="settings-row-title">新手教程</span>
|
||||
<span class="settings-row-desc"
|
||||
>通过高亮与说明窗了解主要功能入口和常用设置。</span
|
||||
>通过高亮与说明窗了解主要功能入口和常用设置</span
|
||||
>
|
||||
</span>
|
||||
<button
|
||||
@ -160,7 +160,7 @@
|
||||
<div class="settings-action-row danger-zone">
|
||||
<span class="settings-row-copy">
|
||||
<span class="settings-row-title">退出登录</span>
|
||||
<span class="settings-row-desc">结束当前账号会话。</span>
|
||||
<span class="settings-row-desc">结束当前账号会话</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
@ -181,7 +181,7 @@
|
||||
<label class="settings-toggle-row">
|
||||
<span class="settings-row-copy">
|
||||
<span class="settings-row-title">启用个性化提示</span>
|
||||
<span class="settings-row-desc">开启后才会注入您的偏好。</span>
|
||||
<span class="settings-row-desc">开启后才会注入您的偏好</span>
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -278,7 +278,7 @@
|
||||
<span class="settings-row-copy">
|
||||
<span class="settings-row-title">回答时必须考虑的信息</span>
|
||||
<span class="settings-row-desc"
|
||||
>最多 {{ maxConsiderations }} 条,可拖动排序。</span
|
||||
>最多 {{ maxConsiderations }} 条,可拖动排序</span
|
||||
>
|
||||
</span>
|
||||
<div class="settings-input-stack wide">
|
||||
@ -326,7 +326,7 @@
|
||||
<div class="settings-select-row">
|
||||
<span class="settings-row-copy">
|
||||
<span class="settings-row-title">智能体交流风格</span>
|
||||
<span class="settings-row-desc">智能体和您交流时使用何种形式。</span>
|
||||
<span class="settings-row-desc">智能体和您交流时使用何种形式</span>
|
||||
</span>
|
||||
<div
|
||||
class="settings-select-wrap"
|
||||
@ -351,7 +351,7 @@
|
||||
:class="{ selected: form.communication_style === '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>
|
||||
</button>
|
||||
<button
|
||||
@ -360,7 +360,7 @@
|
||||
:class="{ selected: form.communication_style === '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>
|
||||
</button>
|
||||
</div>
|
||||
@ -378,7 +378,7 @@
|
||||
<span class="settings-row-copy"
|
||||
><span class="settings-row-title">默认模型</span
|
||||
><span class="settings-row-desc"
|
||||
>为新对话/登录后首次请求选择首选模型。</span
|
||||
>为新对话/登录后首次请求选择首选模型</span
|
||||
></span
|
||||
>
|
||||
<div
|
||||
@ -421,7 +421,7 @@
|
||||
<span class="settings-row-copy"
|
||||
><span class="settings-row-title">默认运行模式</span
|
||||
><span class="settings-row-desc"
|
||||
>登录或新建任务时的初始运行模式。</span
|
||||
>登录或新建任务时的初始运行模式</span
|
||||
></span
|
||||
>
|
||||
<div
|
||||
@ -460,7 +460,7 @@
|
||||
<span class="settings-row-copy"
|
||||
><span class="settings-row-title">思考频率</span
|
||||
><span class="settings-row-desc"
|
||||
>连续快速模式运行多少轮后,系统强制切换到思考模型。</span
|
||||
>连续快速模式运行多少轮后,系统强制切换到思考模型</span
|
||||
></span
|
||||
>
|
||||
<div class="settings-input-stack wide">
|
||||
@ -501,7 +501,7 @@
|
||||
<div class="settings-select-row">
|
||||
<span class="settings-row-copy"
|
||||
><span class="settings-row-title">主题切换</span
|
||||
><span class="settings-row-desc">在经典、明亮和夜间之间切换。</span></span
|
||||
><span class="settings-row-desc">在经典、明亮和夜间之间切换</span></span
|
||||
>
|
||||
<div
|
||||
class="settings-select-wrap"
|
||||
@ -538,7 +538,7 @@
|
||||
><span class="settings-row-copy"
|
||||
><span class="settings-row-title">使用自定义称呼</span
|
||||
><span class="settings-row-desc"
|
||||
>对话区域使用个性化设置中的自称和称呼。</span
|
||||
>对话区域使用个性化设置中的自称和称呼</span
|
||||
></span
|
||||
><input
|
||||
type="checkbox"
|
||||
@ -560,7 +560,7 @@
|
||||
><span class="settings-row-copy"
|
||||
><span class="settings-row-title">增强工具显示</span
|
||||
><span class="settings-row-desc"
|
||||
>工具块显示格式化内容,关闭则显示原始 JSON。</span
|
||||
>工具块显示格式化内容,关闭则显示原始 JSON</span
|
||||
></span
|
||||
><input
|
||||
type="checkbox"
|
||||
@ -581,7 +581,7 @@
|
||||
<div class="settings-select-row">
|
||||
<span class="settings-row-copy"
|
||||
><span class="settings-row-title">堆叠块显示模式</span
|
||||
><span class="settings-row-desc">选择思考/工具块的显示方式。</span></span
|
||||
><span class="settings-row-desc">选择思考/工具块的显示方式</span></span
|
||||
>
|
||||
<div
|
||||
class="settings-select-wrap"
|
||||
@ -619,7 +619,7 @@
|
||||
<span class="settings-row-copy"
|
||||
><span class="settings-row-title">简略消息显示</span
|
||||
><span class="settings-row-desc"
|
||||
>审核、子智能体等紧凑系统消息的显示形式。</span
|
||||
>审核、子智能体等紧凑系统消息的显示形式</span
|
||||
></span
|
||||
>
|
||||
<div
|
||||
@ -665,7 +665,7 @@
|
||||
<div class="settings-select-row">
|
||||
<span class="settings-row-copy"
|
||||
><span class="settings-row-title">默认权限</span
|
||||
><span class="settings-row-desc">新建对话时的默认权限模式。</span></span
|
||||
><span class="settings-row-desc">新建对话时的默认权限模式</span></span
|
||||
>
|
||||
<div
|
||||
class="settings-select-wrap"
|
||||
@ -703,7 +703,7 @@
|
||||
><span class="settings-row-copy"
|
||||
><span class="settings-row-title">允许根目录创建文件</span
|
||||
><span class="settings-row-desc"
|
||||
>关闭则禁止在工作区根目录创建文件。</span
|
||||
>关闭则禁止在工作区根目录创建文件</span
|
||||
></span
|
||||
><input
|
||||
type="checkbox"
|
||||
@ -725,7 +725,7 @@
|
||||
><span class="settings-row-copy"
|
||||
><span class="settings-row-title">AGENTS.md 自动注入</span
|
||||
><span class="settings-row-desc"
|
||||
>工作区根目录存在 AGENTS.md 时自动注入系统提示词。</span
|
||||
>工作区根目录存在 AGENTS.md 时自动注入系统提示词</span
|
||||
></span
|
||||
><input
|
||||
type="checkbox"
|
||||
@ -752,7 +752,7 @@
|
||||
><span class="settings-row-copy"
|
||||
><span class="settings-row-title">审核智能体主动取证</span
|
||||
><span class="settings-row-desc"
|
||||
>开启后审核智能体可运行只读命令(查看文件、跑测试)来核实目标是否达成;关闭则仅依据对话内容判断。</span
|
||||
>开启后可运行只读命令核实目标,关闭则仅依据对话判断</span
|
||||
></span
|
||||
><input
|
||||
type="checkbox"
|
||||
@ -775,7 +775,7 @@
|
||||
<span class="settings-row-copy"
|
||||
><span class="settings-row-title">最大自动续命轮数</span
|
||||
><span class="settings-row-desc"
|
||||
>目标模式下,主模型停下后最多自动继续的轮数(1-100)。</span
|
||||
>主模型停下后最多自动继续 1-100 轮</span
|
||||
></span
|
||||
>
|
||||
<input
|
||||
@ -797,7 +797,7 @@
|
||||
><span class="settings-row-copy"
|
||||
><span class="settings-row-title">启用累计 token 上限</span
|
||||
><span class="settings-row-desc"
|
||||
>开启后,目标模式累计消耗(输入+输出)超过下方上限即停止。</span
|
||||
>累计输入输出超过上限时停止目标模式</span
|
||||
></span
|
||||
><input
|
||||
type="checkbox"
|
||||
@ -816,7 +816,7 @@
|
||||
<div v-if="goalTokenLimitEnabled" class="settings-select-row">
|
||||
<span class="settings-row-copy"
|
||||
><span class="settings-row-title">累计 token 上限</span
|
||||
><span class="settings-row-desc">达到该累计消耗即停止目标模式。</span></span
|
||||
><span class="settings-row-desc">达到该累计消耗即停止目标模式</span></span
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
@ -843,7 +843,7 @@
|
||||
><span class="settings-row-copy"
|
||||
><span class="settings-row-title">最近对话提示</span
|
||||
><span class="settings-row-desc"
|
||||
>把当前工作区最近若干个非空对话注入系统提示词。</span
|
||||
>把当前工作区最近若干个非空对话注入系统提示词</span
|
||||
></span
|
||||
><input
|
||||
type="checkbox"
|
||||
@ -883,7 +883,7 @@
|
||||
<div class="settings-group-title">
|
||||
<span class="settings-row-title">上下文压缩策略</span
|
||||
><span class="settings-row-desc"
|
||||
>可分别控制自动浅层压缩与自动深层压缩。</span
|
||||
>可分别控制自动浅层压缩与自动深层压缩</span
|
||||
>
|
||||
</div>
|
||||
<label class="settings-toggle-row inner"
|
||||
@ -995,6 +995,50 @@
|
||||
"
|
||||
/></label>
|
||||
</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">
|
||||
<button
|
||||
type="button"
|
||||
@ -1012,7 +1056,7 @@
|
||||
><span class="settings-row-copy"
|
||||
><span class="settings-row-title">静默禁用工具</span
|
||||
><span class="settings-row-desc"
|
||||
>禁用工具时不再注入提示消息,模型不会感知被禁用项。</span
|
||||
>禁用工具时不再注入提示消息,模型不会感知被禁用项</span
|
||||
></span
|
||||
><input
|
||||
type="checkbox"
|
||||
@ -1034,7 +1078,7 @@
|
||||
><span class="settings-row-copy"
|
||||
><span class="settings-row-title">工具意图提示</span
|
||||
><span class="settings-row-desc"
|
||||
>调用工具时先用简短文字告诉你要做什么。</span
|
||||
>调用工具时先用简短文字告诉你要做什么</span
|
||||
></span
|
||||
><input
|
||||
type="checkbox"
|
||||
@ -1056,7 +1100,7 @@
|
||||
><span class="settings-row-copy"
|
||||
><span class="settings-row-title">Skill 提示系统</span
|
||||
><span class="settings-row-desc"
|
||||
>检测到特定关键词时自动提示模型阅读相关 skill。</span
|
||||
>检测到特定关键词时自动提示模型阅读相关 skill</span
|
||||
></span
|
||||
><input
|
||||
type="checkbox"
|
||||
@ -1078,7 +1122,7 @@
|
||||
<div class="settings-group-title">
|
||||
<span class="settings-row-title">强约束系统</span
|
||||
><span class="settings-row-desc"
|
||||
>仅对个人空间中已启用的 skill 生效。</span
|
||||
>仅对个人空间中已启用的 skill 生效</span
|
||||
>
|
||||
</div>
|
||||
<label class="settings-toggle-row inner"
|
||||
@ -1158,7 +1202,7 @@
|
||||
<div class="settings-group-title">
|
||||
<span class="settings-row-title">可用 Skills</span
|
||||
><span class="settings-row-desc"
|
||||
>勾选后会注入 system prompt,并同步到工作区的 skills/ 目录。</span
|
||||
>勾选后会注入 system prompt,并同步到工作区的 skills/ 目录</span
|
||||
>
|
||||
</div>
|
||||
<div class="settings-check-grid">
|
||||
@ -1186,7 +1230,7 @@
|
||||
<div class="settings-group-title">
|
||||
<span class="settings-row-title">默认禁用工具类别</span
|
||||
><span class="settings-row-desc"
|
||||
>选择后,这些类别在新任务中保持关闭。</span
|
||||
>选择后,这些类别在新任务中保持关闭</span
|
||||
>
|
||||
</div>
|
||||
<div class="settings-check-grid">
|
||||
@ -1222,7 +1266,7 @@
|
||||
<span class="settings-row-copy"
|
||||
><span class="settings-row-title">图片压缩</span
|
||||
><span class="settings-row-desc"
|
||||
>发送图片或调用 view_image 时自动等比缩放。</span
|
||||
>发送图片或调用 view_image 时自动等比缩放</span
|
||||
></span
|
||||
>
|
||||
<div
|
||||
@ -1271,7 +1315,7 @@
|
||||
<p class="usage-summary-eyebrow">对话用量</p>
|
||||
<h3>用量统计</h3>
|
||||
<p class="usage-summary-desc">
|
||||
累计统计所有对话的输入/输出 Token、对话数量、用户消息与工具调用。
|
||||
累计统计所有对话的输入/输出 Token、对话数量、用户消息与工具调用
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -1334,7 +1378,7 @@
|
||||
<span class="settings-row-copy"
|
||||
><span class="settings-row-title">MCP 服务器管理</span
|
||||
><span class="settings-row-desc"
|
||||
>统一配置 MCP Server,集中管理工具扩展能力。</span
|
||||
>统一配置 MCP Server,集中管理工具扩展能力</span
|
||||
></span
|
||||
><button
|
||||
type="button"
|
||||
@ -1348,7 +1392,7 @@
|
||||
<span class="settings-row-copy"
|
||||
><span class="settings-row-title">管理员监控入口</span
|
||||
><span class="settings-row-desc"
|
||||
>展示配额趋势、容器运行、项目存储与上传安全。</span
|
||||
>展示配额趋势、容器运行、项目存储与上传安全</span
|
||||
></span
|
||||
><button
|
||||
type="button"
|
||||
@ -1361,7 +1405,7 @@
|
||||
<div class="settings-action-row">
|
||||
<span class="settings-row-copy"
|
||||
><span class="settings-row-title">API 管理</span
|
||||
><span class="settings-row-desc">管理 API 调用与账户。</span></span
|
||||
><span class="settings-row-desc">管理 API 调用与账户</span></span
|
||||
><button
|
||||
type="button"
|
||||
class="settings-secondary-button"
|
||||
@ -1374,7 +1418,7 @@
|
||||
<span class="settings-row-copy"
|
||||
><span class="settings-row-title">自定义工具管理</span
|
||||
><span class="settings-row-desc"
|
||||
>在线创建/编辑自定义工具文件。</span
|
||||
>在线创建/编辑自定义工具文件</span
|
||||
></span
|
||||
><button
|
||||
type="button"
|
||||
@ -1615,14 +1659,14 @@ const permissionModeOptions: Array<{
|
||||
label: string;
|
||||
desc: string;
|
||||
}> = [
|
||||
{ id: 'readonly', label: '只读', desc: '仅允许读取/检索类工具,修改操作将被拒绝。' },
|
||||
{ id: 'approval', label: '批准', desc: '对工作区文件进行修改的工具需人工批准后执行。' },
|
||||
{ id: 'readonly', label: '只读', desc: '仅允许读取/检索类工具,修改操作将被拒绝' },
|
||||
{ id: 'approval', label: '批准', desc: '对工作区文件进行修改的工具需人工批准后执行' },
|
||||
{
|
||||
id: 'auto_approval',
|
||||
label: '自动审核',
|
||||
desc: '工作区内写入直通;高风险操作由后台审核智能体自动审批。'
|
||||
desc: '工作区内写入直通;高风险操作由后台审核智能体自动审批'
|
||||
},
|
||||
{ id: 'unrestricted', label: '无限制', desc: '工具按常规流程直接执行。' }
|
||||
{ id: 'unrestricted', label: '无限制', desc: '工具按常规流程直接执行' }
|
||||
];
|
||||
|
||||
const policyStore = usePolicyStore();
|
||||
@ -2143,7 +2187,7 @@ const compactMessageDisplayOptions = [
|
||||
{
|
||||
id: 'brief',
|
||||
label: '简略信息',
|
||||
desc: '仅以一行横线概要替代(如「目标审核完成」)',
|
||||
desc: '用一行横线概要替代系统消息',
|
||||
value: 'brief' as const
|
||||
}
|
||||
];
|
||||
|
||||
@ -43,6 +43,8 @@ interface PersonalForm {
|
||||
shallow_compress_max_replace_per_round: number | null;
|
||||
shallow_compress_trigger_tool_calls_interval: number | null;
|
||||
deep_compress_trigger_tokens: number | null;
|
||||
deep_compress_form: 'file' | 'inject';
|
||||
deep_compress_behavior: 'continue' | 'wait';
|
||||
agents_md_auto_inject: boolean;
|
||||
allow_root_file_creation: boolean;
|
||||
theme: 'classic' | 'light' | 'dark';
|
||||
@ -134,6 +136,8 @@ const defaultForm = (): PersonalForm => ({
|
||||
shallow_compress_max_replace_per_round: null,
|
||||
shallow_compress_trigger_tool_calls_interval: null,
|
||||
deep_compress_trigger_tokens: null,
|
||||
deep_compress_form: 'file',
|
||||
deep_compress_behavior: 'continue',
|
||||
agents_md_auto_inject: false,
|
||||
allow_root_file_creation: false,
|
||||
theme: loadCachedTheme(),
|
||||
@ -339,6 +343,8 @@ export const usePersonalizationStore = defineStore('personalization', {
|
||||
deep_compress_trigger_tokens: this.normalizeCompressionNumber(
|
||||
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,
|
||||
allow_root_file_creation: !!data.allow_root_file_creation,
|
||||
theme: ['classic', 'light', 'dark'].includes(data.theme) ? data.theme : fallbackTheme,
|
||||
|
||||
@ -51,6 +51,75 @@ class ServerRefactorSmokeTest(unittest.TestCase):
|
||||
self.assertTrue(callable(conversation.compute_workspace_storage))
|
||||
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__":
|
||||
unittest.main()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user