Compare commits

...

4 Commits

Author SHA1 Message Date
5723da8eb1 fix(android): 修复 App 内本地图片/视频选择无效的问题
- AndroidManifest.xml 补充 READ_MEDIA_IMAGES / READ_MEDIA_VIDEO 权限(Android 13+ 必需)
- 将已废弃的 startActivityForResult 替换为 registerForActivityResult(Activity Result API),解决 Android 13+ 文件选择器回调丢失
- 同步更新 versionCode 24 / versionName 1.0.22
2026-06-07 16:34:26 +08:00
180fb9bfbd chore(deps): add sherpa-onnx for voice/ASR support 2026-06-07 16:23:33 +08:00
78544cb205 refactor: remove run_python tool, consolidate all execution to run_command 2026-06-07 16:23:01 +08:00
4bf0733907 feat: add minimal tool reel animation 2026-06-07 14:59:44 +08:00
40 changed files with 881 additions and 362 deletions

View File

@ -108,7 +108,7 @@ python3 -m server.app --path <当前目录> --port 8091 --thinking-mode
- `list_files`: 列出目录内容
- `search_files`: 搜索文件
> 说明:目录创建/重命名/删除等通用文件管理建议统一使用 `run_command``run_python`
> 说明:目录创建/重命名/删除等通用文件管理建议统一使用 `run_command`;需要 Python 时先探测并选择合适解释器
#### 终端操作
@ -119,8 +119,7 @@ python3 -m server.app --path <当前目录> --port 8091 --thinking-mode
- `sleep`: 等待命令执行完成
**快速命令执行**:
- `run_command`: 执行 Shell 命令
- `run_python`: 执行 Python 代码
- `run_command`: 执行 Shell 命令;需要 Python 时通过命令探测并调用合适解释器
#### 网络检索
- `web_search`: 网络搜索

View File

