feat: 完成分享、单会话与发布审批改造

- 增加工作流协作分享与知识库卡片分享入口,统一低版本浏览器复制反馈

- Web 新登录替换旧会话,并保持 API Key 会话隔离

- 发布审批增加必填说明并在审批详情展示

- 账号重置与导入改用可配置默认强密码
This commit is contained in:
2026-07-23 16:09:31 +08:00
parent caa1f07b66
commit 5a42826d44
71 changed files with 3191 additions and 132 deletions

View File

@@ -0,0 +1,51 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { copyTextWithFeedback } from '../clipboard-feedback';
const { copyTextToClipboard, error, success } = vi.hoisted(() => ({
copyTextToClipboard: vi.fn(),
error: vi.fn(),
success: vi.fn(),
}));
vi.mock('@easyflow/utils', () => ({
copyTextToClipboard,
}));
vi.mock('element-plus', () => ({
ElMessage: {
error,
success,
},
}));
describe('copyTextWithFeedback', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('copies through the shared Chrome 90 compatible helper', async () => {
copyTextToClipboard.mockResolvedValue('exec-command');
await expect(
copyTextWithFeedback('https://example.test/share', '复制成功'),
).resolves.toBe(true);
expect(copyTextToClipboard).toHaveBeenCalledWith(
'https://example.test/share',
);
expect(success).toHaveBeenCalledWith('复制成功');
expect(error).not.toHaveBeenCalled();
});
it('reports a recoverable error when copying fails', async () => {
copyTextToClipboard.mockRejectedValue(new Error('copy denied'));
await expect(
copyTextWithFeedback('content', '复制成功', '复制失败,请手动复制'),
).resolves.toBe(false);
expect(error).toHaveBeenCalledWith('复制失败,请手动复制');
expect(success).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,26 @@
import { copyTextToClipboard } from '@easyflow/utils';
import { ElMessage } from 'element-plus';
/**
* 复制文本并统一展示成功或失败反馈。
*
* @param text 待复制文本
* @param successMessage 成功提示
* @param failureMessage 失败提示
* @returns 是否复制成功
*/
export async function copyTextWithFeedback(
text: string,
successMessage: string,
failureMessage = '复制失败,请手动复制',
): Promise<boolean> {
try {
await copyTextToClipboard(text);
ElMessage.success(successMessage);
return true;
} catch {
ElMessage.error(failureMessage);
return false;
}
}

View File

@@ -0,0 +1,88 @@
/**
* 工作流协作分享请求头。
*/
export const WORKFLOW_SHARE_HEADER = 'X-Workflow-Share-Key';
interface WorkflowShareResolutionOptions<T> {
currentWorkflowId?: null | T;
onFailure: (error: unknown) => Promise<void> | void;
resolve: () => Promise<null | T | undefined>;
shareKey?: unknown;
}
/**
* 从页面地址读取工作流分享密钥,兼容 history 与 hash 路由。
*/
export function readWorkflowShareKey(url?: string): null | string {
const currentUrl =
url || (typeof window === 'undefined' ? '' : window.location.href);
if (!currentUrl) {
return null;
}
try {
const parsed = new URL(
currentUrl,
typeof window === 'undefined'
? 'http://localhost'
: window.location.origin,
);
const historyKey = parsed.searchParams.get('shareKey')?.trim();
if (historyKey) {
return historyKey;
}
const queryIndex = parsed.hash.indexOf('?');
if (queryIndex === -1) {
return null;
}
return (
new URLSearchParams(parsed.hash.slice(queryIndex + 1))
.get('shareKey')
?.trim() || null
);
} catch {
return null;
}
}
/**
* 在保留现有请求头的基础上附加工作流分享密钥。
*/
export function withWorkflowShareHeader(
headers: Record<string, string>,
url?: string,
): Record<string, string> {
const shareKey = readWorkflowShareKey(url);
if (!shareKey) {
return headers;
}
return {
...headers,
[WORKFLOW_SHARE_HEADER]: shareKey,
};
}
/**
* 解析分享地址对应的工作流,并在链接失效时统一收口异常。
*/
export async function resolveWorkflowShareWorkflowId<T>({
currentWorkflowId,
onFailure,
resolve,
shareKey,
}: WorkflowShareResolutionOptions<T>): Promise<null | T> {
if (currentWorkflowId !== null && currentWorkflowId !== undefined) {
return currentWorkflowId;
}
const normalizedShareKey = Array.isArray(shareKey)
? shareKey.find((value) => String(value || '').trim())
: shareKey;
if (!String(normalizedShareKey || '').trim()) {
return null;
}
try {
return (await resolve()) ?? null;
} catch (error) {
await onFailure(error);
return null;
}
}