From 8888d9ea851572ad0a2f37156c5550e5f12c99d3 Mon Sep 17 00:00:00 2001 From: Echo <1711788888@qq.com> Date: Mon, 2 Mar 2026 01:14:16 +0800 Subject: [PATCH] =?UTF-8?q?:art:=20=E6=96=B0=E5=A2=9E=E6=AD=A3=E5=88=99?= =?UTF-8?q?=E7=AE=A1=E7=90=86=20=E5=92=8C=20=E5=85=A8=E5=B1=80=E6=AD=A3?= =?UTF-8?q?=E5=88=99=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Echo <1711788888@qq.com> --- server/service/app/regex_script.go | 64 +++ web-app/package-lock.json | 32 +- web-app/package.json | 3 +- web-app/src/api/regex.ts | 17 +- web-app/src/pages/RegexScriptManagePage.tsx | 412 ++++++++++++++++++++ 5 files changed, 519 insertions(+), 9 deletions(-) create mode 100644 web-app/src/pages/RegexScriptManagePage.tsx diff --git a/server/service/app/regex_script.go b/server/service/app/regex_script.go index ffc91e7..e884302 100644 --- a/server/service/app/regex_script.go +++ b/server/service/app/regex_script.go @@ -3,8 +3,12 @@ package app import ( "encoding/json" "errors" + "fmt" + "math/rand" "regexp" + "strconv" "strings" + "time" "git.echol.cn/loser/st/server/global" "git.echol.cn/loser/st/server/model/app" @@ -236,14 +240,74 @@ func (s *RegexScriptService) ExecuteScript(script *app.RegexScript, text string, // substituteMacros 替换宏变量 func (s *RegexScriptService) substituteMacros(text string, userName string, charName string) string { result := text + + // 保存原始文本 + result = strings.ReplaceAll(result, "{{original}}", text) + + // 用户名变量 if userName != "" { result = strings.ReplaceAll(result, "{{user}}", userName) result = strings.ReplaceAll(result, "{{User}}", userName) } + + // 角色名变量 if charName != "" { result = strings.ReplaceAll(result, "{{char}}", charName) result = strings.ReplaceAll(result, "{{Char}}", charName) } + + // 时间变量 + now := time.Now() + result = strings.ReplaceAll(result, "{{time}}", now.Format("15:04:05")) + result = strings.ReplaceAll(result, "{{date}}", now.Format("2006-01-02")) + result = strings.ReplaceAll(result, "{{datetime}}", now.Format("2006-01-02 15:04:05")) + result = strings.ReplaceAll(result, "{{timestamp}}", fmt.Sprintf("%d", now.Unix())) + result = strings.ReplaceAll(result, "{{time_12h}}", now.Format("03:04:05 PM")) + result = strings.ReplaceAll(result, "{{date_short}}", now.Format("01/02/06")) + result = strings.ReplaceAll(result, "{{weekday}}", now.Weekday().String()) + result = strings.ReplaceAll(result, "{{month}}", now.Month().String()) + result = strings.ReplaceAll(result, "{{year}}", fmt.Sprintf("%d", now.Year())) + + // 随机数变量 + result = regexp.MustCompile(`\{\{random:(\d+)-(\d+)\}\}`).ReplaceAllStringFunc(result, func(match string) string { + re := regexp.MustCompile(`\{\{random:(\d+)-(\d+)\}\}`) + matches := re.FindStringSubmatch(match) + if len(matches) == 3 { + min, _ := strconv.Atoi(matches[1]) + max, _ := strconv.Atoi(matches[2]) + if max > min { + return fmt.Sprintf("%d", rand.Intn(max-min+1)+min) + } + } + return match + }) + + // 简单随机数 {{random}} + result = regexp.MustCompile(`\{\{random\}\}`).ReplaceAllStringFunc(result, func(match string) string { + return fmt.Sprintf("%d", rand.Intn(100)) + }) + + // 随机选择 {{pick:option1|option2|option3}} + result = regexp.MustCompile(`\{\{pick:([^}]+)\}\}`).ReplaceAllStringFunc(result, func(match string) string { + re := regexp.MustCompile(`\{\{pick:([^}]+)\}\}`) + matches := re.FindStringSubmatch(match) + if len(matches) == 2 { + options := strings.Split(matches[1], "|") + if len(options) > 0 { + return options[rand.Intn(len(options))] + } + } + return match + }) + + // 换行符变量 + result = strings.ReplaceAll(result, "{{newline}}", "\n") + result = strings.ReplaceAll(result, "{{tab}}", "\t") + result = strings.ReplaceAll(result, "{{space}}", " ") + + // 空值变量 + result = strings.ReplaceAll(result, "{{empty}}", "") + return result } diff --git a/web-app/package-lock.json b/web-app/package-lock.json index 417a586..cf2a684 100644 --- a/web-app/package-lock.json +++ b/web-app/package-lock.json @@ -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", diff --git a/web-app/package.json b/web-app/package.json index 9111f49..506ef7e 100644 --- a/web-app/package.json +++ b/web-app/package.json @@ -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", diff --git a/web-app/src/api/regex.ts b/web-app/src/api/regex.ts index 9059a85..02ef517 100644 --- a/web-app/src/api/regex.ts +++ b/web-app/src/api/regex.ts @@ -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 diff --git a/web-app/src/pages/RegexScriptManagePage.tsx b/web-app/src/pages/RegexScriptManagePage.tsx new file mode 100644 index 0000000..a2c3c82 --- /dev/null +++ b/web-app/src/pages/RegexScriptManagePage.tsx @@ -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([]) + const [loading, setLoading] = useState(false) + const [searchKeyword, setSearchKeyword] = useState('') + const [showModal, setShowModal] = useState(false) + const [editingScript, setEditingScript] = useState(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) => { + 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 ( +
+
+ {/* 头部 */} +
+

全局正则脚本

+

管理全局作用域的正则脚本,应用于所有对话

+
+ + {/* 工具栏 */} +
+
+
+ + 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" + /> +
+ + + + + + {scripts.length > 0 && ( + + )} +
+
+ + {/* 脚本列表 */} + {loading ? ( +
加载中...
+ ) : scripts.length === 0 ? ( +
+

暂无全局正则脚本

+ +
+ ) : ( +
+ {scripts.map((script) => ( +
+
+
+
+

{script.name}

+ + {script.disabled ? '已禁用' : '已启用'} + + + {placementNames[script.placement]} + + 顺序: {script.order} +
+
+ +
+ + + +
+
+ +
+
+ 查找正则: + {script.findRegex} +
+
+ 替换为: + {script.replaceWith} +
+
+
+ ))} +
+ )} + + {/* 编辑/创建模态框 */} + {showModal && ( +
+
+
+

+ {editingScript ? '编辑正则脚本' : '新建正则脚本'} +

+ +
+ +
+
+ + 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标签" + /> +
+ +
+ + 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="例如:<[^>]+>" + /> +
+ +
+ + 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="例如:空字符串或替换文本" + /> +

+ 支持变量:{'{{user}}, {{char}}, {{random}}, {{time}}'} +

+
+ +
+
+ + +
+ +
+ + 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" + /> +
+
+ +
+ setFormData({ ...formData, disabled: e.target.checked })} + className="w-4 h-4" + /> + +
+
+ +
+ + +
+
+
+ )} +
+
+ ) +}