🎨 优化角色卡编辑功能,新增正则编辑和测试

Signed-off-by: Echo <1711788888@qq.com>
This commit is contained in:
2026-03-02 00:50:56 +08:00
parent aa461ec6c3
commit fd660c8804
3 changed files with 256 additions and 24 deletions

View File

@@ -427,14 +427,23 @@ func (s *CharacterService) processRegexScriptsFromExtensions(userID, characterID
OwnerCharID: &characterID,
}
// 提取字段
if name, ok := scriptData["name"].(string); ok {
// 提取字段 - 兼容 SillyTavern 的字段名
// 脚本名称:优先使用 scriptName其次 name
if scriptName, ok := scriptData["scriptName"].(string); ok {
script.Name = scriptName
} else if name, ok := scriptData["name"].(string); ok {
script.Name = name
}
// 查找正则表达式
if findRegex, ok := scriptData["findRegex"].(string); ok {
script.FindRegex = findRegex
}
if replaceWith, ok := scriptData["replaceWith"].(string); ok {
// 替换字符串:优先使用 replaceString其次 replaceWith
if replaceString, ok := scriptData["replaceString"].(string); ok {
script.ReplaceWith = replaceString
} else if replaceWith, ok := scriptData["replaceWith"].(string); ok {
script.ReplaceWith = replaceWith
}
if placement, ok := scriptData["placement"].(float64); ok {

View File

@@ -627,7 +627,7 @@ export default function ChatArea({ conversation, character, onConversationUpdate
</div>
)}
<div className={`max-w-[70%] min-w-0 flex flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}>
<div className={`min-w-0 flex flex-col ${msg.role === 'user' ? 'max-w-[70%] items-end' : 'w-[70%] items-start'}`}>
{/* 助手名称 */}
{msg.role === 'assistant' && (
<span className="text-xs text-white/40 mb-1 ml-1">{character.name}</span>
@@ -638,7 +638,7 @@ export default function ChatArea({ conversation, character, onConversationUpdate
className={`relative px-4 py-3 rounded-2xl ${
msg.role === 'user'
? 'bg-primary/25 border border-primary/20 rounded-br-md'
: 'glass rounded-bl-md'
: 'glass rounded-bl-md w-full'
}`}
style={{ wordBreak: 'break-word', overflowWrap: 'anywhere' }}
>

View File

@@ -1,7 +1,7 @@
import {useEffect, useState} from 'react'
import {useNavigate, useSearchParams} from 'react-router-dom'
import Navbar from '../components/Navbar'
import {Book, Code2, Download, Edit, FileJson, Image as ImageIcon, Plus, Search, Trash2, X} from 'lucide-react'
import {Book, Code2, Download, Edit, FileJson, FileUp, Image as ImageIcon, Plus, Search, Trash2, X} from 'lucide-react'
import {type Character, characterApi} from '../api/character'
import {type RegexScript, regexScriptApi} from '../api/regex'
@@ -36,6 +36,14 @@ export default function CharacterManagePage() {
const [showRegexScriptEditor, setShowRegexScriptEditor] = useState(false)
const [regexScripts, setRegexScripts] = useState<RegexScript[]>([])
const [editingTab, setEditingTab] = useState<'basic' | 'worldbook' | 'regex'>('basic')
const [showAddRegexModal, setShowAddRegexModal] = useState(false)
const [newRegexForm, setNewRegexForm] = useState({
name: '',
findRegex: '',
replaceWith: '',
placement: 1,
order: 100,
})
const navigate = useNavigate()
const [searchParams] = useSearchParams()
@@ -759,13 +767,18 @@ export default function CharacterManagePage() {
try {
await regexScriptApi.createRegexScript({
name: '新脚本',
findRegex: '',
findRegex: '.*', // 默认匹配所有内容
replaceWith: '',
scope: 1,
ownerCharId: selectedCharacter.id,
disabled: false,
placement: 1, // 默认输出阶段
order: 100,
})
loadRegexScripts(selectedCharacter.id)
} catch (err: any) {
alert(err.response?.data?.msg || '创建失败')
console.error('创建脚本失败:', err)
alert(err.response?.data?.msg || err.message || '创建失败')
}
}}
className="px-4 py-2 bg-gradient-to-r from-primary to-secondary rounded-lg text-sm cursor-pointer"
@@ -913,10 +926,19 @@ export default function CharacterManagePage() {
if (!testInput) return
try {
const result = await regexScriptApi.testRegexScript(script.id, testInput)
alert(`原文:${result.data.original}\n\n结果${result.data.result}`)
const response = await regexScriptApi.testRegexScript(script.id, testInput)
console.log('测试响应:', response)
// 处理响应数据结构
const testResult = response.data || response
if (testResult.success === false && testResult.error) {
alert(`测试失败:${testResult.error}`)
} else {
alert(`原文:${testResult.original}\n\n结果${testResult.result}`)
}
} catch (err: any) {
alert(err.response?.data?.msg || '测试失败')
console.error('测试失败:', err)
alert(err.response?.data?.msg || err.message || '测试失败')
}
}}
className="px-3 py-1.5 glass-hover rounded-lg text-xs cursor-pointer"
@@ -953,19 +975,7 @@ export default function CharacterManagePage() {
<div className="flex gap-3 mt-6 pt-6 border-t border-white/10">
<button
onClick={async () => {
try {
await regexScriptApi.createRegexScript({
name: '新脚本',
findRegex: '',
scope: 1,
ownerCharId: selectedCharacter.id,
})
loadRegexScripts(selectedCharacter.id)
} catch (err: any) {
alert(err.response?.data?.msg || '创建失败')
}
}}
onClick={() => setShowAddRegexModal(true)}
className="px-4 py-2 glass-hover rounded-lg text-sm cursor-pointer flex items-center gap-2"
>
<Plus className="w-4 h-4" />
@@ -982,6 +992,219 @@ export default function CharacterManagePage() {
</div>
</div>
)}
{/* 添加正则脚本弹窗 */}
{showAddRegexModal && selectedCharacter && (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="glass rounded-3xl p-8 max-w-2xl w-full">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold"></h2>
<button
onClick={() => {
setShowAddRegexModal(false)
setNewRegexForm({
name: '',
findRegex: '',
replaceWith: '',
placement: 1,
order: 100,
})
}}
className="p-2 glass-hover rounded-lg cursor-pointer"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="space-y-6">
{/* 导入 JSON 文件 */}
<div>
<label className="block text-sm text-white/80 mb-2"> JSON </label>
<label className="glass-hover rounded-xl p-6 border-2 border-dashed border-white/20 cursor-pointer block text-center">
<FileUp className="w-8 h-8 mx-auto mb-2 text-white/40" />
<span className="text-sm text-white/60"> JSON </span>
<input
type="file"
accept=".json"
onChange={async (e) => {
const file = e.target.files?.[0]
if (!file) return
try {
const text = await file.text()
const jsonData = JSON.parse(text)
// 支持单个脚本或脚本数组
const scripts = Array.isArray(jsonData) ? jsonData : [jsonData]
for (const scriptData of scripts) {
await regexScriptApi.createRegexScript({
name: scriptData.scriptName || scriptData.name || '导入的脚本',
findRegex: scriptData.findRegex || '.*',
replaceWith: scriptData.replaceString || scriptData.replaceWith || '',
placement: scriptData.placement || 1,
disabled: scriptData.disabled || false,
order: scriptData.order || 100,
scope: 1,
ownerCharId: selectedCharacter.id,
markdownOnly: scriptData.markdownOnly || false,
runOnEdit: scriptData.runOnEdit || false,
promptOnly: scriptData.promptOnly || false,
substituteRegex: scriptData.substituteRegex !== false,
})
}
loadRegexScripts(selectedCharacter.id)
setShowAddRegexModal(false)
alert(`成功导入 ${scripts.length} 个正则脚本`)
} catch (err: any) {
console.error('导入失败:', err)
alert('导入失败:' + (err.message || '文件格式不正确'))
}
}}
className="hidden"
/>
</label>
<p className="text-xs text-white/40 mt-2">
SillyTavern JSON
</p>
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-white/10"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-4 glass text-white/60"></span>
</div>
</div>
{/* 手动输入表单 */}
<div className="space-y-4">
<div>
<label className="text-sm text-white/80 mb-2 block"> *</label>
<input
type="text"
value={newRegexForm.name}
onChange={(e) => setNewRegexForm({ ...newRegexForm, name: e.target.value })}
placeholder="例如: 移除 Markdown 加粗"
className="w-full px-4 py-3 glass rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
<div>
<label className="text-sm text-white/80 mb-2 block"> *</label>
<input
type="text"
value={newRegexForm.findRegex}
onChange={(e) => setNewRegexForm({ ...newRegexForm, findRegex: e.target.value })}
placeholder="例如: \*\*(.+?)\*\*"
className="w-full px-4 py-3 glass rounded-xl text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
<div>
<label className="text-sm text-white/80 mb-2 block"></label>
<input
type="text"
value={newRegexForm.replaceWith}
onChange={(e) => setNewRegexForm({ ...newRegexForm, replaceWith: e.target.value })}
placeholder="例如: <strong>$1</strong>"
className="w-full px-4 py-3 glass rounded-xl text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-sm text-white/80 mb-2 block"></label>
<select
value={newRegexForm.placement}
onChange={(e) => setNewRegexForm({ ...newRegexForm, placement: parseInt(e.target.value) })}
className="w-full px-4 py-3 glass rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary/50"
>
<option value="0"></option>
<option value="1"></option>
<option value="2"></option>
<option value="3"></option>
</select>
</div>
<div>
<label className="text-sm text-white/80 mb-2 block"></label>
<input
type="number"
value={newRegexForm.order}
onChange={(e) => setNewRegexForm({ ...newRegexForm, order: parseInt(e.target.value) })}
className="w-full px-4 py-3 glass rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
</div>
</div>
<div className="flex gap-3 pt-4">
<button
onClick={() => {
setShowAddRegexModal(false)
setNewRegexForm({
name: '',
findRegex: '',
replaceWith: '',
placement: 1,
order: 100,
})
}}
className="flex-1 px-6 py-3 glass-hover rounded-xl cursor-pointer"
>
</button>
<button
onClick={async () => {
if (!newRegexForm.name.trim()) {
alert('请输入脚本名称')
return
}
if (!newRegexForm.findRegex.trim()) {
alert('请输入查找正则表达式')
return
}
try {
await regexScriptApi.createRegexScript({
name: newRegexForm.name,
findRegex: newRegexForm.findRegex,
replaceWith: newRegexForm.replaceWith,
placement: newRegexForm.placement,
order: newRegexForm.order,
scope: 1,
ownerCharId: selectedCharacter.id,
disabled: false,
substituteRegex: true,
})
loadRegexScripts(selectedCharacter.id)
setShowAddRegexModal(false)
setNewRegexForm({
name: '',
findRegex: '',
replaceWith: '',
placement: 1,
order: 100,
})
alert('创建成功')
} catch (err: any) {
console.error('创建失败:', err)
alert(err.response?.data?.msg || err.message || '创建失败')
}
}}
className="flex-1 px-6 py-3 bg-gradient-to-r from-primary to-secondary rounded-xl font-medium hover:opacity-90 transition-opacity cursor-pointer"
>
</button>
</div>
</div>
</div>
</div>
)}
</div>
)
}