feat: refresh model and voice test catalog with favicon

This commit is contained in:
AdamWen95
2026-10-20 11:06:52 +08:00
parent ae7c5bc967
commit 36c228544e
23 changed files with 1026 additions and 42 deletions
+4 -3
View File
@@ -1,6 +1,6 @@
# XAS 测试脚本控制台
核对日期:2026-10-13。当前采用专用网页控制台,替代前期 Rundeck 作为日常入口。
核对日期:2026-10-20。当前采用专用网页控制台,替代前期 Rundeck 作为日常入口。
访问 [公司内网测试控制台](http://10.88.118.188:14440/),无需账号密码。
@@ -9,9 +9,9 @@
- 运行:配置参数后排队执行,查看实时标准输出/错误及登记的 .log 内容。
- 记录:查看状态、版本、配置快照、历史日志和归档结果,预览或下载报告。
当前整理为 23 项业务测试(8 项套件、15 项单项测试),关联原先登记的 307 个源文件。22 项原位置入口已接入原环境执行,保留原目录、解释器、配置和输出行为;普通测试预检通过后提交,资源占用/服务启停测试每次显示影响并确认。两个控制台验收样例默认隐藏,可在“项目用途”中选择查看。原文件、GPU/端口设置和历史记录均保留。
当前整理为 28 项业务测试(8 项套件、20 项单项测试),关联 320 个原位置源文件,另有 3 个托管脚本。25 项已配置启用,3 项新增语音测试因服务预设、管理凭据或辅助服务尚未齐备而标为待配置。原环境执行保留原入口;语音识别和网关回归复用原依赖镜像,源目录只读、输出独立归档。资源占用/服务启停测试每次显示影响并确认,两个控制台验收样例默认隐藏。
2026-10-13 23:27 只读预检:16 个原环境方案满足登记的启动条件,6 个因所需模型端口未监听而被拦截。预检通过不等于业务测试通过,也不预留 GPU。GPU 综合套件支持菜单数字、y/n/q 和回车;需要自由文本的维护功能仍从原维护入口使用。本轮仅用独立短样例验证执行链路,没有运行原 GPU/长稳测试。
2026-10-20 核对:GPU 综合套件仍为 v3.5.0。DeepSeek 当前主名为 `deepseek-v4.1-flash-1017`(18100,仍保留旧别名),新增可选目标 Qwen3.8-27B(GPU 14,18112);这两个目标在运行前会只读核对模型目录。预检通过不等于业务测试通过,也不预留 GPU。GPU 综合套件支持菜单数字、y/n/q 和回车;自由文本维护仍从原入口使用。本轮没有执行原模型、语音或长稳业务测试。
上传版本保存在服务器 `/adamwen-work-space/xas-test-library/runtime/library/`,日志和结果在 `runtime/runs/`,SQLite 在 `runtime/console-data/`,仅记录元数据、状态与文件引用。外部结果原文件保留,同时可归档本次变更;Git 不参与每次运行时拉取。
@@ -22,6 +22,7 @@
- [本轮验收记录](docs/acceptance-dedicated-2026-10-13.md)
- [按测试行为整理后的验收](docs/acceptance-behavior-catalog-2026-10-13.md)
- [原环境接入与本轮验收](docs/acceptance-native-2026-10-13.md)
- [2026-10-20 模型与测试目录更新](docs/acceptance-refresh-2026-10-20.md)
- [原环境执行架构决策](docs/adr/0004-native-execution.md)
- [专用控制台架构决策](docs/adr/0002-dedicated-console.md)
- [按测试行为组织目录的规则](docs/adr/0003-test-behavior-catalog.md)
+17 -4
View File
@@ -1,10 +1,23 @@
{
"schema_version": 1,
"observed_at": "2026-10-13T17:19:29+08:00",
"observed_at": "2026-10-20T09:45:59+08:00",
"evidence": "Dedicated console inventories source files read-only; execution readiness is managed per script in SQLite",
"sources": [
{"id": "company-gpu-suite", "path": "/XAS/SH/XAS-GPU-Test/XAS-GPU-Test-v3.5.0"},
{"id": "dsv41-trial-20261010", "path": "/dengyuyun-joy-work-space/workpc-claude-code/20261010-dsv41-trial/scripts"},
{"id": "model-trials-20261011", "path": "/dengyuyun-joy-work-space/workpc-claude-code/20261011-model-trials/scripts"}
{
"id": "company-gpu-suite",
"path": "/XAS/SH/XAS-GPU-Test/XAS-GPU-Test-v3.5.0"
},
{
"id": "dsv41-trial-20261010",
"path": "/dengyuyun-joy-work-space/workpc-claude-code/20261010-dsv41-trial/scripts"
},
{
"id": "model-trials-20261011",
"path": "/dengyuyun-joy-work-space/workpc-claude-code/20261011-model-trials/scripts"
},
{
"id": "drama3-tests-20261015",
"path": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/tests"
}
]
}
+376 -21
View File
@@ -3,9 +3,9 @@
"targets": [
{
"id": "deepseek-a",
"label": "DeepSeek A(原服务,18100)",
"label": "DeepSeek V4.1 Flash 1017(GPU 0–7 · 18100)",
"url": "http://127.0.0.1:18100",
"model": "deepseek-v4.1-flash",
"model": "deepseek-v4.1-flash-1017",
"environment": {
"CT_OFF": "{\"thinking\": false}",
"CT_ON": "{\"thinking\": true}",
@@ -13,7 +13,23 @@
},
"credential_files": [
"/dengyuyun-joy-work-space/workpc-claude-code/20261010-dsv41-trial/configs/vllm.env"
]
],
"verify_model": true
},
{
"id": "qwen38-g14",
"label": "Qwen3.8-27B(GPU 14 · 18112)",
"url": "http://127.0.0.1:18112",
"model": "qwen3.8-27b",
"environment": {
"CT_OFF": "{\"enable_thinking\": false}",
"CT_ON": "{\"enable_thinking\": true}",
"MAX_LEN": "262144"
},
"credential_files": [
"/dengyuyun-joy-work-space/workpc-claude-code/20261011-model-trials/configs/vllm.env"
],
"verify_model": true
},
{
"id": "dsv41cap",
@@ -282,7 +298,8 @@
"2026-10-13 只读核对原源码;启动方案登记不代表本轮业务测试通过。",
"原 config.env 的 DATA_ROOT=/XAS/GPUTest。只打开原菜单,不自动输入或选择任何测试项目。",
"原工具创建的 tmux 持久任务可能脱离本次控制台 cgroup;平台取消后必须核查原工具会话和模型容器,不能承诺全部停止。",
"runs/reports 使用原工具的时间戳产物;只收集本次运行新增或发生变化的文件,不预拷整个历史 runs/reports。"
"runs/reports 使用原工具的时间戳产物;只收集本次运行新增或发生变化的文件,不预拷整个历史 runs/reports。",
"2026-10-20:服务器最高已安装版本仍为 v3.5.0,已登记的 224 个源码文件摘要未变;当前 GPU 0–14 已分配给模型/图像/语音服务。"
],
"available": true
},
@@ -521,7 +538,11 @@
"options": [
{
"value": "deepseek-a",
"label": "DeepSeek A(原服务,18100)"
"label": "DeepSeek V4.1 Flash 1017(GPU 0–7 · 18100)"
},
{
"value": "qwen38-g14",
"label": "Qwen3.8-27B(GPU 14 · 18112)"
},
{
"value": "dsv41cap",
@@ -631,7 +652,11 @@
"options": [
{
"value": "deepseek-a",
"label": "DeepSeek A(原服务,18100)"
"label": "DeepSeek V4.1 Flash 1017(GPU 0–7 · 18100)"
},
{
"value": "qwen38-g14",
"label": "Qwen3.8-27B(GPU 14 · 18112)"
},
{
"value": "dsv41cap",
@@ -784,7 +809,11 @@
"options": [
{
"value": "deepseek-a",
"label": "DeepSeek A(原服务,18100)"
"label": "DeepSeek V4.1 Flash 1017(GPU 0–7 · 18100)"
},
{
"value": "qwen38-g14",
"label": "Qwen3.8-27B(GPU 14 · 18112)"
},
{
"value": "dsv41cap",
@@ -923,7 +952,11 @@
"options": [
{
"value": "deepseek-a",
"label": "DeepSeek A(原服务,18100)"
"label": "DeepSeek V4.1 Flash 1017(GPU 0–7 · 18100)"
},
{
"value": "qwen38-g14",
"label": "Qwen3.8-27B(GPU 14 · 18112)"
},
{
"value": "dsv41cap",
@@ -1032,7 +1065,11 @@
"options": [
{
"value": "deepseek-a",
"label": "DeepSeek A(原服务,18100)"
"label": "DeepSeek V4.1 Flash 1017(GPU 0–7 · 18100)"
},
{
"value": "qwen38-g14",
"label": "Qwen3.8-27B(GPU 14 · 18112)"
},
{
"value": "dsv41cap",
@@ -1169,7 +1206,11 @@
"options": [
{
"value": "deepseek-a",
"label": "DeepSeek A(原服务,18100)"
"label": "DeepSeek V4.1 Flash 1017(GPU 0–7 · 18100)"
},
{
"value": "qwen38-g14",
"label": "Qwen3.8-27B(GPU 14 · 18112)"
},
{
"value": "dsv41cap",
@@ -1298,7 +1339,11 @@
"options": [
{
"value": "deepseek-a",
"label": "DeepSeek A(原服务,18100)"
"label": "DeepSeek V4.1 Flash 1017(GPU 0–7 · 18100)"
},
{
"value": "qwen38-g14",
"label": "Qwen3.8-27B(GPU 14 · 18112)"
},
{
"value": "dsv41cap",
@@ -1426,7 +1471,11 @@
"options": [
{
"value": "deepseek-a",
"label": "DeepSeek A(原服务,18100)"
"label": "DeepSeek V4.1 Flash 1017(GPU 0–7 · 18100)"
},
{
"value": "qwen38-g14",
"label": "Qwen3.8-27B(GPU 14 · 18112)"
},
{
"value": "dsv41cap",
@@ -1546,7 +1595,11 @@
"options": [
{
"value": "deepseek-a",
"label": "DeepSeek A(原服务,18100)"
"label": "DeepSeek V4.1 Flash 1017(GPU 0–7 · 18100)"
},
{
"value": "qwen38-g14",
"label": "Qwen3.8-27B(GPU 14 · 18112)"
},
{
"value": "dsv41cap",
@@ -1855,7 +1908,7 @@
"options": [
{
"value": "deepseek-a",
"label": "DeepSeek A(原服务,18100)"
"label": "DeepSeek V4.1 Flash 1017(GPU 0–7 · 18100)"
}
],
"required": true,
@@ -1911,7 +1964,8 @@
"2026-10-13 只读核对原源码;启动方案登记不代表本轮业务测试通过。",
"通过所选目标的原认证文件在运行时加载 VLLM_API_KEY;不在参数或日志中显示值。",
"原 CLI 的数字参数为必填;表单初值为控制台建议值,不冒充原脚本默认值。",
"原脚本硬编码 DeepSeek 模型和 18100 地址;本方案固定目标 A。"
"原脚本硬编码 DeepSeek 模型和 18100 地址;本方案固定目标 A。",
"2026-10-20:当前 DeepSeek 服务主名为 deepseek-v4.1-flash-1017;元数据接口仍提供 deepseek-v4.1-flash 兼容别名,原脚本保持不变。"
],
"available": true
},
@@ -1935,7 +1989,7 @@
"options": [
{
"value": "deepseek-a",
"label": "DeepSeek A(原服务,18100)"
"label": "DeepSeek V4.1 Flash 1017(GPU 0–7 · 18100)"
}
],
"required": true,
@@ -1975,7 +2029,8 @@
"2026-10-13 只读核对原源码;启动方案登记不代表本轮业务测试通过。",
"通过所选目标的原认证文件在运行时加载 VLLM_API_KEY;不在参数或日志中显示值。",
"复用原 run-d7v-d8.sh 引用的 test-34-vision-input.png;本轮仅核对文件名及大小,未读取或复制图像内容。",
"原入口分别发送关闭/开启 thinking 的两次看图请求;目标服务需已启用视觉。未自动修改或重启模型。"
"原入口分别发送关闭/开启 thinking 的两次看图请求;目标服务需已启用视觉。未自动修改或重启模型。",
"2026-10-20:当前 DeepSeek 服务主名为 deepseek-v4.1-flash-1017;元数据接口仍提供 deepseek-v4.1-flash 兼容别名,原脚本保持不变。"
],
"available": true
},
@@ -1998,7 +2053,11 @@
"options": [
{
"value": "deepseek-a",
"label": "DeepSeek A(原服务,18100)"
"label": "DeepSeek V4.1 Flash 1017(GPU 0–7 · 18100)"
},
{
"value": "qwen38-g14",
"label": "Qwen3.8-27B(GPU 14 · 18112)"
},
{
"value": "dsv41cap",
@@ -2135,7 +2194,11 @@
"options": [
{
"value": "deepseek-a",
"label": "DeepSeek A(原服务,18100)"
"label": "DeepSeek V4.1 Flash 1017(GPU 0–7 · 18100)"
},
{
"value": "qwen38-g14",
"label": "Qwen3.8-27B(GPU 14 · 18112)"
},
{
"value": "dsv41cap",
@@ -2675,7 +2738,8 @@
"details": [
"原链先使用 GPU 8–15,随后停止 DeepSeek A,使用 GPU 0–15,启停多套模型及 16 个 Qwen 实例。",
"原 EXIT trap 会尝试按 start-rec.sh 重启 A;失败、超时或取消不能保证恢复原状态。",
"原日志和结果名称固定,可能覆盖已有 dsv41cap、glm53、mimo、qwen38fp8tp8、qwen38fp8cap 结果;需逐次确认并保全历史文件。"
"原日志和结果名称固定,可能覆盖已有 dsv41cap、glm53、mimo、qwen38fp8tp8、qwen38fp8cap 结果;需逐次确认并保全历史文件。",
"2026-10-20 核对:原链末尾 start-rec.sh 默认切回 UNCENSORED 版,不会恢复当前 1017 主模型。GPU 8–14 另有图像/语音/Qwen 服务,执行前须协调资源。"
],
"requires_confirmation": true
},
@@ -2762,7 +2826,8 @@
"details": [
"原链等待 cap-8x-chain.sh 退出,再检查 GPU 8–15 显存并启动/停止 DeepSeek 与 8 份 Qwen 模型。",
"原结果使用固定 dsv41v-*、qwen38v-* 名称,可能覆盖历史结果。",
"该入口产生本机性能证据,成本折算和报告仍按原配套脚本处理;不代表本轮已验证价格口径。"
"该入口产生本机性能证据,成本折算和报告仍按原配套脚本处理;不代表本轮已验证价格口径。",
"2026-10-20 核对:GPU 8–14 已有图像/语音/Qwen 服务;原测试会在这些 GPU 启动模型,须先协调资源,控制台不自动停止这些新服务。"
],
"requires_confirmation": true
},
@@ -2803,6 +2868,296 @@
"原入口无位置参数;不伪造可配置的编号或输出路径。原链使用固定历史文件名,再次运行可能覆盖同名结果,应在确认前检查并保全相关历史文件。"
],
"available": true
},
{
"id": "drama3-asr",
"source_group": "drama3-tests-20261015",
"entry": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/tests/test_asr.py",
"cwd": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3",
"interpreter": "/opt/omni/bin/python",
"user": "root",
"parameters": [],
"arguments": [],
"environment": {},
"credential_files": [],
"required_files": [
"/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/tests/test_asr.py"
],
"check_ports": [
18301,
18302
],
"timeout_seconds": 3600,
"impact": {
"level": "resource",
"title": "语音识别文件与实时转写会占用共享模型服务",
"details": [
"用现有 Drama3 合成三段录音,核验 Qwen3-ASR 文件转写、实时转写、字错率和响应延迟;会占用共享语音服务。",
"生成的音频与标准输出存入本次运行独立目录,原测试目录和历史结果保留。"
],
"requires_confirmation": true
},
"available": true,
"output_paths": [],
"notes": [
"2026-10-20 已只读核对原源码、依赖镜像和素材,未执行本项业务测试。",
"原脚本只读挂载;测试客户端使用原镜像,无直接 GPU 设备权限,2 CPU / 4 GiB 内存;会通过接口占用既有模型服务。输出使用本次独立目录,不覆盖原历史音频。"
],
"interactive": false,
"docker": {
"image": "sha256:13369a516b4b961e36b1c3c5d47e3e04ec4bfdc03920964c621ccc83232604b8",
"mounts": [
{
"source": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/tests",
"target": "/t"
}
]
}
},
{
"id": "drama3-api",
"source_group": "drama3-tests-20261015",
"entry": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/tests/test_api.py",
"cwd": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3",
"interpreter": "/opt/omni/bin/python",
"user": "root",
"parameters": [],
"arguments": [],
"environment": {},
"credential_files": [],
"required_files": [
"/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/tests/test_api.py",
"/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/secrets/api/keys.txt",
"/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/logs/api-requests.log",
"/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/data/voices-src/female-1.wav",
"/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/data/voices-src/female-2.wav",
"/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/data/voices-src/male-1.wav",
"/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/data/voices-src/male-2.wav"
],
"check_ports": [
18300,
18301
],
"timeout_seconds": 3600,
"impact": {
"level": "resource",
"title": "语音合成网关兼容性与质量回归会占用共享模型服务",
"details": [
"核验本地语音网关的多格式、四种音色、流式、变速、SDK 兼容和八路并发;保留原 selftest 凭据读取方式。",
"生成的音频与标准输出存入本次运行独立目录,原测试目录和历史结果保留。"
],
"requires_confirmation": true
},
"available": true,
"output_paths": [],
"notes": [
"2026-10-20 已只读核对原源码、依赖镜像和素材,未执行本项业务测试。",
"原脚本只读挂载;测试客户端使用原镜像,无直接 GPU 设备权限,2 CPU / 4 GiB 内存;会通过接口占用既有模型服务。输出使用本次独立目录,不覆盖原历史音频。",
"复用原 selftest 身份;只读检查所需日志是否存在,其内容会持续变化,不把日志变化当作启动配置变更。"
],
"interactive": false,
"docker": {
"image": "sha256:13369a516b4b961e36b1c3c5d47e3e04ec4bfdc03920964c621ccc83232604b8",
"mounts": [
{
"source": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/tests",
"target": "/t"
},
{
"source": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/secrets/api",
"target": "/secrets"
},
{
"source": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/data/voices-src",
"target": "/refs"
},
{
"source": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/logs",
"target": "/logs"
}
]
},
"secret_tables": [
{
"path": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/secrets/api/keys.txt",
"key_column": 1,
"label": "selftest"
}
],
"mutable_files": [
"/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/logs/api-requests.log"
]
},
{
"id": "drama3-voice-chat",
"source_group": "drama3-tests-20261015",
"entry": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/tests/test_voice_chat.py",
"cwd": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3",
"interpreter": "/opt/omni/bin/python",
"user": "root",
"parameters": [],
"arguments": [],
"environment": {},
"credential_files": [],
"required_files": [
"/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/tests/test_voice_chat.py",
"/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/secrets/api/keys.txt",
"/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/logs/api-requests.log"
],
"check_ports": [
18300,
18301,
18302,
18343,
18100
],
"timeout_seconds": 3600,
"impact": {
"level": "resource",
"title": "实时语音对话端到端回归会占用共享模型服务",
"details": [
"核验输入、回答、打断和四人对话的完整链路;当前语音服务预设仍指向旧 DeepSeek 模型名,待服务方确认后再开放。",
"生成的音频与标准输出存入本次运行独立目录,原测试目录和历史结果保留。"
],
"requires_confirmation": true
},
"available": false,
"output_paths": [],
"notes": [
"2026-10-20 已只读核对原源码、依赖镜像和素材,未执行本项业务测试。",
"原脚本只读挂载;测试客户端使用原镜像,无直接 GPU 设备权限,2 CPU / 4 GiB 内存;会通过接口占用既有模型服务。输出使用本次独立目录,不覆盖原历史音频。",
"复用原 selftest 身份;只读检查所需日志是否存在,其内容会持续变化,不把日志变化当作启动配置变更。"
],
"interactive": false,
"docker": {
"image": "sha256:13369a516b4b961e36b1c3c5d47e3e04ec4bfdc03920964c621ccc83232604b8",
"mounts": [
{
"source": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/tests",
"target": "/t"
},
{
"source": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/secrets/api",
"target": "/secrets"
},
{
"source": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/logs",
"target": "/logs"
}
]
},
"unavailable_reason": "原语音服务的 DeepSeek 预设仍指向 deepseek-v4.1-flash-uncensored,与当前模型目录不一致;须由服务维护者调整预设后再接入。",
"secret_tables": [
{
"path": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/secrets/api/keys.txt",
"key_column": 1,
"label": "selftest"
}
],
"mutable_files": [
"/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/logs/api-requests.log"
]
},
{
"id": "drama3-engine",
"source_group": "drama3-tests-20261015",
"entry": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/tests/test_drama3.py",
"cwd": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3",
"interpreter": "/opt/omni/bin/python",
"user": "root",
"parameters": [],
"arguments": [],
"environment": {},
"credential_files": [],
"required_files": [
"/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/tests/test_drama3.py",
"/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/secrets/admin.env",
"/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/data/refs/example-en.wav"
],
"check_ports": [
18301
],
"timeout_seconds": 3600,
"impact": {
"level": "resource",
"title": "Drama3 引擎合成与流式回归会占用共享模型服务",
"details": [
"核验 Drama3 引擎的合成、克隆、流式和并发;依赖原管理凭据及外网参考音频下载,暂未接通执行。",
"生成的音频与标准输出存入本次运行独立目录,原测试目录和历史结果保留。"
],
"requires_confirmation": true
},
"available": false,
"output_paths": [],
"notes": [
"2026-10-20 已只读核对原源码、依赖镜像和素材,未执行本项业务测试。",
"原脚本只读挂载;测试客户端使用原镜像,无直接 GPU 设备权限,2 CPU / 4 GiB 内存;会通过接口占用既有模型服务。输出使用本次独立目录,不覆盖原历史音频。"
],
"interactive": false,
"docker": {
"image": "sha256:13369a516b4b961e36b1c3c5d47e3e04ec4bfdc03920964c621ccc83232604b8",
"mounts": [
{
"source": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/tests",
"target": "/t"
}
]
},
"unavailable_reason": "此原测试还需管理凭据适配及外网参考音频下载,尚未核对其调用与输出边界。"
},
{
"id": "drama3-voice-capacity",
"source_group": "drama3-tests-20261015",
"entry": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/tests/load_voice.py",
"cwd": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3",
"interpreter": "/opt/omni/bin/python",
"user": "root",
"parameters": [],
"arguments": [],
"environment": {},
"credential_files": [],
"required_files": [
"/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/tests/load_voice.py",
"/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/secrets/api/keys.txt",
"/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/tests/mock_brain.py"
],
"check_ports": [
18300,
18301,
18302
],
"timeout_seconds": 3600,
"impact": {
"level": "resource",
"title": "语音链路并发容量测试会占用共享模型服务",
"details": [
"用模拟大脑测语音链路的并发容量、首声延迟和播放卡顿;需补齐临时辅助容器生命周期与每次资源上限。",
"生成的音频与标准输出存入本次运行独立目录,原测试目录和历史结果保留。"
],
"requires_confirmation": true
},
"available": false,
"output_paths": [],
"notes": [
"2026-10-20 已只读核对原源码、依赖镜像和素材,未执行本项业务测试。",
"原脚本只读挂载;测试客户端使用原镜像,无直接 GPU 设备权限,2 CPU / 4 GiB 内存;会通过接口占用既有模型服务。输出使用本次独立目录,不覆盖原历史音频。"
],
"interactive": false,
"docker": {
"image": "sha256:13369a516b4b961e36b1c3c5d47e3e04ec4bfdc03920964c621ccc83232604b8",
"mounts": [
{
"source": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/tests",
"target": "/t"
},
{
"source": "/dengyuyun-joy-work-space/workpc-claude-code/20261015-drama3/secrets/api",
"target": "/secrets"
}
]
},
"unavailable_reason": "需要专用模拟大脑容器、原内部网络及并发上限;辅助服务生命周期尚未接入。"
}
],
"reviewed_at": "2026-10-20"
}
+65 -1
View File
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"reviewed_at": "2026-10-13",
"reviewed_at": "2026-10-20",
"basis": "Reviewed README entry points, script purpose comments and call references; registration does not authorize service/GPU actions.",
"example_script_ids": [
"499b615a7dc7435eb30789b7a7dc090e",
@@ -72,6 +72,11 @@
"source_group": "dsv41-trial-20261010",
"pattern": "run-d456.sh",
"role": "variant"
},
{
"source_group": "dsv41-trial-20261010",
"pattern": "run-unc*.sh",
"role": "variant"
}
]
},
@@ -569,6 +574,65 @@
"role": "variant"
}
]
},
{
"key": "drama3-asr",
"source_group": "drama3-tests-20261015",
"entry": "test_asr.py",
"name": "语音识别文件与实时转写",
"description": "用现有 Drama3 合成三段录音,核验 Qwen3-ASR 文件转写、实时转写、字错率和响应延迟;会占用共享语音服务。",
"kind": "suite",
"members": []
},
{
"key": "drama3-api",
"source_group": "drama3-tests-20261015",
"entry": "test_api.py",
"name": "语音合成网关兼容性与质量回归",
"description": "核验本地语音网关的多格式、四种音色、流式、变速、SDK 兼容和八路并发;保留原 selftest 凭据读取方式。",
"kind": "suite",
"members": []
},
{
"key": "drama3-voice-chat",
"source_group": "drama3-tests-20261015",
"entry": "test_voice_chat.py",
"name": "实时语音对话端到端回归",
"description": "核验输入、回答、打断和四人对话的完整链路;当前语音服务预设仍指向旧 DeepSeek 模型名,待服务方确认后再开放。",
"kind": "suite",
"members": [
{
"source_group": "drama3-tests-20261015",
"pattern": "test_voice_logic.py"
}
]
},
{
"key": "drama3-engine",
"source_group": "drama3-tests-20261015",
"entry": "test_drama3.py",
"name": "Drama3 引擎合成与流式回归",
"description": "核验 Drama3 引擎的合成、克隆、流式和并发;依赖原管理凭据及外网参考音频下载,暂未接通执行。",
"kind": "suite",
"members": []
},
{
"key": "drama3-voice-capacity",
"source_group": "drama3-tests-20261015",
"entry": "load_voice.py",
"name": "语音链路并发容量测试",
"description": "用模拟大脑测语音链路的并发容量、首声延迟和播放卡顿;需补齐临时辅助容器生命周期与每次资源上限。",
"kind": "suite",
"members": [
{
"source_group": "drama3-tests-20261015",
"pattern": "mock_brain.py"
},
{
"source_group": "drama3-tests-20261015",
"pattern": "load_summary.py"
}
]
}
]
}
+3
View File
@@ -180,6 +180,9 @@ def discover(store):
item["version"] += 1
item["enabled"] = False
reason = "源文件已变动,请重新确认配置并启用"
elif before and digest and not before["enabled"] and before.get("reason") == "源文件已变动,请重新确认配置并启用":
# A second scan must not clear the outstanding source-change review.
reason = before["reason"]
if blocked:
item["enabled"] = False
item.update(sha256=digest, blocked=blocked, reason="" if item["enabled"] else reason,
+54
View File
@@ -0,0 +1,54 @@
"""One bounded, read-only metadata request to a registered loopback model service."""
import ipaddress
import json
from pathlib import Path
import re
import shlex
import urllib.error
import urllib.request
from urllib.parse import urlsplit
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *args):
return None
def check_model(target, credential_files):
result = {'name': '模型身份', 'ok': False, 'message': '模型目录检查未完成'}
try:
url = urlsplit(target['url'])
if (url.scheme != 'http' or not ipaddress.ip_address(url.hostname).is_loopback
or url.username or url.password or url.query or url.fragment or url.path not in ('', '/')):
raise ValueError('仅允许登记的本机模型服务')
key = ''
for filename in credential_files:
path = Path(filename)
if path.stat().st_size > 262144:
raise ValueError('认证引用文件过大')
for line in path.read_text().splitlines():
match = re.match(r'^(?:export\s+)?VLLM_API_KEY=(.*)$', line.strip())
if not match:
continue
values = shlex.split(match[1], comments=True)
if len(values) > 1:
raise ValueError('认证引用格式不支持')
key = values[0] if values else ''
headers = {'Authorization': 'Bearer ' + key} if key else {}
request = urllib.request.Request(target['url'].rstrip('/') + '/v1/models', headers=headers)
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), NoRedirect())
with opener.open(request, timeout=3) as response:
data = response.read(262145)
if len(data) > 262144:
raise ValueError('模型目录超过检查上限')
payload = json.loads(data)
models = payload.get('data') if isinstance(payload, dict) else None
if not isinstance(models, list) or any(not isinstance(m, dict) or not isinstance(m.get('id'), str) for m in models):
raise ValueError('模型目录格式不支持')
result['ok'] = any(m['id'] == target['model'] for m in models)
result['message'] = ('已核对模型名称:' if result['ok'] else '当前服务未提供登记的模型名称:') + target['model']
except urllib.error.HTTPError as exc:
result['message'] = f'模型目录校验失败(HTTP {exc.code}),请核对服务与认证配置;未发送推理'
except (OSError, ValueError, TypeError, KeyError):
result['message'] = '模型目录或认证引用不可用,未发送推理;请核对原服务配置'
return result
+89
View File
@@ -0,0 +1,89 @@
"""Fixed original-image clients. No user-provided Docker flags or GPU access."""
import json
from pathlib import Path, PurePosixPath
import re
import subprocess
def name(identity):
if not re.fullmatch(r'[a-f0-9]{32}', identity):
raise ValueError('原环境任务编号不合法')
return 'adamwen-native-run-' + identity
def mounts(profile):
values = profile['docker']['mounts']
for mount in values:
for key in ('source', 'target'):
value = mount[key]
if not value.startswith('/') or ',' in value or '\x00' in value or '..' in PurePosixPath(value).parts:
raise ValueError('原镜像挂载路径不合法')
if mount['target'] in ('/', '/out', '/tmp'):
raise ValueError('原镜像挂载不能覆盖任务输出目录')
return values
def command(profile, source, arguments, directory, identity):
image = profile['docker']['image']
if not re.fullmatch(r'sha256:[a-f0-9]{64}', image):
raise ValueError('原镜像必须登记不可变摘要')
container = name(identity) if identity != '{run_id}' else 'adamwen-native-run-{run_id}'
args = ['/usr/bin/docker', 'run', '--rm', '--init', '--name', container, '--label', 'xas.native-run=' + identity,
'--runtime', 'runc', '--network', 'host', '--user', '0:0', '--cap-drop', 'ALL',
'--security-opt', 'no-new-privileges', '--read-only', '--memory', '4g', '--cpus', '2', '--pids-limit', '128',
'--tmpfs', '/tmp:rw,nosuid,nodev,size=268435456', '--env', 'HOME=/tmp',
'--env', 'PYTHONUNBUFFERED=1', '--env', 'PYTHONDONTWRITEBYTECODE=1',
'--env', 'OMP_NUM_THREADS=2', '--env', 'OPENBLAS_NUM_THREADS=2', '--env', 'MKL_NUM_THREADS=2',
'--env', 'NUMBA_NUM_THREADS=2', '--env', 'NUMBA_CACHE_DIR=/tmp/numba']
entry = None
for mount in mounts(profile):
args.extend(['--mount', f"type=bind,src={mount['source']},dst={mount['target']},readonly"])
try:
relative = PurePosixPath(Path(source).as_posix()).relative_to(PurePosixPath(mount['source']))
entry = str(PurePosixPath(mount['target']) / relative)
except ValueError:
pass
if entry is None:
raise ValueError('原入口未包含在只读挂载中')
output = str(PurePosixPath(str(directory)) / 'work')
args += ['--mount', f'type=bind,src={output},dst=/out', '--workdir', '/out',
'--entrypoint', profile['interpreter'], image, '-u', entry, *arguments]
return args
def checks(profile):
result = []
for mount in mounts(profile):
path = Path(mount['source'])
valid = path.exists() and not any(p.is_symlink() for p in (path, *path.parents))
result.append({'name': '原镜像只读依赖', 'ok': valid, 'message': mount['source']})
try:
value = subprocess.run(['/usr/bin/docker', 'image', 'inspect', '--format', '{{.Id}}', profile['docker']['image']],
capture_output=True, text=True, timeout=5)
valid = value.returncode == 0 and value.stdout.strip() == profile['docker']['image']
except (OSError, subprocess.TimeoutExpired):
valid = False
result.append({'name': '原依赖镜像', 'ok': valid, 'message': '使用已登记镜像,不拉取或升级环境'})
return result
def cleanup(identity):
container = name(identity)
value = subprocess.run(['/usr/bin/docker', 'inspect', container], capture_output=True, text=True, timeout=8)
if value.returncode:
if 'no such object:' in value.stderr.lower() or 'no such container:' in value.stderr.lower():
return
raise OSError('本次原镜像客户端状态未知')
item = json.loads(value.stdout)[0]
if item.get('Name') != '/' + container or item.get('Config', {}).get('Labels', {}).get('xas.native-run') != identity:
raise ValueError('原镜像客户端归属核验失败')
identity_value = item.get('Id', '')
if not re.fullmatch(r'[a-f0-9]{64}', identity_value):
raise ValueError('原镜像客户端标识不合法')
value = subprocess.run(['/usr/bin/docker', 'rm', '-f', identity_value], capture_output=True, timeout=15)
if value.returncode:
# --rm may delete the verified container between inspect and removal.
remaining = subprocess.run(['/usr/bin/docker', 'inspect', identity_value], capture_output=True, text=True, timeout=8)
if remaining.returncode and ('no such object:' in remaining.stderr.lower() or 'no such container:' in remaining.stderr.lower()):
return
raise OSError('本次原镜像客户端收尾未确认')
+7
View File
@@ -141,6 +141,13 @@ class NativeExecutor:
if unit:
if not self.wait_stopped(run):return
subprocess.run(["/usr/bin/systemctl", "reset-failed", unit_name(run["id"])], capture_output=True, timeout=8)
if run.get("execution", {}).get("runtime") == "docker":
from .native_container import cleanup
try:cleanup(run["id"])
except (OSError, ValueError, subprocess.TimeoutExpired):
self.store.running_message(run["id"], "本次原镜像客户端收尾未确认,保留运行占位;请核对执行器")
self.worker.stopping = True
return
self.store.finish(run["id"], state, message=message, exit_code=code, artifacts=artifacts)
def wait_stopped(self, run):
+24 -1
View File
@@ -70,13 +70,36 @@ class SecretFilter:
return result
def read_secret_tables(tables):
values = []
for table in tables:
path = Path(table["path"])
column = table.get("key_column", 1)
if type(column) is not int or not 0 <= column <= 8 or path.stat().st_size > 262144:
raise ValueError("原认证表格式或大小不支持")
found = False
for line in path.read_text(encoding="utf-8").splitlines():
if not line.strip() or line.lstrip().startswith("#"):
continue
fields = line.split()
if table.get("label") and fields[0] != table["label"]:
continue
if len(fields) <= column:
raise ValueError("原认证表缺少登记列")
values.append(fields[column])
found = True
if not found:
raise ValueError("原认证表缺少登记的测试身份")
return values
def load_environment(plan):
import pwd
account = pwd.getpwnam(plan["user"])
env = {"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HOME": account.pw_dir,
"USER": account.pw_name, "LOGNAME": account.pw_name, "LANG": "C.UTF-8", "TERM": "xterm",
"PYTHONUNBUFFERED": "1", "PYTHONDONTWRITEBYTECODE": "1"}
secrets = []
secrets = read_secret_tables(plan.get("secret_tables", []))
for path in plan.get("env_setup_files", []):
# Only server-owned recipe paths reach this command; output stays in private process memory.
command = ["/bin/bash", "-c", 'set -a; source "$1" >/dev/null; /usr/bin/env -0', "xas-env", path]
+29 -8
View File
@@ -88,11 +88,11 @@ def public_execution(settings, script):
profile = profile_for(settings, script)
if profile is None:
return {"mode": "container", "interactive": False, "max_timeout_seconds": 7200}
return {"mode": "native", "profile_id": profile["id"], "interpreter": profile["interpreter"],
"user": profile["user"], "cwd": profile["cwd"], "description": "使用服务器原有环境和原始入口,不迁移依赖或修改源文件。",
return {"mode": "native", "runtime": "docker" if profile.get("docker") else "host", "profile_id": profile["id"], "interpreter": profile["interpreter"],
"user": profile["user"], "cwd": profile["cwd"], "description": "复用原依赖镜像和原始入口,只读挂载源文件,结果写入本次独立目录。" if profile.get("docker") else "使用服务器原有环境和原始入口,不迁移依赖或修改源文件。",
"impact": profile["impact"], "parameters": profile.get("parameters", []),
"interactive": profile.get("interactive", False), "max_timeout_seconds": MAX_TIMEOUT,
"notes": profile.get("notes", []), "verification": "启动方案已接入,未完成本轮 GPU/长稳业务验收"}
"notes": profile.get("notes", []), "verification": "启动方案已接入,未完成本轮业务测试验收" if profile.get("available", True) else "原环境方案待配置:" + profile.get("unavailable_reason", "请核对依赖与运行条件")}
def validate_config(settings, script, values):
@@ -138,10 +138,14 @@ def build_plan(settings, script, run_id=None, check_host=True):
return str(variables[match[1]])
return PLACEHOLDER.sub(replace, str(value))
argv = [profile["interpreter"], str(source), *[render(a) for a in profile.get("arguments", [])]]
if profile.get("docker"):
from .native_container import command
argv = command(profile, source, argv[2:], directory, identity)
environment = {**target.get("environment", {}), **{k: render(v) for k, v in profile.get("environment", {}).items()}}
credential_files = list(dict.fromkeys(profile.get("credential_files", []) + target.get("credential_files", [])))
setup_files = [render(p) for p in profile.get("env_setup_files", [])]
required_files = [render(p) for p in profile.get("required_files", [])]
required_files.extend(table["path"] for table in profile.get("secret_tables", []))
if target.get("config_file"):
required_files.append(target["config_file"])
timeout = script["config"].get("timeout_seconds", profile.get("timeout_seconds", 3600))
@@ -153,11 +157,21 @@ def build_plan(settings, script, run_id=None, check_host=True):
check("启动方案", profile.get("available", True), profile.get("unavailable_reason", "已登记原环境入口"))
if check_host:
check("原工作目录", Path(profile["cwd"]).is_dir(), profile["cwd"])
tools = list(dict.fromkeys([profile["interpreter"], "/usr/bin/systemd-run", *profile.get("required_tools", [])]))
tools = list(dict.fromkeys(["/usr/bin/docker" if profile.get("docker") else profile["interpreter"], "/usr/bin/systemd-run", *profile.get("required_tools", [])]))
for tool in tools:
check("工具", Path(tool).is_file() and os.access(tool, os.X_OK), tool)
for path in required_files + credential_files + setup_files:
check("原配置或素材", Path(path).is_file(), path)
if profile.get("docker"):
from .native_container import checks as container_checks
checks.extend(container_checks(profile))
if profile.get("secret_tables"):
from .native_io import read_secret_tables
try:
read_secret_tables(profile["secret_tables"])
check("测试身份引用", True, "已找到原测试身份,凭据仅在内存中读取")
except (OSError, ValueError):
check("测试身份引用", False, "原凭据表缺失、格式不支持或没有登记的测试身份")
for value in profile.get("check_ports", []):
port = int(render(value))
if not 1 <= port <= 65535:
@@ -167,20 +181,27 @@ def build_plan(settings, script, run_id=None, check_host=True):
available = True
except OSError:available = False
check("目标服务端口", available, f"127.0.0.1:{port}" + (" 可连接" if available else " 未监听,请先准备原服务"))
if target.get("verify_model"):
if all(c["ok"] for c in checks):
from .model_preflight import check_model
checks.append(check_model(target, credential_files))
else:
check("模型身份", False, "先满足上方原环境条件,再核对服务提供的模型")
# Binds acknowledgment to saved configuration, source and recipe, not to a mutable browser flag.
dependency_hashes = {}
for name in required_files + setup_files:
path = Path(name)
if name not in credential_files and path.is_file() and path.stat().st_size <= 16 * 1024**2:
secret_paths = [table["path"] for table in profile.get("secret_tables", [])]
if name not in credential_files + secret_paths + profile.get("mutable_files", []) and path.is_file() and path.stat().st_size <= 16 * 1024**2:
dependency_hashes[name] = hashlib.sha256(path.read_bytes()).hexdigest()
fingerprint = {"profile": profile, "target": target, "options": options, "source": source_hash,
"dependencies": dependency_hashes,
"script_id": script["id"], "timeout_seconds": timeout}
token = hashlib.sha256(json.dumps(fingerprint, sort_keys=True, ensure_ascii=False).encode()).hexdigest()
return {"mode": "native", "profile_id": profile["id"], "plan_token": token,
return {"mode": "native", "runtime": "docker" if profile.get("docker") else "host", "profile_id": profile["id"], "plan_token": token,
"ready": all(c["ok"] for c in checks), "checks": checks, "argv": argv, "command_preview": shlex.join(argv),
"cwd": profile["cwd"], "user": profile["user"], "environment": environment,
"credential_files": credential_files, "env_setup_files": setup_files,
"credential_files": credential_files, "env_setup_files": setup_files, "secret_tables": profile.get("secret_tables", []),
"impact": profile["impact"], "interactive": profile.get("interactive", False),
"timeout_seconds": timeout, "source_sha256": source_hash, "dependency_hashes": dependency_hashes,
"output_paths": [render(p) for p in profile.get("output_paths", [])],
@@ -190,4 +211,4 @@ def build_plan(settings, script, run_id=None, check_host=True):
def public_plan(plan):
keys = ("mode", "profile_id", "plan_token", "ready", "checks", "command_preview", "cwd", "user",
"impact", "interactive", "output_paths", "notes", "timeout_seconds", "source_sha256")
return {key: plan[key] for key in keys}
return {**{key: plan[key] for key in keys}, "runtime": plan.get("runtime", "host")}
Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="16" fill="#b9db91"/>
<path d="M14 13h11l7 12 7-12h11L38 32l12 19H39L32 39l-7 12H14l12-19z" fill="#173522"/>
</svg>

After

Width:  |  Height:  |  Size: 213 B

+2
View File
@@ -6,6 +6,8 @@
<meta name="color-scheme" content="light">
<meta name="theme-color" content="#14231e">
<title>XAS · 测试控制台</title>
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32.png">
<link rel="icon" type="image/svg+xml" sizes="any" href="/static/favicon.svg">
<link rel="stylesheet" href="/static/styles.css">
<script type="module" src="/static/app.js"></script>
</head>
+1 -1
View File
@@ -76,7 +76,7 @@ export function nativeScriptInfo(script) {
kv([['运行账号', execution.user], ['原工作目录', execution.cwd], ['解释器', execution.interpreter], ['方案编号', execution.profile_id],
['交互方式', execution.interactive ? '原菜单 · 可发送菜单选项' : '自动执行']]),
execution.description ? el('p', {class: 'native-description'}, execution.description) : null,
impactView(execution.impact), note('启动方案已接入,未完成本轮业务验收。', 'info'));
impactView(execution.impact), note(execution.verification || '启动方案已接入,未完成本轮业务验收。', 'info'));
}
export async function preflightNativeRun(ctx, script) {
+2 -1
View File
@@ -25,7 +25,8 @@ const profileLabel = script => isNative(script) ? '服务器原环境' : script.
const typeLabel = script => script.category === 'example' ? '验收示例' : script.test_kind === 'suite' ? '测试套件' : '单项测试';
const groupLabels = new Map([
['uploaded', '上传的脚本'], ['company-gpu-suite', '公司 GPU 测试套件'],
['dsv41-trial-20261010', 'DeepSeek 试验脚本(10/10)'], ['model-trials-20261011', '模型试验脚本(10/11)']
['dsv41-trial-20261010', 'DeepSeek 试验脚本(10/10)'], ['model-trials-20261011', '模型试验脚本(10/11)'],
['drama3-tests-20261015', 'Drama3 / Qwen-ASR 语音测试']
]);
export async function scriptList(ctx) {
+1 -1
View File
@@ -10,7 +10,7 @@ import sys
ROOT = Path("/adamwen-work-space/xas-test-library")
NAME = "adamwen-script-console"
IMAGE = "adamwen/script-console:20261014-1"
IMAGE = "adamwen/script-console:20261020-1"
UNIT = "adamwen-script-worker.service"
+93
View File
@@ -0,0 +1,93 @@
"""Refresh reviewed sources atomically without resetting saved options or history."""
import argparse
import copy
import json
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from console import catalog, native_plans
from console.settings import Settings
from console.store import Store
def refresh(store, backup, apply=False):
original = {s['id']: s for s in store.scripts()}
class Draft:
settings = store.settings
items = copy.deepcopy(original)
def scripts(self): return list(self.items.values())
def save_script(self, item): self.items[item['id']] = item
draft = Draft()
scan = catalog.discover(draft)
for item in draft.items.values():
profile = native_plans.profile_for(store.settings, item)
if not profile:
continue
if item['id'] not in original:
item['config'] = native_plans.validate_config(store.settings, item,
{'native_options': {}, 'timeout_seconds': profile.get('timeout_seconds', 3600)})
item['enabled'] = profile.get('available', True)
if not profile.get('available', True):
item.update(enabled=False, blocked=True, reason=profile.get('unavailable_reason', '原环境待配置'))
if item['enabled']:
item.update(status='ready', reason='')
elif item['status'] != 'missing':
item['status'] = 'needs_configuration'
changes = []
for identity, value in draft.items.items():
before = original.get(identity)
if before and {k:v for k,v in before.items() if k != 'updated_at'} == {k:v for k,v in value.items() if k != 'updated_at'}:
continue
changes.append((identity, before, value))
if not apply:
return {'scan': scan, 'changed': len(changes), 'applied': False}
with store.connect(write=True) as db:
if db.execute("SELECT 1 FROM runs WHERE state IN ('queued','running')").fetchone():
raise ValueError('请先等待排队和运行任务结束')
records = []
for identity, before, after in changes:
row = db.execute('SELECT data FROM scripts WHERE id=?', (identity,)).fetchone()
if (json.loads(row[0]) if row else None) != before:
raise ValueError('配置有并发修改,本次刷新未写入')
records.append({'id': identity, 'before': row[0] if row else None, 'after': json.dumps(after)})
if not records:
return {'scan': scan, 'changed': 0, 'applied': True}
backup.parent.mkdir(parents=True, exist_ok=True)
with backup.open('x', encoding='utf-8') as handle:
json.dump({'scripts': records}, handle, ensure_ascii=False, indent=2)
backup.chmod(0o600)
for record in records:
db.execute('INSERT OR REPLACE INTO scripts VALUES(?,?)', (record['id'], record['after']))
return {'scan': scan, 'changed': len(changes), 'applied': True}
def restore(store, backup):
records = json.loads(backup.read_text(encoding='utf-8'))['scripts']
with store.connect(write=True) as db:
if db.execute("SELECT 1 FROM runs WHERE state IN ('queued','running')").fetchone():
raise ValueError('请先等待排队和运行任务结束')
for record in records:
row = db.execute('SELECT data FROM scripts WHERE id=?', (record['id'],)).fetchone()
current = row[0] if row else None
if current not in (record['before'], record['after']):
raise ValueError('刷新后配置已修改,请人工核对,禁止覆盖')
if record['before'] is None and db.execute('SELECT 1 FROM runs WHERE script_id=?', (record['id'],)).fetchone():
raise ValueError('新增项目已有运行历史,不能删除登记,请人工恢复')
for record in records:
if record['before'] is None:
db.execute('DELETE FROM scripts WHERE id=?', (record['id'],))
else:
db.execute('UPDATE scripts SET data=? WHERE id=?', (record['before'], record['id']))
return {'restored': len(records), 'history_preserved': True}
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--apply', action='store_true')
parser.add_argument('--restore', action='store_true')
parser.add_argument('--backup', type=Path, required=True)
args = parser.parse_args()
if args.apply and args.restore: parser.error('选择刷新或恢复之一')
store = Store(Settings.from_env())
print(json.dumps(restore(store, args.backup) if args.restore else refresh(store, args.backup, args.apply)))
+57
View File
@@ -0,0 +1,57 @@
# 模型与测试目录更新验收
核对日期:2026-10-20。09:45 只读盘点,10:12 完成控制台切换(北京时间)。用户要求核对当前模型、测试套件变化并更新控制台;原模型服务、源脚本和业务数据不修改,本轮不发推理、不运行原业务测试。
## 现场变化
| 对象 | 当前证据与处理 |
|---|---|
| GPU 综合套件 | 最高已安装版本仍为 XAS-GPU-Test v3.5.0,224 个登记源码摘要均未变,不切换版本 |
| DeepSeek | GPU 0–7,18100,当前权重目录 DeepSeek-V4.1-Flash-1017;模型目录返回主名 deepseek-v4.1-flash-1017 及旧别名 deepseek-v4.1-flash;更新下拉主名,旧复制版 smoke 和数学脚本保持原样 |
| Qwen 文本服务 | GPU 14,18112,模型目录返回 qwen3.8-27b,上下文 262144;已核对复用原 model-trials/configs/vllm.env,新增目标选项 |
| 图像服务 | ComfyUI 公共引擎使用 GPU 8–10,专属引擎使用 GPU 11,网关 8188;仅盘点,不操作这些服务 |
| 语音服务 | Drama3 引擎 GPU 12 / 18301,Qwen3-ASR GPU 13 / 18302,网关 18300/18343;新增专项测试目录 |
| 共享资源 | 盘点时 GPU 0–14 均有显存占用,GPU 15 为 0 MiB;采样利用率均 0% 只表示当时空闲,不代表资源未分配。根盘剩余约 679 GiB、使用率 80% |
原 DeepSeek 目录 3 个启停脚本发生变化、增加 5 个文件;模型综合试验目录的 48 个脚本未变。新增 run-unc* 是切换服务 B 的历史变体,归入 DeepSeek 套件相关文件,不单独平铺成测试卡片。容量链的原收尾脚本默认切回 UNCENSORED 版本,已在逐次影响确认中明确提示不会恢复当前 1017 主模型。
## 控制台改动
- 更新当前 DeepSeek / Qwen 目标。只有支持 MODEL/SPEC_URLS 的通用原 Python 测试增加 Qwen GPU 14 选项,未将缺少对应原配置的 Shell 套件强行改端口。
- 当前两个模型目标增加只读身份预检:一次本机 `/v1/models` 请求,拒绝重定向、3 秒超时、响应限 256 KiB、无重试或推理;名称或认证不匹配即拦截。
- 增加 5 项语音测试:语音识别文件/实时转写、语音合成网关回归可触发;实时语音对话、原引擎回归、语音容量测试为待配置。
- 语音测试使用已存在的原镜像摘要和 `/opt/omni/bin/python`。原脚本、素材、日志和认证引用只读,输出写本次独立 `/out`;仅启动客户端,无直接 GPU 设备权限,不启停模型服务。每次启动须确认共享服务负载。
- 新增客户端受本次运行 ID/标签管理,取消/超时确认本机服务和对应容器收尾;自动删除竞态及 Docker 错误大小写已覆盖,未知状态保留占位。
- 目录刷新采用单事务迁移,保留原配置、超时、禁用选择、版本和历史;重复扫描仍保留“源码变化待确认”的原因。
正式目录现有 **28 项业务测试(8 套件、20 单项)**;**25 项已配置启用、3 项待配置**。已启用不代表服务当前可用,运行前仍预检。登记原位置文件 320 个,加 3 个托管脚本共 323 个文件;两个验收示例继续默认隐藏。
## 待配置与暂未开放项
| 项目 | 原因 |
|---|---|
| 实时语音对话 | 原共享语音服务预设仍请求 deepseek-v4.1-flash-uncensored,当前模型目录不提供此名;需服务维护者调整后核对,控制台不偷偷替换 |
| Drama3 原引擎回归 | 涉及管理凭据与外网参考音频下载,尚未完成适配 |
| 语音容量测试 | 需要模拟大脑辅助容器、原内部网络与并发上限管理 |
| ComfyUI 新工程测试 | 包含测试账号/业务数据写入、插件安装、模型下载删除及引擎重启等维护行为,未直接加入可运行入口 |
| DSH 比赛模拟 | 任务包、run.json、dsh-home 和所需镜像已缺失,且有外部 Provider 调用;未猜测凭据、预算或重新启动 |
## 验证结果
- 最终隔离 Linux **67/67 回归通过**;独立审查未发现未解决的确定 P1/P2。
- 生产库副本完成刷新、幂等复验及精确恢复,runs/versions 内容不变;新增 13 个文件登记、3 个变更文件更新,共 16 项元数据变动。
- 原语音镜像 4 项真实 systemd/Docker 短样例通过:成功、非零失败、超时、取消。使用合成正弦音频验证 librosa resample/pyin 与文件写入,检查凭据脱敏、源文件只读和客户端无残留。
- 首轮样例发现库按宿主线程数初始化导致线程限额错误,以及 Docker 小写“对象不存在”误判;已修复并用新样例复验,首轮失败记录保留,没有把失败改成通过。
- 无网络客户端只读验证原脚本、4 个参考音频、访问日志和 selftest 身份可读,未执行原测试。
- 正式 API 与 1440px/390px 浏览器验证新目标、28 项目录、待配置原因、原镜像预检、影响确认取消和导航;拦截全部测试提交,历史条数不变,无 JS 错误,favicon 保留。
- 切换后 10 条历史运行及版本摘要不变,320 个原脚本摘要不变;全部 11 个其他运行中容器的 ID/StartedAt 不变。只重启本项目网页和空闲 worker,没有运行模型、语音或 GPU 业务测试。
## 发布、证据与恢复
正式镜像 `adamwen/script-console:20261020-1`,摘要 `sha256:43bd6d5e573ffc772ad9836acb14df9a1d3dd8e60e4bf129e772fe24cff14ffa`。备份:`runtime/backups/before-catalog-refresh-20261020/`;旧网页容器:`adamwen-script-console-before-catalog-refresh-20261020`。
新增可审查迁移 `deploy/refresh_catalog.py`;配置恢复使用备份内 `catalog-migration.json`。回退先停止接单并确认无活动任务,停止本项目 worker,恢复配置与匹配的旧源码/网页,再启动 worker。不覆盖 SQLite 或运行目录;若配置已被修改、或新增项目已有运行历史,恢复工具拒绝覆盖,须人工核对。配置恢复已在副本验证,生产故障回退未触发。
服务器证据:`runtime/catalog-refresh-acceptance-20261020.json`、`runtime/releases/catalog-refresh-20261020/validation.json`、`runtime/native-docker-validation-20261020-v2/acceptance.json`。本地证据:`.local/evidence/server-refresh-20261020.json`、`server-details-20261020.json`、`catalog-refresh-browser.json`。
主要变更:目录三个 JSON、`model_preflight.py`、`native_container.py`、原环境计划/脱敏/收尾、目录刷新、页面提示、迁移及测试和维护文档。用户随后于 2026-10-20 明确授权将本次更新与此前已部署的 favicon 一起提交并推送到公司 Git 仓库。
+10
View File
@@ -23,3 +23,13 @@ SQLite 保持 schema v1,仅增加 JSON 配置和运行快照字段。`deploy/n
## 验证范围
方案接入、只读预检、参数/权限边界、模拟及真实短进程生命周期已验证。原项目的 GPU、长稳和模型切换业务不在本轮实际运行,页面如实提示。后续只有在用户实际发起并确认相应影响时才执行原项目测试。
## 2026-10-20:原语音镜像与模型身份预检
语音测试的 Python、SDK 和音频库位于原 Docker 镜像,宿主 Python 不具备相同依赖。可信方案新增固定镜像摘要和只读挂载描述,由 `native_container.py` 构造固定客户端命令;上传代码仍不能选择宿主命令或 Docker 参数。容器使用 runc、无 GPU 设备、只读根目录和原源码,独立 `/out` 输出;2 CPU、4 GiB、128 进程,计算库线程为 2,缓存写临时目录,避免按宿主 128 线程初始化而超限。仅通过原接口使用共享模型,并逐次确认影响。
取消/超时先收尾原 systemd 服务,再按本次运行标签核验客户端容器,使用不可变容器 ID 清理。自动删除竞态确认缺失即成功;Docker 状态未知仍保留运行占位。原 label-key 认证表只读取已登记 selftest 身份用于日志脱敏,值不进入命令行、数据库或网页。
当前 DeepSeek/Qwen 目标在预检中增加一次有界 `/v1/models` 请求:仅允许本机地址、拒绝重定向、无重试、3 秒超时、256 KiB 响应上限,不发推理。名称不匹配或认证异常阻止运行。原共享访问日志只检查存在,不把其持续变化纳入启动配置摘要。
后续已审查来源更新用 `deploy/refresh_catalog.py` 单事务写入;保留已有参数、禁用选择、版本和运行历史,新增业务入口与内部文件分开。新增项目已有运行记录或配置发生后续修改时,配置恢复拒绝覆盖,不能用旧数据库替代新历史。
+16
View File
@@ -75,3 +75,19 @@ Git 远程为公司仓库 `https://code.it.aisuan.cn/wenhuadian/xas-test-library
正式镜像 `adamwen/script-console:20261014-1`,摘要 `sha256:ddbf3622b4e468ae9368864965ed2830d067a021596a6981aeca68bb5109880c`。备份为 `runtime/backups/before-search-alignment-20261014/`;旧网页容器 `adamwen-script-console-before-search-alignment-20261014` 停止保留,可仅切换网页回退,不覆盖数据库或重启 worker。故障回退未触发。
证据:服务器 `runtime/search-alignment-acceptance.json`;本地 `.local/evidence/search-deployed.json` 和 `.local/screenshots/search-*.png`。用户随后于 2026-10-14 明确授权将本次 UI 修复提交并同步到公司 Git 仓库。
## 2026-10-19 19:55 网站 favicon
沿用现有浅绿色圆角底和深绿色 X 标识,新增 `console/static/favicon.svg` 与 32px PNG 备用图标,在 `index.html` 声明。真实浏览器验证两个资源均返回正确内容类型且可解码,项目页及运行记录页均保留图标引用,无 JavaScript 错误。
仅更新网页容器,执行器 PID 和 scripts/versions/runs 摘要不变,仍为 10 条记录、0 活动任务。镜像 `adamwen/script-console:20261019-1`,摘要 `sha256:102266ae01d42284ccdf7d4fb27c1a534b7051e3bcd2f0b266e9b45bf6462e99`。备份为 `runtime/backups/before-favicon-20261019/`,旧网页容器 `adamwen-script-console-before-favicon-20261019` 停止保留;回退仅替换网页,不恢复数据库、不重启 worker。
证据:服务器 `runtime/favicon-acceptance.json`,本地 `.local/evidence/favicon-browser.json`。当次未执行 GPU 测试,也未提交 Git;用户随后于 2026-10-20 授权将 favicon 与模型、测试目录更新一并提交和推送。
## 2026-10-20 10:12 模型与测试目录刷新
当前为 28 项业务测试,25 项已配置、3 项语音测试待配置。新增 DeepSeek 1017 主名与 Qwen GPU 14 / 18112 目标;通用原 Python 测试可选当前 Qwen 服务,运行前只读核对模型身份。旧 DeepSeek 别名仍由服务支持,原复制版轻量脚本不改写。
新增语音识别和语音网关回归复用原镜像与只读依赖,输出归档到本次运行目录。客户端限制 2 CPU、4 GiB、128 进程和 2 个计算库线程;没有直接 GPU 设备,通过已有语音接口运行,仍须逐次确认影响。取消会核对并清理本次客户端,不能据此推断已发出的远端推理停止。3 项待配置原因在详情显示;不自动修复其他维护者的服务预设。
正式镜像 `adamwen/script-console:20261020-1`,旧网页保留为 `adamwen-script-console-before-catalog-refresh-20261020`。目录迁移使用 `deploy/refresh_catalog.py`,不会重置原参数或历史。详细现场状态、验证结果、未接入范围与恢复条件见 [本轮验收](acceptance-refresh-2026-10-20.md)。
+43
View File
@@ -0,0 +1,43 @@
import hashlib
from pathlib import Path
import tempfile
import unittest
from console.settings import Settings
from console.store import Store
from console.catalog import discover
from deploy.refresh_catalog import refresh, restore
class CatalogRefreshTests(unittest.TestCase):
def test_refresh_preserves_options_and_history_and_restores_exactly(self):
with tempfile.TemporaryDirectory() as temporary:
base = Path(temporary); source = base/'source'; source.mkdir()
(source/'old.py').write_text('print(1)')
settings = Settings(base/'runtime', [{'id':'suite','path':str(source)}]); store = Store(settings)
discover(store)
old = store.scripts()[0]; old.update(enabled=True,status='ready',reason='')
old['config']['timeout_seconds'] = 457
store.save_script(old)
old_runs = store.runs()
(source/'old.py').write_text('print(5)')
(source/'new.py').write_text('print(2)'); (source/'blocked.py').write_text('print(3)')
(source/'support.py').write_text('print(4)')
settings.native_catalog['profiles'] = [
{'id':name,'source_group':'suite','entry':name+'.py','cwd':str(source),'interpreter':'/usr/bin/python3',
'user':'root','available':name!='blocked','unavailable_reason':'原服务预设已过期','parameters':[]}
for name in ('old','new','blocked')]
snapshot = base/'migration.json'
self.assertFalse(refresh(store, snapshot)['applied'])
self.assertFalse(snapshot.exists())
refresh(store, snapshot, True)
by_name = {s['name']:s for s in store.scripts()}
self.assertEqual(by_name['old.py']['config']['timeout_seconds'],457)
self.assertFalse(by_name['old.py']['enabled'])
self.assertEqual(by_name['old.py']['version'],2)
self.assertTrue(by_name['new.py']['enabled'])
self.assertFalse(by_name['blocked.py']['enabled'])
self.assertFalse(by_name['support.py']['enabled'])
self.assertEqual(store.runs(),old_runs)
self.assertEqual(refresh(store,base/'again.json',True)['changed'],0)
restore(store,snapshot)
self.assertEqual(store.scripts(),[old])
+69
View File
@@ -0,0 +1,69 @@
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
import tempfile
import threading
import unittest
from unittest.mock import patch
from console.model_preflight import check_model
class ModelPreflightTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.root = Path(self.temp.name)
self.credential = self.root / 'vllm.env'
self.credential.write_text('VLLM_API_KEY="dummy-metadata-key"\n')
self.calls = []
self.status = 200
self.body = {'data': [{'id': 'current-model', 'max_model_len': 262144}]}
case = self
class Handler(BaseHTTPRequestHandler):
def log_message(self, *args): pass
def do_GET(self):
case.calls.append((self.path, self.headers.get('Authorization')))
self.send_response(case.status)
if case.status == 302:
self.send_header('Location', '/redirect-target')
self.end_headers()
self.wfile.write(json.dumps(case.body).encode())
self.server = HTTPServer(('127.0.0.1', 0), Handler)
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
self.thread.start()
self.target = {'url': 'http://127.0.0.1:' + str(self.server.server_port), 'model': 'current-model'}
def tearDown(self):
self.server.shutdown(); self.server.server_close(); self.thread.join()
self.temp.cleanup()
def test_metadata_only_one_request_with_runtime_credentials(self):
result = check_model(self.target, [str(self.credential)])
self.assertTrue(result['ok'])
self.assertEqual(self.calls, [('/v1/models', 'Bearer dummy-metadata-key')])
self.assertNotIn('dummy-metadata-key', json.dumps(result))
def test_wrong_model_fails_even_when_port_is_open(self):
self.target['model'] = 'stale-model'
result = check_model(self.target, [str(self.credential)])
self.assertFalse(result['ok'])
self.assertIn('模型名称', result['message'])
def test_auth_failure_and_redirect_do_not_retry_or_leak(self):
for status in (401, 302):
self.status = status; self.calls.clear()
result = check_model(self.target, [str(self.credential)])
self.assertFalse(result['ok'])
self.assertEqual(len(self.calls), 1)
self.assertNotIn('dummy-metadata-key', json.dumps(result))
def test_external_address_rejected_before_network(self):
self.target['url'] = 'http://example.com'
with patch('console.model_preflight.urllib.request.build_opener') as build:
self.assertFalse(check_model(self.target, [])['ok'])
build.assert_not_called()
def test_malformed_or_oversize_metadata_is_not_success(self):
for body in ({'data': {}}, {'data': ['bad']}, {'data': [], 'padding': 'x' * 262145}):
self.body = body
self.assertFalse(check_model(self.target, [str(self.credential)])['ok'])
+59
View File
@@ -0,0 +1,59 @@
import json
from pathlib import Path
from types import SimpleNamespace
import unittest
import tempfile
from unittest.mock import patch
from console.native_container import command, cleanup
from console.native_io import read_secret_tables, SecretFilter
class NativeContainerTests(unittest.TestCase):
def test_registered_key_table_is_redacted_without_environment_injection(self):
with tempfile.TemporaryDirectory() as temporary:
path = Path(temporary) / 'keys.txt'
path.write_text('# local fixture\noperator dummy-audio-key scope\n')
values = read_secret_tables([{'path': str(path), 'key_column': 1}])
self.assertEqual(values, ['dummy-audio-key'])
self.assertNotIn(b'dummy-audio-key', SecretFilter(values).clean(b'header=dummy-audio-key'))
def setUp(self):
self.identity = 'a' * 32
self.profile = {'docker': {'image': 'sha256:' + 'b' * 64, 'mounts': [{'source': '/original/tests', 'target': '/t'}]},
'interpreter': '/opt/omni/bin/python'}
def test_fixed_image_readonly_sources_and_owned_output_without_gpu(self):
args = command(self.profile, Path('/original/tests/test_api.py'), ['x'], '/runs/' + self.identity, self.identity)
self.assertIn('xas.native-run=' + self.identity, args)
self.assertIn('type=bind,src=/original/tests,dst=/t,readonly', args)
self.assertIn('type=bind,src=/runs/' + self.identity + '/work,dst=/out', args)
self.assertEqual(args[-4:], [self.profile['docker']['image'], '-u', '/t/test_api.py', 'x'])
self.assertNotIn('--gpus', args)
self.assertIn('runc', args)
def test_cleanup_rejects_foreign_container(self):
inspected = SimpleNamespace(returncode=0, stdout=json.dumps([{'Name': '/adamwen-native-run-' + self.identity,
'Config': {'Labels': {'xas.native-run': 'other'}}}]), stderr='')
with patch('console.native_container.subprocess.run', return_value=inspected) as call:
with self.assertRaises(ValueError): cleanup(self.identity)
self.assertEqual(call.call_count, 1)
def test_daemon_error_is_not_treated_as_container_missing(self):
error = SimpleNamespace(returncode=1, stdout='', stderr='Cannot connect to Docker daemon')
with patch('console.native_container.subprocess.run', return_value=error):
with self.assertRaises(OSError): cleanup(self.identity)
def test_missing_owned_container_is_already_clean(self):
missing = SimpleNamespace(returncode=1, stdout='', stderr='error: no such object: adamwen-native-run-' + self.identity)
with patch('console.native_container.subprocess.run', return_value=missing) as call:
cleanup(self.identity)
self.assertEqual(call.call_count, 1)
def test_auto_removed_container_race_is_not_an_uncertain_cleanup(self):
container_id = 'c' * 64
inspected = SimpleNamespace(returncode=0, stdout=json.dumps([{'Id':container_id,
'Name':'/adamwen-native-run-' + self.identity,'Config':{'Labels':{'xas.native-run':self.identity}}}]),stderr='')
gone = SimpleNamespace(returncode=1,stdout='',stderr='Error: No such container: ' + container_id)
with patch('console.native_container.subprocess.run',side_effect=[inspected,gone,gone]) as call:
cleanup(self.identity)
self.assertEqual(call.call_args_list[1].args[0][-1],container_id)