Add files via upload

This commit is contained in:
公明
2026-07-14 14:22:21 +08:00
committed by GitHub
parent 7d1e16b97b
commit 6bafb8fe70
6 changed files with 1322 additions and 106 deletions
+1021 -57
View File
File diff suppressed because it is too large Load Diff
+22 -1
View File
@@ -3131,18 +3131,35 @@
"enabledUpdateFailed": "Failed to update enabled status",
"namePlaceholder": "Basic Web scan",
"descriptionPlaceholder": "Optional",
"toolbarLabel": "Workflow editing tools",
"canvasTools": "Canvas tools",
"moreActions": "More",
"deleteWorkflow": "Delete workflow",
"package": {
"importLocal": "Import local package",
"export": "Export",
"title": "Import local package",
"preflightHint": "Preflight does not modify local workflows",
"preflightHint": "Review package contents and conflicts before you approve the import",
"safeBadge": "Safe preflight",
"stepInspection": "Upload and preflight",
"stepInspectionHint": "Validate format, content, and version",
"stepImport": "Resolve conflict and import",
"stepImportHint": "Choose a strategy and finish importing",
"stepsLabel": "Import steps",
"packageEyebrow": "Local workflow package",
"packageTitle": "Choose a package to import",
"decisionEyebrow": "Import strategy",
"pending": "Pending",
"ready": "Ready",
"footerNote": "Existing workflows stay unchanged until you confirm",
"selectFile": "Choose local package",
"dropTitle": "Drop a local package here",
"dropOr": "or",
"removeFile": "Remove file",
"noFile": "No file selected",
"fileHint": "One .csapkg.zip package only, up to 10 MiB.",
"inspect": "Upload and preflight",
"inspecting": "Running safe preflight…",
"resolutionTitle": "Resolution",
"confirmImport": "Confirm import",
"overwriteTitle": "Overwrite local workflow?",
@@ -3180,6 +3197,8 @@
"none": "No local workflow with the same ID was found. A new workflow can be created."
},
"summary": {
"title": "Preflight result",
"passed": "Checks passed",
"workflowName": "Workflow name",
"unnamedWorkflow": "Untitled workflow",
"sourceId": "Source workflow ID",
@@ -3191,7 +3210,9 @@
},
"resolution": {
"needInspection": "Choose a local package and complete preflight first.",
"createTitle": "Safe to create",
"createHint": "Import will create a new local workflow.",
"identicalTitle": "No duplicate needed",
"identicalHint": "The same content already exists. No duplicate import is needed.",
"keepExisting": "Keep local version",
"keepExistingHint": "Do not change the current local workflow; the import record is retained.",
+22 -1
View File
@@ -3119,18 +3119,35 @@
"enabledUpdateFailed": "更新启用状态失败",
"namePlaceholder": "基础 Web 扫描",
"descriptionPlaceholder": "可选",
"toolbarLabel": "流程编辑工具",
"canvasTools": "画布工具",
"moreActions": "更多",
"deleteWorkflow": "删除流程",
"package": {
"importLocal": "导入本地包",
"export": "导出",
"title": "导入本地包",
"preflightHint": "预检不会修改本地工作流",
"preflightHint": "先检查包内容与冲突,再由你确认是否导入",
"safeBadge": "安全预检",
"stepInspection": "上传与预检",
"stepInspectionHint": "校验格式、内容与版本",
"stepImport": "处理冲突与导入",
"stepImportHint": "确认处理方式并完成导入",
"stepsLabel": "导入步骤",
"packageEyebrow": "本地工作流包",
"packageTitle": "选择需要导入的文件",
"decisionEyebrow": "导入策略",
"pending": "待预检",
"ready": "可处理",
"footerNote": "导入前不会改动现有工作流",
"selectFile": "选择本地包",
"dropTitle": "拖拽本地包到这里",
"dropOr": "或",
"removeFile": "移除文件",
"noFile": "未选择文件",
"fileHint": "仅支持单个 .csapkg.zip,文件不得超过 10 MiB。",
"inspect": "上传并预检",
"inspecting": "正在安全预检…",
"resolutionTitle": "处理方式",
"confirmImport": "确认导入",
"overwriteTitle": "确认覆盖本地工作流?",
@@ -3168,6 +3185,8 @@
"none": "未发现同 ID 的本地工作流,可创建导入。"
},
"summary": {
"title": "预检结果",
"passed": "校验通过",
"workflowName": "工作流名称",
"unnamedWorkflow": "未命名工作流",
"sourceId": "源工作流 ID",
@@ -3179,7 +3198,9 @@
},
"resolution": {
"needInspection": "请先选择本地包并完成预检。",
"createTitle": "可以安全创建",
"createHint": "导入后会创建一个新的本地工作流。",
"identicalTitle": "无需重复导入",
"identicalHint": "内容已经存在,无需重复导入。",
"keepExisting": "保留本地版本",
"keepExistingHint": "不修改当前本地工作流;导入记录会保留。",
@@ -12,6 +12,18 @@ test('图编排提供导入、导出和覆盖确认容器', () => {
assert.equal(zh.workflows.package.importLocal, '导入本地包');
});
test('本地包上传支持拖拽、键盘选择和选中文件反馈', () => {
const html = fs.readFileSync('web/templates/index.html', 'utf8');
const workflows = fs.readFileSync('web/static/js/workflows.js', 'utf8');
const css = fs.readFileSync('web/static/css/style.css', 'utf8');
assert.match(html, /id="workflow-package-dropzone"[\s\S]*?ondrop="onWorkflowPackageDrop\(event\)"/);
assert.match(html, /onkeydown="onWorkflowPackageDropzoneKeydown\(event\)"/);
assert.match(html, /id="workflow-package-selected-file"/);
assert.match(workflows, /window\.onWorkflowPackageDrop = function \(event\)/);
assert.match(workflows, /endsWith\('\.csapkg\.zip'\)/);
assert.match(css, /\.workflow-package-dropzone\.is-dragging/);
});
test('工作流脚本调用包契约的全部端点与冲突错误码', () => {
const workflows = fs.readFileSync('web/static/js/workflows.js', 'utf8');
const client = fs.readFileSync('web/static/js/workflow-package-client.js', 'utf8');
+149 -24
View File
@@ -34,7 +34,8 @@
newWorkflowId: '',
riskChoicesVisible: false,
idempotencyKey: '',
requestSignature: ''
requestSignature: '',
dragDepth: 0
};
const WORKFLOW_PACKAGE_INSPECTION_STORAGE_KEY = 'csai.workflow-package.inspection-id';
const WORKFLOW_PACKAGE_IMPORT_STORAGE_KEY = 'csai.workflow-package.import-id';
@@ -826,6 +827,7 @@
const toggleTitle = esc(_t('workflows.toggleEnabled'));
const editTitle = esc(_t('workflows.editMeta'));
const enabled = wf.enabled !== false;
const statusText = esc(_t(enabled ? 'workflows.statusEnabled' : 'workflows.statusDisabled'));
return `
<div class="workflow-list-item ${isActive ? 'is-active' : ''}">
<button type="button" class="workflow-list-main" onclick="selectWorkflow(decodeURIComponent('${encodedId}'))">
@@ -833,10 +835,10 @@
<span class="workflow-list-meta">${esc(wf.id)} · v${wf.version || 1}</span>
</button>
<div class="workflow-list-actions">
<label class="workflow-switch" title="${toggleTitle}" onclick="event.stopPropagation()">
<input type="checkbox" ${enabled ? 'checked' : ''} aria-label="${toggleTitle}" onchange="toggleWorkflowEnabled(decodeURIComponent('${encodedId}'), this.checked)">
<span class="workflow-switch-slider" aria-hidden="true"></span>
</label>
<button type="button" class="workflow-status-toggle ${enabled ? 'is-enabled' : 'is-disabled'}" title="${toggleTitle}" aria-label="${toggleTitle}" aria-pressed="${enabled ? 'true' : 'false'}" onclick="event.stopPropagation(); toggleWorkflowEnabled(decodeURIComponent('${encodedId}'), ${enabled ? 'false' : 'true'})">
<span class="workflow-status-dot" aria-hidden="true"></span>
<span>${statusText}</span>
</button>
<button type="button" class="btn-icon workflow-list-edit" title="${editTitle}" aria-label="${editTitle}" onclick="event.stopPropagation(); editWorkflowFromList(decodeURIComponent('${encodedId}'))">${WORKFLOW_EDIT_ICON}</button>
</div>
</div>
@@ -1770,13 +1772,20 @@
const btn = document.getElementById('workflow-connect-btn');
if (btn) {
btn.classList.toggle('active', connectMode);
btn.textContent = connectMode ? _t('workflows.connecting') : _t('workflows.connect');
btn.setAttribute('aria-pressed', connectMode ? 'true' : 'false');
const label = btn.querySelector('.workflow-toolbar-label');
if (label) label.textContent = connectMode ? _t('workflows.connecting') : _t('workflows.connect');
}
if (typeof showNotification === 'function') {
showNotification(connectMode ? _t('workflows.connectModeOn') : _t('workflows.connectModeOff'), 'info');
}
};
window.closeWorkflowMoreActions = function () {
const menu = document.getElementById('workflow-more-actions');
if (menu) menu.removeAttribute('open');
};
window.deleteWorkflowSelection = function () {
if (!cy) return;
const selected = selectedElement && selectedElement.length ? selectedElement : cy.$(':selected');
@@ -1907,16 +1916,71 @@
return document.getElementById('workflow-package-submit-btn');
}
function workflowPackageFormatFileSize(bytes) {
const size = Number(bytes) || 0;
if (size < 1024) return `${size} B`;
if (size < 1024 * 1024) return `${(size / 1024).toFixed(size < 10 * 1024 ? 1 : 0)} KiB`;
return `${(size / (1024 * 1024)).toFixed(1)} MiB`;
}
function workflowPackageUpdateFileUI() {
const file = workflowPackageState.file;
const selected = document.getElementById('workflow-package-selected-file');
const name = document.getElementById('workflow-package-file-name');
const size = document.getElementById('workflow-package-file-size');
const inspect = document.getElementById('workflow-package-inspect-btn');
if (selected) selected.hidden = !file;
if (name) name.textContent = file ? file.name : '';
if (size) size.textContent = file ? workflowPackageFormatFileSize(file.size) : '';
if (inspect) inspect.disabled = !file;
}
function workflowPackageSetDragging(active) {
const dropzone = document.getElementById('workflow-package-dropzone');
if (dropzone) dropzone.classList.toggle('is-dragging', active);
}
function workflowPackageSelectFile(file) {
resetWorkflowPackageImport();
if (!file) {
renderWorkflowPackageResolution();
workflowPackageSetStep('inspection');
return;
}
if (file.size > 10 * 1024 * 1024) {
displayWorkflowPackageError({ code: 'WFPKG_FILE_TOO_LARGE' });
workflowPackageSetStep('inspection');
return;
}
if (!String(file.name || '').toLowerCase().endsWith('.csapkg.zip')) {
displayWorkflowPackageError({ code: 'WFPKG_UNSUPPORTED_FORMAT' });
workflowPackageSetStep('inspection');
return;
}
workflowPackageState.file = file;
workflowPackageUpdateFileUI();
renderWorkflowPackageResolution();
workflowPackageSetStep('inspection');
}
function workflowPackageSetStep(step) {
const inspectionStep = document.getElementById('workflow-package-step-inspection');
const importStep = document.getElementById('workflow-package-step-import');
if (inspectionStep) inspectionStep.classList.toggle('is-active', step === 'inspection');
if (importStep) importStep.classList.toggle('is-active', step === 'import');
if (inspectionStep) inspectionStep.classList.toggle('is-complete', step === 'import');
const decisionStatus = document.querySelector('#workflow-package-import-modal .workflow-package-decision-status');
if (decisionStatus) {
decisionStatus.classList.toggle('is-ready', step === 'import');
decisionStatus.textContent = step === 'import'
? workflowPackageText('workflows.package.ready', '可处理')
: workflowPackageText('workflows.package.pending', '待预检');
}
}
function workflowPackageSetStatus(target, message, type) {
if (!target) return;
target.innerHTML = `<div class="workflow-package-status${type ? ' is-' + esc(type) : ''}">${esc(message)}</div>`;
target.innerHTML = `<div class="workflow-package-status${type ? ' is-' + esc(type) : ''}"><span class="workflow-package-status-icon" aria-hidden="true"></span><span>${esc(message)}</span></div>`;
}
function workflowPackageErrorMessage(error) {
@@ -1975,9 +2039,9 @@
[workflowPackageText('workflows.package.summary.expiresAt', '预检有效期'), inspection.expires_at || '—']
];
if (!target) return;
target.innerHTML = `<div class="workflow-package-status is-${conflictCopy.type}">${esc(conflictCopy.message)}</div>` + rows.map(function (row) {
target.innerHTML = `<div class="workflow-package-summary-heading"><span>${esc(workflowPackageText('workflows.package.summary.title', '预检结果'))}</span><b>${esc(workflowPackageText('workflows.package.summary.passed', '校验通过'))}</b></div><div class="workflow-package-status is-${conflictCopy.type}"><span class="workflow-package-status-icon" aria-hidden="true"></span><span>${esc(conflictCopy.message)}</span></div><div class="workflow-package-summary-grid">` + rows.map(function (row) {
return `<div class="workflow-package-summary-row"><span>${esc(row[0])}</span><code title="${esc(row[1])}">${esc(row[1])}</code></div>`;
}).join('');
}).join('') + '</div>';
}
function workflowPackageResolutionCard(action, title, description, selected) {
@@ -2004,11 +2068,11 @@
}
const action = workflowPackageState.resolutionAction;
let html = '';
if (message) html += `<div class="workflow-package-status${type ? ' is-' + esc(type) : ''}">${esc(message)}</div>`;
if (message) html += `<div class="workflow-package-status${type ? ' is-' + esc(type) : ''}"><span class="workflow-package-status-icon" aria-hidden="true"></span><span>${esc(message)}</span></div>`;
if (conflictState === 'none') {
html += `<div class="workflow-package-status is-success">${esc(workflowPackageText('workflows.package.resolution.createHint', '导入后会创建一个新的本地工作流。'))}</div>`;
html += `<div class="workflow-package-resolution-hero is-success"><span class="workflow-package-resolution-icon" aria-hidden="true">✓</span><div><strong>${esc(workflowPackageText('workflows.package.resolution.createTitle', '可以安全创建'))}</strong><small>${esc(workflowPackageText('workflows.package.resolution.createHint', '导入后会创建一个新的本地工作流。'))}</small></div></div>`;
} else if (conflictState === 'identical') {
html += `<div class="workflow-package-status is-success">${esc(workflowPackageText('workflows.package.resolution.identicalHint', '内容已经存在,无需重复导入。'))}</div>`;
html += `<div class="workflow-package-resolution-hero is-success"><span class="workflow-package-resolution-icon" aria-hidden="true">✓</span><div><strong>${esc(workflowPackageText('workflows.package.resolution.identicalTitle', '无需重复导入'))}</strong><small>${esc(workflowPackageText('workflows.package.resolution.identicalHint', '内容已经存在,无需重复导入。'))}</small></div></div>`;
} else {
html += workflowPackageResolutionCard('keep_existing', workflowPackageText('workflows.package.resolution.keepExisting', '保留本地版本'), workflowPackageText('workflows.package.resolution.keepExistingHint', '不修改当前本地工作流;导入记录会保留。'), action === 'keep_existing');
if (!workflowPackageState.riskChoicesVisible) {
@@ -2040,6 +2104,7 @@
workflowPackageState.riskChoicesVisible = false;
workflowPackageState.idempotencyKey = '';
workflowPackageState.requestSignature = '';
workflowPackageState.dragDepth = 0;
if (!keepInspection) {
workflowPackageState.inspection = null;
workflowPackageState.resolutionAction = '';
@@ -2047,9 +2112,9 @@
}
workflowPackageStorageSet(WORKFLOW_PACKAGE_IMPORT_STORAGE_KEY, '');
const input = document.getElementById('workflow-package-file-input');
const name = document.getElementById('workflow-package-file-name');
if (input) input.value = '';
if (name) name.textContent = workflowPackageText('workflows.package.noFile', '未选择文件');
workflowPackageSetDragging(false);
workflowPackageUpdateFileUI();
}
function displayWorkflowPackageError(error) {
@@ -2119,7 +2184,7 @@
renderWorkflowPackageInspection();
renderWorkflowPackageResolution();
workflowPackageSetStep('inspection');
if (typeof openAppModal === 'function') openAppModal('workflow-package-import-modal', { focusEl: document.getElementById('workflow-package-inspect-btn') });
if (typeof openAppModal === 'function') openAppModal('workflow-package-import-modal', { focusEl: document.getElementById('workflow-package-dropzone') });
if (typeof window.applyTranslations === 'function') window.applyTranslations(document.getElementById('workflow-package-import-modal'));
};
@@ -2130,13 +2195,54 @@
window.onWorkflowPackageFileSelected = function () {
const input = document.getElementById('workflow-package-file-input');
const name = document.getElementById('workflow-package-file-name');
const file = input && input.files ? input.files[0] : null;
resetWorkflowPackageImport();
workflowPackageState.file = file || null;
if (name) name.textContent = file ? file.name : workflowPackageText('workflows.package.noFile', '未选择文件');
renderWorkflowPackageResolution();
workflowPackageSetStep('inspection');
workflowPackageSelectFile(file);
};
window.openWorkflowPackageFilePicker = function () {
const input = document.getElementById('workflow-package-file-input');
if (input) input.click();
};
window.onWorkflowPackageDropzoneKeydown = function (event) {
if (!event || (event.key !== 'Enter' && event.key !== ' ')) return;
event.preventDefault();
window.openWorkflowPackageFilePicker();
};
window.onWorkflowPackageDragEnter = function (event) {
if (event) event.preventDefault();
workflowPackageState.dragDepth += 1;
workflowPackageSetDragging(true);
};
window.onWorkflowPackageDragOver = function (event) {
if (!event) return;
event.preventDefault();
if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy';
workflowPackageSetDragging(true);
};
window.onWorkflowPackageDragLeave = function (event) {
if (event) event.preventDefault();
workflowPackageState.dragDepth = Math.max(0, workflowPackageState.dragDepth - 1);
if (!workflowPackageState.dragDepth) workflowPackageSetDragging(false);
};
window.onWorkflowPackageDrop = function (event) {
if (event) event.preventDefault();
workflowPackageState.dragDepth = 0;
workflowPackageSetDragging(false);
const files = event && event.dataTransfer && event.dataTransfer.files;
workflowPackageSelectFile(files && files.length ? files[0] : null);
};
window.clearWorkflowPackageFile = function (event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
workflowPackageSelectFile(null);
};
window.inspectWorkflowPackage = async function () {
@@ -2147,7 +2253,12 @@
displayWorkflowPackageError({ code: 'WFPKG_REQUEST_FAILED', message: workflowPackageText('workflows.package.errors.serviceUnavailable', '导入服务尚未准备好,请刷新页面后重试。') });
return;
}
if (button) button.disabled = true;
if (button) {
button.disabled = true;
button.classList.add('is-loading');
button.setAttribute('aria-busy', 'true');
button.textContent = workflowPackageText('workflows.package.inspecting', '正在安全预检…');
}
try {
const data = await client.createInspection(apiFetch, workflowPackageState.file);
workflowPackageState.inspection = data.inspection || data;
@@ -2161,7 +2272,12 @@
} catch (error) {
displayWorkflowPackageError(error);
} finally {
if (button) button.disabled = false;
if (button) {
button.disabled = !workflowPackageState.file;
button.classList.remove('is-loading');
button.removeAttribute('aria-busy');
button.textContent = workflowPackageText('workflows.package.inspect', '上传并预检');
}
}
};
@@ -2327,7 +2443,9 @@
});
const connectBtn = document.getElementById('workflow-connect-btn');
if (connectBtn) {
connectBtn.textContent = connectMode ? _t('workflows.connecting') : _t('workflows.connect');
connectBtn.setAttribute('aria-pressed', connectMode ? 'true' : 'false');
const label = connectBtn.querySelector('.workflow-toolbar-label');
if (label) label.textContent = connectMode ? _t('workflows.connecting') : _t('workflows.connect');
}
refreshCanvasLabels();
updateWorkflowCanvasTitle();
@@ -2351,4 +2469,11 @@
document.addEventListener('languagechange', function () {
refreshWorkflowsI18n();
});
document.addEventListener('click', function (event) {
const menu = document.getElementById('workflow-more-actions');
if (menu && menu.open && !menu.contains(event.target)) {
menu.removeAttribute('open');
}
});
})();
+96 -23
View File
@@ -2657,14 +2657,42 @@
<span id="workflow-canvas-subtitle" class="workflow-canvas-subtitle" hidden></span>
</div>
</div>
<div class="workflow-toolbar">
<button class="btn-secondary btn-small" data-require-permission="workflow:write" type="button" onclick="toggleWorkflowConnectMode()" id="workflow-connect-btn" data-i18n="workflows.connect">连线</button>
<button class="btn-secondary btn-small" data-require-permission="workflow:delete" type="button" onclick="deleteWorkflowSelection()" data-i18n="workflows.deleteSelected">删除选中</button>
<button class="btn-secondary btn-small" type="button" onclick="layoutWorkflowGraph()" data-i18n="workflows.autoLayout">自动布局</button>
<button class="btn-secondary btn-small" data-require-permission="workflow:execute" type="button" onclick="dryRunWorkflowDraft()" data-i18n="workflows.dryRun">试运行</button>
<button class="btn-secondary btn-small" data-require-permission="workflow:read" type="button" id="workflow-package-export-btn" onclick="exportCurrentWorkflowPackage()" data-i18n="workflows.package.export">导出</button>
<button class="btn-secondary btn-small" data-require-permission="workflow:delete" onclick="deleteCurrentWorkflow()" data-i18n="common.delete">删除</button>
<button class="btn-primary btn-small" data-require-permission="workflow:write" onclick="saveWorkflowDraft()" data-i18n="common.save">保存</button>
<div class="workflow-toolbar" role="toolbar" aria-label="流程编辑工具" data-i18n="workflows.toolbarLabel" data-i18n-attr="aria-label" data-i18n-skip-text="true">
<div class="workflow-toolbar-group" role="group" aria-label="画布工具" data-i18n="workflows.canvasTools" data-i18n-attr="aria-label" data-i18n-skip-text="true">
<button class="btn-secondary btn-small workflow-toolbar-button" data-require-permission="workflow:write" type="button" onclick="toggleWorkflowConnectMode()" id="workflow-connect-btn" aria-pressed="false">
<svg aria-hidden="true" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="2.5"/><circle cx="18" cy="18" r="2.5"/><path d="M8 8l8 8"/></svg>
<span class="workflow-toolbar-label" data-i18n="workflows.connect">连线</span>
</button>
<button class="btn-secondary btn-small workflow-toolbar-button" type="button" onclick="layoutWorkflowGraph()">
<svg aria-hidden="true" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="3" width="6" height="5" rx="1"/><rect x="14" y="16" width="6" height="5" rx="1"/><path d="M7 8v4h10v4"/></svg>
<span class="workflow-toolbar-label" data-i18n="workflows.autoLayout">自动布局</span>
</button>
</div>
<button class="btn-secondary btn-small workflow-toolbar-button workflow-run-button" data-require-permission="workflow:execute" type="button" onclick="dryRunWorkflowDraft()">
<svg aria-hidden="true" width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5.4v13.2a1 1 0 0 0 1.55.84l9.2-6.6a1 1 0 0 0 0-1.68l-9.2-6.6A1 1 0 0 0 8 5.4Z"/></svg>
<span class="workflow-toolbar-label" data-i18n="workflows.dryRun">试运行</span>
</button>
<details class="workflow-more-actions" id="workflow-more-actions" data-require-permission-any="workflow:read workflow:delete">
<summary class="btn-secondary btn-small workflow-more-actions-trigger" aria-haspopup="menu">
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.8"/><circle cx="12" cy="12" r="1.8"/><circle cx="19" cy="12" r="1.8"/></svg>
<span class="workflow-toolbar-label" data-i18n="workflows.moreActions">更多</span>
</summary>
<div class="workflow-more-actions-menu" role="menu">
<button data-require-permission="workflow:read" type="button" id="workflow-package-export-btn" role="menuitem" onclick="closeWorkflowMoreActions(); exportCurrentWorkflowPackage()">
<svg aria-hidden="true" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12m0 0 4-4m-4 4-4-4"/><path d="M5 19h14"/></svg>
<span data-i18n="workflows.package.export">导出</span>
</button>
<div class="workflow-more-actions-divider" role="separator"></div>
<button class="workflow-more-actions-danger" data-require-permission="workflow:delete" type="button" role="menuitem" onclick="closeWorkflowMoreActions(); deleteCurrentWorkflow()">
<svg aria-hidden="true" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M9 7V4h6v3m3 0-1 13H7L6 7"/><path d="M10 11v5m4-5v5"/></svg>
<span data-i18n="workflows.deleteWorkflow">删除流程</span>
</button>
</div>
</details>
<button class="btn-primary btn-small workflow-save-button" data-require-permission="workflow:write" type="button" onclick="saveWorkflowDraft()">
<svg aria-hidden="true" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4h12l2 2v14H5z"/><path d="M8 4v6h8V4M8 20v-6h8v6"/></svg>
<span data-i18n="common.save">保存</span>
</button>
</div>
</section>
<section class="workflow-canvas-wrap" ondragover="workflowCanvasDragOver(event)" ondrop="workflowCanvasDrop(event)">
@@ -4895,35 +4923,75 @@
<div id="workflow-package-import-modal" class="modal workflow-package-modal" style="display: none;" role="dialog" aria-modal="true" aria-labelledby="workflow-package-import-title">
<div class="modal-content workflow-package-modal-content">
<div class="modal-header workflow-package-modal-header">
<div>
<h2 id="workflow-package-import-title" data-i18n="workflows.package.title">导入本地包</h2>
<p class="workflow-package-subtitle" data-i18n="workflows.package.preflightHint">预检不会修改本地工作流</p>
<div class="workflow-package-title-group">
<span class="workflow-package-title-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12"/><path d="m7 10 5 5 5-5"/><path d="M5 19h14"/></svg>
</span>
<div>
<div class="workflow-package-title-line">
<h2 id="workflow-package-import-title" data-i18n="workflows.package.title">导入本地包</h2>
<span class="workflow-package-safe-badge" data-i18n="workflows.package.safeBadge">安全预检</span>
</div>
<p class="workflow-package-subtitle" data-i18n="workflows.package.preflightHint">先检查包内容与冲突,再由你确认是否导入</p>
</div>
</div>
<span class="modal-close" onclick="closeWorkflowPackageImportModal()" aria-label="关闭" data-i18n="common.close" data-i18n-attr="aria-label">&times;</span>
</div>
<div class="workflow-package-steps" aria-label="导入步骤" data-i18n="workflows.package.stepsLabel" data-i18n-attr="aria-label">
<span class="is-active" id="workflow-package-step-inspection"><b>1</b><span data-i18n="workflows.package.stepInspection">上传与预检</span></span>
<span id="workflow-package-step-import"><b>2</b><span data-i18n="workflows.package.stepImport">处理冲突与导入</span></span>
<span class="is-active" id="workflow-package-step-inspection"><b>1</b><span><strong data-i18n="workflows.package.stepInspection">上传与预检</strong><small data-i18n="workflows.package.stepInspectionHint">校验格式、内容与版本</small></span></span>
<span id="workflow-package-step-import"><b>2</b><span><strong data-i18n="workflows.package.stepImport">处理冲突与导入</strong><small data-i18n="workflows.package.stepImportHint">确认处理方式并完成导入</small></span></span>
</div>
<div class="modal-body workflow-package-modal-body">
<section class="workflow-package-pane workflow-package-pane--inspection">
<div class="workflow-package-file-row">
<input id="workflow-package-file-input" type="file" accept=".csapkg.zip,application/zip" hidden onchange="onWorkflowPackageFileSelected()">
<button type="button" class="btn-secondary" onclick="document.getElementById('workflow-package-file-input').click()" data-i18n="workflows.package.selectFile">选择本地包</button>
<span id="workflow-package-file-name" class="workflow-package-file-name" data-i18n="workflows.package.noFile">选择文件</span>
<div class="workflow-package-pane-heading">
<div>
<span class="workflow-package-eyebrow" data-i18n="workflows.package.packageEyebrow">本地工作流</span>
<h3 data-i18n="workflows.package.packageTitle">选择需要导入的文件</h3>
</div>
<span class="workflow-package-format-pill">.CSAPKG.ZIP</span>
</div>
<p class="workflow-package-file-hint" data-i18n="workflows.package.fileHint">仅支持单个 .csapkg.zip,文件不得超过 10 MiB。</p>
<button id="workflow-package-inspect-btn" type="button" class="btn-primary" onclick="inspectWorkflowPackage()" data-i18n="workflows.package.inspect">上传并预检</button>
<input id="workflow-package-file-input" type="file" accept=".csapkg.zip,application/zip" hidden onchange="onWorkflowPackageFileSelected()">
<div id="workflow-package-dropzone" class="workflow-package-dropzone" role="button" tabindex="0" aria-controls="workflow-package-file-input" aria-describedby="workflow-package-file-hint" onclick="openWorkflowPackageFilePicker()" onkeydown="onWorkflowPackageDropzoneKeydown(event)" ondragenter="onWorkflowPackageDragEnter(event)" ondragover="onWorkflowPackageDragOver(event)" ondragleave="onWorkflowPackageDragLeave(event)" ondrop="onWorkflowPackageDrop(event)">
<span class="workflow-package-upload-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M5 14v4a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-4"/></svg>
</span>
<span class="workflow-package-dropzone-copy">
<strong data-i18n="workflows.package.dropTitle">拖拽本地包到这里</strong>
<span><span data-i18n="workflows.package.dropOr"></span> <span class="workflow-package-browse" data-i18n="workflows.package.selectFile">浏览文件</span></span>
</span>
</div>
<div id="workflow-package-selected-file" class="workflow-package-selected-file" hidden>
<span class="workflow-package-file-badge" aria-hidden="true">ZIP</span>
<span class="workflow-package-file-details">
<strong id="workflow-package-file-name" class="workflow-package-file-name"></strong>
<small id="workflow-package-file-size"></small>
</span>
<button type="button" class="workflow-package-file-remove" onclick="clearWorkflowPackageFile(event)" aria-label="移除文件" title="移除文件" data-i18n="workflows.package.removeFile" data-i18n-attr="aria-label,title" data-i18n-skip-text="true">&times;</button>
</div>
<p id="workflow-package-file-hint" class="workflow-package-file-hint" data-i18n="workflows.package.fileHint">仅支持单个 .csapkg.zip,文件不得超过 10 MiB。</p>
<button id="workflow-package-inspect-btn" type="button" class="btn-primary workflow-package-inspect-btn" onclick="inspectWorkflowPackage()" disabled data-i18n="workflows.package.inspect">上传并预检</button>
<div id="workflow-package-inspection" class="workflow-package-summary" aria-live="polite"></div>
</section>
<section class="workflow-package-pane workflow-package-pane--resolution">
<h3 data-i18n="workflows.package.resolutionTitle">处理方式</h3>
<div class="workflow-package-pane-heading">
<div>
<span class="workflow-package-eyebrow" data-i18n="workflows.package.decisionEyebrow">导入策略</span>
<h3 data-i18n="workflows.package.resolutionTitle">处理方式</h3>
</div>
<span class="workflow-package-decision-status" data-i18n="workflows.package.pending">待预检</span>
</div>
<div id="workflow-package-resolution" class="workflow-package-resolution" aria-live="polite"></div>
</section>
</div>
<div class="modal-footer workflow-package-modal-footer">
<button type="button" class="btn-secondary" onclick="closeWorkflowPackageImportModal()" data-i18n="common.cancel">取消</button>
<button id="workflow-package-submit-btn" type="button" class="btn-primary" onclick="submitWorkflowPackageImport()" disabled data-i18n="workflows.package.confirmImport">确认导入</button>
<div class="workflow-package-footer-note">
<span aria-hidden="true"></span>
<span data-i18n="workflows.package.footerNote">导入前不会改动现有工作流</span>
</div>
<div class="workflow-package-footer-actions">
<button type="button" class="btn-secondary" onclick="closeWorkflowPackageImportModal()" data-i18n="common.cancel">取消</button>
<button id="workflow-package-submit-btn" type="button" class="btn-primary" onclick="submitWorkflowPackageImport()" disabled data-i18n="workflows.package.confirmImport">确认导入</button>
</div>
</div>
</div>
</div>
@@ -5930,7 +5998,12 @@
</section>
</div>
<div class="modal-footer robot-binding-footer">
<span>如需配置专用服务账号,请前往“系统设置 → 机器人设置”。</span>
<div class="robot-binding-service-hint">
<span class="robot-binding-service-hint-icon" aria-hidden="true">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.09a2 2 0 0 1 1 1.74v.5a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.38a2 2 0 0 0-.73-2.73l-.15-.09a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2Z" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.8"/></svg>
</span>
<span>如需配置专用服务账号,请前往“系统设置 → 机器人设置”。</span>
</div>
<button type="button" class="btn-secondary" onclick="closeRobotAccountBinding()">关闭</button>
</div>
</div>