Refactor UUID generation, remove unused logger and encryption utilities, and clean up request handling

- Updated `generateUUID` function for improved readability and performance.
- Deleted `logger.ts`, `other.ts`, `request.ts`, `storage.ts`, `tansParams.ts`, and `validate.ts` as they were no longer needed.
- Simplified TypeScript configuration by removing unnecessary paths and aliases.
- Enhanced Vite configuration for better project structure and maintainability.
This commit is contained in:
DEV_DSW
2026-04-17 15:38:08 +08:00
parent b1dea9a5c2
commit 79bea4f107
360 changed files with 14495 additions and 30856 deletions

View File

@@ -0,0 +1,576 @@
import type { ReactNode } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import type { AutomationScript, ScriptRecordingResult, ScriptSaveInput } from '../../../lib/script-types';
import {
CheckIcon,
CloseIcon,
PlayIcon,
StopIcon,
} from './icons';
type ScriptDraft = {
name: string;
description: string;
channel: string;
code: string;
enabled: boolean;
};
type CreateScriptDialogProps = {
open: boolean;
saving: boolean;
onClose: () => void;
onSubmit: (input: ScriptSaveInput) => Promise<void>;
};
type EditScriptDialogProps = {
open: boolean;
script: AutomationScript | null;
saving: boolean;
recordingStatus: 'idle' | 'recording' | 'stopped';
onClose: () => void;
onSubmit: (input: ScriptSaveInput) => Promise<void>;
onStartRecording: (url: string) => Promise<void>;
onStopRecording: () => Promise<ScriptRecordingResult>;
onFeedback: (message: string, tone?: 'info' | 'success' | 'error') => void;
};
type ConfirmDeleteDialogProps = {
open: boolean;
scriptName?: string;
deleting: boolean;
onClose: () => void;
onConfirm: () => Promise<void>;
};
const CHANNEL_URLS = [
'https://hotel.fliggy.com/ebooking/hotelBaseInfoUv.htm#/ebk/homeV1',
'https://me.meituan.com/ebooking/merchant/product#/index',
'https://life.douyin.com/p/travel-ari/hotel/price_amount_state?groupid=1816249020842116',
];
const DEFAULT_SCRIPT_TEMPLATE = `import { chromium } from 'playwright';
import { preparePage, safeDisconnectBrowser } from './common/tabs.js';
(async () => {
const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
const { page } = await preparePage(browser, {
targetUrl: 'about:blank',
});
// Your automation code here
await safeDisconnectBrowser(browser);
process.exit(0);
})();
`;
function buildDefaultScript(channel: string) {
return DEFAULT_SCRIPT_TEMPLATE.replace('about:blank', channel.trim() || 'about:blank');
}
function ModalFrame({
open,
title,
subtitle,
widthClassName,
children,
onClose,
}: {
open: boolean;
title: string;
subtitle: string;
widthClassName: string;
children: ReactNode;
onClose: () => void;
}) {
useEffect(() => {
if (!open) return undefined;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [open, onClose]);
if (!open) return null;
return (
<div
className="absolute inset-0 z-30 flex items-center justify-center bg-black/30 px-4 py-8 backdrop-blur-[1px]"
onClick={onClose}
>
<div
className={[
'max-h-full w-full overflow-hidden rounded-[24px] bg-[#f4f3eb] shadow-[0_24px_80px_rgba(15,23,42,0.18)] dark:bg-[#1f1f22]',
widthClassName,
].join(' ')}
onClick={(event) => event.stopPropagation()}
>
<div className="flex items-start justify-between border-b border-black/5 px-6 py-5 dark:border-white/5">
<div>
<h2
className="mb-2 text-[24px] font-normal tracking-tight text-[#171717] dark:text-[#f3f4f6]"
style={{ fontFamily: "Georgia, Cambria, 'Times New Roman', Times, serif" }}
>
{title}
</h2>
<p className="text-[14px] text-[#99A0AE] dark:text-gray-500">{subtitle}</p>
</div>
<button
type="button"
className="rounded-full p-1 text-[#99A0AE] transition-colors hover:text-[#171717] dark:hover:text-[#f3f4f6]"
onClick={onClose}
aria-label="关闭"
>
<CloseIcon className="h-5 w-5" />
</button>
</div>
<div className="max-h-[calc(100vh-180px)] overflow-y-auto px-6 py-6">
{children}
</div>
</div>
</div>
);
}
function FieldLabel({ children }: { children: ReactNode }) {
return (
<label className="mb-2 block text-[14px] font-bold text-[#171717]/80 dark:text-[#f3f4f6]/80">
{children}
</label>
);
}
function baseFieldClassName(multiline = false) {
return [
'w-full rounded-[14px] border border-transparent bg-[#eceae1] px-4 text-[13px] text-[#171717] outline-none transition-colors',
'placeholder:text-[#171717]/45 focus:border-[#2B7FFF]/50 dark:bg-[#222225] dark:text-[#f3f4f6] dark:placeholder:text-gray-500',
multiline ? 'min-h-[108px] py-3 leading-6' : 'h-[46px]',
].join(' ');
}
function Switch({
checked,
onChange,
disabled,
}: {
checked: boolean;
onChange: (value: boolean) => void;
disabled?: boolean;
}) {
return (
<button
type="button"
role="switch"
aria-checked={checked}
disabled={disabled}
onClick={() => onChange(!checked)}
className={[
'relative inline-flex h-7 w-12 shrink-0 items-center rounded-full border transition-colors',
checked ? 'border-[#2B7FFF] bg-[#2B7FFF]' : 'border-black/10 bg-black/10 dark:border-white/10 dark:bg-white/10',
disabled ? 'cursor-not-allowed opacity-50' : '',
].join(' ')}
>
<span
className={[
'h-5 w-5 rounded-full bg-white shadow-sm transition-transform',
checked ? 'translate-x-[22px]' : 'translate-x-[3px]',
].join(' ')}
/>
</button>
);
}
export function CreateScriptDialog({
open,
saving,
onClose,
onSubmit,
}: CreateScriptDialogProps) {
const [draft, setDraft] = useState<ScriptDraft>({
name: '',
description: '',
channel: '',
code: '',
enabled: true,
});
useEffect(() => {
if (!open) {
setDraft({
name: '',
description: '',
channel: '',
code: '',
enabled: true,
});
}
}, [open]);
async function handleSubmit() {
if (!draft.name.trim()) return;
await onSubmit({
name: draft.name.trim(),
description: draft.description.trim(),
channel: draft.channel.trim(),
code: buildDefaultScript(draft.channel),
enabled: true,
});
}
return (
<ModalFrame
open={open}
title="创建脚本"
subtitle="先创建一个可保存的脚本,再继续录制或编辑 Playwright 代码。"
widthClassName="max-w-[560px]"
onClose={onClose}
>
<div className="space-y-5">
<div>
<FieldLabel></FieldLabel>
<input
value={draft.name}
onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))}
placeholder="例如:飞猪房态采集"
className={baseFieldClassName()}
/>
</div>
<div>
<FieldLabel></FieldLabel>
<textarea
value={draft.description}
onChange={(event) => setDraft((current) => ({ ...current, description: event.target.value }))}
placeholder="简要描述这个脚本做什么。"
className={baseFieldClassName(true)}
/>
</div>
<div>
<FieldLabel></FieldLabel>
<input
value={draft.channel}
onChange={(event) => setDraft((current) => ({ ...current, channel: event.target.value }))}
placeholder="粘贴目标渠道 URL可留空后续再补。"
className={baseFieldClassName()}
/>
<p className="mt-2 text-[12px] text-[#99A0AE] dark:text-gray-500">
codegen
</p>
</div>
<div className="flex justify-end gap-3 pt-3">
<button
type="button"
className="inline-flex h-[42px] items-center justify-center rounded-full bg-[#e8e6de] px-6 text-[13px] font-semibold text-[#4B4B4B] transition-colors hover:bg-[#dfddd4] dark:bg-[#222225] dark:text-gray-300 dark:hover:bg-[#2a2a2d]"
onClick={onClose}
disabled={saving}
>
</button>
<button
type="button"
className="inline-flex h-[42px] items-center justify-center rounded-full bg-[#2B7FFF] px-6 text-[13px] font-semibold text-white transition-colors hover:bg-[#1769e0] disabled:cursor-wait disabled:opacity-70"
onClick={() => {
void handleSubmit();
}}
disabled={saving || !draft.name.trim()}
>
{saving ? '创建中...' : '创建并录制'}
</button>
</div>
</div>
</ModalFrame>
);
}
export function EditScriptDialog({
open,
script,
saving,
recordingStatus,
onClose,
onSubmit,
onStartRecording,
onStopRecording,
onFeedback,
}: EditScriptDialogProps) {
const [draft, setDraft] = useState<ScriptDraft>({
name: '',
description: '',
channel: '',
code: '',
enabled: true,
});
const [recordingBusy, setRecordingBusy] = useState(false);
const previousChannelRef = useRef('');
useEffect(() => {
if (!open || !script) return;
const nextDraft = {
name: script.name || '',
description: script.description || '',
channel: script.channel || '',
code: script.code || '',
enabled: script.enabled ?? true,
};
setDraft(nextDraft);
previousChannelRef.current = nextDraft.channel;
}, [open, script]);
useEffect(() => {
const previousChannel = previousChannelRef.current;
if (!previousChannel || previousChannel === draft.channel) return;
setDraft((current) => {
if (!current.code.includes(previousChannel)) {
previousChannelRef.current = draft.channel;
return current;
}
previousChannelRef.current = draft.channel;
return {
...current,
code: current.code.split(previousChannel).join(draft.channel),
};
});
}, [draft.channel]);
const replaceChannelDisabled = useMemo(() => {
if (!draft.channel.trim()) return true;
return !CHANNEL_URLS.some((url) => draft.code.includes(url));
}, [draft.channel, draft.code]);
if (!script) return null;
async function handleSave() {
if (!draft.name.trim() || !draft.code.trim()) return;
await onSubmit({
id: script.id,
name: draft.name.trim(),
description: draft.description.trim(),
channel: draft.channel.trim(),
code: draft.code,
enabled: draft.enabled,
});
}
async function handleStartRecording() {
if (!draft.channel.trim()) {
onFeedback('开始录制前请先填写渠道链接。', 'error');
return;
}
setRecordingBusy(true);
try {
await onStartRecording(draft.channel.trim());
} finally {
setRecordingBusy(false);
}
}
async function handleStopRecording() {
setRecordingBusy(true);
try {
const result = await onStopRecording();
if (result.code) {
setDraft((current) => ({ ...current, code: result.code || current.code }));
}
} finally {
setRecordingBusy(false);
}
}
function replaceKnownChannelUrls() {
setDraft((current) => ({
...current,
code: CHANNEL_URLS.reduce((source, url) => source.split(url).join(current.channel.trim()), current.code),
}));
onFeedback('已将代码中的已知渠道链接替换为当前输入值。', 'success');
}
return (
<ModalFrame
open={open}
title="编辑脚本"
subtitle="修改脚本信息、维护 Playwright 代码,并在需要时重新录制。"
widthClassName="max-w-[820px]"
onClose={onClose}
>
<div className="space-y-5">
<div>
<FieldLabel></FieldLabel>
<input
value={draft.name}
onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))}
placeholder="输入脚本名称"
className={baseFieldClassName()}
/>
</div>
<div>
<FieldLabel></FieldLabel>
<textarea
value={draft.description}
onChange={(event) => setDraft((current) => ({ ...current, description: event.target.value }))}
placeholder="补充脚本用途、依赖页面或注意事项。"
className={baseFieldClassName(true)}
/>
</div>
<div>
<div className="mb-2 flex items-center justify-between gap-3">
<FieldLabel></FieldLabel>
<button
type="button"
className="text-[12px] font-medium text-[#2B7FFF] transition-colors hover:text-[#1769e0] disabled:cursor-not-allowed disabled:text-[#99A0AE]"
disabled={replaceChannelDisabled}
onClick={replaceKnownChannelUrls}
>
URL
</button>
</div>
<input
value={draft.channel}
onChange={(event) => setDraft((current) => ({ ...current, channel: event.target.value }))}
placeholder="粘贴渠道 URL"
className={baseFieldClassName()}
/>
</div>
<div>
<div className="mb-2 flex items-center justify-between gap-3">
<FieldLabel></FieldLabel>
<div className="flex items-center gap-2">
{recordingStatus === 'recording' ? (
<button
type="button"
className="inline-flex h-8 items-center justify-center rounded-full bg-red-500 px-4 text-[12px] font-semibold text-white transition-colors hover:bg-red-600 disabled:cursor-wait disabled:opacity-70"
onClick={() => {
void handleStopRecording();
}}
disabled={recordingBusy}
>
<StopIcon className="mr-1.5 h-3.5 w-3.5" />
{recordingBusy ? '停止中...' : '停止录制'}
</button>
) : (
<button
type="button"
className="inline-flex h-8 items-center justify-center rounded-full border border-black/10 px-4 text-[12px] font-semibold text-[#171717] transition-colors hover:bg-black/5 dark:border-gray-700 dark:text-[#f3f4f6] dark:hover:bg-white/5 disabled:cursor-wait disabled:opacity-70"
onClick={() => {
void handleStartRecording();
}}
disabled={recordingBusy}
>
<PlayIcon className="mr-1.5 h-3.5 w-3.5" />
{recordingBusy ? '启动中...' : '开始录制'}
</button>
)}
</div>
</div>
{recordingStatus === 'recording' ? (
<p className="mb-3 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-[12px] text-amber-700 dark:border-amber-900/60 dark:bg-amber-900/20 dark:text-amber-300">
</p>
) : null}
<textarea
value={draft.code}
onChange={(event) => setDraft((current) => ({ ...current, code: event.target.value }))}
placeholder="// 在这里编写 Playwright 自动化脚本"
spellCheck={false}
className="min-h-[340px] w-full rounded-[18px] border border-transparent bg-[#eceae1] px-4 py-4 font-mono text-[13px] leading-6 text-[#171717] outline-none transition-colors placeholder:text-[#171717]/45 focus:border-[#2B7FFF]/50 dark:bg-[#0f0f10] dark:text-[#f3f4f6] dark:placeholder:text-gray-500"
/>
</div>
<div className="flex items-center justify-between rounded-[18px] border border-black/5 bg-[#e8e6de]/60 px-4 py-4 dark:border-white/5 dark:bg-[#222225]">
<div>
<p className="text-[14px] font-bold text-[#171717]/80 dark:text-[#f3f4f6]/80"></p>
<p className="mt-1 text-[13px] text-[#99A0AE] dark:text-gray-500"></p>
</div>
<Switch checked={draft.enabled} onChange={(value) => setDraft((current) => ({ ...current, enabled: value }))} />
</div>
<div className="flex justify-end gap-3 pt-3">
<button
type="button"
className="inline-flex h-[42px] items-center justify-center rounded-full bg-[#e8e6de] px-6 text-[13px] font-semibold text-[#4B4B4B] transition-colors hover:bg-[#dfddd4] dark:bg-[#222225] dark:text-gray-300 dark:hover:bg-[#2a2a2d]"
onClick={onClose}
disabled={saving}
>
</button>
<button
type="button"
className="inline-flex h-[42px] items-center justify-center rounded-full bg-[#2B7FFF] px-6 text-[13px] font-semibold text-white transition-colors hover:bg-[#1769e0] disabled:cursor-wait disabled:opacity-70"
onClick={() => {
void handleSave();
}}
disabled={saving || !draft.name.trim() || !draft.code.trim()}
>
<CheckIcon className="mr-1.5 h-4 w-4" />
{saving ? '保存中...' : '保存修改'}
</button>
</div>
</div>
</ModalFrame>
);
}
export function ConfirmDeleteDialog({
open,
scriptName,
deleting,
onClose,
onConfirm,
}: ConfirmDeleteDialogProps) {
return (
<ModalFrame
open={open}
title="删除脚本"
subtitle="删除后脚本文件也会一并移除,请确认这就是你想要的操作。"
widthClassName="max-w-[520px]"
onClose={onClose}
>
<div className="space-y-6">
<div className="rounded-[18px] border border-red-500/20 bg-red-500/8 px-4 py-4 text-[14px] leading-6 text-[#171717] dark:text-gray-200">
{scriptName ? `确认删除 “${scriptName}” 吗?此操作无法撤销。` : '确认删除这个脚本吗?此操作无法撤销。'}
</div>
<div className="flex justify-end gap-3">
<button
type="button"
className="inline-flex h-[42px] items-center justify-center rounded-full bg-[#e8e6de] px-6 text-[13px] font-semibold text-[#4B4B4B] transition-colors hover:bg-[#dfddd4] dark:bg-[#222225] dark:text-gray-300 dark:hover:bg-[#2a2a2d]"
onClick={onClose}
disabled={deleting}
>
</button>
<button
type="button"
className="inline-flex h-[42px] items-center justify-center rounded-full bg-red-500 px-6 text-[13px] font-semibold text-white transition-colors hover:bg-red-600 disabled:cursor-wait disabled:opacity-70"
onClick={() => {
void onConfirm();
}}
disabled={deleting}
>
{deleting ? '删除中...' : '确认删除'}
</button>
</div>
</div>
</ModalFrame>
);
}
export { Switch };