@@ -8,4 +8,5 @@ type ApiGroup struct {
|
|||||||
PresetApi
|
PresetApi
|
||||||
UploadApi
|
UploadApi
|
||||||
WorldbookApi
|
WorldbookApi
|
||||||
|
RegexScriptApi
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ func RegisterTables() {
|
|||||||
app.AIPreset{},
|
app.AIPreset{},
|
||||||
app.Worldbook{},
|
app.Worldbook{},
|
||||||
app.WorldbookEntry{},
|
app.WorldbookEntry{},
|
||||||
|
app.RegexScript{},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.GVA_LOG.Error("register table failed", zap.Error(err))
|
global.GVA_LOG.Error("register table failed", zap.Error(err))
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ func Routers() *gin.Engine {
|
|||||||
appRouter.InitPresetRouter(appGroup) // 预设路由:/app/preset/*
|
appRouter.InitPresetRouter(appGroup) // 预设路由:/app/preset/*
|
||||||
appRouter.InitUploadRouter(appGroup) // 上传路由:/app/upload/*
|
appRouter.InitUploadRouter(appGroup) // 上传路由:/app/upload/*
|
||||||
appRouter.InitWorldbookRouter(appGroup) // 世界书路由:/app/worldbook/*
|
appRouter.InitWorldbookRouter(appGroup) // 世界书路由:/app/worldbook/*
|
||||||
|
appRouter.InitRegexScriptRouter(appGroup) // 正则脚本路由:/app/regex/*
|
||||||
}
|
}
|
||||||
|
|
||||||
//插件路由安装
|
//插件路由安装
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ type RouterGroup struct {
|
|||||||
PresetRouter
|
PresetRouter
|
||||||
UploadRouter
|
UploadRouter
|
||||||
WorldbookRouter
|
WorldbookRouter
|
||||||
|
RegexScriptRouter
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -267,7 +267,18 @@ func (s *CharacterService) ImportCharacterFromPNG(userID uint, file *multipart.F
|
|||||||
IsPublic: false,
|
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 文件导入角色卡
|
// ImportCharacterFromJSON 从 JSON 文件导入角色卡
|
||||||
@@ -312,7 +323,18 @@ func (s *CharacterService) ImportCharacterFromJSON(userID uint, file *multipart.
|
|||||||
IsPublic: false,
|
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
|
// ExportCharacterToJSON 导出角色卡为 JSON
|
||||||
@@ -339,6 +361,16 @@ func (s *CharacterService) ExportCharacterToJSON(userID, characterID uint) (*uti
|
|||||||
json.Unmarshal(character.CharacterBook, &characterBook)
|
json.Unmarshal(character.CharacterBook, &characterBook)
|
||||||
json.Unmarshal(character.Extensions, &extensions)
|
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 格式
|
// 构建 V2 格式
|
||||||
card := &utils.CharacterCardV2{
|
card := &utils.CharacterCardV2{
|
||||||
Spec: character.Spec,
|
Spec: character.Spec,
|
||||||
@@ -364,3 +396,94 @@ func (s *CharacterService) ExportCharacterToJSON(userID, characterID uint) (*uti
|
|||||||
|
|
||||||
return card, nil
|
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
|
PresetService
|
||||||
UploadService
|
UploadService
|
||||||
WorldbookService
|
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 {useEffect, useRef, useState} from 'react'
|
||||||
import {type Conversation, conversationApi} from '../api/conversation'
|
import {type Conversation, conversationApi} from '../api/conversation'
|
||||||
|
import {type Worldbook, getWorldbookList} from '../api/worldbook'
|
||||||
|
|
||||||
interface SettingsPanelProps {
|
interface SettingsPanelProps {
|
||||||
conversation: Conversation
|
conversation: Conversation
|
||||||
@@ -11,6 +12,8 @@ interface SettingsPanelProps {
|
|||||||
interface ConversationSettings {
|
interface ConversationSettings {
|
||||||
themeColor: string
|
themeColor: string
|
||||||
backgroundImage?: string
|
backgroundImage?: string
|
||||||
|
worldbookId?: number
|
||||||
|
worldbookEnabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const THEME_COLORS = [
|
const THEME_COLORS = [
|
||||||
@@ -25,13 +28,16 @@ const THEME_COLORS = [
|
|||||||
export default function SettingsPanel({ conversation, onClose, onUpdate }: SettingsPanelProps) {
|
export default function SettingsPanel({ conversation, onClose, onUpdate }: SettingsPanelProps) {
|
||||||
const [settings, setSettings] = useState<ConversationSettings>({
|
const [settings, setSettings] = useState<ConversationSettings>({
|
||||||
themeColor: '#7C3AED',
|
themeColor: '#7C3AED',
|
||||||
|
worldbookEnabled: false,
|
||||||
})
|
})
|
||||||
const [hasChanges, setHasChanges] = useState(false)
|
const [hasChanges, setHasChanges] = useState(false)
|
||||||
const [backgroundPreview, setBackgroundPreview] = useState<string>()
|
const [backgroundPreview, setBackgroundPreview] = useState<string>()
|
||||||
|
const [worldbooks, setWorldbooks] = useState<Worldbook[]>([])
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSettings()
|
loadSettings()
|
||||||
|
loadWorldbooks()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const loadSettings = () => {
|
const loadSettings = () => {
|
||||||
@@ -44,6 +50,8 @@ export default function SettingsPanel({ conversation, onClose, onUpdate }: Setti
|
|||||||
// 合并默认设置和保存的设置
|
// 合并默认设置和保存的设置
|
||||||
const mergedSettings = {
|
const mergedSettings = {
|
||||||
themeColor: '#7C3AED',
|
themeColor: '#7C3AED',
|
||||||
|
worldbookEnabled: conversation.worldbookEnabled || false,
|
||||||
|
worldbookId: conversation.worldbookId,
|
||||||
...parsed
|
...parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +68,22 @@ export default function SettingsPanel({ conversation, onClose, onUpdate }: Setti
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('解析设置失败:', 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 () => {
|
const handleSave = async () => {
|
||||||
try {
|
try {
|
||||||
// 保存到后端
|
// 保存到后端
|
||||||
await conversationApi.updateConversationSettings(conversation.id, settings)
|
await conversationApi.updateConversationSettings(conversation.id, {
|
||||||
|
settings,
|
||||||
|
worldbookId: settings.worldbookId,
|
||||||
|
worldbookEnabled: settings.worldbookEnabled,
|
||||||
|
})
|
||||||
|
|
||||||
// 应用主题色到根元素
|
// 应用主题色到根元素
|
||||||
if (settings.themeColor) {
|
if (settings.themeColor) {
|
||||||
@@ -119,6 +147,7 @@ export default function SettingsPanel({ conversation, onClose, onUpdate }: Setti
|
|||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
setSettings({
|
setSettings({
|
||||||
themeColor: '#7C3AED',
|
themeColor: '#7C3AED',
|
||||||
|
worldbookEnabled: false,
|
||||||
})
|
})
|
||||||
setBackgroundPreview(undefined)
|
setBackgroundPreview(undefined)
|
||||||
setHasChanges(false)
|
setHasChanges(false)
|
||||||
@@ -214,10 +243,64 @@ export default function SettingsPanel({ conversation, onClose, onUpdate }: Setti
|
|||||||
</div>
|
</div>
|
||||||
</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="glass-hover rounded-2xl p-4">
|
||||||
<div className="text-xs text-white/60 space-y-2">
|
<div className="text-xs text-white/60 space-y-2">
|
||||||
<p>💡 主题颜色会应用到整个界面</p>
|
<p>💡 主题颜色会应用到整个界面</p>
|
||||||
<p>💡 背景图片仅在对话页面显示</p>
|
<p>💡 背景图片仅在对话页面显示</p>
|
||||||
|
<p>💡 世界书会根据对话内容自动触发相关条目</p>
|
||||||
<p>💡 AI 模型可在对话界面顶部切换</p>
|
<p>💡 AI 模型可在对话界面顶部切换</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import {useEffect, useState} from 'react'
|
import {useEffect, useState} from 'react'
|
||||||
import {useNavigate, useSearchParams} from 'react-router-dom'
|
import {useNavigate, useSearchParams} from 'react-router-dom'
|
||||||
import Navbar from '../components/Navbar'
|
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 Character, characterApi} from '../api/character'
|
||||||
|
import {type RegexScript, regexScriptApi} from '../api/regex'
|
||||||
|
|
||||||
interface WorldBookEntry {
|
interface WorldBookEntry {
|
||||||
keys: string[]
|
keys: string[]
|
||||||
@@ -32,6 +33,9 @@ export default function CharacterManagePage() {
|
|||||||
const [total, setTotal] = useState(0)
|
const [total, setTotal] = useState(0)
|
||||||
const [showWorldBookEditor, setShowWorldBookEditor] = useState(false)
|
const [showWorldBookEditor, setShowWorldBookEditor] = useState(false)
|
||||||
const [worldBookEntries, setWorldBookEntries] = useState<WorldBookEntry[]>([])
|
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 navigate = useNavigate()
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
|
|
||||||
@@ -47,6 +51,7 @@ export default function CharacterManagePage() {
|
|||||||
setSelectedCharacter(char)
|
setSelectedCharacter(char)
|
||||||
setShowEditModal(true)
|
setShowEditModal(true)
|
||||||
loadWorldBook(char)
|
loadWorldBook(char)
|
||||||
|
loadRegexScripts(char.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [searchParams, characters])
|
}, [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 () => {
|
const loadCharacters = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -173,6 +207,7 @@ export default function CharacterManagePage() {
|
|||||||
setWorldBookEntries([...worldBookEntries, {
|
setWorldBookEntries([...worldBookEntries, {
|
||||||
keys: [],
|
keys: [],
|
||||||
content: '',
|
content: '',
|
||||||
|
comment: '',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
insertion_order: worldBookEntries.length,
|
insertion_order: worldBookEntries.length,
|
||||||
position: 'after_char'
|
position: 'after_char'
|
||||||
@@ -308,6 +343,8 @@ export default function CharacterManagePage() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedCharacter(char)
|
setSelectedCharacter(char)
|
||||||
setShowEditModal(true)
|
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"
|
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>
|
<p className="text-xs text-white/40 mt-1">公开后其他用户可以在角色广场看到并使用此角色卡</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowWorldBookEditor(true)}
|
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" />
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -572,7 +617,14 @@ export default function CharacterManagePage() {
|
|||||||
worldBookEntries.map((entry, index) => (
|
worldBookEntries.map((entry, index) => (
|
||||||
<div key={index} className="glass rounded-xl p-4">
|
<div key={index} className="glass rounded-xl p-4">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<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">
|
<div className="flex items-center gap-2">
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
@@ -593,6 +645,17 @@ export default function CharacterManagePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<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>
|
<div>
|
||||||
<label className="text-xs text-white/60 mb-1 block">触发关键词(逗号分隔)</label>
|
<label className="text-xs text-white/60 mb-1 block">触发关键词(逗号分隔)</label>
|
||||||
<input
|
<input
|
||||||
@@ -672,6 +735,253 @@ export default function CharacterManagePage() {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user