mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 08:31:59 +00:00
03c18c4d78
一、文案(前端约 1600 处,后端传到界面的约 880 处) - 术语统一:工作流层面用「运行」(继续运行、重新运行、试运行),节点和工具层面用「执行」,不再用「跑」; 总时长 / 执行时长 / 等待审批;服务端;助手;Agent;token;工作流;固定版本;模型接入;系统提示;批准 / 驳回;受管级别 - 口语改书面:取不到、没取回来、读不懂、连不上、多半、照样、兜底、揉成这类说法改成「无法 / 未能 / 失败 / 请」;去掉拟人写法 - 不再露出实现细节:界面上不写 checkpoint、同一拍、哈希(凭证处除外),不写改版历史(「以前…」「老数据」); 报错、校验、门禁、自动修复里裸写的配置键名和枚举值换成界面上的中文标签,由新的 backend/app/engine/labels.py 统一提供; 发布弹窗里结论句的选项不再是 off / require_citation - 同一件事只留一种说法:生成阶段文案、出具档位说明、画布锁定提示各合成一份 - 去掉界面上的工业场景示例(MES、ERP、「张工」「王工」)和真实产品名,换成通用示例 二、说错的提示(都有测试证明) - 数据库测连接失败时的专用提示以前从不出现(按报错文字全等比较,永远比不上):改为按错误类型判断,不再拼出重复的前缀 - 「设置 → 供应商」改指「设置 → 模型接入」;「在设置里建数据源」改指「数据」页 - 缺少 API Key、模型接入已停用的报错不再夹英文,写明去哪里填 - 两处原因说错(「这种引用在后续版本支持」「这一版还不支持」)改成真实原因 - 节点说明里不准确的说法改正:代码节点在 microVM 或系统沙箱中执行;报告撰写、口径卡的说明不再说过头 - 导出的证据 CSV 里两列同名,第三列改为「片段编号」 三、前后端对齐 - 前端按后端原文识别日志、报错的地方,新旧两种写法都认得(历史运行里存的是旧文字),逐条补了新旧原文的核对 - 后端交回模型的原指令放进单独的 for_model 字段,前端在解析响应时去掉,不会显示到界面上 四、检查 - 检查脚本里按原文匹配的断言改成新文案;改完会变成永远成立的反向断言逐条修正,并做了变异验证 - 新增 check-copy(静态扫描前端界面字符串和后端上界面的文案):再出现「跑」「后端」「多半」「令牌」、 界面上的 Copilot、裸写的配置键名、半角标点等就报错;确需保留的写进 scripts/copy-allow.json 并注明理由
110 lines
3.4 KiB
Python
110 lines
3.4 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app.core.config import settings
|
|
from app.tools.registry import ToolContext, register
|
|
|
|
_MAX_READ = 200_000
|
|
|
|
|
|
def _resolve(rel: str, ctx: ToolContext) -> Path:
|
|
"""把相对路径锁死在这次运行自己的工作目录里。
|
|
|
|
两道关都要过:session_dir() 先净化 session_id 再定根目录,然后 rel
|
|
先 resolve 再比对根。少了第一道,`../../etc/passwd` 里的越界检查就是
|
|
在一个被挪到 /etc 的"根"下面做的,检查照样通过——这正是之前的漏洞。
|
|
"""
|
|
root = settings.session_dir(ctx.sandbox_session)
|
|
root.mkdir(parents=True, exist_ok=True)
|
|
target = (root / rel.lstrip("/")).resolve()
|
|
if target != root and root not in target.parents:
|
|
raise ValueError(f"路径越界:{rel}")
|
|
return target
|
|
|
|
|
|
class FileReadArgs(BaseModel):
|
|
path: str = Field(description="相对工作目录的文件路径")
|
|
|
|
|
|
@register(
|
|
name="file_read",
|
|
category="文件",
|
|
description="读取工作目录下的文件内容。",
|
|
args_schema=FileReadArgs,
|
|
)
|
|
async def file_read(ctx: ToolContext, **kwargs: Any) -> dict[str, Any]:
|
|
args = FileReadArgs(**kwargs)
|
|
try:
|
|
target = _resolve(args.path, ctx)
|
|
except ValueError as e:
|
|
return {"error": str(e)}
|
|
if not target.exists():
|
|
return {"error": f"文件不存在:{args.path}"}
|
|
if target.is_dir():
|
|
return {"error": f"{args.path} 是目录,请使用 file_list"}
|
|
data = target.read_bytes()[:_MAX_READ]
|
|
return {
|
|
"path": args.path,
|
|
"size": target.stat().st_size,
|
|
"content": data.decode("utf-8", errors="replace"),
|
|
"truncated": target.stat().st_size > _MAX_READ,
|
|
}
|
|
|
|
|
|
class FileWriteArgs(BaseModel):
|
|
path: str = Field(description="相对工作目录的文件路径")
|
|
content: str = Field(description="要写入的内容")
|
|
append: bool = Field(default=False, description="追加而不是覆盖")
|
|
|
|
|
|
@register(
|
|
name="file_write",
|
|
category="文件",
|
|
description="写入文件到工作目录。父目录会自动创建。",
|
|
args_schema=FileWriteArgs,
|
|
dangerous=True,
|
|
)
|
|
async def file_write(ctx: ToolContext, **kwargs: Any) -> dict[str, Any]:
|
|
args = FileWriteArgs(**kwargs)
|
|
try:
|
|
target = _resolve(args.path, ctx)
|
|
except ValueError as e:
|
|
return {"error": str(e)}
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
with target.open("a" if args.append else "w", encoding="utf-8") as f:
|
|
f.write(args.content)
|
|
return {"path": args.path, "bytes": target.stat().st_size, "ok": True}
|
|
|
|
|
|
class FileListArgs(BaseModel):
|
|
path: str = Field(default=".", description="要列出的目录")
|
|
|
|
|
|
@register(
|
|
name="file_list",
|
|
category="文件",
|
|
description="列出工作目录下的文件。",
|
|
args_schema=FileListArgs,
|
|
)
|
|
async def file_list(ctx: ToolContext, **kwargs: Any) -> dict[str, Any]:
|
|
args = FileListArgs(**kwargs)
|
|
try:
|
|
target = _resolve(args.path, ctx)
|
|
except ValueError as e:
|
|
return {"error": str(e)}
|
|
if not target.exists():
|
|
return {"path": args.path, "entries": []}
|
|
entries = [
|
|
{
|
|
"name": p.name,
|
|
"type": "dir" if p.is_dir() else "file",
|
|
"size": p.stat().st_size if p.is_file() else None,
|
|
}
|
|
for p in sorted(target.iterdir())[:500]
|
|
]
|
|
return {"path": args.path, "entries": entries}
|