mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 12:05:27 +08:00
feat(ql3): add request-scoped console task creation
This commit is contained in:
@@ -35,7 +35,8 @@ body {
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
input,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
@@ -45,6 +46,7 @@ button {
|
||||
|
||||
button:focus-visible,
|
||||
input:focus-visible,
|
||||
textarea:focus-visible,
|
||||
[tabindex]:focus-visible {
|
||||
outline: 3px solid var(--amber);
|
||||
outline-offset: 3px;
|
||||
@@ -321,6 +323,14 @@ input:focus-visible,
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.masthead-actions {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.refresh-button {
|
||||
flex: 0 0 auto;
|
||||
color: var(--deep);
|
||||
@@ -700,6 +710,155 @@ input:focus-visible,
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.task-editor-dialog,
|
||||
.presence-dialog {
|
||||
padding: 0;
|
||||
color: var(--ink);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0;
|
||||
background: var(--paper);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.task-editor-dialog {
|
||||
width: min(720px, calc(100vw - 32px));
|
||||
}
|
||||
|
||||
.presence-dialog {
|
||||
width: min(520px, calc(100vw - 32px));
|
||||
}
|
||||
|
||||
.task-editor-dialog::backdrop,
|
||||
.presence-dialog::backdrop {
|
||||
background: rgba(11, 31, 36, 0.76);
|
||||
}
|
||||
|
||||
.task-editor-dialog form,
|
||||
.presence-dialog form {
|
||||
padding: clamp(22px, 4vw, 34px);
|
||||
}
|
||||
|
||||
.dialog-heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.task-editor-dialog h2,
|
||||
.presence-dialog h2 {
|
||||
margin: 0;
|
||||
font-family: 'Avenir Next Condensed', 'Arial Narrow', sans-serif;
|
||||
font-size: 34px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.editor-intro,
|
||||
.presence-dialog form > p:not(.eyebrow),
|
||||
.editor-note,
|
||||
.presence-expiry {
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.editor-grid {
|
||||
margin-top: 24px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.editor-grid label,
|
||||
.presence-input {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.editor-grid label > span,
|
||||
.presence-input > span {
|
||||
color: var(--muted);
|
||||
font: 700 10px/1.2 ui-monospace, 'SFMono-Regular', Consolas, monospace;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.editor-grid input:not([type='checkbox']),
|
||||
.editor-grid textarea,
|
||||
.presence-input input {
|
||||
width: 100%;
|
||||
padding: 11px 12px;
|
||||
color: var(--ink);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 0;
|
||||
background: var(--white);
|
||||
font: 500 13px/1.45 ui-monospace, 'SFMono-Regular', Consolas, monospace;
|
||||
}
|
||||
|
||||
.editor-grid textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.editor-wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.editor-check {
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.editor-check input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--signal);
|
||||
}
|
||||
|
||||
.editor-note {
|
||||
margin: 18px 0 0;
|
||||
padding-left: 12px;
|
||||
border-left: 3px solid var(--amber);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.proof-ticket {
|
||||
position: relative;
|
||||
margin: 22px 0;
|
||||
padding: 18px 16px 18px 22px;
|
||||
display: block;
|
||||
color: var(--white);
|
||||
border-left: 6px solid var(--signal);
|
||||
background: var(--deep);
|
||||
font: 650 12px/1.5 ui-monospace, 'SFMono-Regular', Consolas, monospace;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.proof-ticket::after {
|
||||
content: 'HOST FILE';
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 10px;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
font-size: 8px;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
.presence-expiry {
|
||||
margin-bottom: 0;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.presence-error {
|
||||
margin: 14px 0 0;
|
||||
padding: 10px 12px;
|
||||
color: var(--danger);
|
||||
border-left: 3px solid var(--danger);
|
||||
background: rgba(186, 74, 67, 0.08);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.confirmation-dialog::backdrop {
|
||||
background: rgba(11, 31, 36, 0.7);
|
||||
}
|
||||
@@ -813,6 +972,11 @@ input:focus-visible,
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.masthead-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.masthead h2 {
|
||||
font-size: 40px;
|
||||
}
|
||||
@@ -836,6 +1000,15 @@ input:focus-visible,
|
||||
.record-side {
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.editor-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.editor-wide,
|
||||
.editor-check {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
'use strict';
|
||||
|
||||
const PROJECT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const TASK_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
const TOKEN_PATTERN =
|
||||
/^ql3c_[A-Za-z0-9][A-Za-z0-9._:-]{0,63}_[A-Za-z0-9_-]{43}$/;
|
||||
const PRESENCE_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
||||
const LOG_READ_BYTES = 32 * 1024;
|
||||
const TERMINAL = new Set(['succeeded', 'failed', 'cancelled', 'timed_out']);
|
||||
const STATUS_LABELS = Object.freeze({
|
||||
@@ -30,6 +32,16 @@
|
||||
run_step_list_unavailable: 'Workflow Step 暂时不可用。',
|
||||
task_start_fence_rejected:
|
||||
'任务在确认期间发生变化,本次启动已安全拒绝。请刷新后重试。',
|
||||
local_presence_rejected:
|
||||
'本机证明不匹配或已过期。请核对文件;过期后关闭窗口并重新保存。',
|
||||
local_presence_unavailable:
|
||||
'暂时无法生成本机证明。请检查部署数据目录权限。',
|
||||
strong_authentication_required:
|
||||
'当前凭据不能执行管理操作;请使用本机 User API Credential。',
|
||||
task_definition_fence_rejected:
|
||||
'Task 或授权在确认期间发生变化。请刷新后重新创建。',
|
||||
invalid_task_definition: 'Task 定义无效。请检查 ID、命令与参数。',
|
||||
task_definition_unavailable: 'Task 暂时无法保存。请检查数据库状态。',
|
||||
run_cancellation_fence_rejected:
|
||||
'运行在确认期间发生变化,本次取消已安全拒绝。请刷新后重试。',
|
||||
request_unavailable: '本次请求没有完成,请确认服务仍在运行。',
|
||||
@@ -49,10 +61,29 @@
|
||||
title: document.getElementById('section-title'),
|
||||
description: document.getElementById('section-description'),
|
||||
refresh: document.getElementById('refresh-button'),
|
||||
createTask: document.getElementById('create-task-button'),
|
||||
dialog: document.getElementById('confirmation-dialog'),
|
||||
dialogTitle: document.getElementById('confirmation-title'),
|
||||
dialogCopy: document.getElementById('confirmation-copy'),
|
||||
dialogAccept: document.getElementById('confirmation-accept'),
|
||||
taskEditor: document.getElementById('task-editor-dialog'),
|
||||
taskEditorForm: document.getElementById('task-editor-form'),
|
||||
taskEditorClose: document.getElementById('task-editor-close'),
|
||||
taskEditorSave: document.getElementById('task-editor-save'),
|
||||
taskId: document.getElementById('task-id-input'),
|
||||
taskName: document.getElementById('task-name-input'),
|
||||
taskDescription: document.getElementById('task-description-input'),
|
||||
taskCommand: document.getElementById('task-command-input'),
|
||||
taskArgs: document.getElementById('task-args-input'),
|
||||
taskEnabled: document.getElementById('task-enabled-input'),
|
||||
presenceDialog: document.getElementById('presence-dialog'),
|
||||
presenceForm: document.getElementById('presence-form'),
|
||||
presenceFile: document.getElementById('presence-file'),
|
||||
presenceProof: document.getElementById('presence-proof-input'),
|
||||
presenceExpiry: document.getElementById('presence-expiry'),
|
||||
presenceError: document.getElementById('presence-error'),
|
||||
presenceCancel: document.getElementById('presence-cancel'),
|
||||
presenceSubmit: document.getElementById('presence-submit'),
|
||||
toast: document.getElementById('toast'),
|
||||
});
|
||||
|
||||
@@ -62,6 +93,7 @@
|
||||
view: 'tasks',
|
||||
selectedId: null,
|
||||
pendingAction: null,
|
||||
pendingTaskMutation: null,
|
||||
toastTimer: null,
|
||||
};
|
||||
|
||||
@@ -169,6 +201,9 @@
|
||||
body = JSON.stringify(options.body);
|
||||
headers['content-type'] = 'application/json';
|
||||
}
|
||||
if (options.presence !== undefined) {
|
||||
headers['x-qinglong-local-presence'] = options.presence;
|
||||
}
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(path, {
|
||||
@@ -193,7 +228,7 @@
|
||||
response.headers.get('x-request-id'),
|
||||
);
|
||||
}
|
||||
if (!response.ok) {
|
||||
if (!response.ok && response.status !== options.acceptStatus) {
|
||||
throw new ConsoleRequestError(
|
||||
typeof value.code === 'string' ? value.code : 'request_unavailable',
|
||||
response.status,
|
||||
@@ -310,6 +345,148 @@
|
||||
nodes.dialog.showModal();
|
||||
}
|
||||
|
||||
function openTaskEditor() {
|
||||
nodes.taskEditorForm.reset();
|
||||
nodes.taskCommand.value = '/bin/echo';
|
||||
nodes.taskEnabled.checked = true;
|
||||
nodes.taskEditor.returnValue = '';
|
||||
nodes.taskEditor.showModal();
|
||||
nodes.taskId.focus();
|
||||
}
|
||||
|
||||
function taskDraft() {
|
||||
const taskId = nodes.taskId.value.trim();
|
||||
const name = nodes.taskName.value.trim();
|
||||
const description = nodes.taskDescription.value.trim();
|
||||
const file = nodes.taskCommand.value.trim();
|
||||
const args = nodes.taskArgs.value
|
||||
.split(/\r?\n/u)
|
||||
.map((value) => value.trim())
|
||||
.filter((value) => value.length > 0);
|
||||
if (!TASK_PATTERN.test(taskId)) {
|
||||
throw new TypeError('Task ID 格式无效。');
|
||||
}
|
||||
if (!name || !file || args.length > 128) {
|
||||
throw new TypeError('名称、命令或参数数量无效。');
|
||||
}
|
||||
return Object.freeze({
|
||||
taskId,
|
||||
body: Object.freeze({
|
||||
expectedRevision: null,
|
||||
mutationId: newMutationId(),
|
||||
name,
|
||||
...(description ? { description } : {}),
|
||||
kind: 'command',
|
||||
spec: Object.freeze({
|
||||
schema: 'qinglong/command@v1',
|
||||
config: Object.freeze({
|
||||
command: Object.freeze({ kind: 'argv', file, args }),
|
||||
}),
|
||||
}),
|
||||
labels: Object.freeze({ 'qinglong.source': 'local-console' }),
|
||||
enabled: nodes.taskEnabled.checked,
|
||||
occurredAtMs: Date.now(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function showPresenceChallenge(mutation, challenge) {
|
||||
if (
|
||||
challenge?.code !== 'local_presence_required' ||
|
||||
typeof challenge.proofFileName !== 'string' ||
|
||||
!/^[0-9a-f-]{36}\.json$/u.test(challenge.proofFileName) ||
|
||||
!Number.isSafeInteger(challenge.expiresAtMs)
|
||||
) {
|
||||
throw new ConsoleRequestError('response_unavailable', 503, null);
|
||||
}
|
||||
state.pendingTaskMutation = Object.freeze({ mutation, challenge });
|
||||
nodes.presenceFile.textContent = `console-presence/${challenge.proofFileName}`;
|
||||
nodes.presenceExpiry.textContent = `证明将在 ${formatTime(
|
||||
challenge.expiresAtMs,
|
||||
)} 失效;内容改变后必须重新生成。`;
|
||||
nodes.presenceProof.value = '';
|
||||
nodes.presenceError.textContent = '';
|
||||
nodes.presenceError.hidden = true;
|
||||
nodes.taskEditor.close();
|
||||
nodes.presenceDialog.returnValue = '';
|
||||
nodes.presenceDialog.showModal();
|
||||
nodes.presenceProof.focus();
|
||||
}
|
||||
|
||||
async function saveTaskDraft() {
|
||||
let mutation;
|
||||
try {
|
||||
mutation = taskDraft();
|
||||
} catch (error) {
|
||||
showToast(
|
||||
error instanceof Error ? error.message : 'Task 定义无效。',
|
||||
'error',
|
||||
);
|
||||
return;
|
||||
}
|
||||
nodes.taskEditorSave.disabled = true;
|
||||
try {
|
||||
const value = await api(
|
||||
`/api/v3/projects/${state.project}/tasks/${mutation.taskId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: mutation.body,
|
||||
acceptStatus: 428,
|
||||
},
|
||||
);
|
||||
if (value.code === 'local_presence_required') {
|
||||
showPresenceChallenge(mutation, value);
|
||||
return;
|
||||
}
|
||||
throw new ConsoleRequestError('response_unavailable', 503, null);
|
||||
} catch (error) {
|
||||
showToast(describeError(error), 'error');
|
||||
} finally {
|
||||
nodes.taskEditorSave.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function completeTaskMutation() {
|
||||
const pending = state.pendingTaskMutation;
|
||||
const proof = nodes.presenceProof.value.trim();
|
||||
if (!pending || !PRESENCE_PATTERN.test(proof)) {
|
||||
nodes.presenceError.textContent =
|
||||
'proof 格式无效。请完整复制私有文件中的 proof 字段。';
|
||||
nodes.presenceError.hidden = false;
|
||||
nodes.presenceProof.focus();
|
||||
return;
|
||||
}
|
||||
nodes.presenceSubmit.disabled = true;
|
||||
nodes.presenceError.hidden = true;
|
||||
try {
|
||||
const value = await api(
|
||||
`/api/v3/projects/${state.project}/tasks/${pending.mutation.taskId}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: pending.mutation.body,
|
||||
presence: proof,
|
||||
},
|
||||
);
|
||||
state.pendingTaskMutation = null;
|
||||
nodes.presenceProof.value = '';
|
||||
nodes.presenceDialog.close();
|
||||
showToast(
|
||||
value.status === 'existing'
|
||||
? '已找到同一 Task 请求。'
|
||||
: 'Task 已创建。',
|
||||
);
|
||||
state.selectedId = pending.mutation.taskId;
|
||||
await refresh();
|
||||
await selectTask(pending.mutation.taskId);
|
||||
} catch (error) {
|
||||
nodes.presenceError.textContent = describeError(error);
|
||||
nodes.presenceError.hidden = false;
|
||||
nodes.presenceProof.select();
|
||||
} finally {
|
||||
nodes.presenceSubmit.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function renderTasks() {
|
||||
const value = await api(`/api/v3/projects/${state.project}/tasks?limit=64`);
|
||||
const tasks = Array.isArray(value.tasks) ? value.tasks : [];
|
||||
@@ -595,7 +772,11 @@
|
||||
unavailable: '日志暂时不可用;Run 状态与 Event 仍可独立核验。',
|
||||
};
|
||||
section.append(
|
||||
element('p', 'run-log-placeholder', labels[logView.status] || labels.unavailable),
|
||||
element(
|
||||
'p',
|
||||
'run-log-placeholder',
|
||||
labels[logView.status] || labels.unavailable,
|
||||
),
|
||||
);
|
||||
return section;
|
||||
}
|
||||
@@ -702,11 +883,13 @@
|
||||
else button.removeAttribute('aria-current');
|
||||
}
|
||||
if (state.view === 'tasks') {
|
||||
nodes.createTask.hidden = false;
|
||||
nodes.kicker.textContent = 'Project task authority';
|
||||
nodes.title.textContent = '任务调度台';
|
||||
nodes.description.textContent =
|
||||
'查看当前 Task revision 与内容围栏。运行前会再次读取详情并要求显式确认。';
|
||||
'创建命令 Task,查看当前 revision 与内容围栏。管理写入需要部署设备上的一次性本机证明。';
|
||||
} else {
|
||||
nodes.createTask.hidden = true;
|
||||
nodes.kicker.textContent = 'Durable run evidence';
|
||||
nodes.title.textContent = '运行事实账本';
|
||||
nodes.description.textContent =
|
||||
@@ -750,6 +933,9 @@
|
||||
state.token = null;
|
||||
state.selectedId = null;
|
||||
state.pendingAction = null;
|
||||
state.pendingTaskMutation = null;
|
||||
if (nodes.taskEditor.open) nodes.taskEditor.close();
|
||||
if (nodes.presenceDialog.open) nodes.presenceDialog.close();
|
||||
nodes.token.value = '';
|
||||
nodes.token.disabled = false;
|
||||
nodes.project.disabled = false;
|
||||
@@ -757,6 +943,7 @@
|
||||
nodes.disconnect.hidden = true;
|
||||
nodes.nav.hidden = true;
|
||||
nodes.refresh.hidden = true;
|
||||
nodes.createTask.hidden = true;
|
||||
setConnection('idle', '等待凭据');
|
||||
nodes.kicker.textContent = 'Connection gate';
|
||||
nodes.title.textContent = '先建立一条本机连接';
|
||||
@@ -793,6 +980,23 @@
|
||||
|
||||
nodes.disconnect.addEventListener('click', disconnect);
|
||||
nodes.refresh.addEventListener('click', refresh);
|
||||
nodes.createTask.addEventListener('click', openTaskEditor);
|
||||
nodes.taskEditorClose.addEventListener('click', () =>
|
||||
nodes.taskEditor.close(),
|
||||
);
|
||||
nodes.taskEditorForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
await saveTaskDraft();
|
||||
});
|
||||
nodes.presenceCancel.addEventListener('click', () => {
|
||||
state.pendingTaskMutation = null;
|
||||
nodes.presenceProof.value = '';
|
||||
nodes.presenceDialog.close();
|
||||
});
|
||||
nodes.presenceForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
await completeTaskMutation();
|
||||
});
|
||||
|
||||
for (const button of nodes.nav.querySelectorAll('button[data-view]')) {
|
||||
button.addEventListener('click', () => {
|
||||
@@ -820,7 +1024,10 @@
|
||||
event.ctrlKey ||
|
||||
event.altKey ||
|
||||
event.target instanceof HTMLInputElement ||
|
||||
nodes.dialog.open
|
||||
event.target instanceof HTMLTextAreaElement ||
|
||||
nodes.dialog.open ||
|
||||
nodes.taskEditor.open ||
|
||||
nodes.presenceDialog.open
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,9 +87,14 @@
|
||||
输入 quickstart 交付的 Owner API Credential。连接成功后才能读取或执行操作。
|
||||
</p>
|
||||
</div>
|
||||
<button class="refresh-button" id="refresh-button" type="button" hidden>
|
||||
<span aria-hidden="true">↻</span> 刷新
|
||||
</button>
|
||||
<div class="masthead-actions">
|
||||
<button class="action-button" id="create-task-button" type="button" hidden>
|
||||
<span aria-hidden="true">+</span> 创建任务
|
||||
</button>
|
||||
<button class="refresh-button" id="refresh-button" type="button" hidden>
|
||||
<span aria-hidden="true">↻</span> 刷新
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="workspace-grid">
|
||||
@@ -125,6 +130,89 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="task-editor-dialog" class="task-editor-dialog">
|
||||
<form id="task-editor-form" autocomplete="off">
|
||||
<div class="dialog-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Task authoring</p>
|
||||
<h2>创建命令任务</h2>
|
||||
</div>
|
||||
<button class="quiet-button" id="task-editor-close" type="button">关闭</button>
|
||||
</div>
|
||||
<p class="editor-intro">
|
||||
定义会先绑定到一次本机证明,再以同一事务写入 Task revision 与安全审计。
|
||||
</p>
|
||||
<div class="editor-grid">
|
||||
<label>
|
||||
<span>Task ID</span>
|
||||
<input id="task-id-input" maxlength="128" spellcheck="false" required />
|
||||
</label>
|
||||
<label>
|
||||
<span>名称</span>
|
||||
<input id="task-name-input" maxlength="160" required />
|
||||
</label>
|
||||
<label class="editor-wide">
|
||||
<span>说明(可选)</span>
|
||||
<input id="task-description-input" maxlength="1000" />
|
||||
</label>
|
||||
<label class="editor-wide">
|
||||
<span>可执行文件</span>
|
||||
<input
|
||||
id="task-command-input"
|
||||
value="/bin/echo"
|
||||
maxlength="4096"
|
||||
spellcheck="false"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="editor-wide">
|
||||
<span>参数 · 每行一个</span>
|
||||
<textarea id="task-args-input" rows="5" maxlength="16384" spellcheck="false"></textarea>
|
||||
</label>
|
||||
<label class="editor-check">
|
||||
<input id="task-enabled-input" type="checkbox" checked />
|
||||
<span>创建后允许运行</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="editor-note">
|
||||
Alpha 当前从 Console 创建 <code>qinglong/command@v1</code>;高级 Task schema 仍使用受信任管理入口。
|
||||
</p>
|
||||
<div class="dialog-actions">
|
||||
<button class="primary-button" id="task-editor-save" type="submit">
|
||||
保存并生成本机证明
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="presence-dialog" class="presence-dialog">
|
||||
<form id="presence-form" autocomplete="off">
|
||||
<p class="eyebrow">Local presence · 02:00</p>
|
||||
<h2>从部署设备取得证明</h2>
|
||||
<p>
|
||||
使用部署 QingLong 的系统用户读取下面的私有文件。证明只绑定这次 Task 内容,且只能使用一次。
|
||||
</p>
|
||||
<code class="proof-ticket" id="presence-file"></code>
|
||||
<label class="presence-input">
|
||||
<span>文件中的 proof</span>
|
||||
<input
|
||||
id="presence-proof-input"
|
||||
type="password"
|
||||
maxlength="128"
|
||||
spellcheck="false"
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<p class="presence-expiry" id="presence-expiry"></p>
|
||||
<p class="presence-error" id="presence-error" role="alert" hidden></p>
|
||||
<div class="dialog-actions">
|
||||
<button class="quiet-button" id="presence-cancel" type="button">取消</button>
|
||||
<button class="primary-button" id="presence-submit" type="submit">验证并创建</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user