diff --git a/README.md b/README.md index 46fd773..214d4a5 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/catalog/external-sources.json b/catalog/external-sources.json index eadaf14..7a8e687 100644 --- a/catalog/external-sources.json +++ b/catalog/external-sources.json @@ -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" + } ] } diff --git a/catalog/native-profiles.json b/catalog/native-profiles.json index c3c5e99..90df504 100644 --- a/catalog/native-profiles.json +++ b/catalog/native-profiles.json @@ -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" } diff --git a/catalog/test-definitions.json b/catalog/test-definitions.json index 606020e..6b409ee 100644 --- a/catalog/test-definitions.json +++ b/catalog/test-definitions.json @@ -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" + } + ] } ] } diff --git a/console/catalog.py b/console/catalog.py index 09dcb11..2622755 100644 --- a/console/catalog.py +++ b/console/catalog.py @@ -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, diff --git a/console/model_preflight.py b/console/model_preflight.py new file mode 100644 index 0000000..07bfd5e --- /dev/null +++ b/console/model_preflight.py @@ -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 diff --git a/console/native_container.py b/console/native_container.py new file mode 100644 index 0000000..d6a92bd --- /dev/null +++ b/console/native_container.py @@ -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('本次原镜像客户端收尾未确认') diff --git a/console/native_executor.py b/console/native_executor.py index 068001f..ebe93cb 100644 --- a/console/native_executor.py +++ b/console/native_executor.py @@ -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): diff --git a/console/native_io.py b/console/native_io.py index 46675b2..110da61 100644 --- a/console/native_io.py +++ b/console/native_io.py @@ -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] diff --git a/console/native_plans.py b/console/native_plans.py index 22fa0fe..f2e5eea 100644 --- a/console/native_plans.py +++ b/console/native_plans.py @@ -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")} diff --git a/console/static/favicon-32.png b/console/static/favicon-32.png new file mode 100644 index 0000000..3dd93c4 Binary files /dev/null and b/console/static/favicon-32.png differ diff --git a/console/static/favicon.svg b/console/static/favicon.svg new file mode 100644 index 0000000..8baa423 --- /dev/null +++ b/console/static/favicon.svg @@ -0,0 +1,4 @@ + diff --git a/console/static/index.html b/console/static/index.html index 287d320..9a84cdd 100644 --- a/console/static/index.html +++ b/console/static/index.html @@ -6,6 +6,8 @@