feat(工作流):支持本地图编排策略包导入导出 (#195)

* docs: define local workflow package mvp

* docs: fix workflow package api contract

* feat: add local workflow package mvp

* feat(workflow): add package API client

* feat(workflow): add package import interface

* feat(workflow): connect package import flow

* fix(workflow): map package validation errors

* fix(workflow): handle import key generation errors

* feat(workflow): localize package import states

* fix(workflow): reset package import modal on open

---------

Co-authored-by: ruanmingchen <“ruanm@chenchen”>
This commit is contained in:
chenchen
2026-07-14 11:28:37 +08:00
committed by GitHub
parent 2e5c1ff286
commit acfacfe1b3
24 changed files with 3154 additions and 3 deletions
+117
View File
@@ -0,0 +1,117 @@
(function (root, factory) {
const client = factory(root);
if (typeof module === 'object' && module.exports) module.exports = client;
if (root) root.WorkflowPackageClient = client;
})(typeof globalThis !== 'undefined' ? globalThis : this, function (root) {
const ACTIONS_BY_CONFLICT = {
none: ['create'],
identical: ['keep_existing'],
id_conflict: ['keep_existing', 'overwrite', 'rename']
};
class WorkflowPackageError extends Error {
constructor(code, message, details) {
super(message || code || 'WFPKG_REQUEST_FAILED');
this.name = 'WorkflowPackageError';
this.code = code || '';
this.details = details || {};
}
}
function allowedActions(conflictState) {
return (ACTIONS_BY_CONFLICT[conflictState] || []).slice();
}
function buildImportRequest(input) {
const data = input || {};
const inspectionId = String(data.inspectionId || '').trim();
const action = String(data.action || '').trim();
const newWorkflowId = String(data.newWorkflowId || '').trim();
const confirmOverwrite = data.confirmOverwrite === true;
if (!inspectionId) throw new WorkflowPackageError('WFPKG_INSPECTION_REQUIRED', '缺少预检记录');
if (!['create', 'keep_existing', 'overwrite', 'rename'].includes(action)) {
throw new WorkflowPackageError('WFPKG_INVALID_ACTION', '导入处理方式无效');
}
if (action === 'rename' && !newWorkflowId) {
throw new WorkflowPackageError('WFPKG_INVALID_RENAME_ID', '请输入新的工作流 ID');
}
if (action !== 'rename' && newWorkflowId) {
throw new WorkflowPackageError('WFPKG_INVALID_ACTION', '当前处理方式不允许填写新的工作流 ID');
}
if (action === 'overwrite' && !confirmOverwrite) {
throw new WorkflowPackageError('WFPKG_OVERWRITE_CONFIRMATION_REQUIRED', '请确认覆盖本地工作流');
}
return {
inspection_id: inspectionId,
resolution: {
action: action,
new_workflow_id: action === 'rename' ? newWorkflowId : ''
},
confirm_overwrite: action === 'overwrite'
};
}
function createIdempotencyKey() {
const cryptoApi = root && root.crypto;
if (!cryptoApi || typeof cryptoApi.randomUUID !== 'function') {
throw new WorkflowPackageError('WFPKG_IDEMPOTENCY_KEY_REQUIRED', '浏览器无法生成导入请求标识');
}
return cryptoApi.randomUUID();
}
async function readApiError(response) {
const payload = await response.json().catch(function () { return {}; });
const error = payload && payload.error ? payload.error : {};
return {
code: error.code || '',
message: error.message || '',
details: error.details || {}
};
}
async function readJsonOrThrow(response) {
if (response.ok) return response.json();
const error = await readApiError(response);
throw new WorkflowPackageError(error.code, error.message, error.details);
}
async function createInspection(apiFetch, file) {
if (!file) throw new WorkflowPackageError('WFPKG_FILE_REQUIRED', '请选择本地图编排包');
const body = new FormData();
body.append('file', file, file.name || 'workflow.csapkg.zip');
const response = await apiFetch('/api/workflow-package-inspections', { method: 'POST', body: body });
return readJsonOrThrow(response);
}
async function getInspection(apiFetch, inspectionId) {
const response = await apiFetch('/api/workflow-package-inspections/' + encodeURIComponent(inspectionId));
return readJsonOrThrow(response);
}
async function applyImport(apiFetch, request, idempotencyKey) {
if (!idempotencyKey) throw new WorkflowPackageError('WFPKG_IDEMPOTENCY_KEY_REQUIRED', '缺少导入请求标识');
const response = await apiFetch('/api/workflow-package-imports', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey },
body: JSON.stringify(request)
});
return readJsonOrThrow(response);
}
async function getImport(apiFetch, importId) {
const response = await apiFetch('/api/workflow-package-imports/' + encodeURIComponent(importId));
return readJsonOrThrow(response);
}
return {
WorkflowPackageError: WorkflowPackageError,
allowedActions: allowedActions,
buildImportRequest: buildImportRequest,
createIdempotencyKey: createIdempotencyKey,
readApiError: readApiError,
createInspection: createInspection,
getInspection: getInspection,
applyImport: applyImport,
getImport: getImport
};
});
@@ -0,0 +1,51 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const client = require('./workflow-package-client.js');
test('id 冲突默认仅允许保留、本地覆盖或另存', () => {
assert.deepEqual(client.allowedActions('id_conflict'), ['keep_existing', 'overwrite', 'rename']);
assert.deepEqual(client.allowedActions('identical'), ['keep_existing']);
assert.deepEqual(client.allowedActions('none'), ['create']);
});
test('覆盖导入请求必须带确认和空的新 ID', () => {
assert.deepEqual(client.buildImportRequest({
inspectionId: 'wpi_1',
action: 'overwrite',
newWorkflowId: '',
confirmOverwrite: true
}), {
inspection_id: 'wpi_1',
resolution: { action: 'overwrite', new_workflow_id: '' },
confirm_overwrite: true
});
});
test('预检以 multipart file 请求并保留 API 返回体', async () => {
let observed;
const apiFetch = async (url, options) => {
observed = { url, options };
return new Response(JSON.stringify({ inspection: { id: 'wpi_1', status: 'ready' } }), { status: 201 });
};
const data = await client.createInspection(apiFetch, new Blob(['zip']));
assert.equal(observed.url, '/api/workflow-package-inspections');
assert.equal(observed.options.method, 'POST');
assert.equal(observed.options.body instanceof FormData, true);
assert.equal(data.inspection.id, 'wpi_1');
});
test('导入以稳定幂等键发送规范请求体', async () => {
let observed;
const apiFetch = async (url, options) => {
observed = { url, options };
return new Response(JSON.stringify({ import: { id: 'wpii_1', status: 'succeeded' } }), { status: 201 });
};
const request = client.buildImportRequest({
inspectionId: 'wpi_1', action: 'keep_existing', newWorkflowId: '', confirmOverwrite: false
});
const data = await client.applyImport(apiFetch, request, 'f1d13d55-c35d-4693-b507-d2e2ea5703f9');
assert.equal(observed.url, '/api/workflow-package-imports');
assert.equal(observed.options.headers['Idempotency-Key'], 'f1d13d55-c35d-4693-b507-d2e2ea5703f9');
assert.deepEqual(JSON.parse(observed.options.body), request);
assert.equal(data.import.id, 'wpii_1');
});
@@ -0,0 +1,70 @@
const fs = require('node:fs');
const test = require('node:test');
const assert = require('node:assert/strict');
test('图编排提供导入、导出和覆盖确认容器', () => {
const html = fs.readFileSync('web/templates/index.html', 'utf8');
const zh = JSON.parse(fs.readFileSync('web/static/i18n/zh-CN.json', 'utf8'));
assert.match(html, /onclick="openWorkflowPackageImportModal\(\)"/);
assert.match(html, /onclick="exportCurrentWorkflowPackage\(\)"/);
assert.match(html, /id="workflow-package-import-modal"/);
assert.match(html, /id="workflow-package-overwrite-modal"/);
assert.equal(zh.workflows.package.importLocal, '导入本地包');
});
test('工作流脚本调用包契约的全部端点与冲突错误码', () => {
const workflows = fs.readFileSync('web/static/js/workflows.js', 'utf8');
const client = fs.readFileSync('web/static/js/workflow-package-client.js', 'utf8');
assert.match(workflows, /\/api\/workflows\/\$\{encodeURIComponent\(id\)\}\/package/);
assert.match(client, /\/api\/workflow-package-inspections/);
assert.match(client, /\/api\/workflow-package-imports/);
assert.match(workflows, /WFPKG_CONFLICT_CHANGED/);
assert.match(workflows, /WFPKG_INSPECTION_EXPIRED/);
});
test('预检无效包的契约错误码都有中文状态分支', () => {
const workflows = fs.readFileSync('web/static/js/workflows.js', 'utf8');
[
'WFPKG_INVALID_ARCHIVE',
'WFPKG_UNSUPPORTED_FORMAT',
'WFPKG_INVALID_MANIFEST',
'WFPKG_CHECKSUM_MISMATCH',
'WFPKG_MULTIPLE_WORKFLOWS',
'WFPKG_WORKFLOW_INVALID'
].forEach((code) => assert.match(workflows, new RegExp(code)));
});
test('导入请求标识生成失败会进入中文错误处理', () => {
const workflows = fs.readFileSync('web/static/js/workflows.js', 'utf8');
assert.match(workflows, /async function performWorkflowPackageImport\(request\)[\s\S]*?try\s*\{[\s\S]*?client\.createIdempotencyKey\(\)[\s\S]*?catch \(error\) \{\s*displayWorkflowPackageError\(error\)/);
});
test('工作流包动态状态同时提供中文和英文词条', () => {
const zh = JSON.parse(fs.readFileSync('web/static/i18n/zh-CN.json', 'utf8'));
const en = JSON.parse(fs.readFileSync('web/static/i18n/en-US.json', 'utf8'));
const keys = [
['errors', 'invalidArchive'],
['conflict', 'idConflict'],
['summary', 'workflowName'],
['resolution', 'keepExisting'],
['result', 'overwritten']
];
keys.forEach(([section, key]) => {
assert.equal(typeof zh.workflows.package[section][key], 'string');
assert.equal(typeof en.workflows.package[section][key], 'string');
});
});
test('语言切换会刷新工作流包弹窗及其动态状态', () => {
const workflows = fs.readFileSync('web/static/js/workflows.js', 'utf8');
assert.match(workflows, /function refreshWorkflowsI18n\(\)[\s\S]*?workflow-package-import-modal[\s\S]*?workflow-package-overwrite-modal[\s\S]*?renderWorkflowPackageInspection\(\)[\s\S]*?renderWorkflowPackageResolution\(\)/);
});
test('每次打开导入弹窗都会开始新的导入会话', () => {
const workflows = fs.readFileSync('web/static/js/workflows.js', 'utf8');
const start = workflows.indexOf('window.openWorkflowPackageImportModal = async function ()');
const end = workflows.indexOf('window.closeWorkflowPackageImportModal = function ()', start);
const openHandler = workflows.slice(start, end);
assert.match(openHandler, /resetWorkflowPackageImport\(\);/);
assert.doesNotMatch(openHandler, /restoreWorkflowPackageState\(\)/);
});
+443
View File
@@ -26,6 +26,18 @@
const WORKFLOW_TOOL_SELECT_ID = 'workflow-tool-name';
let workflowToolSelectRegistry = null;
let workflowToolSelectDocBound = false;
const workflowPackageState = {
file: null,
inspection: null,
importRecord: null,
resolutionAction: '',
newWorkflowId: '',
riskChoicesVisible: false,
idempotencyKey: '',
requestSignature: ''
};
const WORKFLOW_PACKAGE_INSPECTION_STORAGE_KEY = 'csai.workflow-package.inspection-id';
const WORKFLOW_PACKAGE_IMPORT_STORAGE_KEY = 'csai.workflow-package.import-id';
const KNOWN_NODE_LABELS = {
start: ['开始', 'Start'],
@@ -1863,6 +1875,427 @@
}
};
function workflowPackageClient() {
return window.WorkflowPackageClient || null;
}
function workflowPackageText(key, fallback, opts) {
const translated = _t(key, opts);
return translated === key ? fallback : translated;
}
function workflowPackageStorageGet(key) {
try { return window.sessionStorage.getItem(key) || ''; } catch (_) { return ''; }
}
function workflowPackageStorageSet(key, value) {
try {
if (value) window.sessionStorage.setItem(key, value);
else window.sessionStorage.removeItem(key);
} catch (_) { /* session restore is best effort */ }
}
function workflowPackageInspectionEl() {
return document.getElementById('workflow-package-inspection');
}
function workflowPackageResolutionEl() {
return document.getElementById('workflow-package-resolution');
}
function workflowPackageSubmitBtn() {
return document.getElementById('workflow-package-submit-btn');
}
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');
}
function workflowPackageSetStatus(target, message, type) {
if (!target) return;
target.innerHTML = `<div class="workflow-package-status${type ? ' is-' + esc(type) : ''}">${esc(message)}</div>`;
}
function workflowPackageErrorMessage(error) {
const code = error && error.code ? error.code : '';
const errorKeys = {
WFPKG_FILE_REQUIRED: 'fileRequired',
WFPKG_FILE_TOO_LARGE: 'fileTooLarge',
WFPKG_INVALID_ARCHIVE: 'invalidArchive',
WFPKG_UNSUPPORTED_FORMAT: 'unsupportedFormat',
WFPKG_INVALID_MANIFEST: 'invalidManifest',
WFPKG_CHECKSUM_MISMATCH: 'checksumMismatch',
WFPKG_MULTIPLE_WORKFLOWS: 'multipleWorkflows',
WFPKG_WORKFLOW_INVALID: 'workflowInvalid',
WFPKG_INSPECTION_NOT_FOUND: 'inspectionNotFound',
WFPKG_INSPECTION_EXPIRED: 'inspectionExpired',
WFPKG_INSPECTION_CONSUMED: 'inspectionConsumed',
WFPKG_CONFLICT_CHANGED: 'conflictChanged',
WFPKG_ID_CONFLICT: 'idConflict',
WFPKG_INVALID_ACTION: 'invalidAction',
WFPKG_INVALID_RENAME_ID: 'invalidRenameId',
WFPKG_OVERWRITE_CONFIRMATION_REQUIRED: 'overwriteConfirmationRequired',
WFPKG_IDEMPOTENCY_KEY_REQUIRED: 'idempotencyKeyRequired',
WFPKG_IDEMPOTENCY_KEY_REUSED: 'idempotencyKeyReused',
WFPKG_IMPORT_FAILED: 'importFailed',
WFPKG_EXPORT_FAILED: 'exportFailed',
WFPKG_WORKFLOW_NOT_FOUND: 'workflowNotFound'
};
const key = errorKeys[code];
if (key) return workflowPackageText('workflows.package.errors.' + key, '操作未完成,请稍后重试。');
return (error && error.message) || workflowPackageText('workflows.package.errors.generic', '操作未完成,请稍后重试。');
}
function workflowPackageConflictCopy(conflict) {
const state = (conflict && conflict.state) || 'none';
if (state === 'identical') return { message: workflowPackageText('workflows.package.conflict.identical', '检测到同 ID 且内容相同的本地工作流;本次将跳过导入。'), type: 'success' };
if (state === 'id_conflict') return { message: workflowPackageText('workflows.package.conflict.idConflict', '检测到同 ID 但内容不同的本地工作流。默认保留本地版本。'), type: 'warning' };
return { message: workflowPackageText('workflows.package.conflict.none', '未发现同 ID 的本地工作流,可创建导入。'), type: 'success' };
}
function renderWorkflowPackageInspection() {
const target = workflowPackageInspectionEl();
const inspection = workflowPackageState.inspection;
if (!inspection) {
if (target) target.innerHTML = '';
return;
}
const workflow = inspection.workflow || {};
const conflict = inspection.conflict || {};
const conflictCopy = workflowPackageConflictCopy(conflict);
const rows = [
[workflowPackageText('workflows.package.summary.workflowName', '工作流名称'), workflow.name || workflowPackageText('workflows.package.summary.unnamedWorkflow', '未命名工作流')],
[workflowPackageText('workflows.package.summary.sourceId', '源工作流 ID'), workflow.source_id || '—'],
[workflowPackageText('workflows.package.summary.sourceRevision', '源版本'), workflow.source_revision || '—'],
[workflowPackageText('workflows.package.summary.graphSize', '图规模'), workflowPackageText('workflows.package.summary.graphSizeValue', `${workflow.node_count || 0} 个节点 · ${workflow.edge_count || 0} 条连线`, { nodes: workflow.node_count || 0, edges: workflow.edge_count || 0 })],
[workflowPackageText('workflows.package.summary.contentHash', '内容摘要'), workflow.content_hash || '—'],
[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) {
return `<div class="workflow-package-summary-row"><span>${esc(row[0])}</span><code title="${esc(row[1])}">${esc(row[1])}</code></div>`;
}).join('');
}
function workflowPackageResolutionCard(action, title, description, selected) {
return `<label class="workflow-package-choice${selected ? ' is-selected' : ''}">
<input type="radio" name="workflow-package-resolution-action" value="${esc(action)}" ${selected ? 'checked' : ''} onchange="selectWorkflowPackageResolution('${esc(action)}')">
<span><strong>${esc(title)}</strong><small>${esc(description)}</small></span>
</label>`;
}
function renderWorkflowPackageResolution(message, type) {
const target = workflowPackageResolutionEl();
const submit = workflowPackageSubmitBtn();
const inspection = workflowPackageState.inspection;
if (!inspection) {
if (target) workflowPackageSetStatus(target, message || workflowPackageText('workflows.package.resolution.needInspection', '请先选择本地包并完成预检。'), type || '');
if (submit) submit.disabled = true;
return;
}
const client = workflowPackageClient();
const conflictState = (inspection.conflict && inspection.conflict.state) || 'none';
const allowed = client ? client.allowedActions(conflictState) : [];
if (allowed.indexOf(workflowPackageState.resolutionAction) === -1) {
workflowPackageState.resolutionAction = conflictState === 'none' ? 'create' : 'keep_existing';
}
const action = workflowPackageState.resolutionAction;
let html = '';
if (message) html += `<div class="workflow-package-status${type ? ' is-' + esc(type) : ''}">${esc(message)}</div>`;
if (conflictState === 'none') {
html += `<div class="workflow-package-status is-success">${esc(workflowPackageText('workflows.package.resolution.createHint', '导入后会创建一个新的本地工作流。'))}</div>`;
} else if (conflictState === 'identical') {
html += `<div class="workflow-package-status is-success">${esc(workflowPackageText('workflows.package.resolution.identicalHint', '内容已经存在,无需重复导入。'))}</div>`;
} else {
html += workflowPackageResolutionCard('keep_existing', workflowPackageText('workflows.package.resolution.keepExisting', '保留本地版本'), workflowPackageText('workflows.package.resolution.keepExistingHint', '不修改当前本地工作流;导入记录会保留。'), action === 'keep_existing');
if (!workflowPackageState.riskChoicesVisible) {
html += `<button type="button" class="workflow-package-reveal" onclick="revealWorkflowPackageRiskChoices()">${esc(workflowPackageText('workflows.package.resolution.revealRiskChoices', '更改处理方式'))}</button>`;
} else {
html += workflowPackageResolutionCard('overwrite', workflowPackageText('workflows.package.resolution.overwrite', '覆盖本地版本'), workflowPackageText('workflows.package.resolution.overwriteHint', '替换名称、描述、图定义和启用状态;需要再次确认。'), action === 'overwrite');
html += workflowPackageResolutionCard('rename', workflowPackageText('workflows.package.resolution.rename', '另存为新 ID'), workflowPackageText('workflows.package.resolution.renameHint', '保留当前本地工作流,并以新的 ID 创建副本。'), action === 'rename');
if (action === 'rename') {
html += `<label class="workflow-package-rename-field">${esc(workflowPackageText('workflows.package.resolution.newWorkflowId', '新的工作流 ID'))}<input id="workflow-package-new-id" class="form-input" type="text" value="${esc(workflowPackageState.newWorkflowId)}" placeholder="${esc(workflowPackageText('workflows.package.resolution.newWorkflowIdPlaceholder', '例如:web-scan-basic-copy'))}" oninput="updateWorkflowPackageRenameId()" autocomplete="off"></label>`;
}
}
}
if (target) target.innerHTML = html;
if (submit) {
const renameIncomplete = action === 'rename' && !workflowPackageState.newWorkflowId.trim();
submit.disabled = !action || renameIncomplete;
if (action === 'overwrite') submit.textContent = workflowPackageText('workflows.package.resolution.continueOverwrite', '继续确认覆盖');
else if (action === 'rename') submit.textContent = workflowPackageText('workflows.package.resolution.confirmRename', '确认另存');
else submit.textContent = action === 'create' ? workflowPackageText('workflows.package.resolution.confirmCreate', '确认创建') : workflowPackageText('workflows.package.resolution.confirmComplete', '确认完成');
}
workflowPackageSetStep('import');
}
function resetWorkflowPackageImport(options) {
const keepInspection = options && options.keepInspection;
workflowPackageState.file = null;
workflowPackageState.importRecord = null;
workflowPackageState.newWorkflowId = '';
workflowPackageState.riskChoicesVisible = false;
workflowPackageState.idempotencyKey = '';
workflowPackageState.requestSignature = '';
if (!keepInspection) {
workflowPackageState.inspection = null;
workflowPackageState.resolutionAction = '';
workflowPackageStorageSet(WORKFLOW_PACKAGE_INSPECTION_STORAGE_KEY, '');
}
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', '未选择文件');
}
function displayWorkflowPackageError(error) {
const message = workflowPackageErrorMessage(error);
if (error && (error.code === 'WFPKG_INSPECTION_EXPIRED' || error.code === 'WFPKG_CONFLICT_CHANGED' || error.code === 'WFPKG_INSPECTION_CONSUMED')) {
workflowPackageState.inspection = null;
workflowPackageState.resolutionAction = '';
workflowPackageStorageSet(WORKFLOW_PACKAGE_INSPECTION_STORAGE_KEY, '');
workflowPackageSetStep('inspection');
renderWorkflowPackageInspection();
}
renderWorkflowPackageResolution(message, 'warning');
if (typeof showNotification === 'function') showNotification(message, 'error');
}
function renderWorkflowPackageImportResult(importRecord) {
const result = importRecord && importRecord.result;
const resultKeys = {
created: 'created',
overwritten: 'overwritten',
renamed: 'renamed',
kept_existing: 'keptExisting',
skipped_identical: 'skippedIdentical'
};
const resultKey = resultKeys[result];
const message = resultKey
? workflowPackageText('workflows.package.result.' + resultKey, '导入已完成。')
: workflowPackageText('workflows.package.result.complete', '导入已完成。');
renderWorkflowPackageResolution(message, result === 'kept_existing' || result === 'skipped_identical' ? '' : 'success');
const submit = workflowPackageSubmitBtn();
if (submit) {
submit.disabled = true;
submit.textContent = workflowPackageText('workflows.package.result.completedAction', '导入已完成');
}
}
async function restoreWorkflowPackageState() {
const client = workflowPackageClient();
if (!client || typeof apiFetch !== 'function') return;
const importId = workflowPackageStorageGet(WORKFLOW_PACKAGE_IMPORT_STORAGE_KEY);
if (importId) {
try {
const data = await client.getImport(apiFetch, importId);
workflowPackageState.importRecord = data.import || data;
renderWorkflowPackageImportResult(workflowPackageState.importRecord);
return;
} catch (_) {
workflowPackageStorageSet(WORKFLOW_PACKAGE_IMPORT_STORAGE_KEY, '');
}
}
const inspectionId = workflowPackageStorageGet(WORKFLOW_PACKAGE_INSPECTION_STORAGE_KEY);
if (!inspectionId) return;
try {
const data = await client.getInspection(apiFetch, inspectionId);
workflowPackageState.inspection = data.inspection || data;
renderWorkflowPackageInspection();
renderWorkflowPackageResolution();
} catch (error) {
workflowPackageStorageSet(WORKFLOW_PACKAGE_INSPECTION_STORAGE_KEY, '');
displayWorkflowPackageError(error);
}
}
window.openWorkflowPackageImportModal = async function () {
if (typeof requirePermission === 'function' && !requirePermission('workflow:write')) return;
resetWorkflowPackageImport();
renderWorkflowPackageInspection();
renderWorkflowPackageResolution();
workflowPackageSetStep('inspection');
if (typeof openAppModal === 'function') openAppModal('workflow-package-import-modal', { focusEl: document.getElementById('workflow-package-inspect-btn') });
if (typeof window.applyTranslations === 'function') window.applyTranslations(document.getElementById('workflow-package-import-modal'));
};
window.closeWorkflowPackageImportModal = function () {
if (typeof closeAppModal === 'function') closeAppModal('workflow-package-import-modal');
resetWorkflowPackageImport();
};
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');
};
window.inspectWorkflowPackage = async function () {
if (typeof requirePermission === 'function' && !requirePermission('workflow:write')) return;
const client = workflowPackageClient();
const button = document.getElementById('workflow-package-inspect-btn');
if (!client || typeof apiFetch !== 'function') {
displayWorkflowPackageError({ code: 'WFPKG_REQUEST_FAILED', message: workflowPackageText('workflows.package.errors.serviceUnavailable', '导入服务尚未准备好,请刷新页面后重试。') });
return;
}
if (button) button.disabled = true;
try {
const data = await client.createInspection(apiFetch, workflowPackageState.file);
workflowPackageState.inspection = data.inspection || data;
workflowPackageState.importRecord = null;
workflowPackageState.riskChoicesVisible = false;
workflowPackageState.newWorkflowId = '';
workflowPackageState.resolutionAction = '';
workflowPackageStorageSet(WORKFLOW_PACKAGE_INSPECTION_STORAGE_KEY, workflowPackageState.inspection.id || '');
renderWorkflowPackageInspection();
renderWorkflowPackageResolution();
} catch (error) {
displayWorkflowPackageError(error);
} finally {
if (button) button.disabled = false;
}
};
window.revealWorkflowPackageRiskChoices = function () {
workflowPackageState.riskChoicesVisible = true;
renderWorkflowPackageResolution();
};
window.selectWorkflowPackageResolution = function (action) {
const client = workflowPackageClient();
const conflict = workflowPackageState.inspection && workflowPackageState.inspection.conflict;
const allowed = client ? client.allowedActions((conflict && conflict.state) || 'none') : [];
if (allowed.indexOf(action) === -1) return;
workflowPackageState.resolutionAction = action;
if (action !== 'rename') workflowPackageState.newWorkflowId = '';
renderWorkflowPackageResolution();
};
window.updateWorkflowPackageRenameId = function () {
const input = document.getElementById('workflow-package-new-id');
workflowPackageState.newWorkflowId = input ? input.value : '';
const submit = workflowPackageSubmitBtn();
if (submit) submit.disabled = !workflowPackageState.newWorkflowId.trim();
};
async function performWorkflowPackageImport(request) {
const client = workflowPackageClient();
const submit = workflowPackageSubmitBtn();
if (submit) submit.disabled = true;
try {
const signature = JSON.stringify(request);
if (!workflowPackageState.idempotencyKey || workflowPackageState.requestSignature !== signature) {
workflowPackageState.idempotencyKey = client.createIdempotencyKey();
workflowPackageState.requestSignature = signature;
}
const data = await client.applyImport(apiFetch, request, workflowPackageState.idempotencyKey);
workflowPackageState.importRecord = data.import || data;
workflowPackageStorageSet(WORKFLOW_PACKAGE_IMPORT_STORAGE_KEY, workflowPackageState.importRecord.id || '');
workflowPackageStorageSet(WORKFLOW_PACKAGE_INSPECTION_STORAGE_KEY, '');
workflowPackageState.inspection = null;
renderWorkflowPackageImportResult(workflowPackageState.importRecord);
if (typeof window.refreshWorkflows === 'function') await window.refreshWorkflows();
} catch (error) {
displayWorkflowPackageError(error);
} finally {
if (submit && !workflowPackageState.importRecord) submit.disabled = false;
}
}
window.submitWorkflowPackageImport = async function () {
const client = workflowPackageClient();
if (!client || !workflowPackageState.inspection) return;
if (workflowPackageState.resolutionAction === 'overwrite') {
const checkbox = document.getElementById('workflow-package-overwrite-confirm');
const submit = document.getElementById('workflow-package-overwrite-submit-btn');
if (checkbox) checkbox.checked = false;
if (submit) submit.disabled = true;
if (typeof openAppModal === 'function') openAppModal('workflow-package-overwrite-modal', { focusEl: checkbox });
return;
}
let request;
try {
request = client.buildImportRequest({
inspectionId: workflowPackageState.inspection.id,
action: workflowPackageState.resolutionAction,
newWorkflowId: workflowPackageState.newWorkflowId,
confirmOverwrite: false
});
} catch (error) {
displayWorkflowPackageError(error);
return;
}
await performWorkflowPackageImport(request);
};
window.closeWorkflowPackageOverwriteModal = function () {
if (typeof closeAppModal === 'function') closeAppModal('workflow-package-overwrite-modal');
};
window.updateWorkflowPackageOverwriteConfirmation = function () {
const checkbox = document.getElementById('workflow-package-overwrite-confirm');
const submit = document.getElementById('workflow-package-overwrite-submit-btn');
if (submit) submit.disabled = !checkbox || !checkbox.checked;
};
window.confirmWorkflowPackageOverwrite = async function () {
const checkbox = document.getElementById('workflow-package-overwrite-confirm');
const client = workflowPackageClient();
if (!client || !checkbox || !checkbox.checked || !workflowPackageState.inspection) return;
try {
const request = client.buildImportRequest({
inspectionId: workflowPackageState.inspection.id,
action: 'overwrite',
newWorkflowId: '',
confirmOverwrite: true
});
window.closeWorkflowPackageOverwriteModal();
await performWorkflowPackageImport(request);
} catch (error) {
displayWorkflowPackageError(error);
}
};
window.exportCurrentWorkflowPackage = async function () {
if (typeof requirePermission === 'function' && !requirePermission('workflow:read')) return;
const id = currentWorkflowId || readWorkflowMetaFromForm().id;
if (!id) {
if (typeof showNotification === 'function') showNotification(workflowPackageText('workflows.package.exportSelectSaved', '请先选择已保存的工作流后再导出。'), 'warning');
return;
}
try {
const response = await apiFetch(`/api/workflows/${encodeURIComponent(id)}/package`, { method: 'GET' });
if (!response.ok) {
const error = workflowPackageClient() ? await workflowPackageClient().readApiError(response) : {};
throw error;
}
const blob = await response.blob();
const disposition = response.headers.get('Content-Disposition') || '';
const match = disposition.match(/filename\*?=(?:UTF-8''|\")?([^;\"]+)/i);
const filename = match && match[1] ? decodeURIComponent(match[1].trim()) : `${id}.csapkg.zip`;
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
window.setTimeout(function () { URL.revokeObjectURL(url); }, 0);
} catch (error) {
const message = workflowPackageErrorMessage(error);
if (typeof showNotification === 'function') showNotification(message, 'error');
}
};
function refreshCanvasLabels() {
if (!cy) return;
cy.nodes().forEach(function (node) {
@@ -1888,6 +2321,10 @@
if (page && typeof window.applyTranslations === 'function') {
window.applyTranslations(page);
}
['workflow-package-import-modal', 'workflow-package-overwrite-modal'].forEach(function (id) {
const modal = document.getElementById(id);
if (modal && typeof window.applyTranslations === 'function') window.applyTranslations(modal);
});
const connectBtn = document.getElementById('workflow-connect-btn');
if (connectBtn) {
connectBtn.textContent = connectMode ? _t('workflows.connecting') : _t('workflows.connect');
@@ -1903,6 +2340,12 @@
if (typeof loadWorkflowOptionsForRoleModal === 'function') {
loadWorkflowOptionsForRoleModal();
}
if (workflowPackageState.importRecord) {
renderWorkflowPackageImportResult(workflowPackageState.importRecord);
} else {
renderWorkflowPackageInspection();
renderWorkflowPackageResolution();
}
}
document.addEventListener('languagechange', function () {