@ -354,8 +354,8 @@ terminal_input(command="vim file.txt", output_wait=5) # vim 需要完整 TTY
✅ **正确:**
```python
# 使用 run_python 执行 Python 代码
run_python(code="print('hello')", timeout=5)
# 使用 run_command 探测并调用合适的 Python 解释器
run_command(command="python3 -c 'print(\"hello\")'", timeout=5)
# 使用 edit_file 修改文件
edit_file(file_path="file.txt", old_string="...", new_string="...")

View File

@ -1,5 +1,9 @@
# App 更新说明
# 1.0.22
- 修复 Android App 内从本地发送图片/视频无效的问题:补充 Android 13+ 所需的媒体读取权限(`READ_MEDIA_IMAGES` / `READ_MEDIA_VIDEO`
- 修复 Android App 文件选择器在 Android 13+ 上回调丢失的问题:将已废弃的 `startActivityForResult` 升级为 `registerForActivityResult`Activity Result API
# 1.0.21
- 修复 Android App 内手机端展开“对话记录”时仍沿用旧宽度的问题,面板宽度与桌面端对话侧栏保持一致
- 修复 Android App 内对话记录展开面板顶部残留系统栏空白的问题,内容从面板顶端开始显示

View File

@ -16,8 +16,8 @@ android {
applicationId = "com.cyjai.agent"
minSdk = 24
targetSdk = 35
versionCode = 23
versionName = "1.0.21"
versionCode = 24
versionName = "1.0.22"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@ -2,6 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<application
android:allowBackup="true"

View File

@ -18,6 +18,8 @@ import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
@ -29,6 +31,14 @@ class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
private var filePathCallback: ValueCallback<Array<Uri>>? = null
private val fileChooserLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val results = WebChromeClient.FileChooserParams.parseResult(result.resultCode, result.data)
filePathCallback?.onReceiveValue(results)
filePathCallback = null
}
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -111,7 +121,7 @@ class MainActivity : AppCompatActivity() {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
startActivityForResult(intent, FILE_CHOOSER_REQUEST_CODE)
fileChooserLauncher.launch(intent)
return true
}
}
@ -136,16 +146,6 @@ class MainActivity : AppCompatActivity() {
}
}
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode != FILE_CHOOSER_REQUEST_CODE) return
val results = WebChromeClient.FileChooserParams.parseResult(resultCode, data)
filePathCallback?.onReceiveValue(results)
filePathCallback = null
}
override fun onSaveInstanceState(outState: Bundle) {
webView.saveState(outState)
super.onSaveInstanceState(outState)
@ -300,7 +300,6 @@ class MainActivity : AppCompatActivity() {
companion object {
private const val HOME_URL = "https://agent.cyjai.com"
private const val WEB_ASSET_VERSION = "20260406_4"
private const val FILE_CHOOSER_REQUEST_CODE = 9001
}
private fun buildHomeUrl(): String {

View File

@ -26,7 +26,7 @@
"label": "终端指令",
"default_enabled": true,
"silent_when_disabled": false,
"tools": ["run_command", "run_python"]
"tools": ["run_command"]
}
]
}
@ -38,4 +38,3 @@
- `categories[].label`:分类展示名
- `categories[].tools`:该分类下具体工具名称列表(用于前端渲染/筛选/白名单提示)
- `default_enabled/silent_when_disabled`:用于 UI/策略展示(并不等于“你一定能调用到该工具”,真实可用性仍取决于服务端策略与运行时环境)

View File

@ -158,7 +158,6 @@ export function reduceTaskEvent(items: TimelineItem[], state: EventRenderState,
function toolTitle(name: string, args: any, status: string): string {
if (name === 'run_command') return `运行 ${args.command || ''}`.trim();
if (name === 'run_python') return '运行 Python';
if (name === 'write_file') return `Write ${args.file_path || args.path || ''} ${countWrite(args.content || '')}`.trim();
if (name === 'edit_file') return `Edited ${args.file_path || args.path || ''} ${countEdit(args)}`.trim();
if (name === 'read_file' || name === 'read_skill') return `阅读 ${args.path || args.file_path || ''}`.trim();
@ -193,7 +192,7 @@ function toolInitialBody(name: string, args: any): string {
function toolResultBody(name: string, args: any, result: any): string {
if (typeof result === 'string') return summarizeText(result);
if (!result) return '';
if (name === 'run_command' || name === 'terminal_input' || name === 'run_python') {
if (name === 'run_command' || name === 'terminal_input') {
const exitCode = result.exit_code ?? result.returncode;
const text = String(result?.output || result?.stdout || result?.stderr || '').trim();
if (exitCode !== undefined && exitCode !== 0) return text ? `exit code ${exitCode}` : `exit code ${exitCode}\n(no output)`;

View File

@ -131,7 +131,7 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
broadcast_callback=None, # CLI模式不需要广播
container_session=container_session,
)
# 让 run_command/run_python 复用终端容器,保持环境一致
# 让 run_command 复用终端容器,保持环境一致
self.terminal_ops.attach_terminal_manager(self.terminal_manager)
self._apply_container_session(container_session)

View File

@ -368,8 +368,6 @@ class MainTerminalCommandMixin:
print(f"{OUTPUT_FORMATS['terminal']} 执行终端命令")
elif tool_name == "web_search":
print(f"{OUTPUT_FORMATS['search']} 网络搜索")
elif tool_name == "run_python":
print(f"{OUTPUT_FORMATS['code']} 执行Python代码")
elif tool_name == "run_command":
print(f"{OUTPUT_FORMATS['terminal']} 执行系统命令")
elif tool_name == "update_memory":

View File

@ -125,7 +125,7 @@ class MainTerminalContextMixin:
detailed_rules = (
"- 所有命令和工具均可直接使用,无需用户批准\n"
"- run_command支持任意命令包括管道、重定向、子shell等\n"
"- 文件操作read_file / write_file / edit_file 可直接使用;其他文件管理请通过 run_command 或 run_python 执行"
"- 文件操作read_file / write_file / edit_file 可直接使用;其他文件管理请通过 run_command 执行;需要 Python 时先探测并选择合适解释器"
)
elif mode == "readonly":
detailed_rules = (
@ -138,7 +138,7 @@ class MainTerminalContextMixin:
detailed_rules = (
"- run_command先在系统只读沙箱执行仅当出现权限拒绝时才触发审批\n"
"- 审批通过后:仅当前这一次命令会重试为可写沙箱执行\n"
"- 需要用户批准terminal_input、run_python、write_file、edit_file、save_webpage 及其他会修改工作区的操作\n"
"- 需要用户批准terminal_input、write_file、edit_file、save_webpage 及其他会修改工作区的操作\n"
"- 被拒绝或超时:本次操作不会执行写入"
)
elif mode == "auto_approval":

View File

@ -502,7 +502,7 @@ class MainTerminalToolsDefinitionMixin:
"type": "function",
"function": {
"name": "read_file",
"description": "读取/搜索/抽取 UTF-8 文本文件内容。通过 type 参数选择 read阅读、search搜索、extract具体行段支持限制返回字符数。若文件非 UTF-8 或过大,请改用 run_python",
"description": "读取/搜索/抽取 UTF-8 文本文件内容。通过 type 参数选择 read阅读、search搜索、extract具体行段支持限制返回字符数。若文件非 UTF-8 或过大,请改用 run_command 调用合适的解析工具或 Python 解释器",
"parameters": {
"type": "object",
"properties": self._inject_intent({
@ -809,24 +809,6 @@ class MainTerminalToolsDefinitionMixin:
}
}
},
{
"type": "function",
"function": {
"name": "run_python",
"description": "执行一次性 Python 脚本,可用于处理二进制或非 UTF-8 文件(如 Excel、Word、PDF、图片或进行数据分析与验证。必须提供 timeout最长60秒一旦超时脚本会被打断且无法继续执行需要重新运行并返回已捕获输出。",
"parameters": {
"type": "object",
"properties": self._inject_intent({
"code": {"type": "string", "description": "Python代码"},
"timeout": {
"type": "number",
"description": "超时时长必填最大60"
}
}),
"required": ["code", "timeout"]
}
}
},
{
"type": "function",
"function": {

View File

@ -195,7 +195,6 @@ class MainTerminalToolsExecutionMixin:
}
_APPROVAL_REQUIRED_TOOLS = {
"run_command",
"run_python",
"terminal_input",
"write_file",
"edit_file",
@ -1407,12 +1406,6 @@ class MainTerminalToolsExecutionMixin:
"path": target_path
}
elif tool_name == "run_python":
result = await self.terminal_ops.run_python_code(
arguments["code"],
timeout=arguments.get("timeout")
)
elif tool_name == "run_command":
permission_mode = "unrestricted"
try:

View File

@ -66,7 +66,7 @@ TOOL_CATEGORIES: Dict[str, ToolCategory] = {
),
"terminal_command": ToolCategory(
label="终端指令",
tools=["run_command", "run_python"],
tools=["run_command"],
),
"memory": ToolCategory(
label="记忆",

View File

@ -82,7 +82,7 @@ class WebTerminal(MainTerminal):
broadcast_callback=message_callback,
container_session=self.container_session
)
# 让 run_command/run_python 与实时终端共享同一容器环境
# 让 run_command 与实时终端共享同一容器环境
self.terminal_ops.attach_terminal_manager(self.terminal_manager)
print(f"[WebTerminal] 初始化完成,项目路径: {project_path}")
@ -477,12 +477,6 @@ class WebTerminal(MainTerminal):
'status': 'saving_webpage',
'detail': f'保存网页: {arguments.get("url", "")}'
})
elif tool_name == "run_python":
self.broadcast('tool_status', {
'tool': tool_name,
'status': 'running_code',
'detail': '执行Python代码'
})
elif tool_name == "run_command":
self.broadcast('tool_status', {
'tool': tool_name,

View File

@ -6,7 +6,7 @@
- 执行环境Execution Mode
- 权限模式Permission Mode
- 路径授权Path Authorization
- `run_command` / `run_python` / `terminal` / `sub_agent` 的实际行为
- `run_command` / `terminal` / `sub_agent` 的实际行为
---
@ -136,25 +136,20 @@
- 在 approval 模式可触发“权限拒绝 -> 审批 -> 单次可写重试”
- 在 auto_approval 模式可触发“权限拒绝 -> 自动审批 -> 单次可写重试”
### 7.2 run_python
- 复用 `run_command` 执行路径与执行环境策略
- 与 `run_command` 一致受 sandbox/direct 与权限模式影响
### 7.3 terminal 系列
### 7.2 terminal 系列
- 终端会话从启动时就在沙箱里host+sandbox
- 同一会话中的后续输入继承该会话沙箱上下文
- 若路径授权更新,需要新开终端会话才能让该会话使用新策略
### 7.4 子智能体sub_agent
### 7.3 子智能体sub_agent
- 宿主机下已接入执行环境策略:
- `sandbox`:子智能体进程经宿主机沙箱启动
- `direct`:直接宿主机启动
- Docker 模式维持容器执行
### 7.5 自动审批approval agent
### 7.4 自动审批approval agent
- 自动审批由独立审批智能体执行(与主智能体分离)
- 配置文件:`config/auto_approval.json`

View File

@ -2,6 +2,7 @@
from __future__ import annotations
import shlex
import string
from typing import Dict, Any, Optional
@ -62,8 +63,11 @@ class CustomToolExecutor:
except Exception as exc:
return {"success": False, "error": f"模板渲染失败: {exc}", "tool_id": tool_id}
# 执行 python 代码
result = await self.terminal_ops.run_python_code(rendered, timeout=timeout)
# 通过统一的 run_command 链路执行 Python复用同一套解释器探测、沙箱与审批语义。
result = await self.terminal_ops.run_command(
f"python3 -u -c {shlex.quote(rendered)}",
timeout=timeout,
)
result["custom_tool"] = True
result["tool_id"] = tool_id
result["code_rendered"] = rendered

View File

@ -445,7 +445,7 @@ class FileManager:
except UnicodeDecodeError:
return {
"success": False,
"error": "文件不是 UTF-8 文本,无法直接读取,请改用 run_python 解析"
"error": "文件不是 UTF-8 文本,无法直接读取,请改用 run_command 调用合适的解析工具或 Python 解释器"
}
except Exception as e:
return {"success": False, "error": f"读取文件失败: {e}"}

View File

@ -13,7 +13,6 @@ from typing import Dict, Optional, Tuple, TYPE_CHECKING
from types import SimpleNamespace
try:
from config import (
CODE_EXECUTION_TIMEOUT,
TERMINAL_COMMAND_TIMEOUT,
FORBIDDEN_COMMANDS,
OUTPUT_FORMATS,
@ -25,7 +24,6 @@ except ImportError:
if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))
from config import (
CODE_EXECUTION_TIMEOUT,
TERMINAL_COMMAND_TIMEOUT,
FORBIDDEN_COMMANDS,
OUTPUT_FORMATS,
@ -111,7 +109,7 @@ class TerminalOperator:
def _resolve_active_container_session(self) -> Optional[SimpleNamespace]:
"""
若已存在活动终端且在容器内运行返回一个临时的容器句柄
以便 run_command/run_python 复用同一个容器环境
以便 run_command 复用同一个容器环境
"""
manager = self._terminal_manager
if not manager:
@ -734,129 +732,6 @@ class TerminalOperator:
"elapsed_ms": int((time.time() - start_ts) * 1000)
}
async def run_python_code(
self,
code: str,
timeout: int = None
) -> Dict:
"""
执行Python代码
Args:
code: Python代码
timeout: 超时时间
Returns:
执行结果字典
"""
if timeout is None or timeout <= 0:
return {
"success": False,
"error": "timeout 参数必填且需大于0",
"status": "error",
"output": "timeout 参数缺失",
"return_code": -1
}
timeout = self._clamp_timeout(timeout, default=timeout, max_limit=CODE_EXECUTION_TIMEOUT)
# 强制重置工具容器,避免上一段代码仍在运行时输出混入
self._reset_toolbox()
# 创建临时Python文件
temp_file = self.project_path / ".temp_code.py"
try:
# 写入代码
with open(temp_file, 'w', encoding='utf-8') as f:
f.write(code)
print(f"{OUTPUT_FORMATS['code']} 执行Python代码")
try:
relative_temp = temp_file.relative_to(self.project_path).as_posix()
except ValueError:
relative_temp = temp_file.as_posix()
# 使用通用 python3 占位,由 run_command 根据执行环境重写
result = await self.run_command(
f'python3 -u "{relative_temp}"',
timeout=timeout
)
# 添加代码到结果
result["code"] = code
return result
finally:
# 清理临时文件
if temp_file.exists():
temp_file.unlink()
async def run_python_file(
self,
file_path: str,
args: str = "",
timeout: int = None
) -> Dict:
"""
执行Python文件
Args:
file_path: Python文件路径
args: 命令行参数
timeout: 超时时间
Returns:
执行结果字典
"""
# 构建完整路径
full_path = (self.project_path / file_path).resolve()
# 验证文件存在
if not full_path.exists():
return {
"success": False,
"error": "文件不存在",
"output": "",
"return_code": -1
}
# 验证是Python文件
if not full_path.suffix == '.py':
return {
"success": False,
"error": "不是Python文件",
"output": "",
"return_code": -1
}
# 验证文件在项目内
try:
full_path.relative_to(self.project_path)
except ValueError:
return {
"success": False,
"error": "文件必须在项目文件夹内",
"output": "",
"return_code": -1
}
print(f"{OUTPUT_FORMATS['code']} 执行Python文件: {file_path}")
try:
relative_path = full_path.relative_to(self.project_path).as_posix()
except ValueError:
relative_path = full_path.as_posix()
# 使用通用 python3 占位,由 run_command 根据执行环境重写
command = f'python3 "{relative_path}"'
if args:
command += f" {args}"
# 执行命令
return await self.run_command(command, timeout=timeout)
async def install_package(self, package: str) -> Dict:
"""
安装Python包

View File

@ -55,7 +55,7 @@ def _build_sandbox_options(container_session: Optional["ContainerHandle"] = None
class ToolboxContainer:
"""为 run_command/run_python 提供的专用容器."""
"""为 run_command 提供的专用容器."""
def __init__(
self,

7
package-lock.json generated
View File

@ -29,6 +29,7 @@
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"sherpa-onnx": "^1.13.2",
"socket.io-client": "^4.7.5",
"unified": "^11.0.5",
"vue": "^3.4.15",
@ -6417,6 +6418,12 @@
"node": ">=8"
}
},
"node_modules/sherpa-onnx": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/sherpa-onnx/-/sherpa-onnx-1.13.2.tgz",
"integrity": "sha512-hheOLl4JlOzERco73u+1Q/LaNDdFChDe4r3+IlMYIO3wggBZrReHDSxWwRUIW+YLw3eLslyiXw/hfb75aOylEA==",
"license": "Apache-2.0"
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",

View File

@ -36,6 +36,7 @@
"remark-gfm": "^4.0.1",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.2",
"sherpa-onnx": "^1.13.2",
"socket.io-client": "^4.7.5",
"unified": "^11.0.5",
"vue": "^3.4.15",

View File

@ -20,7 +20,7 @@
| **信息检索** | 网络搜索(`web_search`)、网页内容提取(`extract_webpage`、`save_webpage`)、信息整合 |
| **MCP扩展** | 通过 MCP 服务扩展外部工具;可用 `list_mcp_servers` 查看已配置服务与工具别名 |
| **文件管理** | 批量处理文件、目录操作、自动化任务 |
| **终端操作** | 执行命令(`run_command`、运行Python代码`run_python`)、持久终端会话(`terminal_session` 系列) |
| **终端操作** | 执行命令(`run_command`,需要 Python 时先探测并选择合适解释器)、持久终端会话(`terminal_session` 系列) |
| **视觉理解** | 分析图片内容、识别文字/物体/表格(`vlm_analyze`/`view_image` |
| **任务协作** | 创建子智能体(`create_sub_agent`)并行处理独立子任务 |
@ -87,7 +87,7 @@
- `write_file`:写入文件(`append` 控制覆盖/追加)
- `read_file`:读取文件(支持 `read`/`search`/`extract` 三种模式)
- `edit_file`:精确字符串替换(`replace_all` 必填,必须显式传入 `true/false``old_string` 建议至少 3 行以提升定位稳定性。需要批量替换的场景可以单行或不足一行)
- 其余文件管理(创建/重命名/删除目录与文件)请优先使用 `run_command` / `run_python`
- 其余文件管理(创建/重命名/删除目录与文件)请优先使用 `run_command`
**注意**:超大文件用 `search` 定位 + `extract` 抽取,避免全文读取。
@ -105,8 +105,7 @@
- 终端卡死需用 `terminal_session` 的 reset 操作恢复
**快速执行**
- `run_command`一次性命令最长30s输出限10000字符
- `run_python`:执行 Python 脚本最长60s
- `run_command`一次性命令最长30s输出限10000字符需要 Python 时先用命令探测可用解释器(如 `python3 --version` / `python --version`)再执行
**终端禁忌**禁止运行交互式程序vim、python REPL等不确定命令是否结束时必须用 `terminal_snapshot` 确认。

View File

@ -20,7 +20,7 @@
| **信息检索** | 网络搜索(`web_search`)、网页内容提取(`extract_webpage`、`save_webpage`)、信息整合 |
| **MCP扩展** | 通过 MCP 服务扩展外部工具;可用 `list_mcp_servers` 查看已配置服务与工具别名 |
| **文件管理** | 批量处理文件、目录操作、自动化任务 |
| **终端操作** | 执行命令(`run_command`、运行Python代码`run_python`)、持久终端会话(`terminal_session` 系列) |
| **终端操作** | 执行命令(`run_command`,需要 Python 时先探测并选择合适解释器)、持久终端会话(`terminal_session` 系列) |
| **视觉理解** | 自带多模态能力,可直接分析图片内容、识别文字/物体/表格/场景 |
| **任务协作** | 创建子智能体(`create_sub_agent`)并行处理独立子任务 |
@ -87,7 +87,7 @@
- `write_file`:写入文件(`append` 控制覆盖/追加)
- `read_file`:读取文件(支持 `read`/`search`/`extract` 三种模式)
- `edit_file`:精确字符串替换(`replace_all` 必填,必须显式传入 `true/false``old_string` 建议至少 3 行以提升定位稳定性。需要批量替换的场景可以单行或不足一行)
- 其余文件管理(创建/重命名/删除目录与文件)请优先使用 `run_command` / `run_python`
- 其余文件管理(创建/重命名/删除目录与文件)请优先使用 `run_command`
**注意**:超大文件用 `search` 定位 + `extract` 抽取,避免全文读取。
@ -104,7 +104,7 @@
#### 细节增强与"切图放大"方法(推荐)
当图片文字很小、细节密集、或需要逐块检查时:
1. **用 `run_python` 切图**把原图按区域裁切成若干张更小的局部图(例如:左上/右上/左下/右下;或按表格/按钮/车标/铭牌所在区域裁切)
1. **用 `run_command` 调用合适的 Python 解释器切图**:先探测可用解释器,再把原图按区域裁切成若干张更小的局部图(例如:左上/右上/左下/右下;或按表格/按钮/车标/铭牌所在区域裁切)
2. **必要时做多版本增强**:对同一区域输出多张增强版本(例如:对比度增强、锐化、灰度、二值化)用于读字/看边缘
3. **再次查看局部图**:对每张局部图分别观察并给出结论,再把结论汇总回原图任务
@ -123,8 +123,7 @@
- 终端卡死需用 `terminal_session` 的 reset 操作恢复
**快速执行**
- `run_command`一次性命令最长30s输出限10000字符
- `run_python`:执行 Python 脚本最长60s切图处理必备
- `run_command`一次性命令最长30s输出限10000字符需要 Python/切图处理时先用命令探测可用解释器(如 `python3 --version` / `python --version`)再执行
**终端禁忌**禁止运行交互式程序vim、python REPL等不确定命令是否结束时必须用 `terminal_snapshot` 确认。

View File

@ -332,7 +332,7 @@ def detect_malformed_tool_call(text):
'create_file', 'read_file', 'write_file', 'edit_file', 'delete_file',
'terminal_session', 'terminal_input', 'web_search',
'extract_webpage', 'save_webpage',
'run_python', 'run_command', 'sleep',
'run_command', 'sleep',
]
for tool in tool_names:
if tool in text and '{' in text:

View File

@ -120,16 +120,6 @@ def _build_tool_approval_preview(web_terminal, function_name: str, arguments: Di
preview["summary"] = f"执行命令: {args.get('command') or ''}"
return preview
if function_name in {"run_python", "runpython"}:
code = str(args.get("code") or "")
timeout = args.get("timeout")
preview["code"] = code
preview["timeout"] = timeout
# 兼容前端已有 command 展示分支
preview["command"] = code
preview["summary"] = f"执行 Python 代码timeout={timeout if timeout is not None else '未提供'}s"
return preview
if function_name in {"create_file", "create_folder", "delete_file"}:
preview["path"] = args.get("path")
preview["summary"] = f"{function_name}: {args.get('path') or ''}"

View File

@ -0,0 +1,443 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>极简模式 · 并行工具转轮动画 Demo</title>
<style>
:root {
color-scheme: light;
--surface-base: #faf9f5;
--surface-card: #f0ece3;
--surface-muted: #e7e1d5;
--surface-raised: #fffdf8;
--text-primary: #25231f;
--text-secondary: #6c665d;
--text-muted: #91887a;
--border-subtle: #ddd5c8;
--accent: #b9654f;
--state-running: #8b7567;
--state-done: #47735e;
--shadow-panel: 0 18px 52px rgba(53, 45, 35, .14);
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
--sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
background:
linear-gradient(180deg, var(--surface-base), var(--surface-muted));
color: var(--text-primary);
font-family: var(--sans);
}
.stage {
width: min(760px, calc(100vw - 40px));
padding: 34px;
border: 1px solid var(--border-subtle);
border-radius: 24px;
background: var(--surface-raised);
box-shadow: var(--shadow-panel);
}
.eyebrow {
margin: 0 0 10px;
color: var(--text-muted);
font-size: 12px;
font-weight: 700;
letter-spacing: .16em;
text-transform: uppercase;
}
h1 {
margin: 0 0 12px;
font-size: 24px;
line-height: 1.25;
letter-spacing: -.03em;
}
.desc {
max-width: 620px;
margin: 0 0 28px;
color: var(--text-secondary);
font-size: 14px;
line-height: 1.75;
}
.minimal-line {
display: grid;
grid-template-columns: 10px minmax(0, 1fr) auto;
align-items: center;
gap: 12px;
height: 46px;
padding: 0 14px;
border: 1px solid var(--border-subtle);
border-radius: 16px;
background: var(--surface-card);
overflow: hidden;
}
.dot {
width: 7px;
height: 7px;
border-radius: 999px;
background: var(--state-running);
transform-origin: center;
animation: breathe 1.25s ease-in-out infinite;
}
.dot.done {
background: var(--state-done);
animation: none;
}
@keyframes breathe {
0%, 100% { transform: scale(.72); opacity: .55; }
50% { transform: scale(1); opacity: 1; }
}
.summary {
min-width: 0;
display: flex;
align-items: center;
gap: 8px;
color: var(--text-primary);
font-size: 14px;
white-space: nowrap;
}
.intent {
flex: 0 0 auto;
color: var(--text-secondary);
}
.tool-window {
position: relative;
flex: 1 1 auto;
min-width: 0;
height: 26px;
overflow: hidden;
font-family: var(--mono);
color: var(--text-primary);
}
.tool-window::before,
.tool-window::after {
content: "";
position: absolute;
left: 0;
right: 0;
z-index: 2;
height: 7px;
pointer-events: none;
}
.tool-window::before {
top: 0;
background: linear-gradient(180deg, var(--surface-card), transparent);
}
.tool-window::after {
bottom: 0;
background: linear-gradient(0deg, var(--surface-card), transparent);
}
.tool-item,
.typing-text,
.done-text {
height: 26px;
display: flex;
align-items: center;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.typing-text,
.done-text {
position: absolute;
inset: 0;
}
.typing-text::after {
content: "";
width: 1px;
height: 15px;
margin-left: 2px;
background: var(--accent);
animation: caret .75s steps(1) infinite;
}
@keyframes caret {
0%, 45% { opacity: 1; }
46%, 100% { opacity: 0; }
}
.tool-reel {
position: absolute;
left: 0;
right: 0;
top: 0;
will-change: transform;
transform: translateY(0);
}
.tool-reel.rolling {
transition: transform 520ms cubic-bezier(.22, .9, .25, 1);
}
.tool-reel.settle {
transition: transform 170ms cubic-bezier(.2, .72, .26, 1);
}
.badge {
flex: 0 0 auto;
height: 24px;
display: inline-flex;
align-items: center;
padding: 0 9px;
border-radius: 999px;
background: var(--surface-muted);
color: var(--text-secondary);
font-size: 12px;
font-weight: 650;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 18px;
}
button {
height: 36px;
padding: 0 14px;
border: 1px solid var(--border-subtle);
border-radius: 12px;
background: var(--surface-card);
color: var(--text-primary);
font: 650 13px/1 var(--sans);
cursor: pointer;
}
button.primary {
border-color: var(--accent);
background: var(--accent);
color: var(--surface-raised);
}
button:disabled {
cursor: not-allowed;
opacity: .55;
}
.notes {
margin: 18px 0 0;
padding-left: 18px;
color: var(--text-secondary);
font-size: 13px;
line-height: 1.8;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: .001ms !important;
animation-iteration-count: 1 !important;
transition-duration: .001ms !important;
}
}
</style>
</head>
<body>
<main class="stage">
<p class="eyebrow">Minimal Mode Motion Test</p>
<h1>并行工具调用摘要行 · 转轮动画</h1>
<p class="desc">
演示逻辑:并行批次开始后,第一个工具先按打字机显示;随后摘要行在本批次工具之间做纵向转轮循环,直到批次结束后停在最终状态。
</p>
<section class="minimal-line" aria-label="极简模式摘要行">
<span id="dot" class="dot" aria-hidden="true"></span>
<div class="summary">
<span id="intent" class="intent">正在并行调用</span>
<span id="toolWindow" class="tool-window" aria-live="polite"></span>
</div>
<span id="badge" class="badge">准备中</span>
</section>
<div class="controls">
<button id="startBtn" class="primary" type="button">播放并行调用</button>
<button id="finishBtn" type="button" disabled>结束批次</button>
<button id="resetBtn" type="button">重置</button>
</div>
<ul class="notes">
<li>工具列表是一整列,窗口只露出当前命中的一项;所有工具同时向上滚动。</li>
<li>每项停留约 1.45 秒;落位时只轻微多冲一点,再回弹,模拟克制的“刹不住车”顿挫。</li>
<li>第一个工具保留打字机式出现,避免开头突兀。</li>
</ul>
</main>
<script>
const tools = [
"search_query · 检索相关资料",
"read_file · 读取项目片段",
"run_command · 执行最小验证",
"browser_preview · 检查本地界面"
];
const toolWindow = document.getElementById("toolWindow");
const badge = document.getElementById("badge");
const dot = document.getElementById("dot");
const startBtn = document.getElementById("startBtn");
const finishBtn = document.getElementById("finishBtn");
const resetBtn = document.getElementById("resetBtn");
let typingTimer = null;
let wheelTimer = null;
let settleTimer = null;
let index = 0;
let running = false;
const itemHeight = 26;
function clearTimers() {
window.clearInterval(typingTimer);
window.clearInterval(wheelTimer);
window.clearTimeout(settleTimer);
typingTimer = null;
wheelTimer = null;
settleTimer = null;
}
function clearToolWindow() {
toolWindow.replaceChildren();
}
function setTextNode(text, className) {
clearToolWindow();
const node = document.createElement("span");
node.className = className;
node.textContent = text;
toolWindow.appendChild(node);
return node;
}
function buildReel() {
clearToolWindow();
const reel = document.createElement("div");
reel.id = "toolReel";
reel.className = "tool-reel";
[...tools, tools[0]].forEach((tool) => {
const item = document.createElement("div");
item.className = "tool-item";
item.textContent = tool;
reel.appendChild(item);
});
toolWindow.appendChild(reel);
index = 0;
reel.style.transform = "translateY(0)";
return reel;
}
function typeFirstTool(done) {
const text = tools[0];
let cursor = 0;
const typingNode = setTextNode("", "typing-text");
badge.textContent = "1 / " + tools.length;
typingTimer = window.setInterval(() => {
cursor += 1;
typingNode.textContent = text.slice(0, cursor);
if (cursor >= text.length) {
window.clearInterval(typingTimer);
typingTimer = null;
window.setTimeout(done, 520);
}
}, 34);
}
function spinToNext() {
const reel = document.getElementById("toolReel");
if (!reel) return;
index = (index + 1) % tools.length;
badge.textContent = `${index + 1} / ${tools.length}`;
const visualIndex = index === 0 ? tools.length : index;
const target = -visualIndex * itemHeight;
const overshoot = target - 2;
reel.className = "tool-reel rolling";
reel.style.transform = `translateY(${overshoot}px)`;
settleTimer = window.setTimeout(() => {
reel.className = "tool-reel settle";
reel.style.transform = `translateY(${target}px)`;
if (index === 0) {
settleTimer = window.setTimeout(() => {
reel.className = "tool-reel";
reel.style.transform = "translateY(0)";
}, 175);
}
}, 520);
}
function startWheel() {
buildReel();
wheelTimer = window.setInterval(spinToNext, 1450);
}
function startDemo() {
if (running) return;
clearTimers();
running = true;
index = 0;
dot.classList.remove("done");
startBtn.disabled = true;
finishBtn.disabled = false;
badge.textContent = "启动中";
typeFirstTool(startWheel);
}
function finishDemo() {
if (!running) return;
clearTimers();
running = false;
setTextNode("并行工具调用完成", "done-text");
badge.textContent = "完成";
dot.classList.add("done");
startBtn.disabled = false;
finishBtn.disabled = true;
}
function resetDemo() {
clearTimers();
running = false;
index = 0;
setTextNode("等待并行工具批次", "done-text");
badge.textContent = "准备中";
dot.classList.remove("done");
startBtn.disabled = false;
finishBtn.disabled = true;
}
startBtn.addEventListener("click", startDemo);
finishBtn.addEventListener("click", finishDemo);
resetBtn.addEventListener("click", resetDemo);
resetDemo();
</script>
</body>
</html>

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>虚拟显示器 - 单次终端指令run_python演示</title>
<title>虚拟显示器 - 单次终端指令run_command 调 Python演示</title>
<style>
:root {
--panel: #0b0d14;
@ -80,7 +80,7 @@
<div class="monitor">
<div class="monitor-shell">
<div class="monitor-top">
<div>Agent Display Surface · run_python</div>
<div>Agent Display Surface · run_command Python</div>
<div class="status-pill" id="statusPill">待机</div>
</div>
<div class="monitor-screen" id="monitorScreen">
@ -318,7 +318,7 @@
helpers.setStatus('正在准备');
await helpers.sleep(600);
helpers.setStatus('正在规划');
await helpers.showBubble('run_python打开独立的 Python 窗口。', { duration: 2200 });
await helpers.showBubble('run_command调用合适的 Python 解释器。', { duration: 2200 });
const pythonIcon = helpers.getAppIcon('pythonApp');
await helpers.moveMouseTo(pythonIcon);
@ -352,7 +352,7 @@
await helpers.sleep(1500);
helpers.setStatus('正在输出');
await helpers.showBubble('run_python 完成,输出已写入结果面板。', { duration: 2000 });
await helpers.showBubble('run_command 完成,输出已写入结果面板。', { duration: 2000 });
helpers.setStatus('已完成');
replayBtn.disabled = false;
}

View File

@ -12,15 +12,35 @@
>
<div class="summary-content-wrapper">
<span class="summary-preview">
<template v-if="isSummaryRunning(group.actions, group.id)">
<template v-if="shouldAnimateSummary(group.actions, group.id)">
<span
v-for="(char, idx) in getAnimatedSummaryChars(getSummaryPreview(group.actions))"
:key="`${group.id}-${idx}`"
class="summary-char"
:style="{ animationDelay: `${(idx + 1) * 0.12}s` }"
v-if="shouldShowToolReel(group.actions, group.id)"
class="summary-tool-reel-window"
>
{{ char === ' ' ? '\u00A0' : char }}
<span
class="summary-tool-reel-track"
:class="getToolReelPhase(group.id)"
:style="getToolReelTrackStyle(group.id)"
>
<span
v-for="(item, idx) in getToolReelDisplayItems(group.actions, group.id)"
:key="`${group.id}-tool-reel-${idx}-${item}`"
class="summary-tool-reel-item"
>
{{ item }}
</span>
</span>
</span>
<template v-else>
<span
v-for="(char, idx) in getAnimatedSummaryChars(getSummaryPreview(group.actions))"
:key="`${group.id}-${idx}`"
class="summary-char"
:style="{ animationDelay: `${(idx + 1) * 0.12}s` }"
>
{{ char === ' ' ? '\u00A0' : char }}
</span>
</template>
</template>
<template v-else>
{{ getSummaryPreview(group.actions) }}
@ -125,7 +145,7 @@
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick, Component } from 'vue';
import { ref, reactive, computed, watch, nextTick, onBeforeUnmount, Component } from 'vue';
import { usePersonalizationStore } from '@/stores/personalization';
import { renderEnhancedToolResult } from './actions/toolRenderers';
import { getRandomLoader } from './loaders/index';
@ -139,6 +159,10 @@ interface Action {
tool?: {
name?: string;
intent?: string;
intent_rendered?: string;
intent_full?: string;
status?: string;
display_name?: string;
arguments?: any;
result?: any;
};
@ -181,6 +205,26 @@ const expandedGroups = ref(new Set<string>());
const thinkingRefs = new Map<string, HTMLElement>();
const summaryLoaders = new Map<string, Component>();
const SUMMARY_PREVIEW_MAX_CHARS = 25;
const TOOL_REEL_ITEM_HEIGHT = 26;
const TOOL_REEL_INTERVAL_MS = 1450;
const TOOL_REEL_ROLL_MS = 520;
const TOOL_REEL_SETTLE_MS = 170;
const TOOL_REEL_OVERSHOOT_PX = 2;
type ToolReelPhase = 'idle' | 'rolling' | 'settle';
interface ToolReelState {
index: number;
offsetPx: number;
phase: ToolReelPhase;
signature: string;
items: string[];
completing?: boolean;
}
const toolReelStates = reactive<Record<string, ToolReelState>>({});
const toolReelIntervals = new Map<string, number>();
const toolReelTimeouts = new Map<string, number[]>();
const truncateSummaryPreview = (raw: string) => {
const text = typeof raw === 'string' ? raw : '';
@ -192,6 +236,251 @@ const truncateSummaryPreview = (raw: string) => {
const getAnimatedSummaryChars = (text: string) => Array.from(text || '');
const getFirstLine = (raw: string) => {
const text = typeof raw === 'string' ? raw : '';
const firstLineEnd = text.indexOf('\n');
return firstLineEnd > 0 ? text.substring(0, firstLineEnd) : text;
};
const getToolSummaryText = (action: Action) => {
const tool = action.tool;
if (!tool) return '';
const intentEnabled = personalizationStore.form.tool_intent_enabled;
const intentText = tool.intent_rendered || tool.intent_full || '';
if (intentEnabled && intentText) {
return truncateSummaryPreview(getFirstLine(intentText));
}
if (tool.status === 'preparing') {
return `准备调用 ${tool.name || '工具'}...`;
}
if (tool.status === 'running') {
return `正在调用 ${tool.name || '工具'}...`;
}
if (tool.status === 'completed') {
return tool.display_name || tool.name || '工具执行完成';
}
return action.streaming ? '正在执行工具...' : tool.display_name || tool.name || '执行工具';
};
const isActiveToolAction = (action: Action) => {
if (action.type !== 'tool') return false;
if (action.streaming) return true;
const status = String(action.tool?.status || '').toLowerCase();
return [
'hinted',
'preparing',
'running',
'pending',
'pending_approval',
'awaiting_approval',
'awaiting_user_answer'
].includes(status);
};
const getLatestToolSegment = (actions: Action[]) => {
let lastToolIndex = -1;
for (let i = actions.length - 1; i >= 0; i--) {
if (actions[i].type === 'tool') {
lastToolIndex = i;
break;
}
if (actions[i].type === 'thinking') {
break;
}
}
if (lastToolIndex < 0) return [];
let startIndex = lastToolIndex;
while (startIndex > 0 && actions[startIndex - 1].type === 'tool') {
startIndex--;
}
return actions.slice(startIndex, lastToolIndex + 1);
};
const getLatestActiveToolSegment = (actions: Action[]) => {
const segment = getLatestToolSegment(actions);
return segment.some(isActiveToolAction) ? segment : [];
};
const getToolReelItems = (actions: Action[]) =>
getLatestActiveToolSegment(actions)
.map(getToolSummaryText)
.filter((item) => item.trim().length > 0);
const getToolReelDisplayItems = (actions: Action[], groupId: string) => {
const completingItems = toolReelStates[groupId]?.completing
? toolReelStates[groupId].items
: null;
if (completingItems?.length) {
return completingItems;
}
const items = getToolReelItems(actions);
return items.length > 0 ? [...items, items[0]] : [];
};
const shouldShowToolReel = (actions: Action[], groupId: string) =>
!!toolReelStates[groupId]?.completing ||
(isSummaryRunning(actions, groupId) && getToolReelItems(actions).length > 1);
const shouldAnimateSummary = (actions: Action[], groupId: string) =>
isSummaryRunning(actions, groupId) || !!toolReelStates[groupId]?.completing;
const getToolReelPhase = (groupId: string) => toolReelStates[groupId]?.phase || 'idle';
const getToolReelTrackStyle = (groupId: string) => ({
transform: `translateY(${toolReelStates[groupId]?.offsetPx || 0}px)`
});
const clearToolReelTimers = (groupId: string) => {
const interval = toolReelIntervals.get(groupId);
if (interval) {
window.clearInterval(interval);
toolReelIntervals.delete(groupId);
}
const timeouts = toolReelTimeouts.get(groupId) || [];
timeouts.forEach((timeout) => window.clearTimeout(timeout));
toolReelTimeouts.delete(groupId);
};
const pushToolReelTimeout = (groupId: string, timeout: number) => {
const timeouts = toolReelTimeouts.get(groupId) || [];
timeouts.push(timeout);
toolReelTimeouts.set(groupId, timeouts);
};
const spinToolReel = (groupId: string, itemCount: number) => {
const state = toolReelStates[groupId];
if (!state || state.completing || itemCount < 2) return;
const nextIndex = (state.index + 1) % itemCount;
const visualIndex = nextIndex === 0 ? itemCount : nextIndex;
const targetOffset = -visualIndex * TOOL_REEL_ITEM_HEIGHT;
state.phase = 'rolling';
state.offsetPx = targetOffset - TOOL_REEL_OVERSHOOT_PX;
const settleTimeout = window.setTimeout(() => {
state.phase = 'settle';
state.offsetPx = targetOffset;
state.index = nextIndex;
if (nextIndex === 0) {
const resetTimeout = window.setTimeout(() => {
state.phase = 'idle';
state.offsetPx = 0;
}, TOOL_REEL_SETTLE_MS);
pushToolReelTimeout(groupId, resetTimeout);
}
}, TOOL_REEL_ROLL_MS);
pushToolReelTimeout(groupId, settleTimeout);
};
const finishToolReel = (groupId: string, actions: Action[]) => {
const state = toolReelStates[groupId];
if (!state || state.completing) return;
const items =
state.items.length > 1
? state.items
: getLatestToolSegment(actions)
.map(getToolSummaryText)
.filter((item) => item.trim().length > 0);
if (items.length < 2) {
clearToolReelTimers(groupId);
delete toolReelStates[groupId];
return;
}
clearToolReelTimers(groupId);
state.items = items;
state.signature = items.join('\u0001');
state.completing = true;
const finalIndex = items.length - 1;
const normalizedOffset = -state.index * TOOL_REEL_ITEM_HEIGHT;
const targetOffset = -finalIndex * TOOL_REEL_ITEM_HEIGHT;
state.phase = 'idle';
state.offsetPx = normalizedOffset;
window.requestAnimationFrame(() => {
state.phase = 'rolling';
state.offsetPx = targetOffset - TOOL_REEL_OVERSHOOT_PX;
const settleTimeout = window.setTimeout(() => {
state.phase = 'settle';
state.offsetPx = targetOffset;
state.index = finalIndex;
}, TOOL_REEL_ROLL_MS);
pushToolReelTimeout(groupId, settleTimeout);
const cleanupTimeout = window.setTimeout(() => {
clearToolReelTimers(groupId);
delete toolReelStates[groupId];
}, TOOL_REEL_ROLL_MS + TOOL_REEL_SETTLE_MS + 260);
pushToolReelTimeout(groupId, cleanupTimeout);
});
};
const syncToolReels = () => {
const activeGroups = new Set<string>();
blockGroups.value.forEach((group) => {
if (group.type !== 'summary' || !group.actions || !shouldShowToolReel(group.actions, group.id)) {
return;
}
const items = getToolReelItems(group.actions);
const signature = items.join('\u0001');
activeGroups.add(group.id);
if (!toolReelStates[group.id] || toolReelStates[group.id].signature !== signature) {
clearToolReelTimers(group.id);
toolReelStates[group.id] = {
index: 0,
offsetPx: 0,
phase: 'idle',
signature,
items
};
} else {
toolReelStates[group.id].items = items;
}
if (!toolReelIntervals.has(group.id)) {
const interval = window.setInterval(
() => spinToolReel(group.id, items.length),
TOOL_REEL_INTERVAL_MS
);
toolReelIntervals.set(group.id, interval);
}
});
Object.keys(toolReelStates).forEach((groupId) => {
if (!activeGroups.has(groupId)) {
const group = blockGroups.value.find((item) => item.id === groupId);
if (group?.type === 'summary' && group.actions && !toolReelStates[groupId].completing) {
activeGroups.add(groupId);
finishToolReel(groupId, group.actions);
return;
}
if (!toolReelStates[groupId].completing) {
clearToolReelTimers(groupId);
delete toolReelStates[groupId];
}
}
});
};
// action
const getSummaryLoader = (groupId: string) => {
const group = blockGroups.value.find((g) => g.id === groupId);
@ -342,9 +631,11 @@ const getSummaryPreview = (actions: Action[]) => {
return `正在调用 ${tool.name || '工具'}...`;
}
if (tool.status === 'completed') {
return '工具执行完成';
return truncateSummaryPreview(tool.display_name || tool.name || '工具执行完成');
}
return currentStep.streaming ? '正在执行工具...' : '工具执行完成';
return currentStep.streaming
? '正在执行工具...'
: truncateSummaryPreview(tool.display_name || tool.name || '工具执行完成');
}
return '';
};
@ -533,10 +824,21 @@ const handleScroll = (blockId: string, event: Event) => {
watch(
() => props.actions,
() => {
//
syncToolReels();
},
{ deep: true }
{ deep: true, immediate: true }
);
watch(
() => [props.conversationRunning, props.isLatestMessage],
() => {
syncToolReels();
}
);
onBeforeUnmount(() => {
Object.keys(toolReelStates).forEach(clearToolReelTimers);
});
</script>
<style scoped>
@ -596,6 +898,63 @@ watch(
color: inherit;
}
.summary-tool-reel-window {
position: relative;
display: inline-block;
width: min(100%, 36em);
height: 26px;
overflow: hidden;
vertical-align: top;
}
.summary-tool-reel-window::before,
.summary-tool-reel-window::after {
content: '';
position: absolute;
left: 0;
right: 0;
z-index: 1;
height: 6px;
pointer-events: none;
}
.summary-tool-reel-window::before {
top: 0;
background: linear-gradient(180deg, var(--surface-base), transparent);
}
.summary-tool-reel-window::after {
bottom: 0;
background: linear-gradient(0deg, var(--surface-base), transparent);
}
.summary-tool-reel-track {
position: absolute;
top: 0;
left: 0;
right: 0;
display: block;
will-change: transform;
}
.summary-tool-reel-track.rolling {
transition: transform 520ms cubic-bezier(0.22, 0.9, 0.25, 1);
}
.summary-tool-reel-track.settle {
transition: transform 170ms cubic-bezier(0.2, 0.72, 0.26, 1);
}
.summary-tool-reel-item {
display: block;
height: 26px;
line-height: 26px;
overflow: hidden;
color: var(--text-secondary);
text-overflow: ellipsis;
white-space: nowrap;
}
.summary-status-icon {
flex-shrink: 0;
margin-left: 0;

View File

@ -133,16 +133,6 @@
</div>
<div v-else class="search-empty">未返回详细的搜索结果</div>
</div>
<div v-else-if="action.tool?.name === 'run_python' && action.tool?.result">
<div class="code-block">
<div class="code-label">代码</div>
<pre><code class="language-python">{{ action.tool.result.code || action.tool.arguments?.code }}</code></pre>
</div>
<div v-if="action.tool.result.output" class="output-block">
<div class="output-label">输出</div>
<pre>{{ action.tool.result.output }}</pre>
</div>
</div>
<div v-else>
<pre>{{
JSON.stringify(action.tool?.result || action.tool?.arguments, null, 2)

View File

@ -159,8 +159,6 @@ function renderEnhancedToolResult(): string {
//
else if (name === 'run_command') {
return renderRunCommand(result, args);
} else if (name === 'run_python') {
return renderRunPython(result, args);
}
//
@ -733,25 +731,6 @@ function renderRunCommand(result: any, args: any): string {
return html;
}
function renderRunPython(result: any, args: any): string {
const code = result.code || args.code || '';
const output = result.output || '';
let html = '<div class="code-block">';
html += '<div class="code-label">代码:</div>';
html += `<pre><code class="language-python">${escapeHtml(code)}</code></pre>`;
html += '</div>';
if (output) {
html += '<div class="output-block">';
html += '<div class="output-label">输出:</div>';
html += `<pre>${escapeHtml(output)}</pre>`;
html += '</div>';
}
return html;
}
// ===== =====
function renderUpdateMemory(result: any, args: any): string {
const operations = args.operations || [];
@ -1339,7 +1318,7 @@ function renderEasterEgg(result: any, args: any): string {
word-wrap: break-word;
}
/* 暗色模式:run_python 代码/输出统一白字,去除任何描边/阴影观感 */
/* 暗色模式:代码/输出统一白字,去除任何描边/阴影观感 */
:root[data-theme='dark'] .code-block pre,
:root[data-theme='dark'] .output-block pre {
color: var(--text-primary) !important;

View File

@ -95,8 +95,6 @@ export function renderEnhancedToolResult(
// 终端指令类
else if (name === 'run_command') {
return renderRunCommand(result, args);
} else if (name === 'run_python') {
return renderRunPython(result, args);
}
// 记忆类
@ -673,25 +671,6 @@ function renderRunCommand(result: any, args: any): string {
return html;
}
function renderRunPython(result: any, args: any): string {
const code = result.code || args.code || '';
const output = result.output || '';
let html = '<div class="code-block">';
html += '<div class="code-label">代码:</div>';
html += `<pre><code class="language-python">${escapeHtml(code)}</code></pre>`;
html += '</div>';
if (output) {
html += '<div class="output-block">';
html += '<div class="output-label">输出:</div>';
html += `<pre>${escapeHtml(output)}</pre>`;
html += '</div>';
}
return html;
}
// 记忆类渲染函数
function renderUpdateMemory(result: any, args: any): string {
const operation = args.operation || result.operation || '';

View File

@ -3202,59 +3202,6 @@ export class MonitorDirector implements MonitorDriver {
await sleep(500);
};
this.sceneHandlers.runPython = async (payload, runtime) => {
const toolLabel = payload?.name || payload?.tool || 'run_python';
this.applySceneStatus(runtime, 'runPython', `调用 ${toolLabel}`);
const runId =
payload?.executionId ||
payload?.execution_id ||
payload?.id ||
`${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
// 丢弃过期的回放任务
if (this.latestPythonExecutionId && this.latestPythonExecutionId !== runId) {
const older =
typeof runId === 'number' && typeof this.latestPythonExecutionId === 'number'
? runId < this.latestPythonExecutionId
: false;
if (older) {
return;
}
}
this.latestPythonExecutionId = runId;
const code = payload?.arguments?.code || 'print("Hello")';
const codeLines = this.normalizeLines(code);
const animate = codeLines.length <= 15;
const runToken = ++this.pythonRunToken;
const reuse = this.isWindowVisible(this.elements.pythonWindow);
if (reuse) {
this.showWindow(this.elements.pythonWindow);
this.scrollPythonToTop();
await this.focusPythonInput();
this.elements.pythonOutput.innerHTML = '';
} else {
await this.revealPythonWindow('Python');
}
await this.typePythonCode(code, { deletePrevious: true, animate });
const completion = await runtime.waitForResult(payload.executionId || payload.id);
if (!this.ensureSuccessOrErrorBubble(completion, payload, 'Python 执行失败')) {
return;
}
if (runToken !== this.pythonRunToken) {
// 有新的 Python 运行已启动,本次结果丢弃
return;
}
const output = completion?.result?.output || completion?.result?.stdout || '>>> 执行完成';
const lines = this.sanitizeTerminalOutput(
typeof output === 'string'
? output.split('\n')
: Array.isArray(output)
? output.map(String)
: [String(output || '')]
);
this.appendPythonOutput('输出', lines.join('\n') || '>>> 执行完成');
await sleep(500);
};
this.sceneHandlers.reader = async (payload, runtime) => {
const targetPath = payload?.arguments?.path || payload?.result?.path || '文档';
const readMode = String(

View File

@ -12,7 +12,6 @@ export const SCENE_PROGRESS_LABELS: Record<string, string> = {
unfocus: '正在处理',
// 运行类工具显示具体工具名,由运行时传入
runCommand: '运行命令',
runPython: '运行 Python',
terminalSession: '打开终端',
terminalInput: '终端输入',
terminalSnapshot: '获取终端输出',

View File

@ -187,7 +187,7 @@ onMounted(() => {
const isEditPreview = (item: any) => item?.tool_name === 'edit_file' && item?.preview?.edit_context;
const isCommandPreview = (item: any) =>
['run_command', 'terminal_input', 'run_python', 'runpython'].includes(item?.tool_name);
['run_command', 'terminal_input'].includes(item?.tool_name);
const isWritePreview = (item: any) => item?.tool_name === 'write_file';
const isRenamePreview = (item: any) => item?.tool_name === 'rename_file';
const isPathOnlyPreview = (item: any) =>
@ -210,9 +210,7 @@ const getToolLabel = (toolName: string) => {
delete_file: '删除文件',
rename_file: '重命名文件',
write_file: '写入文件',
edit_file: '编辑文件',
run_python: '执行 Python',
runpython: '执行 Python'
edit_file: '编辑文件'
};
return map[name] || name || '待审批操作';
};

View File

@ -108,7 +108,6 @@ const TOOL_SCENE_MAP: Record<string, string> = {
write_file: 'modifyFile',
edit_file: 'modifyFile',
run_command: 'runCommand',
run_python: 'runPython',
terminal_session: 'terminalSession',
terminal_input: 'terminalInput',
terminal_snapshot: 'terminalSnapshot',

View File

@ -16,7 +16,6 @@ const RUNNING_ANIMATIONS: Record<string, string> = {
extract_webpage: 'search-animation',
save_webpage: 'file-animation',
vlm_analyze: 'file-animation',
run_python: 'code-animation',
run_command: 'terminal-animation',
update_memory: 'memory-animation',
sleep: 'wait-animation',
@ -40,7 +39,6 @@ const RUNNING_STATUS_TEXTS: Record<string, string> = {
web_search: '正在搜索网络...',
extract_webpage: '正在提取网页...',
save_webpage: '正在保存网页...',
run_python: '调用 run_python',
run_command: '调用 run_command',
update_memory: '正在更新记忆...',
terminal_session: '正在管理终端会话...',
@ -63,7 +61,6 @@ const COMPLETED_STATUS_TEXTS: Record<string, string> = {
extract_webpage: '网页提取完成',
save_webpage: '网页保存完成(纯文本)',
vlm_analyze: '图片解析完成',
run_python: '代码执行完成',
run_command: '命令执行完成',
update_memory: '记忆更新成功',
terminal_session: '终端操作完成',

View File

@ -67,7 +67,6 @@ export const TOOL_ICON_MAP = Object.freeze({
read_skill: 'book',
rename_file: 'pencil',
run_command: 'terminal',
run_python: 'python',
save_webpage: 'save',
sleep: 'clock',
todo_create: 'stickyNote',

View File

@ -68,7 +68,6 @@ AUTO_SHALLOW_TOOL_WHITELIST = {
"terminal_snapshot",
"web_search",
"extract_webpage",
"run_python",
"run_command",
"view_image",
"view_video",

View File

@ -816,14 +816,6 @@ def _format_run_command(result_data: Dict[str, Any]) -> str:
return text
def _format_run_python(result_data: Dict[str, Any]) -> str:
text = _plain_command_output(result_data)
if (result_data.get("status") or "").lower() == "timeout":
suggestion = "建议将代码保存为脚本后在持久终端中执行terminal_session + terminal_input或拆分/优化代码以缩短运行时间。"
text = f"{text}\n{suggestion}" if text else suggestion
return text
def _format_todo_create(result_data: Dict[str, Any]) -> str:
if not result_data.get("success"):
return _format_failure("todo_create", result_data)
@ -1191,7 +1183,6 @@ TOOL_FORMATTERS = {
"terminal_input": _format_terminal_input,
"sleep": _format_sleep,
"run_command": _format_run_command,
"run_python": _format_run_python,
"extract_webpage": _format_extract_webpage,
"vlm_analyze": _format_vlm_analyze,
"ocr_image": _format_ocr_image,