@@ -19,12 +19,13 @@ type Conversation struct {
|
|||||||
Title string `gorm:"type:varchar(200)" json:"title"` // 对话标题
|
Title string `gorm:"type:varchar(200)" json:"title"` // 对话标题
|
||||||
|
|
||||||
// 对话配置
|
// 对话配置
|
||||||
PresetID *uint `gorm:"index" json:"presetId"` // 使用的预设ID
|
PresetID *uint `gorm:"index" json:"presetId"` // 使用的预设ID
|
||||||
WorldbookID *uint `gorm:"index" json:"worldbookId"` // 使用的世界书ID
|
WorldbookID *uint `gorm:"index" json:"worldbookId"` // 使用的世界书ID
|
||||||
AIProvider string `gorm:"type:varchar(50)" json:"aiProvider"` // AI提供商 (openai/anthropic)
|
AIProvider string `gorm:"type:varchar(50)" json:"aiProvider"` // AI提供商 (openai/anthropic)
|
||||||
Model string `gorm:"type:varchar(100)" json:"model"` // 使用的模型
|
Model string `gorm:"type:varchar(100)" json:"model"` // 使用的模型
|
||||||
Settings datatypes.JSON `gorm:"type:jsonb" json:"settings"` // 对话设置 (temperature等)
|
Settings datatypes.JSON `gorm:"type:jsonb" json:"settings"` // 对话设置 (temperature等)
|
||||||
WorldbookEnabled bool `gorm:"default:false" json:"worldbookEnabled"` // 是否启用世界书
|
WorldbookEnabled bool `gorm:"default:false" json:"worldbookEnabled"` // 是否启用世界书
|
||||||
|
Variables datatypes.JSON `gorm:"type:jsonb;default:'{}'" json:"variables"` // 变量存储 ({{setvar::}}/{{getvar::}})
|
||||||
|
|
||||||
// 统计信息
|
// 统计信息
|
||||||
MessageCount int `gorm:"default:0" json:"messageCount"` // 消息数量
|
MessageCount int `gorm:"default:0" json:"messageCount"` // 消息数量
|
||||||
|
|||||||
@@ -101,14 +101,10 @@ func (s *ConversationService) CreateConversation(userID uint, req *request.Creat
|
|||||||
userName = user.NickName
|
userName = user.NickName
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用输出阶段正则脚本处理开场白
|
// 【重要】不再应用正则脚本处理开场白,保留原始内容
|
||||||
|
// 让前端来处理 <Status_block> 和 <maintext> 的渲染
|
||||||
processedFirstMes := character.FirstMes
|
processedFirstMes := character.FirstMes
|
||||||
var regexService RegexScriptService
|
global.GVA_LOG.Info(fmt.Sprintf("[开场白] 保留原始内容,长度=%d", len(processedFirstMes)))
|
||||||
outputScripts, err := regexService.GetScriptsForPlacement(userID, 1, &character.ID, nil)
|
|
||||||
if err == nil && len(outputScripts) > 0 {
|
|
||||||
processedFirstMes = regexService.ExecuteScripts(outputScripts, processedFirstMes, userName, character.Name)
|
|
||||||
global.GVA_LOG.Info(fmt.Sprintf("开场白应用正则脚本: 原始长度=%d, 处理后长度=%d", len(character.FirstMes), len(processedFirstMes)))
|
|
||||||
}
|
|
||||||
|
|
||||||
firstMessage := app.Message{
|
firstMessage := app.Message{
|
||||||
ConversationID: conversation.ID,
|
ConversationID: conversation.ID,
|
||||||
@@ -418,16 +414,75 @@ func (s *ConversationService) SendMessage(userID, conversationID uint, req *requ
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用显示阶段的正则脚本 (Placement 3)
|
// 提取并保存变量 (从 AI 回复中提取 {{setvar::key::value}})
|
||||||
displayContent := assistantMessage.Content
|
newVars, cleanedContent := regexService.ExtractSetVars(assistantMessage.Content)
|
||||||
displayScripts, err := regexService.GetScriptsForPlacement(userID, 3, &conversation.CharacterID, nil)
|
if len(newVars) > 0 {
|
||||||
if err == nil && len(displayScripts) > 0 {
|
// 加载现有变量
|
||||||
displayContent = regexService.ExecuteScripts(displayScripts, displayContent, userName, character.Name)
|
var existingVars map[string]string
|
||||||
global.GVA_LOG.Info(fmt.Sprintf("应用了 %d 个显示阶段正则脚本", len(displayScripts)))
|
if len(conversation.Variables) > 0 {
|
||||||
|
json.Unmarshal(conversation.Variables, &existingVars)
|
||||||
|
}
|
||||||
|
if existingVars == nil {
|
||||||
|
existingVars = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并新变量
|
||||||
|
for k, v := range newVars {
|
||||||
|
existingVars[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存回数据库
|
||||||
|
varsJSON, _ := json.Marshal(existingVars)
|
||||||
|
global.GVA_DB.Model(&conversation).Update("variables", datatypes.JSON(varsJSON))
|
||||||
|
global.GVA_LOG.Info(fmt.Sprintf("提取并保存了 %d 个变量: %v", len(newVars), newVars))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 先替换 {{getvar::}} 为实际变量值(在应用正则脚本之前)
|
||||||
|
var currentVars map[string]string
|
||||||
|
if len(conversation.Variables) > 0 {
|
||||||
|
json.Unmarshal(conversation.Variables, ¤tVars)
|
||||||
|
}
|
||||||
|
displayContent := cleanedContent // 使用清理后的内容(移除了 {{setvar::}})
|
||||||
|
if currentVars != nil {
|
||||||
|
displayContent = regexService.SubstituteGetVars(displayContent, currentVars)
|
||||||
|
global.GVA_LOG.Info(fmt.Sprintf("替换了 {{getvar::}} 变量"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取 <Status_block> 和 <maintext>,保护它们不被正则脚本修改
|
||||||
|
statusBlock, contentWithoutStatus := regexService.ExtractStatusBlock(displayContent)
|
||||||
|
maintext, contentWithoutMaintext := regexService.ExtractMaintext(contentWithoutStatus)
|
||||||
|
|
||||||
|
global.GVA_LOG.Info(fmt.Sprintf("[状态栏] 提取到 Status_block 长度: %d, maintext 长度: %d", len(statusBlock), len(maintext)))
|
||||||
|
if len(statusBlock) > 0 {
|
||||||
|
previewLen := len(statusBlock)
|
||||||
|
if previewLen > 100 {
|
||||||
|
previewLen = 100
|
||||||
|
}
|
||||||
|
global.GVA_LOG.Info(fmt.Sprintf("[状态栏] Status_block 内容预览: %s", statusBlock[:previewLen]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用显示阶段的正则脚本 (Placement 3) - 只处理剩余内容
|
||||||
|
finalProcessedContent := contentWithoutMaintext
|
||||||
|
displayScripts, err2 := regexService.GetScriptsForPlacement(userID, 3, &conversation.CharacterID, nil)
|
||||||
|
if err2 == nil && len(displayScripts) > 0 {
|
||||||
|
global.GVA_LOG.Info(fmt.Sprintf("[状态栏] 应用正则脚本前的内容长度: %d", len(finalProcessedContent)))
|
||||||
|
finalProcessedContent = regexService.ExecuteScripts(displayScripts, finalProcessedContent, userName, character.Name)
|
||||||
|
global.GVA_LOG.Info(fmt.Sprintf("[状态栏] 应用了 %d 个显示阶段正则脚本,处理后内容长度: %d", len(displayScripts), len(finalProcessedContent)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新组装内容:maintext + Status_block + 处理后的内容
|
||||||
|
finalContent := finalProcessedContent
|
||||||
|
if maintext != "" {
|
||||||
|
finalContent = "<maintext>" + maintext + "</maintext>\n\n" + finalContent
|
||||||
|
}
|
||||||
|
if statusBlock != "" {
|
||||||
|
finalContent = finalContent + "\n\n<Status_block>\n" + statusBlock + "\n</Status_block>"
|
||||||
|
}
|
||||||
|
|
||||||
|
global.GVA_LOG.Info(fmt.Sprintf("[状态栏] 最终返回内容长度: %d", len(finalContent)))
|
||||||
|
|
||||||
resp := response.ToMessageResponse(&assistantMessage)
|
resp := response.ToMessageResponse(&assistantMessage)
|
||||||
resp.Content = displayContent // 使用处理后的显示内容
|
resp.Content = finalContent // 使用处理后的显示内容
|
||||||
return &resp, nil
|
return &resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,31 +632,10 @@ func (s *ConversationService) callAIService(conversation app.Conversation, chara
|
|||||||
}
|
}
|
||||||
global.GVA_LOG.Info(fmt.Sprintf("========== AI返回的完整内容 ==========\n%s\n==========================================", aiResponse))
|
global.GVA_LOG.Info(fmt.Sprintf("========== AI返回的完整内容 ==========\n%s\n==========================================", aiResponse))
|
||||||
|
|
||||||
// 应用输出阶段的正则脚本 (Placement 1)
|
// 【重要】不再应用 Placement 1 正则脚本,保留 AI 原始回复
|
||||||
var regexService RegexScriptService
|
// 让 SendMessage 函数来提取和保护 <Status_block> 和 <maintext>
|
||||||
global.GVA_LOG.Info(fmt.Sprintf("查询输出阶段正则脚本: userID=%d, placement=1, charID=%d", conversation.UserID, conversation.CharacterID))
|
// 前端会负责渲染这些标签
|
||||||
outputScripts, err := regexService.GetScriptsForPlacement(conversation.UserID, 1, &conversation.CharacterID, nil)
|
global.GVA_LOG.Info("[AI回复] 保留原始内容,不应用输出阶段正则脚本")
|
||||||
if err != nil {
|
|
||||||
global.GVA_LOG.Error(fmt.Sprintf("查询输出阶段正则脚本失败: %v", err))
|
|
||||||
} else {
|
|
||||||
global.GVA_LOG.Info(fmt.Sprintf("找到 %d 个输出阶段正则脚本", len(outputScripts)))
|
|
||||||
if len(outputScripts) > 0 {
|
|
||||||
// 获取用户信息
|
|
||||||
var user app.AppUser
|
|
||||||
err = global.GVA_DB.Where("id = ?", conversation.UserID).First(&user).Error
|
|
||||||
userName := ""
|
|
||||||
if err == nil {
|
|
||||||
userName = user.Username
|
|
||||||
if userName == "" {
|
|
||||||
userName = user.NickName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
originalResponse := aiResponse
|
|
||||||
aiResponse = regexService.ExecuteScripts(outputScripts, aiResponse, userName, character.Name)
|
|
||||||
global.GVA_LOG.Info(fmt.Sprintf("应用了 %d 个输出阶段正则脚本,原文: %s, 处理后: %s", len(outputScripts), originalResponse, aiResponse))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return aiResponse, nil
|
return aiResponse, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -311,6 +311,78 @@ func (s *RegexScriptService) substituteMacros(text string, userName string, char
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExtractSetVars 从文本中提取 {{setvar::key::value}} 并返回变量映射和清理后的文本
|
||||||
|
func (s *RegexScriptService) ExtractSetVars(text string) (map[string]string, string) {
|
||||||
|
vars := make(map[string]string)
|
||||||
|
|
||||||
|
// 匹配 {{setvar::key::value}}
|
||||||
|
re := regexp.MustCompile(`\{\{setvar::([^:]+)::([^}]*)\}\}`)
|
||||||
|
matches := re.FindAllStringSubmatch(text, -1)
|
||||||
|
|
||||||
|
for _, match := range matches {
|
||||||
|
if len(match) == 3 {
|
||||||
|
key := strings.TrimSpace(match[1])
|
||||||
|
value := match[2]
|
||||||
|
vars[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从文本中移除所有 {{setvar::}} 标记
|
||||||
|
cleanText := re.ReplaceAllString(text, "")
|
||||||
|
|
||||||
|
return vars, cleanText
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubstituteGetVars 替换文本中的 {{getvar::key}} 为实际变量值
|
||||||
|
func (s *RegexScriptService) SubstituteGetVars(text string, variables map[string]string) string {
|
||||||
|
result := text
|
||||||
|
|
||||||
|
// 匹配 {{getvar::key}}
|
||||||
|
re := regexp.MustCompile(`\{\{getvar::([^}]+)\}\}`)
|
||||||
|
result = re.ReplaceAllStringFunc(result, func(match string) string {
|
||||||
|
matches := re.FindStringSubmatch(match)
|
||||||
|
if len(matches) == 2 {
|
||||||
|
key := strings.TrimSpace(matches[1])
|
||||||
|
if value, ok := variables[key]; ok {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "" // 如果变量不存在,返回空字符串
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractStatusBlock 提取 <Status_block> 中的 YAML 数据
|
||||||
|
func (s *RegexScriptService) ExtractStatusBlock(text string) (string, string) {
|
||||||
|
// 匹配 <Status_block>...</Status_block>
|
||||||
|
re := regexp.MustCompile(`(?s)<Status_block>\s*(.*?)\s*</Status_block>`)
|
||||||
|
matches := re.FindStringSubmatch(text)
|
||||||
|
|
||||||
|
if len(matches) == 2 {
|
||||||
|
statusBlock := strings.TrimSpace(matches[1])
|
||||||
|
cleanText := re.ReplaceAllString(text, "")
|
||||||
|
return statusBlock, cleanText
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", text
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractMaintext 提取 <maintext> 中的内容
|
||||||
|
func (s *RegexScriptService) ExtractMaintext(text string) (string, string) {
|
||||||
|
// 匹配 <maintext>...</maintext>
|
||||||
|
re := regexp.MustCompile(`(?s)<maintext>\s*(.*?)\s*</maintext>`)
|
||||||
|
matches := re.FindStringSubmatch(text)
|
||||||
|
|
||||||
|
if len(matches) == 2 {
|
||||||
|
maintext := strings.TrimSpace(matches[1])
|
||||||
|
cleanText := re.ReplaceAllString(text, "")
|
||||||
|
return maintext, cleanText
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", text
|
||||||
|
}
|
||||||
|
|
||||||
// GetScriptsForPlacement 获取指定阶段的脚本
|
// GetScriptsForPlacement 获取指定阶段的脚本
|
||||||
func (s *RegexScriptService) GetScriptsForPlacement(userID uint, placement int, charID *uint, presetID *uint) ([]app.RegexScript, error) {
|
func (s *RegexScriptService) GetScriptsForPlacement(userID uint, placement int, charID *uint, presetID *uint) ([]app.RegexScript, error) {
|
||||||
var scripts []app.RegexScript
|
var scripts []app.RegexScript
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export interface UpdateCharacterRequest {
|
|||||||
postHistoryInstructions?: string
|
postHistoryInstructions?: string
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
alternateGreetings?: string[]
|
alternateGreetings?: string[]
|
||||||
characterBook?: Record<string, any>
|
characterBook?: Record<string, any> | null
|
||||||
extensions?: Record<string, any>
|
extensions?: Record<string, any>
|
||||||
isPublic?: boolean
|
isPublic?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,24 @@ export default function ChatArea({ conversation, character, onConversationUpdate
|
|||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}, [messages])
|
}, [messages])
|
||||||
|
|
||||||
|
// 监听状态栏按钮点击事件
|
||||||
|
useEffect(() => {
|
||||||
|
const handleStatusBarAction = (event: CustomEvent) => {
|
||||||
|
const action = event.detail
|
||||||
|
if (action && typeof action === 'string' && !sending) {
|
||||||
|
console.log('[ChatArea] 收到状态栏操作,自动发送消息:', action)
|
||||||
|
setInputValue(action)
|
||||||
|
// 延迟发送,确保 inputValue 已更新
|
||||||
|
setTimeout(() => {
|
||||||
|
handleSendMessage(action)
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('sendMessageFromStatusBar', handleStatusBarAction as EventListener)
|
||||||
|
return () => window.removeEventListener('sendMessageFromStatusBar', handleStatusBarAction as EventListener)
|
||||||
|
}, [sending])
|
||||||
|
|
||||||
const loadMessages = async () => {
|
const loadMessages = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -185,11 +203,10 @@ export default function ChatArea({ conversation, character, onConversationUpdate
|
|||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSend = async () => {
|
const handleSendMessage = async (message: string) => {
|
||||||
if (!inputValue.trim() || sending) return
|
if (!message.trim() || sending) return
|
||||||
|
|
||||||
const userMessage = inputValue.trim()
|
const userMessage = message.trim()
|
||||||
setInputValue('')
|
|
||||||
setSending(true)
|
setSending(true)
|
||||||
|
|
||||||
const tempUserMessage: Message = {
|
const tempUserMessage: Message = {
|
||||||
@@ -277,6 +294,13 @@ export default function ChatArea({ conversation, character, onConversationUpdate
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSend = async () => {
|
||||||
|
if (!inputValue.trim() || sending) return
|
||||||
|
const userMessage = inputValue.trim()
|
||||||
|
setInputValue('')
|
||||||
|
await handleSendMessage(userMessage)
|
||||||
|
}
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey && !sending && inputValue.trim()) {
|
if (e.key === 'Enter' && !e.shiftKey && !sending && inputValue.trim()) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -644,7 +668,7 @@ export default function ChatArea({ conversation, character, onConversationUpdate
|
|||||||
>
|
>
|
||||||
<MessageContent
|
<MessageContent
|
||||||
content={msg.content}
|
content={msg.content}
|
||||||
role={msg.role}
|
role={msg.role as 'user' | 'assistant'}
|
||||||
onChoiceSelect={(choice) => {
|
onChoiceSelect={(choice) => {
|
||||||
setInputValue(choice)
|
setInputValue(choice)
|
||||||
textareaRef.current?.focus()
|
textareaRef.current?.focus()
|
||||||
|
|||||||
@@ -127,21 +127,35 @@ function parseChoices(content: string): { choices: Choice[]; cleanContent: strin
|
|||||||
return { choices, cleanContent }
|
return { choices, cleanContent }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理脚本输出内容
|
// 提取 <maintext> 内容
|
||||||
function cleanScriptOutput(content: string): string {
|
function extractMaintext(content: string): { maintext: string; cleanContent: string } {
|
||||||
// 移除 <UpdateVariable>...</UpdateVariable> 块
|
const maintextRegex = /<maintext>([\s\S]*?)<\/maintext>/i
|
||||||
let cleaned = content.replace(/<UpdateVariable>[\s\S]*?<\/UpdateVariable>/gi, '')
|
const match = content.match(maintextRegex)
|
||||||
|
|
||||||
// 移除 <Analysis>...</Analysis> 块
|
if (!match) {
|
||||||
cleaned = cleaned.replace(/<Analysis>[\s\S]*?<\/Analysis>/gi, '')
|
return { maintext: '', cleanContent: content }
|
||||||
|
}
|
||||||
|
|
||||||
// 移除 _.set() 调用
|
const maintext = match[1].trim()
|
||||||
cleaned = cleaned.replace(/^\s*_.set\([^)]+\);\s*$/gm, '')
|
const cleanContent = content.replace(maintextRegex, '').trim()
|
||||||
|
return { maintext, cleanContent }
|
||||||
return cleaned.trim()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析状态面板数据
|
// 提取 <Status_block> YAML 数据
|
||||||
|
function extractStatusBlock(content: string): { statusYaml: string; cleanContent: string } {
|
||||||
|
const statusRegex = /<Status_block>\s*([\s\S]*?)\s*<\/Status_block>/i
|
||||||
|
const match = content.match(statusRegex)
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return { statusYaml: '', cleanContent: content }
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusYaml = match[1].trim()
|
||||||
|
const cleanContent = content.replace(statusRegex, '').trim()
|
||||||
|
return { statusYaml, cleanContent }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析状态面板数据(JSON 格式 - 保留兼容性)
|
||||||
function parseStatusPanel(content: string): { status: any; cleanContent: string } {
|
function parseStatusPanel(content: string): { status: any; cleanContent: string } {
|
||||||
const statusRegex = /<status_current_variable>([\s\S]*?)<\/status_current_variable>/i
|
const statusRegex = /<status_current_variable>([\s\S]*?)<\/status_current_variable>/i
|
||||||
const match = content.match(statusRegex)
|
const match = content.match(statusRegex)
|
||||||
@@ -167,28 +181,49 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
|
|||||||
const [allowScript, setAllowScript] = useState(false)
|
const [allowScript, setAllowScript] = useState(false)
|
||||||
const [choices, setChoices] = useState<Choice[]>([])
|
const [choices, setChoices] = useState<Choice[]>([])
|
||||||
const [displayContent, setDisplayContent] = useState(content)
|
const [displayContent, setDisplayContent] = useState(content)
|
||||||
const [remainingText, setRemainingText] = useState('')
|
|
||||||
const [statusPanel, setStatusPanel] = useState<any>(null)
|
const [statusPanel, setStatusPanel] = useState<any>(null)
|
||||||
|
const [statusYaml, setStatusYaml] = useState<string>('')
|
||||||
|
const [maintext, setMaintext] = useState<string>('')
|
||||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||||
|
const statusIframeRef = useRef<HTMLIFrameElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('[MessageContent] 原始内容:', content)
|
console.log('[MessageContent] 原始内容:', content)
|
||||||
|
|
||||||
let processedContent = content
|
let processedContent = content
|
||||||
|
|
||||||
// 解析状态面板
|
// 提取 <maintext> 内容
|
||||||
const { status, cleanContent: contentAfterStatus } = parseStatusPanel(processedContent)
|
const { maintext: extractedMaintext, cleanContent: contentAfterMaintext } = extractMaintext(processedContent)
|
||||||
console.log('[MessageContent] 状态面板:', status)
|
if (extractedMaintext) {
|
||||||
setStatusPanel(status)
|
console.log('[MessageContent] 提取到 maintext:', extractedMaintext)
|
||||||
|
setMaintext(extractedMaintext)
|
||||||
|
processedContent = contentAfterMaintext
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取 <Status_block> YAML 数据
|
||||||
|
const { statusYaml: extractedYaml, cleanContent: contentAfterStatus } = extractStatusBlock(processedContent)
|
||||||
|
if (extractedYaml) {
|
||||||
|
console.log('[MessageContent] 提取到 Status_block YAML:', extractedYaml)
|
||||||
|
setStatusYaml(extractedYaml)
|
||||||
|
processedContent = contentAfterStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析状态面板(JSON 格式 - 保留兼容性)
|
||||||
|
const { status, cleanContent: contentAfterStatusPanel } = parseStatusPanel(processedContent)
|
||||||
|
if (status) {
|
||||||
|
console.log('[MessageContent] 状态面板:', status)
|
||||||
|
setStatusPanel(status)
|
||||||
|
processedContent = contentAfterStatusPanel
|
||||||
|
}
|
||||||
|
|
||||||
// 解析选择项
|
// 解析选择项
|
||||||
const { choices: parsedChoices, cleanContent } = parseChoices(contentAfterStatus)
|
const { choices: parsedChoices, cleanContent } = parseChoices(contentAfterStatus)
|
||||||
setChoices(parsedChoices)
|
setChoices(parsedChoices)
|
||||||
console.log('[MessageContent] 选择项:', parsedChoices)
|
console.log('[MessageContent] 选择项:', parsedChoices)
|
||||||
|
|
||||||
// 清理脚本输出
|
// 直接使用清理后的内容,不再进行脚本输出清理
|
||||||
const finalContent = cleanScriptOutput(cleanContent)
|
const finalContent = cleanContent
|
||||||
console.log('[MessageContent] 清理后内容:', finalContent)
|
console.log('[MessageContent] 最终内容:', finalContent)
|
||||||
|
|
||||||
// 检测是否包含 HTML 标签或代码块
|
// 检测是否包含 HTML 标签或代码块
|
||||||
const htmlRegex = /<[^>]+>/g
|
const htmlRegex = /<[^>]+>/g
|
||||||
@@ -277,7 +312,6 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
|
|||||||
}
|
}
|
||||||
|
|
||||||
setDisplayContent(renderedContent)
|
setDisplayContent(renderedContent)
|
||||||
setRemainingText('')
|
|
||||||
|
|
||||||
setHasHtml(hasHtmlTags || hasCodeBlocks)
|
setHasHtml(hasHtmlTags || hasCodeBlocks)
|
||||||
setHasScript(hasScriptContent)
|
setHasScript(hasScriptContent)
|
||||||
@@ -336,6 +370,30 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
|
|||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script>
|
||||||
|
// 监听来自父页面的消息(用于更新状态栏)
|
||||||
|
window.addEventListener('message', function(event) {
|
||||||
|
if (event.data.type === 'updateStatus') {
|
||||||
|
// 更新状态栏中的变量值
|
||||||
|
Object.keys(event.data.variables || {}).forEach(function(key) {
|
||||||
|
var elements = document.querySelectorAll('[data-var="' + key + '"]');
|
||||||
|
elements.forEach(function(el) {
|
||||||
|
el.textContent = event.data.variables[key];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 向父页面发送消息的辅助函数
|
||||||
|
function sendToParent(type, data) {
|
||||||
|
window.parent.postMessage({ type: type, data: data }, '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户操作触发函数(供状态栏按钮调用)
|
||||||
|
function onPlayerAction(action) {
|
||||||
|
sendToParent('playerAction', { action: action });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
${displayContent}
|
${displayContent}
|
||||||
@@ -354,6 +412,239 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 渲染状态栏 iframe(使用 doc.write 动态注入 YAML 数据)
|
||||||
|
const renderStatusBar = () => {
|
||||||
|
if (!statusIframeRef.current || !statusYaml) return
|
||||||
|
|
||||||
|
console.log('[MessageContent] 开始渲染状态栏,YAML 数据长度:', statusYaml.length)
|
||||||
|
|
||||||
|
const iframe = statusIframeRef.current
|
||||||
|
const doc = iframe.contentDocument || iframe.contentWindow?.document
|
||||||
|
|
||||||
|
if (doc) {
|
||||||
|
// 使用 doc.write() 分步注入,避免模板字符串的转义问题
|
||||||
|
doc.open()
|
||||||
|
|
||||||
|
// 1. 写入 HTML 头部
|
||||||
|
doc.write(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js"><\/script>
|
||||||
|
<script src="https://cdn.tailwindcss.com"><\/script>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
background: transparent;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.status-block {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.status-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.status-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(157, 124, 245, 1);
|
||||||
|
}
|
||||||
|
.status-info {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
.character-card {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 8px 0;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
.character-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: rgba(157, 124, 245, 1);
|
||||||
|
}
|
||||||
|
.attribute {
|
||||||
|
margin: 4px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.attribute-key {
|
||||||
|
color: rgba(157, 124, 245, 0.8);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.option-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 4px 0;
|
||||||
|
background: rgba(157, 124, 245, 0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border-left: 2px solid rgba(157, 124, 245, 0.3);
|
||||||
|
}
|
||||||
|
.option-item:hover {
|
||||||
|
background: rgba(157, 124, 245, 0.2);
|
||||||
|
border-left-color: rgba(157, 124, 245, 0.7);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
`)
|
||||||
|
|
||||||
|
// 2. 创建 YAML script 标签并直接设置 textContent(避免 HTML 转义)
|
||||||
|
doc.write('<script id="yaml-data-source" type="text/yaml"><\/script>')
|
||||||
|
|
||||||
|
// 3. 写入渲染脚本
|
||||||
|
doc.write(`
|
||||||
|
<script>
|
||||||
|
// 向父页面发送消息
|
||||||
|
function sendToParent(type, data) {
|
||||||
|
window.parent.postMessage({ type: type, data: data }, '*');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户操作触发函数
|
||||||
|
function onPlayerAction(action) {
|
||||||
|
console.log('[StatusBar] 发送操作:', action);
|
||||||
|
sendToParent('playerAction', { action: action });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即执行渲染
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
const yamlScript = document.getElementById('yaml-data-source');
|
||||||
|
if (!yamlScript || !yamlScript.textContent.trim()) {
|
||||||
|
document.body.innerHTML = '<div style="padding:20px;text-align:center;color:rgba(255,255,255,0.5);">状态栏数据加载中...</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const yamlData = jsyaml.load(yamlScript.textContent);
|
||||||
|
if (!yamlData || Object.keys(yamlData).length === 0) {
|
||||||
|
document.body.innerHTML = '<div style="padding:20px;text-align:center;color:rgba(255,255,255,0.5);">状态栏数据为空</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染状态栏
|
||||||
|
const rootKey = Object.keys(yamlData)[0];
|
||||||
|
const data = yamlData[rootKey];
|
||||||
|
|
||||||
|
let html = '<div class="status-block">';
|
||||||
|
|
||||||
|
// 渲染标题
|
||||||
|
html += '<div class="status-header">';
|
||||||
|
html += '<div class="status-title">' + rootKey + '</div>';
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// 渲染基本信息(日期、地点等)
|
||||||
|
Object.entries(data).forEach(([key, value]) => {
|
||||||
|
if (key.includes('日期') || key.includes('时间') || key.includes('地点') || key.includes('位置')) {
|
||||||
|
html += '<div class="status-info">' + value + '</div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 渲染用户列表
|
||||||
|
if (data['用户列表'] && Array.isArray(data['用户列表'])) {
|
||||||
|
html += '<div style="margin-top:12px;">';
|
||||||
|
data['用户列表'].forEach(userItem => {
|
||||||
|
const userData = userItem['用户'] || userItem;
|
||||||
|
if (typeof userData === 'object') {
|
||||||
|
html += '<div class="character-card">';
|
||||||
|
const userName = userData['名字'] || '未知角色';
|
||||||
|
html += '<div class="character-name">' + userName + '</div>';
|
||||||
|
|
||||||
|
Object.entries(userData).forEach(([k, v]) => {
|
||||||
|
if (k !== '名字') {
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
html += '<div class="attribute"><span class="attribute-key">' + k + ':</span></div>';
|
||||||
|
v.forEach(item => {
|
||||||
|
if (typeof item === 'object') {
|
||||||
|
Object.values(item).forEach(val => {
|
||||||
|
html += '<div style="margin-left:12px;font-size:12px;">' + val + '</div>';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
html += '<div style="margin-left:12px;font-size:12px;">' + item + '</div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
html += '<div class="attribute"><span class="attribute-key">' + k + ':</span> ' + v + '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染行动选项
|
||||||
|
if (data['行动选项'] && typeof data['行动选项'] === 'object') {
|
||||||
|
const actionOptions = data['行动选项'];
|
||||||
|
html += '<div style="margin-top:12px;">';
|
||||||
|
html += '<div style="font-weight:600;margin-bottom:8px;color:rgba(157,124,245,1);">';
|
||||||
|
html += (actionOptions['名字'] || '角色') + ' 的行动选项';
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
if (actionOptions['选项'] && Array.isArray(actionOptions['选项'])) {
|
||||||
|
actionOptions['选项'].forEach(option => {
|
||||||
|
const escapedOption = option.replace(/'/g, "\\\\'").replace(/"/g, '"');
|
||||||
|
html += '<div class="option-item" onclick="onPlayerAction(\\'' + escapedOption + '\\')">' + option + '</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
document.body.innerHTML = html;
|
||||||
|
console.log('[StatusBar] 渲染成功');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[StatusBar] 渲染失败:', error);
|
||||||
|
document.body.innerHTML = '<div style="padding:20px;text-align:center;color:rgba(255,100,100,0.8);">状态栏渲染失败: ' + error.message + '</div>';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
<\/script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`)
|
||||||
|
|
||||||
|
doc.close()
|
||||||
|
|
||||||
|
// 4. 关键步骤:直接设置 YAML 数据到 script 标签的 textContent
|
||||||
|
// 这样可以避免 HTML 转义问题
|
||||||
|
const yamlScript = doc.getElementById('yaml-data-source')
|
||||||
|
if (yamlScript) {
|
||||||
|
yamlScript.textContent = statusYaml
|
||||||
|
console.log('[MessageContent] YAML 数据已注入,长度:', statusYaml.length)
|
||||||
|
} else {
|
||||||
|
console.error('[MessageContent] 找不到 yaml-data-source 元素')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 自动调整 iframe 高度
|
||||||
|
setTimeout(() => {
|
||||||
|
if (doc.body) {
|
||||||
|
const height = doc.body.scrollHeight
|
||||||
|
iframe.style.height = `${Math.max(height + 32, 200)}px`
|
||||||
|
console.log('[MessageContent] iframe 高度已调整:', iframe.style.height)
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (allowScript && hasScript && iframeRef.current) {
|
if (allowScript && hasScript && iframeRef.current) {
|
||||||
// 延迟渲染确保 iframe 已挂载
|
// 延迟渲染确保 iframe 已挂载
|
||||||
@@ -363,6 +654,15 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
|
|||||||
}
|
}
|
||||||
}, [allowScript, hasScript, displayContent])
|
}, [allowScript, hasScript, displayContent])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (statusYaml && statusIframeRef.current) {
|
||||||
|
// 延迟渲染确保 iframe 已挂载
|
||||||
|
setTimeout(() => {
|
||||||
|
renderStatusBar()
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
}, [statusYaml])
|
||||||
|
|
||||||
// 如果是用户消息,直接显示纯文本
|
// 如果是用户消息,直接显示纯文本
|
||||||
if (role === 'user') {
|
if (role === 'user') {
|
||||||
return <p className="text-sm leading-relaxed whitespace-pre-wrap break-words overflow-wrap-anywhere">{content}</p>
|
return <p className="text-sm leading-relaxed whitespace-pre-wrap break-words overflow-wrap-anywhere">{content}</p>
|
||||||
@@ -451,7 +751,7 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 状态面板 */}
|
{/* 状态面板(JSON 格式 - 保留兼容性) */}
|
||||||
{statusPanel && (
|
{statusPanel && (
|
||||||
<div className="mt-4 p-4 glass rounded-lg border border-secondary/20">
|
<div className="mt-4 p-4 glass rounded-lg border border-secondary/20">
|
||||||
<div className="text-sm font-medium text-secondary mb-3">状态面板</div>
|
<div className="text-sm font-medium text-secondary mb-3">状态面板</div>
|
||||||
@@ -478,6 +778,31 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 状态栏(YAML 格式 - SillyTavern 兼容) */}
|
||||||
|
{statusYaml && (
|
||||||
|
<div className="mt-4 w-full border border-white/10 rounded-lg bg-black/20 overflow-hidden">
|
||||||
|
<iframe
|
||||||
|
ref={statusIframeRef}
|
||||||
|
className="w-full"
|
||||||
|
sandbox="allow-scripts allow-same-origin"
|
||||||
|
style={{ minHeight: '200px', border: 'none' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* maintext 内容(如果提取到) */}
|
||||||
|
{maintext && !statusYaml && (
|
||||||
|
<div className="mt-4 p-4 glass rounded-lg border border-primary/20">
|
||||||
|
<div className="text-sm leading-relaxed break-words overflow-wrap-anywhere">
|
||||||
|
{maintext.split('\n').map((line, index) => (
|
||||||
|
<div key={index} className="min-h-[1.5em]">
|
||||||
|
{line ? <DialogueText text={line} /> : <br />}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export default function AdminPage() {
|
|||||||
function AIConfigSection() {
|
function AIConfigSection() {
|
||||||
const [configs, setConfigs] = useState<AIConfig[]>([])
|
const [configs, setConfigs] = useState<AIConfig[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [showAddModal, setShowAddModal] = useState(false)
|
const [_showAddModal, setShowAddModal] = useState(false)
|
||||||
const [editingConfig, setEditingConfig] = useState<AIConfig | null>(null)
|
const [editingConfig, setEditingConfig] = useState<AIConfig | null>(null)
|
||||||
const [testingConfig, setTestingConfig] = useState<number | null>(null)
|
const [testingConfig, setTestingConfig] = useState<number | null>(null)
|
||||||
const [testResult, setTestResult] = useState<{ success: boolean; message: string; latency: number } | null>(null)
|
const [testResult, setTestResult] = useState<{ success: boolean; message: string; latency: number } | null>(null)
|
||||||
@@ -286,7 +286,7 @@ function AIConfigSection() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(showAddModal || editingConfig) && (
|
{(_showAddModal || editingConfig) && (
|
||||||
<AIConfigModal
|
<AIConfigModal
|
||||||
config={editingConfig}
|
config={editingConfig}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
@@ -800,8 +800,8 @@ function CharactersManageSection() {
|
|||||||
function PresetsManageSection() {
|
function PresetsManageSection() {
|
||||||
const [presets, setPresets] = useState<any[]>([])
|
const [presets, setPresets] = useState<any[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [showAddModal, setShowAddModal] = useState(false)
|
const [_showAddModal, setShowAddModal] = useState(false)
|
||||||
const [editingPreset, setEditingPreset] = useState<any>(null)
|
const [_editingPreset, setEditingPreset] = useState<any>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadPresets()
|
loadPresets()
|
||||||
@@ -971,8 +971,8 @@ function UsersManageSection() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleToggleStatus = async (id: number, enable: boolean) => {
|
const handleToggleStatus = async (_id: number, _enable: boolean) => {
|
||||||
if (!confirm(`确定要${enable ? '禁用' : '启用'}这个用户吗?`)) return
|
if (!confirm(`确定要${_enable ? '禁用' : '启用'}这个用户吗?`)) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 需要后端提供更新用户状态的接口
|
// 需要后端提供更新用户状态的接口
|
||||||
@@ -983,8 +983,8 @@ function UsersManageSection() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleToggleAdmin = async (id: number, isAdmin: boolean) => {
|
const handleToggleAdmin = async (_id: number, _isAdmin: boolean) => {
|
||||||
if (!confirm(`确定要${isAdmin ? '移除' : '授予'}管理员权限吗?`)) return
|
if (!confirm(`确定要${_isAdmin ? '移除' : '授予'}管理员权限吗?`)) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 需要后端提供更新用户权限的接口
|
// 需要后端提供更新用户权限的接口
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Navbar from '../components/Navbar'
|
|||||||
import {Book, Code2, Download, Edit, FileJson, FileUp, 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 Character, characterApi} from '../api/character'
|
||||||
import {type RegexScript, regexScriptApi} from '../api/regex'
|
import {type RegexScript, regexScriptApi} from '../api/regex'
|
||||||
import {useAppStore} from '../store'
|
// import {useAppStore} from '../store'
|
||||||
|
|
||||||
interface WorldBookEntry {
|
interface WorldBookEntry {
|
||||||
keys: string[]
|
keys: string[]
|
||||||
@@ -36,7 +36,7 @@ export default function CharacterManagePage() {
|
|||||||
const [worldBookEntries, setWorldBookEntries] = useState<WorldBookEntry[]>([])
|
const [worldBookEntries, setWorldBookEntries] = useState<WorldBookEntry[]>([])
|
||||||
const [showRegexScriptEditor, setShowRegexScriptEditor] = useState(false)
|
const [showRegexScriptEditor, setShowRegexScriptEditor] = useState(false)
|
||||||
const [regexScripts, setRegexScripts] = useState<RegexScript[]>([])
|
const [regexScripts, setRegexScripts] = useState<RegexScript[]>([])
|
||||||
const [editingTab, setEditingTab] = useState<'basic' | 'worldbook' | 'regex'>('basic')
|
const [_editingTab, _setEditingTab] = useState<'basic' | 'worldbook' | 'regex'>('basic')
|
||||||
const [showAddRegexModal, setShowAddRegexModal] = useState(false)
|
const [showAddRegexModal, setShowAddRegexModal] = useState(false)
|
||||||
const [newRegexForm, setNewRegexForm] = useState({
|
const [newRegexForm, setNewRegexForm] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -82,7 +82,7 @@ export default function CharacterManagePage() {
|
|||||||
|
|
||||||
const loadRegexScripts = async (characterId: number) => {
|
const loadRegexScripts = async (characterId: number) => {
|
||||||
try {
|
try {
|
||||||
const response = await regexScriptApi.getRegexScriptList({
|
const response = await regexScriptApi.getList({
|
||||||
scope: 1,
|
scope: 1,
|
||||||
pageSize: 100,
|
pageSize: 100,
|
||||||
})
|
})
|
||||||
@@ -766,7 +766,7 @@ export default function CharacterManagePage() {
|
|||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await regexScriptApi.createRegexScript({
|
await regexScriptApi.create({
|
||||||
name: '新脚本',
|
name: '新脚本',
|
||||||
findRegex: '.*', // 默认匹配所有内容
|
findRegex: '.*', // 默认匹配所有内容
|
||||||
replaceWith: '',
|
replaceWith: '',
|
||||||
@@ -821,7 +821,7 @@ export default function CharacterManagePage() {
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (!confirm('确定要删除这个脚本吗?')) return
|
if (!confirm('确定要删除这个脚本吗?')) return
|
||||||
try {
|
try {
|
||||||
await regexScriptApi.deleteRegexScript(script.id)
|
await regexScriptApi.delete(script.id)
|
||||||
loadRegexScripts(selectedCharacter.id)
|
loadRegexScripts(selectedCharacter.id)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert(err.response?.data?.msg || '删除失败')
|
alert(err.response?.data?.msg || '删除失败')
|
||||||
@@ -927,7 +927,7 @@ export default function CharacterManagePage() {
|
|||||||
if (!testInput) return
|
if (!testInput) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await regexScriptApi.testRegexScript(script.id, testInput)
|
const response = await regexScriptApi.test(script.id, testInput)
|
||||||
console.log('测试响应:', response)
|
console.log('测试响应:', response)
|
||||||
|
|
||||||
// 处理响应数据结构
|
// 处理响应数据结构
|
||||||
@@ -949,7 +949,7 @@ export default function CharacterManagePage() {
|
|||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
await regexScriptApi.updateRegexScript(script.id, {
|
await regexScriptApi.update(script.id, {
|
||||||
name: script.name,
|
name: script.name,
|
||||||
findRegex: script.findRegex,
|
findRegex: script.findRegex,
|
||||||
replaceWith: script.replaceWith,
|
replaceWith: script.replaceWith,
|
||||||
@@ -1039,7 +1039,7 @@ export default function CharacterManagePage() {
|
|||||||
const scripts = Array.isArray(jsonData) ? jsonData : [jsonData]
|
const scripts = Array.isArray(jsonData) ? jsonData : [jsonData]
|
||||||
|
|
||||||
for (const scriptData of scripts) {
|
for (const scriptData of scripts) {
|
||||||
await regexScriptApi.createRegexScript({
|
await regexScriptApi.create({
|
||||||
name: scriptData.scriptName || scriptData.name || '导入的脚本',
|
name: scriptData.scriptName || scriptData.name || '导入的脚本',
|
||||||
findRegex: scriptData.findRegex || '.*',
|
findRegex: scriptData.findRegex || '.*',
|
||||||
replaceWith: scriptData.replaceString || scriptData.replaceWith || '',
|
replaceWith: scriptData.replaceString || scriptData.replaceWith || '',
|
||||||
@@ -1170,7 +1170,7 @@ export default function CharacterManagePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await regexScriptApi.createRegexScript({
|
await regexScriptApi.create({
|
||||||
name: newRegexForm.name,
|
name: newRegexForm.name,
|
||||||
findRegex: newRegexForm.findRegex,
|
findRegex: newRegexForm.findRegex,
|
||||||
replaceWith: newRegexForm.replaceWith,
|
replaceWith: newRegexForm.replaceWith,
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ export default function ChatPage() {
|
|||||||
|
|
||||||
// 使用 MVU store
|
// 使用 MVU store
|
||||||
const {
|
const {
|
||||||
currentConversation,
|
currentConversation: storeConversation,
|
||||||
currentCharacter,
|
|
||||||
loading,
|
loading,
|
||||||
sidebarOpen: showSidebar,
|
sidebarOpen: showSidebar,
|
||||||
setCurrentConversation,
|
setCurrentConversation,
|
||||||
@@ -29,6 +28,18 @@ export default function ChatPage() {
|
|||||||
setVariable,
|
setVariable,
|
||||||
} = useAppStore()
|
} = useAppStore()
|
||||||
|
|
||||||
|
// 使用本地状态来存储完整的 Conversation 和 Character 对象
|
||||||
|
const [currentConversation, setLocalConversation] = useState<Conversation | null>(null)
|
||||||
|
const [currentCharacter, setLocalCharacter] = useState<Character | null>(null)
|
||||||
|
|
||||||
|
// 同步 store 和本地状态
|
||||||
|
useEffect(() => {
|
||||||
|
if (storeConversation && currentConversation?.id !== storeConversation.id) {
|
||||||
|
// 当 store 中的对话改变时,重新加载完整数据
|
||||||
|
initializeChat()
|
||||||
|
}
|
||||||
|
}, [storeConversation?.id])
|
||||||
|
|
||||||
// 从URL参数获取角色ID或对话ID
|
// 从URL参数获取角色ID或对话ID
|
||||||
const characterId = searchParams.get('character')
|
const characterId = searchParams.get('character')
|
||||||
const conversationId = searchParams.get('conversation')
|
const conversationId = searchParams.get('conversation')
|
||||||
@@ -37,6 +48,23 @@ export default function ChatPage() {
|
|||||||
initializeChat()
|
initializeChat()
|
||||||
}, [characterId, conversationId])
|
}, [characterId, conversationId])
|
||||||
|
|
||||||
|
// 监听 iframe 消息(状态栏按钮点击事件)
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMessage = (event: MessageEvent) => {
|
||||||
|
if (event.data.type === 'playerAction') {
|
||||||
|
// 用户点击了状态栏中的按钮,自动发送消息
|
||||||
|
const action = event.data.data.action
|
||||||
|
console.log('[ChatPage] 收到状态栏操作:', action)
|
||||||
|
// 这里需要触发消息发送,但需要访问 ChatArea 的 sendMessage 方法
|
||||||
|
// 暂时通过自定义事件来实现
|
||||||
|
window.dispatchEvent(new CustomEvent('sendMessageFromStatusBar', { detail: action }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', handleMessage)
|
||||||
|
return () => window.removeEventListener('message', handleMessage)
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentConversation?.settings) {
|
if (currentConversation?.settings) {
|
||||||
try {
|
try {
|
||||||
@@ -72,10 +100,12 @@ export default function ChatPage() {
|
|||||||
// 如果有对话ID,加载现有对话
|
// 如果有对话ID,加载现有对话
|
||||||
if (conversationId) {
|
if (conversationId) {
|
||||||
const convResp = await conversationApi.getConversationById(Number(conversationId))
|
const convResp = await conversationApi.getConversationById(Number(conversationId))
|
||||||
|
setLocalConversation(convResp.data)
|
||||||
setCurrentConversation(convResp.data)
|
setCurrentConversation(convResp.data)
|
||||||
|
|
||||||
// 加载对话关联的角色
|
// 加载对话关联的角色
|
||||||
const charResp = await characterApi.getCharacterById(convResp.data.characterId)
|
const charResp = await characterApi.getCharacterById(convResp.data.characterId)
|
||||||
|
setLocalCharacter(charResp.data)
|
||||||
setCurrentCharacter(charResp.data)
|
setCurrentCharacter(charResp.data)
|
||||||
|
|
||||||
// 更新变量系统
|
// 更新变量系统
|
||||||
@@ -84,6 +114,7 @@ export default function ChatPage() {
|
|||||||
// 如果有角色ID,创建新对话
|
// 如果有角色ID,创建新对话
|
||||||
else if (characterId) {
|
else if (characterId) {
|
||||||
const charResp = await characterApi.getCharacterById(Number(characterId))
|
const charResp = await characterApi.getCharacterById(Number(characterId))
|
||||||
|
setLocalCharacter(charResp.data)
|
||||||
setCurrentCharacter(charResp.data)
|
setCurrentCharacter(charResp.data)
|
||||||
|
|
||||||
// 更新变量系统
|
// 更新变量系统
|
||||||
@@ -94,6 +125,7 @@ export default function ChatPage() {
|
|||||||
characterId: Number(characterId),
|
characterId: Number(characterId),
|
||||||
title: `与 ${charResp.data.name} 的对话`,
|
title: `与 ${charResp.data.name} 的对话`,
|
||||||
})
|
})
|
||||||
|
setLocalConversation(convResp.data)
|
||||||
setCurrentConversation(convResp.data)
|
setCurrentConversation(convResp.data)
|
||||||
|
|
||||||
// 更新URL为对话ID
|
// 更新URL为对话ID
|
||||||
@@ -108,10 +140,16 @@ export default function ChatPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleConversationChange = (conversation: Conversation) => {
|
const handleConversationChange = (conversation: Conversation) => {
|
||||||
|
setLocalConversation(conversation)
|
||||||
setCurrentConversation(conversation)
|
setCurrentConversation(conversation)
|
||||||
navigate(`/chat?conversation=${conversation.id}`)
|
navigate(`/chat?conversation=${conversation.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleConversationUpdate = (conversation: Conversation) => {
|
||||||
|
setLocalConversation(conversation)
|
||||||
|
setCurrentConversation(conversation)
|
||||||
|
}
|
||||||
|
|
||||||
const handleSettingsUpdate = (settings: any) => {
|
const handleSettingsUpdate = (settings: any) => {
|
||||||
if (settings.backgroundImage !== undefined) {
|
if (settings.backgroundImage !== undefined) {
|
||||||
setBackgroundImage(settings.backgroundImage)
|
setBackgroundImage(settings.backgroundImage)
|
||||||
@@ -188,7 +226,7 @@ export default function ChatPage() {
|
|||||||
<ChatArea
|
<ChatArea
|
||||||
conversation={currentConversation}
|
conversation={currentConversation}
|
||||||
character={currentCharacter}
|
character={currentCharacter}
|
||||||
onConversationUpdate={setCurrentConversation}
|
onConversationUpdate={handleConversationUpdate}
|
||||||
/>
|
/>
|
||||||
{showCharacterPanel && (
|
{showCharacterPanel && (
|
||||||
<CharacterPanel
|
<CharacterPanel
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { Plus, Search, Edit2, Trash2, Upload, Download, Play, X } from 'lucide-react'
|
import { Plus, Search, Edit2, Trash2, Upload, Download, X } from 'lucide-react'
|
||||||
import { regexAPI } from '../api/regex'
|
import { regexAPI } from '../api/regex'
|
||||||
|
|
||||||
interface RegexScript {
|
interface RegexScript {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import {
|
import {
|
||||||
getWorldbookList,
|
getWorldbookList,
|
||||||
createWorldbook,
|
createWorldbook,
|
||||||
updateWorldbook,
|
|
||||||
deleteWorldbook,
|
deleteWorldbook,
|
||||||
importWorldbook,
|
importWorldbook,
|
||||||
exportWorldbook,
|
exportWorldbook,
|
||||||
@@ -21,7 +20,7 @@ const WorldbookManagePage: React.FC = () => {
|
|||||||
const [selectedWorldbook, setSelectedWorldbook] = useState<Worldbook | null>(null);
|
const [selectedWorldbook, setSelectedWorldbook] = useState<Worldbook | null>(null);
|
||||||
const [entries, setEntries] = useState<WorldbookEntry[]>([]);
|
const [entries, setEntries] = useState<WorldbookEntry[]>([]);
|
||||||
const [selectedEntry, setSelectedEntry] = useState<WorldbookEntry | null>(null);
|
const [selectedEntry, setSelectedEntry] = useState<WorldbookEntry | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [_loading, setLoading] = useState(false);
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
const [showEntryModal, setShowEntryModal] = useState(false);
|
const [showEntryModal, setShowEntryModal] = useState(false);
|
||||||
const [searchKeyword, setSearchKeyword] = useState('');
|
const [searchKeyword, setSearchKeyword] = useState('');
|
||||||
|
|||||||
9
web-app/src/vite-env.d.ts
vendored
Normal file
9
web-app/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_API_BASE_URL?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
@@ -3,4 +3,20 @@ import react from '@vitejs/plugin-react'
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
// 禁用文件名哈希,强制浏览器重新加载
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
entryFileNames: `assets/[name].js`,
|
||||||
|
chunkFileNames: `assets/[name].js`,
|
||||||
|
assetFileNames: `assets/[name].[ext]`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
// 开发服务器禁用缓存
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-store',
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user