@@ -19,12 +19,13 @@ type Conversation struct {
|
||||
Title string `gorm:"type:varchar(200)" json:"title"` // 对话标题
|
||||
|
||||
// 对话配置
|
||||
PresetID *uint `gorm:"index" json:"presetId"` // 使用的预设ID
|
||||
WorldbookID *uint `gorm:"index" json:"worldbookId"` // 使用的世界书ID
|
||||
AIProvider string `gorm:"type:varchar(50)" json:"aiProvider"` // AI提供商 (openai/anthropic)
|
||||
Model string `gorm:"type:varchar(100)" json:"model"` // 使用的模型
|
||||
Settings datatypes.JSON `gorm:"type:jsonb" json:"settings"` // 对话设置 (temperature等)
|
||||
WorldbookEnabled bool `gorm:"default:false" json:"worldbookEnabled"` // 是否启用世界书
|
||||
PresetID *uint `gorm:"index" json:"presetId"` // 使用的预设ID
|
||||
WorldbookID *uint `gorm:"index" json:"worldbookId"` // 使用的世界书ID
|
||||
AIProvider string `gorm:"type:varchar(50)" json:"aiProvider"` // AI提供商 (openai/anthropic)
|
||||
Model string `gorm:"type:varchar(100)" json:"model"` // 使用的模型
|
||||
Settings datatypes.JSON `gorm:"type:jsonb" json:"settings"` // 对话设置 (temperature等)
|
||||
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"` // 消息数量
|
||||
|
||||
@@ -101,14 +101,10 @@ func (s *ConversationService) CreateConversation(userID uint, req *request.Creat
|
||||
userName = user.NickName
|
||||
}
|
||||
|
||||
// 应用输出阶段正则脚本处理开场白
|
||||
// 【重要】不再应用正则脚本处理开场白,保留原始内容
|
||||
// 让前端来处理 <Status_block> 和 <maintext> 的渲染
|
||||
processedFirstMes := character.FirstMes
|
||||
var regexService RegexScriptService
|
||||
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)))
|
||||
}
|
||||
global.GVA_LOG.Info(fmt.Sprintf("[开场白] 保留原始内容,长度=%d", len(processedFirstMes)))
|
||||
|
||||
firstMessage := app.Message{
|
||||
ConversationID: conversation.ID,
|
||||
@@ -418,16 +414,75 @@ func (s *ConversationService) SendMessage(userID, conversationID uint, req *requ
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 应用显示阶段的正则脚本 (Placement 3)
|
||||
displayContent := assistantMessage.Content
|
||||
displayScripts, err := regexService.GetScriptsForPlacement(userID, 3, &conversation.CharacterID, nil)
|
||||
if err == nil && len(displayScripts) > 0 {
|
||||
displayContent = regexService.ExecuteScripts(displayScripts, displayContent, userName, character.Name)
|
||||
global.GVA_LOG.Info(fmt.Sprintf("应用了 %d 个显示阶段正则脚本", len(displayScripts)))
|
||||
// 提取并保存变量 (从 AI 回复中提取 {{setvar::key::value}})
|
||||
newVars, cleanedContent := regexService.ExtractSetVars(assistantMessage.Content)
|
||||
if len(newVars) > 0 {
|
||||
// 加载现有变量
|
||||
var existingVars map[string]string
|
||||
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.Content = displayContent // 使用处理后的显示内容
|
||||
resp.Content = finalContent // 使用处理后的显示内容
|
||||
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))
|
||||
|
||||
// 应用输出阶段的正则脚本 (Placement 1)
|
||||
var regexService RegexScriptService
|
||||
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)
|
||||
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))
|
||||
}
|
||||
}
|
||||
// 【重要】不再应用 Placement 1 正则脚本,保留 AI 原始回复
|
||||
// 让 SendMessage 函数来提取和保护 <Status_block> 和 <maintext>
|
||||
// 前端会负责渲染这些标签
|
||||
global.GVA_LOG.Info("[AI回复] 保留原始内容,不应用输出阶段正则脚本")
|
||||
|
||||
return aiResponse, nil
|
||||
}
|
||||
|
||||
@@ -311,6 +311,78 @@ func (s *RegexScriptService) substituteMacros(text string, userName string, char
|
||||
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 获取指定阶段的脚本
|
||||
func (s *RegexScriptService) GetScriptsForPlacement(userID uint, placement int, charID *uint, presetID *uint) ([]app.RegexScript, error) {
|
||||
var scripts []app.RegexScript
|
||||
|
||||
@@ -63,7 +63,7 @@ export interface UpdateCharacterRequest {
|
||||
postHistoryInstructions?: string
|
||||
tags?: string[]
|
||||
alternateGreetings?: string[]
|
||||
characterBook?: Record<string, any>
|
||||
characterBook?: Record<string, any> | null
|
||||
extensions?: Record<string, any>
|
||||
isPublic?: boolean
|
||||
}
|
||||
|
||||
@@ -75,6 +75,24 @@ export default function ChatArea({ conversation, character, onConversationUpdate
|
||||
scrollToBottom()
|
||||
}, [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 () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
@@ -185,11 +203,10 @@ export default function ChatArea({ conversation, character, onConversationUpdate
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!inputValue.trim() || sending) return
|
||||
const handleSendMessage = async (message: string) => {
|
||||
if (!message.trim() || sending) return
|
||||
|
||||
const userMessage = inputValue.trim()
|
||||
setInputValue('')
|
||||
const userMessage = message.trim()
|
||||
setSending(true)
|
||||
|
||||
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>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey && !sending && inputValue.trim()) {
|
||||
e.preventDefault()
|
||||
@@ -644,7 +668,7 @@ export default function ChatArea({ conversation, character, onConversationUpdate
|
||||
>
|
||||
<MessageContent
|
||||
content={msg.content}
|
||||
role={msg.role}
|
||||
role={msg.role as 'user' | 'assistant'}
|
||||
onChoiceSelect={(choice) => {
|
||||
setInputValue(choice)
|
||||
textareaRef.current?.focus()
|
||||
|
||||
@@ -127,21 +127,35 @@ function parseChoices(content: string): { choices: Choice[]; cleanContent: strin
|
||||
return { choices, cleanContent }
|
||||
}
|
||||
|
||||
// 清理脚本输出内容
|
||||
function cleanScriptOutput(content: string): string {
|
||||
// 移除 <UpdateVariable>...</UpdateVariable> 块
|
||||
let cleaned = content.replace(/<UpdateVariable>[\s\S]*?<\/UpdateVariable>/gi, '')
|
||||
// 提取 <maintext> 内容
|
||||
function extractMaintext(content: string): { maintext: string; cleanContent: string } {
|
||||
const maintextRegex = /<maintext>([\s\S]*?)<\/maintext>/i
|
||||
const match = content.match(maintextRegex)
|
||||
|
||||
// 移除 <Analysis>...</Analysis> 块
|
||||
cleaned = cleaned.replace(/<Analysis>[\s\S]*?<\/Analysis>/gi, '')
|
||||
if (!match) {
|
||||
return { maintext: '', cleanContent: content }
|
||||
}
|
||||
|
||||
// 移除 _.set() 调用
|
||||
cleaned = cleaned.replace(/^\s*_.set\([^)]+\);\s*$/gm, '')
|
||||
|
||||
return cleaned.trim()
|
||||
const maintext = match[1].trim()
|
||||
const cleanContent = content.replace(maintextRegex, '').trim()
|
||||
return { maintext, cleanContent }
|
||||
}
|
||||
|
||||
// 解析状态面板数据
|
||||
// 提取 <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 } {
|
||||
const statusRegex = /<status_current_variable>([\s\S]*?)<\/status_current_variable>/i
|
||||
const match = content.match(statusRegex)
|
||||
@@ -167,28 +181,49 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
|
||||
const [allowScript, setAllowScript] = useState(false)
|
||||
const [choices, setChoices] = useState<Choice[]>([])
|
||||
const [displayContent, setDisplayContent] = useState(content)
|
||||
const [remainingText, setRemainingText] = useState('')
|
||||
const [statusPanel, setStatusPanel] = useState<any>(null)
|
||||
const [statusYaml, setStatusYaml] = useState<string>('')
|
||||
const [maintext, setMaintext] = useState<string>('')
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||
const statusIframeRef = useRef<HTMLIFrameElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
console.log('[MessageContent] 原始内容:', content)
|
||||
|
||||
let processedContent = content
|
||||
|
||||
// 解析状态面板
|
||||
const { status, cleanContent: contentAfterStatus } = parseStatusPanel(processedContent)
|
||||
console.log('[MessageContent] 状态面板:', status)
|
||||
setStatusPanel(status)
|
||||
// 提取 <maintext> 内容
|
||||
const { maintext: extractedMaintext, cleanContent: contentAfterMaintext } = extractMaintext(processedContent)
|
||||
if (extractedMaintext) {
|
||||
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)
|
||||
setChoices(parsedChoices)
|
||||
console.log('[MessageContent] 选择项:', parsedChoices)
|
||||
|
||||
// 清理脚本输出
|
||||
const finalContent = cleanScriptOutput(cleanContent)
|
||||
console.log('[MessageContent] 清理后内容:', finalContent)
|
||||
// 直接使用清理后的内容,不再进行脚本输出清理
|
||||
const finalContent = cleanContent
|
||||
console.log('[MessageContent] 最终内容:', finalContent)
|
||||
|
||||
// 检测是否包含 HTML 标签或代码块
|
||||
const htmlRegex = /<[^>]+>/g
|
||||
@@ -277,7 +312,6 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
|
||||
}
|
||||
|
||||
setDisplayContent(renderedContent)
|
||||
setRemainingText('')
|
||||
|
||||
setHasHtml(hasHtmlTags || hasCodeBlocks)
|
||||
setHasScript(hasScriptContent)
|
||||
@@ -336,6 +370,30 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
|
||||
max-width: 100% !important;
|
||||
}
|
||||
</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>
|
||||
<body>
|
||||
${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(() => {
|
||||
if (allowScript && hasScript && iframeRef.current) {
|
||||
// 延迟渲染确保 iframe 已挂载
|
||||
@@ -363,6 +654,15 @@ export default function MessageContent({ content, role, onChoiceSelect }: Messag
|
||||
}
|
||||
}, [allowScript, hasScript, displayContent])
|
||||
|
||||
useEffect(() => {
|
||||
if (statusYaml && statusIframeRef.current) {
|
||||
// 延迟渲染确保 iframe 已挂载
|
||||
setTimeout(() => {
|
||||
renderStatusBar()
|
||||
}, 50)
|
||||
}
|
||||
}, [statusYaml])
|
||||
|
||||
// 如果是用户消息,直接显示纯文本
|
||||
if (role === 'user') {
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* 状态面板 */}
|
||||
{/* 状态面板(JSON 格式 - 保留兼容性) */}
|
||||
{statusPanel && (
|
||||
<div className="mt-4 p-4 glass rounded-lg border border-secondary/20">
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* 状态栏(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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ export default function AdminPage() {
|
||||
function AIConfigSection() {
|
||||
const [configs, setConfigs] = useState<AIConfig[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showAddModal, setShowAddModal] = useState(false)
|
||||
const [_showAddModal, setShowAddModal] = useState(false)
|
||||
const [editingConfig, setEditingConfig] = useState<AIConfig | null>(null)
|
||||
const [testingConfig, setTestingConfig] = useState<number | null>(null)
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; message: string; latency: number } | null>(null)
|
||||
@@ -286,7 +286,7 @@ function AIConfigSection() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(showAddModal || editingConfig) && (
|
||||
{(_showAddModal || editingConfig) && (
|
||||
<AIConfigModal
|
||||
config={editingConfig}
|
||||
onClose={() => {
|
||||
@@ -800,8 +800,8 @@ function CharactersManageSection() {
|
||||
function PresetsManageSection() {
|
||||
const [presets, setPresets] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showAddModal, setShowAddModal] = useState(false)
|
||||
const [editingPreset, setEditingPreset] = useState<any>(null)
|
||||
const [_showAddModal, setShowAddModal] = useState(false)
|
||||
const [_editingPreset, setEditingPreset] = useState<any>(null)
|
||||
|
||||
useEffect(() => {
|
||||
loadPresets()
|
||||
@@ -971,8 +971,8 @@ function UsersManageSection() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleStatus = async (id: number, enable: boolean) => {
|
||||
if (!confirm(`确定要${enable ? '禁用' : '启用'}这个用户吗?`)) return
|
||||
const handleToggleStatus = async (_id: number, _enable: boolean) => {
|
||||
if (!confirm(`确定要${_enable ? '禁用' : '启用'}这个用户吗?`)) return
|
||||
|
||||
try {
|
||||
// 需要后端提供更新用户状态的接口
|
||||
@@ -983,8 +983,8 @@ function UsersManageSection() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleAdmin = async (id: number, isAdmin: boolean) => {
|
||||
if (!confirm(`确定要${isAdmin ? '移除' : '授予'}管理员权限吗?`)) return
|
||||
const handleToggleAdmin = async (_id: number, _isAdmin: boolean) => {
|
||||
if (!confirm(`确定要${_isAdmin ? '移除' : '授予'}管理员权限吗?`)) return
|
||||
|
||||
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 {type Character, characterApi} from '../api/character'
|
||||
import {type RegexScript, regexScriptApi} from '../api/regex'
|
||||
import {useAppStore} from '../store'
|
||||
// import {useAppStore} from '../store'
|
||||
|
||||
interface WorldBookEntry {
|
||||
keys: string[]
|
||||
@@ -36,7 +36,7 @@ export default function CharacterManagePage() {
|
||||
const [worldBookEntries, setWorldBookEntries] = useState<WorldBookEntry[]>([])
|
||||
const [showRegexScriptEditor, setShowRegexScriptEditor] = useState(false)
|
||||
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 [newRegexForm, setNewRegexForm] = useState({
|
||||
name: '',
|
||||
@@ -82,7 +82,7 @@ export default function CharacterManagePage() {
|
||||
|
||||
const loadRegexScripts = async (characterId: number) => {
|
||||
try {
|
||||
const response = await regexScriptApi.getRegexScriptList({
|
||||
const response = await regexScriptApi.getList({
|
||||
scope: 1,
|
||||
pageSize: 100,
|
||||
})
|
||||
@@ -766,7 +766,7 @@ export default function CharacterManagePage() {
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await regexScriptApi.createRegexScript({
|
||||
await regexScriptApi.create({
|
||||
name: '新脚本',
|
||||
findRegex: '.*', // 默认匹配所有内容
|
||||
replaceWith: '',
|
||||
@@ -821,7 +821,7 @@ export default function CharacterManagePage() {
|
||||
onClick={async () => {
|
||||
if (!confirm('确定要删除这个脚本吗?')) return
|
||||
try {
|
||||
await regexScriptApi.deleteRegexScript(script.id)
|
||||
await regexScriptApi.delete(script.id)
|
||||
loadRegexScripts(selectedCharacter.id)
|
||||
} catch (err: any) {
|
||||
alert(err.response?.data?.msg || '删除失败')
|
||||
@@ -927,7 +927,7 @@ export default function CharacterManagePage() {
|
||||
if (!testInput) return
|
||||
|
||||
try {
|
||||
const response = await regexScriptApi.testRegexScript(script.id, testInput)
|
||||
const response = await regexScriptApi.test(script.id, testInput)
|
||||
console.log('测试响应:', response)
|
||||
|
||||
// 处理响应数据结构
|
||||
@@ -949,7 +949,7 @@ export default function CharacterManagePage() {
|
||||
<button
|
||||
onClick={async () => {
|
||||
try {
|
||||
await regexScriptApi.updateRegexScript(script.id, {
|
||||
await regexScriptApi.update(script.id, {
|
||||
name: script.name,
|
||||
findRegex: script.findRegex,
|
||||
replaceWith: script.replaceWith,
|
||||
@@ -1039,7 +1039,7 @@ export default function CharacterManagePage() {
|
||||
const scripts = Array.isArray(jsonData) ? jsonData : [jsonData]
|
||||
|
||||
for (const scriptData of scripts) {
|
||||
await regexScriptApi.createRegexScript({
|
||||
await regexScriptApi.create({
|
||||
name: scriptData.scriptName || scriptData.name || '导入的脚本',
|
||||
findRegex: scriptData.findRegex || '.*',
|
||||
replaceWith: scriptData.replaceString || scriptData.replaceWith || '',
|
||||
@@ -1170,7 +1170,7 @@ export default function CharacterManagePage() {
|
||||
}
|
||||
|
||||
try {
|
||||
await regexScriptApi.createRegexScript({
|
||||
await regexScriptApi.create({
|
||||
name: newRegexForm.name,
|
||||
findRegex: newRegexForm.findRegex,
|
||||
replaceWith: newRegexForm.replaceWith,
|
||||
|
||||
@@ -18,8 +18,7 @@ export default function ChatPage() {
|
||||
|
||||
// 使用 MVU store
|
||||
const {
|
||||
currentConversation,
|
||||
currentCharacter,
|
||||
currentConversation: storeConversation,
|
||||
loading,
|
||||
sidebarOpen: showSidebar,
|
||||
setCurrentConversation,
|
||||
@@ -29,6 +28,18 @@ export default function ChatPage() {
|
||||
setVariable,
|
||||
} = 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
|
||||
const characterId = searchParams.get('character')
|
||||
const conversationId = searchParams.get('conversation')
|
||||
@@ -37,6 +48,23 @@ export default function ChatPage() {
|
||||
initializeChat()
|
||||
}, [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(() => {
|
||||
if (currentConversation?.settings) {
|
||||
try {
|
||||
@@ -72,10 +100,12 @@ export default function ChatPage() {
|
||||
// 如果有对话ID,加载现有对话
|
||||
if (conversationId) {
|
||||
const convResp = await conversationApi.getConversationById(Number(conversationId))
|
||||
setLocalConversation(convResp.data)
|
||||
setCurrentConversation(convResp.data)
|
||||
|
||||
// 加载对话关联的角色
|
||||
const charResp = await characterApi.getCharacterById(convResp.data.characterId)
|
||||
setLocalCharacter(charResp.data)
|
||||
setCurrentCharacter(charResp.data)
|
||||
|
||||
// 更新变量系统
|
||||
@@ -84,6 +114,7 @@ export default function ChatPage() {
|
||||
// 如果有角色ID,创建新对话
|
||||
else if (characterId) {
|
||||
const charResp = await characterApi.getCharacterById(Number(characterId))
|
||||
setLocalCharacter(charResp.data)
|
||||
setCurrentCharacter(charResp.data)
|
||||
|
||||
// 更新变量系统
|
||||
@@ -94,6 +125,7 @@ export default function ChatPage() {
|
||||
characterId: Number(characterId),
|
||||
title: `与 ${charResp.data.name} 的对话`,
|
||||
})
|
||||
setLocalConversation(convResp.data)
|
||||
setCurrentConversation(convResp.data)
|
||||
|
||||
// 更新URL为对话ID
|
||||
@@ -108,10 +140,16 @@ export default function ChatPage() {
|
||||
}
|
||||
|
||||
const handleConversationChange = (conversation: Conversation) => {
|
||||
setLocalConversation(conversation)
|
||||
setCurrentConversation(conversation)
|
||||
navigate(`/chat?conversation=${conversation.id}`)
|
||||
}
|
||||
|
||||
const handleConversationUpdate = (conversation: Conversation) => {
|
||||
setLocalConversation(conversation)
|
||||
setCurrentConversation(conversation)
|
||||
}
|
||||
|
||||
const handleSettingsUpdate = (settings: any) => {
|
||||
if (settings.backgroundImage !== undefined) {
|
||||
setBackgroundImage(settings.backgroundImage)
|
||||
@@ -188,7 +226,7 @@ export default function ChatPage() {
|
||||
<ChatArea
|
||||
conversation={currentConversation}
|
||||
character={currentCharacter}
|
||||
onConversationUpdate={setCurrentConversation}
|
||||
onConversationUpdate={handleConversationUpdate}
|
||||
/>
|
||||
{showCharacterPanel && (
|
||||
<CharacterPanel
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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'
|
||||
|
||||
interface RegexScript {
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
getWorldbookList,
|
||||
createWorldbook,
|
||||
updateWorldbook,
|
||||
deleteWorldbook,
|
||||
importWorldbook,
|
||||
exportWorldbook,
|
||||
@@ -21,7 +20,7 @@ const WorldbookManagePage: React.FC = () => {
|
||||
const [selectedWorldbook, setSelectedWorldbook] = useState<Worldbook | null>(null);
|
||||
const [entries, setEntries] = useState<WorldbookEntry[]>([]);
|
||||
const [selectedEntry, setSelectedEntry] = useState<WorldbookEntry | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [_loading, setLoading] = useState(false);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showEntryModal, setShowEntryModal] = useState(false);
|
||||
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({
|
||||
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