import {api, el, button, link, badge, note, empty, loading, field, input, textarea, select, heading, kv, errorBox, toast, dialog, date, enc, scriptUrl} from './ui.js'; import {parseArgs, parseLines, requestId} from './core.js'; import {runTable} from './runs.js'; import {relatedFiles} from './related-files.js'; import {isNative, maxTimeout, nativeParameterForm, nativeScriptInfo, preflightNativeRun} from './native-ui.js'; const pendingRuns = new Map(); function pendingRequest(id) { if (pendingRuns.has(id)) return pendingRuns.get(id); let value; try { value = sessionStorage.getItem(`xas.pending-run.${id}`); } catch { /* Private browser settings can disable storage. */ } if (!value || !/^[0-9a-f-]{36}$/.test(value)) value = requestId(); pendingRuns.set(id, value); try { sessionStorage.setItem(`xas.pending-run.${id}`, value); } catch { /* The in-memory request still supports safe retry. */ } return value; } function clearRequest(id) { pendingRuns.delete(id); try { sessionStorage.removeItem(`xas.pending-run.${id}`); } catch { /* Storage may be unavailable. */ } } const scriptState = script => !script.enabled && script.status === 'ready' ? 'disabled' : script.status; const scriptBadge = script => badge(scriptState(script), scriptState(script) === 'disabled' ? '未启用' : undefined); const sourceLabel = script => script.kind === 'managed' ? '托管脚本' : '外部原文件'; const profileLabel = script => isNative(script) ? '服务器原环境' : script.config.profile === 'legacy_smoke' ? '旧版 Smoke 兼容环境' : '标准运行环境'; 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)'] ]); export async function scriptList(ctx) { ctx.show(heading('测试项目', '按实际测试行为管理,一项对应一次完整测试。'), loading()); try { const {items} = await api('/api/scripts?include_examples=true', {}, ctx.signal); if (!ctx.alive) return; const search = input({type: 'search', placeholder: '搜索测试名称、用途或入口', 'aria-label': '搜索测试'}); const group = select('来源目录', [['', '全部来源'], ...Array.from(new Set(items.map(item => item.source_group || '未分组'))).sort().map(name => [name, groupLabels.get(name) || name])]); const status = select('脚本状态', [['', '全部状态'], ['ready', '可运行'], ['needs_configuration', '待配置'], ['missing', '源文件缺失'], ['disabled', '未启用']]); const kind = select('测试类型', [['', '全部类型'], ['suite', '测试套件'], ['test', '单项测试']]); const purpose = select('项目用途', [['test', '业务测试'], ['example', '验收示例'], ['', '全部项目']], 'test'); const results = el('div'); const count = el('span', {class: 'muted', role: 'status'}); const discover = button('↻ 刷新来源', async () => { discover.disabled = true; try { const result = await api('/api/discover', {method: 'POST'}, ctx.signal); if (ctx.alive) { toast(`目录已刷新:${result.tests} 项测试。内部文件更新不会自动新增测试项目。`); await scriptList(ctx); } } catch (error) { if (ctx.alive) { toast(error.message); discover.disabled = false; } } }); ctx.show(heading('测试项目', '一项对应一次测试;套件内部的多个脚本统一在详情中查看。', [discover, button('+ 新增测试', () => uploadDialog(ctx), 'primary')]), el('div', {class: 'filters'}, el('div', {class: 'search'}, search), purpose, kind, status, group), el('div', {class: 'section-title'}, el('span', {class: 'muted'}, '按测试行为组织 · 验收示例默认隐藏'), count), results); const render = () => { const term = search.value.trim().toLowerCase(); const filtered = items.filter(item => (!group.value || (item.source_group || '未分组') === group.value) && (!status.value || scriptState(item) === status.value) && (!kind.value || item.test_kind === kind.value) && (!purpose.value || item.category === purpose.value) && (!term || [item.name, item.description, item.source_path].some(value => (value || '').toLowerCase().includes(term)))); count.textContent = `${filtered.length} 项测试`; results.replaceChildren(); if (!filtered.length) { results.append(empty(items.length ? '没有匹配的测试项目' : '从第一个测试开始', items.length ? '调整关键词或筛选条件。' : '上传一个测试的入口脚本,创建测试项目。')); return; } const groups = new Map(); for (const item of filtered) { const name = item.category === 'example' ? '控制台验收示例' : item.test_kind === 'suite' ? '测试套件' : '单项测试'; if (!groups.has(name)) groups.set(name, []); groups.get(name).push(item); } for (const [name, scripts] of groups) results.append(el('section', {}, el('div', {class: 'section-title'}, el('h2', {}, name, el('span', {class: 'count'}, `${scripts.length} 项`))), el('div', {class: 'catalog-grid'}, scripts.map(scriptCard)))); }; [search, group, status, kind, purpose].forEach(control => control.addEventListener('input', render)); render(); } catch (error) { if (ctx.alive) ctx.show(heading('测试项目', '查看、配置与运行测试。'), errorBox(error, () => scriptList(ctx))); } } function scriptCard(script) { return el('a', {class: 'script-card', href: `#/scripts/${enc(script.id)}`, 'aria-label': script.name}, el('div', {class: 'card-top'}, el('span', {class: `language ${script.language}`}, typeLabel(script)), scriptBadge(script)), el('h3', {}, script.name), el('p', {class: 'script-description'}, script.description || '尚未填写说明。'), el('p', {class: 'card-source', title: script.source_path}, script.source_path), script.reason ? el('p', {class: 'card-reason'}, script.reason) : null, el('div', {class: 'card-bottom'}, el('span', {}, `${script.file_count} 个相关文件 · v${script.version}`), el('span', {class: 'button detail-link'}, '查看项目详情 →'))); } function uploadDialog(ctx, script) { const file = input({type: 'file', accept: '.py,.sh', required: '', name: 'file'}); const name = input({name: 'name', required: '', maxlength: 120, placeholder: '例如:模型接口连通性检查'}); const description = textarea({name: 'description', placeholder: '说明测试目标、运行条件和输出结果'}); const feedback = el('div', {'aria-live': 'polite'}); const submit = el('button', {class: 'button primary', type: 'submit'}, script ? '上传新版本' : '创建脚本'); const form = el('form', {}, note(script ? '新版本单独保存,不覆盖历史源码。后续运行使用最新版本,已有运行记录保持原版本。' : '支持 Python 和 Shell。脚本代码只读;后续修改请上传新版本。'), field('脚本文件', file, '选择 .py 或 .sh 文件。'), script ? null : field('脚本名称', name), script ? null : field('说明', description), feedback, el('div', {class: 'form-footer'}, submit)); const panel = dialog(script ? `上传新版本 · ${script.name}` : '上传新脚本', form); form.addEventListener('submit', async event => { event.preventDefault(); if (submit.disabled || !form.reportValidity()) return; const data = new FormData(form); submit.disabled = true; submit.textContent = '正在上传…'; feedback.replaceChildren(); try { const saved = await api(script ? `${scriptUrl(script.id)}/versions` : '/api/scripts/upload', {method: 'POST', body: data}, ctx.signal); if (!ctx.alive) return; panel.close(); toast(script ? '新版本已保存。' : '脚本已创建。'); if (location.hash === `#/scripts/${enc(saved.id)}`) await scriptDetail(ctx, saved.id); else location.hash = `/scripts/${enc(saved.id)}`; } catch (error) { if (ctx.alive) feedback.replaceChildren(errorBox(error), note('如果连接中断,请先关闭窗口并刷新脚本库,核对是否已保存,再决定重新上传。')); } finally { submit.disabled = false; submit.textContent = script ? '上传新版本' : '创建脚本'; } }); } export async function scriptDetail(ctx, id) { const navigation = () => el('div', {class: 'detail-navigation'}, link('← 返回测试项目', '#/scripts', 'button back-button')); ctx.show(navigation(), loading('正在读取脚本与版本…')); try { const script = await api(scriptUrl(id), {}, ctx.signal); if (!ctx.alive) return; if (!script.is_test) { const source = el('section', {class: 'panel'}); ctx.show(navigation(), heading(script.name, '这是测试内部的源码文件。'), note('请从测试项目的执行入口配置和运行。'), source); sourcePanel(ctx, source, script); return; } let dirty = false; const feedback = el('div', {'aria-live': 'polite'}); const start = button('▶ 开始运行', async () => { if (start.disabled) return; start.disabled = true; start.textContent = '正在提交…'; feedback.replaceChildren(); let prepared; try { if (isNative(script)) { start.textContent = '正在预检…'; prepared = await preflightNativeRun(ctx, script); if (!prepared || !ctx.alive || dirty) return; start.textContent = '正在提交…'; } const run = await api(`${scriptUrl(id)}/runs`, {method: 'POST', body: {request_id: pendingRequest(id), ...prepared?.payload}}, ctx.signal); clearRequest(id); if (ctx.alive) location.hash = `/runs/${enc(run.id)}`; } catch (error) { if (ctx.alive) feedback.replaceChildren(errorBox(error), note(isNative(script) ? '再次点击会重新预检并重新确认影响,同时复用本次请求编号核对原记录,避免重复排队。' : '再次点击会复用本次请求编号,核对原运行记录,避免重复排队。')); } finally { prepared?.close(); if (ctx.alive) { start.disabled = dirty || !script.enabled || script.status !== 'ready'; start.textContent = '▶ 开始运行'; } } }, 'primary'); start.disabled = !script.enabled || script.status !== 'ready'; const actions = [link('运行记录', `#/runs?script_id=${enc(id)}`, 'button')]; if (script.kind === 'managed') actions.push(button('上传新版本', () => uploadDialog(ctx, script))); actions.push(start); const source = el('section', {class: 'panel'}); const history = el('section', {class: 'panel'}, el('div', {class: 'panel-header'}, el('h2', {}, '近期运行')), loading()); ctx.show(navigation(), el('div', {class: 'breadcrumbs'}, link('测试项目', '#/scripts'), ' / ', script.name), heading(script.name, script.description || '查看源代码、配置参数并开始一次独立运行。', actions), feedback, script.reason ? note(script.reason, script.status === 'missing' ? 'danger' : '') : null, !script.enabled ? note('当前脚本未启用。确认运行参数和环境要求后,在配置中启用并保存。') : null, nativeScriptInfo(script), el('div', {class: 'detail-grid'}, source, configForm(ctx, script, () => { dirty = true; start.disabled = true; start.title = '请先保存修改后的运行配置。'; })), relatedFiles(ctx, script), el('div', {class: 'section-title'}, el('h2', {}, '执行记录')), history); sourcePanel(ctx, source, script); const loadHistory = async () => { try { const {items} = await api(`/api/runs?script_id=${enc(id)}&limit=10`, {}, ctx.signal); if (ctx.alive) history.replaceChildren(items.length ? runTable(items) : empty('还没有运行记录', '首次运行后,可在这里回看状态、日志和结果文件。')); } catch (error) { if (ctx.alive) history.replaceChildren(errorBox(error, loadHistory)); } }; await loadHistory(); } catch (error) { if (ctx.alive) ctx.show(navigation(), heading('脚本详情', '读取脚本信息失败。'), errorBox(error, () => scriptDetail(ctx, id))); } } function sourcePanel(ctx, target, script) { const versions = script.versions?.length ? script.versions : [{version: script.version}]; const version = select('查看源码版本', versions.map(item => [item.version, `版本 ${item.version}${String(item.version) === String(script.version) ? ' · 当前' : ''}`]), script.version); const download = link('下载源码', '', 'button small'); const code = el('pre', {class: 'code', tabindex: '0', 'aria-label': '脚本源代码,只读'}, '正在读取源码…'); const info = el('div', {class: 'source-info mono'}); const feedback = el('div'); target.append(el('div', {class: 'panel-header'}, el('h2', {}, script.is_test ? '执行入口(代码只读)' : '内部源码(只读)'), scriptBadge(script)), kv([['来源类型', sourceLabel(script)], ['源文件', script.source_path], ['运行环境', profileLabel(script)], ['更新时间', date(script.updated_at)]]), el('div', {class: 'source-header'}, version, download), info, feedback, code, el('p', {class: 'source-info'}, '代码只读。查看历史版本不改变运行版本;“开始运行”使用当前已保存版本与配置。')); let generation = 0; const load = async () => { const ticket = ++generation; code.textContent = '正在读取源码…'; download.hidden = true; info.textContent = ''; feedback.replaceChildren(); try { const data = await api(`${scriptUrl(script.id)}/code?version=${enc(version.value)}`, {}, ctx.signal); if (!ctx.alive || ticket !== generation) return; code.textContent = data.code || '(空文件)'; info.textContent = `${data.filename} · v${version.value}\nSHA-256 ${data.sha256}`; download.href = `${scriptUrl(script.id)}/download?version=${enc(version.value)}`; download.hidden = Boolean(data.redacted); if (data.redacted) feedback.append(note('源码中包含敏感内容,预览已脱敏,下载不可用。')); if (data.truncated) feedback.append(note(data.redacted ? '文件较大,当前仅显示部分已脱敏源码。' : '文件较大,当前仅显示部分源码。可下载完整文件。')); } catch (error) { if (ctx.alive && ticket === generation) { code.textContent = ''; feedback.replaceChildren(errorBox(error, load)); } } }; version.addEventListener('change', load); load(); } function configForm(ctx, script, onDirty) { const config = script.config; const native = isNative(script); const limit = maxTimeout(script); const parameters = native ? nativeParameterForm(script) : null; const name = input({value: script.name, required: '', maxlength: 120}); const description = textarea({value: script.description || ''}); const args = textarea({value: JSON.stringify(config.args, null, 2), rows: 4, class: 'input mono', spellcheck: 'false'}); const cwd = input({value: native ? script.execution.cwd || config.cwd || '' : config.cwd || '', placeholder: script.kind === 'managed' ? '留空:使用本次运行目录' : '原脚本工作目录'}); const outputs = textarea({value: (config.output_paths || []).join('\n'), class: 'input mono', spellcheck: 'false', placeholder: '每行一个日志或结果路径'}); if (native) [args, cwd, outputs].forEach(control => control.setAttribute('readonly', '')); const timeout = input({type: 'number', value: config.timeout_seconds, required: '', min: '5', max: String(limit), step: '1'}); const enabled = input({type: 'checkbox', checked: script.enabled}); const feedback = el('div', {'aria-live': 'polite'}); const submit = el('button', {type: 'submit', class: 'button primary'}, '保存配置'); const form = el('form', {class: 'panel'}, el('div', {class: 'panel-header'}, el('h2', {}, '运行配置')), field('脚本名称', name), field('说明', description), parameters?.node, field(native ? '原入口参数(只读)' : '参数', args, native ? '参数由原环境方案计算;可调整的选项在上方填写,实际命令以本次预检为准。' : 'JSON 字符串数组,或每行一个参数;不会交给 shell 拼接。'), field(native ? '原工作目录(只读)' : '工作目录', cwd, native ? '固定使用登记的原目录。' : script.kind === 'managed' ? '托管脚本留空。可通过 XAS_RUN_DIR 获取本次结果目录。' : '必须位于已登记的来源目录内。'), field(native ? '原输出规则(只读)' : '归档输出路径', outputs, native ? '由固定方案管理;运行前预检显示本次输出规则,保留原脚本的输出行为。' : script.kind === 'managed' ? '托管脚本留空;本次结果目录统一归档,.log 文件的新增内容进入实时日志。' : '登记该来源 logs/ 或 results/ 内已存在的文件或目录,每行一项。.log 新增内容进入实时日志,其他结果结束后归档。'), field('超时时间(秒)', timeout, `填写 5–${limit} 秒之间的整数。${native ? '原环境方案最长支持 72 小时。' : ''}`), el('label', {class: 'checkbox'}, enabled, '启用此脚本'), native ? note('复用原账号、解释器、目录与已有配置,不修改原项目文件。实际运行按原脚本逻辑产生输出与影响。') : note(`${profileLabel(script)}。提供 Python 标准库、Bash 和 curl;额外依赖不会自动安装。`), !native && script.config.profile === 'legacy_smoke' ? note('兼容配置由服务端管理,只读。代理请求有固定上限,不能在此调整服务或模型。', 'info') : null, feedback, el('div', {class: 'form-footer'}, submit)); form.addEventListener('input', () => { onDirty(); feedback.replaceChildren(note('配置有未保存的修改。保存后才能开始运行。')); }); form.addEventListener('submit', async event => { event.preventDefault(); if (submit.disabled || !form.reportValidity()) return; feedback.replaceChildren(); let payload; try { const seconds = Number(timeout.value); if (!Number.isSafeInteger(seconds) || seconds < 5 || seconds > limit) throw new Error(`超时时间必须是 5–${limit} 秒之间的整数。`); if (!name.value.trim()) throw new Error('请填写脚本名称。'); payload = {name: name.value.trim(), description: description.value, enabled: enabled.checked, config: native ? {native_options: parameters.read(), timeout_seconds: seconds} : {args: parseArgs(args.value), cwd: cwd.value.trim(), output_paths: parseLines(outputs.value), timeout_seconds: seconds}}; } catch (error) { feedback.replaceChildren(errorBox(error)); return; } submit.disabled = true; submit.textContent = '正在保存…'; try { await api(scriptUrl(script.id), {method: 'PATCH', body: payload}, ctx.signal); if (ctx.alive) { toast('配置已保存,后续运行将使用新配置。'); await scriptDetail(ctx, script.id); } } catch (error) { if (ctx.alive) feedback.replaceChildren(errorBox(error)); } finally { submit.disabled = false; submit.textContent = '保存配置'; } }); return form; }