mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 14:52:02 +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 并注明理由
184 lines
7.9 KiB
TypeScript
184 lines
7.9 KiB
TypeScript
/**
|
||
* FastAPI 的 422(请求没过 pydantic 校验)翻成人话。
|
||
*
|
||
* 422 的 detail 是 [{type, loc, msg, input, ctx}]:msg 是 pydantic 的英文原文,loc 是
|
||
* 请求体里的内部键名。原样拼出来就是「name:Field required」——表单大多先在前端
|
||
* 校验过,但 Copilot、画布这类只靠后端校验的路径会直接撞上。
|
||
*
|
||
* 这里按 type 翻成中文,loc 换成表单上的叫法;查不到叫法就写字段路径,没见过的
|
||
* type 写笼统说法。英文原文不丢:ApiError 的 detail 和 raw 里都在,界面收进「详情」。
|
||
* 输入值(input)一律不回显:密码、密钥也可能在里面。
|
||
*/
|
||
|
||
export const VALIDATION_TITLE = '提交的内容不符合要求'
|
||
|
||
export interface ValidationItem {
|
||
type?: string
|
||
loc?: (string | number)[]
|
||
msg?: string
|
||
ctx?: Record<string, unknown>
|
||
}
|
||
|
||
/** 多数表单上一样的叫法 */
|
||
const COMMON_LABELS: Record<string, string> = {
|
||
name: '名称',
|
||
description: '说明',
|
||
kind: '类型',
|
||
enabled: '启用',
|
||
title: '标题',
|
||
content: '内容',
|
||
tags: '标签',
|
||
}
|
||
|
||
/**
|
||
* 按接口覆盖:同一个键在不同表单上叫法不同(数据源的 name 是「标识」,
|
||
* 自定义工具的 description 是「描述」)。和页面上 <Field label> 的写法保持一致,
|
||
* 先匹配到的算数,所以更具体的路径写在前面
|
||
*/
|
||
const LABELS_BY_PATH: [RegExp, Record<string, string>][] = [
|
||
[/^\/(settings\/)?providers/, { base_url: 'Base URL', api_key: 'API Key', default_model: '默认模型', models: '可选模型' }],
|
||
[/^\/datasources\/upload/, { name: '数据源名', header_row: '表头在第几行', file: '文件' }],
|
||
[/^\/datasources/, {
|
||
name: '标识', host: '主机', port: '端口', database: '数据库', username: '用户名', password: '密码',
|
||
readonly: '只读', options: '高级参数',
|
||
}],
|
||
[/^\/custom-tools/, { description: '描述', parameters: '参数 JSON Schema', config: '配置' }],
|
||
[/^\/mcp/, { transport: '传输方式', command: '命令', args: '参数', env: '环境变量', url: 'URL' }],
|
||
[/^\/tools\//, { args: '参数', sandbox_session: '沙箱会话' }],
|
||
[/^\/memory/, { importance: '重要度', scope: '作用域' }],
|
||
[/^\/kb/, { collection: '知识库', file: '文件' }],
|
||
[/^\/skills/, { description: '一句话说明', instructions: '指令内容', examples: '示例', suggested_tools: '建议工具' }],
|
||
[/^\/workflows/, { graph: '工作流', note: '版本说明', level: '发布级别', version: '版本' }],
|
||
[/^\/copilot/, { instruction: '输入的内容', base_graph: '当前工作流', datasource_ids: '圈定的数据源', question: '问题' }],
|
||
[/^\/runs/, {
|
||
input: '输入', run_class: '运行类别', version: '版本', memory_scope: '记忆作用域', collection: '知识库',
|
||
workflow_id: '工作流', graph: '工作流',
|
||
}],
|
||
[/^\/conversations/, { question: '问题', archived: '归档' }],
|
||
[/^\/settings/, { values: '设置项' }],
|
||
]
|
||
|
||
function labelOf(key: string, path?: string): string | undefined {
|
||
const own = path ? LABELS_BY_PATH.find(([re]) => re.test(path))?.[1] : undefined
|
||
return own?.[key] ?? COMMON_LABELS[key]
|
||
}
|
||
|
||
/**
|
||
* loc → 「字段」。每一段都查得到叫法才换(数组下标写成「第 N 项」),有一段查不到
|
||
* 就整条写字段路径:半中半英的「工作流 › nodes › type」比路径还难认
|
||
*/
|
||
function fieldOf(loc: (string | number)[], path?: string): string {
|
||
const [where, ...rest] = loc
|
||
const segs = where === 'body' || where === 'query' || where === 'path' || where === 'header' || where === 'cookie'
|
||
? rest : loc
|
||
if (!segs.length) return ''
|
||
const names = segs.map((s) => (typeof s === 'number' ? `第 ${s + 1} 项` : labelOf(s, path)))
|
||
const field = names.every(Boolean) && typeof segs[0] === 'string'
|
||
? `「${names[0]}」${names.slice(1).join('')}`
|
||
: `「${segs.join('.')}」`
|
||
if (where === 'query') return `参数${field}`
|
||
if (where === 'path') return `地址里的${field}`
|
||
return field
|
||
}
|
||
|
||
const num = (v: unknown) => (typeof v === 'number' ? String(v) : String(v ?? ''))
|
||
|
||
/** ctx.expected:「'a', 'b' or 'c'」或「1, 2 or 3」→ ['a', 'b', 'c'] */
|
||
function choicesOf(expected: unknown): string[] {
|
||
if (typeof expected !== 'string' || !expected.trim()) return []
|
||
const quoted = [...expected.matchAll(/'((?:[^'\\]|\\.)*)'/g)].map((m) => m[1])
|
||
return quoted.length ? quoted : expected.split(/,\s*|\s+or\s+/).map((s) => s.trim()).filter(Boolean)
|
||
}
|
||
|
||
const joinChoices = (xs: string[]) => (xs.length > 1 ? `${xs.slice(0, -1).join('、')} 或 ${xs[xs.length - 1]}` : xs[0])
|
||
|
||
/** 自定义校验器(value_error / assertion_error)的话是后端写的:写的是中文就照用 */
|
||
function customMessage(msg: string | undefined): string | undefined {
|
||
const text = (msg ?? '').replace(/^(?:Value error|Assertion failed),\s*/, '').trim()
|
||
return /[一-鿿]/.test(text) ? text : undefined
|
||
}
|
||
|
||
/** 一条:「字段」+ 怎么了。field 为空(整个请求体)时各自单说 */
|
||
function describeOne(d: ValidationItem, path?: string): string {
|
||
const type = d.type ?? ''
|
||
const ctx = d.ctx ?? {}
|
||
if (type === 'json_invalid') return '提交的内容不是合法的 JSON'
|
||
const field = fieldOf(Array.isArray(d.loc) ? d.loc : [], path)
|
||
if (!field && type === 'missing') return '未收到提交的内容'
|
||
const f = field || '提交的内容'
|
||
switch (type) {
|
||
case 'missing':
|
||
return `${f}为必填项`
|
||
case 'string_too_short':
|
||
return Number(ctx.min_length) <= 1 ? `${f}不能为空` : `${f}过短,至少 ${num(ctx.min_length)} 个字符`
|
||
case 'string_too_long':
|
||
return `${f}过长,最多 ${num(ctx.max_length)} 个字符`
|
||
case 'too_short':
|
||
return `${f}至少需要 ${num(ctx.min_length)} 项`
|
||
case 'too_long':
|
||
return `${f}最多 ${num(ctx.max_length)} 项`
|
||
case 'string_pattern_mismatch':
|
||
return `${f}格式不正确`
|
||
case 'string_type':
|
||
return `${f}应为文本`
|
||
case 'int_parsing':
|
||
case 'int_type':
|
||
case 'int_from_float':
|
||
return `${f}应为整数`
|
||
case 'float_parsing':
|
||
case 'float_type':
|
||
case 'decimal_parsing':
|
||
case 'decimal_type':
|
||
return `${f}应为数字`
|
||
case 'bool_parsing':
|
||
case 'bool_type':
|
||
return `${f}只能是「是」或「否」`
|
||
case 'greater_than':
|
||
return `${f}应大于 ${num(ctx.gt)}`
|
||
case 'greater_than_equal':
|
||
return `${f}不能小于 ${num(ctx.ge)}`
|
||
case 'less_than':
|
||
return `${f}应小于 ${num(ctx.lt)}`
|
||
case 'less_than_equal':
|
||
return `${f}不能大于 ${num(ctx.le)}`
|
||
case 'literal_error':
|
||
case 'enum': {
|
||
const xs = choicesOf(ctx.expected)
|
||
// 列太长就不列了:一长串内部枚举值对填表的人没有帮助
|
||
return xs.length && xs.length <= 6 ? `${f}只能取 ${joinChoices(xs)}` : `${f}不在允许的取值范围内`
|
||
}
|
||
case 'list_type':
|
||
case 'tuple_type':
|
||
case 'set_type':
|
||
return `${f}应为列表`
|
||
case 'dict_type':
|
||
case 'model_type':
|
||
case 'model_attributes_type':
|
||
return `${f}应为键值对象`
|
||
case 'url_parsing':
|
||
case 'url_type':
|
||
case 'url_scheme':
|
||
return `${f}不是合法的网址`
|
||
case 'date_parsing':
|
||
case 'date_type':
|
||
case 'datetime_parsing':
|
||
case 'datetime_type':
|
||
case 'time_parsing':
|
||
return `${f}不是合法的日期或时间`
|
||
case 'extra_forbidden':
|
||
return `${f}不是可接受的字段`
|
||
case 'value_error':
|
||
case 'assertion_error': {
|
||
const said = customMessage(d.msg)
|
||
return said ? (field ? `${field}:${said}` : said) : `${f}取值无效`
|
||
}
|
||
default:
|
||
return `${f}不符合要求`
|
||
}
|
||
}
|
||
|
||
/** 422 的 detail 数组 → 一条条中文。path 是请求路径(不带 /api),用来查表单叫法 */
|
||
export function describeValidation(items: unknown[], path?: string): string[] {
|
||
return items.map((d) => (d && typeof d === 'object' ? describeOne(d as ValidationItem, path) : '有一项不符合要求'))
|
||
}
|