feat(ql3): add optional console run drilldown

This commit is contained in:
whyour
2026-08-20 09:23:19 +08:00
parent cf21e984cb
commit 0a5f1448f1
29 changed files with 1193 additions and 68 deletions
@@ -234,7 +234,7 @@ h3 {
}
.mode-tabs {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(4, 1fr);
margin: 1.8rem 0 1.2rem;
border-bottom: 1px solid var(--line);
}
@@ -5,6 +5,11 @@
const routes = Object.freeze({
inspect: '/api/v1/copilot/inspect',
output: '/api/v1/copilot/output',
run_cancellation_status: '/api/v1/run-management/cancellation-status',
run_cancellation_blocked_list:
'/api/v1/run-management/blocked-cancellations',
run_cancellation_inspect:
'/api/v1/run-management/cancellation-inspect',
run_list: '/api/v1/observe/run-list',
run_read: '/api/v1/observe/run',
run_event_list: '/api/v1/observe/run-events',
@@ -20,6 +25,9 @@
const labels = Object.freeze({
inspect: 'Copilot 诊断状态',
output: 'Copilot 诊断内容',
run_cancellation_status: '取消可用性',
run_cancellation_blocked_list: 'Blocked Cancellations',
run_cancellation_inspect: '取消诊断',
run_list: 'Run 目录',
run_read: 'Run 详情',
run_event_list: 'Run Events',
@@ -110,6 +118,12 @@
if (operation === 'inspect' || operation === 'output') {
result.sourceRunId = value('source-run-id');
result.requestId = value('diagnosis-request-id');
} else if (operation === 'run_cancellation_status') {
return result;
} else if (operation === 'run_cancellation_blocked_list') {
result.cursor = null;
} else if (operation === 'run_cancellation_inspect') {
result.runId = value('cancellation-run-id');
} else if (operation === 'run_list') {
result.afterCreatedAtMs = null;
result.afterRunId = null;
@@ -156,7 +170,13 @@
const nextPage = function (operation, prior, fact) {
const next = Object.assign({}, prior, { requestId: requestId() });
if (operation === 'run_list' && fact.hasMore === true && fact.next) {
if (
operation === 'run_cancellation_blocked_list' &&
fact.truncated === true &&
typeof fact.nextCursor === 'string'
) {
next.cursor = fact.nextCursor;
} else if (operation === 'run_list' && fact.hasMore === true && fact.next) {
next.afterCreatedAtMs = fact.next.createdAtMs;
next.afterRunId = fact.next.runId;
} else if (
@@ -200,6 +220,39 @@
return next;
};
const appendDrilldownControls = function (entry, operation, fact) {
if (
operation === 'run_cancellation_status' &&
fact.operatorAction === 'inspect'
) {
const button = document.createElement('button');
button.type = 'button';
button.textContent = '显式读取 Blocked Runs';
button.addEventListener('click', function () {
void execute('run_cancellation_blocked_list');
});
entry.append(button);
return;
}
if (
operation !== 'run_cancellation_blocked_list' ||
!Array.isArray(fact.items)
) {
return;
}
fact.items.forEach(function (item) {
if (!item || typeof item.runId !== 'string') return;
const button = document.createElement('button');
button.type = 'button';
button.textContent = '显式检查 ' + item.runId;
button.addEventListener('click', function () {
document.getElementById('cancellation-run-id').value = item.runId;
void execute('run_cancellation_inspect');
});
entry.append(button);
});
};
const appendEvidence = function (operation, request, response) {
const fact = response.result.result;
const observedAtMs = Date.now();
@@ -235,6 +288,7 @@
});
entry.append(button);
}
appendDrilldownControls(entry, operation, fact);
ledger.prepend(entry);
evidenceRecords.push({ record: record, bytes: recordBytes, entry: entry });
evidenceBytes += recordBytes;
@@ -28,6 +28,9 @@
const operations = Object.freeze([
'inspect',
'output',
'run_cancellation_status',
'run_cancellation_blocked_list',
'run_cancellation_inspect',
'run_list',
'run_read',
'run_event_list',
@@ -44,6 +47,9 @@
const requestFields = Object.freeze({
inspect: ['projectId', 'requestId', 'sourceRunId'],
output: ['projectId', 'requestId', 'sourceRunId'],
run_cancellation_status: ['projectId', 'requestId'],
run_cancellation_blocked_list: ['cursor', 'projectId', 'requestId'],
run_cancellation_inspect: ['projectId', 'requestId', 'runId'],
run_list: [
'afterCreatedAtMs',
'afterRunId',
@@ -112,7 +118,9 @@
afterStepRunId: 'step',
afterTaskId: 'task',
artifactId: 'artifact',
attemptId: 'attempt',
contentDigest: 'digest',
cursor: 'cursor',
diagnosisRunId: 'run',
executionId: 'execution',
id: 'identifier',
@@ -133,7 +141,10 @@
});
const safeContainers = new Set([
'attempts',
'blockingResults',
'counts',
'dispatch',
'dispatches',
'events',
'items',
'metadata',
@@ -142,6 +153,7 @@
'run',
'runs',
'source',
'signals',
'step',
'steps',
'summary',
@@ -168,9 +180,15 @@
]);
const safeEnumKeys = new Set([
'finishReason',
'assessment',
'cancelReason',
'kind',
'lastResult',
'operation',
'operatorAction',
'outcome',
'runStatus',
'severity',
'stage',
'status',
]);
@@ -178,11 +196,15 @@
'accepted',
'active',
'admission',
'attention_required',
'available',
'blocked',
'cancelled',
'completed',
'completion',
'converging',
'critical',
'clear',
'dispatch',
'dispatching',
'disabled',
@@ -191,12 +213,18 @@
'failed',
'finalization',
'installed',
'inspect',
'invalid',
'identity_mismatch',
'local',
'lost',
'missing',
'model',
'none',
'not_found',
'pending',
'pid_mismatch',
'policy',
'post_model',
'pre_model',
'prompt',
@@ -204,6 +232,8 @@
'queued',
'ready',
'recovery',
'rearm',
'reconcile',
'rejected',
'remote',
'retained',
@@ -217,13 +247,20 @@
'step',
'stop',
'succeeded',
'shutdown',
'system',
'task',
'terminal',
'timed_out',
'timeout',
'tool',
'trigger',
'unknown',
'unsupported',
'user',
'wait',
'warning',
'ok',
'unavailable',
'workflow',
]);
@@ -232,7 +269,7 @@
const freeTextKey =
/text|content|stdout|stderr|command|input|output|environment|reason|error|message|description|name|path|url|uri|host|endpoint/iu;
const numericKey =
/^(?:schemaVersion|version|revision|sequence|attempt|priority|limit|offset|size|total|count|[A-Za-z0-9_]*(?:AtMs|TimeMs|DurationMs|Bytes|Tokens|Micros|Sequence|Version|Count|Limit|Offset|Size|Total))$/u;
/^(?:schemaVersion|version|revision|sequence|attempt|priority|limit|offset|size|total|count|exitCode|pending|leased|retryWait|dispatched|blocked|due|expiredLease|identityMismatch|pidMismatch|unsupported|invalid|[A-Za-z0-9_]*(?:AtMs|TimeMs|DurationMs|Bytes|Tokens|Micros|Sequence|Version|Count|Limit|Offset|Size|Total))$/u;
const schemaValue = /^[a-z0-9][a-z0-9./_-]{0,126}@[a-z0-9._-]{1,16}$/u;
class ClusterConsoleEvidenceBundleError extends TypeError {
@@ -24,7 +24,7 @@
<div class="boundary" aria-label="当前权限边界">
<span class="boundary-dot" aria-hidden="true"></span>
<span>本机只读 BFF</span>
<strong>Run · Task · Workflow · Copilot</strong>
<strong>Run · Task · Workflow · Copilot · Optional management</strong>
</div>
</header>
@@ -51,6 +51,7 @@
<nav class="mode-tabs" aria-label="观察面">
<button type="button" class="mode-tab active" data-panel="runtime-panel" aria-pressed="true">运行态</button>
<button type="button" class="mode-tab" data-panel="management-panel" aria-pressed="false">取消可用性</button>
<button type="button" class="mode-tab" data-panel="workflow-panel" aria-pressed="false">工作流</button>
<button type="button" class="mode-tab" data-panel="copilot-panel" aria-pressed="false">Copilot</button>
</nav>
@@ -79,6 +80,25 @@
</div>
</section>
<section id="management-panel" class="mode-panel" hidden>
<div class="control-group">
<div class="control-title"><span>01</span><strong>Project 状态</strong></div>
<button type="button" class="primary" data-read="run_cancellation_status">读取取消可用性</button>
<p class="field-note">只有启动进程显式提供独立 Run management 配置与短期 assertion 时可用;页面不会自动刷新。</p>
</div>
<div class="control-group">
<div class="control-title"><span>02</span><strong>Blocked Runs</strong></div>
<button type="button" data-read="run_cancellation_blocked_list">读取首屏 Blocked Runs</button>
<p class="field-note">固定 16 项快照页。下一页必须在证据条目中再次显式点击。</p>
</div>
<div class="control-group">
<div class="control-title"><span>03</span><strong>单 Run 诊断</strong></div>
<input id="cancellation-run-id" type="text" maxlength="128" autocomplete="off" spellcheck="false" placeholder="run-id" />
<button type="button" data-read="run_cancellation_inspect">读取取消诊断</button>
<p class="field-note">该只读面没有 rearm、stop、retry 或其他 mutation 入口。</p>
</div>
</section>
<section id="workflow-panel" class="mode-panel" hidden>
<label class="field-label" for="package-name">Package</label>
<input id="package-name" type="text" maxlength="63" autocomplete="off" spellcheck="false" placeholder="ops-package" />
@@ -110,7 +130,7 @@
<aside class="trust-note">
<span>Authority boundary</span>
<p>Cluster credential 只由本机进程从私有文件读取。浏览器无法提交任意路径,也没有 start、cancel 或 diagnose 权限入口。脱敏导出只处理本页已读事实,不补读或上传。</p>
<p>Project credential 与可选 Run management authority 只由本机进程从彼此独立的私有文件读取。浏览器无法提交任意路径,也没有 start、stop、retry、rearm 或 diagnose 权限入口。脱敏导出只处理本页已读事实,不补读或上传。</p>
</aside>
</aside>
@@ -141,7 +161,7 @@
<footer>
<span>Loopback only · explicit reads · zero polling</span>
<span>QingLong 3.0 incubation / D-330</span>
<span>QingLong 3.0 incubation / D-369</span>
</footer>
</div>
</body>