@@ -8,4 +8,5 @@ type ApiGroup struct {
|
||||
PresetApi
|
||||
UploadApi
|
||||
WorldbookApi
|
||||
RegexScriptApi
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ func RegisterTables() {
|
||||
app.AIPreset{},
|
||||
app.Worldbook{},
|
||||
app.WorldbookEntry{},
|
||||
app.RegexScript{},
|
||||
)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("register table failed", zap.Error(err))
|
||||
|
||||
@@ -154,6 +154,7 @@ func Routers() *gin.Engine {
|
||||
appRouter.InitPresetRouter(appGroup) // 预设路由:/app/preset/*
|
||||
appRouter.InitUploadRouter(appGroup) // 上传路由:/app/upload/*
|
||||
appRouter.InitWorldbookRouter(appGroup) // 世界书路由:/app/worldbook/*
|
||||
appRouter.InitRegexScriptRouter(appGroup) // 正则脚本路由:/app/regex/*
|
||||
}
|
||||
|
||||
//插件路由安装
|
||||
|
||||
@@ -8,4 +8,5 @@ type RouterGroup struct {
|
||||
PresetRouter
|
||||
UploadRouter
|
||||
WorldbookRouter
|
||||
RegexScriptRouter
|
||||
}
|
||||
|
||||
@@ -267,7 +267,18 @@ func (s *CharacterService) ImportCharacterFromPNG(userID uint, file *multipart.F
|
||||
IsPublic: false,
|
||||
}
|
||||
|
||||
return s.CreateCharacter(userID, req)
|
||||
// 创建角色卡
|
||||
resp, err := s.CreateCharacter(userID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 处理扩展数据中的正则脚本
|
||||
if card.Data.Extensions != nil {
|
||||
s.processRegexScriptsFromExtensions(userID, resp.ID, card.Data.Extensions)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ImportCharacterFromJSON 从 JSON 文件导入角色卡
|
||||
@@ -312,7 +323,18 @@ func (s *CharacterService) ImportCharacterFromJSON(userID uint, file *multipart.
|
||||
IsPublic: false,
|
||||
}
|
||||
|
||||
return s.CreateCharacter(userID, req)
|
||||
// 创建角色卡
|
||||
resp, err := s.CreateCharacter(userID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 处理扩展数据中的正则脚本
|
||||
if card.Data.Extensions != nil {
|
||||
s.processRegexScriptsFromExtensions(userID, resp.ID, card.Data.Extensions)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ExportCharacterToJSON 导出角色卡为 JSON
|
||||
@@ -339,6 +361,16 @@ func (s *CharacterService) ExportCharacterToJSON(userID, characterID uint) (*uti
|
||||
json.Unmarshal(character.CharacterBook, &characterBook)
|
||||
json.Unmarshal(character.Extensions, &extensions)
|
||||
|
||||
// 查询角色关联的正则脚本并添加到 extensions
|
||||
var regexScripts []app.RegexScript
|
||||
global.GVA_DB.Where("owner_char_id = ? AND scope = 1", characterID).Find(®exScripts)
|
||||
if len(regexScripts) > 0 {
|
||||
if extensions == nil {
|
||||
extensions = make(map[string]interface{})
|
||||
}
|
||||
extensions["regex_scripts"] = regexScripts
|
||||
}
|
||||
|
||||
// 构建 V2 格式
|
||||
card := &utils.CharacterCardV2{
|
||||
Spec: character.Spec,
|
||||
@@ -364,3 +396,94 @@ func (s *CharacterService) ExportCharacterToJSON(userID, characterID uint) (*uti
|
||||
|
||||
return card, nil
|
||||
}
|
||||
|
||||
// processRegexScriptsFromExtensions 从扩展数据中提取并创建正则脚本
|
||||
func (s *CharacterService) processRegexScriptsFromExtensions(userID, characterID uint, extensions map[string]interface{}) {
|
||||
// 检查是否包含正则脚本
|
||||
regexScriptsData, ok := extensions["regex_scripts"]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为 JSON 以便解析
|
||||
scriptsJSON, err := json.Marshal(regexScriptsData)
|
||||
if err != nil {
|
||||
global.GVA_LOG.Error("序列化正则脚本失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 解析正则脚本数组
|
||||
var scripts []map[string]interface{}
|
||||
if err := json.Unmarshal(scriptsJSON, &scripts); err != nil {
|
||||
global.GVA_LOG.Error("解析正则脚本失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 创建正则脚本记录
|
||||
for _, scriptData := range scripts {
|
||||
script := app.RegexScript{
|
||||
UserID: userID,
|
||||
Scope: 1, // 角色作用域
|
||||
OwnerCharID: &characterID,
|
||||
}
|
||||
|
||||
// 提取字段
|
||||
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 {
|
||||
script.ReplaceWith = replaceWith
|
||||
}
|
||||
if placement, ok := scriptData["placement"].(float64); ok {
|
||||
script.Placement = int(placement)
|
||||
}
|
||||
if disabled, ok := scriptData["disabled"].(bool); ok {
|
||||
script.Disabled = disabled
|
||||
}
|
||||
if markdownOnly, ok := scriptData["markdownOnly"].(bool); ok {
|
||||
script.MarkdownOnly = markdownOnly
|
||||
}
|
||||
if runOnEdit, ok := scriptData["runOnEdit"].(bool); ok {
|
||||
script.RunOnEdit = runOnEdit
|
||||
}
|
||||
if promptOnly, ok := scriptData["promptOnly"].(bool); ok {
|
||||
script.PromptOnly = promptOnly
|
||||
}
|
||||
if substituteRegex, ok := scriptData["substituteRegex"].(bool); ok {
|
||||
script.SubstituteRegex = substituteRegex
|
||||
}
|
||||
if order, ok := scriptData["order"].(float64); ok {
|
||||
script.Order = int(order)
|
||||
}
|
||||
|
||||
// 处理可选的整数字段
|
||||
if minDepth, ok := scriptData["minDepth"].(float64); ok {
|
||||
depth := int(minDepth)
|
||||
script.MinDepth = &depth
|
||||
}
|
||||
if maxDepth, ok := scriptData["maxDepth"].(float64); ok {
|
||||
depth := int(maxDepth)
|
||||
script.MaxDepth = &depth
|
||||
}
|
||||
|
||||
// 处理 trimStrings 数组
|
||||
if trimStrings, ok := scriptData["trimStrings"].([]interface{}); ok {
|
||||
trimStringsJSON, _ := json.Marshal(trimStrings)
|
||||
script.TrimStrings = datatypes.JSON(trimStringsJSON)
|
||||
}
|
||||
|
||||
// 处理扩展字段
|
||||
if scriptExtensions, ok := scriptData["extensions"].(map[string]interface{}); ok {
|
||||
extensionsJSON, _ := json.Marshal(scriptExtensions)
|
||||
script.Extensions = datatypes.JSON(extensionsJSON)
|
||||
}
|
||||
|
||||
// 创建记录
|
||||
if err := global.GVA_DB.Create(&script).Error; err != nil {
|
||||
global.GVA_LOG.Error("创建正则脚本失败: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,5 @@ type AppServiceGroup struct {
|
||||
PresetService
|
||||
UploadService
|
||||
WorldbookService
|
||||
RegexScriptService
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {Image, Palette, RotateCcw, Save, Upload, X} from 'lucide-react'
|
||||
import {Image, Palette, RotateCcw, Save, Upload, X, BookOpen} from 'lucide-react'
|
||||
import {useEffect, useRef, useState} from 'react'
|
||||
import {type Conversation, conversationApi} from '../api/conversation'
|
||||
import {type Worldbook, getWorldbookList} from '../api/worldbook'
|
||||
|
||||
interface SettingsPanelProps {
|
||||
conversation: Conversation
|
||||
@@ -11,6 +12,8 @@ interface SettingsPanelProps {
|
||||
interface ConversationSettings {
|
||||
themeColor: string
|
||||
backgroundImage?: string
|
||||
worldbookId?: number
|
||||
worldbookEnabled: boolean
|
||||
}
|
||||
|
||||
const THEME_COLORS = [
|
||||
@@ -25,13 +28,16 @@ const THEME_COLORS = [
|
||||
export default function SettingsPanel({ conversation, onClose, onUpdate }: SettingsPanelProps) {
|
||||
const [settings, setSettings] = useState<ConversationSettings>({
|
||||
themeColor: '#7C3AED',
|
||||
worldbookEnabled: false,
|
||||
})
|
||||
const [hasChanges, setHasChanges] = useState(false)
|
||||
const [backgroundPreview, setBackgroundPreview] = useState<string>()
|
||||
const [worldbooks, setWorldbooks] = useState<Worldbook[]>([])
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings()
|
||||
loadWorldbooks()
|
||||
}, [])
|
||||
|
||||
const loadSettings = () => {
|
||||
@@ -44,6 +50,8 @@ export default function SettingsPanel({ conversation, onClose, onUpdate }: Setti
|
||||
// 合并默认设置和保存的设置
|
||||
const mergedSettings = {
|
||||
themeColor: '#7C3AED',
|
||||
worldbookEnabled: conversation.worldbookEnabled || false,
|
||||
worldbookId: conversation.worldbookId,
|
||||
...parsed
|
||||
}
|
||||
|
||||
@@ -60,6 +68,22 @@ export default function SettingsPanel({ conversation, onClose, onUpdate }: Setti
|
||||
} catch (e) {
|
||||
console.error('解析设置失败:', e)
|
||||
}
|
||||
} else {
|
||||
// 如果没有 settings,从 conversation 直接读取世界书配置
|
||||
setSettings(prev => ({
|
||||
...prev,
|
||||
worldbookEnabled: conversation.worldbookEnabled || false,
|
||||
worldbookId: conversation.worldbookId,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const loadWorldbooks = async () => {
|
||||
try {
|
||||
const response = await getWorldbookList({ page: 1, pageSize: 100 })
|
||||
setWorldbooks(response.data.list || [])
|
||||
} catch (error) {
|
||||
console.error('加载世界书列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +123,11 @@ export default function SettingsPanel({ conversation, onClose, onUpdate }: Setti
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
// 保存到后端
|
||||
await conversationApi.updateConversationSettings(conversation.id, settings)
|
||||
await conversationApi.updateConversationSettings(conversation.id, {
|
||||
settings,
|
||||
worldbookId: settings.worldbookId,
|
||||
worldbookEnabled: settings.worldbookEnabled,
|
||||
})
|
||||
|
||||
// 应用主题色到根元素
|
||||
if (settings.themeColor) {
|
||||
@@ -119,6 +147,7 @@ export default function SettingsPanel({ conversation, onClose, onUpdate }: Setti
|
||||
const handleReset = () => {
|
||||
setSettings({
|
||||
themeColor: '#7C3AED',
|
||||
worldbookEnabled: false,
|
||||
})
|
||||
setBackgroundPreview(undefined)
|
||||
setHasChanges(false)
|
||||
@@ -214,10 +243,64 @@ export default function SettingsPanel({ conversation, onClose, onUpdate }: Setti
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-hover rounded-2xl p-6">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="p-2 bg-primary/20 rounded-lg">
|
||||
<BookOpen className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<h3 className="font-semibold">世界书设置</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="text-sm font-medium">启用世界书</label>
|
||||
<p className="text-xs text-white/60 mt-1">根据关键词自动注入背景信息</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleChange('worldbookEnabled', !settings.worldbookEnabled)}
|
||||
className={`relative w-12 h-6 rounded-full transition-colors cursor-pointer ${
|
||||
settings.worldbookEnabled ? 'bg-primary' : 'bg-white/20'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute top-1 left-1 w-4 h-4 bg-white rounded-full transition-transform ${
|
||||
settings.worldbookEnabled ? 'translate-x-6' : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{settings.worldbookEnabled && (
|
||||
<div>
|
||||
<label className="text-sm text-white/80 mb-3 block">选择世界书</label>
|
||||
<select
|
||||
value={settings.worldbookId || ''}
|
||||
onChange={(e) => handleChange('worldbookId', e.target.value ? Number(e.target.value) : undefined)}
|
||||
className="w-full px-4 py-3 glass rounded-xl text-sm bg-white/5 border border-white/10 focus:border-primary/50 focus:outline-none cursor-pointer"
|
||||
>
|
||||
<option value="">未选择</option>
|
||||
{worldbooks.map((wb) => (
|
||||
<option key={wb.id} value={wb.id}>
|
||||
{wb.name} ({wb.entryCount} 条目)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{worldbooks.length === 0 && (
|
||||
<p className="text-xs text-white/40 mt-2">
|
||||
还没有世界书,请先在世界书管理页面创建
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-hover rounded-2xl p-4">
|
||||
<div className="text-xs text-white/60 space-y-2">
|
||||
<p>💡 主题颜色会应用到整个界面</p>
|
||||
<p>💡 背景图片仅在对话页面显示</p>
|
||||
<p>💡 世界书会根据对话内容自动触发相关条目</p>
|
||||
<p>💡 AI 模型可在对话界面顶部切换</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {useEffect, useState} from 'react'
|
||||
import {useNavigate, useSearchParams} from 'react-router-dom'
|
||||
import Navbar from '../components/Navbar'
|
||||
import {Book, Download, Edit, FileJson, Image as ImageIcon, Plus, Search, Trash2, X} from 'lucide-react'
|
||||
import {Book, Code2, Download, Edit, FileJson, Image as ImageIcon, Plus, Search, Trash2, X} from 'lucide-react'
|
||||
import {type Character, characterApi} from '../api/character'
|
||||
import {type RegexScript, regexScriptApi} from '../api/regex'
|
||||
|
||||
interface WorldBookEntry {
|
||||
keys: string[]
|
||||
@@ -32,6 +33,9 @@ export default function CharacterManagePage() {
|
||||
const [total, setTotal] = useState(0)
|
||||
const [showWorldBookEditor, setShowWorldBookEditor] = useState(false)
|
||||
const [worldBookEntries, setWorldBookEntries] = useState<WorldBookEntry[]>([])
|
||||
const [showRegexScriptEditor, setShowRegexScriptEditor] = useState(false)
|
||||
const [regexScripts, setRegexScripts] = useState<RegexScript[]>([])
|
||||
const [editingTab, setEditingTab] = useState<'basic' | 'worldbook' | 'regex'>('basic')
|
||||
const navigate = useNavigate()
|
||||
const [searchParams] = useSearchParams()
|
||||
|
||||
@@ -47,6 +51,7 @@ export default function CharacterManagePage() {
|
||||
setSelectedCharacter(char)
|
||||
setShowEditModal(true)
|
||||
loadWorldBook(char)
|
||||
loadRegexScripts(char.id)
|
||||
}
|
||||
}
|
||||
}, [searchParams, characters])
|
||||
@@ -66,6 +71,35 @@ export default function CharacterManagePage() {
|
||||
}
|
||||
}
|
||||
|
||||
const loadRegexScripts = async (characterId: number) => {
|
||||
try {
|
||||
const response = await regexScriptApi.getRegexScriptList({
|
||||
scope: 1,
|
||||
pageSize: 100,
|
||||
})
|
||||
console.log('正则脚本 API 响应:', response)
|
||||
|
||||
// apiClient 拦截器返回 response.data,所以这里的 response 就是后端的完整响应
|
||||
// 后端返回: {code: 0, data: {list: [...], total: 10, ...}, msg: '获取成功'}
|
||||
let list = []
|
||||
if (response.data && response.data.list && Array.isArray(response.data.list)) {
|
||||
// response.data.list 是正确的路径
|
||||
list = response.data.list
|
||||
} else {
|
||||
console.warn('正则脚本列表数据格式不正确:', response)
|
||||
setRegexScripts([])
|
||||
return
|
||||
}
|
||||
|
||||
const charScripts = list.filter((s: any) => s.ownerCharId === characterId)
|
||||
console.log(`找到 ${charScripts.length} 个角色关联的正则脚本`)
|
||||
setRegexScripts(charScripts)
|
||||
} catch (err) {
|
||||
console.error('加载正则脚本失败:', err)
|
||||
setRegexScripts([])
|
||||
}
|
||||
}
|
||||
|
||||
const loadCharacters = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
@@ -173,6 +207,7 @@ export default function CharacterManagePage() {
|
||||
setWorldBookEntries([...worldBookEntries, {
|
||||
keys: [],
|
||||
content: '',
|
||||
comment: '',
|
||||
enabled: true,
|
||||
insertion_order: worldBookEntries.length,
|
||||
position: 'after_char'
|
||||
@@ -308,6 +343,8 @@ export default function CharacterManagePage() {
|
||||
onClick={() => {
|
||||
setSelectedCharacter(char)
|
||||
setShowEditModal(true)
|
||||
loadWorldBook(char)
|
||||
loadRegexScripts(char.id)
|
||||
}}
|
||||
className="flex-1 px-4 py-2 glass-hover rounded-lg text-sm cursor-pointer flex items-center justify-center gap-2"
|
||||
>
|
||||
@@ -513,14 +550,22 @@ export default function CharacterManagePage() {
|
||||
<p className="text-xs text-white/40 mt-1">公开后其他用户可以在角色广场看到并使用此角色卡</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowWorldBookEditor(true)}
|
||||
className="w-full px-4 py-3 glass-hover rounded-xl text-sm cursor-pointer flex items-center justify-center gap-2"
|
||||
className="px-4 py-3 glass-hover rounded-xl text-sm cursor-pointer flex items-center justify-center gap-2"
|
||||
>
|
||||
<Book className="w-4 h-4" />
|
||||
编辑世界书 ({getWorldBookEntryCount(selectedCharacter)} 个条目)
|
||||
世界书 ({getWorldBookEntryCount(selectedCharacter)})
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowRegexScriptEditor(true)}
|
||||
className="px-4 py-3 glass-hover rounded-xl text-sm cursor-pointer flex items-center justify-center gap-2"
|
||||
>
|
||||
<Code2 className="w-4 h-4" />
|
||||
正则脚本 ({regexScripts.length})
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -572,7 +617,14 @@ export default function CharacterManagePage() {
|
||||
worldBookEntries.map((entry, index) => (
|
||||
<div key={index} className="glass rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm font-medium">条目 #{index + 1}</span>
|
||||
<div className="flex-1">
|
||||
<span className="text-sm font-medium">
|
||||
{entry.comment ? entry.comment : `条目 #${index + 1}`}
|
||||
</span>
|
||||
{entry.comment && (
|
||||
<span className="text-xs text-white/40 ml-2">#{index + 1}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
@@ -593,6 +645,17 @@ export default function CharacterManagePage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs text-white/60 mb-1 block">条目标题</label>
|
||||
<input
|
||||
type="text"
|
||||
value={entry.comment || ''}
|
||||
onChange={(e) => updateWorldBookEntry(index, 'comment', e.target.value)}
|
||||
placeholder="例如: 魔法系统设定"
|
||||
className="w-full px-3 py-2 glass rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-white/60 mb-1 block">触发关键词(逗号分隔)</label>
|
||||
<input
|
||||
@@ -672,6 +735,253 @@ export default function CharacterManagePage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showRegexScriptEditor && 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-4xl w-full max-h-[80vh] overflow-hidden flex flex-col">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-2xl font-bold">编辑正则脚本</h2>
|
||||
<button
|
||||
onClick={() => setShowRegexScriptEditor(false)}
|
||||
className="p-2 glass-hover rounded-lg cursor-pointer"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto pr-2 space-y-4">
|
||||
{regexScripts.length === 0 ? (
|
||||
<div className="glass rounded-xl p-8 text-center">
|
||||
<Code2 className="w-12 h-12 mx-auto mb-3 text-white/40" />
|
||||
<p className="text-white/60 mb-4">还没有正则脚本</p>
|
||||
<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 || '创建失败')
|
||||
}
|
||||
}}
|
||||
className="px-4 py-2 bg-gradient-to-r from-primary to-secondary rounded-lg text-sm cursor-pointer"
|
||||
>
|
||||
添加第一个脚本
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
regexScripts.map((script) => (
|
||||
<div key={script.id} className="glass rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<input
|
||||
type="text"
|
||||
value={script.name}
|
||||
onChange={(e) => {
|
||||
const newScripts = regexScripts.map(s =>
|
||||
s.id === script.id ? { ...s, name: e.target.value } : s
|
||||
)
|
||||
setRegexScripts(newScripts)
|
||||
}}
|
||||
className="flex-1 px-3 py-2 glass rounded-lg text-sm font-medium focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
/>
|
||||
<div className="flex items-center gap-2 ml-3">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!script.disabled}
|
||||
onChange={(e) => {
|
||||
const newScripts = regexScripts.map(s =>
|
||||
s.id === script.id ? { ...s, disabled: !e.target.checked } : s
|
||||
)
|
||||
setRegexScripts(newScripts)
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
<span className="text-xs text-white/60">启用</span>
|
||||
</label>
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!confirm('确定要删除这个脚本吗?')) return
|
||||
try {
|
||||
await regexScriptApi.deleteRegexScript(script.id)
|
||||
loadRegexScripts(selectedCharacter.id)
|
||||
} catch (err: any) {
|
||||
alert(err.response?.data?.msg || '删除失败')
|
||||
}
|
||||
}}
|
||||
className="p-1 glass-hover rounded text-red-400 cursor-pointer"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs text-white/60 mb-1 block">查找正则表达式</label>
|
||||
<input
|
||||
type="text"
|
||||
value={script.findRegex}
|
||||
onChange={(e) => {
|
||||
const newScripts = regexScripts.map(s =>
|
||||
s.id === script.id ? { ...s, findRegex: e.target.value } : s
|
||||
)
|
||||
setRegexScripts(newScripts)
|
||||
}}
|
||||
placeholder="例如: /\*\*(.+?)\*\*/"
|
||||
className="w-full px-3 py-2 glass rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-white/60 mb-1 block">替换为</label>
|
||||
<input
|
||||
type="text"
|
||||
value={script.replaceWith}
|
||||
onChange={(e) => {
|
||||
const newScripts = regexScripts.map(s =>
|
||||
s.id === script.id ? { ...s, replaceWith: e.target.value } : s
|
||||
)
|
||||
setRegexScripts(newScripts)
|
||||
}}
|
||||
placeholder="例如: <strong>$1</strong>"
|
||||
className="w-full px-3 py-2 glass rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="text-xs text-white/60 mb-1 block">执行阶段</label>
|
||||
<select
|
||||
value={script.placement}
|
||||
onChange={(e) => {
|
||||
const newScripts = regexScripts.map(s =>
|
||||
s.id === script.id ? { ...s, placement: parseInt(e.target.value) } : s
|
||||
)
|
||||
setRegexScripts(newScripts)
|
||||
}}
|
||||
className="w-full px-3 py-2 glass rounded-lg 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-xs text-white/60 mb-1 block">执行顺序</label>
|
||||
<input
|
||||
type="number"
|
||||
value={script.order}
|
||||
onChange={(e) => {
|
||||
const newScripts = regexScripts.map(s =>
|
||||
s.id === script.id ? { ...s, order: parseInt(e.target.value) } : s
|
||||
)
|
||||
setRegexScripts(newScripts)
|
||||
}}
|
||||
className="w-full px-3 py-2 glass rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary/50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1 pt-5">
|
||||
<label className="flex items-center gap-2 cursor-pointer text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={script.substituteRegex}
|
||||
onChange={(e) => {
|
||||
const newScripts = regexScripts.map(s =>
|
||||
s.id === script.id ? { ...s, substituteRegex: e.target.checked } : s
|
||||
)
|
||||
setRegexScripts(newScripts)
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
<span className="text-white/60">替换宏</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={async () => {
|
||||
const testInput = prompt('输入测试文本:', '这是一段**测试**文本')
|
||||
if (!testInput) return
|
||||
|
||||
try {
|
||||
const result = await regexScriptApi.testRegexScript(script.id, testInput)
|
||||
alert(`原文:${result.data.original}\n\n结果:${result.data.result}`)
|
||||
} catch (err: any) {
|
||||
alert(err.response?.data?.msg || '测试失败')
|
||||
}
|
||||
}}
|
||||
className="px-3 py-1.5 glass-hover rounded-lg text-xs cursor-pointer"
|
||||
>
|
||||
测试
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await regexScriptApi.updateRegexScript(script.id, {
|
||||
name: script.name,
|
||||
findRegex: script.findRegex,
|
||||
replaceWith: script.replaceWith,
|
||||
placement: script.placement,
|
||||
disabled: script.disabled,
|
||||
order: script.order,
|
||||
substituteRegex: script.substituteRegex,
|
||||
})
|
||||
alert('保存成功')
|
||||
} catch (err: any) {
|
||||
alert(err.response?.data?.msg || '保存失败')
|
||||
}
|
||||
}}
|
||||
className="px-3 py-1.5 bg-gradient-to-r from-primary to-secondary rounded-lg text-xs cursor-pointer"
|
||||
>
|
||||
保存
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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 || '创建失败')
|
||||
}
|
||||
}}
|
||||
className="px-4 py-2 glass-hover rounded-lg text-sm cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
添加脚本
|
||||
</button>
|
||||
<div className="flex-1" />
|
||||
<button
|
||||
onClick={() => setShowRegexScriptEditor(false)}
|
||||
className="px-4 py-2 bg-gradient-to-r from-primary to-secondary rounded-lg text-sm cursor-pointer"
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user