Compare commits
4 Commits
a1dd97d5f1
...
5723da8eb1
| Author | SHA1 | Date | |
|---|---|---|---|
| 5723da8eb1 | |||
| 180fb9bfbd | |||
| 78544cb205 | |||
| 4bf0733907 |
@ -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`: 网络搜索
|
||||
|
||||
@ -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="...")
|
||||
|
||||
@ -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 内对话记录展开面板顶部残留系统栏空白的问题,内容从面板顶端开始显示
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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/策略展示(并不等于“你一定能调用到该工具”,真实可用性仍取决于服务端策略与运行时环境)
|
||||
|
||||
|
||||
@ -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)`;
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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":
|
||||
|
||||
@ -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":
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -66,7 +66,7 @@ TOOL_CATEGORIES: Dict[str, ToolCategory] = {
|
||||
),
|
||||
"terminal_command": ToolCategory(
|
||||
label="终端指令",
|
||||
tools=["run_command", "run_python"],
|
||||
tools=["run_command"],
|
||||
),
|
||||
"memory": ToolCategory(
|
||||
label="记忆",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}"}
|
||||
|
||||
@ -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包
|
||||
|
||||
@ -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
7
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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` 确认。
|
||||
|
||||
|
||||
@ -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` 确认。
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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 ''}"
|
||||
|
||||
443
static/demo/minimal-tool-roller-demo.html
Normal file
443
static/demo/minimal-tool-roller-demo.html
Normal 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>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 || '';
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -12,7 +12,6 @@ export const SCENE_PROGRESS_LABELS: Record<string, string> = {
|
||||
unfocus: '正在处理',
|
||||
// 运行类工具显示具体工具名,由运行时传入
|
||||
runCommand: '运行命令',
|
||||
runPython: '运行 Python',
|
||||
terminalSession: '打开终端',
|
||||
terminalInput: '终端输入',
|
||||
terminalSnapshot: '获取终端输出',
|
||||
|
||||
@ -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 || '待审批操作';
|
||||
};
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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: '终端操作完成',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -68,7 +68,6 @@ AUTO_SHALLOW_TOOL_WHITELIST = {
|
||||
"terminal_snapshot",
|
||||
"web_search",
|
||||
"extract_webpage",
|
||||
"run_python",
|
||||
"run_command",
|
||||
"view_image",
|
||||
"view_video",
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user