32
web-app/package-lock.json
generated
32
web-app/package-lock.json
generated
@@ -16,7 +16,8 @@
|
||||
"react-router-dom": "^7.13.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-gfm": "^4.0.1"
|
||||
"remark-gfm": "^4.0.1",
|
||||
"zustand": "^5.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.3",
|
||||
@@ -4638,6 +4639,35 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "5.0.11",
|
||||
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.11.tgz",
|
||||
"integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/zwitch": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"react-router-dom": "^7.13.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-gfm": "^4.0.1"
|
||||
"remark-gfm": "^4.0.1",
|
||||
"zustand": "^5.0.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.3",
|
||||
|
||||
@@ -80,34 +80,37 @@ export interface RegexScriptListResponse {
|
||||
}
|
||||
|
||||
// API 方法
|
||||
export const regexScriptApi = {
|
||||
export const regexAPI = {
|
||||
// 创建正则脚本
|
||||
createRegexScript: (data: CreateRegexScriptRequest): Promise<{ data: RegexScript }> => {
|
||||
create: (data: CreateRegexScriptRequest): Promise<{ data: RegexScript }> => {
|
||||
return apiClient.post('/app/regex', data)
|
||||
},
|
||||
|
||||
// 获取正则脚本列表
|
||||
getRegexScriptList: (params?: GetRegexScriptListRequest): Promise<{ data: RegexScriptListResponse }> => {
|
||||
getList: (params?: GetRegexScriptListRequest): Promise<{ data: RegexScriptListResponse }> => {
|
||||
return apiClient.get('/app/regex', { params })
|
||||
},
|
||||
|
||||
// 获取正则脚本详情
|
||||
getRegexScriptById: (id: number): Promise<{ data: RegexScript }> => {
|
||||
getById: (id: number): Promise<{ data: RegexScript }> => {
|
||||
return apiClient.get(`/app/regex/${id}`)
|
||||
},
|
||||
|
||||
// 更新正则脚本
|
||||
updateRegexScript: (id: number, data: UpdateRegexScriptRequest) => {
|
||||
update: (id: number, data: UpdateRegexScriptRequest) => {
|
||||
return apiClient.put(`/app/regex/${id}`, data)
|
||||
},
|
||||
|
||||
// 删除正则脚本
|
||||
deleteRegexScript: (id: number) => {
|
||||
delete: (id: number) => {
|
||||
return apiClient.delete(`/app/regex/${id}`)
|
||||
},
|
||||
|
||||
// 测试正则脚本
|
||||
testRegexScript: (id: number, testString: string): Promise<{ data: { original: string; result: string; success: boolean; error?: string } }> => {
|
||||
test: (id: number, testString: string): Promise<{ data: { original: string; result: string; success: boolean; error?: string } }> => {
|
||||
return apiClient.post(`/app/regex/${id}/test`, { testString })
|
||||
},
|
||||
}
|
||||
|
||||
// 保持向后兼容
|
||||
export const regexScriptApi = regexAPI
|
||||
|
||||
412
web-app/src/pages/RegexScriptManagePage.tsx
Normal file
412
web-app/src/pages/RegexScriptManagePage.tsx
Normal file
@@ -0,0 +1,412 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Plus, Search, Edit2, Trash2, Upload, Download, Play, X } from 'lucide-react'
|
||||
import { regexAPI } from '../api/regex'
|
||||
|
||||
interface RegexScript {
|
||||
id: number
|
||||
name: string
|
||||
findRegex: string
|
||||
replaceWith: string
|
||||
placement: number
|
||||
disabled: boolean
|
||||
order: number
|
||||
scope: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
const placementNames = ['输入', '输出', '世界书', '显示']
|
||||
|
||||
export default function RegexScriptManagePage() {
|
||||
const [scripts, setScripts] = useState<RegexScript[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [searchKeyword, setSearchKeyword] = useState('')
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [editingScript, setEditingScript] = useState<RegexScript | null>(null)
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
findRegex: '',
|
||||
replaceWith: '',
|
||||
placement: 1,
|
||||
disabled: false,
|
||||
order: 100,
|
||||
scope: 0,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
loadScripts()
|
||||
}, [searchKeyword])
|
||||
|
||||
const loadScripts = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const response = await regexAPI.getList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: searchKeyword,
|
||||
scope: 0, // 只加载全局脚本
|
||||
})
|
||||
setScripts(response.data.list || [])
|
||||
} catch (error) {
|
||||
console.error('加载正则脚本失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditingScript(null)
|
||||
setFormData({
|
||||
name: '',
|
||||
findRegex: '',
|
||||
replaceWith: '',
|
||||
placement: 1,
|
||||
disabled: false,
|
||||
order: 100,
|
||||
scope: 0,
|
||||
})
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
const handleEdit = (script: RegexScript) => {
|
||||
setEditingScript(script)
|
||||
setFormData({
|
||||
name: script.name,
|
||||
findRegex: script.findRegex,
|
||||
replaceWith: script.replaceWith,
|
||||
placement: script.placement,
|
||||
disabled: script.disabled,
|
||||
order: script.order,
|
||||
scope: 0,
|
||||
})
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (editingScript) {
|
||||
await regexAPI.update(editingScript.id, formData)
|
||||
} else {
|
||||
await regexAPI.create(formData)
|
||||
}
|
||||
setShowModal(false)
|
||||
loadScripts()
|
||||
} catch (error) {
|
||||
console.error('保存正则脚本失败:', error)
|
||||
alert('保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm('确定要删除这个正则脚本吗?')) return
|
||||
try {
|
||||
await regexAPI.delete(id)
|
||||
loadScripts()
|
||||
} catch (error) {
|
||||
console.error('删除正则脚本失败:', error)
|
||||
alert('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
const text = await file.text()
|
||||
const data = JSON.parse(text)
|
||||
|
||||
// 支持导入单个脚本或脚本数组
|
||||
const scriptsToImport = Array.isArray(data) ? data : [data]
|
||||
|
||||
for (const script of scriptsToImport) {
|
||||
await regexAPI.create({
|
||||
name: script.scriptName || script.name || '未命名脚本',
|
||||
findRegex: script.findRegex || '',
|
||||
replaceWith: script.replaceString || script.replaceWith || '',
|
||||
placement: script.placement ?? 1,
|
||||
disabled: script.disabled ?? false,
|
||||
order: script.order ?? 100,
|
||||
scope: 0, // 全局作用域
|
||||
})
|
||||
}
|
||||
|
||||
loadScripts()
|
||||
alert(`成功导入 ${scriptsToImport.length} 个脚本`)
|
||||
} catch (error) {
|
||||
console.error('导入失败:', error)
|
||||
alert('导入失败,请检查文件格式')
|
||||
}
|
||||
|
||||
e.target.value = ''
|
||||
}
|
||||
|
||||
const handleExport = (script: RegexScript) => {
|
||||
const data = {
|
||||
scriptName: script.name,
|
||||
findRegex: script.findRegex,
|
||||
replaceString: script.replaceWith,
|
||||
placement: script.placement,
|
||||
disabled: script.disabled,
|
||||
order: script.order,
|
||||
}
|
||||
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `${script.name}.json`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
const handleExportAll = () => {
|
||||
const data = scripts.map(script => ({
|
||||
scriptName: script.name,
|
||||
findRegex: script.findRegex,
|
||||
replaceString: script.replaceWith,
|
||||
placement: script.placement,
|
||||
disabled: script.disabled,
|
||||
order: script.order,
|
||||
}))
|
||||
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'global-regex-scripts.json'
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-purple-900 to-gray-900 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* 头部 */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-white mb-2">全局正则脚本</h1>
|
||||
<p className="text-gray-400">管理全局作用域的正则脚本,应用于所有对话</p>
|
||||
</div>
|
||||
|
||||
{/* 工具栏 */}
|
||||
<div className="glass rounded-xl p-4 mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索脚本名称..."
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 bg-black/30 border border-white/10 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-primary/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-primary hover:bg-primary/80 text-white rounded-lg transition-colors"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
新建脚本
|
||||
</button>
|
||||
|
||||
<label className="flex items-center gap-2 px-4 py-2 glass-hover rounded-lg cursor-pointer">
|
||||
<Upload className="w-5 h-5" />
|
||||
导入
|
||||
<input type="file" accept=".json" onChange={handleImport} className="hidden" />
|
||||
</label>
|
||||
|
||||
{scripts.length > 0 && (
|
||||
<button
|
||||
onClick={handleExportAll}
|
||||
className="flex items-center gap-2 px-4 py-2 glass-hover rounded-lg"
|
||||
>
|
||||
<Download className="w-5 h-5" />
|
||||
导出全部
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 脚本列表 */}
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-gray-400">加载中...</div>
|
||||
) : scripts.length === 0 ? (
|
||||
<div className="text-center py-12 glass rounded-xl">
|
||||
<p className="text-gray-400 mb-4">暂无全局正则脚本</p>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
className="px-6 py-2 bg-primary hover:bg-primary/80 text-white rounded-lg transition-colors"
|
||||
>
|
||||
创建第一个脚本
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
{scripts.map((script) => (
|
||||
<div key={script.id} className="glass rounded-xl p-6 hover:bg-white/5 transition-colors">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-lg font-semibold text-white">{script.name}</h3>
|
||||
<span className={`px-2 py-1 text-xs rounded ${
|
||||
script.disabled ? 'bg-red-500/20 text-red-400' : 'bg-green-500/20 text-green-400'
|
||||
}`}>
|
||||
{script.disabled ? '已禁用' : '已启用'}
|
||||
</span>
|
||||
<span className="px-2 py-1 text-xs rounded bg-blue-500/20 text-blue-400">
|
||||
{placementNames[script.placement]}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">顺序: {script.order}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleEdit(script)}
|
||||
className="p-2 glass-hover rounded-lg"
|
||||
title="编辑"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleExport(script)}
|
||||
className="p-2 glass-hover rounded-lg"
|
||||
title="导出"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(script.id)}
|
||||
className="p-2 glass-hover rounded-lg text-red-400"
|
||||
title="删除"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-400">查找正则:</span>
|
||||
<code className="ml-2 px-2 py-1 bg-black/30 rounded text-primary">{script.findRegex}</code>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">替换为:</span>
|
||||
<code className="ml-2 px-2 py-1 bg-black/30 rounded text-green-400">{script.replaceWith}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 编辑/创建模态框 */}
|
||||
{showModal && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||
<div className="glass rounded-xl p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl font-bold text-white">
|
||||
{editingScript ? '编辑正则脚本' : '新建正则脚本'}
|
||||
</h2>
|
||||
<button onClick={() => setShowModal(false)} className="p-2 glass-hover rounded-lg">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">脚本名称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="w-full px-4 py-2 bg-black/30 border border-white/10 rounded-lg text-white focus:outline-none focus:border-primary/50"
|
||||
placeholder="例如:移除HTML标签"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">查找正则表达式</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.findRegex}
|
||||
onChange={(e) => setFormData({ ...formData, findRegex: e.target.value })}
|
||||
className="w-full px-4 py-2 bg-black/30 border border-white/10 rounded-lg text-white font-mono focus:outline-none focus:border-primary/50"
|
||||
placeholder="例如:<[^>]+>"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">替换为</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.replaceWith}
|
||||
onChange={(e) => setFormData({ ...formData, replaceWith: e.target.value })}
|
||||
className="w-full px-4 py-2 bg-black/30 border border-white/10 rounded-lg text-white font-mono focus:outline-none focus:border-primary/50"
|
||||
placeholder="例如:空字符串或替换文本"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
支持变量:{'{{user}}, {{char}}, {{random}}, {{time}}'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">执行阶段</label>
|
||||
<select
|
||||
value={formData.placement}
|
||||
onChange={(e) => setFormData({ ...formData, placement: Number(e.target.value) })}
|
||||
className="w-full px-4 py-2 bg-black/30 border border-white/10 rounded-lg text-white focus:outline-none focus:border-primary/50"
|
||||
>
|
||||
<option value={0}>输入(用户消息)</option>
|
||||
<option value={1}>输出(AI回复)</option>
|
||||
<option value={2}>世界书</option>
|
||||
<option value={3}>显示(前端渲染)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">执行顺序</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.order}
|
||||
onChange={(e) => setFormData({ ...formData, order: Number(e.target.value) })}
|
||||
className="w-full px-4 py-2 bg-black/30 border border-white/10 rounded-lg text-white focus:outline-none focus:border-primary/50"
|
||||
placeholder="100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="disabled"
|
||||
checked={formData.disabled}
|
||||
onChange={(e) => setFormData({ ...formData, disabled: e.target.checked })}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<label htmlFor="disabled" className="text-sm text-gray-400">禁用此脚本</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 mt-6">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="flex-1 px-4 py-2 bg-primary hover:bg-primary/80 text-white rounded-lg transition-colors"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="flex-1 px-4 py-2 glass-hover rounded-lg"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user