feat: refresh model and voice test catalog with favicon
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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('本次原镜像客户端收尾未确认')
|
||||
@@ -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
@@ -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
@@ -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 |
@@ -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 |
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user