// ==UserScript== // @name 에리의 크랙 로어 인젝터 (Universal) // @namespace 에리의 크랙 로어 인젝터 // @version 1.4.0-universal.1 // @description 단일 번들로 로드 안정성을 높인 범용 로어 인젝터 // @author 로컬AI // @match https://crack.wrtn.ai/stories/*/episodes/* // @match https://crack.wrtn.ai/characters/*/chats/* // @match https://crack.wrtn.ai/u/*/c/* // @require https://cdn.jsdelivr.net/npm/dexie@4.2.1/dist/dexie.min.js // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant unsafeWindow // @sandbox raw // @connect generativelanguage.googleapis.com // @connect googleapis.com // @connect oauth2.googleapis.com // @connect firebasevertexai.googleapis.com // @connect www.gstatic.com // @connect contents-api.wrtn.ai // @connect crack-api.wrtn.ai // @connect * // @run-at document-start // ==/UserScript== (function(){"use strict";const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;_w.__LoreInj=_w.__LoreInj||{};const L=_w.__LoreInj;L.chatBootstrapVersion="1.4.0-universal.1";L.universalBundle=true;if(!L.__menuQueue)L.__menuQueue=[];if(!L.__subMenuQueue)L.__subMenuQueue=[];L.registerMenu=L.registerMenu||function(key,cb){L.__menuQueue.push({key:key,cb:cb})};L.registerSubMenu=L.registerSubMenu||function(key,cb){L.__subMenuQueue.push({key:key,cb:cb})};if(!_w.__LoreInjReady){let resolveReady;const p=new Promise(r=>{resolveReady=r});p.__resolve=resolveReady;_w.__LoreInjReady=p}})();class ToastifyInjector{static findInjector(){if(document.__toastifyInjector){return document.__toastifyInjector}document.__toastifyInjector=new ToastifyInjector;return document.__toastifyInjector}constructor(){this.#init()}#trackNotification(){const current=(new Date).getTime();const toastifies=document.getElementsByClassName("Toastify");if(toastifies.length<=0){return}const rootNode=toastifies[0];if(rootNode.childNodes.length>0){if(rootNode.getElementsByClassName("chasm-toastify-track").length!=rootNode.childNodes.length){for(const element of Array.from(rootNode.getElementsByClassName("chasm-toastify-track"))){if(element.hasAttribute("completed")){element.removeAttribute("completed");element.removeAt=current+1e3}}}}for(const element of rootNode.getElementsByClassName("chasm-toastify-track")){if(element.expireAt0){for(const element of Array.from(toastifies[0].childNodes)){element.remove()}}const rootNode=toastifies[0];rootNode.append(wrapperNode);setTimeout(()=>{wrapperNode.setAttribute("completed","true")})}}class ImageMappable{imageMap=new Map;constructor(imageContainer){if(imageContainer){for(let[key,value]of Object.entries(imageContainer)){this.imageMap.set(key,value)}}}light(){return this.imageMap.get("light")}dark(){return this.imageMap.get("dark")}image(key){return this.imageMap.get(key)}}class Visibility{originName;constructor(originName){this.originName=originName}static PUBLIC=new Visibility("public");static PRIVATE=new Visibility("private");static LINK_ONLY=new Visibility("linkonly");static of(name){if(name.toLowerCase()==="private"){return this.PRIVATE}if(name.toLowerCase()==="public"){return this.PUBLIC}return this.LINK_ONLY}}class CrackUser{id;userId;wrtnUid;nickname;introdution;profileImage;createdAt;updatedAt;follower;following;constructor(id,userId,wrtnUid,nickname,introdution,profileImage,createdAt,updatedAt,follower,following){this.id=id;this.userId=userId;this.wrtnUid=wrtnUid;this.nickname=nickname;this.introdution=introdution;this.profileImage=profileImage;this.createdAt=createdAt;this.updatedAt=updatedAt;this.follower=follower;this.following=following}}class CrackPromptTemplate extends ImageMappable{name;id;constructor(name,id,iconContainer){super(iconContainer);this.name=name;this.id=id}static of(container){return new CrackPromptTemplate(container.name,container.template,container.icon)}}class CrackGenre{id;name;type;constructor(id,name,type){this.id=id;this.name=name;this.type=type}static of(container){return new CrackGenre(container._id,container.name,container.type)}}class CrackStoryDescription{base;simple;detail;constructor(description,simple,detail){this.base=description;this.simple=simple;this.detail=detail}static of(container){return new CrackStoryDescription(container.description,container.simpleDescription,container.detailDescription)}}class CrackMappedImage extends ImageMappable{constructor(imageContainer){super(imageContainer)}origin(){return super.image("origin")??"about:blank"}w200(){return super.image("w200")}w600(){return super.image("w600")}gif(){return super.image("gif")}tryOrigin(){return this.gif()??this.origin()}}class CrackModelInfo extends ImageMappable{name;model;constructor(name,model,iconContainer){super(iconContainer);this.name=name;this.model=model}}class CrackStoryStatus{genre;categories;target;chatType;imageCount;visibility;status;profileImage;createdAt;updatedAt;isAdult;isForceConverted;constructor(genre,imageCount,visibility,status,categories,target,chatType,profileImage,createdAt,updatedAt,isAdult,isForceConverted){this.genre=genre;this.categories=categories;this.imageCount=imageCount;this.visibility=visibility;this.status=status;this.target=target;this.chatType=chatType;this.profileImage=profileImage;this.createdAt=createdAt;this.updatedAt=updatedAt;this.isAdult=isAdult;this.isForceConverted=isForceConverted}static of(container){return new CrackStoryStatus(CrackGenre.of(container.genre),container.imageCount,Visibility.of(container.visibility),container.status,container.categories,container.target?.name,container.chatType?.name,new CrackMappedImage(container.profileImage),new Date(container.createdAt),new Date(container.updatedAt),container.isAdult,container.isConvertedToAdult)}}class CrackStoryStartingSet{id;baseSetId;name;initialMessages;replySuggestion;playGuide;constructor(id,baseSetId,name,initialMessages,replySuggestion,playGuide){this.id=id;this.baseSetId=baseSetId;this.name=name;this.initialMessages=initialMessages;this.replySuggestion=replySuggestion;this.playGuide=playGuide}static of(container){return new CrackStoryStartingSet(container._id,container.baseSetId,container.name,container.initialMessages,container.replySuggestions,container.playGuide)}}class CrackComment{id;content;writer;constructor(id,content,writer){this.id=id;this.content=content;this.writer=writer}static of(container){return new CrackComment(container._id,container.content,UserInfo.of(container.writer))}}class CrackStoryInfo{id;initialMessages;crackerModel;model;creator;name;description;statistics;status;template;representiveComment;startingSets;shareUrl;constructor(id,initialMessages,crackerModel,model,creator,name,description,statistics,status,template,representiveComment,startingSets,shareUrl){this.id=id;this.initialMessages=initialMessages;this.crackerModel=crackerModel;this.model=model;this.creator=creator;this.name=name;this.description=description;this.statistics=statistics;this.status=status;this.template=template;this.representiveComment=representiveComment;this.startingSets=startingSets;this.shareUrl=shareUrl}static of(container){return new CrackStoryInfo(container._id,container.initialMessages,container.defaultCrackerModel,container.chatModelId,UserInfo.of(container.creator),container.name,CrackStoryDescription.of(container),CrackArticleStatistics.of(container),CrackStoryStatus.of(container),CrackPromptTemplate.of(container.promptTemplate),CrackComment.of(container.representativeComment),Array.from(container.startingSets).map(item=>CrackStoryStartingSet.of(item)),container.shareUrl)}}class CrackArticleStatistics{messageSent;roomCreated;players;likes;comments;constructor(messageSent,roomCreated,players,likes,comments){this.messageSent=messageSent;this.roomCreated=roomCreated;this.players=players;this.likes=likes;this.comments=comments}static of(container){return new CrackArticleStatistics(container.totalMessageCount,container.chatUserCount,container.chatUserCount,container.likeCount,container.commentCount)}}class CrackChattingLog{id;userId;messageId;role;content;model;turnId;status;recommendList;crackerModel;chatModelId;isContinuallyGeneratable;isContinued;situationImages;parameterSnapshots;isPrologue;reroll;constructor(id,userId,messageId,role,content,model,turnId,status,recommendList,crackerModel,chatModelId,isContinuallyGeneratable,isContinued,situationImages,parameterSnapshots,isPrologue,reroll){this.id=id;this.userId=userId;this.messageId=messageId;this.role=role;this.content=content;this.model=model;this.turnId=turnId;this.status=status;this.recommendList=recommendList;this.crackerModel=crackerModel;this.chatModelId=chatModelId;this.isContinuallyGeneratable=isContinuallyGeneratable;this.isContinued=isContinued;this.situationImages=situationImages;this.parameterSnapshots=parameterSnapshots;this.isPrologue=isPrologue;this.reroll=reroll}isBot(){return this.role==="assistant"}isUser(){return this.role==="user"}simplify(){return new CrackSimplifiedChattingLog(this.role,this.content,this.situationImages,this.parameterSnapshots)}static of(container){return new CrackChattingLog(container._id,container.userId,container.chatId,container.role,container.content,container.model,container.turnId,container.status,container.dynamicChipList,container.crackerModel,container.chatModelId,container.isContinuallyGeneratable,container.isContinued,container.situationImages,container.parameterSnapshots,container.isPrologue,container.reroll??false)}}class CrackSimplifiedChattingLog{role;content;situationImages;parameterSnapshots;constructor(role,content,situationImages,parameterSnapshots){this.role=role;this.content=content;this.situationImages=situationImages;this.parameterSnapshots=parameterSnapshots}}class CrackStorySessionInfo extends ImageMappable{id;snapshotId;name;startingSetId;baseSetId;isAdult;constructor(id,snapshotId,name,images,startingSetId,baseSetId,isAdult){super(images);this.id=id;this.snapshotId=snapshotId;this.name=name;this.startingSetId=startingSetId;this.baseSetId=baseSetId;this.isAdult=isAdult}static of(container){return new CrackStorySessionInfo(container._id,container.snapshotId,container.name,container.profileImage,container.statringSetId,container.baseSetId,container.isAdult)}}class CrackCharacterSessionInfo{static of(container){return new CrackCharacterSessionInfo}}class CrackChatRoom{id;userId;title;lastMessage;model;modelId;hasUserNote;createdAt;updatedAt;isSummaryUpdated;doRecommendNextMessage;chatProfileId;story;character;constructor(id,userId,title,lastMessage,model,modelId,hasUserNote,createdAt,updatedAt,isSummaryUpdated,doRecommendNextMessage,chatProfileId,story,character){this.id=id;this.userId=userId;this.title=title;this.lastMessage=lastMessage;this.model=model;this.modelId=modelId;this.hasUserNote=hasUserNote;this.createdAt=createdAt;this.updatedAt=updatedAt;this.isSummaryUpdated=isSummaryUpdated;this.doRecommendNextMessage=doRecommendNextMessage;this.chatProfileId=chatProfileId;this.story=story;this.character=character}static of(container){return new CrackChatRoom(container._id,container.userId,container.title,container.lastMessage,container.model,container.chatModelId,container.hasUserNote,new Date(container.createdAt),new Date(container.updatedAt),container.isSummaryUpdated,container.isAutoRecommendUserNextMessage,container.chatProfile?._id,container.story?CrackStorySessionInfo.of(container.story):null,container.character?CrackCharacterSessionInfo.of(container.character):null)}}class CrackPersona{id;name;isRepresentative;profileId;created;updated;constructor(id,name,isRepresentative,profileId,created,updated){this.id=id;this.name=name;this.isRepresentative=isRepresentative;this.profileId=profileId;this.created=created;this.updated=updated}}class _CrackGenericUtil{isValid(item){if(item===undefined||item===null){return false}return true}}class CrackerModel{id;name;quantity;serviceType;constructor(id,name,quantity,serviceType){this.id=id;this.name=name;this.quantity=quantity;this.serviceType=serviceType}}class UserInfo extends CrackMappedImage{id;nickname;wrtnId;profileId;isCertificated;constructor(id,nickname,wrtnId,isCertificated,profileId,container){super(container);this.id=id;this.nickname=nickname;this.wrtnId=wrtnId;this.isCertificated=isCertificated;this.profileId=profileId}static of(container){return new UserInfo(container.userId,container.nickname,container.wrtnId,container.isCertifiedCreator,container.profileId,container.profileImage)}}class _CrackThemeApi{isDarkTheme(){return document.body.getAttribute("data-theme")==="dark"}isLightTheme(){return!this.isDarkTheme()}}class _CrackPathApi{isDashboardPath(){return"/"===location.pathname}isStoryPath(){return/\/stories\/[a-f0-9]+\/episodes\/[a-f0-9]+/.test(location.pathname)||/\/u\/[a-f0-9]+\/c\/[a-f0-9]+/.test(location.pathname)}isCharacterPath(){return/\/characters\/[a-f0-9]+\/chats\/[a-f0-9]+/.test(location.pathname)}isChattingPath(){return this.isStoryPath()||this.isCharacterPath()}character(){if(this.isChattingPath()){const split=window.location.pathname.substring(1).split("/");const characterId=split[1];return characterId}return undefined}chatRoom(){if(this.isChattingPath()){const split=window.location.pathname.substring(1).split("/");const characterId=split[1];const chatRoomId=split[3];return chatRoomId}return undefined}}class _CrackCookieApi{getCookie(key){const e=document.cookie.match(new RegExp(`(?:^|; )${key.replace(/([.$?*|{}()[\]\\/+^])/g,"\\$1")}=([^;]*)`));return e?decodeURIComponent(e[1]):undefined}getAuthToken(){const token=this.getCookie("access_token");if(!token){throw new Error("쿠키에 인증 토큰이 없습니다.")}return token}}class _CrackNetworkApi{#cookie;constructor(cookie){this.#cookie=cookie}async authFetch(method,url,body=undefined){try{const param={method:method,headers:{Authorization:`Bearer ${this.#cookie.getAuthToken()}`,"Content-Type":"application/json"}};if(body){param.body=JSON.stringify(body)}const result=await fetch(url,param);if(!result.ok){const errorItem=new Error(`HTTP 요청 실패 (${result.status}) [${await result.json()}]`);Object.assign(errorItem,{code:result.status});return errorItem}return await result.json()}catch(t){return new Error(`알 수 없는 오류 (${t.message??JSON.stringify(t)})`)}}}class _CrackAttendApi{#network;constructor(network){this.#network=network}async isAttendable(){const webResult=await this.#network.authFetch("GET","https://crack-api.wrtn.ai/crack-cash/attendance");if(webResult instanceof Error)return webResult;if(webResult.data&&webResult.data.attendanceStatus&&webResult.data.attendanceStatus==="NOT_ATTENDED"){return true}return false}async performAttend(){const result=await this.#network.authFetch("POST","https://crack-api.wrtn.ai/crack-cash/attendance");if(result instanceof Error){return false}return true}isAttendableTime(){const time=(new Date).getHours();if(time<6)return false;return true}}class _CrackCrackerApi{#network;constructor(network){this.#network=network}async current(){let result=await this.#network.authFetch("GET","https://crack-api.wrtn.ai/crack-cash/crackers");if(result instanceof Error){return result}return result.data.quantity??0}async crackerModelList(){const result=await this.#network.authFetch("GET","https://crack-api.wrtn.ai/crack-gen/v3/chat-models");if(result instanceof Error){return result}if(!result.data.models){return[]}const array=[];for(const model of result.data.models){array.push(new CrackerModel(model._id,model.name,model.crackerQuantity,model.serviceType))}return array}async crackerModelMap(){const array=await this.crackerModelList();if(array instanceof Error){return array}const map=new Map;for(let model of array){map.set(model.name,model)}return map}async crackerModel(name){const result=await this.crackerModelMap();if(result instanceof Error){return result}return result.get(name)??null}}class _CrackCharacterApi{#network;constructor(network){this.#network=network}}class _CrackUserApi{#network;constructor(network){this.#network=network}async currentUser(){const result=await this.#network.authFetch("GET","https://crack-api.wrtn.ai/crack-api/profiles");if(result instanceof Error){return result}const data=result.data;if(!data){return new Error("크랙에서 잘못된 API 응답을 반환하였습니다; API 스키마가 변경되었을 가능성이 존재합니다.")}return new CrackUser(data._id,data.userId,data.wrtnUid,data.nickname,data.introdution,data.profileImage?.origin,new Date(data.createdAt),new Date(data.updatedAt),data.follower,data.following)}async currentId(){const result=await this.currentUser();if(result instanceof Error){return result}return result.id}async personaList(crackId){const result=await this.#network.authFetch("GET",`https://crack-api.wrtn.ai/crack-api/profiles/${crackId}/chat-profiles`);if(result instanceof Error||!result.data?.chatProfiles){return result}const array=[];for(let item of result.data.chatProfiles){array.push(new CrackPersona(item._id,item.name,item.isRepresentative,item.profileId,new Date(item.createdAt),new Date(item.updatedAt)))}return array}async personaMap(crackId){const result=await this.personaList(crackId);if(result instanceof Error){return result}const map=new Map;for(let persona of result){map.set(persona.id,persona)}return map}async currentPersona(){const currentId=await this.currentId();if(currentId instanceof Error){return currentId}return await this.personaList(currentId)}async currentPersonaMap(){const currentId=await this.currentId();if(currentId instanceof Error){return currentId}return await this.personaMap(currentId)}}class _CrackStoryApi{#generic;#user;#network;constructor(generic,user,network){this.#generic=generic;this.#user=user;this.#network=network}async createRoom(storyId,baseSetId,profileId,crackerModel="normalchat",autoRecommend=false){const result=await this.#network.authFetch("POST","https://crack-api.wrtn.ai/crack-gen/v3/chats",{storyId:storyId,chatProfileId:profileId,baseSetId:baseSetId,crackerModel:crackerModel,isAutoRecommendUserNextMessage:autoRecommend});if(result instanceof Error){return result}const data=result.data;if(!data){return new Error("크랙 API에서 예상치 못한 스키마가 반환되었습니다. 이는 일시적 오류일 수 있지만, 크랙 API 변경이 원인일 수 있습니다.")}return CrackChatRoom.of(data)}async of(storyId){const result=await this.#network.authFetch("GET",`https://crack-api.wrtn.ai/crack-api/stories/${storyId}`);if(result instanceof Error){return result}if(!result.data){return new Error("크랙 API에서 데이터 컨테이너를 반환하지 않았습니다.")}return CrackStoryInfo.of(result.data)}}class _CrackChatRoomApi{#generic;#user;#network;#cookie;#io;constructor(generic,user,network,cookie,io){this.#generic=generic;this.#user=user;this.#network=network;this.#cookie=cookie;this.#io=io}async roomData(chatId){const result=await this.#network.authFetch("GET",`https://crack-api.wrtn.ai/crack-gen/v3/chats/${chatId}`);if(result instanceof Error){return result}const data=result.data;if(!data){return new Error("크랙 API에서 예상치 못한 스키마가 반환되었습니다. 이는 일시적 오류일 수 있지만, 크랙 API 변경이 원인일 수 있습니다.")}return CrackChatRoom.of(data)}async*iterateLogs(chatId,{maxCount:maxCount=-1,delay:delay=20,itemPerPage:itemPerPage=20}={}){let amount=0;let cursor=undefined;while(maxCount===-1||amount=maxCount){break}}if(result.data.nextCursor){cursor=result.data.nextCursor}else{break}delay>0&&await new Promise(resolve=>setTimeout(resolve,delay))}}async extractLogs(chatId,{maxCount:maxCount=-1,delay:delay=20,naturalOrder:naturalOrder=true}={}){const logs=[];for await(let log of this.iterateLogs(chatId,{maxCount:maxCount,delay:delay})){if(naturalOrder){logs.unshift(log)}else{logs.push(log)}}return logs}async fetchLastMessage(chatId){try{const next=(await this.iterateLogs(chatId,{maxCount:1}).next()).value;if(next)return next}catch(error){if(error instanceof Error)return error}return null}async findLastMessageId(chatId,requireRole="assistant"){try{for await(let item of this.iterateLogs(chatId)){if(item.role===requireRole){return item}}}catch(error){if(error instanceof Error)return error}return null}async findLastBotMessage(chatId){return this.findLastMessageId(chatId,"assistant")}async findLastUserMessage(chatId){return this.findLastMessageId(chatId,"user")}async editMessage(chatId,messageId,contents){const result=await this.#network.authFetch("PATCH",`https://contents-api.wrtn.ai/character-chat/v3/chats/${chatId}/messages/${messageId}`,{message:contents});if(result instanceof Error){return result}return true}async getMessage(chatId,messageId){const result=await this.#network.authFetch("GET",`https://crack-api.wrtn.ai/crack-gen/v3/chats/${chatId}/messages/${messageId}`);if(result instanceof Error){return result}return CrackChattingLog.of(result.data)}async deleteMessage(chatId,messageId){const result=await this.#network.authFetch("DELETE",`https://crack-api.wrtn.ai/crack-gen/v3/chats/${chatId}/messages/${messageId}`);if(result instanceof Error){return result}return true}async currentPersona(chatId){const chatData=await this.roomData(chatId);if(chatData instanceof Error){return chatData}const personaMap=await this.#user.currentPersonaMap();if(personaMap instanceof Error){return personaMap}return personaMap.get(chatData.chatProfileId??"--UNKNOWN--")??null}async changeChatModel(chatId,modelId){const result=await this.#network.authFetch("PATCH",`https://crack-api.wrtn.ai/crack-gen/v3/chats/${chatId}`,{chatModelId:modelId});if(result instanceof Error){return result}return true}async connect(chatId){const socket=(await this.#io.prepareIo())("https://contents-api.wrtn.ai/v3/chats",{reconnectionDelayMax:1e3,transports:["websocket"],path:"/character-chat/socket.io",auth:{token:this.#cookie.getCookie("access_token"),refreshToken:this.#cookie.getCookie("refresh_token"),platform:"web"}});await new Promise(async(resolve,reject)=>{socket.emit("enter",{chatId:chatId},async response=>{if(response.result!=="success"){reject(new Error("socket.io 방 입장 감지에 실패하였습니다."));return}resolve(undefined)})});return socket}async send(chatId,message,{socket:socket=undefined,onMessageSent:onMessageSent=undefined,timeout:timeout=1e5}={}){try{const currentSocket=socket??await this.connect(chatId);const socketCloser=()=>{if(!socket){try{currentSocket?.close()}catch(err){}}};const lastMessage=await this.fetchLastMessage(chatId);if(lastMessage instanceof Error){return lastMessage}if(!lastMessage){socketCloser();return new Error("불가능한 상태입니다: 첫 메시지가 존재하지 않습니다.")}const result=await this.#emitMessage(currentSocket,timeout,chatId,message,lastMessage,socketCloser,onMessageSent);if(!result){socketCloser();return new Error("지정한 시간 안에 메시지 응답이 완료되지 않았습니다, 실패로 간주합니다.")}}catch(error){if(error instanceof Error)return error}return undefined}async#emitMessage(currentSocket,timeout,chatId,message,lastMessage,socketCloser,onMessageSent){return await new Promise(async(resolve,reject)=>{let isResolved=false;const taskId=setTimeout(()=>{if(!isResolved){socketCloser();isResolved=true;resolve(false)}},timeout);try{currentSocket.emit("send",{chatId:chatId,message:message,prevMessageId:lastMessage.id},async sendResponse=>{if(sendResponse.result==="success"){currentSocket.once("characterMessageGenerated",async response=>{socketCloser();if(!isResolved){isResolved=true;clearTimeout(taskId)}try{await this.#handleResponse(chatId,onMessageSent)}finally{resolve(true)}})}else{socketCloser();reject(new Error("socket.io 메시지 전송에 실패하였습니다."))}})}catch(err){reject(err)}})}async sendUserMessage(chatId,message,{socket:socket=undefined,timeout:timeout=1e5,onMessageSent:onMessageSent=undefined}={}){return await this.send(chatId,message,{socket:socket,timeout:timeout,onMessageSent:async(user,bot)=>{await this.deleteMessage(chatId,bot.id);const result=onMessageSent?.(user);if(result instanceof Promise){await result}}})}async sendBotMessage(chatId,message,{socket:socket=undefined,timeout:timeout=1e5,onMessageSent:onMessageSent=undefined}={}){return await this.send(chatId,message,{socket:socket,timeout:timeout,onMessageSent:async(user,bot)=>{await this.deleteMessage(chatId,user.id);const result=onMessageSent?.(bot);if(result instanceof Promise){await result}}})}async#handleResponse(chatId,onMessageSent){if(onMessageSent){const message=await this.extractLogs(chatId,{maxCount:2,naturalOrder:true});if(message instanceof Error){throw message}const result=onMessageSent(message[0],message[1]);if(result instanceof Promise)await result}}}class _CrackComponentApi{sidePanel(){const node=document.getElementsByClassName("css-c82bbp")[0];return node?.children[0]?.children[0]}}class _SocketIoUtil{#socketIo;async enable(){if(this.#socketIo)return;const{io:io}=await import("https://cdn.socket.io/4.8.1/socket.io.esm.min.js");this.#socketIo=io}async prepareIo(){if(!this.#socketIo)await this.enable();return this.io()}io(){if(this.#socketIo){return this.#socketIo}throw new Error("socket.io not prepared yet")}}class CrackUtil{static#io=new _SocketIoUtil;static#cookie=new _CrackCookieApi;static#path=new _CrackPathApi;static#generic=new _CrackGenericUtil;static#component=new _CrackComponentApi;static#network=new _CrackNetworkApi(this.#cookie);static#theme=new _CrackThemeApi;static#user=new _CrackUserApi(this.#network);static#room=new _CrackChatRoomApi(this.#generic,this.#user,this.#network,this.#cookie,this.#io);static#story=new _CrackStoryApi(this.#generic,this.#user,this.#network);static#cracker=new _CrackCrackerApi(this.#network);static#attend=new _CrackAttendApi(this.#network);static attend(){return this.#attend}static chatRoom(){return this.#room}static story(){return this.#story}static user(){return this.#user}static cracker(){return this.#cracker}static theme(){return this.#theme}static cookie(){return this.#cookie}static network(){return this.#network}static path(){return this.#path}static component(){return this.#component}}function parentElement(element){return element.parentElement}function childNodes(element){return element.childNodes}function purifyNodes(nodes){return nodes}function isAttributeEquals(element,elementToCompare,attributeName,compareUndefined=true){if(!compareUndefined&&(!element.hasAttribute(attributeName)||!elementToCompare.hasAttribute(attributeName))){return false}return element.getAttribute(attributeName)===elementToCompare.getAttribute(attributeName)}class GenericUtil{static attachObserver(observeTarget,lambda){const Observer=window.MutationObserver||window.WebKitMutationObserver;if(observeTarget&&Observer){let instance=new Observer(lambda);instance.observe(observeTarget,{childList:true,subtree:true,attributes:true})}}static attachHrefObserver(node,lambda){let oldHref=location.href;this.attachObserver(node,()=>{if(oldHref!==location.href){oldHref=location.href;lambda()}})}static onPageReady(runner){"loading"===document.readyState?document.addEventListener("DOMContentLoaded",runner):runner(),window.addEventListener("load",runner)}static refine(element){return element}static clone(element){return this.refine(element.cloneNode(true))}static cloneCast(element){return this.refine(element.cloneNode(true))}static assert(item){if(item===undefined||item===null){throw new Error("ASSERTION FAILED; null or undefined object detected at non-null logic")}return item}static isValid(item){if(item===undefined||item===null){return false}return true}static formatNumber(number,minDigit=1,maxdigit=1){return number.toLocaleString("en-US",{minimumFractionDigits:minDigit,maximumFractionDigits:maxdigit})}}class LocaleStorageConfig{constructor(key,configTemplate){this.key=key;this.config=configTemplate}load(){const loadedSettings=localStorage.getItem(this.key);if(loadedSettings){const json=JSON.parse(loadedSettings);for(let key of Object.keys(json)){this.config[key]=json[key]}}return this.config}save(){localStorage.setItem(this.key,JSON.stringify(this.config))}fork(key){return new LocaleStorageConfig(key,structuredClone(this.config))}}class LogUtil{constructor(prefix,enableDebug){this.prefix=prefix;this.debugId=`cDynamicDebug_${crypto.randomUUID().toString()}`;this.prefixStyle="color: cyan;";this.debugTitleStyle="color: gray;";this.infoTitleStyle="color: blue;";this.warningTitleStyle="color: yellow;";this.errorTitleStyle="color: red;";this.debugTextStyle="color: gray;";this.infoTextStyle="color: inherit;";this.warningTextStyle="color: inherit;";this.errorTextStyle="color: inherit;";if(enableDebug){this.log(`디버그 로그가 활성화된 상태입니다.`)}else{this.log(`디버그 로그를 활성화하려면 콘솔 창에 'document["${this.debugId}"] = true'를 입력하세요.`)}}debug(message,extra=undefined){if(message){console.debug(`%c${this.prefix}: %cDEBUG: %c`+message,this.prefixStyle,this.debugTitleStyle,this.debugTextStyle)}if(extra){console.log(extra)}}log(message,extra=undefined){if(message){console.log(`%c${this.prefix}: %cInfo: %c`+message,this.prefixStyle,this.infoTitleStyle,this.infoTextStyle)}if(extra){console.log(extra)}}warn(message,extra=undefined){if(message){console.log(`%c${this.prefix}: %cWarning: %c`+message,this.prefixStyle,this.warningTitleStyle,this.warningTextStyle)}if(extra){console.log(extra)}}error(message,extra=undefined){if(message){console.log(`%c${this.prefix}: %cError: %c`+message,this.prefixStyle,this.errorTitleStyle,this.errorTextStyle)}if(extra){console.log(extra)}}}class TimeUtil{MMMM=["January","February","March","April","May","June","July","August","September","October","November","December"];MMM=this.MMMM.map(m=>m.slice(0,3));dddd=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];ddd=this.dddd.map(d=>d.slice(0,3));ii(i,len=2){return(i+"").padStart(len,"0")}tzHHMM(tz){const sign=tz>0?"-":"+";const tzv=Math.abs(tz);const tzHrs=Math.floor(tzv/60);const tzMin=tzv%60;return sign+this.ii(tzHrs)+":"+this.ii(tzMin)}formatDate(date,format,utc=false){const y=utc?date.getUTCFullYear():date.getFullYear();const M=utc?date.getUTCMonth():date.getMonth();const d=utc?date.getUTCDate():date.getDate();const H=utc?date.getUTCHours():date.getHours();const h=H>12?H-12:H==0?12:H;const m=utc?date.getUTCMinutes():date.getMinutes();const s=utc?date.getUTCSeconds():date.getSeconds();const f=utc?date.getUTCMilliseconds():date.getMilliseconds();const TT=H<12?"AM":"PM";const tt=TT.toLowerCase();const day=utc?date.getUTCDay():date.getDay();const replacements={y:y,yy:y.toString().slice(-2),yyy:y,yyyy:y,M:M,MM:this.ii(M),MMM:this.MMM[M],MMMM:this.MMMM[M],d:d,dd:this.ii(d),ddd:this.ddd[day],dddd:this.dddd[day],H:H,HH:this.ii(H),h:h,hh:this.ii(h),m:m,mm:this.ii(m),s:s,ss:this.ii(s),f:Math.round(f/100),ff:this.ii(Math.round(f/10)),fff:this.ii(f,3),ffff:this.ii(f*10,4),T:TT[0],TT:TT,t:tt[0],tt:tt,K:utc?"Z":this.tzHHMM(date.getTimezoneOffset()),"\\":""};return format.replace(/(?:\\(?=.)|(?replacements[$0])}}const DECENTRAL_VERSION="Decentrallized Modal v1.0.15";const DECENTRAL_CSS_VALUES=`\n /*\n * 변수 선언부 \n */\n .decentral-color-container {\n --decentral-text: #000000;\n --decentral-text-inverted: #FFFFFF;\n --decentral-text-formal: #64748B;\n --decentral-text-inactive-hover: #64748B;\n --decentral-text-inactive: #64748B;\n --decentral-background: #FFFFFF;\n --decentral-background-menu: #EFF6FF;\n --decentral-hover: #F1F5F9;\n --decentral-border: #CBD5E1;\n --decentral-active-item: #2563EB;\n --decentral-inactive-item: #94A3B8;\n --decentral-active-text: #2563EB;\n --decentral-background-active-item: #0EA5E910;\n --decentral-text-background: #F1F5F9;\n --decentral-text-border: #E2E8F0;\n --decentral-switch-background: #F8FAFC;\n --decentral-switch-inactive: #CBD5E1;\n }\n\n .decentral-color-container[theme="dark"] {\n --decentral-text: #FFFFFF;\n --decentral-text-inverted: #000000;\n --decentral-text-formal: #b3b7bdff;\n --decentral-text-inactive-hover: #64748B;\n --decentral-text-inactive: #949494ff;\n --decentral-background: #272727ff;\n --decentral-background-menu: #292929ff;\n --decentral-hover: #5a5a5aff;\n --decentral-border: #686868ff;\n --decentral-active-item: #4e7ce0ff;\n --decentral-inactive-item: #94A3B8;\n --decentral-active-text: #4e7ce0ff;\n --decentral-background-active-item: #0EA5E910;\n --decentral-text-background: transparent;\n --decentral-text-border: #686868ff;\n --decentral-switch-background: transparent;\n --decentral-switch-inactive: #7a7a7aff;\n }\n\n /*\n * 모달 컨테이너 선언\n * 원본 페이지\n * └ **모달 컨테이너** \n */\n .decentral-modal-container {\n display: flex;\n flex-direction: column;\n z-index: 999;\n margin: auto;\n pointer-events: auto;\n background-color: #99999970;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n position: fixed;\n font-family: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, system-ui, Roboto, "Helvetica Neue", "Segoe UI", "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif;\n }\n \n\n /*\n * 모달 베이스 선언\n * 원본 페이지\n * └ 모달 컨테이너\n * └ **모달 베이스** \n */\n .decentral-modal {\n display: flex;\n flex-direction: row;\n min-width: 300px;\n max-width: 1000px;\n min-height: 640px;\n max-height: 90%;\n width: 100%;\n height: 60%;\n border-radius: 3px;\n background-color: var(--decentral-background);\n margin: auto;\n color: var(--decentral-text);\n }\n\n /*\n * 모달 헤더(제목) 선언\n * 원본 페이지\n * └ 모달 컨테이너\n * └ 모달 베이스\n * └ **모달 헤더**\n */\n /* 모달 헤더 아이콘 */\n .decentral-modal-title-icon {\n width: 32px;\n height: 32px;\n display: flex;\n padding: 0px 16px;\n }\n\n /* 모달 헤더 제목 */\n .decentral-modal-title-container {\n display: flex;\n flex-direction: row;\n padding: 15px 20px;\n align-items: center;\n border-bottom: 1px solid var(--decentral-border);\n }\n\n /* 모달 헤더 제목 텍스트*/\n .decental-modal-title-text {\n margin-left: 10px;\n }\n\n \n /* 모달 헤더 우측 툴바 컨테이너 */\n .decentral-modal-button-container {\n margin-left: auto;\n display: flex;\n flex-direction: row;\n align-items: center;\n }\n\n /*\n * 모달 좌측 PC / 태블릿 메뉴 컨테이너 선언\n * 원본 페이지\n * └ 모달 컨테이너\n * └ 모달 베이스\n * └ **모달 메뉴**\n */\n /* 모달 메뉴 컨테이너 */\n .decentral-menu-container {\n border-right: 1px solid var(--decentral-border);\n display: flex;\n width: 200px;\n min-width: 200px;\n height: 100%;\n flex-direction: column;\n padding: 20px 15px;\n background-color: var(--decentral-background-menu);\n color: var(--decentral-text-inactive);\n font-weight: 400;\n user-select: none;\n overflow-y: auto;\n }\n\n /* 모달 최상단 메뉴 요소 */\n .decentral-menu-container .decentral-menu-element {\n display: flex;\n font-size: 15px;\n padding: 12px 10px;\n border-radius: 2px;\n padding-left: 15px;\n width: 100%;\n border-radius: 2px;\n }\n \n /* 모달 최상단 메뉴 요소 컨테이너 */\n .decentral-menu-element-container .decentral-sub-menu-container {\n display: none;\n width: 100%;\n }\n\n .decentral-menu-element-container[active="true"] .decentral-sub-menu-container {\n display: flex;\n flex-direction: column;\n margin-top: 5px;\n }\n\n /* 모달 서브 메뉴 요소 */\n .decentral-menu-container .decentral-sub-menu-element {\n display: flex;\n font-size: 14px;\n padding: 8px 6px;\n border-radius: 2px;\n padding-left: 15px;\n margin-left: 10px;\n width: 100%;\n border-radius: 2px;\n }\n\n /* 메뉴 커서 핸들러 */\n .decentral-menu-element, .decentral-sub-menu-element {\n cursor: pointer;\n }\n \n /* 표시된 메뉴 호버시 백그라운드 색상 적용 */\n .decentral-menu-element:not([active="true"]):hover, .decentral-sub-menu-element:not([active="true"]):hover {\n background-color: var(--decentral-hover);\n }\n\n \n .decentral-menu-element:is([active="true"], [child-active="true"]),.decentral-sub-menu-element[active="true"] {\n color: var(--decentral-active-text); \n background-color: var(--decentral-background-active-item);\n }\n \n /* 선택된 메뉴 색상 및 볼드 적용 */\n .decentral-menu-container .decentral-menu-element[active="true"],.decentral-sub-menu-element[active="true"] {\n position: relative;\n font-weight: 700;\n }\n\n /* 선택된 메뉴 좌측 보더 적용 */\n .decentral-menu-container .decentral-menu-element[active="true"]:before,.decentral-sub-menu-element[active="true"]:before {\n top: 20%;\n content: '';\n position: absolute;\n left: -1px;\n width: 2px;\n height: 60%;\n background-color: blue;\n }\n\n \n /*\n * 모바일 메뉴 컨테이너 선언\n * 원본 페이지\n * └ 모달 컨테이너\n * └ 모달 베이스\n * └ **모바일 모달 메뉴**\n */\n /* 모바일 모달 메뉴 컨테이너 */\n .decentral-mobile-menu-container {\n display: none;\n z-index: 2500;\n border: 1px solid var(--decentral-border);\n width: 100%;\n height: fit-content;\n flex-direction: column;\n padding: 20px 15px;\n background-color: var(--decentral-background-menu);\n color: var(--decentral-text-inactive);\n font-weight: 400;\n user-select: none;\n overflow-y: auto;\n }\n\n .decentral-mobile-menu-container[active="true"] {\n display: flex;\n position: absolute;\n }\n\n\n\n /* 모바일 모달 최상단 메뉴 요소 */\n .decentral-mobile-menu-container .decentral-mobile-menu-element {\n display: flex;\n font-size: 15px;\n padding: 12px 10px;\n border-radius: 2px;\n padding-left: 15px;\n width: 100%;\n border-radius: 2px;\n }\n \n /* 모바일 모달 최상단 메뉴 요소 컨테이너 */\n .decentral-mobile-menu-element-container .decentral-mobile-sub-menu-container {\n display: none;\n width: 100%;\n }\n\n .decentral-mobile-menu-element-container[active="true"] .decentral-mobile-sub-menu-container {\n display: flex;\n flex-direction: column;\n margin-top: 5px;\n }\n\n /* 모바일 모달 서브 메뉴 요소 */\n .decentral-mobile-menu-container .decentral-mobile-sub-menu-element {\n display: flex;\n font-size: 14px;\n padding: 8px 6px;\n border-radius: 2px;\n padding-left: 15px;\n margin-left: 10px;\n width: 100%;\n border-radius: 2px;\n }\n\n /* 모바일 메뉴 커서 핸들러 */\n .decentral-mobile-menu-element, .decentral-mobile-sub-menu-element {\n cursor: pointer;\n }\n \n /* 모바일 표시된 메뉴 호버시 백그라운드 색상 적용 */\n .decentral-mobile-menu-element:not([active="true"]):hover, .decentral-mobile-sub-menu-element:not([active="true"]):hover {\n background-color: var(--decentral-hover);\n }\n\n \n .decentral-mobile-menu-element:is([active="true"], [child-active="true"]),.decentral-mobile-sub-menu-element[active="true"] {\n color: var(--decentral-active-text); \n background-color: var(--decentral-background-active-item);\n }\n \n /* 선택된 모바일 메뉴 색상 및 볼드 적용 */\n .decentral-mobile-menu-container .decentral-menu-element[active="true"],.decentral-mobile-sub-menu-element[active="true"] {\n position: relative;\n font-weight: 700;\n }\n\n /* 선택된 모바일 메뉴 좌측 보더 적용 */\n .decentral-mobile-menu-container .decentral-mobile-menu-element[active="true"]:before,.decentral-mobile-sub-menu-element[active="true"]:before {\n top: 20%;\n content: '';\n position: absolute;\n left: -1px;\n width: 2px;\n height: 60%;\n background-color: blue;\n }\n\n \n /*\n * 모달 우측 컨텐츠 선언\n * 원본 페이지\n * └ 모달 컨테이너\n * └ 모달 베이스\n * └ 모달 컨텐츠\n */\n /* 스크롤 한계 적용을 위한 컨텐츠 그리드 래퍼 */\n .decentral-grid-container {\n display: flex;\n width: 100%;\n height: 100%;\n overflow-y: auto;\n overflow-x: hidden;\n min-height: 0;\n min-width: 0; \n position: relative;\n }\n\n /* 컨텐츠 그리드 컨테이너 */\n .decentral-grid {\n display: grid;\n width: 100%;\n min-width: 200px;\n grid-template-columns: repeat(2, 1fr);\n gap: 3em;\n grid-row-gap: 0.3em;\n padding: 8px 16px;\n min-height: 0;\n min-width: 0; \n position: absolute;\n }\n\n /* 컨텐츠 고정 푸터 */\n .decentral-modal-footer {\n border-top: 1px solid var(--decentral-border);\n display: flex;\n flex-direction: column;\n padding: 0px 16px;\n }\n\n /* 컨텐츠 제목 텍스트 */\n .decentral-element-title {\n display: flex;\n flex-direction: row;\n font-size: 13px;\n font-weight: light;\n padding-bottom: 8px;\n color: var(--decentral-text-formal);\n user-select: none;\n width: 100%;\n align-items: bottom;\n }\n\n .decentral-modifiable-component {\n }\n\n .decentral-element-title-suffix {\n font-size: 13px;\n font-weight: light;\n color: var(--decentral-text-formal);\n user-select: none;\n margin-left: auto;\n width: fit-content;\n height: fit-content;\n }\n \n /* 2단 적재 가능한 속성 요소 */\n .decentral-grid-element {\n display: flex; \n flex-direction: column;\n max-width: 100%;\n height: fit-content;\n padding: 10px 5px;\n min-height: 0;\n min-width: 0; \n }\n\n /* 동시 적재 불가능한 속성 요소 */\n .decentral-grid-element-long {\n display: flex;\n flex-direction: column;\n grid-column: 1 / 3;\n max-width: 100%;\n height: fit-content;\n padding: 10px 5px;\n min-height: 0;\n min-width: 0; \n }\n\n /* 동시 적재 불가능한 속성 요소 */\n .decentral-grid-element-long-semi-flat {\n display: flex;\n flex-direction: column;\n grid-column: 1 / 3;\n max-width: 100%;\n height: fit-content;\n padding: 5px 5px;\n min-height: 0;\n min-width: 0; \n }\n\n\n /* 동시 적재 불가능한 속성 요소 */\n .decentral-grid-element-long-flat {\n display: flex;\n flex-direction: column;\n grid-column: 1 / 3;\n max-width: 100%;\n height: fit-content;\n padding: 0px 5px;\n min-height: 0;\n min-width: 0; \n }\n\n \n /*\n * 범용 클래스 선언 \n */\n /* Weight 1 기반 가변 수직 컨테이너 */\n .decentral-vertical-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n position: relative;\n }\n\n \n /*\n * 체크박스-스위치 선언 \n */\n /* 스위치 최상단 컨테이너 */\n .decentral-boxed-field {\n display: flex;\n width: 100%;\n background-color: var(--decentral-switch-background);\n border: 1px solid var(--decentral-text-border);\n user-select: none;\n padding: 12px 0px;\n }\n\n .decentral-padded-boxed-field {\n display: flex;\n width: 100%;\n background-color: var(--decentral-switch-background);\n border: 1px solid var(--decentral-text-border);\n user-select: none;\n padding: 12px 16px;\n }\n\n /* 스위치 텍스트 컨테이너 */\n .decentral-boxed-field .element-text-container {\n display: flex;\n flex-direction: column;\n justify-items: center;\n padding: 8px 16px;\n }\n\n \n /* 긴 스위치 텍스트 컨테이너 */\n .decentral-boxed-field .element-text-container-full {\n display: flex;\n flex-direction: column;\n justify-items: center;\n padding: 8px 16px;\n width: 100%;\n }\n\n\n /* 스위치 제목 */\n .decentral-boxed-field .element-title {\n font-size: 14px;\n font-weight: normal;\n margin-bottom: 4px;\n color: var(--decentral-text);\n }\n\n /* 스위치 설명 */\n .decentral-boxed-field .element-description {\n font-size: 13px;\n font-weight: light;\n color: var(--decentral-text-formal);\n }\n\n \n /* 스위치 내 체크박스 컨테이너 */\n .decentral-boxed-field .element-input-container {\n display: flex;\n margin-left: auto;\n align-items: center;\n padding: 4px 8px;\n }\n\n .decentral-boxed-field .element-input-container-long {\n display: flex;\n width: 100%;\n align-items: center;\n margin-top: 8px;\n }\n\n .decentral-boxed-field .element-small-input {\n background: var(--decentral-text-background);\n border: 1px solid var(--decentral-text-border);\n color: var(--decentral-text);\n border-radius: 3px;\n width: 64px;\n height: 36px;\n padding: 4px 8px;\n }\n\n \n .decentral-boxed-field .element-medium-input {\n background: var(--decentral-text-background);\n border: 1px solid var(--decentral-text-border);\n color: var(--decentral-text);\n border-radius: 3px;\n width: 96px;\n height: 36px;\n padding: 4px 8px;\n }\n\n \n .decentral-boxed-field .element-large-input {\n background: var(--decentral-text-background);\n border: 1px solid var(--decentral-text-border);\n color: var(--decentral-text);\n border-radius: 3px;\n width: 128px;\n height: 36px;\n padding: 4px 8px;\n }\n\n \n /*\n Switch Checkbox source code from:\n https://www.daleseo.com/css-toggle-switch/\n */\n /* 커스텀 체크박스 설정 */\n .element-switch {\n appearance: none;\n position: relative;\n border-radius: 1.25em;\n width: 4em;\n height: 2em;\n background-color: var(--decentral-switch-inactive);\n border-color: var(--decentral-switch-inactive);\n }\n\n /* 체크박스 스위치 버튼 */\n .element-switch::before {\n content: "";\n position: absolute;\n top: -0.05em;\n left: 0;\n width: 2em;\n height: 2em;\n border-radius: 50%;\n transform: scale(0.6);\n background-color: white;\n transition: left 50ms linear;\n }\n\n /* 선택된 체크박스의 스위치 위치 이동 */\n .element-switch:checked::before {\n left: 2em;\n }\n /* 체크된 스위치 체크박스 색상 */\n .element-switch:checked {\n background-color: var(--decentral-active-item);\n border-color: var(--decentral-active-item);\n }\n\n /* 비활성화된 스위치 체크박스 설정 */\n .element-switch:disabled {\n border-color: lightgray;\n opacity: 0.7;\n cursor: not-allowed;\n }\n\n /* 비활성화된 스위치 버튼 색상 설정 */\n .element-switch:disabled:before {\n background-color: var(--decentral-text-inactive);\n }\n\n /*\n * input 기반 1단 텍스트필드 선언 \n */\n .decentral-text-field {\n background-color: var(--decentral-text-background);\n border: 1px solid var(--decentral-text-border);\n border-radius: 4px;\n width: 100%;\n padding: 4px 8px;\n color: var(--decentral-text);\n height: 36px;\n }\n\n \n /*\n * textarea 기반 다단식 텍스트에리어 선언 \n */\n /* 수정 가능한 일반 textarea */\n .decentral-text-area {\n background-color: var(--decentral-text-background);\n border: 1px solid var(--decentral-text-border);\n resize: none;\n width: 100%;\n padding: 4px 8px;\n color: var(--decentral-text);\n height: 64px;\n }\n\n /* \n * 데이터 표시용 수정 불가능 textarea\n */\n .decentral-logging-area {\n background-color: var(--decentral-text-background);\n border: 1px solid var(--decentral-text-border);\n resize: none;\n width: 100%;\n padding: 4px 8px;\n color: var(--decentral-text);\n height: 128px;\n }\n\n .decentral-button {\n background-color: var(--decentral-active-item);\n color: var(--decentral-text-inverted);\n width: 100%;\n height: 32px;\n padding: 4px 16px;\n border-radius: 3px;\n font-weight: normal;\n font-size: 14px;\n }\n\n .decentral-button[disabled="true"] {\n background-color: var(--decentral-inactive-item);\n cursor: not-allowed;\n }\n\n .decentral-text-title {\n font-size: 20px;\n font-weight: bold;\n color: var(--decentral-text);\n margin-top: 4px;\n }\n\n .decentral-text-plain {\n font-size: 14px;\n font-weight: light;\n color: var(--decentral-text-formal);\n }\n\n /**\n * 커스텀 셀렉트박스 드롭다운 \n */\n .decentral-select { \n width: 100%;\n border-radius: 3px;\n background-color: transparent; \n overflow: hidden;\n position: relative; \n color: var(--decentral-text); \n border: 1px solid var(--decentral-text-border);\n user-select: none; \n appearance: none; \n -webkit-appearance: none; \n -moz-appearance: none;\n font-size: 14px;\n }\n\n .decentral-select .decentral-option { \n position: relative;\n background-color: var(--decentral-background);\n color: var(--decentral-text);\n float: left;\n z-index: 2002;\n position: relative;\n display: flex;\n width: 100%;\n }\n\n .decentral-select:not([list-enabled="true"]) .decentral-list { \n display: none;\n }\n\n .decentral-outer-click-detection { \n position: absolute;\n z-index: 2000; \n top: 0; \n left: 0; \n width: 100%; \n height: 100% \n }\n\n .decentral-option-group { \n font-size: 12px; \n color: var(--decentral-text-formal); \n font-weight: bold; \n margin-top: 5px; \n margin-bottom: 5px; \n margin-left: 5px; \n }\n\n .decentral-option { \n padding: 4px 10px; \n background-color: transparent; \n background-color: var(--decentral-text); \n transition: color 0.1s ease, background-color 0.1s ease; \n }\n\n .decentral-option:not(:nth-child(1)) { \n z-index: 2100; \n }\n\n .decentral-option:not(:nth-child(1)):hover { \n background-color: var(--decentral-hover); \n transition: color 0.1s ease, background-color 0.1s ease; \n }\n\n .decentral-select[list-enabled="true"] .decentral-list {\n display: flex; \n flex-direction: column; \n position: fixed; \n padding: 5px; \n z-index: 2300; \n height: 250px; \n overflow-y: scroll; \n border: 1px solid var(--decentral-text-border); \n background-color: var(--decentral-background); \n width: 244px;\n }\n\n\n /**\n * 모바일 레이아웃 대응 \n */\n .decentral-mobile-menu-button {\n display: none;\n }\n\n @media screen and (max-width:600px) {\n .decentral-menu-container {\n display: none;\n width: 100%;\n }\n .decentral-mobile-menu-button {\n display: block;\n margin-right: 8px;\n }\n\n .decentral-modal-mobile-menu[active="true"] {\n display: flex;\n }\n\n .decentral-grid {\n grid-template-columns: 100%; \n column-count: 1;\n }\n \n .decentral-grid-element {\n grid-column: 1 / 1;\n }\n .decentral-grid-element-long {\n grid-column: 1 / 1;\n }\n .decentral-grid-element-long-semi-flat {\n grid-column: 1 / 1;\n }\n .decentral-grid-element-long-flat {\n grid-column: 1 / 1;\n }\n }\n\n @media screen and (max-width:850px) {\n .decentral-grid {\n grid-template-columns: 100%; \n column-count: 1;\n }\n .decentral-grid-element {\n grid-column: 1 / 1;\n }\n .decentral-grid-element-long {\n grid-column: 1 / 1;\n }\n .decentral-grid-element-long-semi-flat {\n grid-column: 1 / 1;\n }\n .decentral-grid-element-long-flat {\n grid-column: 1 / 1;\n }\n }\n /**\n * 연동 전용\n */\n /* 스크롤 제거 */\n .decentral-disable-scroll-flag {\n overflow: hidden !important;\n }\n`;const DECENTRAL_DEFAULT_ICON_SVG=`\n \n`;const DECENTRAL_CLOSE_ICON_SVG=`\n\n`;const DECENTRAL_MENU_ICON_SVG=`\n \n`;const DECENTRAL_ARROW_ICON_SVG=`\n\x3c!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools --\x3e \n`;class ModalManager{static doesInit=false;static#modalMap=new Map;#opener=new Map;#closer=new Map;#licenseAdjusters=[];static __doInit(){if(!this.doesInit){this.doesInit=true;const globalDoc=_refine(document);if(!globalDoc.__modalManager){try{GM_addStyle(DECENTRAL_CSS_VALUES)}catch(ex){console.warn("!! WARNING !!");console.warn("!! WARNING !! GM_addStyle 콜에 실패하였습니다.\n브라우저가 아닌 환경에서 decentralized-modal이 초기화되었을 가능성이 존재합니다.\n해당 환경에서는 decentralized-modal.js가 오작동할 가능성이 존재합니다.")}globalDoc.__modalManager=this.#modalMap}this.#modalMap=globalDoc.__modalManager}}static getOrCreateManager(name){this.__doInit();if(!this.#modalMap.has(name)){const manager=new ModalManager(name);this.#modalMap.set(name,manager);return manager}return this.#modalMap.get(name)}constructor(name){this.name=name;this.__modal=new DecentrallizedModal(name,this.#closer)}addOpenListener(namespace,action){this.#opener.set(namespace,action);return this}addCloseListener(namespace,action){this.#opener.set(namespace,action);return this}withScrollRestorer(namespace,element){this.addOpenListener(namespace,()=>{element.classList.add("decentral-disable-scroll-flag")});this.addCloseListener(namespace,()=>{element.classList.remove("decentral-disable-scroll-flag")});return this}display(isDarktheme,preSelected=undefined){for(let element of this.#opener.values()){element()}this.__modal.display(isDarktheme,preSelected)}getOpened(){return this.__modal}close(){this.__modal.close()}createMenu(menuName,menuAction){return this.__modal.createMenu(menuName,menuAction)}addLicenseDisplay(licenseAppender){this.#licenseAdjusters.push(licenseAppender);return this}withLicenseCredential(menuName){this.createMenu(menuName??"프레임워크에 대하여",modal=>{modal.replaceContentPanel(panel=>{panel.addTitleText(DECENTRAL_VERSION).addText("decentralized.js는 IGX 팀에서 제작되었습니다.").addText("이 프레임워크는 임베딩 가능한 탈중앙화된 모달 프레임워크입니다.").addTitleText("SVG 아이콘 디자인").addText("decentralized.js의 모든 아이콘은 SVGRepo에서 가져왔습니다.").addText("- 설정 아이콘 (https://www.svgrepo.com/svg/458353/setting-line)").addText("- 닫기 아이콘 (https://www.svgrepo.com/svg/494725/close)").addText("- 메뉴 아이콘 (https://www.svgrepo.com/svg/522418/menu)").addText("- 스위치 소스 코드 참조 (https://www.daleseo.com/css-toggle-switch/)");for(let adjuster of this.#licenseAdjusters){adjuster(panel)}},"decentralized.js")});return this}}class ModalMenu{__subMenus=new Map;__activiator=undefined;__activiatorMobile=undefined;constructor(action){this.action=action}async onDisplay(modal){this.action(modal);if(this.__activiator)this.__activiator();if(this.__activiatorMobile)this.__activiatorMobile()}createSubMenu(menuName,menuAction){let menuItem=this.__subMenus.get(menuName);if(!menuItem){menuItem=new ModalMenu(menuAction);this.__subMenus.set(menuName,menuItem)}return this}}class HTMLComponentConvertable{asHTML(){return document.createElement("div")}}class DecentrallizedModal{#selectedMenu=[];#preOpenHandler=[];#menuItems=new Map;__container;__modal;__menuPanel;__mobileMenuPanel;__contentPanel;#closer;constructor(baseId,closer){this.baseId=baseId;this.#closer=closer}getVersion(){return DECENTRAL_VERSION}createMenu(menuName,menuAction){let menuItem=this.#menuItems.get(menuName);if(!menuItem){menuItem=new ModalMenu(menuAction);this.#menuItems.set(menuName,menuItem)}return menuItem}getMenu(menuName){return this.#menuItems.get(menuName)??null}deleteMenu(menuName){this.#menuItems.delete(menuName)}display(isDarkTheme,preSelected){if(this.__container){return}this.close();if(preSelected){this.#selectedMenu.length=0;this.#selectedMenu.push(...preSelected)}this.init(isDarkTheme)}withPreOpenHandler(handler){this.#preOpenHandler.push(handler)}triggerSelect(preSelected){if(preSelected){if(preSelected.length===1){this.#menuItems.get(preSelected[0])?.onDisplay(this)}else if(preSelected.length===2){this.#menuItems.get(preSelected[0])?.__subMenus?.get(preSelected[1])?.onDisplay(this)}else{this.#selectedMenu.length=0;preSelected&&this.#selectedMenu.push(...preSelected)}}else{this.#selectedMenu.length=0}}close(){if(this.__container){for(let element of this.#closer.values()){element()}this.__container.remove();this.__container=undefined;this.#selectedMenu=[]}}init(isDarkTheme){for(let handler of this.#preOpenHandler){handler(this)}this.__container=setupClassNode("div","decentral-modal-container decentral-color-container",node=>{node.id=`decentral-container-${this.baseId}`});this.__container.onclick=e=>{e.stopPropagation()};this.__modal=setupClassNode("div","decentral-modal",node=>{node.id=`decentral-container-${this.baseId}`});this.__container.appendChild(this.__modal);this.__modal.appendChild((this.__menuPanel=new MenuPanel(this,this.#menuItems,this.#selectedMenu)).asHTML());const verticalPanel=setupClassNode("div","decentral-vertical-container");verticalPanel.appendChild((this.__mobileMenuPanel=new MobileMenuPanel(this,this.#menuItems,this.#selectedMenu)).asHTML());verticalPanel.appendChild((this.__contentPanel=new ContentPanel(`decentral-content-${this.baseId}`,"여기에 텍스트 입력",DECENTRAL_DEFAULT_ICON_SVG,()=>{this.__mobileMenuPanel?.open()},()=>{this.close()})).asHTML());this.__modal.append(verticalPanel);this.__container.setAttribute("theme",isDarkTheme?"dark":"light");document.body.append(this.__container);this.__menuPanel.runSelected()}replaceContentPanel(lambda,title,iconSvg=undefined){const element=assert(document.getElementById(`decentral-content-${this.baseId}`));this.__contentPanel=new ContentPanel(`decentral-content-${this.baseId}`,title,iconSvg??DECENTRAL_DEFAULT_ICON_SVG,()=>{this.__mobileMenuPanel?.open()},()=>{this.close()});lambda(this.__contentPanel);element.id="";assert(element.parentElement).insertBefore(this.__contentPanel.asHTML(),element);element.remove()}refreshMenuPanel(){const mainMenu=new MenuPanel(this,this.#menuItems,this.#selectedMenu);const mobileMenu=new MobileMenuPanel(this,this.#menuItems,this.#selectedMenu);assert(this.__menuPanel?.currentMenu()?.parentElement).insertBefore(mainMenu.asHTML(),assert(this.__menuPanel?.currentMenu()));this.__menuPanel?.currentMenu()?.remove();assert(this.__mobileMenuPanel?.currentMenu()?.parentElement).append(mobileMenu.asHTML(),assert(this.__mobileMenuPanel?.currentMenu()));this.__mobileMenuPanel?.currentMenu()?.remove();this.__menuPanel=mainMenu;this.__mobileMenuPanel=mobileMenu}}class BaseMenuPanel extends HTMLComponentConvertable{constructor(modal,menus,selectedMenu){super();this.menus=menus;this.modal=modal;this.selectedMenu=selectedMenu}runSelected(){if(this.selectedMenu.length===0&&this.menus.size>0){assert(this.menus?.entries()?.next()?.value)[1]?.onDisplay(this.modal);return}if(this.selectedMenu.length===1){this.menus.get(this.selectedMenu[0])?.onDisplay(this.modal);return}if(this.selectedMenu.length===2){this.menus.get(this.selectedMenu[0])?.__subMenus?.get(this.selectedMenu[1])?.onDisplay(this.modal)}}replaceSelected(selected){this.selectedMenu.length=0;this.selectedMenu.push(...selected)}hideAllActive(selectors){for(const selector of selectors){for(const menu of document.getElementsByClassName(selector)){menu.removeAttribute("active");if(selector.includes("-element")){menu.removeAttribute("child-active")}}}}}class MenuPanel extends BaseMenuPanel{#menu;constructor(modal,menus,selectedMenu){super(modal,menus,selectedMenu);this.menus=menus;this.modal=modal;this.selectedMenu=selectedMenu;this.#menu=undefined}hideAllActive(){super.hideAllActive(["decentral-menu-element-container","decentral-menu-element","decentral-sub-menu-element"])}currentMenu(){return this.#menu}asHTML(){if(this.#menu){return this.#menu}const container=setupClassNode("div","decentral-menu-container");let isElementActiveSelected=false;for(const[itemName,menuItem]of this.menus){const menuContainer=this.createMenuContainer(itemName,menuItem,isActive=>{isElementActiveSelected=isActive});container.append(menuContainer)}this.setDefaultActiveState(isElementActiveSelected,container);return this.#menu=container}createMenuContainer(itemName,menuItem,setActiveSelected){const menuContainer=setupClassNode("div","decentral-menu-element-container");const menuText=setupClassNode("span","decentral-menu-element",node=>{node.textContent=itemName;menuItem.__activiator=()=>{this.hideAllActive();node.setAttribute("active","true");menuContainer.setAttribute("active","true")};node.onclick=()=>{this.hideAllActive();this.replaceSelected([itemName]);this.runSelected()}});menuContainer.appendChild(menuText);if(menuItem.__subMenus.size>0){const subMenuContainer=this.createSubMenuContainer(itemName,menuItem,menuContainer,menuText,setActiveSelected);menuContainer.append(subMenuContainer)}return menuContainer}createSubMenuContainer(itemName,menuItem,menuContainer,menuText,setActiveSelected){const subMenuContainer=setupClassNode("div","decentral-sub-menu-container");for(const[subItemName,subItem]of menuItem.__subMenus){const subMenuNode=setupClassNode("span","decentral-sub-menu-element",node=>{node.textContent=subItemName;const expectedSubmenu=[itemName,subItemName];_refine(subItem).__activiator=()=>{this.hideAllActive();menuContainer.setAttribute("active","true");menuText.setAttribute("child-active","true");node.setAttribute("active","true")};node.onclick=()=>{this.replaceSelected(expectedSubmenu);this.runSelected()}});subMenuContainer.appendChild(subMenuNode);if(!setActiveSelected(false)&&this.selectedMenu.length===2&&this.selectedMenu[0]===itemName&&this.selectedMenu[1]===subItemName){setActiveSelected(true);subMenuContainer.setAttribute("active","true")}}return subMenuContainer}setDefaultActiveState(isElementActiveSelected,container){if(!isElementActiveSelected){const selectedItemName=this.selectedMenu[0];let targetMenuContainer;if(this.selectedMenu.length===0){targetMenuContainer=container.querySelector(".decentral-menu-element-container")}else if(this.selectedMenu.length===1){const menuElements=container.querySelectorAll(".decentral-menu-element");for(const menuText of menuElements){if(menuText.textContent===selectedItemName){targetMenuContainer=menuText.parentElement;break}}}if(targetMenuContainer){targetMenuContainer.setAttribute("active","true");targetMenuContainer.querySelector(".decentral-menu-element")?.setAttribute("active","true")}}}}class MobileMenuPanel extends BaseMenuPanel{#menu;constructor(modal,menus,selectedMenu){super(modal,menus,selectedMenu);this.#menu=undefined}open(){this.#menu?.setAttribute("active","true")}close(){this.#menu?.removeAttribute("active")}hideAllActive(){super.hideAllActive(["decentral-mobile-menu-element-container","decentral-mobile-menu-element","decentral-mobile-sub-menu-element"])}asHTML(){const container=this.#menu=setupClassNode("div","decentral-mobile-menu-container");let isElementActiveSelected=false;for(const[itemName,menuItem]of this.menus){const menuContainer=this.createMenuContainer(itemName,menuItem,isActive=>{isElementActiveSelected=isActive;return false});container.append(menuContainer)}this.setDefaultActiveState(isElementActiveSelected,container);return container}currentMenu(){return this.#menu}createMenuContainer(itemName,menuItem,setActiveSelected){const menuContainer=setupClassNode("div","decentral-mobile-menu-element-container");const menuText=setupClassNode("span","decentral-mobile-menu-element",node=>{node.textContent=itemName;_refine(menuItem).__activiatorMobile=()=>{this.hideAllActive();node.setAttribute("active","true");menuContainer.setAttribute("active","true")};node.onclick=()=>{this.close();this.hideAllActive();this.replaceSelected([itemName]);this.runSelected()}});menuContainer.appendChild(menuText);if(menuItem.__subMenus.size>0){const subMenuContainer=this.createSubMenuContainer(itemName,menuItem,menuContainer,menuText,setActiveSelected);menuContainer.append(subMenuContainer)}return menuContainer}createSubMenuContainer(itemName,menuItem,menuContainer,menuText,setActiveSelected){const subMenuContainer=setupClassNode("div","decentral-mobile-sub-menu-container");for(const[subItemName,subItem]of menuItem.__subMenus){const subMenuNode=setupClassNode("span","decentral-mobile-sub-menu-element",node=>{node.textContent=subItemName;const expectedSubmenu=[itemName,subItemName];_refine(subItem).__activiatorMobile=()=>{this.hideAllActive();menuContainer.setAttribute("active","true");menuText.setAttribute("child-active","true");node.setAttribute("active","true")};node.onclick=()=>{this.close();this.replaceSelected(expectedSubmenu);this.runSelected()}});subMenuContainer.appendChild(subMenuNode);if(!setActiveSelected(false)&&this.selectedMenu.length===2&&this.selectedMenu[0]===itemName&&this.selectedMenu[1]===subItemName){setActiveSelected(true);subMenuContainer.setAttribute("active","true")}}return subMenuContainer}setDefaultActiveState(isElementActiveSelected,container){if(!isElementActiveSelected){const selectedItemName=this.selectedMenu[0];let targetMenuContainer;if(this.selectedMenu.length===0){targetMenuContainer=container.querySelector(".decentral-mobile-menu-element-container")}else if(this.selectedMenu.length===1){const menuElements=container.querySelectorAll(".decentral-mobile-menu-element");for(const menuText of menuElements){if(menuText.textContent===selectedItemName){targetMenuContainer=menuText.parentElement;break}}}if(targetMenuContainer){targetMenuContainer.setAttribute("active","true");targetMenuContainer.querySelector(".decentral-mobile-menu-element")?.setAttribute("active","true")}}}}class ComponentAppender extends HTMLComponentConvertable{constructor(parentElement){super();this.parentElement=parentElement}addGrid(titleText,isLongField,lambda){this.parentElement.appendChild(createGridElement(titleText,isLongField,lambda));return this}addTitleText(text,{initializer:initializer=undefined,__proxy:__proxy=this}={initializer:undefined,__proxy:this}){__proxy.parentElement.append(createLongFlatGridElement(null,node=>{node.append(setupClassNode("p","decentral-text-title",textNode=>{textNode.innerText=text;if(initializer){initializer(textNode)}}))}));return this}addText(text,{initializer:initializer=undefined}={}){this.parentElement.append(createLongFlatGridElement(null,node=>{node.append(setupClassNode("p","decentral-text-plain",textNode=>{textNode.innerText=text;if(initializer){initializer(textNode)}}))}));return this}constructInputGrid(id,titleText,isLongField,{defaultValue:defaultValue=undefined,onInit:onInit=undefined,onChange:onChange=undefined}={}){let inputNode=setupClassNode("input","decentral-text-field decentral-modifiable-component",inputField=>{inputField.id=id;inputField.setAttribute("type","text");if(defaultValue){_refine(inputField).value=defaultValue}if(onChange){let lastValue=_refine(inputField).value;inputField.onchange=()=>{lastValue=_refine(inputField).value;onChange(inputField,_refine(inputField).value)};_refine(inputField).onVerifyChange=()=>{if(lastValue!=_refine(inputField).value){lastValue=_refine(inputField).value;onChange(inputField,lastValue)}}}});this.parentElement.append(createGridElement(titleText,isLongField,(node,title,suffix)=>{node.append(inputNode);onInit?.(inputNode,title,suffix)}));return inputNode}addInputGrid(id,titleText,isLongField,{defaultValue:defaultValue=undefined,onInit:onInit=undefined,onChange:onChange=undefined}={}){this.constructInputGrid(id,titleText,isLongField,{defaultValue:defaultValue,onInit:onInit,onChange:onChange});return this}constructTextAreaGrid(id,titleText,{defaultValue:defaultValue=undefined,onInit:onInit=undefined,onChange:onChange=undefined}={}){let textNode=setupClassNode("textarea","decentral-text-area decentral-modifiable-component",area=>{area.id=id;defaultValue&&(_refine(area).value=defaultValue);if(onChange){let lastValue=_refine(area).value;area.onchange=()=>{onChange(area,_refine(area).value)};_refine(area).onVerifyChange=()=>{if(lastValue!=_refine(area).value){lastValue=_refine(area).value;onChange(area,lastValue)}}}});this.parentElement.append(createGridElement(titleText,true,(node,title,suffix)=>{node.append(textNode);onInit?.(textNode,title,suffix)}));return textNode}constructBoxedTextAreaGrid(id,titleText,description,{defaultValue:defaultValue=undefined,onInit:onInit=undefined,onChange:onChange=undefined}={}){let textNode=setupClassNode("textarea","decentral-text-area decentral-modifiable-component",area=>{area.id=id;defaultValue&&(_refine(area).value=defaultValue);if(onChange){let lastValue=_refine(area).value;area.onchange=()=>{onChange(area,_refine(area).value)};_refine(area).onVerifyChange=()=>{if(lastValue!=_refine(area).value){lastValue=_refine(area).value;onChange(area,lastValue)}}}});let topNode=this.constructLongBoxedField(titleText,description,{onInit:node=>{node.append(textNode)}});this.parentElement.append(topNode);onInit?.(textNode,topNode);return textNode}addTextAreaGrid(id,titleText,{defaultValue:defaultValue=undefined,onInit:onInit=undefined,onChange:onChange=undefined}={}){this.constructTextAreaGrid(id,titleText,{defaultValue:defaultValue,onInit:onInit,onChange:onChange});return this}constructLoggingArea(id,titleText,{defaultValue:defaultValue=undefined,onInit:onInit=undefined}={}){let textNode=setupClassNode("textarea","decentral-logging-area",area=>{area.id=id;area.setAttribute("readonly","true");defaultValue&&(_refine(area).value=defaultValue)});this.parentElement.append(createGridElement(titleText,true,(node,title,suffix)=>{node.append(textNode);onInit?.(textNode,title,suffix)}));return textNode}addLoggingArea(id,titleText,{defaultValue:defaultValue=undefined,onInit:onInit=undefined}={}){this.constructLoggingArea(id,titleText,{defaultValue:defaultValue,onInit:onInit});return this}addBoxedButton(id,titleText,description,{onInit:onInit=undefined,onTrigger:onTrigger=undefined}={}){const buttonNode=setupClassNode("button","decentral-button",button=>{button.id=id;button.innerText=titleText;onInit?.(button);if(onTrigger){button.onclick=()=>{onTrigger(button)}}});this.addBoxedField(titleText,description,{onInit:node=>{node.append(buttonNode)}});return buttonNode}constructButton(id,titleText,short,{onInit:onInit=undefined,onTrigger:onTrigger=undefined}={}){let buttonNode=setupClassNode("button","decentral-button",button=>{button.id=id;button.innerText=titleText;onInit?.(button);if(onTrigger){button.onclick=()=>{onTrigger(button)}}});this.parentElement.append(createGridElement(null,short,node=>{node.append(buttonNode)}));return buttonNode}addShortButton(id,titleText,{onInit:onInit=undefined,onTrigger:onTrigger=undefined}={}){this.constructButton(id,titleText,true,{onInit:onInit,onTrigger:onTrigger});return this}addButton(id,titleText,{onInit:onInit=undefined,onTrigger:onTrigger=undefined}={}){this.constructButton(id,titleText,false,{onInit:onInit,onTrigger:onTrigger});return this}constructBoxedField(title,description,{onInit:onInit=undefined}={}){let topNode;this.parentElement.append(topNode=createLongSemiFlatGridElement(null,node=>{node.append(setupClassNode("div","decentral-boxed-field",area=>{area.append(setupClassNode("div","element-text-container",field=>{field.append(setupClassNode("p","element-title",text=>{text.innerText=title}));field.append(setupClassNode("p","element-description",text=>{text.innerText=description}))}));area.append(setupClassNode("div","element-input-container",onInit))}))}));return topNode}addBoxedField(title,description,{onInit:onInit=undefined}={}){this.parentElement.append(createLongSemiFlatGridElement(null,node=>{node.append(setupClassNode("div","decentral-boxed-field",area=>{area.append(setupClassNode("div","element-text-container",field=>{field.append(setupClassNode("p","element-title",text=>{text.innerText=title}));field.append(setupClassNode("p","element-description",text=>{text.innerText=description}))}));area.append(setupClassNode("div","element-input-container",container=>{onInit?.(container)}))}))}));return this}addLongBox(padded,{onInit:onInit=undefined}={}){this.parentElement.append(createLongSemiFlatGridElement(null,node=>{node.append(setupClassNode("div",padded?"decentral-padded-boxed-field":"decentral-boxed-field",onInit))}));return this}constructLongBoxedField(title,description,{onInit:onInit=undefined}={}){let topNode=createLongSemiFlatGridElement(null,node=>{node.append(setupClassNode("div","decentral-boxed-field",area=>{area.append(setupClassNode("div","element-text-container-full",field=>{field.append(setupClassNode("p","element-title",text=>{text.innerText=title}));field.append(setupClassNode("p","element-description",text=>{text.innerText=description}));field.append(setupClassNode("div","element-input-container-long",onInit))}))}))});this.parentElement.append(topNode);return topNode}constructSwitchBox(id,title,description,{defaultValue:defaultValue=false,onInit:onInit=undefined,onChange:onChange=undefined}={}){let inputNode=setupClassNode("input","element-switch decentral-modifiable-component",switcher=>{switcher.id=id;switcher.setAttribute("type","checkbox");switcher.setAttribute("role","switch");_refine(switcher).checked=defaultValue;onInit?.(switcher);if(onChange){let lastValue=_refine(switcher).checked;switcher.onchange=()=>{onChange(switcher,_refine(switcher).checked)};_refine(switcher).onVerifyChange=()=>{if(lastValue!=_refine(switcher).checked){lastValue=_refine(switcher).checked;onChange(switcher,lastValue)}}}});this.addBoxedField(title,description,{onInit:node=>{node.append(inputNode=setupClassNode("div","element-input-container",container=>{container.append(inputNode)}))}});return inputNode}addSwitchBox(id,title,description,{defaultValue:defaultValue=false,onInit:onInit=undefined,onChange:onChange=undefined}={}){this.constructSwitchBox(id,title,description,{defaultValue:defaultValue,onInit:onInit,onChange:onChange});return this}__addNumberBox(id,title,description,type,{defaultValue:defaultValue=0,min:min=0,max:max=1e3,onInit:onInit=undefined,onChange:onChange=undefined}={}){let inputNode=setupClassNode("div","element-input-container",container=>{container.append(setupClassNode("input",(type===0?"element-small-input":type===1?"element-medium-input":"element-large-input")+" decentral-modifiable-component",inputField=>{inputField.id=id;inputField.setAttribute("type","number");inputField.setAttribute("min",min.toString());inputField.setAttribute("max",max.toString());_refine(inputField).value=defaultValue??0;onInit?.(inputField);if(onChange){let lastValue=_refine(inputField).value;inputField.onchange=()=>{onChange(inputField,parseInt(_refine(inputField).value))};_refine(inputField).onVerifyChange=()=>{if(lastValue!=_refine(inputField).value){lastValue=_refine(inputField).value;onChange(inputField,lastValue)}}}}))});this.addBoxedField(title,description,{onInit:node=>{node.append(inputNode)}});return inputNode}constructShortNumberBox(id,title,description,{defaultValue:defaultValue=0,min:min=0,max:max=1e3,onInit:onInit=undefined,onChange:onChange=undefined}={}){return this.__addNumberBox(id,title,description,0,{defaultValue:defaultValue,min:min,max:max,onInit:onInit,onChange:onChange})}addShortNumberBox(id,title,description,{defaultValue:defaultValue=0,min:min=0,max:max=1e3,onInit:onInit=undefined,onChange:onChange=undefined}={}){this.constructShortNumberBox(id,title,description,{defaultValue:defaultValue,min:min,max:max,onInit:onInit,onChange:onChange});return this}constructMediumNumberBox(id,title,description,{defaultValue:defaultValue=0,min:min=0,max:max=1e3,onInit:onInit=undefined,onChange:onChange=undefined}={}){return this.__addNumberBox(id,title,description,1,{defaultValue:defaultValue,min:min,max:max,onInit:onInit,onChange:onChange})}addMediumNumberBox(id,title,description,{defaultValue:defaultValue=0,min:min=0,max:max=1e3,onInit:onInit=undefined,onChange:onChange=undefined}={}){this.constructMediumNumberBox(id,title,description,{defaultValue:defaultValue,min:min,max:max,onInit:onInit,onChange:onChange});return this}constructNumberBox(id,title,description,{defaultValue:defaultValue=0,min:min=0,max:max=1e3,onInit:onInit=undefined,onChange:onChange=undefined}={}){return this.__addNumberBox(id,title,description,2,{defaultValue:defaultValue,min:min,max:max,onInit:onInit,onChange:onChange})}addNumberBox(id,title,description,{defaultValue:defaultValue=0,min:min=0,max:max=1e3,onInit:onInit=undefined,onChange:onChange=undefined}={}){this.constructNumberBox(id,title,description,{defaultValue:defaultValue,min:min,max:max,onInit:onInit,onChange:onChange});return this}constructBoxedInputGrid(id,title,description,{defaultValue:defaultValue=undefined,onInit:onInit=undefined,onChange:onChange=undefined}={}){let inputNode=setupClassNode("input","decentral-text-field",inputField=>{inputField.id=id;inputField.setAttribute("type","text");defaultValue&&(_refine(inputField).value=defaultValue);if(onChange){inputField.onchange=()=>{onChange(inputField,_refine(inputField).value)}}});const topNode=this.constructLongBoxedField(title,description,{onInit:node=>{node.append(inputNode)}});onInit?.(inputNode,topNode);return inputNode}addBoxedInputGrid(id,title,description,{defaultValue:defaultValue=undefined,onInit:onInit=undefined,onChange:onChange=undefined}={}){this.constructBoxedInputGrid(id,title,description,{defaultValue:defaultValue,onInit:onInit,onChange:onChange});return this}createOuterClickDetection(lambda){return setupClassNode("div","decentral-outer-click-detection",node=>{node.id="decentral-outer-click-detection";node.onclick=event=>{event?.stopPropagation();event?.preventDefault();lambda?.()}})}removeOuterClickDetection(){const node=document.getElementById("decentral-outer-click-detection");if(node)node.remove()}hasOuterClickDetection(){if(document.getElementById("decentral-outer-click-detection")){return true}return false}triggerOuterClickDetection(){const node=document.getElementById("decentral-outer-click-detection");if(node){node.onclick?.(document.createEvent("PointerEvent"))}}constructSelectBox(titleText,initialText,initialId,isLong){let topNode=setupClassNode("ul","decentral-select");let optionContainer=setupClassNode("div","decentral-list");topNode.setAttribute("decentral-selected",initialId);const title=setupFullNode("div","decentral-option","width: 100%; display: flex; flex-direction: row; align-items: center;",option=>{option.append(setupStyleNode("span","height: fit-content;",textNode=>{textNode.textContent=initialText}));option.append(setupStyleNode("div","margin-left: auto;",iconNode=>{iconNode.append(setupNode("div",node=>{node.innerHTML=DECENTRAL_ARROW_ICON_SVG}))}));option.onclick=event=>{if(this.hasOuterClickDetection()){event.preventDefault();event.stopPropagation();this.triggerOuterClickDetection();return}if(topNode.hasAttribute("list-enabled")){topNode.removeAttribute("list-enabled")}else{topNode.setAttribute("list-enabled","true");optionContainer.style.cssText=`top: ${topNode.getBoundingClientRect().top+topNode.clientHeight}px;`;this.parentElement?.parentElement?.parentElement?.parentElement?.append(this.createOuterClickDetection(()=>{topNode.removeAttribute("list-enabled");this.removeOuterClickDetection()}))}}});topNode.append(title);topNode.append(optionContainer);this.addGrid(titleText,isLong,node=>{node.append(topNode)});topNode.setAttribute("decentral-selected",initialId);return{node:topNode,addOption:(text,id,onclick)=>{const element=setupClassNode("div","decentral-option",option=>{option.textContent=text;option.setAttribute("decentral-option-text",text);option.setAttribute("decentral-option-id",id);option.onclick=()=>{this.removeOuterClickDetection();topNode.removeAttribute("list-enabled");const selectedId=option.getAttribute("decentral-option-id");if(selectedId&&onclick?.(selectedId,option)){topNode.setAttribute("decentral-selected",selectedId);title.childNodes[0].textContent=option.getAttribute("decentral-option-text")}}});optionContainer.append(element);return element},clear:()=>{topNode.getElementsByClassName("decentral-list")[0].innerHTML=""},runSelected:()=>{for(const element of topNode.getElementsByClassName("decentral-option")){if(element.getAttribute("decentral-option-id")===topNode.getAttribute("decentral-selected")){_refine(element)?.onclick?.()}}},listGroup:()=>Array.from(topNode.getElementsByClassName("decentral-option")).map(it=>it.getAttribute("decentral-selected")).filter(it=>it!=undefined),setSelected:target=>{if(typeof target==="string"){topNode.setAttribute("decentral-selected",target)}else if(target instanceof Element&&target.hasAttribute("decentral-option-id")){const attribute=assert(target.getAttribute("decentral-option-id"));topNode.setAttribute("decentral-selected",attribute)}},getSelected:()=>topNode.getAttribute("decentral-selected"),findSelected:()=>{for(const element of topNode.getElementsByClassName("decentral-option")){if(element.getAttribute("decentral-option-id")===topNode.getAttribute("decentral-selected")){return element}}return undefined},appendTo:node=>{node.append(topNode)},findGroup:groupId=>{for(const element of topNode.getElementsByClassName("decentral-option")){if(element.getAttribute("decentral-option-id")===groupId){return element}}return undefined},addGroup:(text,lambda)=>{const node=setupClassNode("div","decentral-option-group",group=>{group.innerText=text});optionContainer.append(node);lambda&&lambda(node)}}}}class ContentPanel extends ComponentAppender{__verticalContainer=setupClassNode("div","decentral-vertical-container",()=>{});__footerGrid=setupClassNode("div","decentral-grid-container",()=>{});__footer=setupClassNode("div","decentral-modal-footer",node=>{node.append(this.__footerGrid)});__footerAppender=new ComponentAppender(this.__footerGrid);constructor(id,title,svg,mobileOpenAction,closeAction){super(setupClassNode("div","decentral-grid",()=>{}));this.__element=this.parentElement;this.__verticalContainer.id=id;const icon=setupClassNode("div","decentral-modal-title-icon",node=>{});const gridWrapper=setupClassNode("div","decentral-grid-container",()=>{});this.__verticalContainer.append(setupClassNode("div","decentral-modal-title-container",node=>{node.append(icon);node.append(setupClassNode("p","decental-modal-title-text",node=>{node.innerText=title}));node.append(setupClassNode("div","decentral-modal-button-container",node=>{node.append(setupClassNode("div","decentral-mobile-menu-button",svgNode=>{svgNode.innerHTML=DECENTRAL_MENU_ICON_SVG;svgNode.id=`${id}-menu`;mobileOpenAction&&(svgNode.onclick=mobileOpenAction)}));node.append(setupClassNode("div","decentral-close-button",svgNode=>{svgNode.innerHTML=DECENTRAL_CLOSE_ICON_SVG;svgNode.id=`${id}-close`;closeAction&&(svgNode.onclick=closeAction)}))}))}));svg&&(icon.outerHTML=svg);icon.classList.add("decentral-modal-title-icon");gridWrapper.append(this.__element);this.__verticalContainer.append(gridWrapper);this.__verticalContainer.append(this.__footer)}footer(useVerticalFooter){if(useVerticalFooter){this.__footerGrid.classList.add("decentral-vertical-container")}return this.__footerAppender}runModifyVerification(){const foundElements=this.__verticalContainer.getElementsByClassName("decentral-modifiable-component");for(const element of foundElements){if("onVerifyChange"in element){element.onVerifyChange()}}}asHTML(){return this.__verticalContainer}}function _refine(element){return element}function _clone(element){return _refine(element.cloneNode(true))}function assert(item){if(item===undefined||item===null){throw new Error("ASSERTION FAILED; null or undefined object detected at non-null logic")}return item}function setupParagraphNode(text,setupLambda){const node=document.createElement("p");node.textContent=text;setupLambda?.(node);return node}function setupOptionNode(text,setupLambda){const node=document.createElement("option");text&&(node.textContent=text);setupLambda?.(node);return node}function setupClassOptionNode(text,cls,setupLambda){const node=document.createElement("li");text&&(node.textContent=text);cls&&(node.className=cls);setupLambda?.(node);return node}function setupFullOptionNode(text,cls,style,setupLambda){const node=document.createElement("option");node.textContent=text;cls&&(node.className=cls);style&&(node.style.cssText=style);setupLambda?.(node);return node}function setupNode(name,setupLambda=undefined){const node=document.createElement(name);setupLambda?.(node);return node}function setupClassNode(name,cls,setupLambda){const node=document.createElement(name);cls&&(node.className=cls);setupLambda?.(node);return node}function setupStyleNode(name,style,setupLambda){const node=document.createElement(name);style&&(node.style.cssText=style);setupLambda?.(node);return node}function setupFullNode(tagName,cls,style,setupLambda){const node=document.createElement(tagName);cls&&(node.className=cls);style&&(node.style.cssText=style);setupLambda?.(node);return node}function createGridElement(titleText,isLongField,lambda){return setupClassNode("div",isLongField?"decentral-grid-element-long":"decentral-grid-element",node=>{if(titleText){const suffix=setupClassNode("div","decentral-element-title-suffix");const title=setupClassNode("div","decentral-element-title",elementTitle=>{elementTitle.append(setupNode("p",node=>{node.textContent=titleText}));elementTitle.append(suffix)});node.append(title);lambda&&lambda(node,title,suffix)}else{lambda&&lambda(node,undefined,undefined)}})}function createLongFlatGridElement(titleText,lambda){return setupClassNode("div","decentral-grid-element-long-flat",node=>{if(titleText){const suffix=setupClassNode("div","decentral-element-title-suffix");const title=setupClassNode("div","decentral-element-title",elementTitle=>{elementTitle.append(setupNode("p",node=>{node.textContent=titleText}));elementTitle.append(suffix)});node.append(title);lambda?.(node,title,suffix)}else{lambda?.(node,undefined)}})}function createLongSemiFlatGridElement(titleText,lambda){return setupClassNode("div","decentral-grid-element-long-semi-flat",node=>{if(titleText){const suffix=setupClassNode("div","decentral-element-title-suffix");const title=setupClassNode("div","decentral-element-title",elementTitle=>{elementTitle.append(setupNode("p",node=>{node.textContent=titleText}));elementTitle.append(suffix)});node.append(title);lambda?.(node,title,suffix)}else{lambda?.(node)}})}(function(){"use strict";const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const C=_w.__LoreCore=_w.__LoreCore||{};if(C.__uiLoaded)return;var _statusBadge=null;var _pulseStyleAdded=false;var _statusLabel=null;var _statusDot=null;var _statusAutoHideTimer=null;function showStatusBadge(text){if(_w.__LoreInj&&_w.__LoreInj.settings&&_w.__LoreInj.settings.config&&_w.__LoreInj.settings.config.statusBadgeEnabled===false){hideStatusBadge();return}if(!_pulseStyleAdded){try{var head=document.head||document.getElementsByTagName("head")[0];if(head){var style=document.createElement("style");style.id="lore-pulse-style";style.textContent="@keyframes lore-pulse{0%,100%{opacity:1}50%{opacity:.3}}";head.appendChild(style);_pulseStyleAdded=true}}catch(_){}}if(!_statusBadge){_statusBadge=document.createElement("div");_statusBadge.id="lore-status-badge";_statusBadge.style.cssText="position:fixed;bottom:calc(env(safe-area-inset-bottom, 0px) + 118px);right:20px;z-index:2147483646;background:#1a1a1a;border:1px solid #333;border-radius:20px;padding:8px 16px;font-size:12px;color:#ccc;box-shadow:0 4px 12px rgba(0,0,0,0.4);display:flex;align-items:center;gap:8px;font-family:inherit;transition:opacity .3s;opacity:0;pointer-events:none;";_statusDot=document.createElement("span");_statusDot.style.cssText="display:inline-block;width:8px;height:8px;border-radius:50%;background:#4a9;animation:lore-pulse 1s infinite;flex-shrink:0;";_statusLabel=document.createElement("span");_statusBadge.appendChild(_statusDot);_statusBadge.appendChild(_statusLabel)}var mountTarget=document.body||document.documentElement;if(mountTarget&&_statusBadge.parentNode!==mountTarget){try{mountTarget.appendChild(_statusBadge)}catch(_){}}if(_statusLabel&&_statusLabel.textContent!==text)_statusLabel.textContent=text;if(_statusBadge.style.opacity!=="1")_statusBadge.style.opacity="1";if(_statusBadge.style.pointerEvents!=="auto")_statusBadge.style.pointerEvents="auto";if(_statusAutoHideTimer){clearTimeout(_statusAutoHideTimer);_statusAutoHideTimer=null}if(text==="에리가 응답 기다리는 중"){_statusAutoHideTimer=setTimeout(function(){if(_statusLabel&&_statusLabel.textContent==="에리가 응답 기다리는 중")hideStatusBadge()},45e3)}if(_statusDot&&!_statusDot.__pulseTimer){var __on=true;_statusDot.__pulseTimer=setInterval(function(){__on=!__on;try{_statusDot.style.opacity=__on?"1":"0.35"}catch(_){}},500)}}function hideStatusBadge(){if(_statusAutoHideTimer){clearTimeout(_statusAutoHideTimer);_statusAutoHideTimer=null}if(_statusBadge){_statusBadge.style.opacity="0";_statusBadge.style.pointerEvents="none"}if(_statusDot&&_statusDot.__pulseTimer){try{clearInterval(_statusDot.__pulseTimer)}catch(_){}_statusDot.__pulseTimer=null;try{_statusDot.style.opacity="1"}catch(_){}}}function setFullWidth(node){const p=node.parentElement;if(p){p.style.display="block";p.style.padding="0";p.style.border="none";p.style.background="transparent";Array.from(p.children).forEach(c=>{if(c!==node)c.style.display="none"})}node.style.cssText="width:100%;display:block;padding:10px 14px;box-sizing:border-box;background:transparent;border:none;margin-bottom:12px;";node.innerHTML=""}function createToggleRow(title,desc,isChecked,onChange){const wrap=document.createElement("div");wrap.style.cssText="display:flex;justify-content:space-between;align-items:center;gap:10px;width:100%;margin-bottom:8px;";const left=document.createElement("div");left.style.cssText="display:flex;flex-direction:column;gap:4px;flex:1;";const t=document.createElement("div");t.textContent=title;t.style.cssText="font-size:13px;color:#ccc;font-weight:bold;";const d=document.createElement("div");d.textContent=desc;d.style.cssText="font-size:11px;color:#888;line-height:1.4;word-break:keep-all;";left.appendChild(t);left.appendChild(d);const right=document.createElement("div");right.style.cssText="display:flex;align-items:center;gap:8px;";const swLabel=document.createElement("span");swLabel.textContent=isChecked?"ON":"OFF";swLabel.style.cssText="font-size:12px;color:#ccc;font-weight:bold;width:22px;text-align:center;";const sw=document.createElement("div");sw.style.cssText=`width:36px;height:20px;border-radius:10px;cursor:pointer;background:${isChecked?"#285":"#444"};position:relative;flex-shrink:0;`;const dot=document.createElement("div");dot.style.cssText=`width:16px;height:16px;border-radius:50%;background:#fff;position:absolute;top:2px;left:${isChecked?"18px":"2px"};transition:left .2s;`;sw.appendChild(dot);sw.onclick=()=>{isChecked=!isChecked;onChange(isChecked);swLabel.textContent=isChecked?"ON":"OFF";sw.style.background=isChecked?"#285":"#444";dot.style.left=isChecked?"18px":"2px"};right.appendChild(swLabel);right.appendChild(sw);wrap.appendChild(left);wrap.appendChild(right);return wrap}function createApiInput(config,prefix,nd,onChange){const triggerSave=()=>{if(typeof onChange==="function")onChange()};const apiTypeKey=prefix+"ApiType";const keyKey=prefix==="gemini"?"geminiKey":prefix+"Key";const jsonKey=prefix+"VertexJson";const locKey=prefix+"VertexLocation";const projKey=prefix+"VertexProjectId";const fbScriptKey=prefix+"FirebaseScript";const fbEmbKey=prefix+"FirebaseEmbedKey";const S="width:100%;padding:6px 8px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:12px;box-sizing:border-box;";const typeRow=document.createElement("div");typeRow.style.cssText="display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap;";const btnKey=document.createElement("button");const btnVertex=document.createElement("button");const btnFirebase=document.createElement("button");const keyArea=document.createElement("div");const vertexArea=document.createElement("div");const firebaseArea=document.createElement("div");const curMode=()=>config[apiTypeKey]||"key";const sty=on=>`padding:6px 12px;font-size:12px;border-radius:4px;cursor:pointer;border:1px solid ${on?"#285":"#444"};background:${on?"#285":"transparent"};color:${on?"#fff":"#ccc"};`;const updateBtns=()=>{const m=curMode();btnKey.style.cssText=sty(m==="key");btnVertex.style.cssText=sty(m==="vertex");btnFirebase.style.cssText=sty(m==="firebase");keyArea.style.display=m==="key"?"":"none";vertexArea.style.display=m==="vertex"?"":"none";firebaseArea.style.display=m==="firebase"?"":"none"};btnKey.textContent="API Key";btnVertex.textContent="Vertex AI (JSON)";btnFirebase.textContent="Firebase";btnKey.onclick=()=>{config[apiTypeKey]="key";updateBtns();triggerSave()};btnVertex.onclick=()=>{config[apiTypeKey]="vertex";updateBtns();triggerSave()};btnFirebase.onclick=()=>{config[apiTypeKey]="firebase";updateBtns();triggerSave()};typeRow.appendChild(btnKey);typeRow.appendChild(btnVertex);typeRow.appendChild(btnFirebase);nd.appendChild(typeRow);const ki=document.createElement("input");ki.type="text";ki.value=config[keyKey]||"";ki.placeholder="AIzaSy...";ki.setAttribute("autocomplete","off");ki.style.cssText=S+"-webkit-text-security:disc;";ki.onchange=()=>{const val=ki.value.trim();if(val.startsWith("{")&&val.includes("client_email")){config[apiTypeKey]="vertex";config[jsonKey]=val;ki.value="";updateBtns();triggerSave();return}config[keyKey]=val;triggerSave()};keyArea.appendChild(ki);nd.appendChild(keyArea);const jta=document.createElement("textarea");jta.value=config[jsonKey]||"";jta.placeholder='{ "type": "service_account", ... }';jta.style.cssText=S+"height:100px;font-family:monospace;resize:vertical;";jta.onchange=()=>{config[jsonKey]=jta.value;triggerSave()};vertexArea.appendChild(jta);const locRow=document.createElement("div");locRow.style.cssText="display:flex;gap:12px;margin-top:8px;";const locInput=document.createElement("input");locInput.value=config[locKey]||"global";locInput.placeholder="Location";locInput.style.cssText=S;locInput.onchange=()=>{config[locKey]=locInput.value||"global";triggerSave()};const projInput=document.createElement("input");projInput.value=config[projKey]||"";projInput.placeholder="Project ID";projInput.style.cssText=S;projInput.onchange=()=>{config[projKey]=projInput.value;triggerSave()};const ld=document.createElement("div");ld.style.flex="1";ld.appendChild(locInput);const pd=document.createElement("div");pd.style.flex="1";pd.appendChild(projInput);locRow.appendChild(ld);locRow.appendChild(pd);vertexArea.appendChild(locRow);nd.appendChild(vertexArea);const fbNote=document.createElement("div");fbNote.textContent="Firebase SDK. Firebase 콘솔 > 프로젝트 설정 > 웹 앱의 firebaseConfig = {...} 덩어리를 그대로 붙여넣으면 됨";fbNote.style.cssText="font-size:11px;color:#888;margin-bottom:6px;line-height:1.4;";firebaseArea.appendChild(fbNote);const fbTa=document.createElement("textarea");fbTa.value=config[fbScriptKey]||"";fbTa.placeholder='const firebaseConfig = {\n apiKey: "...",\n projectId: "...",\n ...\n};';fbTa.style.cssText=S+"height:120px;font-family:monospace;resize:vertical;margin-bottom:8px;";fbTa.onchange=()=>{config[fbScriptKey]=fbTa.value;triggerSave()};firebaseArea.appendChild(fbTa);const fbEmbNote=document.createElement("div");fbEmbNote.textContent="의미 검색용 Gemini API Key. Firebase 방식은 검색 준비용 별도 키 필요. Google AI Studio에서 무료 키 발급 가능.";fbEmbNote.style.cssText="font-size:11px;color:#888;margin-bottom:4px;line-height:1.4;";firebaseArea.appendChild(fbEmbNote);const fbEmbInput=document.createElement("input");fbEmbInput.type="text";fbEmbInput.value=config[fbEmbKey]||"";fbEmbInput.placeholder="AIzaSy...";fbEmbInput.setAttribute("autocomplete","off");fbEmbInput.style.cssText=S+"-webkit-text-security:disc;";fbEmbInput.onchange=()=>{config[fbEmbKey]=fbEmbInput.value.trim();triggerSave()};firebaseArea.appendChild(fbEmbInput);nd.appendChild(firebaseArea);updateBtns()}Object.assign(C,{showStatusBadge:showStatusBadge,hideStatusBadge:hideStatusBadge,setFullWidth:setFullWidth,createToggleRow:createToggleRow,createApiInput:createApiInput,__uiLoaded:true});console.log("[LoreCore:ui] loaded")})();(function(){"use strict";const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;if(_w.__LoreCore&&_w.__LoreCore.__kernelLoaded)return;const VER="1.4.0-test";const DB_SCHEMA_VERSION=9;const LOCAL_MIGRATION_VERSION="1.4.0-test-pass11-local";const TIMELINE_EVENT_TYPE="timeline_event";const TIMELINE_SCHEMA_VERSION=1;const TIMELINE_COMPRESSION_LEVELS={full:"full",compact:"compact",micro:"micro"};const _gHost="generativelanguage.googleapis.com";const _gBase="https://"+_gHost+"/v1beta/models/";const SAFETY=[{category:"HARM_CATEGORY_HARASSMENT",threshold:"BLOCK_NONE"},{category:"HARM_CATEGORY_HATE_SPEECH",threshold:"BLOCK_NONE"},{category:"HARM_CATEGORY_SEXUALLY_EXPLICIT",threshold:"BLOCK_NONE"},{category:"HARM_CATEGORY_DANGEROUS_CONTENT",threshold:"BLOCK_NONE"},{category:"HARM_CATEGORY_CIVIC_INTEGRITY",threshold:"BLOCK_NONE"}];const PLATFORM={contextTokens:5500,recentTurnsSafe:4,summaryRefreshTurns:15,inputCharLimit:2e3,outputTokens:800,avgUserInputChars:80};const DEFAULTS={loreBudgetChars:350,loreBudgetMax:600,sceneTagChars:90,firstEncounterChars:240,reunionTagChars:140,targetCharsPerEntry:{full:140,compact:70,micro:35},autoCompression:false,scanRange:4,scanOffset:2,maxEntries:4,strictMatch:true,similarityMatch:true,decayEnabled:true,decayHalfLife:{identity:15,character:15,relationship:8,first_encounter:8,promise:5,event:4,scene:2,default:6},aiMemoryTurns:3,embeddingEnabled:false,embeddingModel:"gemini-embedding-001",embeddingDimensions:768,embeddingTaskType:"RETRIEVAL_DOCUMENT",embeddingWeight:.4,temporalGraphEnabled:true,temporalWeight:.18,activeEntityWeight:.25,relationshipGraphWeight:.22,unresolvedWeight:.28,maintenanceWeight:.12,temporalHintChars:180,temporalEventMaxTriggers:12,temporalEventMaxHooks:8,temporalEventMaxLinkedLore:12,temporalEventDefaultCompression:"compact",activeCharDetection:true,activeCharBoost:3,inactiveCharPenalty:.15,workingMemoryEnabled:true,workingMemoryChars:60,honorificMatrixChars:80,autoExtTurns:8,autoExtScanRange:4,autoExtOffset:2,autoExtMaxRetries:1,prefix:"**OOC:Lore",suffix:"**",position:"before",importChunkSize:3e3,importMaxEntries:50,rerankPrompt:`Given the current RP conversation context, score each lore entry for INJECTION PRIORITY (1-5).\n\nScoring criteria:\n5 = Directly referenced or contradicted in current scene\n4 = Active character/location in current scene\n3 = Related to active relationship or pending promise\n2 = Background info that adds depth\n1 = Not relevant to current scene\n\nRules:\n- Pending promises near trigger conditions → boost to 4-5\n- Inactive characters not in scene → cap at 2\n- Return JSON array: [{"i":,"s":}]\n- Sort by score descending\n- Omit entries scoring 1\n\nContext: "{context}"\nQuery: "{query}"\n\nEntries:\n{candidates}`};let _db=null;function getDB(){if(_db)return _db;_db=new Dexie("lore-injector");_db.version(1).stores({entries:"++id, name, type, packName, *triggers",packs:"name, entryCount"});_db.version(2).stores({entries:"++id, name, type, packName, project, *triggers",packs:"name, entryCount, project"});_db.version(3).stores({entries:"++id, name, type, packName, project, *triggers",packs:"name, entryCount, project",snapshots:"++id, packName, timestamp, type"});_db.version(4).stores({entries:"++id, name, type, packName, project, *triggers",packs:"name, entryCount, project",snapshots:"++id, packName, timestamp, type",embeddings:"++id, entryId, model, &[entryId+field]",workingMemory:"url",encounters:"++id, &[char1+char2]"});_db.version(5).stores({entries:"++id, name, type, packName, project, *triggers",packs:"name, entryCount, project",snapshots:"++id, packName, timestamp, type",embeddings:"++id, entryId, model, &[entryId+field]",workingMemory:"url",encounters:"++id, &[char1+char2], lastSeenTurn"});_db.version(6).stores({entries:"++id, name, type, packName, project, rootId, isCurrentArc, *triggers",packs:"name, entryCount, project",snapshots:"++id, packName, timestamp, type",embeddings:"++id, entryId, model, &[entryId+field]",workingMemory:"url",encounters:"++id, &[char1+char2], lastSeenTurn"});_db.version(7).stores({entries:"++id, name, type, packName, project, rootId, isCurrentArc, *triggers",packs:"name, entryCount, project",snapshots:"++id, packName, timestamp, type",embeddings:"++id, entryId, model, &[entryId+field]",workingMemory:"url",encounters:"++id, &[char1+char2], lastSeenTurn",entryVersions:"++id, entryId, ts, turn"});_db.version(8).stores({entries:"++id, name, type, packName, project, rootId, isCurrentArc, *triggers",packs:"name, entryCount, project",snapshots:"++id, packName, timestamp, type",embeddings:"++id, entryId, packName, model, field, sourceHash, entryUpdatedAt, schemaVersion, &[entryId+field]",workingMemory:"url",encounters:"++id, &[char1+char2], lastSeenTurn",entryVersions:"++id, entryId, ts, turn"});_db.version(9).stores({entries:"++id, name, type, packName, project, rootId, isCurrentArc, createdTurn, updatedTurn, lastMentionedTurn, eventTurn, sceneId, arcId, realTimestamp, *entities, *subjects, *objects, *locations, *promises, *triggers",packs:"name, entryCount, project",snapshots:"++id, packName, timestamp, type",embeddings:"++id, entryId, packName, model, field, sourceHash, entryUpdatedAt, schemaVersion, &[entryId+field]",workingMemory:"url",encounters:"++id, &[char1+char2], lastSeenTurn",entryVersions:"++id, entryId, ts, turn"});return _db}const _GM_xhr=typeof GM_xmlhttpRequest!=="undefined"?GM_xmlhttpRequest:typeof GM!=="undefined"&&GM.xmlHttpRequest?GM.xmlHttpRequest.bind(GM):null;function gmFetch(url,opts){const signal=opts&&opts.signal;if(!_GM_xhr){return fetch(url,{method:opts.method||"GET",headers:opts.headers||{},body:opts.body||null,signal:signal||undefined})}return new Promise((resolve,reject)=>{if(signal&&signal.aborted){reject(new Error("aborted"));return}let xhrHandle=null;let cleanedUp=false;const cleanup=()=>{if(cleanedUp)return;cleanedUp=true;if(signal){try{signal.removeEventListener("abort",onAbort)}catch(_){}}};const onAbort=()=>{try{xhrHandle&&xhrHandle.abort&&xhrHandle.abort()}catch(_){}cleanup();reject(new Error("aborted"))};if(signal){try{signal.addEventListener("abort",onAbort,{once:true})}catch(_){}}xhrHandle=_GM_xhr({method:opts.method||"GET",url:url,headers:opts.headers||{},data:opts.body||null,responseType:"text",onload:r=>{cleanup();resolve({ok:r.status>=200&&r.status<300,status:r.status,text:()=>Promise.resolve(r.responseText),json:()=>Promise.resolve(JSON.parse(r.responseText))})},onerror:()=>{cleanup();reject(new Error("네트워크 오류"))},ontimeout:()=>{cleanup();reject(new Error("타임아웃"))},onabort:()=>{cleanup();reject(new Error("aborted"))}})})}function parseServiceAccountJson(jsonStr){try{const obj=JSON.parse(jsonStr);if(!obj.client_email||!obj.private_key)return{ok:false,error:"client_email 또는 private_key 누락"};return{ok:true,projectId:obj.project_id||"",clientEmail:obj.client_email,privateKey:obj.private_key,tokenUri:obj.token_uri||"https://oauth2.googleapis.com/token"}}catch(e){return{ok:false,error:"JSON 파싱 실패"}}}function pemToArrayBuffer(pem){const b64=pem.replace(/-----[A-Z ]+-----/g,"").replace(/[\r\n\s]/g,"");const bin=atob(b64);const buf=new Uint8Array(bin.length);for(let i=0;inow+60)return cache.token;const header=b64url(JSON.stringify({alg:"RS256",typ:"JWT"}));const payload=b64url(JSON.stringify({iss:sa.clientEmail,sub:sa.clientEmail,aud:sa.tokenUri,iat:now,exp:now+3600,scope:"https://www.googleapis.com/auth/cloud-platform"}));const signingInput=header+"."+payload;const cryptoKey=await crypto.subtle.importKey("pkcs8",pemToArrayBuffer(sa.privateKey),{name:"RSASSA-PKCS1-v1_5",hash:"SHA-256"},false,["sign"]);const sigBuf=await crypto.subtle.sign("RSASSA-PKCS1-v1_5",cryptoKey,(new TextEncoder).encode(signingInput));const jwt=signingInput+"."+b64url(sigBuf);const resp=await gmFetch(sa.tokenUri,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${jwt}`});if(!resp.ok)throw new Error("토큰 교환 실패");const data=await resp.json();cache.token=data.access_token;cache.expiry=now+3600;return cache.token}let _fbSdkPromise=null;const _fbAppCache=Object.create(null);const _fbAiCache=Object.create(null);const _fbModelCache=Object.create(null);function loadFirebaseSdk(){if(_w.__crackExtFirebaseSdk)return Promise.resolve(_w.__crackExtFirebaseSdk);if(_fbSdkPromise)return _fbSdkPromise;_fbSdkPromise=new Promise((resolve,reject)=>{const to=setTimeout(()=>{_fbSdkPromise=null;reject(new Error("Firebase SDK 로드 타임아웃"))},2e4);_w.addEventListener("crack-ext-fbsdk-ready",()=>{clearTimeout(to);resolve(_w.__crackExtFirebaseSdk)},{once:true});const script=document.createElement("script");script.type="module";script.textContent='import { initializeApp } from "https://www.gstatic.com/firebasejs/12.8.0/firebase-app.js";\nimport { getAI, getGenerativeModel, GoogleAIBackend, VertexAIBackend, HarmBlockThreshold, HarmCategory } from "https://www.gstatic.com/firebasejs/12.8.0/firebase-ai.js";\nwindow.__crackExtFirebaseSdk = { initializeApp, getAI, getGenerativeModel, GoogleAIBackend, VertexAIBackend, HarmBlockThreshold, HarmCategory };\nwindow.dispatchEvent(new CustomEvent("crack-ext-fbsdk-ready"));';script.onerror=()=>{clearTimeout(to);_fbSdkPromise=null;reject(new Error("Firebase SDK 스크립트 로드 실패"))};(document.head||document.documentElement).appendChild(script)});return _fbSdkPromise}function extractBalancedObjectLiteral(src,startIdx){const open=src.indexOf("{",startIdx);if(open<0)return"";let depth=0,quote="",esc=false;for(let i=open;i=0){const obj=extractBalancedObjectLiteral(t,assign);if(obj)return new Function("return "+obj)()}const init=t.search(/initializeApp\s*\(/i);if(init>=0){const obj=extractBalancedObjectLiteral(t,init);if(obj)return new Function("return "+obj)()}const firstObj=extractBalancedObjectLiteral(t,0);if(firstObj&&/apiKey|projectId/i.test(firstObj))return new Function("return "+firstObj)()}catch(e){}return null}async function warmupFirebase(firebaseScript,model="gemini-3-flash-preview"){try{const cfg=parseFirebaseConfig(firebaseScript);if(!cfg||!cfg.apiKey||!cfg.projectId)return false;const sdk=await loadFirebaseSdk();const fb_is3x=model.includes("gemini-3")||model.includes("gemini-2.0-flash-thinking");const fb_loc=fb_is3x?"global":"us-central1";const appName="crack-ext-"+simpleHash(cfg.apiKey+":"+cfg.projectId);if(!_fbAppCache[appName])_fbAppCache[appName]=sdk.initializeApp(cfg,appName);const aiKey=appName+"|"+fb_loc;if(!_fbAiCache[aiKey])_fbAiCache[aiKey]=sdk.getAI(_fbAppCache[appName],{backend:new sdk.VertexAIBackend(fb_loc)});return true}catch(e){return false}}async function callGeminiApi(prompt,opts={}){const{apiType:apiType="key",key:key="",vertexJson:vertexJson="",vertexLocation:vertexLocation="global",vertexProjectId:vertexProjectId="",firebaseScript:firebaseScript="",firebaseKey:firebaseKey="",firebaseProjectId:firebaseProjectId="",firebaseLocation:firebaseLocation="global",model:model="gemini-3-flash-preview",thinkingConfig:thinkingConfig={},maxRetries:maxRetries=1,responseMimeType:responseMimeType,cacheKey:cacheKey="generate",costContext:costContext=null,signal:signal=null,timeoutMs:timeoutMs=9e4,maxOutputTokens:maxOutputTokens=null}=opts;const _trackCost=(usageMeta,promptText,outText)=>{const core=_w.__LoreCore;if(!core||typeof core.recordApiCost!=="function")return null;const ctx=costContext||{feature:"unknown",chatKey:"global"};try{let inTok,outTok,estimated=false;if(usageMeta&&(usageMeta.promptTokenCount!=null||usageMeta.candidatesTokenCount!=null)){inTok=Number(usageMeta.promptTokenCount)||0;outTok=(Number(usageMeta.candidatesTokenCount)||0)+(Number(usageMeta.thoughtsTokenCount)||0)}else{inTok=Math.ceil(String(promptText||"").length/4);outTok=Math.ceil(String(outText||"").length/4);estimated=true}return core.recordApiCost({chatKey:ctx.chatKey||"global",feature:ctx.feature||"unknown",model:model,inTok:inTok,outTok:outTok,estimated:estimated})}catch(_){return null}};const isVertex=apiType==="vertex";const isFirebase=apiType==="firebase";let url,headers;if(isFirebase){let cfg=parseFirebaseConfig(firebaseScript);if(!cfg||!cfg.apiKey||!cfg.projectId){const liveScript=_w.__LoreInj&&_w.__LoreInj.settings&&_w.__LoreInj.settings.config?_w.__LoreInj.settings.config.autoExtFirebaseScript:"";if(liveScript&&liveScript!==firebaseScript)cfg=parseFirebaseConfig(liveScript)}if(!cfg||!cfg.apiKey||!cfg.projectId){const len=String(firebaseScript||"").length;const head=String(firebaseScript||"").trim().slice(0,40).replace(/\s+/g," ");return{text:null,status:0,error:"Firebase 스크립트 형식 오류 (firebaseConfig = {...} 형태 붙여넣기 필요; len="+len+", head="+head+")",retries:0}}if(signal&&signal.aborted)return{text:null,status:0,error:"aborted",retries:0};try{const sdk=await loadFirebaseSdk();const fb_is3x=model.includes("gemini-3")||model.includes("gemini-2.0-flash-thinking");const fb_loc=fb_is3x?"global":"us-central1";const fbGenConfig={};if(Object.keys(thinkingConfig).length>0)fbGenConfig.thinkingConfig=thinkingConfig;if(responseMimeType)fbGenConfig.responseMimeType=responseMimeType;if(maxOutputTokens!=null)fbGenConfig.maxOutputTokens=maxOutputTokens;const appName="crack-ext-"+simpleHash(cfg.apiKey+":"+cfg.projectId);let app=_fbAppCache[appName];if(!app){app=sdk.initializeApp(cfg,appName);_fbAppCache[appName]=app}const aiKey=appName+"|"+fb_loc;let ai=_fbAiCache[aiKey];if(!ai){ai=sdk.getAI(app,{backend:new sdk.VertexAIBackend(fb_loc)});_fbAiCache[aiKey]=ai}const fbSafety=[{category:sdk.HarmCategory.HARM_CATEGORY_HATE_SPEECH,threshold:sdk.HarmBlockThreshold.OFF},{category:sdk.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,threshold:sdk.HarmBlockThreshold.OFF},{category:sdk.HarmCategory.HARM_CATEGORY_HARASSMENT,threshold:sdk.HarmBlockThreshold.OFF},{category:sdk.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,threshold:sdk.HarmBlockThreshold.OFF}];const modelKey=aiKey+"|"+model+"|"+simpleHash(JSON.stringify(fbGenConfig));let gm=_fbModelCache[modelKey];if(!gm){gm=sdk.getGenerativeModel(ai,{model:model,safetySettings:fbSafety,generationConfig:fbGenConfig});_fbModelCache[modelKey]=gm}const result=await gm.generateContent(prompt);const fbText=result.response.text();const _fbCost=_trackCost(result.response&&result.response.usageMetadata,prompt,fbText);return{text:fbText||null,status:200,error:fbText?null:"응답 없음",retries:0,cost:_fbCost}}catch(fbErr){return{text:null,status:0,error:"Firebase: "+(fbErr.message||String(fbErr)),retries:0}}const fbKey=firebaseKey||key;if(!fbKey)return{text:null,status:0,error:"Firebase Web API Key 누락",retries:0};if(!firebaseProjectId)return{text:null,status:0,error:"Firebase projectId 누락",retries:0};const is3x=model.includes("gemini-3")||model.includes("gemini-2.0-flash-thinking");const loc=is3x?"global":firebaseLocation||"global";url=`https://firebasevertexai.googleapis.com/v1beta/projects/${firebaseProjectId}/locations/${loc}/publishers/google/models/${model}:generateContent`;headers={"Content-Type":"application/json","x-goog-api-key":fbKey}}else if(isVertex){const sa=parseServiceAccountJson(vertexJson);if(!sa.ok)return{text:null,status:0,error:sa.error,retries:0};const projId=vertexProjectId||sa.projectId;if(!projId)return{text:null,status:0,error:"project_id 누락",retries:0};try{const token=await getVertexAccessToken(sa,cacheKey);const is3x=model.includes("gemini-3")||model.includes("gemini-2.0-flash-thinking");const host=is3x?"aiplatform.googleapis.com":`${vertexLocation}-aiplatform.googleapis.com`;const loc=is3x?"global":vertexLocation;url=`https://${host}/v1/projects/${projId}/locations/${loc}/publishers/google/models/${model}:generateContent`;headers={"Content-Type":"application/json",Authorization:`Bearer ${token}`}}catch(e){return{text:null,status:0,error:e.message,retries:0}}}else{if(!key)return{text:null,status:0,error:"API 키 누락",retries:0};url=_gBase+model+":generateContent";headers={"Content-Type":"application/json","x-goog-api-key":key}}const genConfig={};if(Object.keys(thinkingConfig).length>0)genConfig.thinkingConfig=thinkingConfig;if(responseMimeType)genConfig.responseMimeType=responseMimeType;if(maxOutputTokens!=null)genConfig.maxOutputTokens=maxOutputTokens;const body=JSON.stringify({safetySettings:SAFETY,contents:[{role:"user",parts:[{text:prompt}]}],generationConfig:genConfig});let lastStatus=0,lastError=null;for(let attempt=0;attempt<=maxRetries;attempt++){try{if(signal&&signal.aborted){lastError="aborted";break}const r=await gmFetch(url,{method:"POST",headers:headers,body:body,signal:signal,timeout:timeoutMs});lastStatus=r.status;if(r.status===401&&isVertex){if(_tokenCaches[cacheKey]){_tokenCaches[cacheKey].token=null;_tokenCaches[cacheKey].expiry=0}if(attempt""):"";lastError=`HTTP ${r.status} ${errBody.slice(0,500).replace(/\\n/g," ")}`;if([400,403,404].includes(r.status))break;if(r.status===429&&attemptsetTimeout(res,waitMs));continue}}else{const json=await r.json();const parts=json.candidates?.[0]?.content?.parts||[];const textPart=parts.find(p=>p.text&&!p.thought);const text=textPart?.text??null;const _restCost=_trackCost(json.usageMetadata,prompt,text);if(text)return{text:text,status:r.status,error:null,retries:attempt,cost:_restCost};lastError="응답 파싱 실패"}}catch(e){lastError=e.message}if(attemptsetTimeout(res,waitMs))}}return{text:null,status:lastStatus,error:lastError,retries:maxRetries}}async function embedTexts(texts,opts={}){const{apiType:apiType="key",key:key="",vertexJson:vertexJson="",vertexLocation:vertexLocation="global",vertexProjectId:vertexProjectId="",firebaseEmbedKey:firebaseEmbedKey="",firebaseKey:firebaseKey="",firebaseProjectId:firebaseProjectId="",firebaseLocation:firebaseLocation="global",model:model=DEFAULTS.embeddingModel,dimensions:dimensions=DEFAULTS.embeddingDimensions,taskType:taskType=DEFAULTS.embeddingTaskType,cacheKey:cacheKey="embed",costContext:costContext=null}=opts;const arr=Array.isArray(texts)?texts:[texts];const _trackEmbedCost=(textArr,modelUsed)=>{const core=_w.__LoreCore;if(!core||typeof core.recordApiCost!=="function")return;try{const ctx=costContext||{feature:"embed",chatKey:"global"};const inputChars=(textArr||[]).reduce((acc,t)=>acc+String(t||"").length,0);core.recordApiCost({chatKey:ctx.chatKey||"global",feature:ctx.feature||"embed",model:modelUsed,inTok:Math.ceil(inputChars/4),outTok:0,estimated:true})}catch(_){}};const isVertex=apiType==="vertex";const isFirebase=apiType==="firebase";if(isFirebase){if(!firebaseEmbedKey)throw new Error("Firebase 모드 임베딩: 별도 Gemini API Key 필요 (embedding-001 한정)");return embedTexts(arr,{...opts,apiType:"key",key:firebaseEmbedKey,model:"gemini-embedding-001"});const fbKey=firebaseKey||key;if(!fbKey)throw new Error("Firebase Web API Key 누락");if(!firebaseProjectId)throw new Error("Firebase projectId 누락");const embLoc=!firebaseLocation||firebaseLocation==="global"?"us-central1":firebaseLocation;const url=`https://firebasevertexai.googleapis.com/v1beta/projects/${firebaseProjectId}/locations/${embLoc}/publishers/google/models/${model}:predict`;const body=JSON.stringify({instances:arr.map(t=>({content:t})),parameters:{outputDimensionality:dimensions}});const r=await gmFetch(url,{method:"POST",headers:{"Content-Type":"application/json","x-goog-api-key":fbKey},body:body});if(!r.ok)throw new Error("Firebase 임베딩 실패: "+r.status);const json=await r.json();return json.predictions.map(p=>normalizeVector(p.embeddings.values))}else if(isVertex){const sa=parseServiceAccountJson(vertexJson);if(!sa.ok)throw new Error(sa.error);const projId=vertexProjectId||sa.projectId;const token=await getVertexAccessToken(sa,cacheKey);const embLoc=!vertexLocation||vertexLocation==="global"?"us-central1":vertexLocation;const host=`${embLoc}-aiplatform.googleapis.com`;const url=`https://${host}/v1/projects/${projId}/locations/${embLoc}/publishers/google/models/${model}:predict`;const r=await gmFetch(url,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${token}`},body:JSON.stringify({instances:arr.map(t=>({content:t})),parameters:{outputDimensionality:dimensions}})});if(!r.ok)throw new Error("Vertex 임베딩 실패: "+r.status);const json=await r.json();_trackEmbedCost(arr,model);return json.predictions.map(p=>normalizeVector(p.embeddings.values))}else{if(!key)throw new Error("API 키 누락");const embHeaders={"Content-Type":"application/json","x-goog-api-key":key};if(arr.length===1){const url=_gBase+model+":embedContent";const bodyObj={content:{parts:[{text:arr[0]}]},output_dimensionality:dimensions};if(model.includes("embedding-001"))bodyObj.taskType=taskType;const r=await gmFetch(url,{method:"POST",headers:embHeaders,body:JSON.stringify(bodyObj)});if(!r.ok)throw new Error("임베딩 API 실패: "+r.status);const json=await r.json();const embs=json.embeddings||[json.embedding];_trackEmbedCost(arr,model);return embs.map(e=>normalizeVector(e.values))}else{const url=_gBase+model+":batchEmbedContents";const requests=arr.map(t=>{const req={model:"models/"+model,content:{parts:[{text:t}]},outputDimensionality:dimensions};if(model.includes("embedding-001"))req.taskType=taskType;return req});const r=await gmFetch(url,{method:"POST",headers:embHeaders,body:JSON.stringify({requests:requests})});if(!r.ok)throw new Error("배치 임베딩 API 실패: "+r.status);const json=await r.json();if(!json.embeddings)throw new Error("임베딩 결과가 없습니다.");_trackEmbedCost(arr,model);return json.embeddings.map(e=>normalizeVector(e.values))}}}async function embedText(text,opts){const results=await embedTexts(text,opts);return results[0]}function normalizeVector(vec){let norm=0;for(let i=0;i({role:m.role,message:m.content}))}catch(e){return[]}}async function fetchAllMemories(chatRoomId){let token="";try{const CU=crackUtil();token=CU?CU.cookie().getAuthToken():""}catch(e){}if(!token)return{goal:[],shortTerm:[],relationship:[],longTerm:[]};const headers={Authorization:"Bearer "+token};const result={goal:[],shortTerm:[],relationship:[],longTerm:[]};const endpoints={goal:{url:`https://contents-api.wrtn.ai/character-chat/v3/chats/${chatRoomId}/summaries?limit=10`,creds:true,hasTitle:false},shortTerm:{url:`https://crack-api.wrtn.ai/crack-gen/v3/chats/${chatRoomId}/summaries?limit=20&type=shortTerm&orderBy=newest`,hasTitle:true},relationship:{url:`https://crack-api.wrtn.ai/crack-gen/v3/chats/${chatRoomId}/summaries?limit=20&type=relationship&orderBy=newest`,hasTitle:true},longTerm:{url:`https://crack-api.wrtn.ai/crack-gen/v3/chats/${chatRoomId}/summaries?limit=20&type=longTerm&orderBy=newest&filter=all`,hasTitle:true}};for(const[key,cfg]of Object.entries(endpoints)){try{const opts={method:"GET",headers:headers};if(cfg.creds)opts.credentials="include";const cleanUrl=cfg.url.replace(/\{\{|\}\}/g,"");const res=await fetch(cleanUrl,opts);const json=await res.json();if(json.result==="SUCCESS"&&json.data?.summaries){result[key]=json.data.summaries.map(s=>cfg.hasTitle?`${s.title}: ${s.summary}`:s.summary)}}catch(e){}}return result}async function fetchPersonaName(){try{const chatId=getCurrentChatId();if(!chatId)return null;const CU=crackUtil();if(!CU)return null;const persona=await CU.chatRoom().currentPersona(chatId);if(persona&&!(persona instanceof Error)&&persona.name)return persona.name}catch(e){}return null}Object.assign(C,{getCurUrl:getCurUrl,getCurrentChatId:getCurrentChatId,fetchLogs:fetchLogs,fetchAllMemories:fetchAllMemories,fetchPersonaName:fetchPersonaName,__platformLoaded:true});console.log("[LoreCore:platform] loaded")})();(function(){"use strict";const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const C=_w.__LoreCore;if(!C||!C.__kernelLoaded){console.error("[LoreCore:memory] kernel 미로드");return}if(C.__memoryLoaded)return;const{getDB:getDB,DEFAULTS:DEFAULTS}=C;function calcForgottenScore(turnsSinceLastMention,halfLife){if(turnsSinceLastMention<=0)return 0;return 1-Math.exp(-turnsSinceLastMention*Math.LN2/halfLife)}function getHalfLife(entryType,config){const hl=config?.decayHalfLife||DEFAULTS.decayHalfLife;return hl[entryType]||hl.default||20}function calcReinjectionScore(turnsSinceLastMention,entryType,config,entry){if(turnsSinceLastMention<=0)return 0;if(entry&&entry.anchor===true)return 1;const aiMem=config?.aiMemoryTurns||4;const halfLife=getHalfLife(entryType,config);let needsReinjection;if(turnsSinceLastMention<=aiMem){needsReinjection=0}else{const overLimit=turnsSinceLastMention-aiMem;needsReinjection=1-Math.exp(-overLimit*.5)}const relevanceDecay=Math.exp(-turnsSinceLastMention*Math.LN2/halfLife);return needsReinjection*relevanceDecay}function detectActiveCharacters(recentMsgs,allEntries){const recent=recentMsgs.slice(-6);const pool=recent.map(m=>m.message||"").join(" ").toLowerCase();const characters=allEntries.filter(e=>e.type==="identity"||e.type==="character").map(e=>{const names=[e.name];if(e.detail?.nicknames){Object.values(e.detail.nicknames).forEach(n=>{if(typeof n==="string")names.push(n)})}return{entry:e,names:names.map(n=>(n||"").toLowerCase()).filter(Boolean)}});const active=[];for(const c of characters){let hit=false;for(const n of c.names){const isCJK=/[가-힣㐀-鿿]/.test(n);const minLen=2;if(n.lengthtext.includes(name.toLowerCase()))}async function checkFirstEncounter(char1,char2){const db=getDB();let enc=await db.encounters.where({char1:char1,char2:char2}).first();if(!enc)enc=await db.encounters.where({char1:char2,char2:char1}).first();return enc||null}async function recordFirstEncounter(char1,char2,data){const db=getDB();const existing=await checkFirstEncounter(char1,char2);const turn=data.turnApprox||0;if(existing){await db.encounters.update(existing.id,{lastSeenTurn:turn,totalEncounters:(existing.totalEncounters||1)+1,lastSeenAt:Date.now()})}else{await db.encounters.put({char1:char1,char2:char2,location:data.location||"",introducer:data.introducer||"",turnApprox:turn,firstMetTurn:turn,lastSeenTurn:turn,totalEncounters:1,impressions:data.impressions||{},timestamp:Date.now(),lastSeenAt:Date.now()})}}async function findUnmetPairs(activeNames){const unmet=[];for(let i=0;i=gap)out.push({pair:[activeNames[i],activeNames[j]],gap:diff})}}return out}async function getWorkingMemory(url){const db=getDB();return await db.workingMemory.get(url)||{url:url,scene:"",emotion:"",activeChars:[],lastAction:"",turn:0}}async function updateWorkingMemory(url,data){const db=getDB();const existing=await getWorkingMemory(url);await db.workingMemory.put({...existing,...data,url:url})}function extractSceneKeywords(recentMsgs){const last2=recentMsgs.slice(-4).map(m=>m.message||"").join(" ");const locationPatterns=/(?:에서|으로|에|장소[:\s]*|곳[:\s]*)([\uAC00-\uD7A3a-zA-Z]+(?:방|집|카페|학교|공원|거리|사무실|병원|숲|바다|호텔|교실|옥상|지하|성|궁전|마을|도시|광장|시장|골목|강|호수|산|절벽|동굴|탑|성벽|아파트|편의점))/gi;const locations=[];let m;while((m=locationPatterns.exec(last2))!==null)locations.push(m[1]);const enLocPattern=/\b(?:at|in|on|into|inside|toward|towards|near)\s+(?:the\s+|a\s+|an\s+)?([a-z][a-z\s'-]{1,30}?(?:room|house|home|cafe|school|park|street|office|hospital|forest|woods|sea|ocean|beach|hotel|classroom|rooftop|basement|castle|palace|village|town|city|plaza|market|alley|river|lake|mountain|cliff|cave|tower|wall|apartment|store|shop|bar|club|restaurant|library|garden|bridge|station|airport|harbor|temple|church|shrine))\b/gi;while((m=enLocPattern.exec(last2))!==null)locations.push(m[1].trim());const actionWords=["키스","포옹","악수","싸움","도망","울","웃","잡","안","밀","때리","달리","숨","기다","잠","먹","마시","요리","노래","춤","싸우","치료","kiss","hug","embrace","shake","fight","flee","cry","laugh","hold","push","hit","run","hide","wait","sleep","eat","drink","cook","sing","dance","heal"];const _last2Low=last2.toLowerCase();function _wordHit(w){if(/[가-힣]/.test(w))return last2.includes(w);try{return new RegExp("(^|[^a-z0-9])"+w.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"([^a-z0-9]|$)","i").test(_last2Low)}catch(e){return _last2Low.includes(w.toLowerCase())}}const actions=actionWords.filter(_wordHit);const emotionWords=["기쁨","슬픔","분노","공포","놀람","긴장","행복","불안","절망","설렘","부끄","당황","차분","joy","sad","sadness","anger","angry","fear","scared","surprise","surprised","tension","tense","happy","happiness","anxiety","anxious","despair","excited","shy","embarrass","embarrassed","calm","relief","relieved"];const emotions=emotionWords.filter(_wordHit);return{locations:locations,actions:actions,emotions:emotions}}function formatSceneTag(keywords){const loc=keywords&&keywords.locations&&keywords.locations.length?keywords.locations.slice(-1)[0]:"";if(!loc)return"";return"[현재 장면: 장소="+loc+"]"}function _arr(x){return Array.isArray(x)?x.filter(Boolean).map(v=>String(v).trim()).filter(Boolean):[]}function _uniq(a){return Array.from(new Set(_arr(a)))}function _clamp(text,max){text=String(text||"").replace(/\s+/g," ").trim();if(!max||text.length<=max)return text;return text.slice(0,Math.max(0,max-1)).trim()+"…"}function _summaryLevels(summary,fallback){const src=summary&&typeof summary==="object"&&!Array.isArray(summary)?summary:{};const raw=typeof summary==="string"?summary:"";const full=_clamp(src.full||src.compact||src.micro||raw||fallback||"",700);const compact=_clamp(src.compact||src.micro||full||fallback||"",180);const micro=_clamp(src.micro||compact||fallback||"",60);return{full:full,compact:compact,micro:micro}}function isTimelineEvent(entry){return!!(entry&&String(entry.type||"").toLowerCase()===(C.TIMELINE_EVENT_TYPE||"timeline_event"))}function stableTimelineEventId(entry){if(!entry)return"";if(entry.eventId)return String(entry.eventId);const when=entry.when||{};const seed=[entry.packName||"",entry.title||entry.name||"",_arr(entry.participants).join("|"),entry.location||"",when.anchor||"",when.turnStart||entry.eventTurn||entry.timeline?.eventTurn||"",when.turnEnd||""].join("::");return"te_"+(C.simpleHash?C.simpleHash(seed):Math.abs(seed.split("").reduce((a,ch)=>(a<<5)-a+ch.charCodeAt(0)|0,0)).toString(36))}function normalizeTimelineEvent(entry,ctx={}){const e={...entry||{}};e.type=C.TIMELINE_EVENT_TYPE||"timeline_event";e.timelineSchemaVersion=C.TIMELINE_SCHEMA_VERSION||1;const nowTurn=Number(ctx.currentTurn||ctx.turn||e.when?.turnEnd||e.when?.turnStart||e.eventTurn||e.timeline?.eventTurn||0);const title=e.title||e.name||e.summary?.micro||e.summary?.compact||"Timeline event";e.title=String(title);e.name=e.name||e.title;e.when=e.when&&typeof e.when==="object"?{...e.when}:{};e.when.turnStart=Number(e.when.turnStart||e.timeline?.turnStart||e.eventTurn||e.timeline?.eventTurn||nowTurn||0);e.when.turnEnd=Number(e.when.turnEnd||e.timeline?.turnEnd||e.when.turnStart||nowTurn||0);e.when.relative=e.when.relative||e.timeline?.relativeOrder||"past";e.when.anchor=String(e.when.anchor||e.relativeTimeHint||e.timeline?.sceneLabel||"");e.when.inferredOrder=String(e.when.inferredOrder||"");e.when.confidence=e.when.confidence!=null?e.when.confidence:.7;e.participants=_uniq(e.participants||e.parties||e.entities||[]);e.location=String(e.location||e.place||e.timeline?.location||"");e.actions=_uniq(e.actions||e.action||[]);e.emotions=e.emotions&&typeof e.emotions==="object"&&!Array.isArray(e.emotions)?e.emotions:{};e.hooks=_uniq(e.hooks||e.openHooks||e.unresolvedHooks||[]).slice(0,DEFAULTS.temporalEventMaxHooks||8);e.recallTriggers=_uniq(e.recallTriggers||e.triggers||[]).slice(0,DEFAULTS.temporalEventMaxTriggers||12);e.triggers=_uniq([...e.triggers||[],...e.recallTriggers]).slice(0,DEFAULTS.temporalEventMaxTriggers||12);e.linkedLore=_uniq(e.linkedLore||e.relatedLore||[]).slice(0,DEFAULTS.temporalEventMaxLinkedLore||12);e.summary=_summaryLevels(e.summary,e.title);e.eventId=stableTimelineEventId(e);e.sceneId=e.sceneId||e.when.sceneId||e.timeline?.sceneId||"";e.arcId=e.arcId||e.when.arcId||e.timeline?.arcId||e.rootId||"";e.timeline={...e.timeline&&typeof e.timeline==="object"?e.timeline:{},eventTurn:e.when.turnEnd||e.when.turnStart||nowTurn||0,relativeOrder:e.when.relative||"past",sceneLabel:e.sceneId||e.when.anchor||"",observedRecency:e.timeline?.observedRecency||"old"};e.entities=_uniq([...e.entities||[],...e.participants,e.location,...e.linkedLore]);e.subjects=_uniq(e.subjects||e.participants.slice(0,1));e.objects=_uniq(e.objects||e.participants.slice(1));e.locations=_uniq(e.locations||(e.location?[e.location]:[]));e.createdTurn=e.createdTurn||e.when.turnStart||nowTurn||0;e.updatedTurn=nowTurn||e.when.turnEnd||e.createdTurn||0;e.eventTurn=e.eventTurn||e.timeline.eventTurn||e.createdTurn||0;e.realTimestamp=e.realTimestamp||e.ts||Date.now();e.relativeTimeHint=e.relativeTimeHint||e.when.anchor||e.when.relative||"";e.compressionState=e.compressionState||DEFAULTS.temporalEventDefaultCompression||"compact";return e}function inferEntryEntities(entry){if(!entry)return[];const d=entry.detail||{};const out=[].concat(_arr(entry.entities)).concat(isTimelineEvent(entry)?_arr(entry.participants).concat(_arr(entry.linkedLore),_arr(entry.location?[entry.location]:[])):[]).concat(_arr(entry.parties)).concat(_arr(d.parties)).concat(_arr(entry.subjects)).concat(_arr(entry.objects)).concat(_arr(entry.locations));if(entry.type==="character"||entry.type==="identity")out.push(entry.name);if((entry.type==="rel"||entry.type==="relationship")&&typeof entry.name==="string"){if(entry.name.includes("↔"))out.push(...entry.name.split("↔"));else if(entry.name.includes("&"))out.push(...entry.name.split("&"))}return _uniq(out)}function normalizeTemporalGraph(entry,ctx={}){if(!entry)return entry;if(isTimelineEvent(entry))return normalizeTimelineEvent(entry,ctx);const nowTurn=Number(ctx.currentTurn||ctx.turn||0);const ts=Date.now();const d=entry.detail||{};entry.entities=_uniq(inferEntryEntities(entry));entry.subjects=_uniq(entry.subjects||(entry.type==="character"||entry.type==="identity"?[entry.name]:[]));entry.objects=_uniq(entry.objects||d.objects||[]);entry.locations=_uniq(entry.locations||d.locations||(entry.timeline&&entry.timeline.location?[entry.timeline.location]:[]));entry.promises=_uniq(entry.promises||(entry.type==="promise"||entry.type==="prom"?[entry.name]:[]));if(!Array.isArray(entry.relations))entry.relations=[];if(!entry.createdTurn)entry.createdTurn=entry.timeline?.eventTurn||nowTurn||0;entry.updatedTurn=nowTurn||entry.updatedTurn||entry.createdTurn||0;entry.eventTurn=entry.timeline?.eventTurn||entry.eventTurn||entry.createdTurn||0;entry.sceneId=entry.sceneId||ctx.sceneId||entry.timeline?.sceneId||"";entry.arcId=entry.arcId||ctx.arcId||entry.rootId||entry.sceneId||"";entry.realTimestamp=entry.realTimestamp||entry.ts||ts;entry.relativeTimeHint=entry.relativeTimeHint||entry.timeline?.relativeOrder||"";return entry}function temporalGap(entry,currentTurn){const ev=Number(entry&&(entry.eventTurn||entry.timeline?.eventTurn||entry.createdTurn||0));if(!ev||!currentTurn)return null;return Math.max(0,currentTurn-ev)}function graphOverlapScore(entry,activeNames){const active=_arr(activeNames).map(x=>x.toLowerCase());if(!active.length||!entry)return 0;const ents=inferEntryEntities(entry).map(x=>x.toLowerCase());if(!ents.length)return 0;let hits=0;for(const a of active)if(ents.some(e=>e===a||e.includes(a)||a.includes(e)))hits++;return Math.min(1,hits/Math.max(active.length,1))}function relationshipGraphScore(entry,activeNames){if(!entry)return 0;let score=0;const t=entry.type;if(t==="rel"||t==="relationship")score+=.45;if(entry.callState||entry.call||entry.callHistory)score+=.25;score+=graphOverlapScore(entry,activeNames)*.35;const rels=Array.isArray(entry.relations)?entry.relations.length:0;if(rels)score+=Math.min(.2,rels*.05);return Math.min(1,score)}function unresolvedPriorityScore(entry){if(!entry)return 0;const d=entry.detail||{};const status=String(entry.state||d.status||d.current_status||"").toLowerCase();let score=0;if(entry.type==="promise"||entry.type==="prom")score+=.55;if(/pending|open|unresolved|미해결|보류|진행|약속/.test(status))score+=.35;if(entry.cond||d.condition)score+=.2;if(Array.isArray(entry.eventHistory)&&entry.eventHistory.length)score+=.1;return Math.min(1,score)}function maintenanceRecallScore(entry,currentTurn,config){const lmt=entry&&entry.lastMentionedTurn;if(lmt==null||!currentTurn)return 0;const gap=Math.max(0,currentTurn-lmt);return calcReinjectionScore(gap,entry.type,config||{},entry)}function temporalRecencyScore(entry,currentTurn){const gap=temporalGap(entry,currentTurn);if(gap==null)return 0;if(entry.anchor)return 1;const unresolved=unresolvedPriorityScore(entry);const recency=Math.exp(-gap*Math.LN2/12);return Math.min(1,recency+unresolved*.35)}function buildTemporalHint(entry,currentTurn){if(!entry)return"";const gap=temporalGap(entry,currentTurn);const ev=entry.eventTurn||entry.timeline?.eventTurn||entry.createdTurn||0;const rawScene=entry.sceneId||entry.timeline?.sceneLabel||"";const rawArc=entry.arcId||entry.rootId||"";const scene=rawScene&&!String(rawScene).startsWith("chat:")?rawScene:"";const arc=rawArc&&!String(rawArc).startsWith("chat:")?rawArc:"";if(!ev&&!scene&&!arc)return"";const parts=[];if(gap!=null&&gap>0)parts.push("현재보다 약 "+gap+"턴 앞선 설정");else if(gap===0)parts.push("현재 장면과 이어지는 설정");else parts.push("이전 저장 설정");if(scene)parts.push("장면: "+scene);if(arc)parts.push("흐름: "+arc);return`[과거/연속성 참고: ${entry.name} — ${parts.join("; ")}]`}function buildRelationDeltaHint(entry){const hist=Array.isArray(entry&&entry.callHistory)?entry.callHistory:[];if(!hist.length)return"";const last=hist[hist.length-1];if(!last||!last.from||!last.to||!last.term)return"";const prev=last.prevTerm?`, 이전 호칭 '${last.prevTerm}'`:"";return`[호칭 변화: ${last.from}가 ${last.to}를 '${last.term}'라고 부름${prev}]`}function formatTemporalHints(entries,opts={}){const currentTurn=opts.currentTurn||0;const budget=opts.budget||120;const lines=[];if(currentTurn)lines.push("[현재 맥락: 이번 응답에서 참고할 연속성 정보]");for(const e of entries||[]){const rel=buildRelationDeltaHint(e);const then=buildTemporalHint(e,currentTurn);for(const line of[rel,then]){if(!line)continue;const next=lines.concat(line).join("\n");if(next.length>budget)return lines.join("\n");lines.push(line)}}return lines.join("\n")}function temporalRecallCueScore(text){const raw=String(text||"").toLowerCase();if(!raw)return 0;const cues=["그때","전에","기억","지난번","처음","이후","그 후","그날","예전","아까","방금","지난","앞서","이전","나중","다시","재회","약속","remember","before","after","last time","first time","back then","previously"];let score=0;for(const cue of cues){let hit=false;const isAscii=/^[\x20-\x7e]+$/.test(cue);if(isAscii){try{const esc=cue.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");hit=new RegExp(`(^|[^a-z0-9])${esc}([^a-z0-9]|$)`,"i").test(raw)}catch{hit=raw.includes(cue)}}else{hit=raw.includes(cue)}if(hit)score+=cue.length>=4?.22:.16}return Math.min(1,score)}function temporalRecallPool(userInput,recentMsgs,range=4){const tail=Array.isArray(recentMsgs)?recentMsgs.slice(-range).map(m=>m&&m.message||"").join(" "):"";return String((userInput||"")+" "+tail).toLowerCase()}function temporalTokenize(text){const raw=String(text||"").toLowerCase();const parts=raw.split(/[\s,.!?;:"'()[\]{}<>\/\\|]+/).map(x=>x.trim()).filter(x=>x.length>=2);const cjk=raw.match(/[가-힣]{2,}|[\u4e00-\u9fff]{2,}/g)||[];return _uniq(parts.concat(cjk))}function temporalFieldTerms(entry){if(!entry)return[];const w=entry.when||{};const s=entry.summary||{};const emotionTerms=[];if(entry.emotions&&typeof entry.emotions==="object"&&!Array.isArray(entry.emotions)){for(const[k,arr]of Object.entries(entry.emotions))emotionTerms.push(k,..._arr(arr))}return _uniq([]).concat(_arr(entry.triggers)).concat(_arr(entry.recallTriggers)).concat(_arr(entry.participants)).concat(_arr(entry.entities)).concat(_arr(entry.linkedLore)).concat(_arr(entry.actions)).concat(_arr(entry.hooks)).concat(_arr(entry.locations)).concat(_arr([entry.title,entry.name,entry.location,w.anchor,w.inferredOrder,s.micro,s.compact])).concat(emotionTerms)}function temporalTermOverlap(entry,pool){const lowPool=String(pool||"").toLowerCase();if(!entry||!lowPool)return{score:0,matched:[]};const terms=temporalFieldTerms(entry).map(t=>String(t||"").trim()).filter(Boolean);const matched=[];for(const raw of terms){const t=raw.toLowerCase();if(t.length<2)continue;if(t.includes("&&")){const parts=t.split("&&").map(x=>x.trim()).filter(Boolean);if(parts.length&&parts.every(p=>lowPool.includes(p)))matched.push(raw)}else if(lowPool.includes(t)){matched.push(raw)}}const direct=Math.min(1,matched.length*.18);const poolTokens=temporalTokenize(lowPool);const entryTokens=temporalTokenize(terms.join(" "));let tokenHits=0;for(const t of entryTokens){if(poolTokens.includes(t)||poolTokens.some(p=>p.length>=2&&(p.includes(t)||t.includes(p))))tokenHits++}const fuzzy=Math.min(.45,tokenHits*.06);return{score:Math.min(1,direct+fuzzy),matched:_uniq(matched).slice(0,6)}}function temporalAnchorMatchScore(entry,pool){const lowPool=String(pool||"").toLowerCase();const w=entry&&entry.when||{};const anchors=_arr([w.anchor,w.inferredOrder,entry&&entry.relativeTimeHint,entry&&entry.timeline&&entry.timeline.sceneLabel]);if(!anchors.length||!lowPool)return 0;let score=0;for(const a of anchors){const low=String(a||"").toLowerCase();if(low.length>=2&&lowPool.includes(low))score=Math.max(score,.7);const toks=temporalTokenize(low);if(toks.some(t=>lowPool.includes(t)))score=Math.max(score,.35)}return Math.min(1,score)}function resolveTemporalRecall(userInput,recentMsgs,entries,opts={}){const currentTurn=opts.currentTurn||0;const limit=opts.limit||4;const activeNames=opts.activeNames||[];const pool=temporalRecallPool(userInput,recentMsgs,opts.range||4);const cue=temporalRecallCueScore(pool);const out=[];for(const e of entries||[]){if(!isTimelineEvent(e)||e.enabled===false)continue;const overlap=temporalTermOverlap(e,pool);const anchor=temporalAnchorMatchScore(e,pool);const entity=graphOverlapScore(e,activeNames);const recency=temporalRecencyScore(e,currentTurn);const unresolved=unresolvedPriorityScore(e);let score=cue*.22+overlap.score*.38+anchor*.22+entity*.1+recency*.05+unresolved*.08;if(!cue&&overlap.score<.22&&anchor<.35)score*=.35;if(score<=.08)continue;out.push({entry:e,score:Math.min(1,score),matchedTriggers:overlap.matched,components:{cue:cue,overlap:overlap.score,anchor:anchor,entity:entity,recency:recency,unresolved:unresolved}})}out.sort((a,b)=>b.score-a.score);return{candidates:out.slice(0,limit),hasCue:cue>0,cueScore:cue,pool:pool}}function parseCallKey(key){const match=String(key||"").match(/^(.+?)→(.+?)$/);return match?{from:match[1],to:match[2]}:null}function normalizeCallState(raw,fallbackTerm){const source=raw&&typeof raw==="object"&&!Array.isArray(raw)?raw:{};const currentTerm=source.currentTerm||source.term||source.current||source.call||(typeof raw==="string"?raw:fallbackTerm)||"";let previousTerms=source.previousTerms||source.previous||source.prev||[];if(typeof previousTerms==="string")previousTerms=[previousTerms];if(!Array.isArray(previousTerms))previousTerms=[];previousTerms=previousTerms.map(x=>String(x||"").trim()).filter(x=>x&&x!==currentTerm);return{currentTerm:currentTerm,previousTerms:Array.from(new Set(previousTerms)),tone:source.tone||"",scope:source.scope||"",lastChangedTurn:source.lastChangedTurn||source.turn||0,confidence:source.confidence,reason:source.reason||""}}function isActiveCallPair(from,to,activeNames){if(!activeNames||activeNames.length===0)return true;const fromActive=activeNames.some(n=>from.includes(n)||n.includes(from));const toActive=activeNames.some(n=>to.includes(n)||n.includes(to));return fromActive||toActive}function setCallState(matrix,states,from,to,raw,fallbackTerm){if(!from||!to)return;const state=normalizeCallState(raw,fallbackTerm);if(!state.currentTerm&&!fallbackTerm)return;if(!matrix[from])matrix[from]={};matrix[from][to]=state.currentTerm||fallbackTerm;states[`${from}→${to}`]=state}function buildHonorificMatrix(entries,activeNames){const matrix={};const prevMap={};const states={};for(const e of entries){if(e.type!=="rel"&&e.type!=="relationship")continue;const nicknames=e.call||e.detail?.nicknames||null;if(nicknames&&typeof nicknames==="object"){for(const[key,value]of Object.entries(nicknames)){const pair=parseCallKey(key);if(!pair)continue;if(!isActiveCallPair(pair.from,pair.to,activeNames))continue;setCallState(matrix,states,pair.from,pair.to,{currentTerm:value},value)}}const callState=e.callState||e.detail?.callState||null;if(Array.isArray(callState)){for(const cs of callState){if(!cs)continue;const from=cs.from||cs.speaker||cs.source;const to=cs.to||cs.target||cs.addressee;if(!isActiveCallPair(from||"",to||"",activeNames))continue;setCallState(matrix,states,from,to,cs,cs.currentTerm||cs.term)}}else if(callState&&typeof callState==="object"){if(callState.currentTerm||callState.term){const from=callState.from||callState.speaker||callState.source;const to=callState.to||callState.target||callState.addressee;if(isActiveCallPair(from||"",to||"",activeNames))setCallState(matrix,states,from,to,callState,callState.currentTerm||callState.term)}else{for(const[key,value]of Object.entries(callState)){const pair=parseCallKey(key);if(!pair||!isActiveCallPair(pair.from,pair.to,activeNames))continue;setCallState(matrix,states,pair.from,pair.to,value,typeof value==="string"?value:value?.currentTerm||value?.term)}}}const hist=Array.isArray(e.callHistory)?e.callHistory:[];if(hist.length>=1){const byKey={};for(const h of hist){if(!h||!h.from||!h.to||!h.term)continue;if(!isActiveCallPair(h.from,h.to,activeNames))continue;const k=`${h.from}→${h.to}`;(byKey[k]=byKey[k]||[]).push(h)}for(const k in byKey){const arr=byKey[k].sort((a,b)=>(a.turn||0)-(b.turn||0));const last=arr[arr.length-1];const prevTerms=Array.from(new Set(arr.slice(0,-1).map(h=>h.term).filter(t=>t&&t!==last.term)));if(prevTerms.length)prevMap[k]=prevTerms[prevTerms.length-1];const pair=parseCallKey(k);if(pair)setCallState(matrix,states,pair.from,pair.to,{currentTerm:last.term,previousTerms:prevTerms,tone:last.tone||"",scope:last.scope||"",lastChangedTurn:last.turn||0,confidence:last.confidence,reason:last.reason||""},last.term)}}}return{matrix:matrix,prevMap:prevMap,states:states}}function formatHonorificMatrix(result,budget){const src=result&&typeof result==="object"&&result.matrix?result:{matrix:result||{},prevMap:{},states:{}};const{matrix:matrix,prevMap:prevMap,states:states}=src;const lines=[];for(const[from,targets]of Object.entries(matrix)){for(const[to,hon]of Object.entries(targets)){const k=`${from}→${to}`;const state=states&&states[k]?states[k]:normalizeCallState({currentTerm:hon,previousTerms:prevMap&&prevMap[k]?[prevMap[k]]:[]},hon);const cur=state.currentTerm||hon;if(!cur)continue;const prevTerms=Array.from(new Set([...state.previousTerms||[],prevMap&&prevMap[k]].filter(Boolean))).filter(t=>t!==cur);let line=`${from}는 ${to}를 '${cur}'라고 부름`;if(prevTerms.length)line+=` (이전 호칭 ${prevTerms.slice(-1)[0]}은 참고만)`;lines.push(line)}}let out=lines.length?"[호칭] "+lines.join("; "):"";if(out.length>budget)out=out.slice(0,Math.max(0,budget-1)).replace(/[;,(][^;,(]*$/,"")+"…";return out}async function saveEntryVersion(entry,reason){if(!entry||!entry.id)return;try{const db=getDB();if(!db.entryVersions)return;const snap=JSON.parse(JSON.stringify(entry));delete snap.id;await db.entryVersions.put({entryId:entry.id,ts:Date.now(),turn:0,reason:reason||"auto",snapshot:snap});const all=await db.entryVersions.where("entryId").equals(entry.id).sortBy("ts");if(all.length>20){const delIds=all.slice(0,all.length-20).map(v=>v.id);await db.entryVersions.bulkDelete(delIds)}}catch(e){console.warn("[LoreCore:memory] saveEntryVersion 실패:",e.message)}}async function getEntryVersions(entryId){try{const db=getDB();if(!db.entryVersions)return[];return await db.entryVersions.where("entryId").equals(entryId).reverse().sortBy("ts")}catch(e){return[]}}async function restoreEntryVersion(versionId){const db=getDB();const v=await db.entryVersions.get(versionId);if(!v||!v.snapshot)throw new Error("버전 없음");const cur=await db.entries.get(v.entryId);if(cur)await saveEntryVersion(cur,"pre_restore");const restored={...v.snapshot,id:v.entryId};await db.entries.put(restored);return restored}function isAnchor(entry){return!!(entry&&entry.anchor===true)}Object.assign(C,{calcForgottenScore:calcForgottenScore,calcReinjectionScore:calcReinjectionScore,getHalfLife:getHalfLife,isAnchor:isAnchor,detectActiveCharacters:detectActiveCharacters,isRelatedToActive:isRelatedToActive,checkFirstEncounter:checkFirstEncounter,recordFirstEncounter:recordFirstEncounter,findUnmetPairs:findUnmetPairs,findReunionPairs:findReunionPairs,getWorkingMemory:getWorkingMemory,updateWorkingMemory:updateWorkingMemory,extractSceneKeywords:extractSceneKeywords,formatSceneTag:formatSceneTag,inferEntryEntities:inferEntryEntities,normalizeTemporalGraph:normalizeTemporalGraph,temporalGap:temporalGap,graphOverlapScore:graphOverlapScore,isTimelineEvent:isTimelineEvent,stableTimelineEventId:stableTimelineEventId,normalizeTimelineEvent:normalizeTimelineEvent,relationshipGraphScore:relationshipGraphScore,unresolvedPriorityScore:unresolvedPriorityScore,maintenanceRecallScore:maintenanceRecallScore,temporalRecencyScore:temporalRecencyScore,buildTemporalHint:buildTemporalHint,buildRelationDeltaHint:buildRelationDeltaHint,formatTemporalHints:formatTemporalHints,temporalRecallCueScore:temporalRecallCueScore,resolveTemporalRecall:resolveTemporalRecall,buildHonorificMatrix:buildHonorificMatrix,formatHonorificMatrix:formatHonorificMatrix,saveEntryVersion:saveEntryVersion,getEntryVersions:getEntryVersions,restoreEntryVersion:restoreEntryVersion,__memoryLoaded:true});console.log("[LoreCore:memory] loaded")})();(function(){"use strict";const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const C=_w.__LoreCore;if(!C||!C.__kernelLoaded){console.error("[LoreCore:format] kernel 미로드");return}if(C.__formatLoaded)return;const DEFAULTS=C.DEFAULTS;function charLen(s){return[...String(s||"")].length}function summaryTier(e,level="compact"){const s=e&&e.summary;if(!s)return"";if(typeof s==="object"&&!Array.isArray(s))return String(s[level]||s.full||s.compact||s.micro||"");return String(s||"")}function callStatePairs(e,limit=2){const cs=e&&e.callState;if(!cs||typeof cs!=="object")return"";const out=[];for(const[k,v]of Object.entries(cs)){if(!v)continue;const term=typeof v==="string"?v:v.currentTerm||v.term||"";if(!term)continue;const scope=typeof v==="object"&&v.scope?"@"+v.scope:"";out.push(`${k}:${term}${scope}`);if(out.length>=limit)break}return out.join("/")}const TYPE_ABBR={character:"Char",identity:"Char",relationship:"Rel",promise:"Prom",location:"Loc",event:"Evt",timeline_event:"Time",item:"Item",concept:"Sys",setting:"Sys"};function cfFull(e){if(e.inject?.full){let line=`[${e.name}|${e.state||e.type}] ${e.inject.full}`;if(e.call){const pairs=Object.entries(e.call).map(([k,v])=>`${k}:${v}`).join("/");line+=` Call:${pairs}`}else{const cs=callStatePairs(e);if(cs)line+=` Call:${cs}`}return line}const d=e.detail||{};const abbr=TYPE_ABBR[e.type]||"";const status=d.current_status||d.status||"";let line=abbr?"["+abbr+":"+e.name:"["+e.name;if(status)line+="|"+status;line+="]";const fullSummary=summaryTier(e,"full");if(fullSummary){const sum=charLen(fullSummary)>90?[...fullSummary].slice(0,87).join("")+"...":fullSummary;line+=" "+sum}if(d.nicknames&&typeof d.nicknames==="object"){const pairs=Object.entries(d.nicknames);if(pairs.length>0){const nk=pairs.map(([k,v])=>{const m=k.match(/^(.+?)→(.+?)$/);return m?m[1]+"->"+m[2]+":"+v:k+":"+v}).join(", ");line+=" Call:"+nk}}if(e.type==="promise"&&d.condition)line+=" Cond:"+[...d.condition].slice(0,25).join("");return line}function cfCompact(e){if(e.inject?.compact)return`${e.name}|${e.state||""}: ${e.inject.compact}`;const d=e.detail||{};const status=d.current_status||d.status||"";let line=e.name;if(status)line+="|"+status;line+=":";const compactSummary=summaryTier(e,"compact");if(compactSummary){const sum=charLen(compactSummary)>45?[...compactSummary].slice(0,42).join("")+"...":compactSummary;line+=" "+sum}if(d.nicknames&&typeof d.nicknames==="object"){const pairs=Object.entries(d.nicknames);if(pairs.length>0){const nk=pairs.slice(0,2).map(([k,v])=>{const m=k.match(/^(.+?)→(.+?)$/);return m?m[1]+"→"+v:v}).join("/");line+=" Call:"+nk}}else{const cs=callStatePairs(e,2);if(cs)line+=" Call:"+cs}if(e.type==="promise"&&d.status)line+="["+d.status+"]";return line}function cfMicro(e){if(e.inject?.micro)return e.inject.micro;const d=e.detail||{};const microSummary=summaryTier(e,"micro");const val=d.current_status||d.status||(microSummary?[...microSummary].slice(0,25).join(""):e.type);let hon="";if(d.nicknames&&typeof d.nicknames==="object"){const vals=Object.values(d.nicknames);if(vals.length>0)hon="/"+vals[0]}return e.name+"="+val+hon}function bundleGroupKey(e,activeNames){if(e.type==="character"||e.type==="identity")return e.name;if(e.type==="rel"||e.type==="relationship"){let parties=e.parties||e.detail?.parties;if((!parties||parties.length<2)&&typeof e.name==="string"){if(e.name.includes("↔"))parties=e.name.split("↔").map(s=>s.trim()).filter(Boolean);else if(e.name.includes("&"))parties=e.name.split("&").map(s=>s.trim()).filter(Boolean)}if(parties&&parties.length){const hit=parties.find(p=>(activeNames||[]).some(n=>n===p||p.includes(n)||n.includes(p)));return hit||parties[0]}}if(e.type==="promise"||e.type==="prom"){if(typeof e.name==="string"){const m=e.name.match(/^([^:\[→—-]+)/);if(m)return m[1].trim()}}return e.name}function entryPriority(e,idx=0,activeNames=[]){let s=Number(e.score||0)*100;s+=(e.imp||5)*3+(e.emo||5)*2+(e.sur||5);if(e.anchor)s+=1e3;if(e.type==="promise"||e.type==="prom")s+=80;if(e.type==="relationship"||e.type==="rel")s+=60;if(C.isTimelineEvent&&C.isTimelineEvent(e)){s+=70;if(e._temporalJudge&&e._temporalJudge.recall)s+=80;if(e._nway&&e._nway.timelineRecall)s+=Number(e._nway.timelineRecall||0)*90;if(e.temporalRecall)s+=50}if(e.state==="pending"||e.detail?.status==="pending")s+=40;if(e.callState||e.call)s+=25;if(Array.isArray(e.eventHistory)&&e.eventHistory.length)s+=20;if(e.isCurrentArc)s+=25;if(e._nway)s+=(e._nway.unresolved||0)*50+(e._nway.activeEntity||0)*35+(e._nway.relationshipGraph||0)*35+(e._nway.temporal||0)*25;const ents=Array.isArray(e.entities)?e.entities:e.parties||e.detail?.parties||[];if(activeNames.length&&ents.some(x=>activeNames.some(n=>x===n||String(x).includes(n)||String(n).includes(x))))s+=45;return s-idx*2}function formatEntryAtLevel(e,level){if(level==="full")return cfFull(e);if(level==="compact")return cfCompact(e);return cfMicro(e)}function timelineSummary(e,level="compact"){const s=e&&e.summary;if(s&&typeof s==="object"&&!Array.isArray(s)){return String(s[level]||s.compact||s.full||s.micro||e.title||e.name||"")}return String(s||e?.inject?.compact||e?.title||e?.name||"")}function timelineWhenLabel(e,currentTurn=0){const w=e&&e.when||{};const anchor=w.anchor||e?.relativeTimeHint||e?.timeline?.sceneLabel||"";if(anchor)return String(anchor);const ev=Number(w.turnEnd||w.turnStart||e?.eventTurn||e?.timeline?.eventTurn||0);if(ev&¤tTurn&¤tTurn>ev)return`약 ${currentTurn-ev}턴 전`;if(ev&¤tTurn&¤tTurn===ev)return"현재 장면과 이어짐";return""}function formatTimelineEventAtLevel(e,level="compact",opts={}){if(!e)return"";const currentTurn=opts.currentTurn||0;const when=timelineWhenLabel(e,currentTurn);const participants=Array.isArray(e.participants)&&e.participants.length?e.participants.slice(0,4).join("·"):"";const location=e.location?"@"+e.location:"";const hooks=Array.isArray(e.hooks)&&e.hooks.length?e.hooks.slice(0,level==="full"?3:1).join("/"):"";const actions=Array.isArray(e.actions)&&e.actions.length?e.actions.slice(0,level==="full"?4:2).join("/"):"";const summary=timelineSummary(e,level);if(level==="micro"){return[when,participants,location,timelineSummary(e,"micro")].filter(Boolean).join(" ")}let line="";if(when)line+=when+" — ";if(participants||location)line+="["+[participants,location].filter(Boolean).join(" ")+"] ";line+=summary||e.title||e.name||"과거 사건";const tail=[];if(actions)tail.push("행동:"+actions);if(hooks)tail.push("후크:"+hooks);if(tail.length)line+=" ("+tail.join("; ")+")";return line}function buildTemporalRecallBlock(items,opts={}){const sourceRows=(items||[]).map(x=>x&&x.entry?x.entry:x).filter(e=>e&&(!C.isTimelineEvent||C.isTimelineEvent(e)));if(!sourceRows.length){return{text:"",entries:[],usedChars:0,eventIds:[],mode:"none",level:"none",compressionActions:[],droppedEventIds:[]}}const decision=opts.decision||{};const mode=opts.mode||decision.mode||"compact_timeline";const depth=opts.depth||decision.wantedDepth||(mode==="specific_event"?"detail":"compact");const budget=Math.max(0,opts.budget||decision.maxChars||DEFAULTS.temporalHintChars||300);const maxEvents=Math.max(1,opts.maxEvents||(mode==="specific_event"?3:2));const compressionEnabled=opts.compressionEnabled!==false;const _preserveFields=Array.isArray(opts.preserveFields)?opts.preserveFields:["participants","location","hooks"];void _preserveFields;const preferredLevel=depth==="detail"?"full":depth==="micro"?"micro":"compact";const levelOrder=preferredLevel==="full"?["full","compact","micro"]:preferredLevel==="compact"?["compact","micro"]:["micro"];const importanceOf=(e,idx)=>{const imp=Number(e.importance!=null?e.importance:e.imp!=null?e.imp:5);const emo=Number(e.emotional!=null?e.emotional:e.emo!=null?e.emo:5);const hook=Array.isArray(e.hooks)&&e.hooks.length?1:0;const selected=e._temporalJudge&&e._temporalJudge.recall?1:0;return imp*3+emo*2+hook*4+selected*12-idx*.1};const selectedIds=new Set(Array.isArray(decision.eventIds)?decision.eventIds.map(String):[]);const rows=sourceRows.map((e,idx)=>({e:e,idx:idx,id:String(e.eventId||e.id||e.name||""),priority:importanceOf(e,idx)})).sort((a,b)=>{const aSel=selectedIds.has(a.id)?1:0;const bSel=selectedIds.has(b.id)?1:0;if(aSel!==bSel)return bSel-aSel;return b.priority-a.priority}).slice(0,maxEvents);const header=mode==="specific_event"?"[시간축 회상 — 직접 언급된 과거 사건]":mode==="unresolved_hook"?"[시간축 회상 — 이어질 미해결 사건]":"[시간축 회상]";const lines=[header];const included=[];const compressionActions=[];const droppedEventIds=[];for(const row of rows){const e=row.e;const id=row.id;let picked=null;let pickedLevel=null;const levels=compressionEnabled?levelOrder:[preferredLevel];for(const level of levels){const line="- "+formatTimelineEventAtLevel(e,level,opts);const next=lines.concat(line).join("\n");if(charLen(next)<=budget){picked=line;pickedLevel=level;break}}if(!picked&&compressionEnabled){const current=lines.join("\n");const room=budget-charLen(current)-2;const microLine="- "+formatTimelineEventAtLevel(e,"micro",opts);if(room>=Math.max(30,opts.minCompressedChars||40)){picked=trimToBudget(microLine,room);pickedLevel="micro-trimmed"}}if(picked){lines.push(picked);included.push(e);if(pickedLevel!==preferredLevel){compressionActions.push({eventId:id,action:pickedLevel==="micro-trimmed"?"trim":"downgrade",from:preferredLevel,to:pickedLevel})}}else{droppedEventIds.push(id);compressionActions.push({eventId:id,action:"drop",reason:"temporal_budget"})}}const text=lines.length>1?lines.join("\n"):"";return{text:text,entries:included,usedChars:charLen(text),eventIds:included.map(e=>String(e.eventId||e.id||e.name||"")).filter(Boolean),mode:mode,level:included.length?preferredLevel:"none",compressionActions:compressionActions,droppedEventIds:droppedEventIds}}function buildLoreBudgetPlan(entries,budget,config={},opts={}){const activeNames=opts.activeNames||[];let ordered=entries.map((e,i)=>({e:e,i:i,p:entryPriority(e,i,activeNames)})).sort((a,b)=>b.p-a.p);let bundledCount=0;if(config.bundleByEntity!==false&&ordered.length>1){const groups=new Map;for(const row of ordered){const k=bundleGroupKey(row.e,activeNames);if(!groups.has(k))groups.set(k,{topP:row.p,rows:[]});const g=groups.get(k);g.rows.push(row);if(row.p>g.topP)g.topP=row.p}const sortedGroups=Array.from(groups.values()).sort((a,b)=>b.topP-a.topP);const bundled=[];for(const g of sortedGroups)for(const r of g.rows)bundled.push(r);bundledCount=ordered.length-sortedGroups.length;ordered=bundled}const lines=[],included=[],downgraded=[],dropped=[];let used=0;for(const row of ordered){const preferred=row.p>=120?"full":row.p>=75?"compact":"micro";const levels=preferred==="full"?["full","compact","micro"]:preferred==="compact"?["compact","micro"]:["micro"];let picked=null,pickedLevel=null;for(const level of levels){const line=formatEntryAtLevel(row.e,level);const len=charLen(line);if(line&&used+len+1<=budget){picked=line;pickedLevel=level;break}}if(picked){lines.push(picked);included.push(row.e);used+=charLen(picked)+1;if(pickedLevel!==preferred)downgraded.push({name:row.e.name,from:preferred,to:pickedLevel})}else{dropped.push({name:row.e.name,reason:"budget"})}}const level=included.length?downgraded.length?"planned+downgraded":"planned":"none";return{text:lines.join("\n"),included:included,usedChars:used,level:level,downgraded:downgraded,dropped:dropped,bundledCount:bundledCount,budgetPlan:{loreBudget:budget,used:used,candidates:entries.length,included:included.length,dropped:dropped.length}}}function trimToBudget(text,budget){text=String(text||"");if(charLen(text)<=budget)return text;if(budget<=3)return"";return[...text].slice(0,budget-3).join("")+"..."}function planInjectionBudget(opts){const{userInput:userInput="",maxInputChars:maxInputChars=2e3,entries:entries=[],activeNames:activeNames=[],unmetPairs:unmetPairs=[],sceneTag:sceneTag="",firstEncounterBlock:firstEncounterBlock="",reunionTags:reunionTags="",honorifics:honorifics="",temporalHints:temporalHints="",temporalRecallBlock:temporalRecallBlock="",config:config={},prefix:prefix=DEFAULTS.prefix,suffix:suffix=DEFAULTS.suffix}=opts||{};const userChars=charLen(userInput);const wrapperOverhead=charLen("\n"+prefix+"\n"+"\n"+suffix+"\n")+2;let available=maxInputChars-userChars-wrapperOverhead;const result={text:"",injected:"",included:[],usedChars:0,level:"none",downgraded:[],dropped:[],budgetPlan:{},finalChars:userChars,reason:""};if(available<20){result.reason="no_space_after_user_and_wrapper";return result}const sections=[];const addSection=(key,text,max)=>{if(!text||available<=0)return 0;const capped=trimToBudget(text,Math.min(max,available));if(!capped)return 0;sections.push({key:key,text:capped});const used=charLen(capped)+1;available-=used;return used};addSection("scene",sceneTag,config.sceneTagChars||90);addSection("temporalRecall",temporalRecallBlock,config.temporalRecallChars||450);addSection("firstEncounter",firstEncounterBlock,config.firstEncounterChars||240);addSection("reunion",reunionTags,config.reunionTagChars||140);addSection("honorifics",honorifics,config.honorificMatrixChars||DEFAULTS.honorificMatrixChars||80);addSection("temporal",temporalHints,config.temporalHintChars||DEFAULTS.temporalHintChars||180);const maxLoreBudget=Math.min(available,config.loreBudgetMax||DEFAULTS.loreBudgetMax||600);let loreBudget=Math.max(0,maxLoreBudget);let lore=buildLoreBudgetPlan(entries,loreBudget,config,{activeNames:activeNames,unmetPairs:unmetPairs});let body=[...sections.map(x=>x.text),lore.text].filter(Boolean).join("\n");let injected=body?"\n"+prefix+"\n"+body+"\n"+suffix+"\n":"";let finalChars=userChars+charLen(injected)+2;while(finalChars>maxInputChars&&loreBudget>0){loreBudget=Math.max(0,loreBudget-Math.max(40,finalChars-maxInputChars));lore=buildLoreBudgetPlan(entries,loreBudget,config,{activeNames:activeNames,unmetPairs:unmetPairs});body=[...sections.map(x=>x.text),lore.text].filter(Boolean).join("\n");injected=body?"\n"+prefix+"\n"+body+"\n"+suffix+"\n":"";finalChars=userChars+charLen(injected)+2}if(finalChars>maxInputChars){lore={text:"",included:[],usedChars:0,level:"critical-only",downgraded:[],dropped:entries.map(e=>({name:e.name,reason:"critical_sections_only"})),budgetPlan:{loreBudget:0,used:0,candidates:entries.length,included:0,dropped:entries.length}};body=sections.map(x=>x.text).filter(Boolean).join("\n");injected=body?"\n"+prefix+"\n"+body+"\n"+suffix+"\n":"";finalChars=userChars+charLen(injected)+2}if(finalChars>maxInputChars){result.reason="cancel_over_limit";return result}const sectionChars={};for(const s of sections)sectionChars[s.key]=charLen(s.text);return{text:body,injected:injected,included:lore.included,usedChars:charLen(body),level:lore.level,downgraded:lore.downgraded,dropped:lore.dropped,budgetPlan:{...lore.budgetPlan,userChars:userChars,wrapperOverhead:wrapperOverhead,availableAfterCritical:available,finalChars:finalChars,maxInputChars:maxInputChars},finalChars:finalChars,reason:lore.included.length?"ok":body?"critical_only":"empty",sections:sectionChars,bundledCount:lore.bundledCount||0}}function adaptiveFormat(opts){const{entries:rawEntries=[],activeNames:activeNames=[],unmetPairs:unmetPairs=[],honorifics:honorifics="",budget:budget=350,config:config={}}=opts;if(!rawEntries.length||budget<30)return{text:"",included:[],usedChars:0,level:"none",bundledCount:0};let entries=rawEntries;let bundledCount=0;if(config.bundleByEntity!==false){const order=[];const groups=new Map;for(const e of rawEntries){const k=bundleGroupKey(e,activeNames);if(!groups.has(k)){groups.set(k,[]);order.push(k)}groups.get(k).push(e)}const ordered=[];for(const k of order)for(const e of groups.get(k))ordered.push(e);entries=ordered;bundledCount=entries.length-order.length}const parts=[];let remaining=budget;if(honorifics&&config.honorificMatrixEnabled!==false){const hB=Math.min(Math.floor(remaining*.2),80);let hT=honorifics;if(charLen(hT)>hB)hT=[...hT].slice(0,hB-3).join("")+"...";parts.push(hT);remaining-=charLen(hT)+1}if(config.firstEncounterWarning!==false&&unmetPairs.length){const fB=Math.floor(remaining*.1);let fUsed=0;for(const[a,b]of unmetPairs){const tag="[Unmet:"+a+"&"+b+"]";const tl=charLen(tag);if(fUsed+tl>fB)break;parts.push(tag);fUsed+=tl+1}remaining-=fUsed}const included=[];const loreParts=[];let loreUsed=0;const cmpMode=config.useCompressedFormat===false||config.autoCompression===false?"full":config.compressionMode||"auto";let level=cmpMode;if(cmpMode==="auto"||cmpMode==="full"){for(let i=0;iremaining)break;loreParts.push(line);included.push(e);loreUsed+=len+1}}const rem1=entries.slice(included.length);if(rem1.length>0&&(cmpMode==="auto"||cmpMode==="compact")&&remaining-loreUsed>30){if(cmpMode==="auto")level="mixed";for(const e of rem1){const line=cfCompact(e);const len=charLen(line);if(loreUsed+len+1>remaining)break;loreParts.push(line);included.push(e);loreUsed+=len+1}}const rem2=entries.slice(included.length);if(rem2.length>0&&(cmpMode==="auto"||cmpMode==="micro")&&remaining-loreUsed>15){if(cmpMode==="auto")level="mixed+micro";for(const e of rem2){const line=cfMicro(e);const len=charLen(line);if(loreUsed+len+1>remaining)break;loreParts.push(line);included.push(e);loreUsed+=len+1}}if(included.length===0&&entries.length>0){level="compact";for(const e of entries){let line=cfCompact(e);let len=charLen(line);if(loreUsed+len+1>remaining){line=cfMicro(e);len=charLen(line);level="micro"}if(loreUsed+len+1>remaining)break;loreParts.push(line);included.push(e);loreUsed+=len+1}}if(loreParts.length>0)parts.push(loreParts.join("\n"));const usedChars=budget-remaining+loreUsed;return{text:parts.join("\n"),included:included,usedChars:usedChars,level:level,bundledCount:bundledCount}}function formatEntryFull(e){if(e.inject?.full)return cfFull(e);const d=e.detail||{};let line=`[${e.name}|${d.current_status||e.type}] ${summaryTier(e,"full")||""}`;if(d.nicknames&&typeof d.nicknames==="object"){const nk=Object.entries(d.nicknames).map(([k,v])=>`${k}:${v}`).join("/");if(nk)line+=` 호칭:${nk}`}if(d.cause)line+=` ←${d.cause}`;if(e.type==="promise"&&d.condition)line+=` 조건:${d.condition}`;if(e.type==="promise"&&d.status)line+=` [${d.status}]`;return line}function formatEntryCompact(e){if(e.inject?.compact)return cfCompact(e);const d=e.detail||{};let line=`[${e.name}|${d.current_status||e.type}]`;const compactSummary=summaryTier(e,"compact");if(compactSummary)line+=` ${compactSummary.slice(0,50)}`;if(e.type==="promise"&&d.status)line+=` [${d.status}]`;return line}function formatEntryMicro(e){if(e.inject?.micro)return cfMicro(e);const d=e.detail||{};return`${e.name}:${(d.current_status||summaryTier(e,"micro")||e.type).slice(0,25)}`}function budgetFormat(entries,budget,config){if(!entries.length)return"";const target=config?.targetCharsPerEntry||DEFAULTS.targetCharsPerEntry;let level;if(config?.autoCompression!==false){const avgBudget=budget/entries.length;if(avgBudget>=target.full)level="full";else if(avgBudget>=target.compact)level="compact";else level="micro"}else{level="compact"}const formatter={full:formatEntryFull,compact:formatEntryCompact,micro:formatEntryMicro}[level];const lines=[];let used=0;for(const e of entries){const line=formatter(e);if(used+line.length>budget)break;lines.push(line);used+=line.length+1}return lines.join("\n")}function assembleInjection(opts){const{entries:entries=[],activeNames:activeNames=[],unmetPairs:unmetPairs=[],sceneTag:sceneTag="",honorifics:honorifics="",config:config={},prefix:prefix="",suffix:suffix=""}=opts;const budget=config.loreBudgetChars||DEFAULTS.loreBudgetChars;const parts=[];let remaining=budget;if(sceneTag&&config.workingMemoryEnabled!==false){parts.push(sceneTag);remaining-=sceneTag.length+1}if(honorifics&&config.workingMemoryEnabled!==false){const hBudget=Math.min(remaining*.3,config.honorificMatrixChars||DEFAULTS.honorificMatrixChars);const hText=honorifics.length<=hBudget?honorifics:honorifics.slice(0,hBudget-3)+"...";parts.push(hText);remaining-=hText.length+1}for(const[a,b]of unmetPairs){const tag=`[Unmet:${a}&${b}]`;if(remaining-tag.length<50)break;parts.push(tag);remaining-=tag.length+1}if(remaining>20&&entries.length>0){const loreText=budgetFormat(entries,remaining,config);if(loreText)parts.push(loreText)}const body=parts.join("\n");if(!body)return"";const pfx=prefix||config.prefix||DEFAULTS.prefix;const sfx=suffix||config.suffix||DEFAULTS.suffix;return`\n${pfx}\n${body}\n${sfx}\n`}function formatFirstEncounterBlock(pair){const[a,b]=pair;return`[★ FIRST ENCOUNTER — ${a} × ${b} ★]\n서사상 첫 대면. 서로의 이름/외형/배경 모름.\n- 호칭은 중립어("저 사람", "당신", "~씨")만 허용\n- 이전 장면의 친밀도/별칭/내부 농담 금지\n- 자기소개는 자연스러운 흐름으로만, 첫인상 감각 묘사 1개 포함`}function formatReunionTag(pair,gap){const[a,b]=pair;return`[Reunion: ${a}↔${b} — ${gap}턴만에 재회. 이미 아는 사이. 처음 본 듯한 대사/자기소개 금지]`}Object.assign(C,{charLen:charLen,summaryTier:summaryTier,callStatePairs:callStatePairs,cfFull:cfFull,cfCompact:cfCompact,cfMicro:cfMicro,adaptiveFormat:adaptiveFormat,bundleGroupKey:bundleGroupKey,entryPriority:entryPriority,formatEntryAtLevel:formatEntryAtLevel,formatTimelineEventAtLevel:formatTimelineEventAtLevel,buildTemporalRecallBlock:buildTemporalRecallBlock,buildLoreBudgetPlan:buildLoreBudgetPlan,planInjectionBudget:planInjectionBudget,formatEntryFull:formatEntryFull,formatEntryCompact:formatEntryCompact,formatEntryMicro:formatEntryMicro,budgetFormat:budgetFormat,assembleInjection:assembleInjection,formatFirstEncounterBlock:formatFirstEncounterBlock,formatReunionTag:formatReunionTag,__formatLoaded:true});console.log("[LoreCore:format] loaded")})();(function(){"use strict";const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const C=_w.__LoreCore;if(!C||!C.__kernelLoaded){console.error("[LoreCore:search] kernel 미로드");return}if(!C.__memoryLoaded){console.error("[LoreCore:search] memory 미로드");return}if(C.__searchLoaded)return;const{callGeminiApi:callGeminiApi,getDB:getDB,DEFAULTS:DEFAULTS,calcReinjectionScore:calcReinjectionScore,detectActiveCharacters:detectActiveCharacters,isRelatedToActive:isRelatedToActive}=C;const cosineSimilarity=C.cosineSimilarity||C.cosineSim;function bigramSimilarity(a,b){if(!a||!b)return 0;const bigrams=s=>{const set=new Set;for(let i=0;im&&m.message||"").join(" ");return((userInput||"")+" "+sliceText).toLowerCase()}function triggerScan(userInput,recentMsgs,entries,config){const cfg=config||{};const pool=buildScanPool(userInput,recentMsgs,cfg);const strict=cfg.strictMatch!==false;const similar=cfg.similarityMatch===true;const hits=[];for(const e of entries||[]){if(!e||e.enabled===false)continue;const triggers=Array.isArray(e.triggers)?e.triggers:[];let hit=false,matched="",score=0;for(const raw of triggers){const t=(raw||"").trim();if(!t)continue;if(t.includes("&&")){const parts=t.split("&&").map(s=>s.trim().toLowerCase()).filter(Boolean);if(parts.length&&parts.every(p=>pool.includes(p))){hit=true;matched=t;score=1;break}}else if(t.startsWith("~")){if(!similar)continue;const pattern=t.slice(1).toLowerCase();if(!pattern)continue;if(pool.includes(pattern)){hit=true;matched=t;score=.9;break}let best=0;for(const word of pool.split(/[\s,.!?;:"'()\[\]{}]+/)){if(!word)continue;const sim=bigramSimilarity(word,pattern);if(sim>best)best=sim}if(best>=.75){hit=true;matched=t;score=.7*best;break}}else{const low=t.toLowerCase();if(!pool.includes(low))continue;const isCJK=/[가-힣㐀-鿿]/.test(low);if(!strict||isCJK){hit=true;matched=t;score=1;break}try{const esc=low.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");if(new RegExp("(^|[^a-z0-9])"+esc+"([^a-z0-9]|$)","i").test(pool)){hit=true;matched=t;score=1;break}}catch{hit=true;matched=t;score=1;break}}}if(hit)hits.push({entry:e,triggerScore:score,matchedTrigger:matched})}return hits}async function hybridSearch(userInput,recentMsgs,entries,config,apiOpts){const db=getDB();const cfg=config||{};const embEnabled=!!cfg.embeddingEnabled;const embWeight=embEnabled?cfg.embeddingWeight!=null?cfg.embeddingWeight:.4:0;const trigWeight=1-embWeight;const threshold=cfg.embeddingThreshold!=null?cfg.embeddingThreshold:DEFAULTS.embeddingThreshold!=null?DEFAULTS.embeddingThreshold:.3;const turnCounter=cfg.turnCounter!=null?cfg.turnCounter:null;const activeBoost=cfg.activeCharBoost!=null?cfg.activeCharBoost:1;const inactivePenalty=cfg.inactiveCharPenalty!=null?cfg.inactiveCharPenalty:1;const decayEnabled=cfg.decayEnabled!==false;const temporalEnabled=cfg.temporalGraphEnabled!==false;const activeEntityWeight=cfg.activeEntityWeight!=null?cfg.activeEntityWeight:DEFAULTS.activeEntityWeight||.25;const relationshipGraphWeight=cfg.relationshipGraphWeight!=null?cfg.relationshipGraphWeight:DEFAULTS.relationshipGraphWeight||.22;const temporalWeight=cfg.temporalWeight!=null?cfg.temporalWeight:DEFAULTS.temporalWeight||.18;const unresolvedWeight=cfg.unresolvedWeight!=null?cfg.unresolvedWeight:DEFAULTS.unresolvedWeight||.28;const periodicRecallEnabled=cfg.periodicRecallEnabled!==false;const maintenanceWeight=periodicRecallEnabled?cfg.maintenanceWeight!=null?cfg.maintenanceWeight:DEFAULTS.maintenanceWeight||.12:0;const timelineRetrievalEnabled=cfg.timelineRetrievalEnabled!==false;const timelineRecallWeight=cfg.timelineRecallWeight!=null?cfg.timelineRecallWeight:DEFAULTS.timelineRecallWeight||.32;const timelineNoCuePenalty=periodicRecallEnabled?cfg.timelineNoCuePenalty!=null?cfg.timelineNoCuePenalty:DEFAULTS.timelineNoCuePenalty||.35:0;const timelineRecallLimit=cfg.timelineRecallPoolLimit||Math.max(8,(cfg.maxEntries||4)*3);let lastMentionMap=null;if(decayEnabled&&cfg.chatKey){try{const _ls=typeof unsafeWindow!=="undefined"?unsafeWindow.localStorage:localStorage;const all=JSON.parse(_ls.getItem("lore-last-mention")||"{}");lastMentionMap=all[cfg.chatKey]||null}catch(e){}}const enabled=(entries||[]).filter(e=>e&&e.enabled!==false);const trigHits=triggerScan(userInput,recentMsgs,enabled,cfg);const trigMap={};for(const h of trigHits)trigMap[h.entry.id]={score:h.triggerScore,matched:h.matchedTrigger};const embMap={};let embeddingOk=false;const hasKey=apiOpts&&(apiOpts.key||apiOpts.geminiKey||apiOpts.vertexJson||apiOpts.firebaseEmbedKey);if(embEnabled&&hasKey&&typeof C.embedText==="function"){try{const tail=Array.isArray(recentMsgs)?recentMsgs.slice(-2).map(m=>m&&m.message||"").join(" "):"";const qText=((userInput||"")+" "+tail).slice(0,2e3);const model=apiOpts.model||"gemini-embedding-001";const qTaskType=model.includes("embedding-001")?"RETRIEVAL_QUERY":apiOpts.taskType||"RETRIEVAL_QUERY";const queryVec=await C.embedText(qText,Object.assign({},apiOpts,{taskType:qTaskType}));if(queryVec){const ids=enabled.map(e=>e.id);const allEmbs=ids.length?await db.embeddings.where("entryId").anyOf(ids).toArray():[];const entryMap={};for(const e of enabled)entryMap[e.id]=e;for(const eb of allEmbs){if(!eb||!eb.vector)continue;const entry=entryMap[eb.entryId];if(!entry)continue;if(eb.packName&&eb.packName!==entry.packName)continue;if(C.embeddingSourceHash){const expected=C.embeddingSourceHash(entry,eb.field||"summary");if((eb.sourceHash||eb.hash)!==expected)continue}let boost=1;if(eb.field==="condition")boost=1.2;if(eb.schemaVersion&&eb.schemaVersion<2)boost*=.95;const s=cosineSimilarity(queryVec,eb.vector)*boost;if(!(eb.entryId in embMap)||s>embMap[eb.entryId])embMap[eb.entryId]=s}embeddingOk=true}}catch(e){console.warn("[LoreCore] 쿼리 임베딩 실패:",e&&e.message)}}const activeNames=cfg.activeCharDetection!==false?detectActiveCharacters(recentMsgs||[],enabled)||[]:[];const temporalRecallMap={};let temporalRecallHasCue=false,temporalRecallCount=0;if(temporalEnabled&&timelineRetrievalEnabled&&C.resolveTemporalRecall){try{const resolvedTemporal=C.resolveTemporalRecall(userInput,recentMsgs,enabled,{currentTurn:turnCounter||0,activeNames:activeNames,limit:timelineRecallLimit,range:cfg.scanRange||4});temporalRecallHasCue=!!(resolvedTemporal&&resolvedTemporal.hasCue);const candidates=resolvedTemporal&&resolvedTemporal.candidates||[];for(const c of candidates){if(c&&c.entry&&c.entry.id!=null){temporalRecallMap[c.entry.id]=c;temporalRecallCount++}}}catch(e){console.warn("[LoreCore] 시간축 리콜 해석 실패:",e&&e.message)}}let trigOnly=0,embOnly=0,both=0;const scored=enabled.map(e=>{const tHit=trigMap[e.id];const tScore=tHit?tHit.score:0;const eSimRaw=embMap[e.id]||0;const eScore=eSimRaw>threshold?eSimRaw:0;if(tScore>0&&eScore>0)both++;else if(tScore>0)trigOnly++;else if(eScore>0)embOnly++;let score=trigWeight*tScore+embWeight*eScore;let matched=tHit?tHit.matched:"";let lmt=e.lastMentionedTurn!=null?e.lastMentionedTurn:lastMentionMap&&lastMentionMap[e.id]!=null?lastMentionMap[e.id]:null;if(lmt!=null&&turnCounter!=null&&lmt>turnCounter)lmt=null;if(decayEnabled&&turnCounter!=null&&lmt!=null){const turnsSince=turnCounter-lmt;const reScore=calcReinjectionScore(turnsSince,e.type,cfg,e);score=score*(1+reScore*.5)}if(decayEnabled&&lmt==null&&(tScore>0||eScore>0)){score=score*1.15}if(e.rootId&&e.isCurrentArc===false)score*=.3;if(activeNames.length>0&&(e.type==="character"||e.type==="identity")){const related=isRelatedToActive(e,activeNames);score=related?score*activeBoost:score*inactivePenalty}const components={};if(temporalEnabled){components.activeEntity=C.graphOverlapScore?C.graphOverlapScore(e,activeNames):0;components.relationshipGraph=C.relationshipGraphScore?C.relationshipGraphScore(e,activeNames):0;components.temporal=C.temporalRecencyScore?C.temporalRecencyScore(e,turnCounter||0):0;components.unresolved=C.unresolvedPriorityScore?C.unresolvedPriorityScore(e):0;components.maintenance=periodicRecallEnabled&&C.maintenanceRecallScore?C.maintenanceRecallScore(e,turnCounter||0,cfg):0;score+=components.activeEntity*activeEntityWeight;score+=components.relationshipGraph*relationshipGraphWeight;score+=components.temporal*temporalWeight;score+=components.unresolved*unresolvedWeight;score+=components.maintenance*maintenanceWeight;components.timelineRecall=0;if(timelineRetrievalEnabled&&C.isTimelineEvent&&C.isTimelineEvent(e)){const tr=temporalRecallMap[e.id];if(tr){components.timelineRecall=tr.score||0;if(!matched&&tr.matchedTriggers&&tr.matchedTriggers.length)matched=tr.matchedTriggers.join(",");let recallBoost=components.timelineRecall*timelineRecallWeight*(temporalRecallHasCue?1:timelineNoCuePenalty);if(!temporalRecallHasCue&&tScore<=0&&eScore<=0&&components.timelineRecall<.45)recallBoost=0;score+=recallBoost}}}if(e.anchor===true&&periodicRecallEnabled&&score<.2)score=.2;if(e.anchor===true&&temporalEnabled&&periodicRecallEnabled)score+=.4;return{entry:e,score:score,tScore:tScore,eSim:eSimRaw,matchedTrigger:matched,components:components}});scored.sort((a,b)=>b.score-a.score);const filtered=scored.filter(s=>s.score>0);return{scored:filtered,activeNames:activeNames,searchStats:{trigOnly:trigOnly,embOnly:embOnly,both:both,embeddingOk:embeddingOk,temporalRecallCount:temporalRecallCount,temporalRecallHasCue:temporalRecallHasCue}}}async function smartRerank(query,candidates,recentMsgs,apiOpts,config){if(!candidates||!candidates.length)return candidates||[];const cfg=config||{};const maxCandidates=cfg.rerankMaxCandidates||8;const blendWeight=cfg.rerankBlendWeight!=null?cfg.rerankBlendWeight:.5;const minLlmScore=cfg.rerankMinLlmScore!=null?cfg.rerankMinLlmScore:2;const anchorBoost=cfg.rerankAnchorBoost!=null?cfg.rerankAnchorBoost:1;const truncated=candidates.slice(0,maxCandidates);const recentText=typeof recentMsgs==="string"?recentMsgs:Array.isArray(recentMsgs)?recentMsgs.slice(-4).map(m=>(m.role||"")+": "+(m.message||"").slice(0,100)).join("\n"):"";const summaryOf=e=>{const s=e.summary;if(s&&typeof s==="object"&&!Array.isArray(s))return String(s.compact||s.full||s.micro||"");return String(s||"")};const listText=truncated.map((s,i)=>i+1+". ["+s.entry.type+"] "+s.entry.name+": "+summaryOf(s.entry).slice(0,100)).join("\n");const promptTpl=cfg.rerankPrompt||apiOpts?.rerankPrompt||DEFAULTS.rerankPrompt||"";const prompt=promptTpl?promptTpl.replace("{context}",recentText).replace("{query}",query||"").replace("{candidates}",listText)+'\n\nReturn either {"scores":[5,4,...]} or [{"i":1,"s":5},...] as JSON only.':"명시적 답변 제외: 오직 JSON만 출력.\n장면과 직접 관련성 기준 5점 척도 (5=핵심, 1=무관).\n\n입력:\n"+query+"\n\n최근 대화:\n"+recentText+"\n\n후보:\n"+listText+'\n\n출력: {"scores":[5,4,...]}';try{const res=await callGeminiApi(prompt,Object.assign({},apiOpts,{model:apiOpts&&apiOpts.model||"gemini-3-flash-preview",responseMimeType:"application/json",maxOutputTokens:256,thinkingLevel:"minimal",maxRetries:1}));const data=JSON.parse(res.text);let llmScores=null;if(Array.isArray(data.scores)){llmScores=data.scores}else if(Array.isArray(data)){llmScores=new Array(truncated.length);for(const row of data){if(!row||typeof row!=="object")continue;const idx=Number(row.i)-1;if(idx>=0&&idxs.score);const hMin=Math.min.apply(null,hScores),hMax=Math.max.apply(null,hScores);const hRange=hMax-hMin||1;const reranked=truncated.map((s,i)=>{const raw=llmScores[i];const llmScore=typeof raw==="number"&&raw>=1&&raw<=5?raw:3;const hybNorm=(s.score-hMin)/hRange;const llmNorm=(llmScore-1)/4;let finalScore=(1-blendWeight)*hybNorm+blendWeight*llmNorm;if(s.entry.anchor===true)finalScore+=anchorBoost;return Object.assign({},s,{origScore:s.score,llmScore:llmScore,score:finalScore})});const kept=reranked.filter(s=>s.llmScore>minLlmScore||s.entry.anchor===true);const droppedCount=reranked.length-kept.length;if(droppedCount>0)console.log("[LoreCore:rerank] 무관 "+droppedCount+"개 제거 (llm<="+minLlmScore+")");if(kept.length===0){console.warn("[LoreCore:rerank] 전체 후보가 무관 판정 → 원본 순서 우선");return candidates}kept.sort((a,b)=>b.score-a.score);const minKept=kept[kept.length-1].score;const tail=candidates.slice(maxCandidates).map(s=>Object.assign({},s,{origScore:s.score,score:Math.min(s.score,minKept*.99)}));return kept.concat(tail)}catch(e){console.warn("[LoreCore] 리랭크 실패, 기본 순서 사용:",e&&e.message);return candidates}}Object.assign(C,{bigramSimilarity:bigramSimilarity,buildScanPool:buildScanPool,triggerScan:triggerScan,hybridSearch:hybridSearch,smartRerank:smartRerank,__searchLoaded:true});console.log("[LoreCore:search] loaded v1.3.9")})();(function(){"use strict";const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const C=_w.__LoreCore;if(!C||!C.__kernelLoaded){console.error("[LoreCore:embedding] kernel 미로드");return}if(C.__embeddingLoaded)return;const{getDB:getDB,embedText:embedText,embedTexts:embedTexts,simpleHash:simpleHash,DEFAULTS:DEFAULTS}=C;const EMB_SCHEMA_VERSION=2;function summaryText(entry,level){const s=entry&&entry.summary;if(!s)return"";if(typeof s==="object"&&!Array.isArray(s))return String(s[level]||s.full||s.compact||s.micro||"");return String(s||"")}function flattenCallState(entry){const parts=[];const cs=entry&&entry.callState;if(cs&&typeof cs==="object"){for(const[pair,v]of Object.entries(cs)){if(!v)continue;if(typeof v==="string")parts.push(`${pair} ${v}`);else parts.push([pair,v.currentTerm,...Array.isArray(v.previousTerms)?v.previousTerms.slice(0,2):[],v.tone,v.scope,v.reason].filter(Boolean).join(" "))}}if(entry&&entry.call&&typeof entry.call==="object"){for(const[pair,term]of Object.entries(entry.call))parts.push(`${pair} ${term}`)}return parts.join(" ")}function buildTimelineEmbeddingText(entry){if(!entry)return"";const when=entry.when&&typeof entry.when==="object"?entry.when:{};const timeline=entry.timeline&&typeof entry.timeline==="object"?entry.timeline:{};const emotions=[];if(entry.emotions&&typeof entry.emotions==="object"&&!Array.isArray(entry.emotions)){for(const[who,vals]of Object.entries(entry.emotions)){emotions.push(who,...(Array.isArray(vals)?vals:[vals]).filter(Boolean))}}return[entry.title,entry.name,entry.type,entry.eventId,when.anchor,when.inferredOrder,when.relative,when.turnStart?"start t"+when.turnStart:"",when.turnEnd?"end t"+when.turnEnd:"",timeline.sceneLabel,timeline.relativeOrder,timeline.observedRecency,entry.location,Array.isArray(entry.participants)?entry.participants.join(" "):"",Array.isArray(entry.entities)?entry.entities.join(" "):"",Array.isArray(entry.actions)?entry.actions.join(" "):"",emotions.join(" "),Array.isArray(entry.hooks)?entry.hooks.join(" "):"",Array.isArray(entry.recallTriggers)?entry.recallTriggers.join(" "):"",Array.isArray(entry.triggers)?entry.triggers.join(" "):"",Array.isArray(entry.linkedLore)?entry.linkedLore.join(" "):"",Array.isArray(entry.locations)?entry.locations.join(" "):"",entry.relativeTimeHint,entry.embed_text||"",summaryText(entry,"full"),summaryText(entry,"compact"),summaryText(entry,"micro")].filter(Boolean).join(" ").replace(/\s+/g," ").trim()}function buildEmbeddingText(entry,field="summary"){if(!entry)return"";if(field==="condition")return String(entry.cond||entry.detail?.condition||"");if(C.isTimelineEvent&&C.isTimelineEvent(entry))return buildTimelineEmbeddingText(entry);const detail=entry.detail||{};const parties=Array.isArray(entry.parties)?entry.parties:Array.isArray(detail.parties)?detail.parties:[];const entities=Array.isArray(entry.entities)?entry.entities:[];const timeline=entry.timeline&&typeof entry.timeline==="object"?[entry.timeline.sceneLabel,entry.timeline.relativeOrder,entry.timeline.observedRecency,entry.timeline.eventTurn?"t"+entry.timeline.eventTurn:""].filter(Boolean).join(" "):"";const events=Array.isArray(entry.eventHistory)?entry.eventHistory.slice(-4).map(ev=>ev&&ev.summary).filter(Boolean).join(" "):"";const hooks=[entry.state,detail.current_status,detail.status,entry.cond,detail.condition].filter(Boolean).join(" ");const aliases=[].concat(Array.isArray(entry.aliases)?entry.aliases:[]).concat(Array.isArray(detail.aliases)?detail.aliases:[]).concat(detail.nicknames&&typeof detail.nicknames==="object"?Object.values(detail.nicknames):[]);return[entry.name,entry.type,aliases.join(" "),(entry.triggers||[]).join(" "),entry.embed_text||"",parties.join(" "),entities.join(" "),hooks,flattenCallState(entry),timeline,events,summaryText(entry,"full"),summaryText(entry,"compact"),summaryText(entry,"micro")].filter(Boolean).join(" ").replace(/\s+/g," ").trim()}function embeddingSourceHash(entry,field="summary"){return simpleHash(buildEmbeddingText(entry,field))}function embeddingMeta(entry,field,text,apiOpts,taskType){return{entryId:entry.id,field:field,packName:entry.packName||"",entryUpdatedAt:entry.lastUpdated||entry.ts||0,sourceHash:simpleHash(text),hash:simpleHash(text),schemaVersion:EMB_SCHEMA_VERSION,model:apiOpts.model||DEFAULTS.embeddingModel,taskType:taskType,updatedAt:Date.now()}}async function invalidateEntryEmbeddings(entryId){if(entryId==null)return 0;const db=getDB();return await db.embeddings.where("entryId").equals(entryId).delete()}async function cleanupStaleEmbeddings(packName,apiOpts={}){const db=getDB();const allEntries=await db.entries.toArray();const entryMap={};for(const e of allEntries)entryMap[e.id]=e;const allRaw=await db.embeddings.toArray();const all=packName?allRaw.filter(eb=>{const entry=entryMap[eb.entryId];if(entry)return entry.packName===packName;return eb.packName===packName}):allRaw;const targetModel=apiOpts.model||DEFAULTS.embeddingModel;let removed=0,staleHash=0,missingEntry=0,wrongModel=0,wrongPack=0;for(const eb of all){const entry=entryMap[eb.entryId];let del=false;if(!entry){del=true;missingEntry++}else if(eb.packName&&eb.packName!==entry.packName){del=true;wrongPack++}else{const field=eb.field||"summary";const expectedHash=embeddingSourceHash(entry,field);if((eb.sourceHash||eb.hash)!==expectedHash){del=true;staleHash++}else if(targetModel&&eb.model&&eb.model!==targetModel){del=true;wrongModel++}}if(del){await db.embeddings.delete(eb.id);removed++}}return{removed:removed,missingEntry:missingEntry,staleHash:staleHash,wrongModel:wrongModel,wrongPack:wrongPack,checked:all.length}}async function ensureEmbedding(entry,apiOpts){const db=getDB();const field="summary";const text=buildEmbeddingText(entry,field);if(!text)return;const hash=simpleHash(text);const docTaskType=(apiOpts.model||"").includes("embedding-001")?"RETRIEVAL_DOCUMENT":apiOpts.taskType;const targetModel=apiOpts.model||DEFAULTS.embeddingModel;const existing=await db.embeddings.where({entryId:entry.id,field:field}).first();const taskTypeMismatch=docTaskType!=null&&existing&&existing.taskType!=null&&existing.taskType!==docTaskType;const summaryFresh=existing&&(existing.sourceHash||existing.hash)===hash&&!(existing.model&&existing.model!==targetModel)&&!taskTypeMismatch&&existing.schemaVersion===EMB_SCHEMA_VERSION&&existing.packName===entry.packName;if(!summaryFresh){const vec=await embedText(text,{...apiOpts,taskType:docTaskType});const data={...embeddingMeta(entry,field,text,{...apiOpts,model:targetModel},docTaskType),vector:vec};if(existing)data.id=existing.id;await db.embeddings.put(data)}const condText=buildEmbeddingText(entry,"condition");if((entry.type==="promise"||entry.type==="prom")&&condText){const condHash=simpleHash(condText);const existingCond=await db.embeddings.where({entryId:entry.id,field:"condition"}).first();if(existingCond&&(existingCond.sourceHash||existingCond.hash)===condHash){const condTaskTypeMismatch=docTaskType!=null&&existingCond.taskType!=null&&existingCond.taskType!==docTaskType;const needsRegenCond=existingCond.model&&existingCond.model!==targetModel||condTaskTypeMismatch||existingCond.schemaVersion!==EMB_SCHEMA_VERSION||existingCond.packName!==entry.packName;if(!needsRegenCond)return}const condVec=await embedText(condText,{...apiOpts,taskType:docTaskType});const condData={...embeddingMeta(entry,"condition",condText,{...apiOpts,model:targetModel},docTaskType),vector:condVec};if(existingCond)condData.id=existingCond.id;await db.embeddings.put(condData)}}async function embedPack(packName,apiOpts,onProgress){const db=getDB();const entries=await db.entries.where("packName").equals(packName).toArray();const cleanup=await cleanupStaleEmbeddings(packName,apiOpts);const docTaskType=(apiOpts.model||"").includes("embedding-001")?"RETRIEVAL_DOCUMENT":apiOpts.taskType;const targetModel=apiOpts.model||DEFAULTS.embeddingModel;const allEmbs=entries.length?await db.embeddings.where("entryId").anyOf(entries.map(e=>e.id)).toArray():[];const embMap={};for(const eb of allEmbs){if(eb.field==="summary")embMap[eb.entryId]=eb}const pending=entries.filter(e=>{const text=buildEmbeddingText(e,"summary");const hash=simpleHash(text);const existing=embMap[e.id];return!existing||(existing.sourceHash||existing.hash)!==hash||existing.model!==targetModel||existing.schemaVersion!==EMB_SCHEMA_VERSION||existing.taskType!=null&&docTaskType!=null&&existing.taskType!==docTaskType||existing.packName!==e.packName});let done=0;for(let i=0;ibuildEmbeddingText(e,"summary"));try{const vecs=await embedTexts(texts,{...apiOpts,taskType:docTaskType,model:targetModel});for(let j=0;jb.length-a.length);for(const k of keys)if(m.startsWith(k))return PRICING[k];return null}function computeCost(model,inTok,outTok,contextLen){const p=getPricing(model);if(!p)return null;const i=Math.max(0,Number(inTok)||0);const o=Math.max(0,Number(outTok)||0);const ctx=Math.max(i,Number(contextLen)||0);let inRate=p.in,outRate=p.out;if(p.longThreshold&&ctx>p.longThreshold){if(p.longIn!=null)inRate=p.longIn;if(p.longOut!=null)outRate=p.longOut}return(i*inRate+o*outRate)/1e6}const STORAGE_KEY="lore-api-cost-log";const MAX_EVENTS=5e3;let _cache=null;const CUMUL_KEY="lore-api-cost-cumulative";let _cumulCache=null;function _loadCumul(){if(_cumulCache)return _cumulCache;try{const raw=_w.localStorage.getItem(CUMUL_KEY);const obj=raw?JSON.parse(raw):{};_cumulCache=obj&&typeof obj==="object"?obj:{}}catch(_){_cumulCache={}}return _cumulCache}function _persistCumul(){try{_w.localStorage.setItem(CUMUL_KEY,JSON.stringify(_cumulCache))}catch(_){}}function _loadAll(){if(_cache)return _cache;try{const raw=_w.localStorage.getItem(STORAGE_KEY);const arr=raw?JSON.parse(raw):[];_cache=Array.isArray(arr)?arr:[]}catch(_){_cache=[]}return _cache}function _persist(){try{_w.localStorage.setItem(STORAGE_KEY,JSON.stringify(_cache))}catch(_){}}function recordApiCost(ev){if(!ev||typeof ev!=="object")return null;const events=_loadAll();const inTok=Math.max(0,Number(ev.inTok)||0);const outTok=Math.max(0,Number(ev.outTok)||0);const rawModel=String(ev.model||"");const model=normalizeModel(rawModel)||rawModel;const contextLen=Number(ev.contextLen)||inTok;const usd=computeCost(rawModel,inTok,outTok,contextLen);const unknown=usd==null;const rec={ts:Number(ev.ts)||Date.now(),chatKey:String(ev.chatKey||"global"),feature:String(ev.feature||"unknown"),model:model,inTok:inTok,outTok:outTok,usd:unknown?null:usd,unknown:unknown,estimated:!!ev.estimated};events.push(rec);if(events.length>MAX_EVENTS)events.splice(0,events.length-MAX_EVENTS);_persist();const cumul=_loadCumul();cumul.usd=(Number(cumul.usd)||0)+(Number(rec.usd)||0);cumul.count=(Number(cumul.count)||0)+1;if(rec.unknown)cumul.unknownCount=(Number(cumul.unknownCount)||0)+1;if(rec.estimated)cumul.estimatedCount=(Number(cumul.estimatedCount)||0)+1;if(!cumul.firstTs||rec.tscumul.lastTs)cumul.lastTs=rec.ts;cumul.byFeature=cumul.byFeature||{};const bf=cumul.byFeature[rec.feature]=cumul.byFeature[rec.feature]||{usd:0,count:0};bf.usd+=Number(rec.usd)||0;bf.count++;cumul.byModel=cumul.byModel||{};const bm=cumul.byModel[rec.model]=cumul.byModel[rec.model]||{usd:0,count:0};bm.usd+=Number(rec.usd)||0;bm.count++;_persistCumul();return rec}function getCumulativeCost(){const c=_loadCumul();return{usd:Number(c.usd)||0,count:Number(c.count)||0,unknownCount:Number(c.unknownCount)||0,estimatedCount:Number(c.estimatedCount)||0,firstTs:c.firstTs||null,lastTs:c.lastTs||null,byFeature:c.byFeature||{},byModel:c.byModel||{}}}function clearCumulativeCost(){_cumulCache={};try{_w.localStorage.removeItem(CUMUL_KEY)}catch(_){}}function getCostEvents(){return _loadAll().slice()}function clearCostEvents(){_cache=[];try{_w.localStorage.removeItem(STORAGE_KEY)}catch(_){}}Object.assign(C,{PRICING:PRICING,computeCost:computeCost,recordApiCost:recordApiCost,getCostEvents:getCostEvents,clearCostEvents:clearCostEvents,normalizeModel:normalizeModel,getCumulativeCost:getCumulativeCost,clearCumulativeCost:clearCumulativeCost,__pricingLoaded:true});console.log("[LoreCore:pricing] loaded")})();(function(){"use strict";const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const C=_w.__LoreCore;if(!C||!C.__kernelLoaded){console.error("[LoreCore:importer] kernel 미로드");return}if(C.__importerLoaded)return;const{callGeminiApi:callGeminiApi,gmFetch:gmFetch,getDB:getDB,DEFAULTS:DEFAULTS}=C;function clampText(text,max){text=String(text||"").replace(/\s+/g," ").trim();if(!max||text.length<=max)return text;return text.slice(0,Math.max(0,max-1)).trim()+"…"}function normalizeSummaryValue(summary,name,state){if(summary&&typeof summary==="object"&&!Array.isArray(summary)){const full=clampText(summary.full||summary.compact||summary.micro||"",700);const compact=clampText(summary.compact||full,180);const micro=clampText(summary.micro||(state?`${name}=${state}`:compact||name),60);return{full:full,compact:compact,micro:micro}}const full=clampText(summary||"",700);const compact=clampText(full||state||name||"",180);const micro=clampText(state?`${name}=${state}`:compact||name||"",60);return{full:full,compact:compact,micro:micro}}function mergeText(a,b,max){a=clampText(a||"",max);b=clampText(b||"",max);if(!a)return b;if(!b||a.includes(b))return a;return clampText(a+" / "+b,max)}function mergeLoreSummary(existingSummary,incomingSummary,name,state){const ex=normalizeSummaryValue(existingSummary,name,state);const inc=normalizeSummaryValue(incomingSummary,name,state);return{full:mergeText(ex.full,inc.full,700),compact:mergeText(ex.compact,inc.compact,180),micro:inc.micro||ex.micro}}function normalizeApiOpts(apiOpts={}){const out={...apiOpts||{}};const liveCfg=_w.__LoreInj&&_w.__LoreInj.settings&&_w.__LoreInj.settings.config?_w.__LoreInj.settings.config:{};if(!out.apiType&&liveCfg.autoExtApiType)out.apiType=liveCfg.autoExtApiType;out.apiType=out.apiType||"key";if(!out.key&&liveCfg.autoExtKey)out.key=liveCfg.autoExtKey;if(!out.vertexJson&&liveCfg.autoExtVertexJson)out.vertexJson=liveCfg.autoExtVertexJson;if(!out.vertexLocation&&liveCfg.autoExtVertexLocation)out.vertexLocation=liveCfg.autoExtVertexLocation;if(!out.vertexProjectId&&liveCfg.autoExtVertexProjectId)out.vertexProjectId=liveCfg.autoExtVertexProjectId;if(!out.firebaseScript&&liveCfg.autoExtFirebaseScript)out.firebaseScript=liveCfg.autoExtFirebaseScript;if(!out.firebaseEmbedKey&&liveCfg.autoExtFirebaseEmbedKey)out.firebaseEmbedKey=liveCfg.autoExtFirebaseEmbedKey;if(out.model==="_custom")out.model=out.customModel||out.autoExtCustomModel||liveCfg.autoExtCustomModel||"gemini-3-flash-preview";if(!out.model)out.model=liveCfg.autoExtModel==="_custom"?liveCfg.autoExtCustomModel||"gemini-3-flash-preview":liveCfg.autoExtModel||"gemini-3-flash-preview";if(out.apiType==="firebase"&&!out.firebaseScript){throw new Error("Firebase 모드: firebaseScript 설정이 지식 변환 호출에 전달되지 않았습니다.")}if(out.apiType==="key"&&!out.key){throw new Error("API Key 모드: API 키가 비어 있습니다.")}if(out.apiType==="vertex"&&!out.vertexJson){throw new Error("Vertex 모드: 서비스 계정 JSON이 비어 있습니다.")}return out}function normalizeCallState(entry,turn){const out=entry.callState&&typeof entry.callState==="object"?JSON.parse(JSON.stringify(entry.callState)):{};if(entry.call&&typeof entry.call==="object"){for(const[key,term]of Object.entries(entry.call)){if(!out[key])out[key]={};if(typeof out[key]==="string")out[key]={currentTerm:out[key]};out[key].currentTerm=out[key].currentTerm||term;out[key].lastChangedTurn=out[key].lastChangedTurn||turn||0}}const deltas=Array.isArray(entry.callDelta)?entry.callDelta:[];for(const d of deltas){if(!d||!d.from||!d.to||!d.term)continue;const key=`${d.from}→${d.to}`;const prev=d.prevTerm?[d.prevTerm]:[];out[key]={currentTerm:d.term,previousTerms:prev,tone:d.tone||"",scope:d.scope||"scene",lastChangedTurn:d.turnApprox||turn||0,confidence:d.confidence!=null?d.confidence:.75,reason:d.reason||"observed vocative change"}}return Object.keys(out).length?out:undefined}function normalizeLoreEntry(entry,opts={}){let e={...entry||{}};const turn=opts.turn||e.timeline?.eventTurn||e.eventTurn||0;if(String(e.type||"").toLowerCase()===(C.TIMELINE_EVENT_TYPE||"timeline_event")&&C.normalizeTimelineEvent){e=C.normalizeTimelineEvent(e,{turn:turn,currentTurn:opts.currentTurn||turn})}e.summary=normalizeSummaryValue(e.summary,e.name,e.state);if(!e.inject||typeof e.inject!=="object")e.inject={};e.inject.full=e.inject.full||e.summary.full;e.inject.compact=e.inject.compact||e.summary.compact;e.inject.micro=e.inject.micro||e.summary.micro;if(!e.embed_text){const entities=Array.isArray(e.entities)?e.entities.join(" "):"";const temporalText=String(e.type||"").toLowerCase()===(C.TIMELINE_EVENT_TYPE||"timeline_event")?[e.title,e.location,(e.actions||[]).join(" "),(e.hooks||[]).join(" "),(e.recallTriggers||[]).join(" "),e.when?.anchor,(e.linkedLore||[]).join(" ")].filter(Boolean).join(" "):"";e.embed_text=clampText([e.name,temporalText,entities,(e.triggers||[]).join(" "),e.summary.compact,e.state].filter(Boolean).join(" "),360)}const callState=normalizeCallState(e,turn);if(callState)e.callState=callState;if(!e.timeline||typeof e.timeline!=="object")e.timeline={};e.timeline.eventTurn=e.timeline.eventTurn||turn||0;e.timeline.relativeOrder=e.timeline.relativeOrder||"current";e.timeline.sceneLabel=e.timeline.sceneLabel||"";e.timeline.observedRecency=e.timeline.observedRecency||"recent";if(!Array.isArray(e.entities)){const names=[];if(Array.isArray(e.parties))names.push(...e.parties);if(Array.isArray(e.detail?.parties))names.push(...e.detail.parties);if(e.name)names.push(...String(e.name).split(/[↔&]/).map(x=>x.trim()).filter(Boolean));e.entities=Array.from(new Set(names)).filter(Boolean)}return e}const IMPORT_SCHEMA=`[
{
"type": "character|location|item|event|concept|setting",
"name": "Entity Name",
"triggers": ["keyword1", "keyword2", "A&&B"],
"summary": {
"full": "self-contained continuity summary: who/what/why/current state/unresolved hook",
"compact": "entity + state + relation/hook preserved",
"micro": "stable recall handle + current state"
},
"inject": {
"full": "key facts for injection | max 120 chars",
"compact": "essential continuity | max 70 chars",
"micro": "name=status | max 35 chars"
},
"embed_text": "names aliases relationship terms event causes stakes location unresolved hooks",
"state": "current situation noun phrase",
"timeline": { "eventTurn": 0, "relativeOrder": "current|past|foreshadow", "sceneLabel": "", "observedRecency": "recent|old|unknown" },
"entities": ["characters/places/items involved"],
"detail": { "attributes": "traits/appearance/abilities", "relations": ["relationship facts"], "background_or_history": "background" },
"imp": 5,
"sur": 5,
"emo": 5
},
{
"type": "relationship|rel",
"name": "A↔B",
"parties": ["A", "B"],
"triggers": ["A&&B", "B&&A"],
"summary": {
"full": "relationship cause + current state + unresolved hook",
"compact": "relationship state + hook",
"micro": "A↔B=status"
},
"inject": {
"full": "relationship facts for injection | max 120 chars",
"compact": "essential relationship continuity | max 70 chars",
"micro": "A↔B=status | max 35 chars"
},
"embed_text": "A B aliases call terms relationship stakes hooks",
"state": "current relationship status",
"callState": {
"A→B": {
"currentTerm": "latest vocative",
"previousTerms": ["older vocative"],
"tone": "affectionate|hostile|formal|neutral",
"scope": "scene|stable|private|public",
"lastChangedTurn": 0,
"confidence": 0.8,
"reason": "why this is current"
}
},
"timeline": { "eventTurn": 0, "relativeOrder": "current", "sceneLabel": "", "observedRecency": "recent" },
"entities": ["A", "B"],
"imp": 5,
"sur": 5,
"emo": 5
}
]`;const IMPORT_PROMPT_TEMPLATE=`You are a Lore Structurer for AI RP.
Convert the following source material into structured lore entries for an RP memory system.

RULES:
1. JSON ONLY. Output a valid JSON array. No markdown.
2. Use the ORIGINAL LANGUAGE of the source. Korean source → Korean output.
3. Extract only information useful for later RP injection. Do not dump broad encyclopedia facts.
4. Each entity needs 3-5 triggers using exact names, aliases, places, objects, or relationship cues from the source.
5. For relationships, use bidirectional compound triggers: A&&B and B&&A.
6. summary and inject must both be produced.
- summary.full: continuity-safe and self-contained; include who/what/why/current state/unresolved hook.
- summary.compact: preserve entity, state, relationship, and unresolved hooks.
- summary.micro: stable recall handle + current state only; never a vague teaser.
- inject.full/compact/micro: short text intended for direct OOC injection.
7. embed_text must include names, aliases, relationship terms, event causes, stakes, locations, and unresolved hooks.
8. Extract callState for relationships when vocatives are visible: currentTerm, previousTerms, tone, scope, lastChangedTurn, confidence, reason.
9. Extract timeline, entities, state, imp/sur/emo for every entry when inferable. imp/sur/emo are 1-10.
10. For long source, prefer stable entities, relationships, rules, locations, unresolved hooks, and repeated constraints.
11. Maximum {maxEntries} entries.

Schema:
{schema}

Source Material:
{source}`;async function importFromText(text,packName,apiOpts,opts={}){const maxEntries=opts.maxEntries||DEFAULTS.importMaxEntries;const chunkSize=opts.chunkSize||DEFAULTS.importChunkSize;const maxAttempts=opts.maxAttempts!==undefined?opts.maxAttempts:3;const onProgress=typeof opts.onProgress==="function"?opts.onProgress:null;const allEntries=[];const chunks=[];for(let i=0;i/gi,"\n");const promptTpl=IMPORT_PROMPT_TEMPLATE.replace(//gi,"\n");const prompt=promptTpl.replace("{source}",chunk).replace("{schema}",schemaText).replace("{maxEntries}",String(maxEntries));let ok=false;let status="failed";let lastErr="";let rawSnippet="";let attempts=0;let gotEntries=0;for(let attempt=0;attempt0)raw=raw.slice(first);const lastB=Math.max(raw.lastIndexOf("]"),raw.lastIndexOf("}"));if(lastB!==-1&&lastB0){const db=getDB();let pack=await db.packs.get(packName);if(!pack)await db.packs.put({name:packName,entryCount:0,project:""});for(let e of allEntries){e=normalizeLoreEntry(e,{source:"imported"});e.packName=packName;e.project="";e.enabled=true;e.src=e.src||(e.source==="user_stated"?"us":e.source==="auto_extracted"?"ax":"im");e.source=e.source||"imported";e.ts=e.ts||Date.now();e.lastUpdated=e.ts;await db.entries.put(e)}const count=await db.entries.where("packName").equals(packName).count();await db.packs.update(packName,{entryCount:count})}const okCount=chunkResults.filter(r=>r.status==="ok").length;const emptyCount=chunkResults.filter(r=>r.status==="empty").length;const failedCount=chunkResults.filter(r=>r.status==="failed").length;C.__lastImportReport={added:allEntries.length,chunks:chunks.length,ok:okCount,empty:emptyCount,failed:failedCount,chunkResults:chunkResults};return allEntries.length}async function importFromJson(jsonArray,packName){const db=getDB();let pack=await db.packs.get(packName);if(!pack)await db.packs.put({name:packName,entryCount:0,project:""});let count=0;for(let e of jsonArray){if(!e.name)continue;e=normalizeLoreEntry(e,{source:"imported"});e.packName=packName;e.project=e.project||"";e.enabled=true;e.src=e.src||(e.source==="user_stated"?"us":e.source==="auto_extracted"?"ax":"im");e.source=e.source||"imported";e.ts=e.ts||Date.now();e.lastUpdated=e.ts;await db.entries.put(e);count++}const total=await db.entries.where("packName").equals(packName).count();await db.packs.update(packName,{entryCount:total});return count}async function fetchExternalText(url,opts={}){const errors=[];const normalized=String(url||"").trim();if(!/^https?:\/\//i.test(normalized))throw new Error("URL은 http:// 또는 https:// 로 시작해야 합니다.");const timeoutMs=Number(opts.timeoutMs)>0?Number(opts.timeoutMs):15e3;const onProgress=typeof opts.onProgress==="function"?opts.onProgress:null;const emit=info=>{if(!onProgress)return;try{onProgress(info)}catch(_){}};const directHeaders={Accept:"text/html,application/xhtml+xml,application/xml;q=0.9,text/plain;q=0.8,*/*;q=0.7","Accept-Language":"ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7","Cache-Control":"no-cache"};const withTimeout=(promise,label)=>{let to;const timer=new Promise((_,reject)=>{to=setTimeout(()=>reject(new Error(label+" timeout "+timeoutMs+"ms")),timeoutMs)});return Promise.race([promise,timer]).finally(()=>{try{clearTimeout(to)}catch(_){}})};emit({phase:"fetch:try",method:"gm-direct",attempt:1,total:1,timeoutMs:timeoutMs});try{const resp=await withTimeout(gmFetch(normalized,{method:"GET",headers:directHeaders}),"GM direct");if(resp&&resp.ok){const txt=await resp.text();emit({phase:"fetch:done",method:"gm-direct",bytes:txt.length});return txt}if(resp)errors.push("GM direct HTTP "+resp.status)}catch(e){errors.push("GM direct "+(e.message||String(e)))}emit({phase:"fetch:try",method:"fetch",attempt:1,total:1,timeoutMs:timeoutMs});try{const ac=typeof AbortController!=="undefined"?new AbortController:null;const fetchPromise=fetch(normalized,{method:"GET",credentials:"omit",signal:ac?ac.signal:undefined});const r=await withTimeout(fetchPromise,"fetch").catch(err=>{if(ac){try{ac.abort()}catch(_){}}throw err});if(r&&r.ok){const txt=await r.text();emit({phase:"fetch:done",method:"fetch",bytes:txt.length});return txt}if(r)errors.push("fetch HTTP "+r.status)}catch(e){errors.push("fetch "+(e.message||String(e)))}const fallbackUrls=["https://api.allorigins.win/raw?url="+encodeURIComponent(normalized),"https://api.codetabs.com/v1/proxy/?quest="+encodeURIComponent(normalized),"https://corsproxy.io/?"+encodeURIComponent(normalized)];for(let i=0;i{if(!onProgress)return;try{onProgress(info)}catch(_){}};try{emit({phase:"fetch:start",url:url});const html=await fetchExternalText(url,{onProgress:onProgress,timeoutMs:opts.fetchTimeoutMs});emit({phase:"parse",bytes:html.length});const text=html.replace(/]*>[\s\S]*?<\/script>/gi,"").replace(/]*>[\s\S]*?<\/style>/gi,"").replace(/<[^>]+>/g,"\n").replace(/&[a-z]+;/gi," ").replace(/\n{3,}/g,"\n\n").trim();if(!text)throw new Error("URL 텍스트 추출 결과가 비어 있습니다.");return await importFromText(text,packName,apiOpts,opts)}catch(e){console.error("[LoreCore] URL 임포트 실패:",e);C.__lastImportReport={added:0,chunks:0,ok:0,empty:0,failed:1,chunkResults:[{index:0,status:"failed",attempts:1,error:e.message||String(e)}]};throw e}}function detectDuplicatesInSummary(entries,memories){const summaryText=[...memories.shortTerm||[],...memories.relationship||[],...memories.longTerm||[]].join(" ").toLowerCase();if(!summaryText)return[];const duplicates=[];for(const e of entries){const name=e.name.toLowerCase();const keywords=(e.triggers||[]).map(t=>t.toLowerCase().split("&&")).flat();const nameInSummary=summaryText.includes(name);const keywordsInSummary=keywords.filter(k=>k.length>=3&&summaryText.includes(k));if(nameInSummary&&keywordsInSummary.length>=2)duplicates.push({entryId:e.id,name:e.name,confidence:.8});else if(nameInSummary)duplicates.push({entryId:e.id,name:e.name,confidence:.5})}return duplicates}Object.assign(C,{importFromText:importFromText,importFromJson:importFromJson,importFromUrl:importFromUrl,detectDuplicatesInSummary:detectDuplicatesInSummary,normalizeLoreEntry:normalizeLoreEntry,normalizeSummaryValue:normalizeSummaryValue,mergeLoreSummary:mergeLoreSummary,__importerLoaded:true});console.log("[LoreCore:importer] loaded")})();(function(){"use strict";const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;_w.__LoreRefiner=_w.__LoreRefiner||{};if(_w.__LoreRefiner.__promptsLoaded)return;const PROMPT_VERSION="v1.4.0-callstate-topic-default";const TEMPLATES={full:{name:"전체 검수 (모순 파악 + 끊김 복구)",desc:"로어 모순 파악과 코드블록/문장 끊김 복구를 동시에 수행합니다.",turnHint:1,prompt:`You are an RP Continuity Checker.\nRATE: PG-13 ~ NC-21\n\n## Scope\nYou receive:\n- [Lore]: partial character/world data\n- [Summaries]: memory snapshots\n- [Recent Context]: last N turns of dialogue\n- [New Speech]: the latest AI-generated RP response\n\n## Rule\nFlag an error ONLY when [New Speech] DIRECTLY CONTRADICTS a fact explicitly stated in [Lore] or [Summaries].\n\n## What Counts as an Error\n1. EXPLICIT FACTUAL CONTRADICTION\n2. PRESENCE / ABSENCE CONTRADICTION\n3. NICKNAME / TITLE MISMATCH\n4. STATE CONTRADICTION\n5. PROMISE / ARC CONTRADICTION\n6. REUNION VIOLATION: Characters who already know each other (per [Lore] rel entries or [Reunion] tag) behaving as strangers — self-introduction, "처음 뵙겠습니다", unfamiliarity.\n7. VOCATIVE CONTINUITY CHECK: Compare [New Speech] against the latest stable call-state.\n - A previous nickname or insult is not mandatory forever.\n - Current name/title is valid if [Recent Context] shows relationship normalization, direct name use, emotional cooling, formal setting, or a new scene.\n - Flag only when the speech contradicts the latest stable call-state or an explicit must-use term.\n8. USER IMPERSONATION: AI narrated the user's character's actions, decisions, dialogue, or internal thoughts WITHOUT user's explicit prior input.\n Violation examples: "당신은 웃으며 대답했다" when user never said they laughed; "내심 설렘이 일었다" when user expressed no such feeling.\n Allowed: describing physical cues the AI's character OBSERVES in the user (e.g., "당신의 눈빛을 보며"), but NOT internal states the AI cannot know.\n\n## Truncation Repair\nA) SENTENCE TRUNCATION: Complete ONLY the final interrupted sentence.\nB) STATUS BLOCK TRUNCATION (triple-backtick fenced code blocks \`\`\`...\`\`\` at the END of an assistant message, containing RP stats/HP/variables/scene info):\n Restore truncated/absent status blocks using [Recent Context]. Copy the most recent assistant-side code block verbatim (including the triple backticks). Only update numeric values if [New Speech] explicitly states them.\n\n## Output Format\nReason MUST be in Korean.\nNo issues/repairs needed:\n{passWord}\n\nIssues found (no markdown code fences):\n{"reason":"교정 이유","replacements":[{"from":"원문의 정확한 부분","to":"수정본"}]}\n\n[Lore]:\n{lore}\n\n[Summaries]:\n{memory}\n\n[Recent Context]:\n{context}\n\n[New Speech]:\n{message}`},repairOnly:{name:"단순 끊김 복구 (모순 체크 생략)",desc:"모순 체크를 생략하고 텍스트나 상태창의 끊김만 빠르게 복구합니다.",turnHint:0,prompt:`You are an RP Text Repair AI.\nRATE: PG-13 ~ NC-21\n\n## Rule\nYour ONLY job is to fix truncation in the [New Speech]. Do NOT check for factual logic or lore contradictions.\n\n## Truncation Repair\nA) SENTENCE TRUNCATION: Complete ONLY the final interrupted sentence using [Recent Context] as reference.\nB) STATUS BLOCK TRUNCATION (triple-backtick code blocks \`\`\`...\`\`\` at the END of an assistant message):\n If [Recent Context] contains a complete status block and [New Speech] does not, restore it verbatim including the triple backticks.\n\n## Output Format\nReason MUST be in Korean.\nNo truncation found:\n{passWord}\n\nTruncation fixed (no markdown code fences):\n{"reason":"끊김 복구","replacements":[{"from":"원문의 정확한 부분","to":"수정본"}]}\n\n[Recent Context]:\n{context}\n\n[New Speech]:\n{message}`},logicOnly:{name:"로어 모순 검수 (끊김 복구 생략)",desc:"수집된 로어를 바탕으로 엄격하게 논리적 모순만 체크합니다.",turnHint:1,prompt:`You are an RP Logic Checker.\nRATE: PG-13 ~ NC-21\n\n## Scope\nCheck [New Speech] against [Lore] and [Summaries] for direct logical or factual contradictions.\nDo NOT fix truncated sentences or missing status blocks.\n\n## Output Format\nReason MUST be in Korean.\nNo logical contradictions:\n{passWord}\n\nContradictions found (no markdown code fences):\n{"reason":"모순 설명","replacements":[{"from":"원문의 정확한 부분","to":"수정본"}]}\n\n[Lore]:\n{lore}\n\n[Summaries]:\n{memory}\n\n[Recent Context]:\n{context}\n\n[New Speech]:\n{message}`}};const TOPICS={factual:{label:"1. 명시적 사실 모순",desc:"[Lore]·[Summaries]와 정면 충돌",group:"logic"},presence:{label:"2. 존재/부재 모순",desc:"없어야 할 캐릭터가 등장하거나 반대",group:"logic"},nickname:{label:"3. 호칭 불일치",desc:"저장된 호칭·직함과 다른 부름",group:"logic"},state:{label:"4. 상태 모순",desc:"사망/부상/실종 등 상태 모순",group:"logic"},promise:{label:"5. 약속 모순",desc:"확정된 약속이나 서사 흐름 위반",group:"logic"},reunion:{label:"6. 재회 위반",desc:"이미 아는 사이가 처음 보는 것처럼 행동",group:"logic"},honorific:{label:"7. 호칭 연속성",desc:"최신 안정 호칭 상태와 명시적 충돌만 검수",group:"logic"},impersonation:{label:"8. 유저 사칭",desc:"AI가 유저 캐릭터의 행동/생각을 서술",group:"logic"},truncSentence:{label:"A. 문장 끊김 복구",desc:"잘린 마지막 문장 완성",group:"repair"},truncStatusBlock:{label:"B. 상태창 복구",desc:"코드블록 상태창 끊김/누락 복구",group:"repair"}};const _LOGIC_BLOCKS={factual:"1. EXPLICIT FACTUAL CONTRADICTION",presence:"2. PRESENCE / ABSENCE CONTRADICTION",nickname:"3. NICKNAME / TITLE MISMATCH",state:"4. STATE CONTRADICTION",promise:"5. PROMISE / ARC CONTRADICTION",reunion:'6. REUNION VIOLATION: Characters who already know each other (per [Lore] rel entries or [Reunion] tag) behaving as strangers — self-introduction, "처음 뵙겠습니다", unfamiliarity.',honorific:"7. VOCATIVE CONTINUITY CHECK: Compare [New Speech] against the latest stable call-state. A previous nickname/insult is context, not a permanent requirement. Current name/title is valid when [Recent Context] shows normalization, direct name use, emotional cooling, formal setting, or a new scene. Flag only when the speech contradicts the latest stable call-state or an explicit must-use term.",impersonation:'8. USER IMPERSONATION: AI narrated the user\'s character\'s actions, decisions, dialogue, or internal thoughts WITHOUT user\'s explicit prior input.\n Violation examples: "당신은 웃으며 대답했다" when user never said they laughed; "내심 설렘이 일었다" when user expressed no such feeling.\n Allowed: describing physical cues the AI\'s character OBSERVES in the user (e.g., "당신의 눈빛을 보며"), but NOT internal states the AI cannot know.'};const _REPAIR_BLOCKS={truncSentence:"A) SENTENCE TRUNCATION: Complete ONLY the final interrupted sentence.",truncStatusBlock:"B) STATUS BLOCK REPAIR (triple-backtick fenced code blocks ```...``` at the END of an assistant message, containing RP stats/HP/variables/scene info):\n Use [Recent Context] to find the most recent complete assistant-side status block. If [New Speech] has a missing, truncated, or internally inconsistent status block, restore the complete block structure and labels from that recent block. Preserve changed values only when [New Speech] explicitly states the change; otherwise keep the recent complete value. Copy triple backticks exactly."};function buildDynamicPrompt(topics){topics=topics||{};const logicKeys=["factual","presence","nickname","state","promise","reunion","honorific","impersonation"].filter(k=>topics[k]);const repairKeys=["truncSentence","truncStatusBlock"].filter(k=>topics[k]);const hasLogic=logicKeys.length>0;const hasRepair=repairKeys.length>0;if(!hasLogic&&!hasRepair){return"You are an RP Checker. All topics disabled — always output {passWord}.\n\n[New Speech]:\n{message}"}let p="You are an RP Continuity Checker.\nRATE: PG-13 ~ NC-21\n\n## Scope\nYou receive:\n- [Lore]: partial character/world data\n- [Summaries]: memory snapshots\n- [Recent Context]: last N turns of dialogue\n- [New Speech]: the latest AI-generated RP response\n\n## Rule\nFlag an error ONLY when [New Speech] DIRECTLY CONTRADICTS a fact explicitly stated in [Lore] or [Summaries].\n";if(hasLogic){p+="\n## What Counts as an Error\n";logicKeys.forEach(k=>{p+=_LOGIC_BLOCKS[k]+"\n"})}if(hasRepair){p+="\n## Truncation Repair\n";repairKeys.forEach(k=>{p+=_REPAIR_BLOCKS[k]+"\n"})}p+='\n## Output Format\nReason MUST be in Korean.\nNo issues/repairs needed:\n{passWord}\n\nIssues found (no markdown code fences):\n{"reason":"교정 이유","replacements":[{"from":"원문의 정확한 부분","to":"수정본"}]}\n\n[Lore]:\n{lore}\n\n[Summaries]:\n{memory}\n\n[Recent Context]:\n{context}\n\n[New Speech]:\n{message}';return p}const DEFAULT_PROMPT=TEMPLATES.full.prompt;const LEGACY_PROMPTS=[`You are an RP Continuity Checker.\nRATE: PG-13 ~ NC-21\n\n## Scope\nYou receive:\n- [Lore]: partial character/world data\n- [Summaries]: memory snapshots\n- [Recent Context]: last N turns of dialogue\n- [New Speech]: the latest AI-generated RP response\n\n## Rule\nFlag an error ONLY when [New Speech] DIRECTLY CONTRADICTS a fact explicitly stated in [Lore] or [Summaries].\n\n## What Counts as an Error\n1. EXPLICIT FACTUAL CONTRADICTION\n2. PRESENCE / ABSENCE CONTRADICTION\n3. NICKNAME / TITLE MISMATCH\n4. STATE CONTRADICTION\n5. PROMISE / ARC CONTRADICTION\n6. REUNION VIOLATION: Characters who already know each other (per [Lore] rel entries or [Reunion] tag) behaving as strangers — self-introduction, "처음 뽵겙습니다", unfamiliarity.\n7. VOCATIVE CONTINUITY CHECK: A previous nickname/insult is context, not a permanent requirement. Current name/title is valid when recent context supports normalization, direct name use, emotional cooling, formal setting, or a new scene.\n8. USER IMPERSONATION: AI narrated the user's character's actions, decisions, dialogue, or internal thoughts WITHOUT user's explicit prior input.\n Violation examples: "당신은 웃으며 대답했다" when user never said they laughed; "내심 설렴이 일었다" when user expressed no such feeling.\n Allowed: describing physical cues the AI's character OBSERVES in the user (e.g., "당신의 눈빛을 보며"), but NOT internal states the AI cannot know.\n\n## Truncation Repair\nA) SENTENCE TRUNCATION: Complete ONLY the final interrupted sentence.\nB) STATUS BLOCK TRUNCATION: Restore truncated/absent status blocks using [Recent Context].\n\n## Output Format\nReason MUST be in Korean.\nNo issues/repairs needed:\n{passWord}\n\nIssues found (no markdown code fences):\n{"reason":"교정 이유","replacements":[{"from":"원문의 정확한 부분","to":"수정본"}]}\n\n[Lore]:\n{lore}\n\n[Summaries]:\n{memory}\n\n[Recent Context]:\n{context}\n\n[New Speech]:\n{message}`,`You are an RP Continuity Checker.\nRATE: PG-13 ~ NC-21\n\n## Scope\nYou receive:\n- [Lore]: partial character/world data\n- [Summaries]: memory snapshots\n- [Recent Context]: last N turns of dialogue\n- [New Speech]: the latest AI-generated RP response\n\n## Rule\nFlag an error ONLY when [New Speech] DIRECTLY CONTRADICTS a fact explicitly stated in [Lore] or [Summaries].\n\n## What Counts as an Error\n1. EXPLICIT FACTUAL CONTRADICTION\n2. PRESENCE / ABSENCE CONTRADICTION\n3. NICKNAME / TITLE MISMATCH\n4. STATE CONTRADICTION\n5. PROMISE / ARC CONTRADICTION\n\n## Truncation Repair\nA) SENTENCE TRUNCATION: Complete ONLY the final interrupted sentence.\nB) STATUS BLOCK TRUNCATION: Restore truncated/absent status blocks using [Recent Context].\n\n## Output Format\nReason MUST be in Korean.\nNo issues/repairs needed:\n{passWord}\n\nIssues found (no markdown code fences):\n{"reason":"교정 이유","replacements":[{"from":"원문의 정확한 부분","to":"수정본"}]}\n\n[Lore]:\n{lore}\n\n[Summaries]:\n{memory}\n\n[Recent Context]:\n{context}\n\n[New Speech]:\n{message}`,`You are an RP Continuity Checker.\nRATE: PG-13 ~ NC-21\n\n## Scope\nYou receive:\n- [Lore]: partial character/world data\n- [Summaries]: memory snapshots\n- [Recent Context]: last N turns of dialogue\n- [New Speech]: the latest AI-generated RP response\n\n## Rule\nFlag an error ONLY when [New Speech] DIRECTLY CONTRADICTS a fact explicitly stated in [Lore] or [Summaries].\n\n## What Counts as an Error\n1. EXPLICIT FACTUAL CONTRADICTION\n2. PRESENCE / ABSENCE CONTRADICTION\n3. NICKNAME / TITLE MISMATCH\n4. STATE CONTRADICTION\n5. PROMISE / ARC CONTRADICTION\n6. REUNION VIOLATION: Characters who already know each other (per [Lore] rel entries or [Reunion] tag) behaving as strangers — self-introduction, "처음 뵙겠습니다", unfamiliarity.\n7. VOCATIVE CONTINUITY CHECK: A previous nickname/insult is context, not a permanent requirement. Current name/title is valid when recent context supports normalization, direct name use, emotional cooling, formal setting, or a new scene.\n\n## Truncation Repair\nA) SENTENCE TRUNCATION: Complete ONLY the final interrupted sentence.\nB) STATUS BLOCK TRUNCATION: Restore truncated/absent status blocks using [Recent Context].\n\n## Output Format\nReason MUST be in Korean.\nNo issues/repairs needed:\n{passWord}\n\nIssues found (no markdown code fences):\n{"reason":"교정 이유","replacements":[{"from":"원문의 정확한 부분","to":"수정본"}]}\n\n[Lore]:\n{lore}\n\n[Summaries]:\n{memory}\n\n[Recent Context]:\n{context}\n\n[New Speech]:\n{message}`];_w.__LoreRefiner.PROMPT_VERSION=PROMPT_VERSION;_w.__LoreRefiner.TEMPLATES=TEMPLATES;_w.__LoreRefiner.TOPICS=TOPICS;_w.__LoreRefiner.buildDynamicPrompt=buildDynamicPrompt;_w.__LoreRefiner.DEFAULT_PROMPT=DEFAULT_PROMPT;_w.__LoreRefiner.LEGACY_PROMPTS=LEGACY_PROMPTS;_w.__LoreRefiner.__promptsLoaded=true})();(function(){"use strict";const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const R=_w.__LoreRefiner=_w.__LoreRefiner||{};if(R.__domLoaded)return;function escapeHTML(value){return String(value==null?"":value).replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function normalizeText(text){return String(text||"").replace(/\s+/g," ").trim()}function stripMarkdown(text){return String(text||"").replace(/`{3}\w*\n?/g,"").replace(/`([^`]+)`/g,"$1").replace(/\*\*([^*]+)\*\*/g,"$1").replace(/\*([^*]+)\*/g,"$1").replace(/~~([^~]+)~~/g,"$1").replace(/^#+\s+/gm,"").replace(/^[-*+]\s+/gm,"").replace(/^\d+\.\s+/gm,"").replace(/^>\s+/gm,"").replace(/\[([^\]]+)\]\([^)]+\)/g,"$1").replace(/!\[([^\]]*)\]\([^)]+\)/g,"$1").replace(/\n{2,}/g,"\n").trim()}function getMessageContainer(el){if(!el)return null;return el.closest?.('[data-message-id], [data-id], [data-testid*="message"], [class*="message"], article, section, li')||el.closest?.("div")||el}function findMessageContainerById(messageId){if(!messageId)return null;const id=String(messageId);const candidates=document.querySelectorAll("[data-message-id], [data-id], [id], [href], [data-testid]");for(const el of candidates){const vals=[el.getAttribute("data-message-id"),el.getAttribute("data-id"),el.getAttribute("id"),el.getAttribute("href"),el.getAttribute("data-testid")].filter(Boolean);if(vals.some(v=>String(v).includes(id)))return getMessageContainer(el)}const marked=document.querySelector(`[data-lore-refiner-message-id="${id.replace(/"/g,'\\"')}"]`);return marked?getMessageContainer(marked):null}function findDeepestMatchingElement(searchPlainText,root){const plain=normalizeText(searchPlainText);const snippet=(plain.length>36?plain.slice(-36):plain).trim();if(!snippet||snippet.length<5)return null;let best=null;let bestScore=Infinity;const scope=root||document;const all=scope.querySelectorAll?scope.querySelectorAll("div, p, article, section, span, li"):[];for(const el of all){const text=normalizeText(el.textContent);if(!text||!text.includes(snippet))continue;if(el.tagName==="BODY"||el.tagName==="HTML")continue;if(el.id==="__next"||el.id==="root")continue;const childCount=el.querySelectorAll("*").length;const score=text.length+childCount*50;if(score"+code+"");return"@@LRFENCE"+idx+"@@"});html=html.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g,function(_m,alt,src){return''+alt+''});html=html.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g,'$1');html=html.replace(/^######\s+(.+)$/gm,"
$1
");html=html.replace(/^#####\s+(.+)$/gm,"
$1
");html=html.replace(/^####\s+(.+)$/gm,"

$1

");html=html.replace(/^###\s+(.+)$/gm,"

$1

");html=html.replace(/^##\s+(.+)$/gm,"

$1

");html=html.replace(/^#\s+(.+)$/gm,"

$1

");html=html.replace(/^>\s+(.+)$/gm,"
$1
");html=html.replace(/`([^`\n]+)`/g,"$1").replace(/\*\*(.+?)\*\*/g,"$1").replace(/\*(.+?)\*/g,"$1").replace(/~~(.+?)~~/g,"$1").replace(/\n/g,"
");html=html.replace(/@@LRFENCE(\d+)@@(
)?/g,function(_m,idx){return fences[+idx]});return html}function isTextVisible(text,messageId){const plain=stripMarkdown(text);const snippet=normalizeText(plain.length>36?plain.slice(-36):plain);if(!snippet)return false;const container=findMessageContainerById(messageId);if(container&&normalizeText(container.textContent).includes(snippet))return true;return!!findDeepestMatchingElement(plain)}function waitForVisibleText(text,messageId,timeoutMs){const timeout=timeoutMs||3500;const started=Date.now();return new Promise(resolve=>{const tick=()=>{if(isTextVisible(text,messageId))return resolve(true);if(Date.now()-started>=timeout)return resolve(false);setTimeout(tick,250)};tick()})}function rememberAssistantMessage(messageId,text){const el=findDeepestMatchingElement(stripMarkdown(text));const container=getMessageContainer(el);if(container&&messageId){try{container.setAttribute("data-lore-refiner-message-id",String(messageId))}catch(_){}}return container}function refreshMessageInDOM(originalText,newText,messageId){const oldPlain=stripMarkdown(originalText);const newPlain=stripMarkdown(newText);const oldSnippet=normalizeText(oldPlain.length>36?oldPlain.slice(-36):oldPlain);const newSnippet=normalizeText(newPlain.length>36?newPlain.slice(-36):newPlain);const renderedHTML=renderMarkdownHTML(newText);let targetEl=null;const allMds=document.querySelectorAll(".wrtn-markdown");if(oldSnippet){for(const md of allMds){const t=normalizeText(md.textContent);if(t.includes(oldSnippet)&&(!newSnippet||!t.includes(newSnippet))){targetEl=md;break}}}if(!targetEl&&oldSnippet){for(const md of allMds){if(normalizeText(md.textContent).includes(oldSnippet)){targetEl=md;break}}}if(!targetEl&&newSnippet){for(const md of allMds){if(normalizeText(md.textContent).includes(newSnippet)){try{if(messageId){const c=getMessageContainer(md);if(c)c.setAttribute("data-lore-refiner-message-id",String(messageId))}}catch(_){}return{applied:false,visible:true,status:"done",messageId:messageId||null}}}}if(!targetEl||!document.contains(targetEl)){return{applied:false,visible:false,status:"not_found",messageId:messageId||null}}try{targetEl.innerHTML=renderedHTML;try{if(messageId){const c=getMessageContainer(targetEl);if(c)c.setAttribute("data-lore-refiner-message-id",String(messageId))}}catch(_){}}catch(_){}return{applied:true,visible:true,status:"applied",messageId:messageId||null}}function tryStoreUpdate(rootEl,messageId,newText){if(!messageId)return false;let updated=false;const result=runPath0Mutation(messageId,newText);if(result.hits>0)updated=true;_w.__LR_LAST_PATH0=result;let rerenderHits=0;const opBudget={count:0,max:2e5};let storeBHits=0;let pathCHits=0;let pathCMatches=0;let pathCSameRef=0;let pathEHits=0;let pathFHits=0;let pathFTargets=0;const isMsgRef=v=>{if(!v||typeof v!=="object")return false;try{return v._id===messageId||v.id===messageId||v.messageId===messageId||v.msgId===messageId}catch(_){return false}};const containsMsgDeep=(val,depth,seen)=>{if(++opBudget.count>opBudget.max)return false;if(!val||typeof val!=="object"||depth>14)return false;if(seen.has(val))return false;seen.add(val);if(isMsgRef(val))return true;try{if(Array.isArray(val)){for(let i=0;i{if(++opBudget.count>opBudget.max)return val;if(!val||typeof val!=="object"||depth>14)return val;if(seen.has(val))return seen.get(val);if(isMsgRef(val)){if(!replacementMsg){try{replacementMsg=Object.assign({},val,{content:newText})}catch(_){replacementMsg=val}}seen.set(val,replacementMsg);return replacementMsg}try{if(Array.isArray(val)){seen.set(val,val);let changed=false;const out=new Array(val.length);const cap=Math.min(val.length,500);for(let i=0;ik.startsWith("__reactFiber$"));if(fk){let f=el[fk];let depth=0;while(f&&depth<80){ancestors.push(f);if(f.alternate)ancestors.push(f.alternate);f=f.return;depth++}break}el=el.parentElement}const seenHooks=new WeakSet;const seenStore=new WeakSet;const seenProps=new WeakSet;const tryStoreLikeDispatch=v=>{if(!v||typeof v!=="object"||seenStore.has(v))return false;seenStore.add(v);let did=false;try{if(typeof v.getState==="function"&&typeof v.setState==="function"&&typeof v.subscribe==="function"){const state=v.getState();if(state&&typeof state==="object"){const next=cloneWithMsgReplaced(state,0,new WeakMap);if(next!==state){v.setState(next,true);did=true}}}}catch(_){}try{if(v.constructor&&v.constructor.name==="QueryClient"&&typeof v.getQueryCache==="function"){const cache=v.getQueryCache();const queries=cache&&cache.getAll&&cache.getAll()||[];for(const q of queries){const data=q.state&&q.state.data;if(data&&typeof data==="object"){const next=cloneWithMsgReplaced(data,0,new WeakMap);if(next!==data){v.setQueryData(q.queryKey,next);did=true}}}}}catch(_){}return did};for(const f of ancestors){let h=f.memoizedState;let hd=0;while(h&&hd<60){if(!seenHooks.has(h)&&h.queue&&typeof h.queue.dispatch==="function"){seenHooks.add(h);const cur=h.memoizedState;if(cur&&typeof cur==="object"){try{const next=cloneWithMsgReplaced(cur,0,new WeakMap);if(next!==cur){h.queue.dispatch(next);rerenderHits++;updated=true}}catch(_){}}}h=h.next;hd++}try{const t=f.type;const isProvider=t&&typeof t==="object"&&(t.$$typeof===Symbol.for("react.provider")||t._context);if(isProvider&&f.memoizedProps){const v=f.memoizedProps.value;if(v&&typeof v==="object"){if(tryStoreLikeDispatch(v)){rerenderHits++;updated=true}}}}catch(_){}try{const mp=f.memoizedProps;if(mp&&typeof mp==="object"&&!seenProps.has(mp)){seenProps.add(mp);for(const k of["store","client","queryClient"]){if(tryStoreLikeDispatch(mp[k])){rerenderHits++;updated=true}}}}catch(_){}if(rerenderHits>=5)break}if(rerenderHits===0){const collectRoots=()=>{const out=[];const add=el=>{if(!el)return;try{const k=Object.keys(el).find(k=>k.startsWith("__reactContainer$"));if(k&&el[k]&&el[k].stateNode)out.push(el[k].stateNode.current)}catch(_){}try{if(el._reactRootContainer&&el._reactRootContainer._internalRoot)out.push(el._reactRootContainer._internalRoot.current)}catch(_){}};add(document.getElementById("__next"));add(document.getElementById("root"));document.querySelectorAll("body > div, body > main").forEach(add);return out};const isProviderType=t=>{try{return t&&typeof t==="object"&&(t.$$typeof===Symbol.for("react.provider")||!!t._context)}catch(_){return false}};const seenFibersB=new WeakSet;const rootsB=collectRoots();outer:for(const root of rootsB){const stack=[root];while(stack.length&&opBudget.count=5)break outer}}}try{if(rerenderHits===0){const collectRootsC=()=>{const out=[];const add=el=>{if(!el)return;try{const k=Object.keys(el).find(k=>k.startsWith("__reactContainer$"));if(k&&el[k]&&el[k].stateNode)out.push(el[k].stateNode.current)}catch(_){}try{if(el._reactRootContainer&&el._reactRootContainer._internalRoot)out.push(el._reactRootContainer._internalRoot.current)}catch(_){}};add(document.getElementById("__next"));add(document.getElementById("root"));document.querySelectorAll("body > div, body > main").forEach(add);return out};const seenFibersC=new WeakSet;const seenHooksC=new WeakSet;const rootsC=collectRootsC();outer2:for(const root of rootsC){const stack=[root];while(stack.length&&opBudget.count=3)break outer2}else{pathCSameRef++}}}catch(_){}}}h=h.next;hd++}if(f.child)stack.push(f.child);if(f.sibling)stack.push(f.sibling)}}}}catch(_){}if(rerenderHits===0&&result.hits>0){try{const seenFibersF=new WeakSet;const hitFibers=result&&result._propsHitFibers||[];const targets=[];for(let i=0;i{if(!f||seenFibersF.has(f)||pathFHits>=24)return;seenFibersF.add(f);for(const pk of["memoizedProps","pendingProps"]){try{const cur=f[pk];if(cur&&typeof cur==="object"&&containsMsgDeep(cur,0,new WeakSet)){replacementMsg=null;const next=cloneWithMsgReplaced(cur,0,new WeakMap);if(next!==cur){f[pk]=next;pathFHits++;updated=true}}}catch(_){}}};for(let i=0;i0){try{let f=ancestors[0];let dpt=0;outerE:while(f&&dpt<5){let h=f.memoizedState;let hd=0;while(h&&hd<60){if(h.queue&&typeof h.queue.dispatch==="function"){try{const cur=h.memoizedState;if(cur&&typeof cur==="object"){let next;if(Array.isArray(cur))next=cur.slice();else if(cur instanceof Map)next=new Map(cur);else if(cur instanceof Set)next=new Set(cur);else next=Object.assign({},cur);h.queue.dispatch(next);pathEHits++;rerenderHits++;updated=true;if(pathEHits>=2)break outerE}}catch(_){}}h=h.next;hd++}f=f.return;dpt++}}catch(_){}}}}catch(e){}_w.__LR_LAST_RERENDER_HITS=rerenderHits;_w.__LR_LAST_OPS=opBudget.count;return updated}function runPath0Mutation(messageId,newText){const summary={roots:0,fibersVisited:0,hooksScanned:0,propsScanned:0,isMsgEncounters:0,hits:0,errors:0,mutationErrors:0,propsHitFibers:0};const propsHitFibers=[];const propsHitSeen=new WeakSet;try{Object.defineProperty(summary,"_propsHitFibers",{value:propsHitFibers,enumerable:false})}catch(_){}if(!messageId)return summary;const isMsg=v=>{if(!v||typeof v!=="object")return false;try{return v.id===messageId||v._id===messageId||v.messageId===messageId||v.msgId===messageId}catch(_){return false}};const fieldKeyOf=v=>{try{if("content"in v)return"content";if("message"in v)return"message";if("text"in v)return"text";if("body"in v)return"body"}catch(_){}return null};let totalObjs=0;const OBJ_BUDGET=2e5;const mutate=(val,depth,localSeen,ctx)=>{if(!val||depth>14||typeof val!=="object")return;if(totalObjs++>OBJ_BUDGET)return;if(localSeen.has(val))return;localSeen.add(val);let isM=false;try{isM=isMsg(val)}catch(_){summary.errors++}if(isM){summary.isMsgEncounters++;try{if(ctx&&ctx.fiber&&!propsHitSeen.has(ctx.fiber)){propsHitSeen.add(ctx.fiber);propsHitFibers.push(ctx.fiber);summary.propsHitFibers=propsHitFibers.length}}catch(_){}const fk=fieldKeyOf(val);if(fk){let cur;try{cur=val[fk]}catch(_){cur=undefined;summary.errors++}if(cur!==newText){try{val[fk]=newText;summary.hits++}catch(_){summary.mutationErrors++}}}}let isArr=false;try{isArr=Array.isArray(val)}catch(_){}if(isArr){for(let i=0;i{if(!el)return;try{const k=Object.keys(el).find(k=>k.startsWith("__reactContainer$"));if(k&&el[k]&&el[k].stateNode)roots.push(el[k].stateNode.current)}catch(_){}try{if(el._reactRootContainer&&el._reactRootContainer._internalRoot)roots.push(el._reactRootContainer._internalRoot.current)}catch(_){}};tryAddRoot(document.getElementById("__next"));tryAddRoot(document.getElementById("root"));document.querySelectorAll("body > div, body > main").forEach(tryAddRoot);summary.roots=roots.length;const seenFibers=new WeakSet;let visited=0;const VISIT_CAP=5e4;for(const root of roots){const stack=[root];while(stack.length&&visited{host.dispatchEvent(new MouseEvent(t,{bubbles:true,cancelable:true,view:window}))})}catch(_){}const labelOf=el=>{if(!el)return"";const parts=[el.textContent,el.getAttribute&&el.getAttribute("aria-label"),el.getAttribute&&el.getAttribute("title"),el.getAttribute&&el.getAttribute("data-testid")].filter(Boolean);return parts.join(" ").trim()};const findButton=(root,re)=>{if(!root||!root.querySelectorAll)return null;const buttons=root.querySelectorAll('button, [role="button"], [aria-label], [title]');for(const b of buttons){try{if(re.test(labelOf(b)))return b}catch(_){}}return null};const editBtn=findButton(host,/(수정|편집|edit)/i)||findButton(host.parentElement,/(수정|편집|edit)/i);if(!editBtn)return false;try{editBtn.click()}catch(_){return false}setTimeout(()=>{try{const cancelBtn=findButton(host,/(취소|cancel|닫기|close)/i)||findButton(document,/(취소|cancel)/i);if(cancelBtn){try{cancelBtn.click();return}catch(_){}}const esc=new KeyboardEvent("keydown",{key:"Escape",code:"Escape",bubbles:true,cancelable:true});try{(document.activeElement||document.body).dispatchEvent(esc)}catch(_){}try{document.dispatchEvent(esc)}catch(_){}}catch(_){}},180);return true}catch(_){return false}}function showReloadAction(message){const old=document.querySelector("#refiner-reload-action");if(old)old.remove();const box=document.createElement("div");box.id="refiner-reload-action";box.style.cssText="position:fixed;right:18px;bottom:90px;z-index:999999;background:#1a1a1a;color:#ddd;border:1px solid #444;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.45);padding:12px;max-width:320px;font-size:12px;line-height:1.45;";const text=document.createElement("div");text.textContent=message||"서버 수정 완료. 화면이 아직 예전 응답이면 새로고침으로 반영하세요.";text.style.cssText="margin-bottom:10px;color:#ddd;";const row=document.createElement("div");row.style.cssText="display:flex;justify-content:flex-end;gap:8px;";const close=document.createElement("button");close.textContent="닫기";close.style.cssText="padding:7px 10px;border:0;border-radius:5px;background:#444;color:#ddd;cursor:pointer;";close.onclick=()=>box.remove();const reload=document.createElement("button");reload.textContent="새로고침";reload.style.cssText="padding:7px 10px;border:0;border-radius:5px;background:#285;color:white;font-weight:bold;cursor:pointer;";reload.onclick=()=>location.reload();row.appendChild(close);row.appendChild(reload);box.appendChild(text);box.appendChild(row);document.body.appendChild(box);setTimeout(()=>{if(document.body.contains(box))box.remove()},2e4)}function showRefineConfirm(reason,refinedText,onConfirm,onCancel){const overlay=document.createElement("div");overlay.id="refiner-confirm-overlay";overlay.style.cssText="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);z-index:999999;display:flex;justify-content:center;align-items:center;padding:20px;box-sizing:border-box;";const box=document.createElement("div");box.style.cssText="background:#1a1a1a;border:1px solid #333;border-radius:8px;width:100%;max-width:400px;padding:20px;box-shadow:0 10px 25px rgba(0,0,0,0.5);display:flex;flex-direction:column;gap:12px;";const title=document.createElement("div");title.textContent="AI 응답 교정 제안";title.style.cssText="font-size:16px;font-weight:bold;color:#4a9;margin-bottom:4px;";const reasonTitle=document.createElement("div");reasonTitle.textContent="교정 이유:";reasonTitle.style.cssText="font-size:12px;color:#aaa;font-weight:bold;";const reasonText=document.createElement("div");reasonText.textContent=reason;reasonText.style.cssText="font-size:13px;color:#ccc;background:#222;padding:8px;border-radius:4px;";const refTitle=document.createElement("div");refTitle.textContent="수정된 응답:";refTitle.style.cssText="font-size:12px;color:#aaa;font-weight:bold;margin-top:8px;";const refTa=document.createElement("textarea");refTa.value=refinedText;refTa.style.cssText="width:100%;height:100px;background:#0a0a0a;color:#fff;border:1px solid #444;border-radius:4px;padding:8px;font-size:13px;resize:vertical;box-sizing:border-box;font-family:inherit;";const btnRow=document.createElement("div");btnRow.style.cssText="display:flex;justify-content:flex-end;gap:10px;margin-top:12px;";const btnCancel=document.createElement("button");btnCancel.textContent="원본 유지";btnCancel.style.cssText="padding:10px 16px;border-radius:6px;border:none;background:#444;color:#ccc;cursor:pointer;font-weight:bold;";btnCancel.onclick=()=>{document.body.removeChild(overlay);onCancel()};const btnConfirm=document.createElement("button");btnConfirm.textContent="교정본 변경";btnConfirm.style.cssText="padding:10px 16px;border-radius:6px;border:none;background:#285;color:#fff;cursor:pointer;font-weight:bold;";btnConfirm.onclick=()=>{document.body.removeChild(overlay);onConfirm(refTa.value)};btnRow.appendChild(btnCancel);btnRow.appendChild(btnConfirm);box.appendChild(title);box.appendChild(reasonTitle);box.appendChild(reasonText);box.appendChild(refTitle);box.appendChild(refTa);box.appendChild(btnRow);overlay.appendChild(box);document.body.appendChild(overlay)}R.escapeHTML=escapeHTML;R.stripMarkdown=stripMarkdown;R.findMessageContainerById=findMessageContainerById;R.findDeepestMatchingElement=findDeepestMatchingElement;R.renderMarkdownHTML=renderMarkdownHTML;R.isTextVisible=isTextVisible;R.waitForVisibleText=waitForVisibleText;R.rememberAssistantMessage=rememberAssistantMessage;R.refreshMessageInDOM=refreshMessageInDOM;R.tryStoreUpdate=tryStoreUpdate;R.runPath0Mutation=runPath0Mutation;R.nudgeMessageNativeRender=nudgeMessageNativeRender;R.showReloadAction=showReloadAction;R.showRefineConfirm=showRefineConfirm;R.__version="v20";R.__domLoaded=true})();(function(){"use strict";const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const R=_w.__LoreRefiner=_w.__LoreRefiner||{};if(!R.__promptsLoaded){console.error("[Refiner:core] prompts 미로드");return}if(!R.__domLoaded){console.error("[Refiner:core] dom 미로드");return}if(R.__coreLoaded)return;let Core=null;let ConfigGetter=null;let LogCallback=null;let ToastCallback=null;let GetActivePacksCallback=null;const _ls=typeof unsafeWindow!=="undefined"?unsafeWindow.localStorage:localStorage;const _PROCESSED_KEY_LEGACY="speech-refiner-processed";const _PROCESSED_KEY_PREFIX="speech-refiner-processed:";const PROCESSED_CAP_PER_CHAT=50;const _processedByChat=new Map;function _currentChatKey(){try{return Core&&Core.getCurrentChatId&&Core.getCurrentChatId()||"global"}catch(_){return"global"}}function _loadChat(chatKey){if(_processedByChat.has(chatKey))return _processedByChat.get(chatKey);const set=new Set;try{const raw=_ls.getItem(_PROCESSED_KEY_PREFIX+chatKey);if(raw)JSON.parse(raw).forEach(fp=>set.add(fp))}catch(_){}_processedByChat.set(chatKey,set);return set}try{const legacy=_ls.getItem(_PROCESSED_KEY_LEGACY);if(legacy){const set=_loadChat("global");JSON.parse(legacy).forEach(fp=>set.add(fp));const arr=Array.from(set).slice(-PROCESSED_CAP_PER_CHAT);_ls.setItem(_PROCESSED_KEY_PREFIX+"global",JSON.stringify(arr));_ls.removeItem(_PROCESSED_KEY_LEGACY)}}catch(_){}function saveProcessedFingerprints(chatKeyOverride){const chatKey=chatKeyOverride||_currentChatKey();const set=_processedByChat.get(chatKey);if(!set)return;if(set.size>PROCESSED_CAP_PER_CHAT){const arr=Array.from(set).slice(-PROCESSED_CAP_PER_CHAT);set.clear();arr.forEach(v=>set.add(v))}try{_ls.setItem(_PROCESSED_KEY_PREFIX+chatKey,JSON.stringify(Array.from(set)))}catch(_){}}const _activeRefines=new Set;function _refineLockKey(chatId,messageId,text){return String(chatId||"global")+":"+String(messageId||(text||"").slice(0,80))}function _acquireRefineLock(chatId,messageId,text){const key=_refineLockKey(chatId,messageId,text);if(_activeRefines.has(key))return null;_activeRefines.add(key);return function releaseRefineLock(){_activeRefines.delete(key)}}function formatCallStateForRefiner(entry){const out=[];const addState=(key,raw)=>{const obj=raw&&typeof raw==="object"&&!Array.isArray(raw)?raw:{};const current=obj.currentTerm||obj.term||obj.current||obj.call||(typeof raw==="string"?raw:"");if(!current)return;let prev=obj.previousTerms||obj.previous||obj.prev||[];if(typeof prev==="string")prev=[prev];if(!Array.isArray(prev))prev=[];prev=prev.filter(x=>x&&x!==current);const meta=[];if(obj.tone)meta.push(`tone=${obj.tone}`);if(obj.scope)meta.push(`scope=${obj.scope}`);if(obj.lastChangedTurn||obj.turn)meta.push(`changed=t${obj.lastChangedTurn||obj.turn}`);if(obj.confidence!=null)meta.push(`confidence=${obj.confidence}`);if(obj.reason)meta.push(`reason=${obj.reason}`);out.push(`${key}: current=${current}${prev.length?`; previous-context=${prev.join(", ")}`:""}${meta.length?`; ${meta.join("; ")}`:""}`)};const callState=entry.callState||entry.detail?.callState;if(Array.isArray(callState)){callState.forEach(cs=>addState(`${cs.from||cs.speaker||"?"}→${cs.to||cs.target||cs.addressee||"?"}`,cs))}else if(callState&&typeof callState==="object"){if(callState.currentTerm||callState.term)addState(`${callState.from||callState.speaker||"?"}→${callState.to||callState.target||callState.addressee||"?"}`,callState);else Object.entries(callState).forEach(([k,v])=>addState(k,v))}const hist=Array.isArray(entry.callHistory)?entry.callHistory:[];if(hist.length){const byKey={};hist.forEach(h=>{if(!h||!h.from||!h.to||!h.term)return;const k=`${h.from}→${h.to}`;(byKey[k]=byKey[k]||[]).push(h)});Object.entries(byKey).forEach(([k,arr])=>{arr.sort((a,b)=>(a.turn||0)-(b.turn||0));const last=arr[arr.length-1];const previousTerms=Array.from(new Set(arr.slice(0,-1).map(x=>x.term).filter(t=>t&&t!==last.term)));addState(k,{...last,currentTerm:last.term,previousTerms:previousTerms})})}return Array.from(new Set(out)).join(" | ")}function buildCallChangeContext(entries,recentMsgs){const hasCallData=entries.some(e=>e.call||e.callState||e.detail?.nicknames||e.detail?.callState||Array.isArray(e.callHistory)&&e.callHistory.length);if(!hasCallData)return"";const recent=(recentMsgs||[]).slice(-4).map(m=>`${m.role}: ${m.message||""}`).join("\n");return`[Call Continuity Context]\nPrevious nicknames/insults are context, not permanent requirements. Prefer the latest stable call-state. Current name/title is valid when recent context shows normalization, direct name use, emotional cooling, formal setting, or a new scene.\n${recent}`}function renderLoreForRefiner(entries){const L={personality:"성격",attributes:"특성",abilities:"능력",current_state:"현재",last_interaction:"최근",current_status:"현재 상태",nicknames:"호칭",relations:"관계",background_or_history:"배경",maker:"약속자",target:"대상",condition:"발동 조건",status:"상태",resolution:"결과",parties:"관계자",ingredients:"재료",steps:"순서",tips:"참고",rules:"규칙",effects:"효과",callState:"호칭상태"};return entries.map(e=>{const callStateText=formatCallStateForRefiner(e);if(e.inject?.full){let line=`[${e.type}] ${e.name}: ${e.inject.full}`;if(e.state)line+=` (${e.state})`;if(e.call){const c=Object.entries(e.call).map(([k,v])=>`${k}:current=${v}`).join(", ");line+=` | 호칭(current only; previous terms are context): ${c}`}if(callStateText)line+=` | callState(latest stable): ${callStateText}`;if(e.cond)line+=` | 조건: ${e.cond}`;return line}const d=e.detail||{};let line="["+(e.type||"entity")+"] "+e.name+": "+(e.summary||"");for(const[k,v]of Object.entries(d)){if(v==null||v==="")continue;const lb=L[k]||k;if(Array.isArray(v)){if(!v.length)continue;line+=" | "+lb+": "+(typeof v[0]==="object"?v.map(x=>Object.values(x).filter(Boolean).join(" / ")).join(" → "):v.join(", "))}else if(typeof v==="object"){const f=Object.entries(v).map(([a,b])=>a+": "+b).join(", ");if(f)line+=" | "+lb+": "+f}else line+=" | "+lb+": "+String(v)}if(e.call){const c=Object.entries(e.call).map(([k,v])=>`${k}:current=${v}`).join(", ");line+=` | 호칭(current only; previous terms are context): ${c}`}if(callStateText)line+=` | callState(latest stable): ${callStateText}`;return line}).join("\n")}function matchEntriesByTrigger(entries,recentMsgs,text){const pool=[text.toLowerCase(),...recentMsgs.map(m=>(m.message||"").toLowerCase())].join(" ");return entries.filter(e=>{if(!e.triggers||!e.triggers.length)return false;for(const t of e.triggers){if(!t||t.length<2)continue;if(t.split("&&").map(p=>p.trim().toLowerCase()).every(p=>pool.includes(p)))return true}return false})}async function refineMessage(assistantText,force,enqueueCallback){const config=ConfigGetter();if(!config.refinerEnabled&&!force)return;Core.showStatusBadge("에리가 문장 훑는 중");const chatRoomId=Core.getCurrentChatId();if(!chatRoomId){Core.hideStatusBadge();return}const url=Core.getCurUrl();let targetLog=null;try{targetLog=await CrackUtil.chatRoom().findLastBotMessage(chatRoomId)}catch(_){}const targetMsgId=targetLog&&!(targetLog instanceof Error)?targetLog.id:"";const releaseRefineLock=_acquireRefineLock(chatRoomId,targetMsgId,assistantText);if(!releaseRefineLock){R.lastState={state:"skipped",detail:"duplicate refiner call blocked",at:Date.now(),queue:R.refineQueue?R.refineQueue.length:0,busy:!!R.workerBusy};Core.hideStatusBadge();return}let holdRefineLock=false;let loreText="활성화된 로어 없음.";let activeEntries=[];try{if(GetActivePacksCallback){try{const activePacks=GetActivePacksCallback(url);if(activePacks.length>0){const db=Core.getDB();const entries=await db.entries.toArray();activeEntries=entries.filter(e=>activePacks.includes(e.packName))}}catch(e){}}if(activeEntries.length>0){const matchTurns=config.refinerMatchTurns||5;const _tMsgs=await Core.fetchLogs(Math.max(4,matchTurns*2));let _lE=[];if(config.refinerLoreMode==="semantic"&&config.embeddingEnabled){const apiOpts={apiType:config.autoExtApiType||"key",key:config.autoExtKey,vertexJson:config.autoExtVertexJson,vertexLocation:config.autoExtVertexLocation||"global",vertexProjectId:config.autoExtVertexProjectId,firebaseScript:config.autoExtFirebaseScript,firebaseEmbedKey:config.autoExtFirebaseEmbedKey,model:config.embeddingModel||"gemini-embedding-001",costContext:{feature:"embed",chatKey:chatRoomId||"global"}};const searchConfig={scanRange:matchTurns,strictMatch:true,similarityMatch:true,embeddingEnabled:true,embeddingWeight:.5};try{const searchResult=await Core.hybridSearch(assistantText,_tMsgs,activeEntries,searchConfig,apiOpts);_lE=searchResult.scored.slice(0,10).map(s=>s.entry)}catch(e){console.warn("[Refiner] Semantic search failed, falling back to trigger match.")}}if(_lE.length===0){const autoPacks=config.autoPacks||["자동추출"];const _fE=activeEntries.filter(x=>!autoPacks.includes(x.packName));const _aE=activeEntries.filter(x=>autoPacks.includes(x.packName));const _mF=matchEntriesByTrigger(_fE,_tMsgs,assistantText);const _mA=matchEntriesByTrigger(_aE,_tMsgs,assistantText);_lE=config.refinerLoreMode==="matchedOnly"?[..._mF,..._mA]:[..._mF,..._aE]}if(_lE.length>0)loreText=renderLoreForRefiner(_lE);else loreText="(키워드 매칭된 로어 없음)"}Core.showStatusBadge("에리가 기억 꺼내는 중");let memoryText="메모리 없음.";try{const mems=await Core.fetchAllMemories(chatRoomId);const parts=[];if(mems.goal?.length)parts.push(`[목표]:\n${mems.goal.join("\n")}`);if(mems.shortTerm?.length)parts.push(`[단기기억]:\n${mems.shortTerm.join("\n")}`);if(mems.longTerm?.length)parts.push(`[장기기억]:\n${mems.longTerm.join("\n")}`);if(mems.relationship?.length)parts.push(`[관계도]:\n${mems.relationship.join("\n")}`);if(parts.length>0)memoryText=parts.join("\n\n")}catch(e){}function stripInjectedOOC(msg,role){if(!msg||role!=="user")return msg;return msg.replace(/\*\*OOC:[\s\S]*?\*\*/g,"").replace(/\[System:[\s\S]*?\[\/System\]/g,"").replace(/\(Narrator's note:[\s\S]*?\(End note\)/g,"").replace(/\/\*\*[\s\S]*?\*\*\//g,"").replace(/Remember these established facts[\s\S]*?(?=\n\n|$)/g,"").trim()}let contextText="최근 대화 내역 없음.";let allMsgsForContext=[];const turns=config.refinerContextTurns!==undefined?config.refinerContextTurns:1;if(turns>0){const allMsgs=await Core.fetchLogs(turns*2+2);if(allMsgs&&allMsgs.length>0){allMsgsForContext=allMsgs.slice();let ctxMsgs=allMsgs.slice();const last=ctxMsgs[ctxMsgs.length-1];if(last&&last.role==="assistant"&&assistantText){const a=(last.message||"").slice(0,100);const b=assistantText.slice(0,100);if(a===b)ctxMsgs=ctxMsgs.slice(0,-1)}ctxMsgs=ctxMsgs.slice(-(turns*2+1));if(ctxMsgs.length>0){contextText=ctxMsgs.map(m=>`${m.role}: ${stripInjectedOOC(m.message,m.role)}`).join("\n\n")}}}const callChangeContext=buildCallChangeContext(activeEntries,allMsgsForContext);if(callChangeContext)contextText+="\n\n"+callChangeContext;Core.showStatusBadge("에리가 잼민이에게 묻는 중");const passWord=config.refinerPassKeyword||"PASS";const promptTpl=config.refinerCustomPrompt||R.DEFAULT_PROMPT;const prompt=promptTpl.replace("{lore}",loreText).replace("{memory}",memoryText).replace("{context}",contextText).replace("{message}",assistantText).replace("{passWord}",passWord);const _refModel=(config.refinerModel==="_custom"?config.refinerCustomModel:config.refinerModel)||(config.autoExtModel==="_custom"?config.autoExtCustomModel:config.autoExtModel||"gemini-3-flash-preview");let _refElapsedMs=0,_refCost=null;try{if(ToastCallback)ToastCallback("에리가 응답 검수 중","#258");const apiOpts={apiType:config.autoExtApiType||"key",key:config.autoExtKey,vertexJson:config.autoExtVertexJson,vertexLocation:config.autoExtVertexLocation||"global",vertexProjectId:config.autoExtVertexProjectId,firebaseScript:config.autoExtFirebaseScript,firebaseEmbedKey:config.autoExtFirebaseEmbedKey,model:_refModel,maxRetries:1,costContext:{feature:"refine",chatKey:chatRoomId||"global"}};const is3x=apiOpts.model.includes("gemini-3")||apiOpts.model.includes("gemini-2.0-flash-thinking");if(is3x){const isPro=apiOpts.model.includes("pro");apiOpts.thinkingConfig=isPro?{thinkingLevel:"low"}:{thinkingLevel:"minimal"}}const _refT0=Date.now();const response=await Core.callGeminiApi(prompt,apiOpts);_refElapsedMs=Date.now()-_refT0;_refCost=response&&response.cost||null;if(!response.text)throw new Error(response.error||"AI 응답 없음");const text=response.text.trim();const isPass=text.includes(passWord)&&text.length0){for(const r of replacements){if(r.from&&r.to!==undefined)correctedText=correctedText.replace(r.from,r.to)}}else if(parsed.refined_text){correctedText=parsed.refined_text}if(LogCallback)LogCallback(url,{time:(new Date).toLocaleTimeString(),original:assistantText,result:"Refined",isPass:false,refined:correctedText,reason:parsed.reason,model:_refModel,elapsedMs:_refElapsedMs,cost:_refCost});Core.showStatusBadge("에리가 뭔가 발견");setTimeout(Core.hideStatusBadge,3e3);const applyRefinement=async newText=>{try{const _cid=Core.getCurrentChatId();if(!_cid)throw new Error("채팅방 ID 없음");const lastBot=targetLog&&!(targetLog instanceof Error)?targetLog:await CrackUtil.chatRoom().findLastBotMessage(_cid);if(lastBot&&!(lastBot instanceof Error)){const token=CrackUtil.cookie().getAuthToken();const editUrl=`https://crack-api.wrtn.ai/crack-gen/v3/chats/${_cid}/messages/${lastBot.id}`;const editResult=await Core.gmFetch(editUrl,{method:"PATCH",headers:{Accept:"application/json, text/plain, */*",Authorization:"Bearer "+token,"Content-Type":"application/json",platform:"web","wrtn-locale":"ko-KR"},body:JSON.stringify({message:newText})});let editText="";let editJson=null;try{editText=editResult.text?await editResult.text():""}catch(_){}try{editJson=editText?JSON.parse(editText):null}catch(_){}const serverOk=!!(editResult.ok&&(!editJson||editJson.result==="SUCCESS"));const serverText=editJson&&editJson.data&&typeof editJson.data.content==="string"?editJson.data.content:newText;const serverMessageId=editJson&&editJson.data&&(editJson.data._id||editJson.data.id)||lastBot.id;if(serverOk){const newFp=serverMessageId||serverText.slice(0,40);_loadChat(_currentChatKey()).add(newFp);saveProcessedFingerprints();const originalForDom=lastBot&&lastBot.content||assistantText;const oldPlain=R.stripMarkdown?R.stripMarkdown(originalForDom):originalForDom;const newPlain=R.stripMarkdown?R.stripMarkdown(serverText):serverText;let targetEl=null;try{targetEl=R.findMessageContainerById&&R.findMessageContainerById(serverMessageId)||R.findDeepestMatchingElement&&R.findDeepestMatchingElement(oldPlain)||null}catch(_){}const storeOk=!!(targetEl&&R.tryStoreUpdate&&R.tryStoreUpdate(targetEl,serverMessageId,serverText));const hasCodeFence=/```/.test(serverText||"");const rerenderOk=(_w.__LR_LAST_RERENDER_HITS||0)>0;let domResult=null;let domUpdated=false;const verifyAndPatchVisible=async delayMs=>{if(delayMs)await new Promise(r=>setTimeout(r,delayMs));let visible=false;try{visible=R.waitForVisibleText?await R.waitForVisibleText(serverText,serverMessageId,900):R.isTextVisible?R.isTextVisible(serverText,serverMessageId):storeOk}catch(_){visible=!!storeOk}if(!visible&&R.refreshMessageInDOM&&!hasCodeFence){let fallbackResult=null;try{fallbackResult=R.refreshMessageInDOM(originalForDom,serverText,serverMessageId)}catch(_){}_w.__LR_LAST_DOM_FALLBACK=fallbackResult;visible=!!(fallbackResult===true||fallbackResult&&(fallbackResult.applied||fallbackResult.visible))}else if(!visible&&hasCodeFence){_w.__LR_LAST_DOM_FALLBACK={skipped:true,status:"skipped_codeblock",messageId:serverMessageId}}if(!visible&&R.nudgeMessageNativeRender){let nudged=false;try{nudged=!!R.nudgeMessageNativeRender(serverMessageId)}catch(_){}_w.__LR_LAST_NATIVE_NUDGE=nudged;if(nudged){try{visible=R.waitForVisibleText?await R.waitForVisibleText(serverText,serverMessageId,900):R.isTextVisible?R.isTextVisible(serverText,serverMessageId):visible}catch(_){}}}if(!visible&&delayMs&&R.showReloadAction)R.showReloadAction("서버 수정 완료. 화면이 아직 예전 응답이면 새로고침 필요.");return visible};verifyAndPatchVisible(150).then(visible=>{if(!visible)setTimeout(()=>{verifyAndPatchVisible(0)},1200)});const newFingerprint=R.stripMarkdown?R.stripMarkdown(serverText).slice(0,80):(serverText||"").slice(0,80);if(newFingerprint){_loadChat(_currentChatKey()).add(newFingerprint);saveProcessedFingerprints()}if(ToastCallback)ToastCallback(`에리가 고침 — ${parsed.reason}`,"#285");console.log("[Refiner] PATCH 성공. id=",serverMessageId,"status=",editResult.status,"storeOk=",storeOk,"rerenderOk=",rerenderOk,"domResult=",domResult)}else{console.error("[Refiner] PATCH 실패. status=",editResult.status,"body=",(editText||"").slice(0,300));if(ToastCallback)ToastCallback(`에리: 서버 수정 실패 (${editResult.status})`,"#a55")}}else{if(ToastCallback)ToastCallback("에리: 대상 메시지 못 찾음, 로그에 보관","#a55")}}catch(e){if(ToastCallback)ToastCallback("에리: 수정 중 오류","#a55")}if(enqueueCallback)setTimeout(enqueueCallback,100)};if(config.refinerAutoMode){await applyRefinement(correctedText)}else{const existingPopup=document.querySelector("#refiner-confirm-overlay");if(existingPopup){if(ToastCallback)ToastCallback("에리: 제안을 로그에 보관함","#258");if(enqueueCallback)setTimeout(enqueueCallback,100)}else{holdRefineLock=true;R.showRefineConfirm(parsed.reason,correctedText,newText=>{applyRefinement(newText).finally(releaseRefineLock)},()=>{releaseRefineLock();if(enqueueCallback)setTimeout(enqueueCallback,100)})}}}}catch(e){if(LogCallback)LogCallback(url,{time:(new Date).toLocaleTimeString(),original:assistantText,result:"System Error: "+e.message,isError:true,model:_refModel,elapsedMs:_refElapsedMs,cost:_refCost});Core.hideStatusBadge();if(ToastCallback)ToastCallback(`에리: 교정 실패 — ${e.message}`,"#a55")}}finally{if(!holdRefineLock)releaseRefineLock()}}R.renderLoreForRefiner=renderLoreForRefiner;R.buildCallChangeContext=buildCallChangeContext;R.matchEntriesByTrigger=matchEntriesByTrigger;R.refineMessage=refineMessage;R.setCallbacks=function(coreInstance,configGetterFn,logCb,toastCb,getPacksCb){Core=coreInstance;ConfigGetter=configGetterFn;LogCallback=logCb;ToastCallback=toastCb;GetActivePacksCallback=getPacksCb};R.getProcessedFingerprints=function(){return _loadChat(_currentChatKey())};R.saveProcessedFingerprints=saveProcessedFingerprints;R.__coreLoaded=true})();(function(){"use strict";const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const R=_w.__LoreRefiner=_w.__LoreRefiner||{};if(!R.__coreLoaded){console.error("[Refiner:queue] core 미로드");return}if(R.__queueLoaded)return;const refineQueue=[];R.workerBusy=false;R.workerStartTime=0;const WORKER_TIMEOUT=9e4;function enqueueRefine(text,msgId){R.lastState={state:"queued",detail:"queued for refine",at:Date.now(),queue:refineQueue.length+1,busy:!!R.workerBusy};const fingerprints=R.getProcessedFingerprints();const fingerprint=msgId||text.slice(0,40);if(fingerprints.has(fingerprint)){R.Core&&R.Core.hideStatusBadge();return}if(refineQueue.some(item=>item.fingerprint===fingerprint))return;refineQueue.push({text:text,fingerprint:fingerprint,enqueuedAt:Date.now()});processQueue()}async function processQueue(){if(refineQueue.length===0)return;if(document.querySelector("#refiner-confirm-overlay"))return;if(R.workerBusy){if(Date.now()-R.workerStartTime>WORKER_TIMEOUT){R.workerBusy=false;R.Core&&R.Core.hideStatusBadge()}else return}R.workerBusy=true;R.workerStartTime=Date.now();R.lastState={state:"running",detail:"calling refiner api",at:Date.now(),queue:refineQueue.length,busy:true};const item=refineQueue.shift();const fingerprints=R.getProcessedFingerprints();if(fingerprints.has(item.fingerprint)){R.workerBusy=false;R.lastState={state:"skipped",detail:"already processed",at:Date.now(),queue:refineQueue.length,busy:false};if(refineQueue.length>0)processQueue();return}fingerprints.add(item.fingerprint);R.saveProcessedFingerprints();try{await Promise.race([R.refineMessage(item.text,false,processQueue),new Promise((_,rej)=>setTimeout(()=>rej(new Error("refineMessage 60초 타임아웃")),6e4))])}catch(e){R.Core&&R.Core.hideStatusBadge();R.lastState={state:"error",detail:e.message||String(e),at:Date.now(),queue:refineQueue.length,busy:false}}R.workerBusy=false;if(!R.lastState||R.lastState.state==="running")R.lastState={state:"idle",detail:"done",at:Date.now(),queue:refineQueue.length,busy:false};if(refineQueue.length>0)processQueue()}R.refineQueue=refineQueue;R.enqueueRefine=enqueueRefine;R.processQueue=processQueue;R.WORKER_TIMEOUT=WORKER_TIMEOUT;R.__queueLoaded=true})();(function(){"use strict";const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const R=_w.__LoreRefiner=_w.__LoreRefiner||{};if(!R.__queueLoaded){console.error("[Refiner:observer] queue 미로드");try{_w.__LoreInj?.markFailed?.("refinerObserver","queue missing")}catch(_){}return}if(R.__observerLoaded)return;let lastAssistantMsgId=null;let lastMsgLength=0;let idleCount=0;let lastChangeTime=0;let _needsWarmup=true;let _lastKnownUrl="";let _chatObserver=null;let _pollingInterval=null;let _watchdogInterval=null;let _waitingSince=0;function setRefinerState(state,detail){R.lastState={state:state||"idle",detail:detail||"",at:Date.now(),queue:R.refineQueue?R.refineQueue.length:0,busy:!!R.workerBusy}}function isChatPath(){const fn=_w.__LoreInj&&_w.__LoreInj.isChatPath;if(typeof fn==="function"){try{return!!fn()}catch(_){}}return/\/characters\/[a-f0-9]+\/chats\/[a-f0-9]+/.test(location.pathname)||/\/stories\/[a-f0-9]+\/episodes\/[a-f0-9]+/.test(location.pathname)||/\/u\/[a-f0-9]+\/c\/[a-f0-9]+/.test(location.pathname)}async function checkLatestMessage(){const config=R.ConfigGetter();if(!config.refinerEnabled){R.Core&&R.Core.hideStatusBadge();setRefinerState("off","disabled");return}const currentUrl=R.Core.getCurUrl();if(currentUrl!==_lastKnownUrl){_lastKnownUrl=currentUrl;lastAssistantMsgId=null;lastMsgLength=0;idleCount=0;_needsWarmup=true}const chatId=R.Core.getCurrentChatId();if(!chatId){R.Core&&R.Core.hideStatusBadge();setRefinerState("idle","no chat id");return}try{const lastLog=await CrackUtil.chatRoom().findLastMessageId(chatId,"assistant");if(!lastLog||lastLog instanceof Error)return;const msgId=lastLog.id||(lastLog.content?lastLog.content.slice(0,40):"");const contentLen=lastLog.content?lastLog.content.length:0;if(R.rememberAssistantMessage&&lastLog.content)R.rememberAssistantMessage(msgId,lastLog.content);if(_needsWarmup){lastAssistantMsgId=msgId;lastMsgLength=contentLen;idleCount=0;lastChangeTime=Date.now();_needsWarmup=false;if(msgId){R.getProcessedFingerprints().add(msgId);R.saveProcessedFingerprints()}return}if(msgId!==lastAssistantMsgId){R.Core.showStatusBadge("에리가 응답 기다리는 중");_waitingSince=Date.now();setRefinerState("waiting","assistant response detected");lastAssistantMsgId=msgId;lastMsgLength=contentLen;idleCount=0;lastChangeTime=Date.now()}else{if(contentLen===lastMsgLength&&lastMsgLength>0){idleCount++;if(idleCount>=2&&Date.now()-lastChangeTime>4e3){setRefinerState("queued","stable assistant response");_waitingSince=0;R.enqueueRefine(lastLog.content,msgId)}}else{lastMsgLength=contentLen;idleCount=0;lastChangeTime=Date.now();setRefinerState("waiting","assistant response still changing")}}}catch(e){}}function setupObserver(){if(_chatObserver)_chatObserver.disconnect();if(_pollingInterval)clearInterval(_pollingInterval);if(_watchdogInterval)clearInterval(_watchdogInterval);if(!isChatPath()){console.log("[Refiner:observer] non-chat route: observer skipped");R.Core&&R.Core.hideStatusBadge();setRefinerState("idle","non-chat route");return}_chatObserver=new MutationObserver(()=>{const config=R.ConfigGetter();if(!config.refinerEnabled){R.Core&&R.Core.hideStatusBadge();setRefinerState("off","disabled");return}if(window._refinerDebounceTimer)clearTimeout(window._refinerDebounceTimer);window._refinerDebounceTimer=setTimeout(()=>{checkLatestMessage()},800)});_chatObserver.observe(document.body,{childList:true,subtree:true,characterData:true});_pollingInterval=setInterval(()=>{const config=R.ConfigGetter();if(config.refinerEnabled)checkLatestMessage()},3e3);_watchdogInterval=setInterval(()=>{if(R.workerBusy&&Date.now()-R.workerStartTime>R.WORKER_TIMEOUT){R.workerBusy=false;R.Core&&R.Core.hideStatusBadge();setRefinerState("timeout","worker timeout")}if(_waitingSince&&Date.now()-_waitingSince>45e3){_waitingSince=0;R.Core&&R.Core.hideStatusBadge();setRefinerState("timeout","waiting timeout")}if(R.refineQueue.length>0&&!R.workerBusy)R.processQueue()},2e3)}R.setupObserver=setupObserver;R.setNeedsWarmup=function(){_needsWarmup=true};R.getRefinerState=function(){return R.lastState||{state:"idle",detail:"",at:0,queue:R.refineQueue?R.refineQueue.length:0,busy:!!R.workerBusy}};R.__observerLoaded=true})();(function(){"use strict";const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const R=_w.__LoreRefiner=_w.__LoreRefiner||{};if(R.__loaded)return;function fail(dep,msg){console.error(msg);try{_w.__LoreInj?.markFailed?.("refiner",dep+" missing")}catch(_){}R.__loadError=dep+" missing";return true}if(!R.__promptsLoaded&&fail("prompts","[Refiner] prompts 미로드"))return;if(!R.__domLoaded&&fail("dom","[Refiner] dom 미로드"))return;if(!R.__coreLoaded&&fail("core","[Refiner] core 미로드"))return;if(!R.__queueLoaded&&fail("queue","[Refiner] queue 미로드"))return;if(!R.__observerLoaded&&fail("observer","[Refiner] observer 미로드"))return;R.init=function(coreInstance,configGetterFn,logCb,toastCb,getPacksCb){R.Core=coreInstance;R.ConfigGetter=configGetterFn;R.setCallbacks(coreInstance,configGetterFn,logCb,toastCb,getPacksCb);R.setupObserver()};R.clearProcessed=function(){R.getProcessedFingerprints().clear();const _ls=typeof unsafeWindow!=="undefined"?unsafeWindow.localStorage:localStorage;_ls.removeItem("speech-refiner-processed")};R.manualRefine=async function(text,msgId){if(!text)return;if(msgId)R.getProcessedFingerprints().delete(msgId);try{await R.refineMessage(text,true,R.processQueue)}catch(e){console.error("[Refiner] manual fail:",e);throw e}};R.__loaded=true})();(function(){"use strict";const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;_w.__LoreInj=_w.__LoreInj||{};if(_w.__LoreInj.__interceptorLoaded)return;const _origFetch=_w.fetch.bind(_w);const _origWsSend=_w.WebSocket.prototype.send;let _injectFn=null;_w.WebSocket.prototype.send=function(data){const ws=this;if(_injectFn&&typeof data==="string"&&data.length>10){const bi=data.indexOf("[");if(bi>0){try{const prefix=data.slice(0,bi),arr=JSON.parse(data.slice(bi));if(Array.isArray(arr)&&arr[0]==="send"&&arr[1]&&typeof arr[1].message==="string"&&arr[1].message.length>0){if(!arr[1].message.includes("OOC:")){const orig=arr[1].message;(async()=>{try{const mod=await _injectFn(orig);if(orig!==mod){arr[1].message=mod;_origWsSend.call(ws,prefix+JSON.stringify(arr))}else{_origWsSend.call(ws,data)}}catch(e){console.error("[Lore] WS err:",e);_origWsSend.call(ws,data)}})();return}}}catch(e){}}}return _origWsSend.call(this,data)};_w.fetch=async function(...args){try{if(_injectFn){let reqUrl="",isReq=false;if(args[0]instanceof Request){reqUrl=args[0].url;isReq=true}else reqUrl=args[0];const method=isReq?args[0].method:(args[1]||{}).method||"GET";if(method==="POST"&&reqUrl&&(reqUrl.includes("/messages")||reqUrl.includes("/chat")||reqUrl.includes("wrtn.ai"))){let bodyText=null;if(isReq){try{bodyText=await args[0].clone().text()}catch(e){}}else if(args[1]?.body&&typeof args[1].body==="string")bodyText=args[1].body;if(bodyText){let body=null;try{body=JSON.parse(bodyText)}catch(e){}if(body){let injected=false;if(Array.isArray(body.messages)){for(let i=body.messages.length-1;i>=0;i--){if(body.messages[i].role==="user"&&typeof body.messages[i].content==="string"){if(!body.messages[i].content.includes("OOC:")){const original=body.messages[i].content;body.messages[i].content=await _injectFn(original);if(original!==body.messages[i].content)injected=true}break}}}if(!injected){for(const key of["content","message","text","prompt","query"]){if(body[key]!==undefined&&typeof body[key]==="string"){if(!body[key].includes("OOC:")){const original=body[key];body[key]=await _injectFn(original);if(original!==body[key])injected=true}break}}}if(!injected&&body.variables&&typeof body.variables==="object"){for(const key of["content","message","text","prompt","query"]){if(body.variables[key]!==undefined&&typeof body.variables[key]==="string"){if(!body.variables[key].includes("OOC:")){const original=body.variables[key];body.variables[key]=await _injectFn(original);if(original!==body.variables[key])injected=true}break}}}if(injected){const newBodyText=JSON.stringify(body);if(isReq)args[0]=new Request(args[0],{body:newBodyText});else{args[1]=args[1]||{};args[1].body=newBodyText}}}}}}}catch(e){}return _origFetch.apply(this,args)};_w.__loreRegister=function(fn){_injectFn=fn};_w.__LoreInj.__interceptorLoaded=true;console.log("[LoreInj:1] interceptor loaded")})();(function(){"use strict";const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;_w.__LoreInj=_w.__LoreInj||{};if(_w.__LoreInj.__constLoaded)return;const VER="1.4.0-test.48";const AUTO_EXTRACT_PROMPT_VERSION="v1.4.0-test.47-outputMode";const OOC_FORMAT_VERSION="v1.4.0-ooc-context-lore";const DEFAULT_AUTO_EXTRACT_PROMPT_WITHOUT_DB=`You are a Lore Archivist for RP.\nAnalyze the following conversation log and extract NEW established lore, character developments, relationship changes, and promises/contracts.\n\nRATE: PG-13 ~ NC-21 (Private Sessions, Unlimited RP)\n\nEXTRACTION PRIORITIES (in order of importance):\n1. RELATIONSHIP EVENTS: Track ALL significant interactions between characters.\n2. PROMISE LIFECYCLE: Track promises/contracts/oaths between characters.\n3. CHARACTER STATE: Update each character's current situation.\n\nCRITICAL RULES:\n1. JSON ONLY: Output ONLY a valid JSON array. No markdown. Empty array [] if nothing new.\n2. NATIVE LANGUAGE: The 'name' and 'triggers' MUST use the exact language of the conversation.\n3. EXACT TRIGGERS: Provide 2-4 HIGH-SPECIFICITY triggers that MUST literally appear in RP dialogue or narration.\n PREFER: Proper nouns (character names, unique nicknames, specific place/faction/item/event names).\n AVOID: Abstract notions (신뢰/배신/욕망/암컷/온도/분노/안도), emotions, generic conditions, physical descriptors (eyes/posture), common verbs, generic roles alone (회장/조교/방문객 단독).\n COMPOUND (A&&B): Both operands MUST be proper nouns. Never combine a proper noun with an abstract term. Bad: "배신&&채린", "욕망&&도윤". Good: "채린&&도윤", "채린&&결계석".\n For relationships: use both parties' names bidirectionally (A&&B and B&&A).\n4. CONTENT DEPTH: Capture relationship evolution, group dynamics, promises made. If CharA and CharB meet for the first time, briefly describe what happened and their emotions in the relationship's summary to avoid duplicate encounter entries.\n5. STATE REPLACEMENT: When a status CHANGES, describe ONLY the current state.\n6. SUMMARY QUALITY: Produce summary.full, summary.compact, and summary.micro.\n Bad full: "동맹 관계" Good full: "대한제국과 영국의 상호방위 동맹. 군수물자 지원과 관세 양보를 교환하며 현재 군사 지원 약속이 미해결."\n compact must keep the relation/status/hook. micro must be a stable recall handle + current state.\n7. IMPORTANCE GATING: Rate each entry on three axes (1-10):\n - importance: How critical to the ongoing story?\n - surprise: How new vs already-known information?\n - emotional: How emotionally significant?\n Only include entries where (importance + surprise + emotional) >= 12.\n Filter out: routine actions, generic descriptions, already-established facts with no change.\n8. HONORIFIC TRACKING (rel only):\n - For each rel, set "parties": ["A","B"].\n - Scan dialogue for VOCATIVE terms (how A actually addressed B).\n - Korean cues: "~아/야/씨/님", "너/당신/자기/여보/오빠/누나".\n - Set "call" to the LATEST term used in this window.\n - If the term differs from prior history, also output "callDelta" with from/to/term/prevTerm/turnApprox.\n9. EVENT ACCUMULATION (character/rel/identity only):\n - Significant events are APPENDED to "eventHistory" array, NEVER overwriting prior events.\n - Each event: {turn, summary, imp(1-10), emo(1-10)}\n - Only include events with imp+emo >= 10 (truly memorable).\n - Maximum 3 new events per entry per extraction pass.\n - Summary must be concrete noun-ending Korean for search: "LO와 첫 키스, 카페에서" not "행복한 순간".\n - If no new significant event occurred, OMIT eventHistory for that entry.\n\nSUMMARY AND INJECTION FORMAT RULES:\n- "summary" has three semantic levels, not just shorter copies:\n - "full": self-contained continuity record. Include who/what/why/current state and the unresolved hook.\n - "compact": preserve entity, state, relationship, and unresolved hook.\n - "micro": stable recall handle + current state only. Never output a vague teaser.\n- "inject" may mirror summary tiers, but it must stay concise enough for later 2,000-char budget planning:\n - "full": key facts separated by |. Target 120 chars.\n - "compact": essential continuity only. Target 70 chars.\n - "micro": name=status format. Target 35 chars.\n- "embed_text": keyword cluster, NOT prose. Include names, aliases, relationship terms, event causes, stakes, location, and unresolved hooks.\n- "callState": current vocative state. previousTerms are context only, not permanent requirements.\n- "timeline": event turn/order/scene/observed recency. Do not invent in-story days.\n- "entities": participating characters/places/items.\n- "state": current situation in noun phrases. Replace entirely on update.\n- Field abbreviations: importance→imp, surprise→sur, emotional→emo.\n- Source field is not needed in output (injector adds it).\n\nSchema:\n{schema}\n\nConversation Log:\n{context}`;const DEFAULT_AUTO_EXTRACT_SCHEMA=`[\n {\n "type": "character|location|item|event|concept|setting",\n "name": "Entity Name",\n "triggers": ["keyword1", "CharName&&keyword2"],\n "summary": {\n "full": "Continuity-safe, self-contained: who/what/why/current state.",\n "compact": "Entity + state + relation/hook preserved.",\n "micro": "Stable recall handle + current state."\n },\n "embed_text": "names aliases relationship terms event causes stakes location unresolved hooks",\n "inject": {\n "full": "derived from summary.full; key facts | max 120 chars",\n "compact": "derived from summary.compact; max 70 chars",\n "micro": "derived from summary.micro; max 35 chars"\n },\n "state": "current situation noun phrase",\n "timeline": { "eventTurn": 0, "relativeOrder": "current|past|foreshadow", "sceneLabel": "", "observedRecency": "recent|old|unknown" },\n "entities": ["characters/places/items involved"],\n "eventHistory": [\n {"turn": 12, "summary": "significant concrete event", "imp": 8, "emo": 9}\n ],\n "imp": 5, "sur": 5, "emo": 5\n },\n {\n "type": "rel",\n "name": "CharA↔CharB",\n "parties": ["CharA", "CharB"],\n "triggers": ["CharA&&CharB", "CharB&&CharA"],\n "summary": {\n "full": "relationship cause + current state + unresolved hook",\n "compact": "relationship state + hook",\n "micro": "A↔B=status"\n },\n "embed_text": "CharA CharB aliases call terms relationship stakes hooks",\n "state": "one-word status",\n "callState": {\n "CharA→CharB": {\n "currentTerm": "latest vocative",\n "previousTerms": ["older vocative"],\n "tone": "affectionate|hostile|formal|neutral",\n "scope": "scene|stable|private|public",\n "lastChangedTurn": 0,\n "confidence": 0.8,\n "reason": "why this is current"\n }\n },\n "call": {"CharA→CharB": "latest vocative"},\n "callDelta": [{"from":"CharA","to":"CharB","term":"newHonorific","prevTerm":"oldHonorific","turnApprox":0}],\n "timeline": { "eventTurn": 0, "relativeOrder": "current", "sceneLabel": "", "observedRecency": "recent" },\n "entities": ["CharA", "CharB"],\n "imp": 5, "sur": 5, "emo": 5\n },\n {\n "type": "prom",\n "name": "Promise title",\n "triggers": ["Maker&&keyword", "Target&&keyword"],\n "summary": {\n "full": "who promised what, why it matters, current status, condition",\n "compact": "promise + status + condition",\n "micro": "Maker=status"\n },\n "embed_text": "maker target promise condition stakes",\n "state": "pending|fulfilled|broken|expired|modified",\n "cond": "trigger condition",\n "timeline": { "eventTurn": 0, "relativeOrder": "current", "sceneLabel": "", "observedRecency": "recent" },\n "entities": ["Maker", "Target"],\n "imp": 5, "sur": 5, "emo": 5\n }\n]`;const DEFAULT_AUTO_EXTRACT_PATCH_SCHEMA=`[\n {\n "op": "add",\n "entry": {\n "type": "character|location|item|event|concept|setting|rel|prom",\n "name": "Entity Name",\n "triggers": ["keyword1", "CharA&&CharB"],\n "summary": { "full": "self-contained continuity", "compact": "state + hook", "micro": "name=status" },\n "inject": { "full": "max 120 chars", "compact": "max 70 chars", "micro": "max 35 chars" },\n "embed_text": "names aliases relationship terms causes stakes locations hooks",\n "state": "current situation",\n "timeline": { "eventTurn": 0, "relativeOrder": "current|past|foreshadow", "sceneLabel": "", "observedRecency": "recent|old|unknown" },\n "entities": [],\n "eventHistory": [{"turn": 0, "summary": "new concrete event", "imp": 8, "emo": 8}],\n "imp": 5, "sur": 5, "emo": 5\n }\n },\n {\n "op": "patch",\n "id": 0,\n "reason": "short reason",\n "set": {\n "state": "",\n "summary": { "full": "", "compact": "", "micro": "" },\n "inject": { "full": "", "compact": "", "micro": "" },\n "callState": {},\n "timeline": {},\n "entities": [],\n "cond": ""\n },\n "append": {\n "triggers": [],\n "eventHistory": [{"turn": 0, "summary": "new concrete event", "imp": 8, "emo": 8}],\n "callHistory": [{"turn": 0, "from": "A", "to": "B", "term": "current", "prevTerm": "previous"}]\n }\n }\n]`;const DEFAULT_TEMPORAL_EXTRACT_SCHEMA=`[\n {\n "type": "timeline_event",\n "title": "Short event title in the conversation language",\n "name": "Same as title unless a better stable recall handle exists",\n "when": {\n "turnStart": 0,\n "turnEnd": 0,\n "relative": "past|current|foreshadow",\n "anchor": "RP-understandable time anchor such as 'after the club camp night', not just a turn number",\n "inferredOrder": "after X before Y, if inferable",\n "confidence": 0.8\n },\n "participants": ["characters involved"],\n "location": "place if known",\n "actions": ["walk", "confession attempt", "promise", "fight", "reunion"],\n "emotions": {\n "Character": ["hesitation", "relief"]\n },\n "summary": {\n "full": "Self-contained event memory: who, where, what happened, why it matters, what changed, unresolved hook.",\n "compact": "Event + consequence + hook.",\n "micro": "Stable recall handle=current meaning"\n },\n "hooks": ["unresolved hook or future recall reason"],\n "linkedLore": ["related character/relationship/promise/location names"],\n "recallTriggers": ["literal words, aliases, scene cues, memory question cues"],\n "importance": 8,\n "emotional": 8,\n "confidence": 0.8\n }\n]`;const DEFAULT_TEMPORAL_EXTRACT_PROMPT=`You are a Temporal Memory Extractor for long-form AI RP.\nExtract ONLY concrete timeline events from the conversation log.\n\nPurpose:\n- Build event memories that can be recalled even 1000 turns later.\n- Do NOT create general character/profile lore here. General lore is extracted by another pass.\n- Focus on events that explain current relationship state, promises, emotional changes, conflicts, reunions, reveals, and scene milestones.\n\nCRITICAL RULES:\n1. JSON ONLY: Output ONLY a valid JSON array. No markdown. Empty array [] if no meaningful event occurred.\n2. EVENT ONLY: Extract timeline_event objects only. Do not output character/rel/promise entries in this pass.\n3. NO ROUTINE CHAT: Ignore routine banter, repeated affection with no change, generic descriptions, and facts with no event consequence.\n4. TIME ANCHOR QUALITY:\n - turnStart/turnEnd are internal approximate order markers.\n - "when.anchor" must be understandable to the RP model: e.g. "동아리 합숙 다음날 밤", "공원 산책 중", "첫 고백 직후".\n - Never rely on "turn 52" as the only time expression.\n - Do not invent real-world dates or days unless explicitly stated.\n5. CAUSAL CONTINUITY:\n - Explain what changed because of the event: relationship, trust, promise, conflict, fear, debt, secret, or unresolved hook.\n - Preserve why the event may matter later.\n6. RECALL TRIGGERS:\n - Include literal names, places, actions, nicknames, objects, and user recall cues such as "그때", "기억해", "전에".\n - Include both concrete scene terms and semantic cues.\n7. SUMMARY LEVELS:\n - summary.full: self-contained event memory.\n - summary.compact: event + consequence + hook.\n - summary.micro: stable recall handle + current meaning.\n8. PARTICIPANTS AND LINKS:\n - participants: characters directly involved.\n - linkedLore: related relationship/promise/location/event names if obvious.\n9. IMPORTANCE GATING:\n - Include only events where importance + emotional >= 10, or events that create/resolve a promise/conflict/relationship change.\n10. LANGUAGE:\n - Use the conversation's native language for title, anchor, summaries, hooks, and triggers.\n\nSchema:\n{schema}\n\nConversation Log:\n{context}`;const DEFAULT_TEMPORAL_RECALL_JUDGE_SCHEMA=`{\n "recall": true,\n "mode": "none|compact_timeline|specific_event|unresolved_hook",\n "wantedDepth": "none|micro|compact|detail",\n "maxChars": 0,\n "eventIds": ["candidate eventId or id"],\n "query": {\n "intent": "explicit_recall|natural_continuity|none",\n "participants": ["names"],\n "actions": ["actions"],\n "locations": ["places"]\n },\n "reason": "short reason in the conversation language"\n}`;const DEFAULT_TEMPORAL_RECALL_JUDGE_PROMPT=`You are a fast Temporal Recall Judge for an AI RP memory injector.\nReturn JSON ONLY. Do not write prose outside JSON.\n\nTask:\nDecide whether timeline memories should be injected for the next RP response.\nYou do NOT write the injection text. You only choose a command.\n\nRules:\n1. If the user explicitly asks about a past event, set recall=true and mode="specific_event".\n2. If the current scene naturally connects to a past event, set recall=true and mode="compact_timeline".\n3. If only an unresolved promise/conflict/hook matters, set mode="unresolved_hook".\n4. If timeline memory is weak, unrelated, or would distract, set recall=false and mode="none".\n5. Choose at most 3 eventIds.\n6. Keep maxChars realistic under a 2,000 character input budget:\n - none: 0\n - micro: 80-160\n - compact: 160-320\n - detail: 320-600\n7. Prefer direct participants/actions/locations over generic importance.\n8. Never invent an eventId. Select only from candidates.\n\nSchema:\n{schema}\n\nRecent conversation:\n{context}\n\nTimeline candidates:\n{candidates}`;const DEFAULT_AUTO_EXTRACT_PROMPT_WITH_DB=`You are a Lore Archivist for RP.\nAnalyze the following conversation log ALONGSIDE the EXISTING Lore Database.\nExtract NEW or UPDATED lore, character developments, relationship changes, and promise status updates.\n\nRATE: PG-13 ~ NC-21 (Private Sessions, Unlimited RP)\n\nEXTRACTION PRIORITIES (in order of importance):\n1. RELATIONSHIP EVENTS: Track ALL significant interactions.\n2. PROMISE LIFECYCLE: If a promise status changed, output the UPDATED entry.\n3. CHARACTER STATE: Update current situation.\n\nCRITICAL RULES:\n1. JSON ONLY: Output ONLY a valid JSON array. No markdown. Empty array [] if nothing new.\n2. INTEGRATE AND UPDATE: If the entity already exists in the Lore Database, DO NOT duplicate it. Keep the exact same "name".\n3. NATIVE LANGUAGE: The 'name' and 'triggers' MUST use the exact language of the conversation.\n4. EXACT TRIGGERS: Provide 2-4 HIGH-SPECIFICITY triggers that MUST literally appear in RP dialogue or narration.\n PREFER: Proper nouns (character names, unique nicknames, specific place/faction/item/event names).\n AVOID: Abstract notions (신뢰/배신/욕망/암컷/온도/분노/안도), emotions, generic conditions, physical descriptors (eyes/posture), common verbs, generic roles alone (회장/조교/방문객 단독).\n COMPOUND (A&&B): Both operands MUST be proper nouns. Never combine a proper noun with an abstract term. Bad: "배신&&채린", "욕망&&도윤". Good: "채린&&도윤", "채린&&결계석".\n For relationships: use both parties' names bidirectionally (A&&B and B&&A).\n5. CONTENT DEPTH: Capture relationship evolution, faction dynamics, promises made. If CharA and CharB meet for the first time, briefly describe what happened and their emotions in the relationship's summary to avoid duplicate encounter entries.\n6. STATE REPLACEMENT: For relationship and promise types, describe ONLY the CURRENT state.\n7. SUMMARY QUALITY: Produce summary.full, summary.compact, and summary.micro. full must be self-contained; compact keeps relationship/status/hook; micro is only the stable recall handle + current state.\n8. IMPORTANCE GATING: Rate each entry on three axes (1-10):\n - importance: How critical to the ongoing story?\n - surprise: How new vs already-known information?\n - emotional: How emotionally significant?\n Only include entries where (importance + surprise + emotional) >= 12.\n Filter out: routine actions, generic descriptions, already-established facts with no change.\n9. HONORIFIC TRACKING (rel only):\n - For each rel, set "parties": ["A","B"].\n - Scan dialogue for VOCATIVE terms (how A actually addressed B).\n - Korean cues: "~아/야/씨/님", "너/당신/자기/여보/오빠/누나".\n - Set "call" to the LATEST term used in this window.\n - If the term differs from prior history, also output "callDelta" with from/to/term/prevTerm/turnApprox.\n10. EVENT ACCUMULATION (character/rel/identity only):\n - CRITICAL: Check existing "eventHistory" for each entity in the DB context. Do NOT duplicate events already recorded.\n - Significant events are APPENDED to "eventHistory" array, NEVER overwriting prior events.\n - Each event: {turn, summary, imp(1-10), emo(1-10)}\n - Only include events with imp+emo >= 10 (truly memorable).\n - Maximum 3 new events per entry per extraction pass.\n - Summary must be concrete noun-ending Korean for search: "LO와 첫 키스, 카페에서" not "행복한 순간".\n - If no new significant event occurred, OMIT eventHistory for that entry.\n11. ANCHOR AWARENESS (CRITICAL — USER-LOCKED NARRATIVE FACTS):\n - Some existing entries have "anchor": true. These are user-locked canonical facts.\n - For anchored entries: NEVER output summary, state, detail, call, inject, cond, imp, sur, emo, gs, arc. These fields are PROTECTED and any output will be discarded by the merge layer.\n - You MAY still APPEND new items to eventHistory (if genuinely new and imp+emo >= 10).\n - You MAY add new keywords to triggers.\n - If nothing new qualifies for an anchored entry, OMIT it entirely from output. Do not echo its existing fields.\n12. CONTEXT-SAFE MERGE PATCHES (non-anchored entries):\n - The Existing Lore Database can be partial when the DB is large. Never assume omitted old facts are false.\n - For each existing entry, output ONLY changed/new slots; omitted slots are preserved by the merge layer.\n - If updating summary or inject, preserve the existing kernel and APPEND the new scene detail. Do NOT replace a continuity record with only the latest scene.\n - summary.full must remain self-contained after merge: existing identity/relationship/current state + new detail + unresolved hook.\n - summary.compact must retain relationship/status/hook. summary.micro must remain a stable recall handle + current state.\n - For "state": output only if the status actually changed (e.g. pending→fulfilled, 우호→적대). Stable states are preserved automatically.\n - For "call": output only changed or newly observed pairs. Previous terms are context only, not mandatory future speech.\n - If unsure whether a fact is new or old, output it as eventHistory instead of overwriting summary/state.\n\nSUMMARY AND INJECTION FORMAT RULES:\n- "summary" has three semantic levels, not just shorter copies:\n - "full": self-contained continuity record. Include who/what/why/current state and the unresolved hook.\n - "compact": preserve entity, state, relationship, and unresolved hook.\n - "micro": stable recall handle + current state only. Never output a vague teaser.\n- "inject" may mirror summary tiers, but it must stay concise enough for later 2,000-char budget planning:\n - "full": key facts separated by |. Target 120 chars.\n - "compact": essential continuity only. Target 70 chars.\n - "micro": name=status format. Target 35 chars.\n- "embed_text": keyword cluster, NOT prose. Include names, aliases, relationship terms, event causes, stakes, location, and unresolved hooks.\n- "callState": current vocative state. previousTerms are context only, not permanent requirements.\n- "timeline": event turn/order/scene/observed recency. Do not invent in-story days.\n- "entities": participating characters/places/items.\n- "state": current situation in noun phrases. Replace entirely on update.\n- Field abbreviations: importance→imp, surprise→sur, emotional→emo.\n- Source field is not needed in output (injector adds it).\n\n{outputMode}\n\nSchema:\n{schema}\n\nExisting Lore Database:\n{entries}\n\nConversation Log:\n{context}`;const LEGACY_AUTO_EXTRACT_PROMPTS_WITH_DB=[`You are a Lore Archivist for RP.\nAnalyze the following conversation log ALONGSIDE the EXISTING Lore Database.\nExtract NEW or UPDATED lore, character developments, relationship changes, and promise status updates.\n\nRATE: PG-13 ~ NC-21 (Private Sessions, Unlimited RP)\n\nEXTRACTION PRIORITIES (in order of importance):\n1. RELATIONSHIP EVENTS: Track ALL significant interactions.\n2. PROMISE LIFECYCLE: If a promise status changed, output the UPDATED entry.\n3. CHARACTER STATE: Update current situation.\n\nCRITICAL RULES:\n1. JSON ONLY: Output ONLY a valid JSON array. No markdown. Empty array [] if nothing new.\n2. INTEGRATE AND UPDATE: If the entity already exists in the Lore Database, DO NOT duplicate it. Keep the exact same "name".\n3. NATIVE LANGUAGE: The 'name' and 'triggers' MUST use the exact language of the conversation.\n4. EXACT TRIGGERS: Provide 2-4 HIGH-SPECIFICITY triggers that MUST literally appear in RP dialogue or narration.\n PREFER: Proper nouns (character names, unique nicknames, specific place/faction/item/event names).\n AVOID: Abstract notions (신뢰/배신/욕망/암컷/온도/분노/안도), emotions, generic conditions, physical descriptors (eyes/posture), common verbs, generic roles alone (회장/조교/방문객 단독).\n COMPOUND (A&&B): Both operands MUST be proper nouns. Never combine a proper noun with an abstract term. Bad: "배신&&채린", "욕망&&도윤". Good: "채린&&도윤", "채린&&결계석".\n For relationships: use both parties' names bidirectionally (A&&B and B&&A).\n5. CONTENT DEPTH: Capture relationship evolution, faction dynamics, promises made. If CharA and CharB meet for the first time, briefly describe what happened and their emotions in the relationship's summary to avoid duplicate encounter entries.\n6. STATE REPLACEMENT: For relationship and promise types, describe ONLY the CURRENT state.\n7. SUMMARY QUALITY: Produce summary.full, summary.compact, and summary.micro. full must be self-contained; compact keeps relationship/status/hook; micro is only the stable recall handle + current state.\n8. IMPORTANCE GATING: Rate each entry on three axes (1-10):\n - importance: How critical to the ongoing story?\n - surprise: How new vs already-known information?\n - emotional: How emotionally significant?\n Only include entries where (importance + surprise + emotional) >= 12.\n Filter out: routine actions, generic descriptions, already-established facts with no change.\n9. HONORIFIC TRACKING (rel only):\n - For each rel, set "parties": ["A","B"].\n - Scan dialogue for VOCATIVE terms (how A actually addressed B).\n - Korean cues: "~아/야/씨/님", "너/당신/자기/여보/오빠/누나".\n - Set "call" to the LATEST term used in this window.\n - If the term differs from prior history, also output "callDelta" with from/to/term/prevTerm/turnApprox.\n10. EVENT ACCUMULATION (character/rel/identity only):\n - CRITICAL: Check existing "eventHistory" for each entity in the DB context. Do NOT duplicate events already recorded.\n - Significant events are APPENDED to "eventHistory" array, NEVER overwriting prior events.\n - Each event: {turn, summary, imp(1-10), emo(1-10)}\n - Only include events with imp+emo >= 10 (truly memorable).\n - Maximum 3 new events per entry per extraction pass.\n - Summary must be concrete noun-ending Korean for search: "LO와 첫 키스, 카페에서" not "행복한 순간".\n - If no new significant event occurred, OMIT eventHistory for that entry.\n11. ANCHOR AWARENESS (CRITICAL — USER-LOCKED NARRATIVE FACTS):\n - Some existing entries have "anchor": true. These are user-locked canonical facts.\n - For anchored entries: NEVER output summary, state, detail, call, inject, cond, imp, sur, emo, gs, arc. These fields are PROTECTED and any output will be discarded by the merge layer.\n - You MAY still APPEND new items to eventHistory (if genuinely new and imp+emo >= 10).\n - You MAY add new keywords to triggers.\n - If nothing new qualifies for an anchored entry, OMIT it entirely from output. Do not echo its existing fields.\n12. CONTEXT-SAFE MERGE PATCHES (non-anchored entries):\n - The Existing Lore Database can be partial when the DB is large. Never assume omitted old facts are false.\n - For each existing entry, output ONLY changed/new slots; omitted slots are preserved by the merge layer.\n - If updating summary or inject, preserve the existing kernel and APPEND the new scene detail. Do NOT replace a continuity record with only the latest scene.\n - summary.full must remain self-contained after merge: existing identity/relationship/current state + new detail + unresolved hook.\n - summary.compact must retain relationship/status/hook. summary.micro must remain a stable recall handle + current state.\n - For "state": output only if the status actually changed (e.g. pending→fulfilled, 우호→적대). Stable states are preserved automatically.\n - For "call": output only changed or newly observed pairs. Previous terms are context only, not mandatory future speech.\n - If unsure whether a fact is new or old, output it as eventHistory instead of overwriting summary/state.\n\nSUMMARY AND INJECTION FORMAT RULES:\n- "summary" has three semantic levels, not just shorter copies:\n - "full": self-contained continuity record. Include who/what/why/current state and the unresolved hook.\n - "compact": preserve entity, state, relationship, and unresolved hook.\n - "micro": stable recall handle + current state only. Never output a vague teaser.\n- "inject" may mirror summary tiers, but it must stay concise enough for later 2,000-char budget planning:\n - "full": key facts separated by |. Target 120 chars.\n - "compact": essential continuity only. Target 70 chars.\n - "micro": name=status format. Target 35 chars.\n- "embed_text": keyword cluster, NOT prose. Include names, aliases, relationship terms, event causes, stakes, location, and unresolved hooks.\n- "callState": current vocative state. previousTerms are context only, not permanent requirements.\n- "timeline": event turn/order/scene/observed recency. Do not invent in-story days.\n- "entities": participating characters/places/items.\n- "state": current situation in noun phrases. Replace entirely on update.\n- Field abbreviations: importance→imp, surprise→sur, emotional→emo.\n- Source field is not needed in output (injector adds it).\n\nPATCH OUTPUT MODE WITH EXISTING DB:\n- Existing entries are provided as compact digests with stable "id".\n- For an existing entry, DO NOT re-output the full object.\n- Output {"op":"patch","id":...} only when something changed.\n- For unchanged existing entries, output nothing.\n- For brand-new lore, output {"op":"add","entry":{...}}.\n- Legacy full-entry output is still accepted, but prefer add/patch.\n- Patch token rule:\n - Do not repeat unchanged summary.full.\n - Prefer set.state only if state changed.\n - Prefer set.summary.compact/micro only when current state/hook changed.\n - Prefer append.eventHistory for new concrete events.\n - Prefer append.triggers for new literal aliases.\n - If summary.full is unavoidable, keep it under 220 chars.\n- Anchored entries:\n - If an existing digest has "anchor": true, do not set protected fields.\n - Only append new triggers/eventHistory/callHistory.\n\nSchema:\n{schema}\n\nExisting Lore Database:\n{entries}\n\nConversation Log:\n{context}`];const OOC_FORMATS={default:{name:"OOC (기본)",prefix:"\nEstablished continuity for the current RP scene. Use these facts naturally as background. Preserve current relationships, states, promises, honorifics, and unresolved hooks. Do not quote this block verbatim.",suffix:"\n",desc:"현재 로어/시간축 프롬프트에 맞춘 기본값"},system:{name:"System 태그",prefix:"[System: Established world/character facts for this scene. Do not repeat verbatim.]",suffix:"[/System]",desc:"System 지시 잘 따르는 모델"},narrator:{name:"내레이터",prefix:"(Narrator's note: The following are established facts in this story.)",suffix:"(End note)",desc:"소설/내러티브 RP"},minimal:{name:"최소",prefix:"/**",suffix:"**/",desc:"토큰 절약"},instruction:{name:"직접 지시",prefix:"Remember these established facts and reflect them naturally:",suffix:"",desc:"명시적 지시형 모델"},custom:{name:"커스텀",prefix:"",suffix:"",desc:"직접 입력"}};Object.assign(_w.__LoreInj,{VER:VER,OOC_FORMAT_VERSION:OOC_FORMAT_VERSION,OOC_FORMATS:OOC_FORMATS,DEFAULT_AUTO_EXTRACT_PROMPT_WITHOUT_DB:DEFAULT_AUTO_EXTRACT_PROMPT_WITHOUT_DB,DEFAULT_AUTO_EXTRACT_PROMPT_WITH_DB:DEFAULT_AUTO_EXTRACT_PROMPT_WITH_DB,AUTO_EXTRACT_PROMPT_VERSION:AUTO_EXTRACT_PROMPT_VERSION,LEGACY_AUTO_EXTRACT_PROMPTS_WITH_DB:LEGACY_AUTO_EXTRACT_PROMPTS_WITH_DB,DEFAULT_AUTO_EXTRACT_SCHEMA:DEFAULT_AUTO_EXTRACT_SCHEMA,DEFAULT_AUTO_EXTRACT_PATCH_SCHEMA:DEFAULT_AUTO_EXTRACT_PATCH_SCHEMA,DEFAULT_TEMPORAL_EXTRACT_PROMPT:DEFAULT_TEMPORAL_EXTRACT_PROMPT,DEFAULT_TEMPORAL_EXTRACT_SCHEMA:DEFAULT_TEMPORAL_EXTRACT_SCHEMA,DEFAULT_TEMPORAL_RECALL_JUDGE_PROMPT:DEFAULT_TEMPORAL_RECALL_JUDGE_PROMPT,DEFAULT_TEMPORAL_RECALL_JUDGE_SCHEMA:DEFAULT_TEMPORAL_RECALL_JUDGE_SCHEMA,__constLoaded:true});console.log("[LoreInj:2] constants loaded")})();(async function(){"use strict";if(document.readyState==="loading")await new Promise(r=>document.addEventListener("DOMContentLoaded",r));const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const deadline=Date.now()+1e4;while(!(_w.__LoreInj&&_w.__LoreInj.__constLoaded)&&Date.now()setTimeout(r,50));if(!(_w.__LoreInj&&_w.__LoreInj.__constLoaded)){console.error("[LoreInj:3] const 미로드");return}if(_w.__LoreInj.__settingsLoaded)return;if(_w.__loreInjectorVersion){try{CrackToast.warning("로어 인젝터: "+_w.__loreInjectorVersion+" 실행중. 중복방지로 비활성화.")}catch(e){}return}_w.__loreInjectorVersion="v"+_w.__LoreInj.VER;const C=_w.__LoreCore;const R=_w.__LoreRefiner;if(!C){console.error("[Lore] Core 미감지. 작동 불가.");return}console.log("[Lore] Core v"+C.VER+" 연결 성공. Injector v"+_w.__LoreInj.VER);const{OOC_FORMAT_VERSION:OOC_FORMAT_VERSION,OOC_FORMATS:OOC_FORMATS,DEFAULT_AUTO_EXTRACT_PROMPT_WITHOUT_DB:DEFAULT_AUTO_EXTRACT_PROMPT_WITHOUT_DB,DEFAULT_AUTO_EXTRACT_PROMPT_WITH_DB:DEFAULT_AUTO_EXTRACT_PROMPT_WITH_DB,DEFAULT_AUTO_EXTRACT_SCHEMA:DEFAULT_AUTO_EXTRACT_SCHEMA,DEFAULT_AUTO_EXTRACT_PATCH_SCHEMA:DEFAULT_AUTO_EXTRACT_PATCH_SCHEMA,DEFAULT_TEMPORAL_EXTRACT_PROMPT:DEFAULT_TEMPORAL_EXTRACT_PROMPT,DEFAULT_TEMPORAL_EXTRACT_SCHEMA:DEFAULT_TEMPORAL_EXTRACT_SCHEMA,DEFAULT_TEMPORAL_RECALL_JUDGE_PROMPT:DEFAULT_TEMPORAL_RECALL_JUDGE_PROMPT,DEFAULT_TEMPORAL_RECALL_JUDGE_SCHEMA:DEFAULT_TEMPORAL_RECALL_JUDGE_SCHEMA,AUTO_EXTRACT_PROMPT_VERSION:AUTO_EXTRACT_PROMPT_VERSION,LEGACY_AUTO_EXTRACT_PROMPTS_WITH_DB:LEGACY_AUTO_EXTRACT_PROMPTS_WITH_DB}=_w.__LoreInj;const db=C.getDB();const _ls=_w.localStorage;function isChatRoute(){const fn=_w.__LoreInj&&_w.__LoreInj.isChatPath;if(typeof fn==="function"){try{return!!fn()}catch(_){}}return/\/characters\/[a-f0-9]+\/chats\/[a-f0-9]+/.test(location.pathname)||/\/stories\/[a-f0-9]+\/episodes\/[a-f0-9]+/.test(location.pathname)||/\/u\/[a-f0-9]+\/c\/[a-f0-9]+/.test(location.pathname)}function runWhenChatRouteLocal(init){if(isChatRoute())return init();let last=location.href;let started=false;const tick=()=>{if(last===location.href)return;last=location.href;if(started||!isChatRoute())return;started=true;init()};try{new MutationObserver(tick).observe(document.documentElement,{childList:true,subtree:true})}catch(_){}try{window.addEventListener("popstate",tick)}catch(_){}try{window.addEventListener("hashchange",tick)}catch(_){}}let _heavyRuntimeInitDone=false;let _heavyRuntimeInitPromise=null;async function ensureHeavyRuntimeInit(reason=""){if(_heavyRuntimeInitDone)return true;if(_heavyRuntimeInitPromise)return _heavyRuntimeInitPromise;_heavyRuntimeInitPromise=(async()=>{await runLocalMigration();if(R&&!R.__loreInjectorRuntimeInitDone){R.init(C,()=>settings.config,(url,logItem)=>{if(!settings.config.urlRefinerLogs)settings.config.urlRefinerLogs={};const chatKey=getChatKey();let logs=settings.config.urlRefinerLogs[chatKey]||[];logItem._id=Date.now()+"-"+Math.random().toString(36).slice(2,6);logs.unshift(logItem);if(logs.length>50)logs.length=50;settings.config.urlRefinerLogs[chatKey]=logs;settings.save()},(msg,color)=>{if(typeof ToastifyInjection!=="undefined")ToastifyInjection.show(msg,{duration:3e3,background:color})},url=>settings.config.urlPacks?.[url]||[]);R.__loreInjectorRuntimeInitDone=true}_heavyRuntimeInitDone=true;_w.__LoreInj.__settingsHeavyDeferred=false;_w.__LoreInj.__heavyRuntimeLoaded=true;console.log("[LoreInj:3] heavy runtime initialized",reason||"chat");return true})().catch(e=>{console.warn("[LoreInj:3] heavy runtime init 실패:",e);throw e}).finally(()=>{_heavyRuntimeInitPromise=null});return _heavyRuntimeInitPromise}function defaultRefinerTopics(){const out={};if(R&&R.TOPICS)Object.keys(R.TOPICS).forEach(k=>out[k]=true);return out}function defaultRefinerPrompt(){const topics=defaultRefinerTopics();if(R&&R.buildDynamicPrompt&&Object.keys(topics).length)return R.buildDynamicPrompt(topics);return R?R.DEFAULT_PROMPT:""}function getApiConfigSnapshot(config){const keys=["autoExtApiType","autoExtKey","autoExtVertexJson","autoExtVertexLocation","autoExtVertexProjectId","autoExtFirebaseScript","autoExtFirebaseEmbedKey","autoExtModel","autoExtCustomModel","autoExtReasoning","autoExtBudget","embeddingModel","rerankModel","refinerModel","refinerCustomModel","temporalRecallJudgeModel","temporalRecallJudgeCustomModel","temporalRecallJudgeReasoning"];const out={};keys.forEach(k=>{if(config&&config[k]!==undefined)out[k]=config[k]});return out}function resetSettingsKeepApi(){const api=getApiConfigSnapshot(settings.config);const preserved={urlPacks:settings.config.urlPacks||{},urlAutoExtPacks:settings.config.urlAutoExtPacks||{},autoPacks:settings.config.autoPacks||["자동추출"]};settings.config=JSON.parse(JSON.stringify(defaultSettings));Object.assign(settings.config,api,preserved);settings.save()}function parseJsonLoose(text){if(!text)return null;let t=String(text).trim();t=t.replace(/^```(?:json)?\s*/i,"").replace(/\s*```$/i,"");try{return JSON.parse(t)}catch{}const firstBracket=t.search(/[\[{]/);if(firstBracket>=0){const open=t[firstBracket];const close=open==="["?"]":"}";const lastClose=t.lastIndexOf(close);if(lastClose>firstBracket){try{return JSON.parse(t.slice(firstBracket,lastClose+1))}catch{}}}return null}async function createSnapshot(packName,label,type="auto"){const entries=await db.entries.where("packName").equals(packName).toArray();const clean=entries.map(({id:id,...rest})=>rest);await db.snapshots.add({packName:packName,timestamp:Date.now(),label:label||"자동 저장",type:type,data:clean});const all=await db.snapshots.where("packName").equals(packName).sortBy("timestamp");if(all.length>10)await db.snapshots.bulkDelete(all.slice(0,all.length-10).map(s=>s.id))}async function restoreSnapshot(snapshotId){const snap=await db.snapshots.get(snapshotId);if(!snap)return false;await db.transaction("rw",db.packs,db.entries,async()=>{await db.entries.where("packName").equals(snap.packName).delete();for(const e of snap.data)await db.entries.add(e);await db.packs.update(snap.packName,{entryCount:snap.data.length})});try{_ls.removeItem("lore-local-migration-version");if(settings&&settings.config){settings.config.localMigrationVersion="";settings.save()}await runLocalMigration()}catch(e){console.warn("[LoreInj:3] restore 후 마이그레이션 재실행 실패:",e)}return true}function convertLegacyEventToTimeline(entry,opts={}){if(!entry||typeof entry!=="object")return entry;const _t=String(entry.type||"").toLowerCase();if(_t!=="event"&&_t!=="scene")return entry;const _clamp10=(v,fb)=>{const n=Number(v);if(!Number.isFinite(n))return fb;return Math.max(1,Math.min(10,Math.round(n)))};const out=JSON.parse(JSON.stringify(entry));out.type=C.TIMELINE_EVENT_TYPE||"timeline_event";out.legacyOrigType=entry.type;out.title=out.title||out.name;const _entSrc=Array.isArray(entry.entities)&&entry.entities.length?entry.entities:Array.isArray(entry.parties)?entry.parties:entry.detail&&Array.isArray(entry.detail.parties)?entry.detail.parties:[];out.participants=Array.isArray(entry.participants)&&entry.participants.length?entry.participants:Array.from(new Set(_entSrc));out.hooks=Array.isArray(entry.hooks)?entry.hooks:[];out.recallTriggers=Array.isArray(entry.recallTriggers)&&entry.recallTriggers.length?entry.recallTriggers:Array.from(new Set([...Array.isArray(entry.triggers)?entry.triggers:[],...Array.isArray(entry.hooks)?entry.hooks:[]].filter(Boolean)));out.actions=Array.isArray(entry.actions)?entry.actions:[];out.linkedLore=Array.isArray(entry.linkedLore)?entry.linkedLore:[];if(!out.when||typeof out.when!=="object"){out.when={anchor:entry.when&&entry.when.anchor||"",timeline:entry.timeline&&entry.timeline.sceneLabel||""}}out.imp=_clamp10(entry.imp!=null?entry.imp:entry.importance,6);out.emo=_clamp10(entry.emo!=null?entry.emo:entry.emotional,5);const _surBase=entry.sur!=null?entry.sur:entry.surprise!=null?entry.surprise:entry.confidence!=null?Number(entry.confidence)*10:null;out.sur=_clamp10(_surBase,6);out.gs=out.imp+out.emo+out.sur;if(C.stableTimelineEventId){try{out.eventId=out.eventId||C.stableTimelineEventId(out)}catch(_){}}out.migratedFromVersion=entry.migratedFromVersion||"event-pre-pass11";out.lastUpdated=Date.now();return out}const defaultSettings={enabled:true,position:"before",prefix:OOC_FORMATS.default.prefix,suffix:OOC_FORMATS.default.suffix,scanRange:5,scanOffset:2,maxEntries:3,cooldownEnabled:true,cooldownTurns:3,statusBadgeEnabled:true,strictMatch:true,similarityMatch:true,activeProject:"",autoExtEnabled:true,autoExtTurns:5,autoExtScanRange:5,autoExtOffset:3,autoExtPack:"자동추출",autoExtMaxRetries:2,autoExtApiType:"key",autoExtVertexJson:"",autoExtVertexLocation:"global",autoExtVertexProjectId:"",autoExtFirebaseScript:"",autoExtFirebaseEmbedKey:"",autoExtKey:"",autoExtModel:"gemini-3-flash-preview",autoExtCustomModel:"",autoExtReasoning:"medium",autoExtBudget:2048,autoExtPrefix:"",autoExtSuffix:"",autoExtIncludeDb:true,autoExtIncludePersona:true,autoExtPatchMode:true,autoExtDbDigestLimit:40,temporalExtractEnabled:true,temporalExtractMode:"after_general",temporalCriticEnabled:false,temporalMaxEventsPerPass:5,temporalExtractPrompt:DEFAULT_TEMPORAL_EXTRACT_PROMPT,temporalExtractSchema:DEFAULT_TEMPORAL_EXTRACT_SCHEMA,timelineRetrievalEnabled:true,timelineRecallWeight:.32,timelineNoCuePenalty:.35,timelineRecallPoolLimit:12,temporalInjectionEnabled:true,temporalRecallChars:450,temporalRecallNaturalChars:260,temporalRecallMaxEvents:2,temporalRecallExplicitMaxEvents:3,temporalRecallReserveChars:120,temporalCompressionEnabled:true,temporalCompressionMinChars:40,temporalCompressionApiEnabled:false,temporalCompressionTargetChars:140,temporalCompressionPreserveFields:["participants","location","hooks"],temporalRecallJudgeEnabled:false,temporalRecallJudgeModel:"gemini-3.1-flash-lite-preview",temporalRecallJudgeCustomModel:"",temporalRecallJudgeReasoning:"minimal",temporalRecallJudgeTimeoutMs:8e3,temporalRecallJudgeCandidateLimit:6,temporalRecallFallbackMode:"deterministic",temporalRecallJudgePrompt:DEFAULT_TEMPORAL_RECALL_JUDGE_PROMPT,temporalRecallJudgeSchema:DEFAULT_TEMPORAL_RECALL_JUDGE_SCHEMA,activeTemplateId:"default",templates:[{id:"default",name:"기본 프롬프트",isDefault:true,schema:DEFAULT_AUTO_EXTRACT_SCHEMA,promptWithoutDb:DEFAULT_AUTO_EXTRACT_PROMPT_WITHOUT_DB,promptWithDb:DEFAULT_AUTO_EXTRACT_PROMPT_WITH_DB}],autoPacks:["자동추출"],urlPacks:{},urlDisabledEntries:{},urlTurnCounters:{},urlCooldownMaps:{},urlAutoExtPacks:{},urlExtLogs:{},urlInjLogs:{},embeddingEnabled:true,activeCharDetection:true,decayEnabled:true,periodicRecallEnabled:true,loreBudgetChars:300,loreBudgetMax:500,compressionMode:"auto",embeddingWeight:.35,useCompressedFormat:true,honorificMatrixEnabled:true,firstEncounterWarning:true,activeCharBoostEnabled:true,decayHalfLife:C.DEFAULTS.decayHalfLife,embeddingModel:"gemini-embedding-001",autoEmbedOnExtract:true,aiMemoryTurns:3,importanceGating:true,importanceThreshold:12,pendingPromiseBoost:true,oocFormat:"default",oocPromptVersion:OOC_FORMAT_VERSION,autoExtractPromptVersion:AUTO_EXTRACT_PROMPT_VERSION,rerankEnabled:false,rerankModel:"gemini-3-flash-preview",rerankPrompt:C.DEFAULTS.rerankPrompt,refinerEnabled:false,refinerAutoMode:false,refinerPassKeyword:"PASS",refinerModel:"gemini-3.1-flash-lite-preview",refinerCustomModel:"",refinerContextTurns:3,refinerCustomPrompt:defaultRefinerPrompt(),refinerLoreMode:"semantic",refinerMatchTurns:5,refinerUseDynamic:true,refinerTopics:defaultRefinerTopics(),refinerPromptVersion:R?R.PROMPT_VERSION:"",urlRefinerLogs:{},localMigrationVersion:"",migrationStatus:{version:"",oldFormatDetected:false,migratedEntries:0,staleEmbeddingsRemoved:0,checkedAt:0,message:""}};const settings={config:JSON.parse(JSON.stringify(defaultSettings)),_lastSaveTime:0,save:function(){try{this._lastSaveTime=Date.now();_ls.setItem("lore-injector-v5",JSON.stringify(this.config))}catch(e){}},load:function(){try{const saved=_ls.getItem("lore-injector-v5");if(saved){const p=JSON.parse(saved);if(p&&typeof p==="object"){for(const k in p){if(p[k]!==undefined)this.config[k]=p[k]}if(Array.isArray(this.config.templates)){const dT=this.config.templates.find(t=>t.isDefault||t.id==="default");if(dT){dT.schema=DEFAULT_AUTO_EXTRACT_SCHEMA;dT.promptWithoutDb=DEFAULT_AUTO_EXTRACT_PROMPT_WITHOUT_DB;dT.promptWithDb=DEFAULT_AUTO_EXTRACT_PROMPT_WITH_DB}}try{const oldOocPrefix="**OOC: Reference — factual background data. Incorporate naturally, never repeat verbatim.";const oldOocSuffix="**";const savedOocVer=this.config.oocPromptVersion||"";if(savedOocVer!==OOC_FORMAT_VERSION){const norm=s=>String(s||"").trim().replace(/\s+/g," ");const prefix=norm(this.config.prefix);const suffix=norm(this.config.suffix);const isDefaultish=!prefix||prefix===norm(oldOocPrefix)||prefix===norm(OOC_FORMATS.default.prefix)||this.config.oocFormat!=="custom";const suffixDefaultish=!suffix||suffix===norm(oldOocSuffix)||suffix===norm(OOC_FORMATS.default.suffix)||this.config.oocFormat!=="custom";if(isDefaultish&&suffixDefaultish){this.config.oocFormat="default";this.config.prefix=OOC_FORMATS.default.prefix;this.config.suffix=OOC_FORMATS.default.suffix}this.config.oocPromptVersion=OOC_FORMAT_VERSION;this.save()}}catch(e){}}}try{const APV=AUTO_EXTRACT_PROMPT_VERSION;const LEGACY=LEGACY_AUTO_EXTRACT_PROMPTS_WITH_DB||[];const savedAPV=this.config.autoExtractPromptVersion||"";if(savedAPV!==APV&&Array.isArray(this.config.templates)){const norm=s=>(s||"").trim().replace(/\s+/g," ");let migrated=0;for(const t of this.config.templates){if(!t||!t.promptWithDb)continue;const n=norm(t.promptWithDb);if(LEGACY.some(p=>norm(p)===n)){t.promptWithDb=DEFAULT_AUTO_EXTRACT_PROMPT_WITH_DB;migrated++}}this.config.autoExtractPromptVersion=APV;this.save();if(migrated)console.log("[LoreInj:3] auto-extract prompt 마이그레이션:",migrated,"템플릿")}}catch(e){}const refSaved=_ls.getItem("speech-refiner-v1");if(refSaved){const r=JSON.parse(refSaved);if(r&&typeof r==="object"){this.config.refinerEnabled=r.enabled??this.config.refinerEnabled;this.config.refinerAutoMode=r.autoMode??this.config.refinerAutoMode;this.config.refinerPassKeyword=r.passKeyword??this.config.refinerPassKeyword;this.config.refinerContextTurns=r.contextTurns??this.config.refinerContextTurns;this.config.refinerCustomPrompt=r.customPrompt??this.config.refinerCustomPrompt;this.config.refinerLoreMode=r.refinerLoreMode??this.config.refinerLoreMode;this.config.refinerMatchTurns=r.refinerMatchTurns??this.config.refinerMatchTurns;this.config.urlRefinerLogs=r.urlRefinerLogs??this.config.urlRefinerLogs;_ls.removeItem("speech-refiner-v1");this.save()}}try{const R2=_w.__LoreRefiner;if(R2&&R2.PROMPT_VERSION&&R2.LEGACY_PROMPTS){const saved=(this.config.refinerCustomPrompt||"").trim();const savedVer=this.config.refinerPromptVersion||"";const norm=s=>(s||"").trim().replace(/\s+/g," ");if(savedVer!==R2.PROMPT_VERSION){const isLegacy=!saved||R2.LEGACY_PROMPTS.some(p=>norm(p)===norm(saved))||norm(R2.DEFAULT_PROMPT)===norm(saved);if(isLegacy){const topics={};if(R2.TOPICS)Object.keys(R2.TOPICS).forEach(k=>topics[k]=true);this.config.refinerTopics=topics;this.config.refinerUseDynamic=true;this.config.refinerCustomPrompt=R2.buildDynamicPrompt?R2.buildDynamicPrompt(topics):R2.DEFAULT_PROMPT;this.config.refinerContextTurns=Math.max(this.config.refinerContextTurns||0,3)}this.config.refinerPromptVersion=R2.PROMPT_VERSION;this.save()}}}catch(e){}}catch(e){}},getActiveTemplate:function(){const id=this.config.activeTemplateId||"default";const t=(this.config.templates||[]).find(x=>x.id===id);return t||this.config.templates[0]}};function summaryToLevels(summary,entry){const base=typeof summary==="object"&&summary&&!Array.isArray(summary)?{...summary}:{};const fallback=typeof summary==="string"?summary:entry.inject?.full||entry.inject?.compact||entry.inject?.micro||entry.name||"";const full=String(base.full||fallback||"").trim();const compact=String(base.compact||base.micro||full||entry.name||"").trim();const micro=String(base.micro||compact||entry.name||"").trim();return{full:full,compact:compact,micro:micro}}function normalizeCallStateLocal(entry){const out={};const add=(key,raw,meta={})=>{if(!key)return;const obj=raw&&typeof raw==="object"&&!Array.isArray(raw)?raw:{};const current=obj.currentTerm||obj.term||obj.current||obj.call||(typeof raw==="string"?raw:"");if(!current)return;let previousTerms=obj.previousTerms||obj.previous||obj.prev||[];if(typeof previousTerms==="string")previousTerms=[previousTerms];if(!Array.isArray(previousTerms))previousTerms=[];previousTerms=Array.from(new Set(previousTerms.filter(x=>x&&x!==current)));out[key]={currentTerm:current,previousTerms:previousTerms,tone:obj.tone||meta.tone||"neutral",scope:obj.scope||meta.scope||"stable",lastChangedTurn:obj.lastChangedTurn||obj.turn||meta.turn||entry.updatedTurn||entry.createdTurn||0,confidence:obj.confidence!=null?obj.confidence:.7,reason:obj.reason||meta.reason||"local migration"}};const source=entry.callState||entry.detail?.callState;if(source&&typeof source==="object"){if(Array.isArray(source))source.forEach(cs=>add(`${cs.from||cs.speaker||"?"}→${cs.to||cs.target||cs.addressee||"?"}`,cs));else if(source.currentTerm||source.term)add(`${source.from||source.speaker||"?"}→${source.to||source.target||source.addressee||"?"}`,source);else Object.entries(source).forEach(([k,v])=>add(k,v))}if(entry.detail?.nicknames&&typeof entry.detail.nicknames==="object"){Object.entries(entry.detail.nicknames).forEach(([k,v])=>add(k,v,{reason:"migrated from detail.nicknames"}))}if(entry.call&&typeof entry.call==="object"){Object.entries(entry.call).forEach(([k,v])=>add(k,v,{reason:"migrated from call"}))}if(Array.isArray(entry.callHistory)){const byKey={};entry.callHistory.forEach(h=>{if(!h||!h.from||!h.to||!h.term)return;const k=`${h.from}→${h.to}`;(byKey[k]=byKey[k]||[]).push(h)});Object.entries(byKey).forEach(([k,arr])=>{arr.sort((a,b)=>(a.turn||0)-(b.turn||0));const last=arr[arr.length-1];const previousTerms=Array.from(new Set(arr.slice(0,-1).map(x=>x.term).filter(t=>t&&t!==last.term)));add(k,{...last,currentTerm:last.term,previousTerms:previousTerms,reason:last.reason||"migrated from callHistory"})})}return out}function normalizeEntryForMigration(entry,currentTurn){const patch={};let oldFormat=false;const oldSummary=typeof entry.summary==="string"||!entry.summary||typeof entry.summary!=="object";const summary=summaryToLevels(entry.summary,entry);if(oldSummary||!entry.summary.full||!entry.summary.compact||!entry.summary.micro){patch.summary=summary;oldFormat=true}const inject=entry.inject&&typeof entry.inject==="object"?{...entry.inject}:{};if(!inject.full||!inject.compact||!inject.micro){patch.inject={full:inject.full||summary.full,compact:inject.compact||summary.compact,micro:inject.micro||summary.micro};oldFormat=true}const callState=normalizeCallStateLocal(entry);if(Object.keys(callState).length&&!entry.callState){patch.callState=callState;oldFormat=true}const eventTurn=entry.eventTurn||entry.timeline?.eventTurn||entry.createdTurn||entry.updatedTurn||currentTurn||0;const timeline=entry.timeline&&typeof entry.timeline==="object"?{...entry.timeline}:{};if(!entry.timeline||timeline.eventTurn==null){patch.timeline={...timeline,eventTurn:eventTurn,relativeOrder:timeline.relativeOrder||(eventTurn&¤tTurn&&eventTurn{const n=Number(v);if(!Number.isFinite(n))return fb;return Math.max(1,Math.min(10,Math.round(n)))};const _impVal=entry.imp==null?0:Number(entry.imp);const _emoVal=entry.emo==null?0:Number(entry.emo);const _surVal=entry.sur==null?0:Number(entry.sur);if((!_impVal||_impVal<=0)&&entry.importance!=null){patch.imp=_clamp10(entry.importance,6);oldFormat=true}else if(!_impVal||_impVal<=0){patch.imp=6;oldFormat=true}if((!_emoVal||_emoVal<=0)&&entry.emotional!=null){patch.emo=_clamp10(entry.emotional,5);oldFormat=true}else if(!_emoVal||_emoVal<=0){patch.emo=5;oldFormat=true}if((!_surVal||_surVal<=0)&&entry.surprise!=null){patch.sur=_clamp10(entry.surprise,6);oldFormat=true}else if((!_surVal||_surVal<=0)&&entry.confidence!=null){patch.sur=_clamp10(Number(entry.confidence)*10,6);oldFormat=true}else if(!_surVal||_surVal<=0){patch.sur=6;oldFormat=true}const _finalImp=patch.imp!=null?patch.imp:_impVal||6;const _finalEmo=patch.emo!=null?patch.emo:_emoVal||5;const _finalSur=patch.sur!=null?patch.sur:_surVal||6;const _finalGs=_finalImp+_finalEmo+_finalSur;if(entry.gs!==_finalGs){patch.gs=_finalGs;oldFormat=true}}if(entry.type==="event"&&(!Array.isArray(entry.recallTriggers)||!entry.recallTriggers.length)){const _fromTriggers=Array.isArray(entry.triggers)?entry.triggers:[];const _fromHooks=Array.isArray(entry.hooks)?entry.hooks:[];const _merged=Array.from(new Set([..._fromTriggers,..._fromHooks].filter(Boolean)));if(_merged.length){patch.recallTriggers=_merged;oldFormat=true}}const now=Date.now();if(!entry.createdTurn)patch.createdTurn=eventTurn||currentTurn||0;if(!entry.updatedTurn)patch.updatedTurn=currentTurn||eventTurn||0;if(!entry.realTimestamp&&(entry.lastUpdated||entry.ts))patch.realTimestamp=entry.lastUpdated||entry.ts;if(!entry.migratedFromVersion||oldFormat)patch.migratedFromVersion=entry.migratedFromVersion||"pre-1.4.0-test";patch.localMigrationVersion=C.LOCAL_MIGRATION_VERSION||"1.4.0-test-pass11-local";patch.lastMigrationAt=now;return{patch:patch,oldFormat:oldFormat}}async function runLocalMigration(){const target=C.LOCAL_MIGRATION_VERSION||"1.4.0-test-pass11-local";const last=_ls.getItem("lore-local-migration-version")||settings.config.localMigrationVersion||"";const status={version:target,oldFormatDetected:false,migratedEntries:0,staleEmbeddingsRemoved:0,checkedAt:Date.now(),message:""};try{const currentTurn=getTurnCounter(getChatKey());const entries=await db.entries.toArray();for(const e of entries){const{patch:patch,oldFormat:oldFormat}=normalizeEntryForMigration(e,currentTurn);const keys=Object.keys(patch).filter(k=>JSON.stringify(e[k])!==JSON.stringify(patch[k]));if(!keys.length&&last===target)continue;if(oldFormat)status.oldFormatDetected=true;const update={};keys.forEach(k=>update[k]=patch[k]);if(Object.keys(update).length){await db.entries.update(e.id,update);status.migratedEntries++}}if(C.cleanupStaleEmbeddings){const clean=await C.cleanupStaleEmbeddings(null,{model:settings.config.embeddingModel||C.DEFAULTS.embeddingModel});status.staleEmbeddingsRemoved=clean.removed||0}status.message=status.oldFormatDetected?"Old lore format detected and locally migrated. Review recommended.":"Local migration check complete.";settings.config.localMigrationVersion=target;settings.config.migrationStatus=status;settings.save();_ls.setItem("lore-local-migration-version",target);_ls.setItem("lore-local-migration-status",JSON.stringify(status))}catch(e){status.message="Local migration failed: "+(e.message||String(e));settings.config.migrationStatus=status;settings.save();console.warn("[LoreInj:migration] failed:",e)}return status}function getMigrationStatus(){return settings.config.migrationStatus||JSON.parse(_ls.getItem("lore-local-migration-status")||"null")||null}settings.load();if(isChatRoute()){await ensureHeavyRuntimeInit("initial-chat")}else{_w.__LoreInj.__settingsHeavyDeferred=true;console.log("[LoreInj:3] non-chat route: local migration/refiner init deferred");const bootHeavy=()=>ensureHeavyRuntimeInit("route-chat").catch(e=>console.warn("[LoreInj:3] route heavy init 실패:",e));if(_w.__LoreInj&&typeof _w.__LoreInj.runWhenChatRoute==="function")_w.__LoreInj.runWhenChatRoute(bootHeavy);else runWhenChatRouteLocal(bootHeavy)}window.addEventListener("storage",e=>{if(e.key==="lore-injector-v5")settings.load()});window.addEventListener("focus",()=>{if(Date.now()-(settings._lastSaveTime||0)>3e3)settings.load()});function getChatKey(){try{const id=C.getCurrentChatId();if(id)return"chat:"+id}catch(e){}const m=window.location.pathname.match(/\/(?:chats|episodes)\/([a-f0-9]+)/);if(m)return"chat:"+m[1];return C.getCurUrl()}function incrementTurnCounter(chatKey){return C.incrementTurn(chatKey)}function recordEntryMention(chatKey,entryId){return C.recordMention(chatKey,entryId)}function getTurnCounter(chatKey){const c=JSON.parse(_ls.getItem("lore-turn-counters")||"{}");return c[chatKey]||0}function setTurnCounter(chatKey,val){const c=JSON.parse(_ls.getItem("lore-turn-counters")||"{}");c[chatKey]=val;_ls.setItem("lore-turn-counters",JSON.stringify(c))}function getCooldownMap(chatKey){if(!settings.config.urlCooldownMaps)settings.config.urlCooldownMaps={};return settings.config.urlCooldownMaps[chatKey]||{}}function setCooldownLastTurn(chatKey,id,turn){if(!settings.config.urlCooldownMaps)settings.config.urlCooldownMaps={};if(!settings.config.urlCooldownMaps[chatKey])settings.config.urlCooldownMaps[chatKey]={};settings.config.urlCooldownMaps[chatKey][id]=turn;settings.save()}async function getAutoExtPackForUrl(url){if(!settings.config.urlAutoExtPacks)settings.config.urlAutoExtPacks={};if(settings.config.urlAutoExtPacks[url])return settings.config.urlAutoExtPacks[url];let baseName="자동추출";try{let chatId=C.getCurrentChatId();if(!chatId){const match=url.match(/\/episodes\/([a-f0-9]+)/);if(match)chatId=match[1]}if(chatId){const CU=_w.CrackUtil||(typeof CrackUtil!=="undefined"?CrackUtil:null);const room=CU?await CU.chatRoom().roomData(chatId):null;if(room&&!(room instanceof Error))baseName=room.story?.name||room.title||"자동추출"}}catch(e){}let finalName=baseName,counter=1;const existing=Object.values(settings.config.urlAutoExtPacks||{});while(true){const checkName=counter===1?baseName:`${baseName} ${counter}`;const inDb=await db.packs.get(checkName);if(!inDb&&!existing.includes(checkName)){finalName=checkName;break}counter++}settings.config.urlAutoExtPacks[url]=finalName;settings.save();return finalName}function setAutoExtPackForUrl(url,packName){if(!settings.config.urlAutoExtPacks)settings.config.urlAutoExtPacks={};settings.config.urlAutoExtPacks[url]=packName;settings.save()}function getUrlStateKey(url){const curUrl=url||C.getCurUrl();const packsByUrl=settings.config.urlPacks||{};if(packsByUrl[curUrl])return curUrl;try{const chatId=C.getCurrentChatId&&C.getCurrentChatId();if(chatId){const found=Object.keys(packsByUrl).find(k=>k&&k.includes(chatId)&&packsByUrl[k]&&packsByUrl[k].length);if(found)return found}}catch(_){}return curUrl}function getActivePacksForUrl(url){const key=getUrlStateKey(url);return settings.config.urlPacks&&settings.config.urlPacks[key]||[]}function getDisabledEntriesForUrl(url){const key=getUrlStateKey(url);return settings.config.urlDisabledEntries&&settings.config.urlDisabledEntries[key]||[]}function getExtLog(chatKey){return settings.config.urlExtLogs?.[chatKey]||[]}function addExtLog(chatKey,logItem){if(!settings.config.urlExtLogs)settings.config.urlExtLogs={};let logs=settings.config.urlExtLogs[chatKey]||[];logs.unshift(logItem);if(logs.length>30)logs.length=30;settings.config.urlExtLogs[chatKey]=logs;settings.save()}function clearExtLog(chatKey){if(!settings.config.urlExtLogs)settings.config.urlExtLogs={};settings.config.urlExtLogs[chatKey]=[];settings.save()}function getInjLog(chatKey){return settings.config.urlInjLogs?.[chatKey]||[]}function addInjLog(chatKey,logItem){if(!settings.config.urlInjLogs)settings.config.urlInjLogs={};let logs=settings.config.urlInjLogs[chatKey]||[];logs.unshift(logItem);if(logs.length>100)logs.length=100;settings.config.urlInjLogs[chatKey]=logs;settings.save()}function clearInjLog(chatKey){if(!settings.config.urlInjLogs)settings.config.urlInjLogs={};settings.config.urlInjLogs[chatKey]=[];settings.save()}function isEntryEnabledForUrl(entry){const packs=getActivePacksForUrl();const disabled=getDisabledEntriesForUrl();return packs.includes(entry.packName)&&!disabled.includes(entry.id)}async function setPackEnabled(packName,state){const curUrl=getUrlStateKey();const up=JSON.parse(JSON.stringify(settings.config.urlPacks||{}));const ud=JSON.parse(JSON.stringify(settings.config.urlDisabledEntries||{}));up[curUrl]=up[curUrl]||[];ud[curUrl]=ud[curUrl]||[];if(state){if(!up[curUrl].includes(packName))up[curUrl].push(packName);const its=await db.entries.where("packName").equals(packName).toArray();const ids=its.map(x=>x.id);ud[curUrl]=ud[curUrl].filter(id=>!ids.includes(id))}else{up[curUrl]=up[curUrl].filter(p=>p!==packName)}settings.config.urlPacks=up;settings.config.urlDisabledEntries=ud;settings.save()}function setEntryEnabled(entry,state){const curUrl=getUrlStateKey();const up=JSON.parse(JSON.stringify(settings.config.urlPacks||{}));const ud=JSON.parse(JSON.stringify(settings.config.urlDisabledEntries||{}));up[curUrl]=up[curUrl]||[];ud[curUrl]=ud[curUrl]||[];if(state){ud[curUrl]=ud[curUrl].filter(id=>id!==entry.id);if(!up[curUrl].includes(entry.packName))up[curUrl].push(entry.packName)}else{if(!ud[curUrl].includes(entry.id))ud[curUrl].push(entry.id)}settings.config.urlPacks=up;settings.config.urlDisabledEntries=ud;settings.save()}Object.assign(_w.__LoreInj,{C:C,R:R,db:db,_ls:_ls,defaultSettings:defaultSettings,settings:settings,parseJsonLoose:parseJsonLoose,createSnapshot:createSnapshot,restoreSnapshot:restoreSnapshot,convertLegacyEventToTimeline:convertLegacyEventToTimeline,getChatKey:getChatKey,incrementTurnCounter:incrementTurnCounter,recordEntryMention:recordEntryMention,getTurnCounter:getTurnCounter,setTurnCounter:setTurnCounter,getCooldownMap:getCooldownMap,setCooldownLastTurn:setCooldownLastTurn,getAutoExtPackForUrl:getAutoExtPackForUrl,setAutoExtPackForUrl:setAutoExtPackForUrl,getUrlStateKey:getUrlStateKey,getActivePacksForUrl:getActivePacksForUrl,getDisabledEntriesForUrl:getDisabledEntriesForUrl,getExtLog:getExtLog,addExtLog:addExtLog,clearExtLog:clearExtLog,getInjLog:getInjLog,addInjLog:addInjLog,clearInjLog:clearInjLog,isEntryEnabledForUrl:isEntryEnabledForUrl,setPackEnabled:setPackEnabled,setEntryEnabled:setEntryEnabled,getApiConfigSnapshot:getApiConfigSnapshot,resetSettingsKeepApi:resetSettingsKeepApi,runLocalMigration:runLocalMigration,getMigrationStatus:getMigrationStatus,ensureHeavyRuntimeInit:ensureHeavyRuntimeInit,__settingsLoaded:true});console.log("[LoreInj:3] settings+utils loaded")})();(async function(){"use strict";if(document.readyState==="loading")await new Promise(r=>document.addEventListener("DOMContentLoaded",r));const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const deadline=Date.now()+15e3;while(!(_w.__LoreInj&&_w.__LoreInj.__settingsLoaded)&&Date.now()setTimeout(r,50));if(!(_w.__LoreInj&&_w.__LoreInj.__settingsLoaded)){console.error("[LoreInj:4] settings 미로드");return}if(_w.__LoreInj.__extractLoaded)return;const{C:C,db:db,_ls:_ls,settings:settings,parseJsonLoose:parseJsonLoose,createSnapshot:createSnapshot,getChatKey:getChatKey,getTurnCounter:getTurnCounter,getAutoExtPackForUrl:getAutoExtPackForUrl,addExtLog:addExtLog,setPackEnabled:setPackEnabled,DEFAULT_AUTO_EXTRACT_SCHEMA:DEFAULT_AUTO_EXTRACT_SCHEMA,DEFAULT_AUTO_EXTRACT_PATCH_SCHEMA:DEFAULT_AUTO_EXTRACT_PATCH_SCHEMA,DEFAULT_TEMPORAL_EXTRACT_PROMPT:DEFAULT_TEMPORAL_EXTRACT_PROMPT,DEFAULT_TEMPORAL_EXTRACT_SCHEMA:DEFAULT_TEMPORAL_EXTRACT_SCHEMA}=_w.__LoreInj;const OUTPUT_MODE_PATCH=`OUTPUT MODE: SAVE ONLY CHANGES\n- Existing entries are provided as compact digests with stable "id".\n- Output must be one JSON array. Never return a bare object.\n- For an existing entry, do NOT re-output the full object.\n- Output {"op":"patch","id":...} only when something changed.\n- For unchanged existing entries, output nothing.\n- If nothing changed at all, return exactly [].\n- For brand-new lore, output {"op":"add","entry":{...}}.\n- Do not repeat unchanged summary.full.\n- Prefer set.state only if state changed.\n- Prefer append.eventHistory for new concrete events.\n- Prefer append.triggers for new literal aliases.\n- If summary.full is unavoidable, keep it under 220 chars.\n- Anchored entries: only append new triggers/eventHistory/callHistory.`;const OUTPUT_MODE_FULL=`OUTPUT MODE: FULL UPDATED ENTRIES\n- Existing entries are provided as compact digests with stable "id".\n- Output must be one JSON array. Never return a bare object.\n- For each NEW lore, output the complete entry object.\n- For each UPDATED existing entry, output the complete updated entry object and keep the same "name" when possible.\n- Do NOT use add/patch op format in this mode.\n- For unchanged existing entries, output nothing.\n- Anchored entries: only append new triggers and eventHistory.`;const UNIFIED_EXTRACT_SCHEMA=`${DEFAULT_AUTO_EXTRACT_SCHEMA}\n\nPatch-mode alternative when OUTPUT MODE asks for SAVE ONLY CHANGES:\n${DEFAULT_AUTO_EXTRACT_PATCH_SCHEMA||"[]"}`;const TEMPORAL_OUTPUT_MODE_PATCH=`OUTPUT MODE: SAVE ONLY CHANGES\n- Existing important scene memories are provided as compact digests with stable "id".\n- Output must be one JSON array. Never return a bare object.\n- For an existing scene memory, do NOT re-output the full object.\n- Output {"op":"patch","id":...} only when something changed.\n- For unchanged existing scene memories, output nothing.\n- If nothing changed at all, return exactly [].\n- For brand-new important scenes, output {"op":"add","entry":{...}}.\n- If the conversation only repeats already stored scene memories, output [].\n- Keep patch fields tiny: prefer append.hooks, append.recallTriggers, append.actions, or set.summary.compact/micro only when changed.`;const TEMPORAL_OUTPUT_MODE_FULL=`OUTPUT MODE: FULL UPDATED SCENE MEMORIES\n- Existing important scene memories are provided as compact digests with stable "id".\n- Output must be one JSON array. Never return a bare object.\n- For each NEW important scene, output the complete timeline_event object.\n- For each UPDATED existing scene, output the complete updated timeline_event object and keep the same "name" when possible.\n- Do NOT use add/patch op format in this mode.\n- For unchanged existing scene memories, output nothing.\n- If the conversation only repeats already stored scene memories, output [].`;const TEMPORAL_PATCH_SCHEMA=`[\n {\n "op": "add",\n "entry": {\n "type": "timeline_event",\n "title": "Short event title",\n "name": "Stable recall handle",\n "when": {"turnStart": 0, "turnEnd": 0, "relative": "past|current|foreshadow", "anchor": "scene/time anchor", "confidence": 0.8},\n "participants": [],\n "location": "",\n "actions": [],\n "summary": {"full": "self-contained event memory", "compact": "event + consequence + hook", "micro": "handle=current meaning"},\n "hooks": [],\n "linkedLore": [],\n "recallTriggers": [],\n "importance": 8,\n "emotional": 8,\n "confidence": 0.8\n }\n },\n {\n "op": "patch",\n "id": 0,\n "reason": "short reason",\n "set": {\n "title": "",\n "name": "",\n "when": {},\n "location": "",\n "summary": {"compact": "", "micro": ""}\n },\n "append": {\n "participants": [],\n "actions": [],\n "hooks": [],\n "linkedLore": [],\n "recallTriggers": []\n }\n }\n]`;function normalizeEntryForMerge(entry,turn){if(C.normalizeLoreEntry)return C.normalizeLoreEntry(entry,{turn:turn,source:"auto_extracted"});return entry}function mergeArrayUnique(a,b){return Array.from(new Set([...Array.isArray(a)?a:[],...Array.isArray(b)?b:[]].filter(Boolean)))}function mergeCallState(existing,incoming){if(!incoming||typeof incoming!=="object")return existing;const base=existing&&typeof existing==="object"?JSON.parse(JSON.stringify(existing)):{};for(const[k,v]of Object.entries(incoming)){if(!base[k])base[k]=v;else if(typeof base[k]==="object"&&typeof v==="object")base[k]={...base[k],...v};else base[k]=v}return base}function mergeInject(existing,incoming,name,state){if(!incoming||typeof incoming!=="object")return existing;const base=existing&&typeof existing==="object"?JSON.parse(JSON.stringify(existing)):{};const merged=C.mergeLoreSummary?C.mergeLoreSummary(base,incoming,name,state):null;return{full:merged?merged.full:incoming.full||base.full||"",compact:merged?merged.compact:incoming.compact||base.compact||"",micro:incoming.micro||base.micro||""}}function selectExistingForContext(entries,context,limit){const ctx=String(context||"").toLowerCase();const lim=limit||50;const scoreOne=e=>{let s=0;const name=String(e.name||"").toLowerCase();const hay=[e.name,...e.triggers||[],e.embed_text||"",...e.entities||[],...e.parties||[],...e.detail&&e.detail.parties||[]].join(" ").toLowerCase();if(name&&ctx.includes(name))s+=120;for(const t of e.triggers||[]){if(!t||t.length<2)continue;const parts=String(t).split("&&").map(p=>p.trim().toLowerCase()).filter(Boolean);if(parts.length&&parts.every(p=>ctx.includes(p))){s+=parts.length>1?80:30;break}}if(e.anchor)s+=100;if(e.isCurrentArc)s+=60;if(e.type==="rel"||e.type==="relationship"||e.type==="prom"||e.type==="promise")s+=30;s+=(e.imp||5)+(e.emo||0)+(e.sur||0);if(e.lastUpdated)s+=Math.max(0,30-Math.floor((Date.now()-e.lastUpdated)/864e5));if(hay&&ctx&&hay.split(/\s+/).some(w=>w.length>=2&&ctx.includes(w)))s+=10;return s};return[...entries||[]].map(e=>({e:e,s:scoreOne(e)})).sort((a,b)=>b.s-a.s).slice(0,lim).map(x=>x.e)}function entryDigestForExtract(e){const summary=e.summary&&typeof e.summary==="object"?{compact:e.summary.compact||e.summary.full||"",micro:e.summary.micro||""}:{compact:String(e.summary||e.inject?.compact||"").slice(0,180),micro:""};const out={id:e.id,type:e.type,name:e.name,state:e.state||e.detail?.status||e.detail?.current_status||"",triggers:(e.triggers||[]).slice(0,6),summary:summary,entities:(e.entities||e.parties||e.detail?.parties||[]).slice(0,8),anchor:e.anchor===true?true:undefined};if(e.callState)out.callState=e.callState;if(Array.isArray(e.eventHistory)&&e.eventHistory.length)out.eventHistoryTail=e.eventHistory.slice(-3);if(e.type==="prom"||e.type==="promise")out.cond=e.cond||e.detail?.condition||"";return out}function buildExistingLoreContext(entries,context,limit){const selected=selectExistingForContext(entries,context,limit||20);return JSON.stringify(selected.map(entryDigestForExtract),null,2)}function normKey(v){return String(v||"").trim().toLowerCase().replace(/\s+/g,"")}function arrOverlapScore(a,b){const aa=new Set((Array.isArray(a)?a:[]).map(normKey).filter(Boolean));const bb=new Set((Array.isArray(b)?b:[]).map(normKey).filter(Boolean));let n=0;for(const x of aa)if(bb.has(x))n++;return n}function stableForCompare(v){if(Array.isArray(v))return v.map(stableForCompare);if(v&&typeof v==="object"){const out={};Object.keys(v).sort().forEach(k=>{if(["id","ts","lastUpdated","lastMigrationAt","localMigrationVersion","migratedFromVersion","gateScore"].includes(k))return;if(k.startsWith("_"))return;out[k]=stableForCompare(v[k])});return out}return v}function entryContentSignature(entry){try{return JSON.stringify(stableForCompare(entry||{}))}catch(_){return String(entry&&entry.name||"")+"|"+String(entry&&entry.type||"")}}function entryParties(e){let parties=e.parties||e.entities||e.detail?.parties||[];if((!Array.isArray(parties)||parties.length<2)&&typeof e.name==="string"){if(e.name.includes("↔"))parties=e.name.split("↔").map(s=>s.trim()).filter(Boolean);else if(e.name.includes("&"))parties=e.name.split("&").map(s=>s.trim()).filter(Boolean)}return Array.isArray(parties)?parties:[]}async function findExistingForIncoming(packName,e){const TL_TYPE_MERGE=C.TIMELINE_EVENT_TYPE||"timeline_event";if(!e||!e.name)return null;if(e.id!=null){const byId=await db.entries.get(e.id);if(byId&&byId.packName===packName)return byId}if(e.type===TL_TYPE_MERGE)return null;const candidates=await db.entries.where("packName").equals(packName).and(x=>x.type!==TL_TYPE_MERGE).toArray();const nName=normKey(e.name);const nType=normKey(e.type);const eTriggers=e.triggers||[];const eEntities=e.entities||e.parties||e.detail?.parties||[];const eParties=entryParties(e).map(normKey).sort().join("|");let best=null,bestScore=0;for(const x of candidates){const sameType=normKey(x.type)===nType||["rel","relationship"].includes(nType)&&["rel","relationship"].includes(normKey(x.type))||["prom","promise"].includes(nType)&&["prom","promise"].includes(normKey(x.type));if(!sameType)continue;let score=0;if(normKey(x.name)===nName)score+=100;const xParties=entryParties(x).map(normKey).sort().join("|");if(eParties&&xParties&&eParties===xParties)score+=90;score+=arrOverlapScore(eTriggers,x.triggers)*30;score+=arrOverlapScore(eEntities,x.entities||x.parties||x.detail?.parties)*20;if(nName&&(normKey(x.name).includes(nName)||nName.includes(normKey(x.name))))score+=25;if(score>bestScore){bestScore=score;best=x}}return bestScore>=60?best:null}function hasMeaningfulPatchModeFullUpdate(existing,incoming){if(!existing||!incoming)return true;const newTriggers=arrOverlapScore(incoming.triggers||[],existing.triggers||[])<(incoming.triggers||[]).filter(Boolean).length;const newEntities=arrOverlapScore(incoming.entities||incoming.parties||incoming.detail?.parties||[],existing.entities||existing.parties||existing.detail?.parties||[])<(incoming.entities||incoming.parties||incoming.detail?.parties||[]).filter(Boolean).length;const oldState=existing.state||existing.detail?.current_status||existing.detail?.status||existing.detail?.current_state||"";const newState=incoming.state||incoming.detail?.current_status||incoming.detail?.status||incoming.detail?.current_state||"";if(newState&&normKey(newState)!==normKey(oldState))return true;if(incoming.cond!==undefined&&normKey(incoming.cond)!==normKey(existing.cond))return true;if(newTriggers||newEntities)return true;if(Array.isArray(incoming.callDelta)&&incoming.callDelta.length)return true;if(Array.isArray(incoming.eventHistory)&&incoming.eventHistory.some(ev=>{const s=String(ev&&ev.summary||"").trim();return s&&!(existing.eventHistory||[]).some(x=>String(x&&x.summary||"").trim()===s)}))return true;return false}function hasMeaningfulTemporalFullUpdate(existing,incoming){if(!existing||!incoming)return true;if(incoming.location&&normKey(incoming.location)!==normKey(existing.location))return true;const keys=["participants","actions","hooks","linkedLore","recallTriggers"];for(const k of keys){const arr=incoming[k]||[];if(Array.isArray(arr)&&arr.length&&arrOverlapScore(arr,existing[k]||[])x.summary===norm))continue;existing.eventHistory.push({turn:ev.turn||getTurnCounter(chatKey),summary:norm,imp:ev.imp||5,emo:ev.emo||5,ts:Date.now()})}existing.eventHistory.sort((a,b)=>(a.turn||0)-(b.turn||0));if(existing.eventHistory.length>30)existing.eventHistory=existing.eventHistory.slice(-30)}if(Array.isArray(append.callHistory)&&append.callHistory.length){existing.callHistory=existing.callHistory||[];for(const h of append.callHistory){if(!h||!h.from||!h.to||!h.term)continue;existing.callHistory.push({turn:h.turn||getTurnCounter(chatKey),from:h.from,to:h.to,term:h.term,prevTerm:h.prevTerm||null,ts:Date.now()})}if(existing.callHistory.length>30)existing.callHistory=existing.callHistory.slice(-30)}existing.lastUpdated=Date.now();try{if(C.normalizeTemporalGraph)existing=C.normalizeTemporalGraph(existing,{currentTurn:getTurnCounter(chatKey),sceneId:chatKey})}catch(_){}if(entryContentSignature(existing)===beforeSig)return 0;try{if(C.invalidateEntryEmbeddings)await C.invalidateEntryEmbeddings(existing.id)}catch(_){}await db.entries.put(existing);return 1}async function applyTemporalPatchOp(op,packName,chatKey){if(!op||op.op!=="patch"||op.id==null)return 0;let existing=await db.entries.get(op.id);const TL_TYPE=C.TIMELINE_EVENT_TYPE||"timeline_event";if(!existing||existing.packName!==packName||existing.type!==TL_TYPE)return 0;const beforeSig=entryContentSignature(existing);try{if(C.saveEntryVersion)await C.saveEntryVersion(existing,"temporal_patch")}catch(_){}const set=op.set||{};const append=op.append||{};if(set.title!==undefined)existing.title=set.title;if(set.name!==undefined)existing.name=set.name;if(set.when&&typeof set.when==="object")existing.when={...existing.when||{},...set.when};if(set.location!==undefined)existing.location=set.location;if(set.summary)existing.summary=C.mergeLoreSummary?C.mergeLoreSummary(existing.summary,set.summary,existing.name):{...existing.summary||{},...set.summary};const appendArr=key=>{if(Array.isArray(append[key])&&append[key].length){existing[key]=mergeArrayUnique(existing[key],append[key])}};appendArr("participants");appendArr("actions");appendArr("hooks");appendArr("linkedLore");appendArr("recallTriggers");if(Array.isArray(append.recallTriggers)&&append.recallTriggers.length){existing.triggers=mergeArrayUnique(existing.triggers,append.recallTriggers)}existing.lastUpdated=Date.now();try{if(C.normalizeTemporalGraph)existing=C.normalizeTemporalGraph(existing,{currentTurn:getTurnCounter(chatKey),sceneId:chatKey})}catch(_){}if(entryContentSignature(existing)===beforeSig)return 0;try{if(C.invalidateEntryEmbeddings)await C.invalidateEntryEmbeddings(existing.id)}catch(_){}await db.entries.put(existing);return 1}function isExtractItemObject(item){if(!item||typeof item!=="object"||Array.isArray(item))return false;if(item.op==="patch"||item.op==="add")return true;if(item.entry&&typeof item.entry==="object")return true;return!!(item.name||item.title||item.type||item.summary||item.keywords||item.content)}function normalizeExtractItems(entries){if(Array.isArray(entries))return entries.filter(Boolean);if(entries&&!Array.isArray(entries)){if(Array.isArray(entries.entries))return entries.entries.filter(Boolean);if(Array.isArray(entries.items))return entries.items.filter(Boolean);if(Array.isArray(entries.patches))return entries.patches.filter(Boolean);if(isExtractItemObject(entries))return[entries]}return[]}function normalizeTemporalCandidates(parsed,limit){let arr=parsed;if(arr&&!Array.isArray(arr)&&Array.isArray(arr.events))arr=arr.events;if(arr&&!Array.isArray(arr)&&Array.isArray(arr.entries))arr=arr.entries;if(arr&&!Array.isArray(arr)&&isExtractItemObject(arr))arr=[arr];if(!Array.isArray(arr))return[];const max=Math.max(1,limit||5);const clamp10=(v,fb)=>{const n=Number(v);if(!Number.isFinite(n))return fb;return Math.max(1,Math.min(10,Math.round(n)))};return arr.filter(e=>e&&typeof e==="object").filter(e=>e.op!=="patch").map(e=>{const raw=e.op==="add"&&e.entry?e.entry:e;const title=raw.title||raw.name||raw.summary?.micro||raw.summary?.compact||"timeline event";const impBase=raw.imp!=null?raw.imp:raw.importance;const emoBase=raw.emo!=null?raw.emo:raw.emotional;const surBase=raw.sur!=null?raw.sur:raw.surprise!=null?raw.surprise:raw.confidence!=null?Number(raw.confidence)*10:null;const imp=clamp10(impBase,6);const emo=clamp10(emoBase,5);const sur=clamp10(surBase,6);return{...raw,type:C.TIMELINE_EVENT_TYPE||"timeline_event",title:title,name:raw.name||title,imp:imp,emo:emo,sur:sur,gs:imp+emo+sur,source:raw.source||"temporal_extracted"}}).filter(e=>e.name).slice(0,max)}async function callGeminiJsonWithRepair(prompt,apiOpts,repairHint){let res=await C.callGeminiApi(prompt,apiOpts);let parsed=parseJsonLoose(res&&res.text);if(!parsed&&apiOpts&&apiOpts.maxOutputTokens){const firstCost=res&&res.cost;const retryPrompt=prompt+"\n\nJSON REPAIR REQUEST:\n- Your previous response was not valid complete JSON, or it was truncated.\n- Return only one complete JSON array.\n- If there is no change, return exactly [].\n"+(repairHint||"");res=await C.callGeminiApi(retryPrompt,{...apiOpts,maxRetries:0,maxOutputTokens:null,responseMimeType:"application/json"});parsed=parseJsonLoose(res&&res.text);if(res&&firstCost&&res.cost){const retryCost=res.cost;const unknown=!!(firstCost.unknown||retryCost.unknown||firstCost.usd==null||retryCost.usd==null);res.cost={...retryCost,inTok:(Number(firstCost.inTok)||0)+(Number(retryCost.inTok)||0),outTok:(Number(firstCost.outTok)||0)+(Number(retryCost.outTok)||0),usd:unknown?null:(Number(firstCost.usd)||0)+(Number(retryCost.usd)||0),unknown:unknown,estimated:!!(firstCost.estimated||retryCost.estimated),repairRetry:true}}}return{res:res,parsed:parsed}}function temporalDigestForExtract(e){const summary=e.summary&&typeof e.summary==="object"?{compact:e.summary.compact||e.summary.full||"",micro:e.summary.micro||""}:{compact:String(e.summary||"").slice(0,180),micro:""};return{id:e.id,eventId:e.eventId,title:e.title||e.name,name:e.name,when:e.when?{anchor:e.when.anchor||"",relative:e.when.relative||"",turnStart:e.when.turnStart||e.timeline?.eventTurn||0}:undefined,participants:(e.participants||e.entities||[]).slice(0,8),location:e.location||"",actions:(e.actions||[]).slice(0,6),hooks:(e.hooks||[]).slice(0,6),recallTriggers:(e.recallTriggers||e.triggers||[]).slice(0,8),summary:summary}}function injectTemporalExistingBlock(prompt,existingText,outputModeText){const block=`${outputModeText||""}\n\nExisting Important Scene Memories:\n${existingText}\n\nDEDUP RULE:\n- Output [] if the conversation only repeats or overlaps with the existing memories above.\n- Do not restate, rename, or re-summarize an existing memory.\n- Output only genuinely new important scene memories not already covered above.\n`;const marker="\nConversation Log:";const idx=prompt.indexOf(marker);if(idx>=0)return prompt.slice(0,idx)+"\n"+block+prompt.slice(idx);return prompt+"\n\n"+block}async function runTemporalExtractPass(opts={}){if(settings.config.temporalExtractEnabled===false)return{count:0,skipped:true};const context=opts.context||"";const apiOpts=opts.apiOpts||{};const url=opts.url||C.getCurUrl();const chatKey=opts.chatKey||getChatKey();const isManual=!!opts.isManual;const msgCount=opts.msgCount||0;const promptTpl=settings.config.temporalExtractPrompt||DEFAULT_TEMPORAL_EXTRACT_PROMPT;const baseTemporalSchema=settings.config.temporalExtractSchema||DEFAULT_TEMPORAL_EXTRACT_SCHEMA;const schema=`${baseTemporalSchema}\n\nPatch-mode alternative when OUTPUT MODE asks for SAVE ONLY CHANGES:\n${TEMPORAL_PATCH_SCHEMA}`;if(!promptTpl||!schema||!context)return{count:0,skipped:true};const _tmpModel=apiOpts&&apiOpts.model||null;let apiLog=null,_tmpElapsedMs=0,_tmpCost=null;try{const packName=await getAutoExtPackForUrl(url);const _patchOn=settings.config.autoExtIncludeDb&&settings.config.autoExtPatchMode!==false;let existingTemporalText="[]";try{const TL_TYPE=C.TIMELINE_EVENT_TYPE||"timeline_event";const existingTemporal=await db.entries.where("packName").equals(packName).and(e=>e.type===TL_TYPE).toArray();if(existingTemporal.length){existingTemporal.sort((a,b)=>(b.lastUpdated||b.ts||0)-(a.lastUpdated||a.ts||0));existingTemporalText=JSON.stringify(existingTemporal.slice(0,30).map(temporalDigestForExtract),null,2)}}catch(_){}const outputModeText=_patchOn?TEMPORAL_OUTPUT_MODE_PATCH:TEMPORAL_OUTPUT_MODE_FULL;const prompt=injectTemporalExistingBlock(promptTpl.replace("{context}",context).replace("{schema}",schema),existingTemporalText,outputModeText);const _tmpT0=Date.now();const temporalApiOpts={...apiOpts,responseMimeType:"application/json",maxRetries:1,timeoutMs:12e4,maxOutputTokens:_patchOn?1024:null,costContext:{feature:"temporalExtract",chatKey:chatKey||"global"}};const{res:res,parsed:parsed}=await callGeminiJsonWithRepair(prompt,temporalApiOpts,"Use patch objects only when a real timeline memory changes.");_tmpElapsedMs=Date.now()-_tmpT0;_tmpCost=res&&res.cost||null;apiLog=res?{status:res.status,error:res.error,retries:res.retries}:null;if(!res||!res.text)throw new Error("시간축 AI 응답없음 ("+(res&&res.error||"알수없음")+")");if(!parsed)throw new Error("시간축 JSON 파싱 실패 (응답 스니포: "+(res.text||"").slice(0,100)+")");let patchedCount=0;for(const item of normalizeExtractItems(parsed)){if(item&&item.op==="patch")patchedCount+=await applyTemporalPatchOp(item,packName,chatKey)}const events=normalizeTemporalCandidates(parsed,settings.config.temporalMaxEventsPerPass||5);if(!events.length&&patchedCount<=0){addExtLog(chatKey,{time:(new Date).toLocaleTimeString(),count:0,msgs:msgCount,isManual:isManual,status:"시간축 추출 내용 없음",api:apiLog,model:_tmpModel,elapsedMs:_tmpElapsedMs,cost:_tmpCost});return{count:0,empty:true}}const addCount=events.length?await mergeExtractedData(events,url):0;const count=patchedCount+addCount;let embedMsg="";let embedCount=0;if(count>0&&settings.config.embeddingEnabled&&settings.config.autoEmbedOnExtract!==false){try{const epName=await getAutoExtPackForUrl(url);extBadgeShow("에리가 시간축 임베딩 갱신 중");const embedOpts={...apiOpts,model:settings.config.embeddingModel||"gemini-embedding-001"};embedCount=await C.embedPack(epName,embedOpts);embedMsg=" / 임베딩 "+embedCount+"개 완료"}catch(embErr){console.warn("[Lore] 시간축 자동임베딩 실패:",embErr.message);embedMsg=" / 임베딩 실패"}}addExtLog(chatKey,{time:(new Date).toLocaleTimeString(),count:count,msgs:msgCount,isManual:isManual,status:"시간축 추출 성공"+embedMsg,api:apiLog,model:_tmpModel,elapsedMs:_tmpElapsedMs,cost:_tmpCost});return{count:count,events:events}}catch(err){console.warn("[Lore:temporal] 추출 실패:",err);addExtLog(chatKey,{time:(new Date).toLocaleTimeString(),count:0,msgs:msgCount,isManual:isManual,status:"시간축 추출 실패",error:err.message||String(err),api:apiLog,model:_tmpModel,elapsedMs:_tmpElapsedMs,cost:_tmpCost});return{count:0,error:err.message||String(err)}}}async function mergeExtractedData(entries,url){const packName=await getAutoExtPackForUrl(url);const chatKey=getChatKey();let ap=[...settings.config.autoPacks||[]];if(!ap.includes(packName)){ap.push(packName);settings.config.autoPacks=ap;settings.save()}const proj=settings.config.activeProject||"";let pack=await db.packs.get(packName);if(!pack)await db.packs.put({name:packName,entryCount:0,project:proj});else await createSnapshot(packName,"자동 병합 전 백업","auto");let processedCount=0;for(const item of normalizeExtractItems(entries)){if(!item)continue;if(item.op==="patch"){processedCount+=await applyExtractPatchOp(item,packName,chatKey);continue}let e=item.op==="add"&&item.entry?item.entry:item;if(!e.name)continue;e=normalizeEntryForMerge(e,getTurnCounter(chatKey));e.gs=(e.imp||5)+(e.sur||5)+(e.emo||5);if(settings.config.importanceGating!==false){if(e.gs<(settings.config.importanceThreshold||12))continue;e.gateScore=e.gs}processedCount++;const TL_TYPE_MERGE=C.TIMELINE_EVENT_TYPE||"timeline_event";if(e.type===TL_TYPE_MERGE&&C.stableTimelineEventId&&!e.eventId){try{e.eventId=C.stableTimelineEventId(e)}catch(_){}}let existing;if(e.type===TL_TYPE_MERGE){const _tlId=e.eventId||(C.stableTimelineEventId?function(){try{return C.stableTimelineEventId(e)}catch(_){return null}}():null);if(_tlId){existing=await db.entries.where("packName").equals(packName).and(x=>{if(x.type!==TL_TYPE_MERGE)return false;if(x.eventId===_tlId)return true;try{return!!(C.stableTimelineEventId&&C.stableTimelineEventId(x)===_tlId)}catch(_){return false}}).first()}if(!existing){const _wa=e.when&&e.when.anchor?String(e.when.anchor):"";existing=await db.entries.where("packName").equals(packName).and(x=>x.type===TL_TYPE_MERGE&&x.name===e.name&&(_wa?x.when&&x.when.anchor===_wa:true)).first()}}else{existing=await db.entries.where("packName").equals(packName).and(x=>x.name===e.name&&x.type!==TL_TYPE_MERGE).first();if(!existing)existing=await findExistingForIncoming(packName,e)}let _tlMergeBackup=null;if(existing&&e.type===TL_TYPE_MERGE&&existing.anchor!==true){const _mergeArr=(a,b)=>Array.from(new Set([...Array.isArray(a)?a:[],...Array.isArray(b)?b:[]].filter(Boolean)));_tlMergeBackup={participants:_mergeArr(existing.participants,e.participants),hooks:_mergeArr(existing.hooks,e.hooks),recallTriggers:_mergeArr(existing.recallTriggers,e.recallTriggers),linkedLore:_mergeArr(existing.linkedLore,e.linkedLore),actions:_mergeArr(existing.actions,e.actions),when:{...existing.when||{},...e.when||{}}}}if(existing){existing=normalizeEntryForMerge(existing,getTurnCounter(chatKey));const _beforeExistingSig=entryContentSignature(existing);try{if(C.saveEntryVersion)await C.saveEntryVersion(existing,"extract_merge")}catch(ex){}const _anchorGuard=existing.anchor===true;const _anchorSnap=_anchorGuard?{summary:existing.summary,state:existing.state,detail:existing.detail?JSON.parse(JSON.stringify(existing.detail)):undefined,call:existing.call?JSON.parse(JSON.stringify(existing.call)):undefined,callState:existing.callState?JSON.parse(JSON.stringify(existing.callState)):undefined,callHistory:existing.callHistory?JSON.parse(JSON.stringify(existing.callHistory)):undefined,inject:existing.inject?JSON.parse(JSON.stringify(existing.inject)):undefined,timeline:existing.timeline?JSON.parse(JSON.stringify(existing.timeline)):undefined,entities:existing.entities?JSON.parse(JSON.stringify(existing.entities)):undefined,cond:existing.cond,imp:existing.imp,sur:existing.sur,emo:existing.emo,gs:existing.gs,arc:existing.arc?JSON.parse(JSON.stringify(existing.arc)):undefined}:null;if(!_anchorGuard&&["relationship","promise","rel","prom"].includes(e.type)){const oldS=existing.state||existing.detail?.current_status||existing.detail?.status||null;const newS=e.state||e.detail?.current_status||e.detail?.status||null;if(oldS&&newS&&oldS!==newS){const cLog=JSON.parse(_ls.getItem("lore-contradictions")||"[]");cLog.unshift({name:e.name,type:e.type,oldStatus:oldS,newStatus:newS,turn:getTurnCounter(chatKey),time:Date.now()});if(cLog.length>50)cLog.length=50;_ls.setItem("lore-contradictions",JSON.stringify(cLog))}}existing.triggers=[...new Set([...existing.triggers||[],...e.triggers||[]])];if(e.embed_text)existing.embed_text=e.embed_text;if(e.inject)existing.inject=mergeInject(existing.inject,e.inject,existing.name,e.state||existing.state);if(e.state!==undefined)existing.state=e.state;if(e.call)existing.call={...existing.call||{},...e.call};if(e.callState)existing.callState=mergeCallState(existing.callState,e.callState);if(e.timeline)existing.timeline={...existing.timeline||{},...e.timeline};if(e.entities)existing.entities=mergeArrayUnique(existing.entities,e.entities);if(Array.isArray(e.callDelta)&&e.callDelta.length>0){existing.callHistory=existing.callHistory||[];for(const d of e.callDelta){if(!d.from||!d.to||!d.term)continue;existing.callHistory.push({turn:d.turnApprox||getTurnCounter(chatKey),from:d.from,to:d.to,term:d.term,prevTerm:d.prevTerm||null,ts:Date.now()})}if(existing.callHistory.length>30)existing.callHistory=existing.callHistory.slice(-30)}if(Array.isArray(e.eventHistory)&&e.eventHistory.length>0){existing.eventHistory=existing.eventHistory||[];for(const ev of e.eventHistory){if(!ev||!ev.summary)continue;const normSum=ev.summary.trim();if(existing.eventHistory.some(x=>x.summary===normSum))continue;existing.eventHistory.push({turn:ev.turn||getTurnCounter(chatKey),summary:normSum,imp:ev.imp||5,emo:ev.emo||5,ts:Date.now()})}existing.eventHistory.sort((a,b)=>(a.turn||0)-(b.turn||0));if(existing.eventHistory.length>30){const oldEvents=existing.eventHistory.slice(0,20);const recentEvents=existing.eventHistory.slice(20);const rootId=existing.rootId||existing.id;const lastTurn=oldEvents[oldEvents.length-1]?.turn||0;const firstTurn=oldEvents[0]?.turn||0;const shardEntry={name:`${existing.name} [과거 t${firstTurn}~t${lastTurn}]`,type:existing.type,packName:existing.packName,project:existing.project||"",enabled:true,triggers:[...existing.triggers||[]],embed_text:existing.embed_text,rootId:rootId,isCurrentArc:false,eventHistory:oldEvents,inject:{full:`${existing.name}[과거사] ${oldEvents.slice(-2).map(ev=>`t${ev.turn}:${ev.summary.slice(0,40)}`).join("|")}`,compact:`${existing.name}[과거 ${oldEvents.length}건]`,micro:`${existing.name}=과거`},state:"과거사",summary:{full:`${existing.name} 과거 이벤트 ${oldEvents.length}건: ${oldEvents.slice(-3).map(ev=>`t${ev.turn}:${ev.summary}`).join(" / ")}`,compact:`${existing.name} 과거 이벤트 ${oldEvents.length}건`,micro:`${existing.name}=과거`},timeline:{eventTurn:lastTurn,relativeOrder:"past",sceneLabel:"archived shard",observedRecency:"old"},entities:existing.entities||existing.parties||existing.detail?.parties||[],source:"shard_split",src:"sh",ts:Date.now(),lastUpdated:Date.now(),imp:existing.imp,emo:existing.emo,sur:existing.sur,gs:existing.gs};await db.entries.put(shardEntry);existing.eventHistory=recentEvents;existing.rootId=null;existing.isCurrentArc=true}if(existing.inject&&typeof existing.inject==="object"){const base=(existing.inject.full||"").split(" | 최근:")[0];const recent=existing.eventHistory.slice(-2).map(ev=>`t${ev.turn}:${ev.summary.slice(0,40)}`).join("|");existing.inject.full=recent?base+" | 최근:"+recent:base}}if(e.cond!==undefined)existing.cond=e.cond;if(e.imp)existing.imp=e.imp;if(e.sur)existing.sur=e.sur;if(e.emo)existing.emo=e.emo;existing.gs=(existing.imp||5)+(existing.sur||5)+(existing.emo||5);existing.ts=Date.now();if(e.type==="rel"&&Array.isArray(e.arc)){existing.arc=existing.arc||[];for(const a of e.arc){const dup=existing.arc.find(x=>x.ph===a.ph&&x.t===a.t);if(dup)Object.assign(dup,a);else existing.arc.push(a)}}if(["relationship","promise"].includes(e.type)){if(e.summary)existing.summary=C.mergeLoreSummary?C.mergeLoreSummary(existing.summary,e.summary,existing.name,e.state||existing.state):e.summary;if(e.detail){existing.detail=existing.detail||{};if(e.detail.current_status!==undefined)existing.detail.current_status=e.detail.current_status;if(e.detail.status!==undefined)existing.detail.status=e.detail.status;if(e.detail.parties)existing.detail.parties=e.detail.parties;if(e.detail.condition!==undefined)existing.detail.condition=e.detail.condition;if(e.detail.nicknames){existing.detail.nicknames=existing.detail.nicknames||{};Object.assign(existing.detail.nicknames,e.detail.nicknames)}if(Array.isArray(e.detail.arc)){existing.detail.arc=existing.detail.arc||[];for(const newArc of e.detail.arc){const dup=existing.detail.arc.find(a=>a.phase===newArc.phase&&a.approx_turn===newArc.approx_turn);if(dup)Object.assign(dup,newArc);else existing.detail.arc.push(newArc)}}}}else if(e.type!=="rel"&&e.type!=="prom"){if(e.summary)existing.summary=C.mergeLoreSummary?C.mergeLoreSummary(existing.summary,e.summary,existing.name,e.state||existing.state):e.summary;if(e.detail){existing.detail=existing.detail||{};if(e.detail.current_state!==undefined)existing.detail.current_state=e.detail.current_state;if(e.detail.last_interaction!==undefined)existing.detail.last_interaction=e.detail.last_interaction;for(const k in e.detail){if(["current_state","last_interaction"].includes(k))continue;if(!existing.detail[k])existing.detail[k]=e.detail[k];else if(Array.isArray(e.detail[k]))existing.detail[k]=[...new Set([...existing.detail[k]||[],...e.detail[k]])];else if(typeof existing.detail[k]==="string"&&typeof e.detail[k]==="string"&&!existing.detail[k].includes(e.detail[k]))existing.detail[k]+=" "+e.detail[k]}}}if(e.gateScore)existing.gateScore=e.gateScore;existing.lastUpdated=Date.now();if(e.type==="relationship"||e.type==="rel"){let parties=e.detail?.parties||e.parties;if((!parties||parties.length<2)&&typeof e.name==="string"){if(e.name.includes("↔"))parties=e.name.split("↔").map(s=>s.trim()).filter(Boolean);else if(e.name.includes("&"))parties=e.name.split("&").map(s=>s.trim()).filter(Boolean)}if(parties&&parties.length>=2){const[c1,c2]=parties;try{await C.recordFirstEncounter(c1,c2,{turnApprox:getTurnCounter(chatKey),timestamp:Date.now()})}catch(ex){}}}if(_anchorSnap){for(const k of Object.keys(_anchorSnap)){if(_anchorSnap[k]===undefined)delete existing[k];else existing[k]=_anchorSnap[k]}}if(_tlMergeBackup){if(_tlMergeBackup.participants&&_tlMergeBackup.participants.length)existing.participants=_tlMergeBackup.participants;if(_tlMergeBackup.hooks&&_tlMergeBackup.hooks.length)existing.hooks=_tlMergeBackup.hooks;if(_tlMergeBackup.recallTriggers&&_tlMergeBackup.recallTriggers.length)existing.recallTriggers=_tlMergeBackup.recallTriggers;if(_tlMergeBackup.linkedLore&&_tlMergeBackup.linkedLore.length)existing.linkedLore=_tlMergeBackup.linkedLore;if(_tlMergeBackup.actions&&_tlMergeBackup.actions.length)existing.actions=_tlMergeBackup.actions;if(_tlMergeBackup.when&&Object.keys(_tlMergeBackup.when).length)existing.when=_tlMergeBackup.when}try{if(C.normalizeTemporalGraph)existing=C.normalizeTemporalGraph(existing,{currentTurn:getTurnCounter(chatKey),sceneId:chatKey})}catch(_){}if(entryContentSignature(existing)===_beforeExistingSig){processedCount--;continue}try{if(C.invalidateEntryEmbeddings)await C.invalidateEntryEmbeddings(existing.id)}catch(_){}await db.entries.put(existing)}else{e.packName=packName;e.project=proj;e.enabled=true;e.src=e.src||(e.source==="user_stated"?"us":e.source==="imported"?"im":"ax");e.source=e.source||"auto_extracted";e.ts=Date.now();e.lastUpdated=e.ts;if((e.type==="rel"||e.type==="relationship")&&Array.isArray(e.callDelta)&&e.callDelta.length>0){e.callHistory=e.callHistory||[];for(const d of e.callDelta){if(!d.from||!d.to||!d.term)continue;e.callHistory.push({turn:d.turnApprox||getTurnCounter(chatKey),from:d.from,to:d.to,term:d.term,prevTerm:d.prevTerm||null,ts:Date.now()})}}if(Array.isArray(e.eventHistory)&&e.eventHistory.length>0){e.eventHistory=e.eventHistory.filter(ev=>ev&&ev.summary).map(ev=>({turn:ev.turn||getTurnCounter(chatKey),summary:ev.summary.trim(),imp:ev.imp||5,emo:ev.emo||5,ts:Date.now()})).sort((a,b)=>(a.turn||0)-(b.turn||0));if(e.eventHistory.length>30)e.eventHistory=e.eventHistory.slice(-30);if(e.inject&&typeof e.inject==="object"){const base=(e.inject.full||"").split(" | 최근:")[0];const recent=e.eventHistory.slice(-2).map(ev=>`t${ev.turn}:${ev.summary.slice(0,40)}`).join("|");e.inject.full=recent?base+" | 최근:"+recent:base}}try{if(C.normalizeTemporalGraph)e=C.normalizeTemporalGraph(e,{currentTurn:getTurnCounter(chatKey),sceneId:chatKey})}catch(_){}await db.entries.put(e)}}if(processedCount>0){const count=await db.entries.where("packName").equals(packName).count();await db.packs.update(packName,{entryCount:count});await setPackEnabled(packName,true)}return processedCount}const _extQ={running:false,pendingTurns:0,manualPending:false};let _extBadgeMsg=null,_extBadgeWatchdog=null;function extBadgeShow(msg){_extBadgeMsg=msg;try{C.showStatusBadge(msg)}catch(e){}if(_extBadgeWatchdog)return;_extBadgeWatchdog=setInterval(()=>{if(!_extBadgeMsg)return;const badge=document.getElementById("lore-status-badge");if(badge&&badge.style.opacity==="1"&&(badge.textContent||"").includes(_extBadgeMsg))return;try{C.showStatusBadge(_extBadgeMsg)}catch(e){}},1500)}function extBadgeHide(){_extBadgeMsg=null;if(_extBadgeWatchdog){clearInterval(_extBadgeWatchdog);_extBadgeWatchdog=null}try{C.hideStatusBadge()}catch(e){}}document.addEventListener("visibilitychange",()=>{if(!document.hidden&&_extBadgeMsg){try{C.showStatusBadge(_extBadgeMsg)}catch(e){}}});async function runAutoExtract(isManual=false){if(_extQ.running){_extQ.pendingTurns++;if(isManual)_extQ.manualPending=true;return}_extQ.running=true;_extQ.pendingTurns=0;_extQ.manualPending=false;extBadgeShow("에리가 대화 분석 중");try{await _doExtract(isManual)}finally{_extQ.running=false;extBadgeHide();if(_extQ.pendingTurns>0||_extQ.manualPending){const nextManual=_extQ.manualPending;setTimeout(()=>runAutoExtract(nextManual),500)}}}async function _doExtract(isManual){const _url=C.getCurUrl();const chatKey=getChatKey();const apiType=settings.config.autoExtApiType||"key";const missing=apiType==="vertex"?!settings.config.autoExtVertexJson:apiType==="firebase"?!settings.config.autoExtFirebaseScript:!settings.config.autoExtKey;if(missing){if(isManual)alert("API 설정 미완료.");return}const scanR=settings.config.autoExtScanRange||6;const extraTurns=_extQ.pendingTurns||0;const effectiveRange=scanR+extraTurns;const fetchCount=(effectiveRange+settings.config.autoExtOffset)*2;let recentMsgs=await C.fetchLogs(fetchCount>0?fetchCount:20);if(!recentMsgs.length){if(isManual)alert("대화 기록 없음.");return}const offsetCount=settings.config.autoExtOffset*2;if(offsetCount>0&&recentMsgs.length>offsetCount)recentMsgs=recentMsgs.slice(0,recentMsgs.length-offsetCount);const context=recentMsgs.map(m=>m.role+": "+m.message).join("\n");const _patchOn=settings.config.autoExtIncludeDb&&settings.config.autoExtPatchMode!==false;let entriesText="[]";if(settings.config.autoExtIncludeDb){const packName=await getAutoExtPackForUrl(_url);const existingEntries=await db.entries.where("packName").equals(packName).toArray();if(existingEntries.length>0){entriesText=buildExistingLoreContext(existingEntries,context,20)}}let personaPrefix="";if(settings.config.autoExtIncludePersona){const pName=await C.fetchPersonaName();if(pName)personaPrefix=`[User Persona: "${pName}"] All "user" role messages are from this character. Use "${pName}" as the character name, NOT "user".\n\n`}const tpl=settings.getActiveTemplate();const promptTpl=settings.config.autoExtIncludeDb?tpl.promptWithDb:tpl.promptWithoutDb;const extractSchema=UNIFIED_EXTRACT_SCHEMA||tpl.schema;const outputModeText=settings.config.autoExtIncludeDb?_patchOn?OUTPUT_MODE_PATCH:OUTPUT_MODE_FULL:"";const prompt=personaPrefix+promptTpl.replace("{context}",context).replace("{entries}",entriesText).replace("{schema}",extractSchema).replace("{outputMode}",outputModeText);const _extModel=settings.config.autoExtModel==="_custom"?settings.config.autoExtCustomModel:settings.config.autoExtModel;let apiLog=null,_extElapsedMs=0,_extCost=null;try{const apiOpts={apiType:apiType,key:settings.config.autoExtKey,vertexJson:settings.config.autoExtVertexJson,vertexLocation:settings.config.autoExtVertexLocation||"global",vertexProjectId:settings.config.autoExtVertexProjectId,firebaseScript:settings.config.autoExtFirebaseScript,firebaseEmbedKey:settings.config.autoExtFirebaseEmbedKey,model:_extModel,maxRetries:settings.config.autoExtMaxRetries||1,responseMimeType:"application/json",timeoutMs:12e4,maxOutputTokens:_patchOn?4096:null,costContext:{feature:"autoExtract",chatKey:chatKey||"global"}};const _extT0=Date.now();const{res:res,parsed:parsed}=await callGeminiJsonWithRepair(prompt,apiOpts,"Do not output a single object. Wrap patch/add items in an array.");_extElapsedMs=Date.now()-_extT0;_extCost=res&&res.cost||null;apiLog=res?{status:res.status,error:res.error,retries:res.retries}:null;if(!res||!res.text)throw new Error("AI 응답없음 ("+(res&&res.error||"알수없음")+")");if(!parsed)throw new Error("JSON 파싱 실패 (응답 스니포: "+(res.text||"").slice(0,100)+")");const parsedItems=normalizeExtractItems(parsed);let generalCount=0;let generalStatus="추출 내용 없음";let embedMsg="";let embedCount=0;if(parsedItems.length>0){generalCount=await mergeExtractedData(parsedItems,_url);generalStatus=generalCount>0?"성공":"변경 없음";if(generalCount>0&&settings.config.embeddingEnabled&&settings.config.autoEmbedOnExtract!==false){try{const epName=await getAutoExtPackForUrl(_url);extBadgeShow("에리가 임베딩 갱신 중");const embedOpts={...apiOpts,model:settings.config.embeddingModel||"gemini-embedding-001"};embedCount=await C.embedPack(epName,embedOpts);embedMsg=" (임베딩 "+embedCount+"개 완료)"}catch(embErr){console.warn("[Lore] 자동임베딩 실패:",embErr.message);embedMsg=" (자동 임베딩 실패)"}}addExtLog(chatKey,{time:(new Date).toLocaleTimeString(),count:generalCount,msgs:recentMsgs.length,isManual:isManual,status:generalStatus,api:apiLog,model:_extModel,elapsedMs:_extElapsedMs,cost:_extCost})}else{addExtLog(chatKey,{time:(new Date).toLocaleTimeString(),count:0,msgs:recentMsgs.length,isManual:isManual,status:"추출 내용 없음",api:apiLog,model:_extModel,elapsedMs:_extElapsedMs,cost:_extCost})}let temporalResult=null;if(settings.config.temporalExtractEnabled!==false){temporalResult=await runTemporalExtractPass({context:context,apiOpts:apiOpts,url:_url,chatKey:chatKey,isManual:isManual,msgCount:recentMsgs.length})}if(isManual){const temporalMsg=settings.config.temporalExtractEnabled!==false?" / 중요 장면 "+(temporalResult&&temporalResult.count||0)+"개":"";const baseMsg=generalCount>0?generalCount+"개 로어 추출 및 병합됨":"새로운 일반 로어 없음";alert(baseMsg+temporalMsg+embedMsg+".")}}catch(err){addExtLog(chatKey,{time:(new Date).toLocaleTimeString(),count:0,msgs:recentMsgs.length,isManual:isManual,status:"실패",error:err.message,api:apiLog,model:_extModel,elapsedMs:_extElapsedMs,cost:_extCost});if(isManual)alert("추출 실패: "+err.message)}}async function runBatchExtract(opts={}){const turnsPerBatch=opts.turnsPerBatch||50;const overlap=opts.overlap!==undefined?opts.overlap:5;const maxAttempts=opts.maxAttempts||3;const onProgress=typeof opts.onProgress==="function"?opts.onProgress:null;const _url=C.getCurUrl();const chatKey=getChatKey();const apiType=settings.config.autoExtApiType||"key";const missing=apiType==="vertex"?!settings.config.autoExtVertexJson:apiType==="firebase"?!settings.config.autoExtFirebaseScript:!settings.config.autoExtKey;if(missing)throw new Error("API 설정 미완료");extBadgeShow("에리가 전체 로그 가져오는 중");const allMsgs=await C.fetchLogs(99999);if(!allMsgs||!allMsgs.length){extBadgeHide();throw new Error("대화 기록 없음")}const totalMsgs=allMsgs.length;const batchMsgSize=Math.max(2,turnsPerBatch*2);const overlapMsgs=Math.max(0,overlap*2);const step=Math.max(1,batchMsgSize-overlapMsgs);const batches=[];for(let i=0;i=totalMsgs)break}const report={totalBatches:batches.length,totalMsgs:totalMsgs,ok:0,failed:0,empty:0,entriesAdded:0,batchResults:[]};let personaPrefix="";if(settings.config.autoExtIncludePersona){try{const pName=await C.fetchPersonaName();if(pName)personaPrefix='[User Persona: "'+pName+'"] All "user" role messages are from this character. Use "'+pName+'" as the character name, NOT "user".\n\n'}catch(e){}}const tpl=settings.getActiveTemplate();const promptTpl=settings.config.autoExtIncludeDb?tpl.promptWithDb:tpl.promptWithoutDb;const _batchModel=settings.config.autoExtModel==="_custom"?settings.config.autoExtCustomModel:settings.config.autoExtModel;let _batchTotalElapsedMs=0,_batchTotalUsd=0;let _batchHasUnknown=false,_batchHasEstimated=false,_batchCostKnown=false;for(let bi=0;bim.role+": "+m.message).join("\n");const _patchOn=settings.config.autoExtIncludeDb&&settings.config.autoExtPatchMode!==false;let entriesText="[]";if(settings.config.autoExtIncludeDb){const packName=await getAutoExtPackForUrl(_url);const existing=await db.entries.where("packName").equals(packName).toArray();if(existing.length>0){entriesText=buildExistingLoreContext(existing,context,20)}}const extractSchema=UNIFIED_EXTRACT_SCHEMA||tpl.schema;const outputModeText=settings.config.autoExtIncludeDb?_patchOn?OUTPUT_MODE_PATCH:OUTPUT_MODE_FULL:"";const prompt=personaPrefix+promptTpl.replace("{context}",context).replace("{entries}",entriesText).replace("{schema}",extractSchema).replace("{outputMode}",outputModeText);let ok=false;let status="failed";let lastErr="";let rawSnippet="";let attempts=0;let mergedCount=0;for(let attempt=0;attempt0){mergedCount=await mergeExtractedData(parsedItems,_url);report.entriesAdded+=mergedCount;status=mergedCount>0?"ok":"empty";ok=true}else{status="empty";ok=true}}catch(e){lastErr="예외: "+(e.message||String(e))}}if(ok){if(status==="empty")report.empty++;else report.ok++;report.batchResults.push({batch:bi+1,status:status,attempts:attempts,entries:mergedCount});if(settings.config.temporalExtractEnabled!==false){try{const tApiOpts={apiType:apiType,key:settings.config.autoExtKey,vertexJson:settings.config.autoExtVertexJson,vertexLocation:settings.config.autoExtVertexLocation||"global",vertexProjectId:settings.config.autoExtVertexProjectId,firebaseScript:settings.config.autoExtFirebaseScript,firebaseEmbedKey:settings.config.autoExtFirebaseEmbedKey,model:settings.config.autoExtModel==="_custom"?settings.config.autoExtCustomModel:settings.config.autoExtModel,maxRetries:1,responseMimeType:"application/json",timeoutMs:12e4,maxOutputTokens:_patchOn?4096:null,costContext:{feature:"batchExtract",chatKey:chatKey||"global"}};const tres=await runTemporalExtractPass({context:context,apiOpts:tApiOpts,url:_url,chatKey:chatKey,isManual:true,msgCount:msgs.length});if(tres&&tres.count){report.entriesAdded+=tres.count;report.batchResults.push({batch:bi+1,status:"temporal_ok",attempts:1,entries:tres.count})}else if(tres&&tres.error){report.batchResults.push({batch:bi+1,status:"temporal_failed",attempts:1,error:tres.error})}}catch(terr){console.warn("[Lore:batch] 시간축 추출 실패 (배치 "+(bi+1)+"):",terr.message||terr);report.batchResults.push({batch:bi+1,status:"temporal_failed",attempts:1,error:terr.message||String(terr)})}}}else{report.failed++;report.batchResults.push({batch:bi+1,status:"failed",attempts:attempts,error:lastErr,rawSnippet:rawSnippet});console.warn("[Lore:batch] 배치 "+(bi+1)+"/"+batches.length+" 실패 ("+attempts+"회): "+lastErr);addExtLog(chatKey,{time:(new Date).toLocaleTimeString(),count:0,msgs:msgs.length,isManual:true,status:"배치 "+(bi+1)+"/"+batches.length+" 실패",error:lastErr,model:_batchModel})}}addExtLog(chatKey,{time:(new Date).toLocaleTimeString(),count:report.entriesAdded,msgs:totalMsgs,isManual:true,status:"전체 추출 완료 (성공 "+report.ok+" / 빈 "+report.empty+" / 실패 "+report.failed+" / "+report.totalBatches+"개 배치, 병합 "+report.entriesAdded+"건)",model:_batchModel,elapsedMs:_batchTotalElapsedMs,cost:{usd:_batchCostKnown?_batchTotalUsd:null,estimated:_batchHasEstimated,hasUnknown:_batchHasUnknown,isBatchAggregate:true}});if(settings.config.embeddingEnabled&&settings.config.autoEmbedOnExtract!==false&&report.entriesAdded>0){try{const epName=await getAutoExtPackForUrl(_url);extBadgeShow("에리가 임베딩 갱신 중");const embedOpts={apiType:apiType,key:settings.config.autoExtKey,vertexJson:settings.config.autoExtVertexJson,vertexLocation:settings.config.autoExtVertexLocation||"global",vertexProjectId:settings.config.autoExtVertexProjectId,firebaseScript:settings.config.autoExtFirebaseScript,firebaseEmbedKey:settings.config.autoExtFirebaseEmbedKey,model:settings.config.embeddingModel||"gemini-embedding-001"};await C.embedPack(epName,embedOpts);report.embedded=true}catch(embErr){console.warn("[Lore:batch] 임베딩 실패:",embErr.message);report.embedError=embErr.message}}extBadgeHide();return report}Object.assign(_w.__LoreInj,{mergeExtractedData:mergeExtractedData,runAutoExtract:runAutoExtract,runBatchExtract:runBatchExtract,runTemporalExtractPass:runTemporalExtractPass,extBadgeShow:extBadgeShow,extBadgeHide:extBadgeHide,__extractLoaded:true});console.log("[LoreInj:4] extract loaded")})();(async function(){"use strict";if(document.readyState==="loading")await new Promise(r=>document.addEventListener("DOMContentLoaded",r));const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;try{const deadline=Date.now()+15e3;while(!(_w.__LoreInj&&_w.__LoreInj.__extractLoaded)&&Date.now()setTimeout(r,50));if(!(_w.__LoreInj&&_w.__LoreInj.__extractLoaded)){console.error("[LoreInj:5] extract 미로드");return}if(_w.__LoreInj.__injectLoaded)return;const{C:C,db:db,_ls:_ls,settings:settings,OOC_FORMATS:OOC_FORMATS,parseJsonLoose:parseJsonLoose,getChatKey:getChatKey,incrementTurnCounter:incrementTurnCounter,recordEntryMention:recordEntryMention,getCooldownMap:getCooldownMap,setCooldownLastTurn:setCooldownLastTurn,addInjLog:addInjLog,runAutoExtract:runAutoExtract}=_w.__LoreInj;const MAX_INPUT_CHARS=2e3;function summaryOfEntry(e){const s=e&&e.summary;if(s&&typeof s==="object"&&!Array.isArray(s))return String(s.compact||s.full||s.micro||"");return String(s||e?.inject?.compact||e?.name||"")}function temporalCandidateId(e){return String(e?.eventId||e?.id||e?.name||"")}function resolveActivePackState(url){const packsByUrl=settings.config.urlPacks||{};const disabledByUrl=settings.config.urlDisabledEntries||{};let key=url;let packs=packsByUrl[key]||[];if(!packs.length){try{const chatId=C.getCurrentChatId&&C.getCurrentChatId();if(chatId){const found=Object.keys(packsByUrl).find(k=>k&&k.includes(chatId)&&packsByUrl[k]&&packsByUrl[k].length);if(found){key=found;packs=packsByUrl[found]||[]}}}catch(_){}}return{key:key,packs:packs,disabled:disabledByUrl[key]||[]}}function buildTemporalJudgeCandidates(scored,limit){return(scored||[]).filter(s=>s&&s.entry&&C.isTimelineEvent&&C.isTimelineEvent(s.entry)).slice(0,Math.max(1,limit||6)).map(s=>{const e=s.entry;return{id:temporalCandidateId(e),name:e.name||e.title||"",anchor:e.when?.anchor||e.relativeTimeHint||e.timeline?.sceneLabel||"",participants:Array.isArray(e.participants)?e.participants.slice(0,6):[],location:e.location||"",actions:Array.isArray(e.actions)?e.actions.slice(0,6):[],hooks:Array.isArray(e.hooks)?e.hooks.slice(0,4):[],summary:summaryOfEntry(e).slice(0,220),score:Number((s.score||0).toFixed?s.score.toFixed(3):s.score||0),recall:s.temporalRecall?.score||s.components?.timelineRecall||0}})}async function runTemporalRecallJudge(userInput,recentMsgs,scored,config,apiOpts){if(!config.temporalRecallJudgeEnabled)return null;const candidates=buildTemporalJudgeCandidates(scored,config.temporalRecallJudgeCandidateLimit||6);if(!candidates.length)return null;const recentText=Array.isArray(recentMsgs)?recentMsgs.slice(-4).map(m=>(m.role||"")+": "+String(m.message||"").slice(0,180)).join("\n"):"";const schema=config.temporalRecallJudgeSchema||_w.__LoreInj.DEFAULT_TEMPORAL_RECALL_JUDGE_SCHEMA||"{}";const promptTpl=config.temporalRecallJudgePrompt||_w.__LoreInj.DEFAULT_TEMPORAL_RECALL_JUDGE_PROMPT||"";if(!promptTpl)return null;const prompt=promptTpl.replace("{schema}",schema).replace("{context}",(recentText+"\nuser: "+String(userInput||"").slice(0,500)).trim()).replace("{candidates}",JSON.stringify(candidates,null,2));try{const _judgeTimeoutMs=Math.max(0,Number(config.temporalRecallJudgeTimeoutMs||0));const _judgeModel=(config.temporalRecallJudgeModel==="_custom"?config.temporalRecallJudgeCustomModel:config.temporalRecallJudgeModel)||"gemini-3.1-flash-lite-preview";const _judgeAbortCtrl=typeof AbortController!=="undefined"?new AbortController:null;const _judgeCall=C.callGeminiApi(prompt,{...apiOpts,model:_judgeModel,responseMimeType:"application/json",maxRetries:1,timeoutMs:Math.max(8e3,_judgeTimeoutMs+2e3),maxOutputTokens:512,thinkingConfig:{thinkingLevel:config.temporalRecallJudgeReasoning||"minimal"},costContext:{feature:"judge",chatKey:getChatKey()||"global"},signal:_judgeAbortCtrl?_judgeAbortCtrl.signal:undefined});let _judgeTimer=null;const res=_judgeTimeoutMs>0?await Promise.race([_judgeCall,new Promise((_,rej)=>{_judgeTimer=setTimeout(()=>{try{if(_judgeAbortCtrl)_judgeAbortCtrl.abort()}catch(_){}rej(new Error("temporal_judge_timeout"))},_judgeTimeoutMs)})]).finally(()=>{if(_judgeTimer){clearTimeout(_judgeTimer);_judgeTimer=null}}):await _judgeCall;if(!res||!res.text)return{error:res?.error||"empty_response",fallback:true};const parsed=parseJsonLoose?parseJsonLoose(res.text):JSON.parse(res.text);if(!parsed||typeof parsed!=="object"||Array.isArray(parsed))return{error:"invalid_json",fallback:true};const allowedModes=["none","compact_timeline","specific_event","unresolved_hook"];const mode=allowedModes.includes(parsed.mode)?parsed.mode:parsed.recall?"compact_timeline":"none";let eventIds=Array.isArray(parsed.eventIds)?parsed.eventIds.map(String).filter(Boolean):[];const validIds=new Set(candidates.map(c=>String(c.id)));eventIds=eventIds.filter(id=>validIds.has(id)).slice(0,3);return{recall:!!parsed.recall&&mode!=="none",mode:mode,wantedDepth:parsed.wantedDepth||(mode==="specific_event"?"detail":mode==="none"?"none":"compact"),maxChars:Math.max(0,Math.min(600,Number(parsed.maxChars||0))),eventIds:eventIds,query:parsed.query&&typeof parsed.query==="object"?parsed.query:{},reason:String(parsed.reason||"").slice(0,160)}}catch(e){console.warn("[LoreInj:temporal-judge] 실패, deterministic fallback 사용:",e&&e.message);return{error:e.message||String(e),fallback:true}}}function applyTemporalJudge(scored,decision){if(!decision||decision.error||decision.fallback)return scored;const selected=new Set((decision.eventIds||[]).map(String));for(const s of scored||[]){if(!s||!s.entry||!(C.isTimelineEvent&&C.isTimelineEvent(s.entry)))continue;const id=temporalCandidateId(s.entry);s.temporalJudge=decision;s.entry._temporalJudge=decision;if(!decision.recall){if(s.components?.timelineRecall||s.temporalRecall)s.score*=.55;continue}if(selected.has(id)){const mult=decision.mode==="specific_event"?1.45:decision.mode==="unresolved_hook"?1.25:1.18;s.score*=mult}else if(selected.size>0&&(s.components?.timelineRecall||s.temporalRecall)){s.score*=.85}}scored.sort((a,b)=>b.score-a.score);return scored}function buildTemporalInjectionPlan(scored,decision,config,ctx={}){const empty={text:"",entries:[],entryIds:[],eventIds:[],usedChars:0,mode:"none",level:"none",source:"none"};if(!config||config.temporalInjectionEnabled===false||!(C.isTimelineEvent&&C.buildTemporalRecallBlock))return empty;const rows=(scored||[]).filter(s=>s&&s.entry&&C.isTimelineEvent(s.entry));if(!rows.length)return empty;const validDecision=decision&&!decision.error&&!decision.fallback&&decision.recall;const selected=new Set(validDecision&&Array.isArray(decision.eventIds)?decision.eventIds.map(String):[]);const hasTemporalSignal=s=>selected.has(temporalCandidateId(s.entry))||!!s.temporalRecall||Number(s.components?.timelineRecall||0)>0;const explicitMax=Math.max(1,config.temporalRecallExplicitMaxEvents||3);const naturalMax=Math.max(1,config.temporalRecallMaxEvents||2);let picked;if(selected.size)picked=rows.filter(s=>selected.has(temporalCandidateId(s.entry))).slice(0,explicitMax);else if(validDecision)picked=rows.filter(hasTemporalSignal).slice(0,explicitMax);else picked=rows.filter(s=>hasTemporalSignal(s)&&(s.temporalRecall||Number(s.components?.timelineRecall||0)>=.12)).slice(0,naturalMax);if(!picked.length)return empty;const mode=validDecision?decision.mode:"compact_timeline";const depth=validDecision?decision.wantedDepth:"compact";const budget=validDecision?Math.max(120,Math.min(650,Number(decision.maxChars||config.temporalRecallChars||450))):Math.max(80,Math.min(config.temporalRecallChars||450,config.temporalRecallNaturalChars||260));const block=C.buildTemporalRecallBlock(picked,{decision:validDecision?decision:{mode:mode,wantedDepth:depth,maxChars:budget},mode:mode,depth:depth,budget:budget,maxEvents:validDecision?explicitMax:naturalMax,currentTurn:ctx.currentTurn||0,compressionEnabled:config.temporalCompressionEnabled!==false,minCompressedChars:config.temporalCompressionMinChars||40,preserveFields:config.temporalCompressionPreserveFields||["participants","location","hooks"]});if(!block||!block.text)return empty;const entries=block.entries||picked.map(s=>s.entry);return{text:block.text,entries:entries,entryIds:entries.map(temporalCandidateId).filter(Boolean),eventIds:block.eventIds||entries.map(temporalCandidateId).filter(Boolean),usedChars:block.usedChars||C.charLen(block.text),mode:block.mode||mode,level:block.level||depth,source:validDecision?"judge":"deterministic",compressionActions:block.compressionActions||[],droppedEventIds:block.droppedEventIds||[]}}async function inject(userInput){if(!settings.config.enabled)return userInput;const _url=C.getCurUrl();const chatKey=getChatKey();const turnCounter=incrementTurnCounter(chatKey);if(settings.config.autoExtEnabled&&turnCounter>0&&turnCounter%settings.config.autoExtTurns===0)setTimeout(()=>runAutoExtract(false),100);const activePacksArr=typeof _w.__LoreInj.getActivePacksForUrl==="function"?_w.__LoreInj.getActivePacksForUrl(_url):resolveActivePackState(_url).packs||[];if(!activePacksArr.length){addInjLog(chatKey,{time:(new Date).toLocaleTimeString(),turn:turnCounter,matched:[],count:0,note:"활성 로어팩 없음",reason:"no_active_packs",url:_url});return userInput}const allForPacks=await db.entries.where("packName").anyOf(activePacksArr).toArray();const disabledSet=new Set(typeof _w.__LoreInj.getDisabledEntriesForUrl==="function"?_w.__LoreInj.getDisabledEntriesForUrl(_url):resolveActivePackState(_url).disabled||[]);let enabled=allForPacks.filter(e=>!disabledSet.has(e.id));if(!enabled.length){addInjLog(chatKey,{time:(new Date).toLocaleTimeString(),turn:turnCounter,matched:[],count:0,note:"사용 가능한 로어 없음",reason:"no_enabled_entries",activePacks:activePacksArr.slice(0,8)});return userInput}const fetchCount=Math.max(20,(settings.config.scanRange||6)*3);const recentMsgs=await C.fetchLogs(fetchCount);const config=settings.config;const apiOpts={apiType:config.autoExtApiType||"key",key:config.autoExtKey,vertexJson:config.autoExtVertexJson,vertexLocation:config.autoExtVertexLocation||"global",vertexProjectId:config.autoExtVertexProjectId,firebaseScript:config.autoExtFirebaseScript,firebaseEmbedKey:config.autoExtFirebaseEmbedKey,model:config.embeddingModel||"gemini-embedding-001",costContext:{feature:"embed",chatKey:chatKey||"global"}};const searchConfig={chatKey:chatKey,turnCounter:turnCounter,scanRange:config.scanRange||6,scanOffset:config.scanOffset||0,strictMatch:config.strictMatch!==false,similarityMatch:config.similarityMatch===true,embeddingEnabled:config.embeddingEnabled||false,embeddingWeight:config.embeddingWeight||.4,decayEnabled:config.decayEnabled!==false,decayHalfLife:config.decayHalfLife||C.DEFAULTS.decayHalfLife,temporalGraphEnabled:config.temporalGraphEnabled!==false,temporalWeight:config.temporalWeight!=null?config.temporalWeight:C.DEFAULTS.temporalWeight,activeEntityWeight:config.activeEntityWeight!=null?config.activeEntityWeight:C.DEFAULTS.activeEntityWeight,relationshipGraphWeight:config.relationshipGraphWeight!=null?config.relationshipGraphWeight:C.DEFAULTS.relationshipGraphWeight,unresolvedWeight:config.unresolvedWeight!=null?config.unresolvedWeight:C.DEFAULTS.unresolvedWeight,maintenanceWeight:config.maintenanceWeight!=null?config.maintenanceWeight:C.DEFAULTS.maintenanceWeight,periodicRecallEnabled:config.periodicRecallEnabled!==false,timelineRetrievalEnabled:config.timelineRetrievalEnabled!==false,timelineRecallWeight:config.timelineRecallWeight!=null?config.timelineRecallWeight:C.DEFAULTS.timelineRecallWeight||.32,timelineNoCuePenalty:config.timelineNoCuePenalty!=null?config.timelineNoCuePenalty:C.DEFAULTS.timelineNoCuePenalty||.35,timelineRecallPoolLimit:config.timelineRecallPoolLimit||12,aiMemoryTurns:config.aiMemoryTurns||4,activeCharDetection:config.activeCharDetection!==false,activeCharBoost:config.activeCharBoostEnabled!==false?C.DEFAULTS.activeCharBoost:1,inactiveCharPenalty:config.activeCharBoostEnabled!==false?C.DEFAULTS.inactiveCharPenalty:1};let scored=[],activeNames=[],temporalJudgeDecision=null;try{const r=await C.hybridSearch(userInput,recentMsgs,enabled,searchConfig,apiOpts);scored=r.scored||[];activeNames=r.activeNames||[];if(C.resolveTemporalRecall&&config.timelineRetrievalEnabled!==false){const resolved=C.resolveTemporalRecall(userInput,recentMsgs,enabled,{currentTurn:turnCounter,activeNames:activeNames,limit:4});if(resolved&&resolved.candidates&&resolved.candidates.length&&(config.periodicRecallEnabled!==false||resolved.hasCue)){const byId=new Map(scored.map(s=>[s.entry.id,s]));for(const c of resolved.candidates){const old=byId.get(c.entry.id);if(old){if(old.components&&old.components.timelineRecall>0){old.temporalRecall=old.temporalRecall||c;if(!old.matchedTrigger&&c.matchedTriggers&&c.matchedTriggers.length)old.matchedTrigger=c.matchedTriggers.join(",");continue}old.score+=c.score*(resolved.hasCue?.75:.35);old.temporalRecall=c}else{const row={entry:c.entry,score:c.score*(resolved.hasCue?.9:.45),tScore:0,eSim:0,matchedTrigger:(c.matchedTriggers||[]).join(","),temporalRecall:c};scored.push(row);byId.set(c.entry.id,row)}}scored.sort((a,b)=>b.score-a.score)}}temporalJudgeDecision=await runTemporalRecallJudge(userInput,recentMsgs,scored,config,apiOpts);if(temporalJudgeDecision&&!temporalJudgeDecision.fallback&&!temporalJudgeDecision.error){scored=applyTemporalJudge(scored,temporalJudgeDecision)}if(r.searchStats&&config.embeddingEnabled){const sk="lore-hybrid-stats";const st=JSON.parse(_ls.getItem(sk)||'{"to":0,"eo":0,"b":0,"n":0,"injLog":[],"lastInjected":[]}');st.to+=r.searchStats.trigOnly;st.eo+=r.searchStats.embOnly;st.b+=r.searchStats.both;st.n++;st.tRecall=(st.tRecall||0)+(r.searchStats.temporalRecallCount||0);st.tCue=(st.tCue||0)+(r.searchStats.temporalRecallHasCue?1:0);if(st.lastInjected&&st.lastInjected.length&&recentMsgs.length>=2){const lastAIMsg=[...recentMsgs].reverse().find(m=>m.role==="assistant");const lastAI=(lastAIMsg?.message||"").toLowerCase();const reflected=st.lastInjected.filter(n=>lastAI.includes(n.toLowerCase())).length;st.injLog.push(reflected/Math.max(st.lastInjected.length,1));if(st.injLog.length>30)st.injLog.shift()}if(config.autoTuneEmbeddingWeight===true&&st.n>0&&st.n%20===0){const total=st.to+st.eo+st.b;if(total>5){const embContrib=(st.eo+st.b*.5)/total;const avgReflection=st.injLog.length>0?st.injLog.reduce((a,b)=>a+b,0)/st.injLog.length:.5;const feedbackAdj=avgReflection>.4?0:(.4-avgReflection)*.3;const target=Math.max(.15,Math.min(.65,embContrib+feedbackAdj));const smoothed=.7*(config.embeddingWeight||.4)+.3*target;settings.config.embeddingWeight=parseFloat(smoothed.toFixed(3));settings.save()}}_ls.setItem(sk,JSON.stringify(st))}}catch(e){const tr=C.triggerScan(userInput,recentMsgs,enabled,searchConfig);scored=tr.map(r=>({entry:r.entry,score:r.triggerScore}));activeNames=C.detectActiveCharacters(recentMsgs,enabled)}if(config.pendingPromiseBoost!==false){for(const s of scored){if(s.entry.type==="promise"&&s.entry.detail?.status==="pending")s.score=Math.max(s.score,.3)}scored.sort((a,b)=>b.score-a.score)}for(const s of scored){if(s.entry.rootId&&!s.entry.isCurrentArc)s.score*=.3}scored.sort((a,b)=>b.score-a.score);if(activeNames.length>=2&&config.firstEncounterWarning!==false){for(let i=0;im.role+": "+m.message).join("\n");scored=await C.smartRerank(userInput,scored,last2,{apiType:config.autoExtApiType||"key",key:config.autoExtKey,vertexJson:config.autoExtVertexJson,vertexLocation:config.autoExtVertexLocation||"global",vertexProjectId:config.autoExtVertexProjectId,firebaseScript:config.autoExtFirebaseScript,model:config.rerankModel||config.autoExtModel||"gemini-3-flash-preview",costContext:{feature:"rerank",chatKey:chatKey||"global"}},config)}catch(e){}C.showStatusBadge("에리가 응답 기다리는 중")}let cooldownFilteredAll=false;const scoredCountBeforeCooldown=scored.length;if(config.cooldownEnabled){const cMap=getCooldownMap(chatKey);let staleCooldownCount=0;scored=scored.filter(s=>{const last=cMap[s.entry.id];if(last!==undefined&&turnCounter!=null&&Number(last)>turnCounter){delete cMap[s.entry.id];staleCooldownCount++;return true}return last===undefined||turnCounter-last>=config.cooldownTurns});if(staleCooldownCount>0)settings.save();cooldownFilteredAll=scoredCountBeforeCooldown>0&&!scored.length;if(cooldownFilteredAll){addInjLog(chatKey,{time:(new Date).toLocaleTimeString(),turn:turnCounter,matched:[],count:0,note:"삽입 쿨타임 대기",reason:"cooldown_filtered_all",cooldownTurns:config.cooldownTurns,activePacks:activePacksArr.slice(0,8)})}}const _deltaKey="lore-recent-injections:"+chatKey;let _recentInj={};try{_recentInj=JSON.parse(_ls.getItem(_deltaKey)||"{}")}catch(e){}const _deltaTurns=config.deltaSkipTurns!=null?config.deltaSkipTurns:3;let _deltaSkippedCount=0;const _filteredScored=config.deltaSkipEnabled!==true?scored:scored.filter(s=>{const rec=_recentInj[s.entry.id];if(!rec)return true;if(turnCounter-(rec.turn||0)>=_deltaTurns)return true;const sig=String(s.entry.lastUpdated||s.entry.ts||"");if(sig!==rec.sig)return true;_deltaSkippedCount++;return false});const temporalPlan=buildTemporalInjectionPlan(_filteredScored,temporalJudgeDecision,config,{currentTurn:turnCounter});const temporalIds=new Set(temporalPlan.entryIds||[]);const _loreScored=_filteredScored.filter(s=>{if(!s||!s.entry)return false;if(temporalIds.has(temporalCandidateId(s.entry)))return false;if(C.isTimelineEvent&&C.isTimelineEvent(s.entry))return false;return true});const topScored=_loreScored.slice(0,config.maxEntries||4);const topEntries=topScored.map(s=>{if(s.components)s.entry._nway=s.components;return s.entry});if(!topEntries.length&&!temporalPlan.text){if(!cooldownFilteredAll){addInjLog(chatKey,{time:(new Date).toLocaleTimeString(),turn:turnCounter,matched:[],count:0,note:"삽입 후보 없음",reason:"no_injection_candidates",activePacks:activePacksArr.slice(0,8)})}return userInput}const pfx=config.prefix||OOC_FORMATS.default.prefix;const sfx=config.suffix||OOC_FORMATS.default.suffix;if(C.charLen(userInput)>=MAX_INPUT_CHARS-20){addInjLog(chatKey,{time:(new Date).toLocaleTimeString(),turn:turnCounter,matched:[],count:0,note:"공간부족",budgetPlan:{userChars:C.charLen(userInput),maxInputChars:MAX_INPUT_CHARS},finalChars:C.charLen(userInput),reason:"user_input_too_long"});return userInput}let honorifics="";if(config.honorificMatrixEnabled!==false)honorifics=C.formatHonorificMatrix(C.buildHonorificMatrix(enabled,activeNames),80);let unmetPairs=[];if(config.firstEncounterWarning!==false)try{unmetPairs=await C.findUnmetPairs(activeNames)}catch(e){}if(unmetPairs.length>0){const knownPairs=new Set;for(const r of enabled){if(r.type!=="rel"&&r.type!=="relationship")continue;let parties=r.parties||r.detail?.parties;if((!parties||parties.length<2)&&typeof r.name==="string"){if(r.name.includes("↔"))parties=r.name.split("↔").map(s=>s.trim()).filter(Boolean);else if(r.name.includes("&"))parties=r.name.split("&").map(s=>s.trim()).filter(Boolean)}if(parties&&parties.length>=2)knownPairs.add([parties[0],parties[1]].sort().join("|"))}if(knownPairs.size>0){unmetPairs=unmetPairs.filter(pair=>!knownPairs.has([pair[0],pair[1]].sort().join("|")))}}let firstEncounterBlock="";if(unmetPairs.length>0&&config.firstEncounterWarning!==false){try{const feKey="lore-fe-recent-"+chatKey;const log=JSON.parse(_ls.getItem(feKey)||"[]");const fresh=unmetPairs.filter(p=>{const k=[...p].sort().join("|");const last=log.find(x=>x.key===k);return!last||turnCounter-last.turn>=5});if(fresh.length>0){const pick=fresh[0];firstEncounterBlock=C.formatFirstEncounterBlock(pick);log.push({key:[...pick].sort().join("|"),turn:turnCounter});_ls.setItem(feKey,JSON.stringify(log.slice(-20)));unmetPairs=unmetPairs.filter(p=>p!==pick)}}catch(e){}}let reunionTags="";if(config.firstEncounterWarning!==false){try{const reunions=await C.findReunionPairs(activeNames,turnCounter,10);if(reunions.length>0){reunionTags=reunions.slice(0,2).map(r=>C.formatReunionTag(r.pair,r.gap)).join("\n")}}catch(e){}}let sceneTag="";if(recentMsgs.length>0&&config.firstEncounterWarning!==false){try{const kw=C.extractSceneKeywords(recentMsgs);sceneTag=C.formatSceneTag(kw)}catch(e){}}try{await C.updateWorkingMemory(_url,{turn:turnCounter,activeChars:activeNames.slice(0,5),scene:sceneTag,lastAction:(recentMsgs[recentMsgs.length-1]?.message||"").slice(0,80),updatedAt:Date.now()})}catch(e){}const temporalHints=config.firstEncounterWarning!==false&&C.formatTemporalHints?C.formatTemporalHints(topEntries,{currentTurn:turnCounter,activeNames:activeNames,budget:Math.min(config.temporalHintChars||C.DEFAULTS.temporalHintChars||120,120)}):"";const fmtResult=C.planInjectionBudget?C.planInjectionBudget({userInput:userInput,maxInputChars:MAX_INPUT_CHARS,entries:topEntries,activeNames:activeNames,unmetPairs:unmetPairs,sceneTag:sceneTag,firstEncounterBlock:firstEncounterBlock,reunionTags:reunionTags,honorifics:honorifics,temporalHints:temporalHints,temporalRecallBlock:temporalPlan.text,config:config,prefix:pfx,suffix:sfx}):{injected:"",included:[],usedChars:0,level:"none",reason:"planner_missing",finalChars:C.charLen(userInput),budgetPlan:{}};if(!fmtResult.injected){addInjLog(chatKey,{time:(new Date).toLocaleTimeString(),turn:turnCounter,matched:[],count:0,note:"주입 취소",budgetPlan:fmtResult.budgetPlan||{},finalChars:fmtResult.finalChars||C.charLen(userInput),reason:fmtResult.reason||"empty"});return userInput}const injected=fmtResult.injected;const allIncluded=(()=>{const m=new Map;for(const e of[...fmtResult.included||[],...temporalPlan.entries||[]]){if(e&&e.id!=null)m.set(e.id,e)}return Array.from(m.values())})();try{for(const e of allIncluded){recordEntryMention(chatKey,e.id);setCooldownLastTurn(chatKey,e.id,turnCounter);try{await db.entries.update(e.id,{lastMentionedTurn:turnCounter})}catch(_){}}}catch(e){}try{const sk="lore-hybrid-stats";const st=JSON.parse(_ls.getItem(sk)||"{}");st.lastInjected=allIncluded.map(e=>e.name);_ls.setItem(sk,JSON.stringify(st))}catch(e){}const _injectedLen=C.charLen(injected);const _userLen=C.charLen(userInput);const _finalChars=fmtResult.finalChars||_userLen+_injectedLen+2;try{for(const e of allIncluded){_recentInj[e.id]={turn:turnCounter,sig:String(e.lastUpdated||e.ts||"")}}for(const k of Object.keys(_recentInj)){if(turnCounter-(_recentInj[k].turn||0)>20)delete _recentInj[k]}_ls.setItem(_deltaKey,JSON.stringify(_recentInj))}catch(e){}addInjLog(chatKey,{time:(new Date).toLocaleTimeString(),turn:turnCounter,matched:allIncluded.map(e=>e.name),count:allIncluded.length,budget:fmtResult.budgetPlan?.loreBudget||0,used:fmtResult.usedChars,level:fmtResult.level,activeChars:activeNames.slice(0,5),userInputChars:_userLen,injectedChars:_injectedLen,totalChars:_finalChars,maxChars:MAX_INPUT_CHARS,budgetPlan:fmtResult.budgetPlan||{},downgraded:fmtResult.downgraded||[],dropped:fmtResult.dropped||[],finalChars:_finalChars,reason:fmtResult.reason||"ok",temporalJudge:temporalJudgeDecision,temporalInjection:{source:temporalPlan.source,mode:temporalPlan.mode,level:temporalPlan.level,eventIds:temporalPlan.eventIds||[],chars:temporalPlan.usedChars||0,compressionActions:temporalPlan.compressionActions||[],droppedEventIds:temporalPlan.droppedEventIds||[]},deltaSkipped:_deltaSkippedCount,bundled:fmtResult.bundledCount||0,sections:{scene:fmtResult.sections?.scene||C.charLen(sceneTag||""),firstEnc:fmtResult.sections?.firstEncounter||C.charLen(firstEncounterBlock||""),reunion:fmtResult.sections?.reunion||C.charLen(reunionTags||""),honor:fmtResult.sections?.honorifics||C.charLen(honorifics||""),temporalRecall:fmtResult.sections?.temporalRecall||C.charLen(temporalPlan.text||""),lore:C.charLen(fmtResult.text||"")}});return config.position==="before"?injected+"\n\n"+userInput:userInput+"\n\n"+injected}if(_w.__loreRegister)_w.__loreRegister(inject);Object.assign(_w.__LoreInj,{inject:inject,__injectLoaded:true});console.log("[LoreInj:5] inject loaded & registered")}catch(fatal){console.error("[LoreInj:5] FATAL — inject 등록 실패:",fatal,fatal?.stack);_w.__LoreInj=_w.__LoreInj||{};if(!_w.__LoreInj.inject)_w.__LoreInj.inject=async u=>u;_w.__LoreInj.__injectLoaded=true}})();(async function(){"use strict";if(document.readyState==="loading")await new Promise(r=>document.addEventListener("DOMContentLoaded",r));const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const deadline=Date.now()+15e3;while(!(_w.__LoreInj&&_w.__LoreInj.__injectLoaded)&&Date.now()setTimeout(r,50));if(!(_w.__LoreInj&&_w.__LoreInj.__injectLoaded)){console.error("[LoreInj:6] inject 미로드");return}if(_w.__LoreInj.__inject6Loaded)return;if(!_w.__LoreInj.__menuQueue)_w.__LoreInj.__menuQueue=[];if(!_w.__LoreInj.__subMenuQueue)_w.__LoreInj.__subMenuQueue=[];const MENU_ORDER={main:10,lore:20,file:30,extract:40,merge:50,snapshot:60,refiner:70,log:80,session:90,api:100,help:110};function stableMenuQueue(queue,prefix){return(queue||[]).map((item,index)=>({...item,index:index})).sort((a,b)=>{const ak=prefix+":"+a.key;const bk=prefix+":"+b.key;const ao=MENU_ORDER[a.key]??1e4;const bo=MENU_ORDER[b.key]??1e4;if(ao!==bo)return ao-bo;if(ak!==bk)return akmodal.createMenu(menuName,menuAction),createMenu:(menuName,menuAction)=>modal.createMenu(menuName,menuAction)};const menuQ=stableMenuQueue(_w.__LoreInj.__menuQueue||[],"m");const subQ=stableMenuQueue(_w.__LoreInj.__subMenuQueue||[],"s");const registered=_w.__LoreInj.__registeredMenuKeys=_w.__LoreInj.__registeredMenuKeys||new Set;_w.__LoreInj.__menuOrder={menu:menuQ.map(x=>x.key),subMenu:subQ.map(x=>x.key)};console.log(`[LoreInj:6] setupSubMenus: menu=${menuQ.length}, subMenu=${subQ.length}`,_w.__LoreInj.__menuOrder);menuQ.forEach(({key:key,cb:cb})=>{const regKey="m:"+key;if(registered.has(regKey))return;try{cb(modal);registered.add(regKey)}catch(e){console.error(`[LoreInj:6] 메뉴 등록 실패 (${key}):`,e);_w.__LoreInj?.markFailed?.("menu:"+key,e)}});subQ.forEach(({key:key,cb:cb})=>{const regKey="s:"+key;if(registered.has(regKey))return;try{cb(flatMenuAdapter);registered.add(regKey)}catch(e){console.error(`[LoreInj:6] 서브메뉴 등록 실패 (${key}):`,e);_w.__LoreInj?.markFailed?.("submenu:"+key,e)}})}function scheduleMenuRemount(){const L=_w.__LoreInj;if(!L.__menuModal||L.__menuRemountTimer)return;L.__menuRemountTimer=setTimeout(()=>{L.__menuRemountTimer=0;mountQueuedMenus(L.__menuModal)},100)}function installMenuRegistrars(){const L=_w.__LoreInj;L.registerMenu=function(key,cb){L.__menuQueue.push({key:key,cb:cb});scheduleMenuRemount()};L.registerSubMenu=function(key,cb){L.__subMenuQueue.push({key:key,cb:cb});scheduleMenuRemount()}}installMenuRegistrars();_w.__LoreInj.setupSubMenus=function(modal){if(!modal||typeof modal.createMenu!=="function"){console.error("[LoreInj:6] modal.createMenu 없음");return}_w.__LoreInj.__menuModal=modal;mountQueuedMenus(modal)};_w.__LoreInj.__inject6Loaded=true;console.log("[LoreInj:6] Entry point loaded. Awaiting sub-modules...")})();(async function(){"use strict";if(document.readyState==="loading")await new Promise(r=>document.addEventListener("DOMContentLoaded",r));const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const deadline=Date.now()+15e3;while(!(_w.__LoreInj&&_w.__LoreInj.__settingsLoaded)&&Date.now()setTimeout(r,50));if(!(_w.__LoreInj&&_w.__LoreInj.__settingsLoaded)){console.error("[LoreInj:sub-main] settings 미로드");return}if(_w.__LoreInj.__subMainLoaded)return;const{C:C,db:db,_ls:_ls,settings:settings,OOC_FORMATS:OOC_FORMATS}=_w.__LoreInj;_w.__LoreInj.registerMenu=_w.__LoreInj.registerMenu||function(){};_w.__LoreInj.registerMenu("main",function(modal){modal.createMenu("로어 설정",m=>{m.replaceContentPanel(async panel=>{const PRESETS={beginner:{name:"기본 추천",desc:"의미 검색 + 8턴 자동 정리. 일반 RP용.",config:{embeddingEnabled:true,embeddingWeight:.35,autoExtEnabled:true,autoExtTurns:8,autoExtIncludeDb:true,autoExtIncludePersona:true,autoEmbedOnExtract:true,scanOffset:3,maxEntries:4,cooldownTurns:8,loreBudgetChars:300,loreBudgetMax:500,decayEnabled:true,activeCharDetection:true,activeCharBoostEnabled:true,honorificMatrixEnabled:true,firstEncounterWarning:true,importanceGating:true,importanceThreshold:12,aiMemoryTurns:4,pendingPromiseBoost:true,rerankEnabled:false,useCompressedFormat:true,compressionMode:"auto",strictMatch:true,similarityMatch:true}},minimal:{name:"수동 검색",desc:"자동 추출 OFF. 수동 추출만 사용. API 호출 최소.",config:{embeddingEnabled:true,embeddingWeight:.35,autoExtEnabled:false,autoEmbedOnExtract:true,scanOffset:2,maxEntries:3,cooldownTurns:6,loreBudgetChars:250,loreBudgetMax:400,decayEnabled:true,activeCharDetection:true,activeCharBoostEnabled:true,honorificMatrixEnabled:true,firstEncounterWarning:false,importanceGating:true,importanceThreshold:12,rerankEnabled:false,useCompressedFormat:true,compressionMode:"auto",strictMatch:true,similarityMatch:true}},advanced:{name:"정밀",desc:"5턴 자동 정리 + 후보 재정렬 + 응답 교정. 장문 RP용.",config:{embeddingEnabled:true,embeddingWeight:.4,autoExtEnabled:true,autoExtTurns:5,autoExtIncludeDb:true,autoExtIncludePersona:true,autoEmbedOnExtract:true,scanOffset:3,maxEntries:5,cooldownTurns:8,loreBudgetChars:400,loreBudgetMax:700,decayEnabled:true,activeCharDetection:true,activeCharBoostEnabled:true,honorificMatrixEnabled:true,firstEncounterWarning:true,importanceGating:true,importanceThreshold:10,aiMemoryTurns:4,pendingPromiseBoost:true,rerankEnabled:true,useCompressedFormat:true,compressionMode:"auto",strictMatch:true,similarityMatch:true,refinerEnabled:true,refinerLoreMode:"semantic"}}};panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const title=document.createElement("div");title.textContent="현재 상태";title.style.cssText="font-size:14px;color:#4a9;font-weight:bold;margin-bottom:8px;";nd.appendChild(title);const grid=document.createElement("div");grid.style.cssText="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;";nd.appendChild(grid);const chip=(label,value,ok)=>{const c=document.createElement("div");c.style.cssText="border:1px solid #333;border-radius:6px;padding:8px 10px;background:#111;min-width:0;";const l=document.createElement("div");l.textContent=label;l.style.cssText="font-size:10px;color:#888;margin-bottom:4px;";const v=document.createElement("div");v.textContent=value;v.style.cssText="font-size:12px;font-weight:bold;color:"+(ok?"#4a9":"#da8")+";white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";c.appendChild(l);c.appendChild(v);grid.appendChild(c);return v};const cfg=settings.config||{};const apiType=cfg.autoExtApiType||"key";const apiReady=apiType==="vertex"?!!cfg.autoExtVertexJson:apiType==="firebase"?!!cfg.autoExtFirebaseScript:!!cfg.autoExtKey;const activePacks=_w.__LoreInj.getActivePacksForUrl?_w.__LoreInj.getActivePacksForUrl(C.getCurUrl()):cfg.urlPacks&&cfg.urlPacks[C.getCurUrl()]||[];chip("API",apiReady?"설정됨":"미설정",apiReady);const packValue=chip("활성 로어팩",activePacks.length?activePacks.length+"개":"없음",activePacks.length>0);const entryValue=chip("사용 가능한 로어","확인 중",true);chip("자동 대화 정리",cfg.autoExtEnabled?(cfg.autoExtTurns||8)+"턴마다":"꺼짐",!!cfg.autoExtEnabled);chip("의미 검색",cfg.embeddingEnabled?cfg.autoEmbedOnExtract!==false?"켜짐":"수동 준비":"꺼짐",!!cfg.embeddingEnabled);db.entries.toArray().then(entries=>{const active=new Set(activePacks);const usable=active.size?entries.filter(e=>active.has(e.packName)).length:0;entryValue.textContent=usable+"개";entryValue.style.color=usable?"#4a9":"#da8";if(activePacks.length)packValue.textContent=activePacks.join(", ")}).catch(()=>{entryValue.textContent="확인 실패";entryValue.style.color="#d66"})}});panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const t=document.createElement("div");t.textContent="빠른 설정";t.style.cssText="font-size:14px;color:#4a9;font-weight:bold;margin-bottom:8px;";nd.appendChild(t);const row=document.createElement("div");row.style.cssText="display:flex;gap:8px;flex-wrap:wrap;";for(const[key,preset]of Object.entries(PRESETS)){const btn=document.createElement("button");btn.style.cssText="padding:10px 14px;font-size:12px;border-radius:6px;cursor:pointer;border:1px solid #333;background:#1a1a1a;color:#ccc;display:flex;flex-direction:column;gap:4px;text-align:left;flex:1;min-width:110px;";const nm=document.createElement("div");nm.textContent=preset.name;nm.style.cssText="font-weight:bold;color:#4a9;font-size:13px;";const ds=document.createElement("div");ds.textContent=preset.desc;ds.style.cssText="font-size:10px;color:#888;";btn.appendChild(nm);btn.appendChild(ds);btn.onclick=()=>{if(!confirm("["+preset.name+"] 프리셋 적용?"))return;settings.config=JSON.parse(JSON.stringify(_w.__LoreInj.defaultSettings));Object.assign(settings.config,preset.config);settings.save();m.replaceContentPanel(p=>p.addText("새로고침 필요함."),"설정 갱신 필요")};row.appendChild(btn)}nd.appendChild(row)}});panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);nd.appendChild(C.createToggleRow("로어 인젝션 활성화","대화에 설정 정보를 자동 삽입함.",settings.config.enabled,v=>{settings.config.enabled=v;settings.save()}));nd.appendChild(C.createToggleRow("적응형 로어 압축","주입 공간 부족 시 텍스트를 자동으로 짧게 줄임.",settings.config.useCompressedFormat!==false,v=>{settings.config.useCompressedFormat=v;settings.save()}));const cmpWrap=document.createElement("div");cmpWrap.style.cssText="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;padding-left:10px;";const cmpLbl=document.createElement("div");cmpLbl.textContent="압축 모드";cmpLbl.style.cssText="font-size:12px;color:#aaa;";const cmpSel=document.createElement("select");cmpSel.style.cssText="width:120px;padding:4px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:11px;";[{v:"auto",l:"자동 (공간 맞춤)"},{v:"full",l:"길게"},{v:"compact",l:"짧게"},{v:"micro",l:"아주 짧게"}].forEach(o=>{const opt=document.createElement("option");opt.value=o.v;opt.textContent=o.l;cmpSel.appendChild(opt)});cmpSel.value=settings.config.compressionMode||"auto";cmpSel.onchange=()=>{settings.config.compressionMode=cmpSel.value;settings.save()};cmpWrap.appendChild(cmpLbl);cmpWrap.appendChild(cmpSel);nd.appendChild(cmpWrap)}});panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const t=document.createElement("div");t.textContent="검색 & 감지";t.style.cssText="font-size:14px;color:#4a9;font-weight:bold;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid #333;";nd.appendChild(t);nd.appendChild(C.createToggleRow("의미로 찾기","단어가 달라도 관련 로어 찾음. API/검색 준비 필요.",settings.config.embeddingEnabled,v=>{settings.config.embeddingEnabled=v;settings.save()}));const emRow=document.createElement("div");emRow.style.cssText="display:flex;justify-content:space-between;align-items:center;gap:10px;width:100%;margin-bottom:8px;";const emL=document.createElement("div");emL.style.cssText="display:flex;flex-direction:column;gap:4px;flex:1;";const eml1=document.createElement("div");eml1.textContent="의미 검색 모델";eml1.style.cssText="font-size:13px;color:#ccc;font-weight:bold;";emL.appendChild(eml1);const emSel=document.createElement("select");emSel.style.cssText="width:200px;padding:6px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:12px;";[{v:"gemini-embedding-001",l:"gemini-embedding-001"},{v:"gemini-embedding-2-preview",l:"gemini-embedding-2-preview"}].forEach(o=>{const opt=document.createElement("option");opt.value=o.v;opt.textContent=o.l;emSel.appendChild(opt)});emSel.value=settings.config.embeddingModel||"gemini-embedding-001";emSel.onchange=()=>{settings.config.embeddingModel=emSel.value;settings.save();alert("모델 변경됨. 기존 로어 검색 준비 재실행 필요.")};emRow.appendChild(emL);emRow.appendChild(emSel);nd.appendChild(emRow);nd.appendChild(C.createToggleRow("추출 후 검색 준비","새 로어를 의미 검색용으로 자동 준비함.",settings.config.autoEmbedOnExtract!==false,v=>{settings.config.autoEmbedOnExtract=v;settings.save()}));nd.appendChild(C.createToggleRow("오래된 정보도 가끔 넣기","직접 관련이 약해도 중요한 과거 정보를 주기적으로 넣음.",settings.config.periodicRecallEnabled!==false,v=>{settings.config.periodicRecallEnabled=v;settings.config.decayEnabled=v;settings.save()}));nd.appendChild(C.createToggleRow("AI로 후보 다시 고르기","검색 후보를 AI가 현재 장면 기준으로 다시 정렬함.",settings.config.rerankEnabled||false,v=>{settings.config.rerankEnabled=v;settings.save()}))}});panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const wrap=document.createElement("div");wrap.style.cssText="display:flex;justify-content:space-between;align-items:center;width:100%;";const left=document.createElement("div");left.style.cssText="display:flex;flex-direction:column;gap:4px;flex:1;";const t=document.createElement("div");t.textContent="주입 위치";t.style.cssText="font-size:13px;color:#ccc;font-weight:bold;";const d=document.createElement("div");d.textContent="메시지 기준 로어 삽입 위치.";d.style.cssText="font-size:11px;color:#888;";left.appendChild(t);left.appendChild(d);const right=document.createElement("div");right.style.cssText="display:flex;gap:6px;";const b1=document.createElement("button"),b2=document.createElement("button");const updateBtns=()=>{const isB=settings.config.position==="before";b1.style.cssText=`padding:6px 12px;font-size:12px;border-radius:4px;cursor:pointer;border:1px solid ${isB?"#285":"#444"};background:${isB?"#285":"transparent"};color:${isB?"#fff":"#ccc"};`;b2.style.cssText=`padding:6px 12px;font-size:12px;border-radius:4px;cursor:pointer;border:1px solid ${!isB?"#285":"#444"};background:${!isB?"#285":"transparent"};color:${!isB?"#fff":"#ccc"};`};b1.textContent="메시지 앞";b1.onclick=()=>{settings.config.position="before";settings.save();updateBtns()};b2.textContent="메시지 뒤";b2.onclick=()=>{settings.config.position="after";settings.save();updateBtns()};updateBtns();right.appendChild(b1);right.appendChild(b2);wrap.appendChild(left);wrap.appendChild(right);nd.appendChild(wrap)}});panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const tr=document.createElement("div");tr.textContent="추가 정보 주입";tr.style.cssText="font-size:14px;color:#4a9;font-weight:bold;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid #333;";nd.appendChild(tr);nd.appendChild(C.createToggleRow("호칭 정보","캐릭터 간 호칭 정보 함께 전달함.",settings.config.honorificMatrixEnabled!==false,v=>{settings.config.honorificMatrixEnabled=v;settings.save()}));nd.appendChild(C.createToggleRow("첫 만남/재회 관리","첫 만남/재회 여부 자동 전달함.",settings.config.firstEncounterWarning!==false,v=>{settings.config.firstEncounterWarning=v;settings.save()}))}});panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const t1=document.createElement("div");t1.textContent="출력 포맷";t1.style.cssText="font-size:14px;color:#4a9;font-weight:bold;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid #333;";nd.appendChild(t1);const oocSel=document.createElement("select");oocSel.style.cssText="width:100%;padding:6px 8px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:12px;box-sizing:border-box;margin-bottom:12px;";for(const[k,v]of Object.entries(OOC_FORMATS)){const opt=document.createElement("option");opt.value=k;opt.textContent=v.name+" — "+v.desc;oocSel.appendChild(opt)}oocSel.value=settings.config.oocFormat||"custom";const pInp=document.createElement("input");pInp.value=settings.config.prefix||"";pInp.style.cssText="width:100%;padding:6px 8px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:12px;box-sizing:border-box;margin-bottom:12px;";pInp.onchange=()=>{settings.config.prefix=pInp.value;settings.save()};const sInp=document.createElement("input");sInp.value=settings.config.suffix||"";sInp.style.cssText="width:100%;padding:6px 8px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:12px;box-sizing:border-box;margin-bottom:20px;";sInp.onchange=()=>{settings.config.suffix=sInp.value;settings.save()};oocSel.onchange=()=>{const fmt=OOC_FORMATS[oocSel.value];if(fmt&&oocSel.value!=="custom"){pInp.value=fmt.prefix;sInp.value=fmt.suffix;settings.config.prefix=fmt.prefix;settings.config.suffix=fmt.suffix}settings.config.oocFormat=oocSel.value;settings.save();pInp.disabled=oocSel.value!=="custom";sInp.disabled=oocSel.value!=="custom";pInp.style.opacity=oocSel.value!=="custom"?"0.6":"1";sInp.style.opacity=oocSel.value!=="custom"?"0.6":"1"};nd.appendChild(oocSel);nd.appendChild(pInp);nd.appendChild(sInp);const isCustom=(settings.config.oocFormat||"custom")==="custom";pInp.disabled=!isCustom;sInp.disabled=!isCustom;pInp.style.opacity=isCustom?"1":"0.6";sInp.style.opacity=isCustom?"1":"0.6"}});panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const resetBtn=document.createElement("button");resetBtn.textContent="모든 설정 초기화 (DB 유지)";resetBtn.style.cssText="width:100%;padding:10px;margin-top:20px;background:#833;color:#fff;border:none;border-radius:4px;font-weight:bold;cursor:pointer;";resetBtn.onclick=()=>{if(!confirm("설정 초기화? API 설정값과 DB/로어팩 활성화는 유지됨."))return;if(_w.__LoreInj.resetSettingsKeepApi)_w.__LoreInj.resetSettingsKeepApi();else{const keep={autoExtApiType:settings.config.autoExtApiType,autoExtKey:settings.config.autoExtKey,autoExtVertexJson:settings.config.autoExtVertexJson,autoExtVertexLocation:settings.config.autoExtVertexLocation,autoExtVertexProjectId:settings.config.autoExtVertexProjectId,autoExtFirebaseScript:settings.config.autoExtFirebaseScript,autoExtFirebaseEmbedKey:settings.config.autoExtFirebaseEmbedKey,embeddingModel:settings.config.embeddingModel};_ls.removeItem("lore-injector-v5");Object.assign(settings.config,keep);settings.save()}location.reload()};nd.appendChild(resetBtn)}})},"메인 설정")})});_w.__LoreInj.__subMainLoaded=true})();(async function(){"use strict";if(document.readyState==="loading")await new Promise(r=>document.addEventListener("DOMContentLoaded",r));const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const deadline=Date.now()+15e3;while(!(_w.__LoreInj&&_w.__LoreInj.__settingsLoaded)&&Date.now()setTimeout(r,50));if(!(_w.__LoreInj&&_w.__LoreInj.__settingsLoaded)){console.error("[LoreInj:sub-lore] settings 미로드");return}if(_w.__LoreInj.__subLoreLoaded)return;const{C:C,db:db,settings:settings,isEntryEnabledForUrl:isEntryEnabledForUrl,setEntryEnabled:setEntryEnabled}=_w.__LoreInj;_w.__LoreInj.registerSubMenu=_w.__LoreInj.registerSubMenu||function(){};_w.__LoreInj.registerSubMenu("lore",function(modal){modal.createSubMenu("로어 관리 (목록)",m=>{const renderPanel=async panel=>{const _url=C.getCurUrl();const activePacks=_w.__LoreInj.getActivePacksForUrl?_w.__LoreInj.getActivePacksForUrl(_url):settings.config.urlPacks?.[_url]||[];if(!activePacks.length){panel.addText("활성화된 팩이 없습니다. 파일 탭에서 활성화하세요.");return}const entries=await db.entries.toArray();const filtered=entries.filter(e=>activePacks.includes(e.packName));if(!filtered.length){panel.addText("활성 항목 없음.");return}const byPack={};filtered.forEach(e=>{(byPack[e.packName]=byPack[e.packName]||[]).push(e)});for(const[pk,items]of Object.entries(byPack)){panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);nd.style.cssText+="background:#1a1a1a;border:1px solid #333;border-radius:4px;margin-bottom:12px;";const headerRow=document.createElement("div");headerRow.style.cssText="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;border-bottom:1px solid #333;padding-bottom:6px;cursor:pointer;";const curUrl=C.getCurUrl();const disabledEntries=settings.config.urlDisabledEntries?.[curUrl]||[];const allItemsEnabled=items.every(e=>!disabledEntries.includes(e.id));const pkSw=document.createElement("div");pkSw.style.cssText="width:28px;height:14px;border-radius:7px;background:"+(allItemsEnabled?"#285":"#444")+";position:relative;cursor:pointer;flex-shrink:0;margin-right:8px;";const pkDot=document.createElement("div");pkDot.style.cssText="width:10px;height:10px;border-radius:50%;background:#fff;position:absolute;top:2px;left:"+(allItemsEnabled?"16px":"2px")+";transition:left .2s;";pkSw.appendChild(pkDot);pkSw.onclick=async ev=>{ev.stopPropagation();const newState=!allItemsEnabled;const ud=JSON.parse(JSON.stringify(settings.config.urlDisabledEntries||{}));ud[curUrl]=ud[curUrl]||[];const itemIds=items.map(e=>e.id);if(newState){ud[curUrl]=ud[curUrl].filter(id=>!itemIds.includes(id))}else{for(const id of itemIds){if(!ud[curUrl].includes(id))ud[curUrl].push(id)}}settings.config.urlDisabledEntries=ud;settings.save();m.replaceContentPanel(renderPanel,"로어 관리")};const title=document.createElement("div");title.style.cssText="font-size:14px;font-weight:bold;color:#ccc;flex:1;";title.textContent=pk+" ("+items.length+"개)";const arrow=document.createElement("span");arrow.textContent="▼";arrow.style.cssText="font-size:12px;color:#888;";headerRow.appendChild(pkSw);headerRow.appendChild(title);headerRow.appendChild(arrow);nd.appendChild(headerRow);items.sort((a,b)=>(a.type||"").localeCompare(b.type||""));const listContainer=document.createElement("div");listContainer.style.cssText="display:none;flex-direction:column;gap:0;";let isExpanded=false;headerRow.onclick=()=>{isExpanded=!isExpanded;listContainer.style.display=isExpanded?"flex":"none";arrow.textContent=isExpanded?"▲":"▼"};for(const e of items){const row=document.createElement("div");row.style.cssText="padding:8px 0;border-bottom:1px solid #222;display:flex;flex-direction:column;";const header=document.createElement("div");header.style.cssText="display:flex;justify-content:space-between;align-items:center;";const left=document.createElement("div");left.style.cssText="display:flex;align-items:center;gap:8px;flex:1;";const isEnabled=isEntryEnabledForUrl(e);const swWrap=document.createElement("div");swWrap.style.cssText="display:flex;align-items:center;gap:6px;cursor:pointer;background:#111;padding:2px 8px;border-radius:12px;border:1px solid #333;flex-shrink:0;";const swLabel=document.createElement("span");swLabel.textContent=isEnabled?"ON":"OFF";swLabel.style.cssText="font-size:10px;font-weight:bold;width:20px;text-align:center;color:"+(isEnabled?"#4a7":"#777")+";";const sw=document.createElement("div");sw.style.cssText="width:24px;height:12px;border-radius:6px;background:"+(isEnabled?"#285":"#444")+";position:relative;";const dot=document.createElement("div");dot.style.cssText="width:8px;height:8px;border-radius:50%;background:#fff;position:absolute;top:2px;left:"+(isEnabled?"14px":"2px")+";transition:left .2s;";sw.appendChild(dot);swWrap.appendChild(swLabel);swWrap.appendChild(sw);swWrap.onclick=ev=>{ev.stopPropagation();const ns=!isEntryEnabledForUrl(e);setEntryEnabled(e,ns);swLabel.textContent=ns?"ON":"OFF";swLabel.style.color=ns?"#4a7":"#777";sw.style.background=ns?"#285":"#444";dot.style.left=ns?"14px":"2px"};const nameSpan=document.createElement("span");nameSpan.textContent="["+e.type+"] "+e.name;nameSpan.style.cssText="font-size:13px;color:#ccc;font-weight:bold;cursor:pointer;flex:1;";const embStatusSpan=document.createElement("span");embStatusSpan.style.cssText="margin-left:6px;font-size:10px;padding:1px 4px;border-radius:3px;";const updateEmbStatus=async()=>{try{const emb=await db.embeddings.where({entryId:e.id,field:"summary"}).first()||await db.embeddings.where("entryId").equals(e.id).first();if(emb){const ch=C.embeddingSourceHash?C.embeddingSourceHash(e,emb.field||"summary"):emb.hash||"";const targetModel=settings.config.embeddingModel||"gemini-embedding-001";if((emb.sourceHash||emb.hash)===ch&&(!emb.packName||emb.packName===e.packName)){const needsRegen=emb.model&&emb.model!==targetModel||emb.schemaVersion&&emb.schemaVersion<2||emb.taskType!=="RETRIEVAL_DOCUMENT"&&targetModel.includes("embedding-001");if(needsRegen){embStatusSpan.textContent="재생성";embStatusSpan.style.background="#3a3a1a";embStatusSpan.style.color="#d96"}else{embStatusSpan.textContent="임베딩";embStatusSpan.style.background="#1a3a2a";embStatusSpan.style.color="#4a9"}}else{embStatusSpan.textContent="변경됨";embStatusSpan.style.background="#3a3a1a";embStatusSpan.style.color="#d96"}}else{embStatusSpan.textContent="미임베딩";embStatusSpan.style.background="#2a2a2a";embStatusSpan.style.color="#888"}}catch(ex){}};updateEmbStatus();nameSpan.appendChild(embStatusSpan);left.appendChild(swWrap);left.appendChild(nameSpan);const right=document.createElement("div");right.style.cssText="display:flex;gap:6px;";const B="font-size:11px;padding:3px 8px;border-radius:3px;background:transparent;border:1px solid #555;color:#ccc;cursor:pointer;";const embGenBtn=document.createElement("button");embGenBtn.textContent="임베딩";embGenBtn.style.cssText=B+"color:#4a9;border-color:#264;";embGenBtn.onclick=async ev=>{ev.stopPropagation();embGenBtn.disabled=true;embGenBtn.textContent="...";try{await C.ensureEmbedding(e,{apiType:settings.config.autoExtApiType||"key",key:settings.config.autoExtKey,vertexJson:settings.config.autoExtVertexJson,vertexLocation:settings.config.autoExtVertexLocation||"global",vertexProjectId:settings.config.autoExtVertexProjectId,firebaseEmbedKey:settings.config.autoExtFirebaseEmbedKey,model:settings.config.embeddingModel||"gemini-embedding-001"});embGenBtn.textContent="OK";updateEmbStatus()}catch(err){embGenBtn.textContent="X";alert("실패:"+err.message)}setTimeout(()=>{embGenBtn.textContent="임베딩";embGenBtn.disabled=false},1500)};const copyBtn=document.createElement("button");copyBtn.textContent="복사";copyBtn.style.cssText=B+"color:#4a9;border-color:#264;";copyBtn.onclick=ev=>{ev.stopPropagation();const clean={...e};delete clean.id;delete clean.packName;delete clean.project;delete clean.enabled;navigator.clipboard.writeText(JSON.stringify(clean,null,2)).then(()=>alert("복사됨.")).catch(()=>alert("실패"))};const editBtn=document.createElement("button");editBtn.textContent="수정";editBtn.style.cssText=B+"color:#88c;border-color:#446;";const delBtn=document.createElement("button");delBtn.textContent="삭제";delBtn.style.cssText=B+"color:#a55;border-color:#633;";const histBtn=document.createElement("button");histBtn.textContent="이력";histBtn.style.cssText=B+"color:#da8;border-color:#642;";const anchorBtn=document.createElement("button");const _renderAnchor=()=>{const on=!!e.anchor;anchorBtn.textContent=on?"앵커":"앵커";anchorBtn.title=on?"앵커 해제 — 자동 추출 병합 시 보호 해제됨":"앵커 지정 — summary/state/detail/call/inject 자동 덮어쓰기 차단, 재주입 우선도 최대";anchorBtn.style.cssText=B+(on?"color:#fc4;border-color:#963;background:#2a1a00;":"color:#777;border-color:#444;")};_renderAnchor();anchorBtn.onclick=async ev=>{ev.stopPropagation();e.anchor=!e.anchor;try{await db.entries.put(e);_renderAnchor()}catch(err){alert("앵커 토글 실패: "+err.message);e.anchor=!e.anchor;_renderAnchor()}};right.appendChild(embGenBtn);right.appendChild(copyBtn);right.appendChild(histBtn);right.appendChild(anchorBtn);right.appendChild(editBtn);right.appendChild(delBtn);header.appendChild(left);header.appendChild(right);row.appendChild(header);const historyContainer=document.createElement("div");historyContainer.style.cssText="display:none;margin-top:8px;padding:8px;background:#0a0a0a;border:1px solid #222;border-radius:4px;";histBtn.onclick=async ev=>{ev.stopPropagation();if(historyContainer.style.display!=="none"){historyContainer.style.display="none";return}historyContainer.innerHTML='
불러오는 중...
';historyContainer.style.display="block";try{if(!C.getEntryVersions){historyContainer.innerHTML='
버전 이력 기능 미로드.
';return}const versions=await C.getEntryVersions(e.id);if(!versions||!versions.length){historyContainer.innerHTML='
저장된 버전 없음.
';return}historyContainer.innerHTML="";const hdr=document.createElement("div");hdr.textContent=versions.length+"개 버전 (최신순)";hdr.style.cssText="font-size:11px;color:#da8;margin-bottom:6px;font-weight:bold;";historyContainer.appendChild(hdr);for(const v of versions){const vrow=document.createElement("div");vrow.style.cssText="display:flex;justify-content:space-between;align-items:flex-start;padding:4px 0;border-bottom:1px dashed #222;gap:8px;";const info=document.createElement("div");info.style.cssText="font-size:11px;color:#aaa;flex:1;min-width:0;";const sumPrev=v.snapshot&&v.snapshot.summary?String(v.snapshot.summary).slice(0,80):"";const stPrev=v.snapshot&&(v.snapshot.state||v.snapshot.detail?.current_status)?" / state: "+(v.snapshot.state||v.snapshot.detail?.current_status):"";info.innerHTML=''+new Date(v.ts).toLocaleString()+' ['+(v.reason||"auto")+']
'+(sumPrev||"(요약 없음)")+stPrev+"";const vbtns=document.createElement("div");vbtns.style.cssText="display:flex;gap:4px;flex-shrink:0;";const resBtn=document.createElement("button");resBtn.textContent="복원";resBtn.style.cssText="padding:3px 8px;font-size:10px;border-radius:3px;background:#258;color:#fff;border:none;cursor:pointer;";resBtn.onclick=async ev2=>{ev2.stopPropagation();if(!confirm("이 버전으로 복원? 현재 상태는 자동 백업됨."))return;try{const restored=await C.restoreEntryVersion(v.id);Object.assign(e,restored);nameSpan.textContent="["+e.type+"] "+e.name;nameSpan.appendChild(embStatusSpan);alert("복원됨.");historyContainer.style.display="none"}catch(err){alert("실패: "+err.message)}};vbtns.appendChild(resBtn);vrow.appendChild(info);vrow.appendChild(vbtns);historyContainer.appendChild(vrow)}}catch(err){historyContainer.innerHTML='
오류: '+err.message+"
"}};row.appendChild(historyContainer);const editContainer=document.createElement("div");editContainer.style.cssText="display:none;margin-top:8px;flex-direction:column;gap:8px;";const ta=document.createElement("textarea");ta.style.cssText="width:100%;height:200px;background:#0a0a0a;color:#ccc;border:1px solid #333;border-radius:4px;padding:8px;font-size:12px;font-family:monospace;resize:vertical;box-sizing:border-box;";const editableObj={...e};delete editableObj.id;delete editableObj.packName;delete editableObj.project;delete editableObj.enabled;ta.value=JSON.stringify(editableObj,null,2);const btnRow=document.createElement("div");btnRow.style.cssText="display:flex;justify-content:flex-end;gap:6px;";const saveBtn=document.createElement("button");saveBtn.textContent="저장";saveBtn.style.cssText=B+"background:#285;border-color:#285;color:#fff;";const cancelBtn=document.createElement("button");cancelBtn.textContent="닫기";cancelBtn.style.cssText=B;btnRow.appendChild(cancelBtn);btnRow.appendChild(saveBtn);editContainer.appendChild(ta);editContainer.appendChild(btnRow);row.appendChild(editContainer);const toggleEdit=()=>{editContainer.style.display=editContainer.style.display==="none"?"flex":"none"};nameSpan.onclick=toggleEdit;editBtn.onclick=toggleEdit;cancelBtn.onclick=toggleEdit;saveBtn.onclick=async()=>{try{const parsed=JSON.parse(ta.value);const updated={...e,...parsed};await db.entries.put(updated);try{if(C.invalidateEntryEmbeddings)await C.invalidateEntryEmbeddings(updated.id)}catch(_){}alert("수정됨. 임베딩은 변경됨으로 표시되며 필요 시 재생성하세요.");Object.assign(e,updated);nameSpan.textContent="["+updated.type+"] "+updated.name;nameSpan.appendChild(embStatusSpan);updateEmbStatus();toggleEdit()}catch(err){alert("JSON 오류: "+err.message)}};delBtn.onclick=async()=>{if(confirm("["+e.name+"] 삭제?")){await db.entries.delete(e.id);try{await db.embeddings.where("entryId").equals(e.id).delete()}catch(ex){}const count=await db.entries.where("packName").equals(e.packName).count();if(count<=0){await db.packs.delete(e.packName);row.remove();m.replaceContentPanel(renderLoreUI,"로어 관리")}else{await db.packs.update(e.packName,{entryCount:count});row.remove();title.textContent=pk+" ("+count+"개)"}}};listContainer.appendChild(row)}nd.appendChild(listContainer)}})}};m.replaceContentPanel(renderPanel,"로어 목록 관리")})});_w.__LoreInj.__subLoreLoaded=true})();(async function(){"use strict";if(document.readyState==="loading")await new Promise(r=>document.addEventListener("DOMContentLoaded",r));const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const deadline=Date.now()+15e3;while(!(_w.__LoreInj&&_w.__LoreInj.__settingsLoaded)&&Date.now()setTimeout(r,50));if(!(_w.__LoreInj&&_w.__LoreInj.__settingsLoaded)){console.error("[LoreInj:sub-merge] settings 미로드");return}if(_w.__LoreInj.__subMergeLoaded)return;const{C:C,db:db,settings:settings}=_w.__LoreInj;function summaryValue(summary){if(!summary)return"";if(typeof summary==="object"&&!Array.isArray(summary))return String(summary.full||summary.compact||summary.micro||"");return String(summary||"")}function canonicalMergeType(type){const t=String(type||"").toLowerCase();if(t==="character"||t==="identity"||t==="persona")return"character";if(t==="rel"||t==="relationship")return"relationship";if(t==="prom"||t==="promise")return"promise";if(t==="location"||t==="place")return"location";if(t==="item"||t==="object")return"item";if(t==="event"||t==="scene")return"event";if(t==="concept"||t==="setting"||t==="world")return"setting";return t||"misc"}function typeCompatible(a,b){return canonicalMergeType(a&&a.type)===canonicalMergeType(b&&b.type)}function uniq(arr){return Array.from(new Set((arr||[]).filter(Boolean)))}function mergeObj(a,b){const out=a&&typeof a==="object"&&!Array.isArray(a)?JSON.parse(JSON.stringify(a)):{};if(b&&typeof b==="object"&&!Array.isArray(b)){for(const[k,v]of Object.entries(b)){if(Array.isArray(v))out[k]=uniq([...Array.isArray(out[k])?out[k]:[],...v]);else if(v&&typeof v==="object"&&!Array.isArray(v))out[k]=mergeObj(out[k],v);else if(v!==undefined&&v!==null&&v!=="")out[k]=v}}return out}function mergeSummaries(a,b,maxChars){const av=a&&typeof a==="object"&&!Array.isArray(a)?a:{full:summaryValue(a)};const bv=b&&typeof b==="object"&&!Array.isArray(b)?b:{full:summaryValue(b)};const join=(x,y,max)=>{x=String(x||"").trim();y=String(y||"").trim();if(!x)return y.slice(0,max);if(!y||x.includes(y))return x.slice(0,max);return(x+" / "+y).slice(0,max)};return{full:join(av.full||av.compact||av.micro,bv.full||bv.compact||bv.micro,maxChars||1200),compact:join(av.compact||av.micro,bv.compact||bv.micro,180),micro:bv.micro||av.micro||""}}function mergeDraftWithOriginals(draft,entries,maxChars){const sorted=[...entries].sort((a,b)=>(a.lastUpdated||a.ts||0)-(b.lastUpdated||b.ts||0));const anchored=sorted.find(e=>e.anchor);const base=JSON.parse(JSON.stringify(anchored||sorted[0]||{}));for(const e of sorted){base.triggers=uniq([...base.triggers||[],...e.triggers||[]]).slice(0,12);base.entities=uniq([...base.entities||[],...e.entities||[]]);base.parties=uniq([...base.parties||[],...e.parties||[]]);base.detail=mergeObj(base.detail,e.detail);base.call=mergeObj(base.call,e.call);base.callState=mergeObj(base.callState,e.callState);base.timeline=mergeObj(base.timeline,e.timeline);base.eventHistory=uniq([...base.eventHistory||[],...e.eventHistory||[]]);base.callHistory=uniq([...base.callHistory||[],...e.callHistory||[]]);base.summary=mergeSummaries(base.summary,e.summary,maxChars);base.inject=mergeSummaries(base.inject,e.inject||e.summary,Math.min(maxChars||1200,700))}const merged=mergeObj(base,draft||{});merged.triggers=uniq([...base.triggers||[],...draft&&draft.triggers||[]]).slice(0,12);merged.entities=uniq([...base.entities||[],...draft&&draft.entities||[]]);merged.eventHistory=uniq([...base.eventHistory||[],...draft&&draft.eventHistory||[]]);merged.callHistory=uniq([...base.callHistory||[],...draft&&draft.callHistory||[]]);merged.summary=mergeSummaries(base.summary,draft&&draft.summary,maxChars||1200);merged.inject=mergeSummaries(base.inject,draft&&draft.inject||draft&&draft.summary,Math.min(maxChars||1200,700));return merged}_w.__LoreInj.registerSubMenu=_w.__LoreInj.registerSubMenu||function(){};_w.__LoreInj.registerSubMenu("merge",function(modal){modal.createSubMenu("로어 병합 (중복 정리)",m=>{const renderMerge=async panel=>{const state=_w.__loreMergeState||(_w.__loreMergeState={threshold:.88,maxChars:1200,groups:null});panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const t=document.createElement("div");t.textContent="중복 로어 병합";t.style.cssText="font-size:14px;color:#4a9;font-weight:bold;margin-bottom:4px;";nd.appendChild(t);const d=document.createElement("div");d.innerHTML='임베딩 유사도로 중복 후보를 찾아 그룹별 수동 승인. 캐릭터/관계/약속/장소/사건처럼 대분류가 다른 로어는 유사해도 한 엔트리로 접지 않고 같은 대분류 안에서만 병합 후보를 만든다.
※ 현재 페이지에서 활성화(ON)된 팩의 엔트리만 대상 — 다른 페이지나 비활성 팩의 로어는 건드리지 않음.';d.style.cssText="font-size:11px;color:#888;margin-bottom:10px;line-height:1.5;";nd.appendChild(d);const row=document.createElement("div");row.style.cssText="display:flex;gap:12px;margin-bottom:8px;align-items:center;";const mk=(label,getter,setter,min,max,step)=>{const f=document.createElement("div");f.style.flex="1";const l=document.createElement("div");l.textContent=label;l.style.cssText="font-size:12px;color:#888;margin-bottom:4px;";const i=document.createElement("input");i.type="number";i.value=getter();i.min=min;i.max=max;i.step=step;i.style.cssText="width:100%;padding:6px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:12px;box-sizing:border-box;";i.onchange=()=>{const v=parseFloat(i.value);if(!isNaN(v))setter(v)};f.appendChild(l);f.appendChild(i);return f};row.appendChild(mk("유사도 임계값",()=>state.threshold,v=>state.threshold=v,.7,.99,.01));row.appendChild(mk("최대 글자수 (summary)",()=>state.maxChars,v=>state.maxChars=v,200,3e3,50));nd.appendChild(row);const runBtn=document.createElement("button");runBtn.textContent="병합 후보 찾기";runBtn.style.cssText="width:100%;padding:10px;background:#258;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:12px;font-weight:bold;";const runStatus=document.createElement("div");runStatus.style.cssText="font-size:11px;color:#888;margin-top:6px;text-align:center;";runBtn.onclick=async()=>{runBtn.disabled=true;runBtn.textContent="검색 중...";try{const _url=C.getCurUrl();const activePacks=settings.config.urlPacks?.[_url]||[];const entries=(await db.entries.toArray()).filter(e=>activePacks.includes(e.packName));if(entries.length<2){runStatus.textContent="활성 엔트리 2개 미만.";runStatus.style.color="#d66";return}const embs=await db.embeddings.where("entryId").anyOf(entries.map(e=>e.id)).toArray();const entryById={};entries.forEach(e=>entryById[e.id]=e);const embMap={};for(const eb of embs){if(eb.field!=="summary")continue;const ent=entryById[eb.entryId];if(!ent)continue;if(eb.packName&&eb.packName!==ent.packName)continue;if(C.embeddingSourceHash&&(eb.sourceHash||eb.hash)!==C.embeddingSourceHash(ent,"summary"))continue;embMap[eb.entryId]=eb.vector}const withEmb=entries.filter(e=>embMap[e.id]);if(withEmb.length<2){runStatus.textContent="임베딩 있는 엔트리 2개 미만. 먼저 파일 탭에서 임베딩 생성 필요.";runStatus.style.color="#d66";return}const cos=(a,b)=>{let d=0,na=0,nb=0;const L=Math.min(a.length,b.length);for(let i=0;i{let root=x;while(parent[root]!==root)root=parent[root];while(parent[x]!==root){const next=parent[x];parent[x]=root;x=next}return root};const uni=(a,b)=>{parent[find(a)]=find(b)};withEmb.forEach(e=>parent[e.id]=e.id);const pairs=[];for(let i=0;i=state.threshold){pairs.push({a:withEmb[i].id,b:withEmb[j].id,sim:s});uni(withEmb[i].id,withEmb[j].id)}}}const groupMap={};withEmb.forEach(e=>{const r=find(e.id);(groupMap[r]=groupMap[r]||[]).push(e)});const groups=Object.values(groupMap).filter(g=>g.length>=2).map(g=>{const ids=new Set(g.map(e=>e.id));const maxSim=pairs.filter(p=>ids.has(p.a)&&ids.has(p.b)).reduce((m,p)=>Math.max(m,p.sim),0);return{entries:g,sim:maxSim}}).sort((a,b)=>b.sim-a.sim);state.groups=groups;runStatus.textContent=groups.length>0?"후보 "+groups.length+"개 그룹 발견.":"임계값 이상 후보 없음.";runStatus.style.color=groups.length>0?"#4a9":"#888";m.replaceContentPanel(renderMerge,"로어 병합")}catch(e){runStatus.textContent="실패: "+e.message;runStatus.style.color="#d66"}runBtn.textContent="병합 후보 찾기";runBtn.disabled=false};nd.appendChild(runBtn);nd.appendChild(runStatus);const bulkRow=document.createElement("div");bulkRow.style.cssText="display:flex;gap:8px;align-items:center;margin-top:10px;padding:8px;background:#111;border:1px solid #333;border-radius:4px;";const bulkLbl=document.createElement("div");bulkLbl.textContent="일괄 모드 변경";bulkLbl.style.cssText="font-size:11px;color:#888;white-space:nowrap;";const bulkSel=document.createElement("select");bulkSel.style.cssText="flex:1;padding:6px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:11px;";[["","-- 선택하여 모든 그룹에 일괄 적용 --"],["keep-longest","가장 긴 항목 유지 + 키워드 합집합 (안전)"],["llm-summarize","LLM 요약 병합 (API 호출, 품질↑)"]].forEach(([v,l])=>{const o=document.createElement("option");o.value=v;o.textContent=l;bulkSel.appendChild(o)});bulkSel.onchange=()=>{if(!bulkSel.value)return;const sels=document.querySelectorAll("select.lore-merge-mode-sel");sels.forEach(s=>{s.value=bulkSel.value});bulkSel.value=""};bulkRow.appendChild(bulkLbl);bulkRow.appendChild(bulkSel);nd.appendChild(bulkRow);if(C.__lastMergeUndo&&C.__lastMergeUndo.originals&&C.__lastMergeUndo.originals.length){const undoBtn=document.createElement("button");undoBtn.textContent="직전 병합 취소 ("+C.__lastMergeUndo.originals.length+"개 엔트리 복원)";undoBtn.style.cssText="width:100%;padding:8px;margin-top:10px;background:transparent;color:#da8;border:1px solid #642;border-radius:4px;cursor:pointer;font-size:12px;";undoBtn.onclick=async()=>{if(!confirm("직전 병합을 취소하고 원본 엔트리들을 복원할 것?"))return;try{const undo=C.__lastMergeUndo;if(undo.mergedId!==undefined&&undo.mergedId!==null){await db.entries.delete(undo.mergedId);try{await db.embeddings.where("entryId").equals(undo.mergedId).delete()}catch(_){}}for(const snap of undo.originals){await db.entries.put(snap)}const packs=new Set(undo.originals.map(e=>e.packName));for(const pk of packs){const cnt=await db.entries.where("packName").equals(pk).count();await db.packs.update(pk,{entryCount:cnt})}C.__lastMergeUndo=null;alert("복원 완료.");m.replaceContentPanel(renderMerge,"로어 병합")}catch(e){alert("실패: "+e.message)}};nd.appendChild(undoBtn)}}});if(!state.groups)return;if(state.groups.length===0){panel.addText("후보 없음. 임계값을 낮춰 재시도할 수 있음.");return}for(const[gi,grp]of state.groups.entries()){panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);nd.style.cssText+="background:#1a1a1a;border:1px solid #333;border-radius:6px;margin-bottom:12px;padding:10px;";const hdr=document.createElement("div");hdr.style.cssText="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid #333;";const title=document.createElement("div");title.textContent="그룹 "+(gi+1)+" — "+grp.entries.length+"개 / 유사도 "+(grp.sim*100).toFixed(1)+"%";title.style.cssText="font-size:13px;color:#4a9;font-weight:bold;";const types=[...new Set(grp.entries.map(e=>e.type))];const typeWarn=types.length>1?"타입 불일치: "+types.join(", "):"타입: "+types[0];const warn=document.createElement("div");warn.textContent=typeWarn;warn.style.cssText="font-size:10px;color:"+(types.length>1?"#d96":"#888")+";";hdr.appendChild(title);hdr.appendChild(warn);nd.appendChild(hdr);if(types.length>1){const typeNote=document.createElement("div");typeNote.style.cssText="margin-bottom:8px;padding:6px 8px;background:#2a1a0a;border:1px solid #642;border-radius:4px;font-size:10px;color:#da8;line-height:1.5;";typeNote.innerHTML="타입이 다를 때의 영향
• 주입 시 주제별 분류(캐릭터/장소/사건 등)가 무너져 관련 없는 맥락에서 소환될 수 있음
• 트리거 키워드가 뒤섞여 오탐 증가
• 병합 후에는 한쪽 타입으로 고정되므로 나머지 주제성은 손실";nd.appendChild(typeNote)}for(const e of grp.entries){const row=document.createElement("div");row.style.cssText="padding:6px 0;border-bottom:1px dashed #222;font-size:11px;color:#aaa;";const safeSum=(summaryValue(e.summary)||"(요약 없음)").slice(0,150).replace(/['+e.type+"] "+e.name+' ('+e.packName+")"+(e.anchor?' 앵커':"")+'
'+safeSum+"";nd.appendChild(row)}const ctrl=document.createElement("div");ctrl.style.cssText="margin-top:10px;display:flex;gap:8px;align-items:center;";const modeSel=document.createElement("select");modeSel.className="lore-merge-mode-sel";modeSel.style.cssText="padding:6px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:11px;flex:1;";[["keep-longest","가장 긴 항목 유지 + 키워드 합집합 (안전)"],["llm-summarize","LLM 요약 병합 (API 호출, 품질↑)"]].forEach(([v,l])=>{const o=document.createElement("option");o.value=v;o.textContent=l;modeSel.appendChild(o)});const previewBtn=document.createElement("button");previewBtn.textContent="프리뷰";previewBtn.style.cssText="padding:6px 12px;font-size:11px;border-radius:4px;background:transparent;border:1px solid #446;color:#88c;cursor:pointer;";const execBtn=document.createElement("button");execBtn.textContent="병합 실행";execBtn.style.cssText="padding:6px 12px;font-size:11px;border-radius:4px;background:#285;border:none;color:#fff;cursor:pointer;font-weight:bold;";ctrl.appendChild(modeSel);ctrl.appendChild(previewBtn);ctrl.appendChild(execBtn);nd.appendChild(ctrl);const preview=document.createElement("div");preview.style.cssText="margin-top:8px;padding:8px;background:#0a0a0a;border:1px solid #222;border-radius:4px;font-size:11px;color:#ccc;display:none;white-space:pre-wrap;word-break:break-all;max-height:300px;overflow-y:auto;";nd.appendChild(preview);let mergedDraft=null;const mkKeepLongest=entries=>{const sorted=[...entries].sort((a,b)=>summaryValue(b.summary).length-summaryValue(a.summary).length);return mergeDraftWithOriginals(sorted[0]||{},entries,state.maxChars)};const mkLlmMerge=async entries=>{const clean=entries.map(({id:id,packName:packName,project:project,enabled:enabled,...rest})=>rest);const prompt="다음은 중복으로 판단된 로어 엔트리들이다. 하나의 로어 JSON으로 병합하라.\n"+"원칙:\n1. 핵심 정보는 누락하지 않는다. 각 입력 엔트리를 체크리스트처럼 대조해 이름/관계/상태/약속/사건/원인/미해결 훅을 모두 보존한다\n2. 불필요한 반복·수식어만 제거한다. 서로 다른 사실을 요약 편의상 삭제하지 않는다\n3. summary는 {full, compact, micro} 객체로 병합한다. full은 "+state.maxChars+"자 이내 self-contained, compact는 관계/상태/훅 보존, micro는 안정 회상 핸들+현재 상태\n4. eventHistory/callHistory/callState/timeline/entities/detail은 합집합으로 통합한다. 충돌하는 상태는 최신 timeline/lastUpdated를 우선하되 과거 상태는 eventHistory에 남긴다\n5. embed_text에는 이름/별칭/관계어/사건 원인/이해관계/장소/미해결 훅을 포함한다\n6. triggers는 필수 키워드만 유지하되 양쪽 인물명과 고유명사는 보존한다 (최대 12개)\n7. type은 가장 구체적인 것 유지. 타입이 다르면 summary.full에 각 타입의 역할을 설명한다\n8. 출력은 순수 JSON 객체 하나만. 입력에 없던 설정을 창작하지 않는다.\n\n"+"입력:\n"+JSON.stringify(clean,null,2);const res=await C.callGeminiApi(prompt,{apiType:settings.config.autoExtApiType||"key",key:settings.config.autoExtKey,vertexJson:settings.config.autoExtVertexJson,vertexLocation:settings.config.autoExtVertexLocation||"global",vertexProjectId:settings.config.autoExtVertexProjectId,firebaseScript:settings.config.autoExtFirebaseScript,model:settings.config.autoExtModel,maxRetries:2});if(!res.text)throw new Error("LLM 응답 없음: "+(res.error||""));let txt=res.text.trim().replace(/^```(?:json)?\s*/i,"").replace(/\s*```$/,"").trim();const m1=txt.match(/\{[\s\S]*\}/);if(m1)txt=m1[0];const parsed=JSON.parse(txt);return mergeDraftWithOriginals(parsed,entries,state.maxChars)};const buildPreview=async()=>{preview.style.display="block";preview.textContent="생성 중...";try{if(modeSel.value==="keep-longest")mergedDraft=mkKeepLongest(grp.entries);else mergedDraft=await mkLlmMerge(grp.entries);const sumLen=summaryValue(mergedDraft.summary).length;const over=sumLen>state.maxChars;const color=over?"#d66":"#4a9";preview.innerHTML='
병합 미리보기 (summary '+sumLen+"자"+(over?" — 상한("+state.maxChars+") 초과":"")+')
'+JSON.stringify(mergedDraft,null,2).replace(/"}catch(e){preview.textContent="X "+e.message;mergedDraft=null}};previewBtn.onclick=buildPreview;execBtn.onclick=async()=>{if(!mergedDraft){await buildPreview();if(!mergedDraft)return}const sumLen=summaryValue(mergedDraft.summary).length;if(sumLen>state.maxChars){if(!confirm("summary가 상한("+state.maxChars+"자)을 "+(sumLen-state.maxChars)+"자 초과. 그래도 적용?"))return}if(types.length>1){if(!confirm("타입 불일치 ("+types.join(", ")+"). 계속?"))return}if(!confirm(grp.entries.length+"개 엔트리를 하나로 병합할 것?"))return;try{const originals=grp.entries.map(e=>JSON.parse(JSON.stringify(e)));for(const e of grp.entries){try{if(C.saveEntryVersion)await C.saveEntryVersion(e,"pre_merge")}catch(_){}}const anchored=grp.entries.find(e=>e.anchor);const target=anchored||grp.entries[0];const toDelete=grp.entries.filter(e=>e.id!==target.id);const finalEntry={...mergedDraft,id:target.id,packName:target.packName,project:target.project};await db.entries.put(finalEntry);for(const e of toDelete){await db.entries.delete(e.id);try{if(C.invalidateEntryEmbeddings)await C.invalidateEntryEmbeddings(e.id);else await db.embeddings.where("entryId").equals(e.id).delete()}catch(_){}}try{if(C.invalidateEntryEmbeddings)await C.invalidateEntryEmbeddings(target.id);else await db.embeddings.where("entryId").equals(target.id).delete()}catch(_){}const packs=new Set(grp.entries.map(e=>e.packName));for(const pk of packs){const cnt=await db.entries.where("packName").equals(pk).count();await db.packs.update(pk,{entryCount:cnt})}C.__lastMergeUndo={mergedId:target.id,originals:originals};state.groups=state.groups.filter((_,i)=>i!==gi);let embedMsg="";try{const apiType=settings.config.autoExtApiType||"key";const apiOpts={apiType:apiType,key:settings.config.autoExtKey,vertexJson:settings.config.autoExtVertexJson,vertexLocation:settings.config.autoExtVertexLocation||"global",vertexProjectId:settings.config.autoExtVertexProjectId,firebaseEmbedKey:settings.config.autoExtFirebaseEmbedKey,model:settings.config.embeddingModel||"gemini-embedding-001"};const hasApi=apiType==="vertex"?!!settings.config.autoExtVertexJson:apiType==="firebase"?!!settings.config.autoExtFirebaseEmbedKey:!!settings.config.autoExtKey;if(hasApi){await C.ensureEmbedding(finalEntry,apiOpts);embedMsg="임베딩 재생성 완료."}else{embedMsg="API 미설정 — 파일 탭에서 수동 임베딩 필요."}}catch(embErr){embedMsg="임베딩 재생성 실패: "+(embErr.message||embErr)}alert("병합 완료. "+embedMsg);m.replaceContentPanel(renderMerge,"로어 병합")}catch(e){alert("실패: "+e.message)}}}})}};m.replaceContentPanel(renderMerge,"로어 병합")})});_w.__LoreInj.__subMergeLoaded=true})();(async function(){"use strict";if(document.readyState==="loading")await new Promise(r=>document.addEventListener("DOMContentLoaded",r));const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const deadline=Date.now()+15e3;while(!(_w.__LoreInj&&_w.__LoreInj.__settingsLoaded)&&Date.now()setTimeout(r,50));if(!(_w.__LoreInj&&_w.__LoreInj.__settingsLoaded)){console.error("[LoreInj:sub-snapshot] settings 미로드");return}if(_w.__LoreInj.__subSnapshotLoaded)return;const{C:C,db:db,restoreSnapshot:restoreSnapshot}=_w.__LoreInj;_w.__LoreInj.registerSubMenu=_w.__LoreInj.registerSubMenu||function(){};_w.__LoreInj.registerSubMenu("snapshot",function(modal){modal.createSubMenu("로어 스냅샷 (백업)",m=>{const renderSnapshotUI=async panel=>{panel.addBoxedField("","",{onInit:async nd=>{C.setFullWidth(nd);const t=document.createElement("div");t.textContent="스냅샷 복원";t.style.cssText="font-size:14px;color:#ccc;font-weight:bold;margin-bottom:8px;";nd.appendChild(t);const snaps=await db.snapshots.orderBy("timestamp").reverse().toArray();if(!snaps.length){nd.appendChild(Object.assign(document.createElement("div"),{textContent:"저장된 스냅샷이 없습니다.",style:"color:#888;font-size:12px;"}));return}const list=document.createElement("div");list.style.cssText="display:flex;flex-direction:column;gap:6px;max-height:300px;overflow-y:auto;";for(const s of snaps){const row=document.createElement("div");row.style.cssText="display:flex;justify-content:space-between;align-items:center;padding:6px;background:#1a1a1a;border:1px solid #333;border-radius:4px;";const info=document.createElement("div");info.style.cssText="display:flex;flex-direction:column;";const sTitle=document.createElement("span");sTitle.textContent=`[${s.packName}] ${s.label} (${s.data.length}개)`;sTitle.style.cssText="font-size:12px;color:#4a9;font-weight:bold;";const sTime=document.createElement("span");sTime.textContent=new Date(s.timestamp).toLocaleString();sTime.style.cssText="font-size:10px;color:#888;";info.appendChild(sTitle);info.appendChild(sTime);const btnWrap=document.createElement("div");btnWrap.style.cssText="display:flex;gap:4px;";const rBtn=document.createElement("button");rBtn.textContent="복원";rBtn.style.cssText="padding:4px 8px;font-size:11px;border-radius:3px;background:#258;color:#fff;border:none;cursor:pointer;";rBtn.onclick=async()=>{if(confirm(`[${s.packName}] 팩을 이 시점으로 복원할 것?\n기존 데이터는 덮어씌워집니다.`)){await restoreSnapshot(s.id);alert("복원 완료.");m.replaceContentPanel(renderSnapshotUI,"스냅샷 관리")}};const dBtn=document.createElement("button");dBtn.textContent="삭제";dBtn.style.cssText="padding:4px 8px;font-size:11px;border-radius:3px;background:transparent;color:#d66;border:1px solid #d66;cursor:pointer;";dBtn.onclick=async()=>{if(confirm("삭제?")){await db.snapshots.delete(s.id);m.replaceContentPanel(renderSnapshotUI,"스냅샷 관리")}};btnWrap.appendChild(rBtn);btnWrap.appendChild(dBtn);row.appendChild(info);row.appendChild(btnWrap);list.appendChild(row)}nd.appendChild(list)}})};m.replaceContentPanel(renderSnapshotUI,"스냅샷 관리")})});_w.__LoreInj.__subSnapshotLoaded=true})();(async function(){"use strict";if(document.readyState==="loading")await new Promise(r=>document.addEventListener("DOMContentLoaded",r));const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const deadline=Date.now()+15e3;while(!(_w.__LoreInj&&_w.__LoreInj.__settingsLoaded)&&Date.now()setTimeout(r,50));if(!(_w.__LoreInj&&_w.__LoreInj.__settingsLoaded)){console.error("[LoreInj:sub-file] settings 미로드");return}if(_w.__LoreInj.__subFileLoaded)return;const{C:C,db:db,settings:settings,setPackEnabled:setPackEnabled}=_w.__LoreInj;_w.__LoreInj.registerSubMenu=_w.__LoreInj.registerSubMenu||function(){};_w.__LoreInj.registerSubMenu("file",function(modal){modal.createSubMenu("로어 관리 (파일)",m=>{const renderPackUI=async panel=>{panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const title=document.createElement("div");title.textContent="로어 가져오기";title.style.cssText="font-size:14px;color:#ccc;font-weight:bold;margin-bottom:8px;";nd.appendChild(title);const row=document.createElement("div");row.style.cssText="display:flex;gap:8px;align-items:center;margin-bottom:8px;";const nameInput=document.createElement("input");nameInput.placeholder="로어 이름";nameInput.style.cssText="flex:1;padding:6px 8px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:12px;";row.appendChild(nameInput);const fileInput=document.createElement("input");fileInput.type="file";fileInput.accept=".json";fileInput.style.display="none";const importBtn=document.createElement("button");importBtn.textContent="JSON 파일 가져오기";importBtn.style.cssText="padding:6px 14px;font-size:12px;border-radius:4px;cursor:pointer;background:#258;color:#fff;border:1px solid #258;font-weight:bold;white-space:nowrap;";importBtn.onclick=()=>fileInput.click();fileInput.onchange=async ev=>{const file=ev.target.files[0];if(!file)return;const packName=nameInput.value.trim()||file.name.replace(".json","");try{const text=await file.text();const data=JSON.parse(text);const arr=Array.isArray(data)?data:Array.isArray(data.entries)?data.entries:[data];let count=0;for(let e of arr){if(!e||!e.name)continue;if(C.normalizeLoreEntry)e=C.normalizeLoreEntry(e,{source:"imported"});if(!e.triggers)e.triggers=[e.name];e.packName=packName;e.project=settings.config.activeProject||"";e.enabled=true;e.src=e.src||"im";e.source=e.source||"imported";e.ts=e.ts||Date.now();e.lastUpdated=Date.now();const existing=await db.entries.where("packName").equals(packName).and(x=>x.name===e.name).first();if(existing){await db.entries.update(existing.id,e);try{if(C.invalidateEntryEmbeddings)await C.invalidateEntryEmbeddings(existing.id)}catch(_){}}else{await db.entries.add(e);count++}}const totalCount=await db.entries.where("packName").equals(packName).count();let pack=await db.packs.get(packName);if(pack)await db.packs.update(packName,{entryCount:totalCount});else await db.packs.put({name:packName,entryCount:totalCount,project:settings.config.activeProject||""});await setPackEnabled(packName,true);alert(arr.length+"개 항목 처리 완료 (신규 "+count+"개)");m.replaceContentPanel(renderPackUI,"파일 관리")}catch(err){alert("가져오기 실패: "+err.message)}fileInput.value=""};row.appendChild(fileInput);row.appendChild(importBtn);nd.appendChild(row);const manualLbl=document.createElement("div");manualLbl.textContent="또는 직접 JSON 입력";manualLbl.style.cssText="font-size:12px;color:#888;margin:12px 0 4px;";nd.appendChild(manualLbl);const manualTa=document.createElement("textarea");manualTa.placeholder='[{"name":"이름","triggers":["키워드"],"type":"character","summary":"설명","detail":{}}]';manualTa.style.cssText="width:100%;height:100px;background:#0a0a0a;color:#ccc;border:1px solid #333;border-radius:4px;padding:8px;font-size:12px;font-family:monospace;resize:vertical;box-sizing:border-box;";nd.appendChild(manualTa);const manualBtnRow=document.createElement("div");manualBtnRow.style.cssText="display:flex;justify-content:flex-end;margin-top:6px;";const manualBtn=document.createElement("button");manualBtn.textContent="수동 추가";manualBtn.style.cssText="padding:6px 14px;font-size:12px;border-radius:4px;cursor:pointer;background:#285;color:#fff;border:1px solid #285;font-weight:bold;";manualBtn.onclick=async()=>{const pn=nameInput.value.trim()||"수동추가";try{const txt=manualTa.value.trim();if(!txt){alert("JSON을 입력할 것.");return}const data=JSON.parse(txt);const arr=Array.isArray(data)?data:Array.isArray(data.entries)?data.entries:[data];let cnt=0;for(let e of arr){if(!e||!e.name)continue;if(C.normalizeLoreEntry)e=C.normalizeLoreEntry(e,{source:"imported"});if(!e.triggers)e.triggers=[e.name];e.packName=pn;e.project=settings.config.activeProject||"";e.enabled=true;e.src=e.src||"im";e.source=e.source||"imported";e.ts=e.ts||Date.now();e.lastUpdated=Date.now();const ex=await db.entries.where("packName").equals(pn).and(x=>x.name===e.name).first();if(ex){await db.entries.update(ex.id,e);try{if(C.invalidateEntryEmbeddings)await C.invalidateEntryEmbeddings(ex.id)}catch(_){}}else{await db.entries.add(e);cnt++}}const tc=await db.entries.where("packName").equals(pn).count();let pk=await db.packs.get(pn);if(pk)await db.packs.update(pn,{entryCount:tc});else await db.packs.put({name:pn,entryCount:tc,project:settings.config.activeProject||""});await setPackEnabled(pn,true);alert(arr.length+"개 처리 (신규 "+cnt+"개)");manualTa.value="";m.replaceContentPanel(renderPackUI,"파일 관리")}catch(err){alert("JSON 파싱 실패: "+err.message)}};manualBtnRow.appendChild(manualBtn);nd.appendChild(manualBtnRow)}});panel.addBoxedField("","",{onInit:async nd=>{C.setFullWidth(nd);const rawPacks=await db.packs.toArray();const packs=[];for(const p of rawPacks){const count=await db.entries.where("packName").equals(p.name).count();if(count<=0){await db.packs.delete(p.name);continue}if((p.entryCount||0)!==count)await db.packs.update(p.name,{entryCount:count});packs.push({...p,entryCount:count})}if(!packs.length){const empty=document.createElement("div");empty.textContent="등록된 팩이 없습니다.";empty.style.cssText="color:#666;text-align:center;padding:20px;font-size:12px;";nd.appendChild(empty);return}const curUrl=C.getCurUrl();const enabledPacks=_w.__LoreInj.getActivePacksForUrl?_w.__LoreInj.getActivePacksForUrl(curUrl):settings.config.urlPacks?.[curUrl]||[];for(const pack of packs){const packDiv=document.createElement("div");packDiv.style.cssText="margin-bottom:8px;border:1px solid #333;border-radius:4px;overflow:hidden;";const header=document.createElement("div");header.style.cssText="display:flex;justify-content:space-between;align-items:center;padding:10px 12px;background:#111;";const leftSide=document.createElement("div");leftSide.style.cssText="display:flex;align-items:center;gap:8px;flex:1;";const isEnabled=enabledPacks.includes(pack.name);const swWrap=document.createElement("div");swWrap.style.cssText="display:flex;align-items:center;gap:6px;cursor:pointer;";const swLabel=document.createElement("span");swLabel.textContent=isEnabled?"ON":"OFF";swLabel.style.cssText="font-size:10px;font-weight:bold;width:20px;text-align:center;color:"+(isEnabled?"#4a7":"#777")+";";const sw=document.createElement("div");sw.style.cssText="width:24px;height:12px;border-radius:6px;background:"+(isEnabled?"#285":"#444")+";position:relative;";const dot=document.createElement("div");dot.style.cssText="width:8px;height:8px;border-radius:50%;background:#fff;position:absolute;top:2px;left:"+(isEnabled?"14px":"2px")+";transition:left .2s;";sw.appendChild(dot);swWrap.appendChild(swLabel);swWrap.appendChild(sw);swWrap.onclick=async()=>{const curEnabled=(_w.__LoreInj.getActivePacksForUrl?_w.__LoreInj.getActivePacksForUrl(C.getCurUrl()):settings.config.urlPacks?.[C.getCurUrl()]||[]).includes(pack.name);await setPackEnabled(pack.name,!curEnabled);const nowEnabled=(_w.__LoreInj.getActivePacksForUrl?_w.__LoreInj.getActivePacksForUrl(C.getCurUrl()):settings.config.urlPacks?.[C.getCurUrl()]||[]).includes(pack.name);swLabel.textContent=nowEnabled?"ON":"OFF";swLabel.style.color=nowEnabled?"#4a7":"#777";sw.style.background=nowEnabled?"#285":"#444";dot.style.left=nowEnabled?"14px":"2px"};leftSide.appendChild(swWrap);const nameEl=document.createElement("span");nameEl.textContent=pack.name+" ("+(pack.entryCount||0)+"개)";nameEl.style.cssText="font-size:13px;color:#ccc;font-weight:bold;";leftSide.appendChild(nameEl);header.appendChild(leftSide);const actions=document.createElement("div");actions.style.cssText="display:flex;gap:6px;";const B="font-size:11px;padding:3px 8px;border-radius:3px;background:transparent;border:1px solid #555;color:#ccc;cursor:pointer;";const exportBtn=document.createElement("button");exportBtn.textContent="내보내기";exportBtn.style.cssText=B;exportBtn.onclick=async()=>{const entries=await db.entries.where("packName").equals(pack.name).toArray();if(!entries.length){alert("항목 없음.");return}const clean=entries.map(({id:id,packName:packName,project:project,enabled:enabled,...rest})=>rest);const blob=new Blob([JSON.stringify(clean,null,2)],{type:"application/json"});const url=URL.createObjectURL(blob);const a=document.createElement("a");a.href=url;a.download=pack.name+".json";document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url)};const embBtn=document.createElement("button");embBtn.textContent="임베딩";embBtn.style.cssText=B+"color:#4a9;border-color:#264;";embBtn.onclick=async()=>{const apiType=settings.config.autoExtApiType||"key";const miss=apiType==="vertex"?!settings.config.autoExtVertexJson:apiType==="firebase"?!settings.config.autoExtFirebaseEmbedKey:!settings.config.autoExtKey;if(miss){alert(apiType==="firebase"?"임베딩용 Gemini API 키 필요.":"API 설정 필요.");return}if(!confirm("["+pack.name+"] 임베딩 생성?"))return;embBtn.disabled=true;const orig=embBtn.textContent;try{const cnt=await C.embedPack(pack.name,{apiType:apiType,key:settings.config.autoExtKey,vertexJson:settings.config.autoExtVertexJson,vertexLocation:settings.config.autoExtVertexLocation||"global",vertexProjectId:settings.config.autoExtVertexProjectId,firebaseEmbedKey:settings.config.autoExtFirebaseEmbedKey,model:settings.config.embeddingModel||"gemini-embedding-001"},(done,total)=>{embBtn.textContent=done+"/"+total});embBtn.textContent="OK"+cnt;setTimeout(()=>{embBtn.textContent=orig;embBtn.disabled=false},2e3)}catch(e){embBtn.textContent="X";embBtn.disabled=false;alert("실패:"+e.message)}};const cleanBtn=document.createElement("button");cleanBtn.textContent="정리";cleanBtn.title="API 호출 없이 stale embeddings 삭제";cleanBtn.style.cssText=B+"color:#da8;border-color:#642;";cleanBtn.onclick=async()=>{cleanBtn.disabled=true;const orig=cleanBtn.textContent;cleanBtn.textContent="...";try{const rpt=C.cleanupStaleEmbeddings?await C.cleanupStaleEmbeddings(pack.name,{model:settings.config.embeddingModel||"gemini-embedding-001"}):{removed:0};cleanBtn.textContent="정리 "+rpt.removed;alert("stale embedding 정리 완료: "+JSON.stringify(rpt))}catch(e){cleanBtn.textContent="X";alert("정리 실패: "+e.message)}setTimeout(()=>{cleanBtn.textContent=orig;cleanBtn.disabled=false},1500)};const delBtn=document.createElement("button");delBtn.textContent="삭제";delBtn.style.cssText=B+"color:#a55;border-color:#633;";delBtn.onclick=async()=>{if(!confirm("["+pack.name+"] 삭제?"))return;const es=await db.entries.where("packName").equals(pack.name).toArray();for(const e of es)await db.embeddings.where("entryId").equals(e.id).delete();await db.entries.where("packName").equals(pack.name).delete();await db.packs.delete(pack.name);m.replaceContentPanel(renderPackUI,"파일 관리")};actions.appendChild(exportBtn);actions.appendChild(embBtn);actions.appendChild(cleanBtn);actions.appendChild(delBtn);header.appendChild(actions);packDiv.appendChild(header);nd.appendChild(packDiv)}}})};m.replaceContentPanel(renderPackUI,"파일 관리")})});_w.__LoreInj.__subFileLoaded=true})();(async function(){const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const _ls=_w.localStorage;const deadline=Date.now()+15e3;while(!(_w.__LoreInj&&_w.__LoreInj.__settingsLoaded)&&Date.now()setTimeout(r,50));if(_w.__LoreInj.__subExtractLoaded)return;const{C:C,db:db,settings:settings,getAutoExtPackForUrl:getAutoExtPackForUrl,setAutoExtPackForUrl:setAutoExtPackForUrl,setPackEnabled:setPackEnabled}=_w.__LoreInj;_w.__LoreInj.registerSubMenu=_w.__LoreInj.registerSubMenu||function(){};function buildGenerationApiOpts(overrides={},costContext=null){const cfg=settings.config||{};const model=cfg.autoExtModel==="_custom"?cfg.autoExtCustomModel:cfg.autoExtModel;const opts={apiType:cfg.autoExtApiType||"key",key:cfg.autoExtKey,vertexJson:cfg.autoExtVertexJson,vertexLocation:cfg.autoExtVertexLocation||"global",vertexProjectId:cfg.autoExtVertexProjectId,firebaseScript:cfg.autoExtFirebaseScript,firebaseEmbedKey:cfg.autoExtFirebaseEmbedKey,model:model||"gemini-3-flash-preview",maxRetries:cfg.autoExtMaxRetries||1,responseMimeType:"application/json",costContext:costContext,...overrides};const reasoning=cfg.autoExtReasoning||"medium";if(String(opts.model||"").includes("gemini-3")&&reasoning&&reasoning!=="off"&&reasoning!=="budget"){opts.thinkingConfig={thinkingLevel:reasoning}}if(String(opts.model||"").includes("pro")&&opts.thinkingConfig?.thinkingLevel==="minimal"){opts.thinkingConfig.thinkingLevel="low"}return opts}_w.__LoreInj.registerSubMenu("extract",function(modal){modal.createSubMenu("추출/변환 설정",m=>{m.replaceContentPanel(panel=>{panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);nd.appendChild(C.createToggleRow("자동 대화 정리","정해진 턴마다 대화를 로어에 추가함.",settings.config.autoExtEnabled,v=>{settings.config.autoExtEnabled=v;settings.save()}));nd.appendChild(C.createToggleRow("기존 로어 참고","저장된 로어를 같이 참고해 중복 저장 줄임.",settings.config.autoExtIncludeDb,v=>{settings.config.autoExtIncludeDb=v;settings.save()}));nd.appendChild(C.createToggleRow("변경분만 저장","기존 로어 전체 대신 바뀐 부분만 받아 출력 토큰 줄임.",settings.config.autoExtPatchMode!==false,v=>{settings.config.autoExtPatchMode=v;settings.save()}));nd.appendChild(C.createToggleRow("페르소나 정보 전송","추출 시 페르소나 이름을 같이 보내 정확도 올림.",settings.config.autoExtIncludePersona,v=>{settings.config.autoExtIncludePersona=v;settings.save()}));nd.appendChild(C.createToggleRow("중요 장면 기억하기","중요 사건/약속을 나중에 떠올릴 수 있게 별도 정리함.",settings.config.temporalExtractEnabled!==false,v=>{settings.config.temporalExtractEnabled=v;settings.save()}));const row1=document.createElement("div");row1.style.cssText="display:flex;gap:12px;margin-bottom:8px;align-items:center;";const makeInput=(label,key,defaultVal)=>{const f=document.createElement("div");f.style.flex="1";const l=document.createElement("div");l.textContent=label;l.style.cssText="font-size:12px;color:#888;margin-bottom:4px;";const i=document.createElement("input");i.type="number";i.value=settings.config[key]!==undefined?settings.config[key]:defaultVal;i.style.cssText="width:100%;padding:6px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:12px;box-sizing:border-box;";const saveNum=()=>{const v=parseInt(i.value);if(!isNaN(v)){settings.config[key]=v;settings.save()}};i.oninput=saveNum;i.onchange=saveNum;f.appendChild(l);f.appendChild(i);return f};row1.appendChild(makeInput("자동 정리 주기","autoExtTurns",8));row1.appendChild(makeInput("읽을 최근 대화","autoExtScanRange",6));row1.appendChild(makeInput("최근 제외","autoExtOffset",5));row1.appendChild(makeInput("중요 장면 최대","temporalMaxEventsPerPass",5));nd.appendChild(row1);const injTitle=document.createElement("div");injTitle.textContent="삽입 옵션";injTitle.style.cssText="font-size:13px;color:#ccc;font-weight:bold;margin:12px 0 6px;padding-top:10px;border-top:1px solid #333;";nd.appendChild(injTitle);nd.appendChild(C.createToggleRow("삽입 쿨타임 사용","같은 로어가 너무 자주 들어가지 않게 막음.",settings.config.cooldownEnabled!==false,v=>{settings.config.cooldownEnabled=v;settings.save()}));nd.appendChild(C.createToggleRow("오래된 정보도 가끔 넣기","직접 관련이 약해도 중요한 과거 정보를 주기적으로 넣음.",settings.config.periodicRecallEnabled!==false,v=>{settings.config.periodicRecallEnabled=v;settings.config.decayEnabled=v;settings.save()}));const injRow=document.createElement("div");injRow.style.cssText="display:flex;gap:12px;margin:8px 0 12px;align-items:center;";injRow.appendChild(makeInput("삽입 쿨타임(턴)","cooldownTurns",3));injRow.appendChild(makeInput("한 번에 넣을 로어","maxEntries",3));nd.appendChild(injRow);const row2=document.createElement("div");row2.style.cssText="display:flex;gap:12px;margin-bottom:12px;align-items:center;";const f3=document.createElement("div");f3.style.flex="1";const l3=document.createElement("div");l3.textContent="저장할 로어팩";l3.style.cssText="font-size:12px;color:#888;margin-bottom:4px;";const inputWrap=document.createElement("div");inputWrap.style.cssText="display:flex;gap:6px;";const i3=document.createElement("input");i3.type="text";getAutoExtPackForUrl(C.getCurUrl()).then(name=>i3.value=name);i3.style.cssText="flex:1;padding:6px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:12px;box-sizing:border-box;";const savePackName=()=>{const val=i3.value||"자동추출";settings.config.autoExtPack=val;setAutoExtPackForUrl(C.getCurUrl(),val)};i3.oninput=savePackName;i3.onchange=savePackName;const s3=document.createElement("select");s3.style.cssText="width:100px;padding:6px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:12px;";db.packs.toArray().then(packs=>{const opt=document.createElement("option");opt.value="";opt.textContent="기존 선택";s3.appendChild(opt);packs.forEach(p=>{const o=document.createElement("option");o.value=p.name;o.textContent=p.name;s3.appendChild(o)})});s3.onchange=()=>{if(s3.value){i3.value=s3.value;settings.config.autoExtPack=s3.value;setAutoExtPackForUrl(C.getCurUrl(),s3.value);s3.value=""}};inputWrap.appendChild(i3);inputWrap.appendChild(s3);f3.appendChild(l3);f3.appendChild(inputWrap);row2.appendChild(f3);nd.appendChild(row2);const btnRun=document.createElement("button");btnRun.textContent="수동 추출 실행";btnRun.style.cssText="padding:8px 16px;font-size:12px;border-radius:4px;cursor:pointer;background:#285;color:#fff;border:none;font-weight:bold;width:100%;margin-top:10px;";const btnStatus=document.createElement("div");btnStatus.style.cssText="font-size:11px;color:#888;margin-top:6px;text-align:center;line-height:1.4;";btnStatus.textContent="";btnRun.onclick=async()=>{if(!confirm("수동 추출 시작?"))return;settings.save();btnRun.disabled=true;const origText=btnRun.textContent;const startMs=Date.now();btnRun.textContent="에리가 추출 중...";btnStatus.textContent="에리가 대화 분석 중";btnStatus.style.color="#4a9";const tick=setInterval(()=>{const sec=Math.floor((Date.now()-startMs)/1e3);btnStatus.textContent=`에리가 대화 분석 중 (${sec}초)`},1e3);try{await _w.__LoreInj.runAutoExtract(true);clearInterval(tick);const sec=Math.floor((Date.now()-startMs)/1e3);btnStatus.textContent=`에리: 완료 (${sec}초)`;btnStatus.style.color="#4a9";setTimeout(()=>{btnStatus.textContent=""},4e3)}catch(e){clearInterval(tick);btnStatus.textContent="에리: 실패 — "+(e.message||e).slice(0,50);btnStatus.style.color="#d66"}finally{btnRun.textContent=origText;btnRun.disabled=false}};nd.appendChild(btnRun);nd.appendChild(btnStatus)}});panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const jt=document.createElement("div");jt.textContent="과거 장면 불러오기 판단";jt.style.cssText="font-size:14px;color:#4a9;font-weight:bold;margin-bottom:6px;";nd.appendChild(jt);const jd=document.createElement("div");jd.textContent="사용 여부와 호출 범위만 여기서 조절함. 모델/생각 깊이는 API 설정에서 관리함.";jd.style.cssText="font-size:11px;color:#888;margin-bottom:10px;line-height:1.4;";nd.appendChild(jd);nd.appendChild(C.createToggleRow("AI로 참고 장면 고르기","규칙 판단 뒤 AI가 필요한 과거 장면 한 번 더 고름.",settings.config.temporalRecallJudgeEnabled,v=>{settings.config.temporalRecallJudgeEnabled=v;settings.save()}));const jrow=document.createElement("div");jrow.style.cssText="display:flex;gap:12px;margin-top:10px;";const jmkNum=(label,key,defaultVal,min,max)=>{const f=document.createElement("div");f.style.flex="1";const l=document.createElement("div");l.textContent=label;l.style.cssText="font-size:11px;color:#999;margin-bottom:4px;";const i=document.createElement("input");i.type="number";i.value=settings.config[key]!==undefined?settings.config[key]:defaultVal;if(min!==undefined)i.min=min;if(max!==undefined)i.max=max;i.style.cssText="width:100%;padding:6px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:12px;box-sizing:border-box;";const save=()=>{const v=parseInt(i.value);if(!isNaN(v)){settings.config[key]=v;settings.save()}};i.oninput=save;i.onchange=save;f.appendChild(l);f.appendChild(i);return f};jrow.appendChild(jmkNum("응답 제한 시간(ms)","temporalRecallJudgeTimeoutMs",8e3,1e3,6e4));jrow.appendChild(jmkNum("검토할 장면 수","temporalRecallJudgeCandidateLimit",6,1,30));nd.appendChild(jrow)}});panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const bTitle=document.createElement("div");bTitle.textContent="전체 로그 일괄 추출";bTitle.style.cssText="font-size:14px;color:#4a9;font-weight:bold;margin-bottom:8px;";nd.appendChild(bTitle);const bDesc=document.createElement("div");bDesc.textContent="긴 대화를 배치로 나눠 정리함. API 비용 큼. 초기 정리용.";bDesc.style.cssText="font-size:11px;color:#888;margin-bottom:10px;line-height:1.4;";nd.appendChild(bDesc);const bRow=document.createElement("div");bRow.style.cssText="display:flex;gap:12px;margin-bottom:8px;align-items:center;";const mkNum=(label,getter,setter,defaultVal)=>{const f=document.createElement("div");f.style.flex="1";const l=document.createElement("div");l.textContent=label;l.style.cssText="font-size:12px;color:#888;margin-bottom:4px;";const i=document.createElement("input");i.type="number";const cur=getter();i.value=cur!==undefined&&cur!==null?cur:defaultVal;i.style.cssText="width:100%;padding:6px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:12px;box-sizing:border-box;";const save=()=>{const v=parseInt(i.value);if(!isNaN(v)){setter(v);settings.save()}};i.oninput=save;i.onchange=save;f.appendChild(l);f.appendChild(i);return f};bRow.appendChild(mkNum("배치 크기(턴)",()=>settings.config.batchExtTurnsPerBatch,v=>settings.config.batchExtTurnsPerBatch=v,50));bRow.appendChild(mkNum("오버랩(턴)",()=>settings.config.batchExtOverlap,v=>settings.config.batchExtOverlap=v,5));bRow.appendChild(mkNum("재시도",()=>settings.config.batchExtMaxAttempts,v=>settings.config.batchExtMaxAttempts=v,3));nd.appendChild(bRow);const bBtn=document.createElement("button");bBtn.textContent="전체 일괄 추출 실행";bBtn.style.cssText="padding:8px 16px;font-size:12px;border-radius:4px;cursor:pointer;background:#258;color:#fff;border:none;font-weight:bold;width:100%;margin-top:6px;";const bStatus=document.createElement("div");bStatus.style.cssText="font-size:11px;color:#888;margin-top:6px;text-align:center;line-height:1.5;";bBtn.onclick=async()=>{if(!confirm("전체 로그를 배치로 분석함. API 비용 큼. 계속?"))return;settings.save();bBtn.disabled=true;const orig=bBtn.textContent;bBtn.textContent="실행 중...";bStatus.textContent="전체 로그 가져오는 중";bStatus.style.color="#4a9";const start=Date.now();try{const report=await _w.__LoreInj.runBatchExtract({turnsPerBatch:settings.config.batchExtTurnsPerBatch||50,overlap:settings.config.batchExtOverlap!==undefined?settings.config.batchExtOverlap:5,maxAttempts:settings.config.batchExtMaxAttempts||3,onProgress:ev=>{const sec=Math.floor((Date.now()-start)/1e3);if(ev.phase==="batch")bStatus.textContent="배치 "+ev.index+"/"+ev.total+" 처리 중 ("+sec+"초)"}});const sec=Math.floor((Date.now()-start)/1e3);let msg="완료 ("+sec+"초) — "+report.totalBatches+"개 배치 / 성공 "+report.ok+" / 빈 "+report.empty+" / 실패 "+report.failed+" / 병합 "+report.entriesAdded+"건";if(report.failed>0){msg+=" ⚠️ 실패 상세는 로그 탭";bStatus.style.color="#da8"}else{bStatus.style.color="#4a9"}bStatus.textContent=msg}catch(e){bStatus.textContent="실패 — "+(e.message||String(e)).slice(0,80);bStatus.style.color="#d66"}finally{bBtn.textContent=orig;bBtn.disabled=false}};nd.appendChild(bBtn);nd.appendChild(bStatus)}});panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const S="width:100%;padding:6px 8px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:12px;box-sizing:border-box;margin-bottom:8px;";nd.innerHTML='
지식 변환 (URL/텍스트 → 로어)
';const urlInp=document.createElement("input");urlInp.type="text";urlInp.placeholder="URL 입력";urlInp.style.cssText=S;nd.appendChild(urlInp);const nameInp=document.createElement("input");nameInp.type="text";nameInp.placeholder="팩 이름";nameInp.style.cssText=S;nd.appendChild(nameInp);const rDiv=document.createElement("div");rDiv.style.cssText="font-size:12px;color:#888;margin-top:8px;";const urlBtn=document.createElement("button");urlBtn.textContent="URL 변환";urlBtn.style.cssText="padding:8px 16px;font-size:12px;border-radius:4px;cursor:pointer;background:#285;color:#fff;border:none;font-weight:bold;";urlBtn.onclick=async()=>{if(!urlInp.value.trim()||!nameInp.value.trim()){alert("URL과 팩이름 필요.");return}urlBtn.disabled=true;urlBtn.textContent="변환중...";const startMs=Date.now();let phaseMsg="에리가 URL 본문 가져오는 중";const setBusy=(msg,color="#4a9")=>{phaseMsg=msg;rDiv.textContent=msg;rDiv.style.color=color;try{C.showStatusBadge(msg)}catch(_){}};const tick=setInterval(()=>{const sec=Math.floor((Date.now()-startMs)/1e3);setBusy(phaseMsg.replace(/\s*\(\d+초\)$/,"")+` (${sec}초)`)},1e3);setBusy(phaseMsg+" (0초)");try{const cnt=await C.importFromUrl(urlInp.value.trim(),nameInp.value.trim(),buildGenerationApiOpts({},{feature:"urlImport",chatKey:C.getCurrentChatId&&C.getCurrentChatId()||"global"}),{onProgress:ev=>{if(!ev)return;switch(ev.phase){case"fetch:start":setBusy("에리가 URL 본문 가져오는 중");break;case"fetch:try":{const methodLabel={"gm-direct":"GM 직접 요청",fetch:"브라우저 fetch",proxy:"공용 프록시"}[ev.method]||ev.method;const suffix=ev.method==="proxy"?` #${ev.attempt}/${ev.total}`:"";const sec=Math.max(1,Math.floor((ev.timeoutMs||15e3)/1e3));setBusy(`에리가 URL 가져오는 중 · ${methodLabel}${suffix} (최대 ${sec}초)`);break}case"fetch:done":setBusy(`에리가 URL 본문 받음 · ${((ev.bytes||0)/1024).toFixed(1)}KB`);break;case"parse":setBusy("에리가 HTML 정리 중");break;case"fetch:fail":setBusy("URL 가져오기 실패 — 모든 경로 컷","#d66");break;case"chunk":setBusy(`에리가 URL 내용을 로어로 변환 중: 청크 ${ev.chunk}/${ev.total} · 시도 ${ev.attempt}/${ev.maxAttempts}`);break}}});const rpt=C.__lastImportReport;let msg="✅ "+cnt+"개 생성";if(rpt){if(rpt.failed>0){const firstErr=(rpt.chunkResults.find(r=>r.status==="failed")||{}).error||"";msg+=" ⚠️ 청크 "+rpt.failed+"/"+rpt.chunks+" 실패: "+firstErr.slice(0,80)}else if(cnt===0&&rpt.empty===rpt.chunks){msg="⚠️ 0개 — 모든 청크("+rpt.chunks+"개)에서 AI가 추출 가능한 내용 없다고 판단"}}rDiv.textContent=msg;rDiv.style.color=cnt>0?"#4a9":"#da8";if(cnt>0)await setPackEnabled(nameInp.value.trim(),true)}catch(e){rDiv.textContent="❌ "+(e.message||String(e));rDiv.style.color="#d66"}finally{clearInterval(tick);try{C.hideStatusBadge()}catch(_){}urlBtn.textContent="URL 변환";urlBtn.disabled=false}};nd.appendChild(urlBtn);nd.appendChild(rDiv);const t2=document.createElement("div");t2.innerHTML='
텍스트 → 로어 팩
';nd.appendChild(t2);const ta=document.createElement("textarea");ta.placeholder="설정, 소설 텍스트 등";ta.style.cssText=S+"height:100px;resize:vertical;";nd.appendChild(ta);const nameInp2=document.createElement("input");nameInp2.type="text";nameInp2.placeholder="팩 이름";nameInp2.style.cssText=S;nd.appendChild(nameInp2);const rDiv2=document.createElement("div");rDiv2.style.cssText="font-size:12px;color:#888;margin-top:8px;";const tBtn=document.createElement("button");tBtn.textContent="텍스트 변환";tBtn.style.cssText="padding:8px 16px;font-size:12px;border-radius:4px;cursor:pointer;background:#285;color:#fff;border:none;font-weight:bold;";tBtn.onclick=async()=>{if(!ta.value.trim()||!nameInp2.value.trim()){alert("입력값 필요.");return}tBtn.disabled=true;tBtn.textContent="변환중...";const startMs=Date.now();let phaseMsg="에리가 텍스트를 로어로 변환 중";const setBusy=(msg,color="#4a9")=>{phaseMsg=msg;rDiv2.textContent=msg;rDiv2.style.color=color;try{C.showStatusBadge(msg)}catch(_){}};const tick=setInterval(()=>{const sec=Math.floor((Date.now()-startMs)/1e3);setBusy(phaseMsg.replace(/\s*\(\d+초\)$/,"")+` (${sec}초)`)},1e3);setBusy(phaseMsg+" (0초)");try{const cnt=await C.importFromText(ta.value.trim(),nameInp2.value.trim(),buildGenerationApiOpts({},{feature:"textImport",chatKey:C.getCurrentChatId&&C.getCurrentChatId()||"global"}),{onProgress:ev=>{if(ev&&ev.phase==="chunk"){setBusy(`에리가 텍스트를 로어로 변환 중: 청크 ${ev.chunk}/${ev.total} · 시도 ${ev.attempt}/${ev.maxAttempts}`)}}});const rpt=C.__lastImportReport;let msg="✅ "+cnt+"개 생성";if(rpt){if(rpt.failed>0){const firstErr=(rpt.chunkResults.find(r=>r.status==="failed")||{}).error||"";msg+=" ⚠️ 청크 "+rpt.failed+"/"+rpt.chunks+" 실패: "+firstErr.slice(0,80)}else if(cnt===0&&rpt.empty===rpt.chunks){msg="⚠️ 0개 — 모든 청크("+rpt.chunks+"개)에서 AI가 추출 가능한 내용 없다고 판단"}}rDiv2.textContent=msg;rDiv2.style.color=cnt>0?"#4a9":"#da8";if(cnt>0)await setPackEnabled(nameInp2.value.trim(),true)}catch(e){rDiv2.textContent="❌ "+(e.message||String(e));rDiv2.style.color="#d66"}finally{clearInterval(tick);try{C.hideStatusBadge()}catch(_){}tBtn.textContent="텍스트 변환";tBtn.disabled=false}};nd.appendChild(tBtn);nd.appendChild(rDiv2)}})},"추출/변환 설정")})});_w.__LoreInj.__subExtractLoaded=true})();(async function(){const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const _ls=_w.localStorage;const R=_w.__LoreRefiner;const deadline=Date.now()+15e3;while(!(_w.__LoreInj&&_w.__LoreInj.__settingsLoaded)&&Date.now()setTimeout(r,50));if(_w.__LoreInj.__subRefinerLoaded)return;const{C:C,settings:settings}=_w.__LoreInj;_w.__LoreInj.registerSubMenu=_w.__LoreInj.registerSubMenu||function(){};_w.__LoreInj.registerSubMenu("refiner",function(modal){modal.createSubMenu("AI 응답 교정",m=>{m.replaceContentPanel(async panel=>{if(!R){panel.addText("Refiner 라이브러리 없음.");return}panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const t=document.createElement("div");t.textContent="수동 검수";t.style.cssText="font-size:13px;color:#4a9;font-weight:bold;margin-bottom:4px;";nd.appendChild(t);const d=document.createElement("div");d.textContent="마지막 AI 응답을 지금 즉시 재검수. 이미 처리된 응답도 다시 돌릴 수 있음.";d.style.cssText="font-size:11px;color:#888;margin-bottom:8px;line-height:1.4;";nd.appendChild(d);const btnBox=document.createElement("div");btnBox.style.cssText="position:relative;";const btn=document.createElement("button");btn.textContent="최근 AI 응답 재검수";btn.style.cssText="width:100%;padding:10px;background:#258;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:12px;font-weight:bold;";const statusLine=document.createElement("div");statusLine.style.cssText="font-size:11px;margin-top:6px;text-align:center;line-height:1.4;min-height:16px;display:flex;align-items:center;justify-content:center;gap:6px;color:#888;";btnBox.appendChild(btn);btnBox.appendChild(statusLine);nd.appendChild(btnBox);if(!_w.__loreRefinerPulseCss){_w.__loreRefinerPulseCss=true;const st=document.createElement("style");st.textContent="@keyframes lore-refiner-pulse{0%,100%{opacity:1}50%{opacity:.3}}";document.head.appendChild(st)}const makeDot=()=>{const d=document.createElement("span");d.style.cssText="display:inline-block;width:8px;height:8px;border-radius:50%;background:#4a9;animation:lore-refiner-pulse 1s infinite;";return d};btn.onclick=async()=>{if(!R.manualRefine){alert("Refiner 버전 낮음. Tampermonkey에서 스크립트 수동 업데이트 필요.");return}const cid=C.getCurrentChatId();if(!cid){alert("채팅방 감지 실패.");return}btn.disabled=true;const orig=btn.textContent;btn.textContent="검수 실행 중";statusLine.innerHTML="";statusLine.appendChild(makeDot());const txt=document.createElement("span");txt.textContent="대상 탐색";statusLine.appendChild(txt);statusLine.style.color="#4a9";const start=Date.now();const phases=["대상 탐색","로어 수집","메모리 수집","AI 호출 중","반영 중"];let phaseIdx=0;const tick=setInterval(()=>{if(phaseIdx{statusLine.textContent="";statusLine.style.color="#888"},3e3)}catch(e){clearInterval(tick);statusLine.innerHTML="";statusLine.textContent="실패: "+String(e.message||e).slice(0,50);statusLine.style.color="#d66"}finally{btn.textContent=orig;btn.disabled=false}}}});panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);nd.appendChild(C.createToggleRow("응답 교정 켜기","AI 응답 시 로어 기반 자동 검수.",settings.config.refinerEnabled,v=>{settings.config.refinerEnabled=v;settings.save();if(v&&R.setNeedsWarmup)R.setNeedsWarmup()}));nd.appendChild(C.createToggleRow("자동 반영 (팝업 없음)","검수 결과를 팝업 없이 즉시 적용.",settings.config.refinerAutoMode,v=>{settings.config.refinerAutoMode=v;settings.save()}));nd.appendChild(C.createToggleRow("상태 배지 표시","진행 상태를 화면 우측에 띄움. 모바일에서 겹치면 끄기.",settings.config.statusBadgeEnabled!==false,v=>{settings.config.statusBadgeEnabled=v;settings.save();if(!v&&C.hideStatusBadge)C.hideStatusBadge()}));const live=document.createElement("div");live.style.cssText="font-size:11px;color:#888;margin:8px 0 12px;padding:8px;border:1px solid #333;border-radius:4px;background:#111;line-height:1.4;";const renderLive=()=>{const st=R.getRefinerState?R.getRefinerState():null;if(!st){live.textContent="상태: 대기";return}const age=st.at?Math.max(0,Math.floor((Date.now()-st.at)/1e3)):0;live.textContent="상태: "+(st.state||"idle")+(st.detail?" · "+st.detail:"")+" · "+age+"초 전"};renderLive();try{if(R.__refinerStatusUiTimer)clearInterval(R.__refinerStatusUiTimer)}catch(_){}R.__refinerStatusUiTimer=setInterval(renderLive,1e3);nd.appendChild(live);const S="width:100%;padding:6px 8px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:12px;box-sizing:border-box;margin-bottom:8px;";const modeWrap=document.createElement("div");modeWrap.style.cssText="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;";const modeLbl=document.createElement("div");modeLbl.textContent="로어 검색 모드";modeLbl.style.cssText="font-size:13px;color:#ccc;font-weight:bold;";const modeSel=document.createElement("select");modeSel.style.cssText="width:160px;padding:4px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:12px;";[{v:"matchedOnly",l:"키워드 매칭만"},{v:"semantic",l:"임베딩 (의미 검색)"}].forEach(o=>{const opt=document.createElement("option");opt.value=o.v;opt.textContent=o.l;modeSel.appendChild(opt)});modeSel.value=settings.config.refinerLoreMode||"matchedOnly";modeSel.onchange=()=>{settings.config.refinerLoreMode=modeSel.value;settings.save()};modeWrap.appendChild(modeLbl);modeWrap.appendChild(modeSel);nd.appendChild(modeWrap);const tplWrap=document.createElement("div");tplWrap.style.cssText="margin-bottom:8px;padding-top:8px;border-top:1px dashed #333;";const tplLbl=document.createElement("div");tplLbl.textContent="검수 템플릿 선택";tplLbl.style.cssText="font-size:13px;color:#ccc;font-weight:bold;margin-bottom:4px;";const tplSel=document.createElement("select");tplSel.style.cssText=S;const tplDesc=document.createElement("div");tplDesc.style.cssText="font-size:11px;color:#888;margin-bottom:8px;line-height:1.4;";const customOpt=document.createElement("option");customOpt.value="custom";customOpt.textContent="직접 입력 (커스텀)";tplSel.appendChild(customOpt);if(R.TEMPLATES){Object.entries(R.TEMPLATES).forEach(([k,t])=>{const opt=document.createElement("option");opt.value=k;opt.textContent=t.name;tplSel.appendChild(opt)})}if(R.TOPICS){const dynOpt=document.createElement("option");dynOpt.value="dynamic";dynOpt.textContent="주제별 선택 (체크박스)";tplSel.appendChild(dynOpt)}tplWrap.appendChild(tplLbl);tplWrap.appendChild(tplSel);tplWrap.appendChild(tplDesc);nd.appendChild(tplWrap);const topicsWrap=document.createElement("div");topicsWrap.style.cssText="display:none;margin-bottom:12px;padding:10px;background:#111;border:1px solid #333;border-radius:4px;";const topicsHdr=document.createElement("div");topicsHdr.textContent="검수 주제 선택 — 호칭은 최신 안정 상태 기준으로만 검사";topicsHdr.style.cssText="font-size:12px;color:#4a9;font-weight:bold;margin-bottom:8px;padding-bottom:6px;border-bottom:1px dashed #333;";topicsWrap.appendChild(topicsHdr);const topicsBody=document.createElement("div");topicsWrap.appendChild(topicsBody);nd.appendChild(topicsWrap);const rebuildDynamicPrompt=()=>{const built=R.buildDynamicPrompt(settings.config.refinerTopics||{});settings.config.refinerCustomPrompt=built;ta.value=built;settings.save()};const renderTopics=()=>{if(!settings.config.refinerTopics){const def={};Object.keys(R.TOPICS||{}).forEach(k=>def[k]=true);settings.config.refinerTopics=def}topicsBody.innerHTML="";let curGroup=null;Object.entries(R.TOPICS||{}).forEach(([k,meta])=>{if(meta.group!==curGroup){curGroup=meta.group;const gh=document.createElement("div");gh.textContent=curGroup==="logic"?"— 모순 검수 —":"— 끊김 복구 —";gh.style.cssText="font-size:10px;color:#888;margin:6px 0 4px;font-weight:bold;";topicsBody.appendChild(gh)}const row=document.createElement("label");row.style.cssText="display:flex;align-items:flex-start;gap:8px;padding:5px 0;cursor:pointer;";const cb=document.createElement("input");cb.type="checkbox";cb.checked=!!settings.config.refinerTopics[k];cb.style.cssText="margin-top:3px;flex-shrink:0;accent-color:#4a9;";const txt=document.createElement("div");txt.style.flex="1";const lbl=document.createElement("div");lbl.textContent=meta.label;lbl.style.cssText="font-size:12px;color:#ccc;font-weight:bold;";const dsc=document.createElement("div");dsc.textContent=meta.desc;dsc.style.cssText="font-size:10px;color:#888;line-height:1.4;";txt.appendChild(lbl);txt.appendChild(dsc);cb.onchange=()=>{settings.config.refinerTopics[k]=cb.checked;rebuildDynamicPrompt()};row.appendChild(cb);row.appendChild(txt);topicsBody.appendChild(row)})};const wrap=document.createElement("div");wrap.style.cssText="display:flex;justify-content:space-between;align-items:center;gap:10px;width:100%;margin-bottom:12px;";const left=document.createElement("div");left.style.cssText="display:flex;flex-direction:column;gap:4px;flex:1;";const t=document.createElement("div");t.textContent="참조 대화 턴 수";t.style.cssText="font-size:13px;color:#ccc;font-weight:bold;";left.appendChild(t);const right=document.createElement("div");const inp=document.createElement("input");inp.type="number";inp.value=settings.config.refinerContextTurns!==undefined?settings.config.refinerContextTurns:1;inp.min=0;inp.max=20;inp.style.cssText="width:60px;padding:6px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:12px;text-align:center;";inp.onchange=()=>{settings.config.refinerContextTurns=parseInt(inp.value)||0;settings.save()};right.appendChild(inp);wrap.appendChild(left);wrap.appendChild(right);nd.appendChild(wrap);const tLbl2=document.createElement("div");tLbl2.textContent="프롬프트 미리보기";tLbl2.style.cssText="font-size:11px;color:#999;margin-bottom:4px;";nd.appendChild(tLbl2);const ta=document.createElement("textarea");ta.value=settings.config.refinerCustomPrompt;ta.style.cssText=S+"height:200px;font-family:monospace;resize:vertical;";ta.readOnly=true;nd.appendChild(ta);if(R.TEMPLATES){tplSel.onchange=()=>{const val=tplSel.value;if(val==="dynamic"){settings.config.refinerUseDynamic=true;topicsWrap.style.display="block";renderTopics();rebuildDynamicPrompt();tplDesc.textContent="체크한 검수 주제만 AI 프롬프트에 들어감."}else if(val!=="custom"&&R.TEMPLATES[val]){settings.config.refinerUseDynamic=false;topicsWrap.style.display="none";const tpl=R.TEMPLATES[val];ta.value=tpl.prompt;settings.config.refinerCustomPrompt=tpl.prompt;inp.value=tpl.turnHint;settings.config.refinerContextTurns=tpl.turnHint;tplDesc.textContent=tpl.desc;settings.save()}else{settings.config.refinerUseDynamic=false;topicsWrap.style.display="none";tplDesc.textContent="직접 작성한 프롬프트 사용 중. 내용 수정은 프롬프트 관리에서 함.";settings.save()}};const normalize=s=>(s||"").trim().replace(/\s+/g," ");let matched="custom";const curNorm=normalize(settings.config.refinerCustomPrompt);Object.entries(R.TEMPLATES).forEach(([k,t])=>{if(normalize(t.prompt)===curNorm)matched=k});if(matched==="custom"&&!settings.config.refinerCustomPrompt&&R.TOPICS&&R.buildDynamicPrompt){matched="dynamic";settings.config.refinerUseDynamic=true;const def={};Object.keys(R.TOPICS).forEach(k=>def[k]=true);settings.config.refinerTopics=def;settings.config.refinerCustomPrompt=R.buildDynamicPrompt(def);settings.config.refinerContextTurns=1;ta.value=settings.config.refinerCustomPrompt;inp.value=1;settings.save()}if(settings.config.refinerUseDynamic&&R.TOPICS)matched="dynamic";tplSel.value=matched;if(matched==="dynamic"){topicsWrap.style.display="block";renderTopics();tplDesc.textContent="체크한 검수 주제만 AI 프롬프트에 들어감."}else if(matched!=="custom")tplDesc.textContent=R.TEMPLATES[matched].desc;else tplDesc.textContent="직접 작성한 프롬프트 사용 중. 내용 수정은 프롬프트 관리에서 함."}const clearFpBtn=document.createElement("button");clearFpBtn.textContent="처리 기록 큐 초기화";clearFpBtn.style.cssText="width:100%;padding:8px;margin-top:12px;background:#654;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:12px;";clearFpBtn.onclick=()=>{R.clearProcessed();alert("기록 삭제됨")};nd.appendChild(clearFpBtn)}})},"응답 교정 설정")})});_w.__LoreInj.__subRefinerLoaded=true})();(async function(){const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const _ls=_w.localStorage;const deadline=Date.now()+15e3;while(!(_w.__LoreInj&&_w.__LoreInj.__settingsLoaded)&&Date.now()setTimeout(r,50));if(_w.__LoreInj.__subLogLoaded)return;const{C:C,settings:settings,getChatKey:getChatKey,getInjLog:getInjLog,getExtLog:getExtLog,clearInjLog:clearInjLog,clearExtLog:clearExtLog}=_w.__LoreInj;_w.__LoreInj.registerSubMenu=_w.__LoreInj.registerSubMenu||function(){};_w.__LoreInj.registerSubMenu("log",function(modal){modal.createSubMenu("실행 로그",m=>{const renderLogs=panel=>{const chatKey=getChatKey();const escHtml=v=>String(v==null?"":v).replace(/[&<>"']/g,ch=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[ch]));const shortList=(arr,limit=3)=>(Array.isArray(arr)?arr:[]).map(escHtml).filter(Boolean).slice(0,limit).join(", ");const fmtCostUsd=n=>"$"+(Math.abs(Number(n))<.01?Number(n).toFixed(5):Number(n).toFixed(4));const idFromUrl=url=>{const s=String(url||"");const m=s.match(/\/(?:episodes|chats|c)\/([a-f0-9]+)/i);return m?m[1]:""};const chatLabelMap=(()=>{const out={};const urlMap=settings.config.urlAutoExtPacks||{};for(const[url,packName]of Object.entries(urlMap)){const id=idFromUrl(url);if(!id||!packName)continue;out["chat:"+id]=packName;out[id]=packName}return out})();const displayChatLabel=key=>chatLabelMap[key]||(String(key||"").startsWith("chat:")?String(key).slice(5,13):String(key||"global"));const renderCostLine=i=>{const parts=[];if(i.model)parts.push(escHtml(i.model));if(i.elapsedMs)parts.push((i.elapsedMs/1e3).toFixed(2)+"s");if(i.cost){const c=i.cost;if(c.isBatchAggregate){if(c.usd!=null)parts.push(fmtCostUsd(c.usd)+(c.hasUnknown?' (일부 제외)':"")+(c.estimated?' ~':""));else if(c.hasUnknown)parts.push('')}else{if(c.usd!=null)parts.push(fmtCostUsd(c.usd)+(c.estimated?' ~':""));else parts.push('')}}return parts.length?'
'+parts.join(" · ")+"":""};const iLog=getInjLog(chatKey);const eLog=getExtLog(chatKey);const cLog=JSON.parse(_ls.getItem("lore-contradictions")||"[]");const rLog=settings.config.urlRefinerLogs?.[chatKey]||[];const makeLogBox=(title,color,items,renderer)=>{panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const hRow=document.createElement("div");hRow.style.cssText="display:flex;justify-content:space-between;align-items:center;padding-bottom:8px;border-bottom:1px solid #333;margin-bottom:8px;cursor:pointer;";const leftWrap=document.createElement("div");leftWrap.style.cssText="display:flex;align-items:center;gap:8px;flex:1;";const arrow=document.createElement("span");arrow.textContent="▶";arrow.style.cssText="font-size:11px;color:#888;transition:transform .15s;";const t=document.createElement("div");t.textContent=`${title} (${items.length})`;t.style.cssText=`font-size:14px;color:${color};font-weight:bold;`;leftWrap.appendChild(arrow);leftWrap.appendChild(t);hRow.appendChild(leftWrap);if(items.length>0){const btn=document.createElement("button");btn.textContent="초기화";btn.style.cssText="padding:4px 10px;font-size:11px;border-radius:4px;cursor:pointer;background:transparent;color:#d66;border:1px solid #d66;";btn.onclick=ev=>{ev.stopPropagation();if(confirm("삭제?")){renderer(true);m.replaceContentPanel(renderLogs,"로그")}};hRow.appendChild(btn)}nd.appendChild(hRow);const listCon=document.createElement("div");listCon.style.display="none";if(!items.length){listCon.appendChild(Object.assign(document.createElement("div"),{textContent:"기록 없음.",style:"font-size:12px;color:#888;"}))}else{items.slice(0,20).forEach(i=>renderer(false,i,listCon))}nd.appendChild(listCon);hRow.onclick=()=>{const isOpen=listCon.style.display!=="none";listCon.style.display=isOpen?"none":"block";arrow.textContent=isOpen?"▶":"▼"}}})};const renderCostBox=panel=>{panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);let period="all";let detailsOpen=false;const headerRow=document.createElement("div");headerRow.style.cssText="display:flex;justify-content:space-between;align-items:center;padding-bottom:8px;border-bottom:1px solid #333;margin-bottom:8px;flex-wrap:wrap;gap:8px;";const leftWrap=document.createElement("div");leftWrap.style.cssText="display:flex;align-items:center;gap:6px;flex:1;cursor:pointer;min-width:0;flex-wrap:wrap;";const arrow=document.createElement("span");arrow.textContent="▶";arrow.style.cssText="font-size:11px;color:#888;transition:transform .15s;";const titleSpan=document.createElement("span");titleSpan.textContent="API 비용";titleSpan.style.cssText="font-size:14px;color:#cc6;font-weight:bold;";const totalSpan=document.createElement("span");totalSpan.style.cssText="font-size:14px;color:#fff;font-weight:bold;";const subSpan=document.createElement("span");subSpan.style.cssText="font-size:11px;color:#888;";leftWrap.append(arrow,titleSpan,totalSpan,subSpan);const rightWrap=document.createElement("div");rightWrap.style.cssText="display:flex;align-items:center;gap:4px;flex-wrap:wrap;";const periods=[{k:"24h",l:"24h"},{k:"7d",l:"7d"},{k:"30d",l:"30d"},{k:"all",l:"전체"}];const periodBtns={};periods.forEach(p=>{const b=document.createElement("button");b.textContent=p.l;b.style.cssText="padding:3px 8px;font-size:10px;border-radius:3px;cursor:pointer;background:transparent;border:1px solid #444;color:#aaa;";b.onclick=ev=>{ev.stopPropagation();period=p.k;refresh()};periodBtns[p.k]=b;rightWrap.appendChild(b)});const clearBtn=document.createElement("button");clearBtn.textContent="초기화";clearBtn.style.cssText="padding:3px 8px;font-size:10px;border-radius:3px;cursor:pointer;background:transparent;color:#d66;border:1px solid #d66;margin-left:4px;";clearBtn.onclick=ev=>{ev.stopPropagation();if(confirm("전체 API 비용 기록(이벤트 + 전체 기간 누적)을 삭제하시겠습니까?")){if(C.clearCostEvents)C.clearCostEvents();if(C.clearCumulativeCost)C.clearCumulativeCost();refresh()}};rightWrap.appendChild(clearBtn);headerRow.append(leftWrap,rightWrap);nd.appendChild(headerRow);const detailsCon=document.createElement("div");detailsCon.style.display="none";nd.appendChild(detailsCon);leftWrap.onclick=()=>{detailsOpen=!detailsOpen;detailsCon.style.display=detailsOpen?"block":"none";arrow.textContent=detailsOpen?"▼":"▶"};const fmtUsd=n=>"$"+(Math.abs(n)<.01?n.toFixed(5):n.toFixed(4));const fmtTok=n=>n>=1e3?(n/1e3).toFixed(1)+"k":String(n);const refresh=()=>{Object.entries(periodBtns).forEach(([k,b])=>{if(k===period){b.style.background="#cc6";b.style.color="#222";b.style.borderColor="#cc6"}else{b.style.background="transparent";b.style.color="#aaa";b.style.borderColor="#444"}});const all=C.getCostEvents&&C.getCostEvents()||[];const now=Date.now();const ms={"24h":864e5,"7d":7*864e5,"30d":30*864e5}[period];const events=ms?all.filter(e=>now-(e.ts||0)<=ms):all;let totalUsd=0,totalCalls=events.length,unknownCalls=0,estCalls=0;const byModel={},byFeature={},byChat={};const bump=(bucket,key,e)=>{if(!bucket[key])bucket[key]={calls:0,usd:0,unknown:0,est:0,inTok:0,outTok:0};const b=bucket[key];b.calls++;b.inTok+=Number(e.inTok)||0;b.outTok+=Number(e.outTok)||0;if(e.estimated)b.est++;if(e.unknown||e.usd==null)b.unknown++;else b.usd+=Number(e.usd)||0};for(const e of events){if(e.unknown||e.usd==null)unknownCalls++;else totalUsd+=Number(e.usd)||0;if(e.estimated)estCalls++;bump(byModel,e.model||"?",e);bump(byFeature,e.feature||"?",e);bump(byChat,e.chatKey||"global",e)}const cumul=period==="all"&&C.getCumulativeCost?C.getCumulativeCost():null;const cumulOverride=!!(cumul&&Number(cumul.count)>0);const truncated=cumulOverride&&Number(cumul.count)>events.length;if(cumulOverride){totalUsd=Number(cumul.usd)||0;totalCalls=Number(cumul.count)||0;unknownCalls=Number(cumul.unknownCount)||0;estCalls=Number(cumul.estimatedCount)||0}totalSpan.textContent=" "+fmtUsd(totalUsd);const subParts=[totalCalls+"회"];if(unknownCalls)subParts.push("직접입력 "+unknownCalls+"회 제외");if(estCalls)subParts.push("추정 "+estCalls);subSpan.textContent=" ("+subParts.join(" · ")+")";const featureLabels={autoExtract:"자동추출",batchExtract:"배치추출",temporalExtract:"시간축추출",urlImport:"URL 가져오기",textImport:"텍스트 변환",refine:"교정",rerank:"리랭킹",judge:"판단 AI",embed:"임베딩",apiTest:"API 테스트"};const renderTable=(title,bucket,keyLabel,labelMap)=>{const rows=Object.entries(bucket).sort((a,b)=>b[1].usd-a[1].usd||b[1].calls-a[1].calls);if(!rows.length)return"";let html='
'+title+"
";html+='';for(const[k,v]of rows){const usdCell=v.unknown===v.calls?'':fmtUsd(v.usd)+(v.unknown?' (+'+v.unknown+" 제외)":"")+(v.est?' ~':"");const labelText=labelMap&&labelMap[k]?escHtml(labelMap[k]):escHtml(k);html+='"}html+="
'+keyLabel+'호출입력출력USD
'+labelText+''+v.calls+''+fmtTok(v.inTok)+''+fmtTok(v.outTok)+''+usdCell+"
";return html};if(!events.length){if(cumulOverride){detailsCon.innerHTML='
최근 이벤트 기록 없음. 헤더 합계는 전체 기간 누적치(보존).
'}else{detailsCon.innerHTML='
기록 없음.
'}}else{let html="";if(truncated){html+='
아래 상세는 최근 이벤트 '+events.length+"건 (전체 "+totalCalls+"건 중 FIFO 5000 초과분은 헤더 누적치에만 반영).
"}html+=renderTable("모델별",byModel,"모델",null);html+=renderTable("기능별",byFeature,"기능",featureLabels);const chatLabels={};Object.keys(byChat).forEach(k=>{chatLabels[k]=displayChatLabel(k)});html+=renderTable("로어팩별",byChat,"로어팩",chatLabels);html+='
~ usageMetadata 없어 char/4 추정. — 가격 미등록 모델(직접입력 등) 호출수만 표기, USD 합산 제외.
';detailsCon.innerHTML=html}};refresh()}})};renderCostBox(panel);makeLogBox("주입 기록","#4a9",iLog,(clear,i,nd)=>{if(clear){clearInjLog(chatKey);return}const r=document.createElement("div");r.style.cssText="margin-bottom:6px;border-bottom:1px dashed #222;padding-bottom:4px;";let h=`${i.turn}턴 (${i.time})`;if(i.totalChars&&i.maxChars){const pct=Math.round(i.totalChars/i.maxChars*100);const color=pct>80?"#d66":pct>60?"#da8":"#4a9";h+=`
총 ${i.totalChars}/${i.maxChars}자 (${pct}%)`;h+=` 유저 ${i.userInputChars||0} + 주입 ${i.injectedChars||0}`}if(i.sections){const s=i.sections;const parts=[];if(s.scene)parts.push(`씬 ${s.scene}`);if(s.firstEnc)parts.push(`첫만남 ${s.firstEnc}`);if(s.reunion)parts.push(`재회 ${s.reunion}`);if(s.honor)parts.push(`호칭 ${s.honor}`);if(s.temporalRecall)parts.push(`시간축 ${s.temporalRecall}`);if(s.lore)parts.push(`로어 ${s.lore}`);if(parts.length)h+=`
내역: ${parts.join(" / ")}`}if(i.temporalInjection&&(i.temporalInjection.source||i.temporalInjection.chars||(i.temporalInjection.eventIds||[]).length)){const ti=i.temporalInjection;if(ti.source&&ti.source!=="none"){const src=ti.source==="judge"?"판단AI":ti.source==="deterministic"?"규칙기반":escHtml(ti.source);const ids=shortList(ti.eventIds,4);h+=`
시간축: ${src} / ${escHtml(ti.mode||"")} / ${escHtml(ti.level||"")} / ${ti.chars||0}자${ids?" / ID "+ids:""}`}const actions=Array.isArray(ti.compressionActions)?ti.compressionActions:[];const drops=Array.isArray(ti.droppedEventIds)?ti.droppedEventIds:[];if(actions.length||drops.length){const aTxt=actions.slice(0,3).map(a=>`${escHtml(a.action||"")} ${escHtml(a.from||"")}→${escHtml(a.to||"")}`).join(", ");const dTxt=shortList(drops,3);h+=`
압축: ${aTxt||"없음"}${dTxt?" / 드롭 "+dTxt:""}`}}if(i.temporalJudge&&(i.temporalJudge.reason||i.temporalJudge.mode||i.temporalJudge.error)){const tj=i.temporalJudge;const state=tj.error?"오류 "+escHtml(tj.error):`${tj.recall?"회상":"미회상"} ${escHtml(tj.mode||"")}`;h+=`
판단: ${state}${tj.reason?" — "+escHtml(tj.reason):""}`;const _tjQ=tj.query||{};const _tjQParts=[];if(Array.isArray(_tjQ.participants)&&_tjQ.participants.length)_tjQParts.push("인물 "+shortList(_tjQ.participants,3));if(Array.isArray(_tjQ.actions)&&_tjQ.actions.length)_tjQParts.push("행동 "+shortList(_tjQ.actions,3));if(Array.isArray(_tjQ.locations)&&_tjQ.locations.length)_tjQParts.push("장소 "+shortList(_tjQ.locations,2));if(_tjQParts.length)h+=`
쿼리: ${_tjQParts.join(" / ")}`}{const optParts=[];if(i.bundled)optParts.push(`번들 ${i.bundled}`);if(i.deltaSkipped)optParts.push(`스킵 ${i.deltaSkipped}`);if(optParts.length)h+=`
최적화: ${optParts.join(" / ")}`}if(i.budget)h+=`
로어예산 ${i.used||0}/${i.budget}${i.level?" ("+i.level+")":""}`;h+=`
${i.count>0?i.count+"개: "+i.matched.join(", "):i.note||"매치없음"}`;r.innerHTML=h;nd.appendChild(r)});makeLogBox("추출 기록","#da8",eLog,(clear,i,nd)=>{if(clear)clearExtLog(chatKey);else{const r=document.createElement("div");r.style.cssText="margin-bottom:6px;border-bottom:1px dashed #222;padding-bottom:4px;font-size:12px;";r.innerHTML=`[${i.time}] ${i.isManual?"수동":"자동"} - ${i.status} (${i.count||0}개)${i.api?`
API: ${i.api.status}${i.api.error?" | "+i.api.error:""}`:""}${renderCostLine(i)}`;nd.appendChild(r)}});makeLogBox("교정 기록","#ea5",rLog,(clear,i,nd)=>{if(clear){settings.config.urlRefinerLogs[chatKey]=[];settings.save()}else{const r=document.createElement("div");r.style.cssText="margin-bottom:6px;border-bottom:1px dashed #222;padding-bottom:4px;font-size:12px;";r.innerHTML=`[${i.time}] ${i.isPass?"통과":i.isError?"에러":"교정됨"}${i.reason?`
${i.reason}`:""}${renderCostLine(i)}`;nd.appendChild(r)}});makeLogBox("모순 기록","#d96",cLog,(clear,i,nd)=>{if(clear)_ls.removeItem("lore-contradictions");else{const r=document.createElement("div");r.style.cssText="margin-bottom:6px;border-bottom:1px dashed #222;padding-bottom:4px;font-size:12px;";r.innerHTML=`${i.name}
"${i.oldStatus}" → "${i.newStatus}"
${new Date(i.time).toLocaleString()} (~${i.turn}턴)`;nd.appendChild(r)}})};m.replaceContentPanel(renderLogs,"로그 조회")})});_w.__LoreInj.__subLogLoaded=true})();(async function(){const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const _ls=_w.localStorage;const deadline=Date.now()+15e3;while(!(_w.__LoreInj&&_w.__LoreInj.__settingsLoaded)&&Date.now()setTimeout(r,50));if(_w.__LoreInj.__subSessionLoaded)return;const{C:C,db:db,settings:settings,getChatKey:getChatKey,getTurnCounter:getTurnCounter,getCooldownMap:getCooldownMap,isEntryEnabledForUrl:isEntryEnabledForUrl}=_w.__LoreInj;_w.__LoreInj.registerSubMenu=_w.__LoreInj.registerSubMenu||function(){};_w.__LoreInj.registerSubMenu("session",function(modal){modal.createSubMenu("세션 상태 관리",m=>{const renderSessionStatus=async panel=>{const chatKey=getChatKey();const turnCounter=getTurnCounter(chatKey);const cMap=getCooldownMap(chatKey);const urlPacks=_w.__LoreInj.getActivePacksForUrl?_w.__LoreInj.getActivePacksForUrl(C.getCurUrl()):settings.config.urlPacks?.[C.getCurUrl()]||[];let allEntries=[];if(urlPacks.length>0){const all=await db.entries.toArray();allEntries=all.filter(e=>urlPacks.includes(e.packName)&&isEntryEnabledForUrl(e))}panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const headerRow=document.createElement("div");headerRow.style.cssText="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;border-bottom:1px solid #333;padding-bottom:8px;";const title=document.createElement("div");title.textContent=`현재 세션 상태 (턴: ${turnCounter})`;title.style.cssText="font-size:14px;color:#4a9;font-weight:bold;";const clearAllBtn=document.createElement("button");clearAllBtn.textContent="세션 전체 초기화";clearAllBtn.style.cssText="padding:6px 12px;font-size:11px;border-radius:4px;cursor:pointer;background:#833;color:#fff;border:none;font-weight:bold;";clearAllBtn.onclick=async()=>{if(!confirm("이 채팅방의 모든 쿨다운, 시간감쇠(망각) 점수 및 턴 수를 초기화할 것?"))return;try{const curUrl=C.getCurUrl();if(settings.config.urlCooldownMaps)delete settings.config.urlCooldownMaps[chatKey];const lastMention=JSON.parse(_ls.getItem("lore-last-mention")||"{}");delete lastMention[chatKey];_ls.setItem("lore-last-mention",JSON.stringify(lastMention));const turnCounters=JSON.parse(_ls.getItem("lore-turn-counters")||"{}");delete turnCounters[chatKey];_ls.setItem("lore-turn-counters",JSON.stringify(turnCounters));_ls.removeItem("lore-recent-injections:"+chatKey);_ls.removeItem("lore-fe-recent-"+chatKey);const packs=_w.__LoreInj.getActivePacksForUrl?_w.__LoreInj.getActivePacksForUrl(curUrl):settings.config.urlPacks?.[curUrl]||[];if(packs.length){const entries=await db.entries.where("packName").anyOf(packs).toArray();for(const e of entries){try{await db.entries.update(e.id,{lastMentionedTurn:0})}catch(_){}}}try{if(db.workingMemory)await db.workingMemory.delete(curUrl)}catch(_){}settings.save();m.replaceContentPanel(renderSessionStatus,"세션 상태 관리")}catch(e){console.error("[LoreInj:session] 전체 초기화 실패:",e);alert("세션 초기화 실패: "+(e.message||e))}};headerRow.appendChild(title);headerRow.appendChild(clearAllBtn);nd.appendChild(headerRow);const mig=settings.config.migrationStatus;if(mig&&mig.message){const migBox=document.createElement("div");const ok=!/failed/i.test(mig.message);migBox.style.cssText=`margin-bottom:10px;padding:8px;border-radius:6px;border:1px solid ${ok?"#285":"#833"};background:#111;color:#ccc;font-size:11px;line-height:1.5;`;migBox.textContent=`마이그레이션: ${mig.message} / 엔트리 ${mig.migratedEntries||0}개 정리 / stale embedding ${mig.staleEmbeddingsRemoved||0}개 삭제`;nd.appendChild(migBox)}if(!allEntries.length){const empty=document.createElement("div");empty.textContent="현재 활성화된 로어가 없습니다.";empty.style.cssText="color:#888;font-size:12px;text-align:center;padding:10px;";nd.appendChild(empty);return}const lastMentionMap=JSON.parse(_ls.getItem("lore-last-mention")||"{}")[chatKey]||{};const statusList=[];for(const e of allEntries){let cooldownRem=0;const lastInj=cMap[e.id];if(lastInj!==undefined){const elap=turnCounter-lastInj;cooldownRem=Math.max(0,settings.config.cooldownTurns-elap)}const lastMent=lastMentionMap[e.id]||0;const turnsSince=turnCounter-lastMent;let reinjScore=0;if(settings.config.decayEnabled){reinjScore=C.calcReinjectionScore(turnsSince,e.type,settings.config)}if(cooldownRem>0||reinjScore>.1||turnsSince>0){const evTurn=e.eventTurn||e.timeline?.eventTurn||e.createdTurn||0;const gap=evTurn?Math.max(0,turnCounter-evTurn):null;statusList.push({id:e.id,name:e.name,type:e.type,pack:e.packName,cooldownRem:cooldownRem,turnsSince:turnsSince,reinjScore:reinjScore,eventTurn:evTurn,gap:gap,entities:(C.inferEntryEntities?C.inferEntryEntities(e):e.entities||[]).slice(0,4)})}}statusList.sort((a,b)=>b.reinjScore-a.reinjScore);if(statusList.length===0){const empty2=document.createElement("div");empty2.textContent="표시할 상태(쿨다운/점수)가 없습니다.";empty2.style.cssText="color:#888;font-size:12px;text-align:center;padding:10px;";nd.appendChild(empty2);return}const listContainer=document.createElement("div");listContainer.style.cssText="display:flex;flex-direction:column;gap:8px;max-height:400px;overflow-y:auto;";for(const st of statusList){const row=document.createElement("div");row.style.cssText="display:flex;justify-content:space-between;align-items:center;background:#1a1a1a;border:1px solid #333;border-radius:6px;padding:8px 12px;";const info=document.createElement("div");info.style.cssText="display:flex;flex-direction:column;gap:4px;";const nameEl=document.createElement("div");nameEl.textContent=`[${st.type}] ${st.name}`;nameEl.style.cssText="font-size:13px;font-weight:bold;color:#ccc;";const statText=document.createElement("div");statText.style.cssText="font-size:11px;color:#888;display:flex;gap:12px;";let cdStr=st.cooldownRem>0?`⏳ 쿨다운 ${st.cooldownRem}턴 남음`:`✅ 쿨다운 완료`;let decayStr="";if(settings.config.decayEnabled){const p=Math.round(st.reinjScore*100);const pColor=p>70?"#d66":p>40?"#da8":"#888";decayStr=`망각: ${st.turnsSince}턴 경과 (재주입 점수: ${p}%)`}const timeStr=st.eventTurn?`사건:t${st.eventTurn}${st.gap!=null?" / gap "+st.gap+"턴":""}`:"";const entStr=st.entities&&st.entities.length?`엔티티:${st.entities.join(",")}`:"";statText.innerHTML=[cdStr,decayStr,timeStr,entStr].filter(Boolean).join("");info.appendChild(nameEl);info.appendChild(statText);const resetBtn=document.createElement("button");resetBtn.textContent="리셋";resetBtn.style.cssText="padding:4px 10px;font-size:11px;border-radius:4px;cursor:pointer;background:transparent;border:1px solid #555;color:#ccc;";resetBtn.onclick=()=>{if(settings.config.urlCooldownMaps?.[chatKey]){delete settings.config.urlCooldownMaps[chatKey][st.id]}const allMentions=JSON.parse(_ls.getItem("lore-last-mention")||"{}");if(allMentions[chatKey]&&allMentions[chatKey][st.id]){delete allMentions[chatKey][st.id];_ls.setItem("lore-last-mention",JSON.stringify(allMentions))}settings.save();m.replaceContentPanel(renderSessionStatus,"세션 상태 관리")};row.appendChild(info);row.appendChild(resetBtn);listContainer.appendChild(row)}nd.appendChild(listContainer)}})};m.replaceContentPanel(renderSessionStatus,"세션 상태 조회")})});_w.__LoreInj.__subSessionLoaded=true})();(async function(){const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const deadline=Date.now()+15e3;while(!(_w.__LoreInj&&_w.__LoreInj.__settingsLoaded)&&Date.now()setTimeout(r,50));if(_w.__LoreInj.__subApiLoaded)return;const{C:C,settings:settings}=_w.__LoreInj;_w.__LoreInj.registerSubMenu=_w.__LoreInj.registerSubMenu||function(){};if(settings.config.autoExtApiType==="firebase"&&settings.config.autoExtFirebaseScript&&C.warmupFirebase){C.warmupFirebase(settings.config.autoExtFirebaseScript,settings.config.autoExtModel||"gemini-3-flash-preview").catch(()=>{})}const FIELD_STYLE="width:100%;padding:6px 8px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:12px;box-sizing:border-box;";function addSelect(nd,label,value,groups,onChange,opts={}){const l=document.createElement("div");l.textContent=label;l.style.cssText="font-size:11px;color:#999;margin:10px 0 4px;";nd.appendChild(l);const sel=document.createElement("select");sel.style.cssText=FIELD_STYLE;groups.forEach(([g,items])=>{const og=document.createElement("optgroup");og.label=g;items.forEach(([text,val])=>{const o=document.createElement("option");o.value=val;o.textContent=text;og.appendChild(o)});sel.appendChild(og)});sel.value=value;nd.appendChild(sel);let customInput=null;if(opts.customKey){customInput=document.createElement("input");customInput.value=settings.config[opts.customKey]||"";customInput.placeholder="모델명 직접 입력";customInput.style.cssText=FIELD_STYLE+"margin-top:6px;"+(sel.value==="_custom"?"":"display:none;");customInput.onchange=()=>{settings.config[opts.customKey]=customInput.value;settings.save();if(opts.onCustomChange)opts.onCustomChange(customInput.value)};nd.appendChild(customInput)}sel.onchange=()=>{onChange(sel.value);if(customInput)customInput.style.display=sel.value==="_custom"?"":"none";if(opts.onAfterChange)opts.onAfterChange(sel.value,customInput)};return{sel:sel,customInput:customInput}}function addPromptArea(nd,label,value,onChange,opts={}){const row=document.createElement("div");row.style.cssText="display:flex;justify-content:space-between;align-items:center;margin:12px 0 4px;";const l=document.createElement("div");l.textContent=label;l.style.cssText="font-size:12px;color:#ccc;font-weight:bold;";row.appendChild(l);if(opts.reset){const btn=document.createElement("button");btn.textContent="기본값 복구";btn.style.cssText="font-size:10px;padding:2px 6px;border-radius:3px;background:transparent;border:1px solid #446;color:#88c;cursor:pointer;";row.appendChild(btn);btn.onclick=()=>{if(!confirm(label+" 기본값 복구?"))return;ta.value=opts.reset();onChange(ta.value)}}nd.appendChild(row);const ta=document.createElement("textarea");ta.value=value||"";ta.style.cssText=FIELD_STYLE+"height:"+(opts.height||140)+"px;font-family:monospace;resize:vertical;margin-bottom:10px;";ta.onchange=()=>onChange(ta.value);nd.appendChild(ta);return ta}function renderPromptSettings(panel){panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const title=document.createElement("div");title.textContent="추출 프롬프트";title.style.cssText="font-size:14px;color:#4a9;font-weight:bold;margin-bottom:8px;";nd.appendChild(title);const note=document.createElement("div");note.textContent="자동/수동 추출과 지식 변환에서 쓰는 템플릿. 기본 템플릿은 직접 수정 안 됨.";note.style.cssText="font-size:11px;color:#888;margin-bottom:10px;line-height:1.4;";nd.appendChild(note);const tplHeader=document.createElement("div");tplHeader.style.cssText="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;";const tplTitle=document.createElement("div");tplTitle.textContent="템플릿";tplTitle.style.cssText="font-size:12px;color:#ccc;font-weight:bold;";const newTplBtn=document.createElement("button");newTplBtn.textContent="+ 새 템플릿";newTplBtn.style.cssText="font-size:11px;padding:3px 8px;border-radius:3px;background:#258;border:none;color:#fff;cursor:pointer;";tplHeader.appendChild(tplTitle);tplHeader.appendChild(newTplBtn);nd.appendChild(tplHeader);const tplSelectWrap=document.createElement("div");tplSelectWrap.style.cssText="display:flex;gap:8px;margin-bottom:12px;align-items:center;";const tplSelect=document.createElement("select");tplSelect.style.cssText="flex:1;padding:6px;border:1px solid #333;border-radius:4px;background:#0a0a0a;color:#ccc;font-size:12px;";const tplRenameBtn=document.createElement("button");tplRenameBtn.textContent="이름 변경";tplRenameBtn.style.cssText="font-size:11px;padding:4px 8px;border-radius:3px;background:transparent;border:1px solid #446;color:#88c;cursor:pointer;";const tplDelBtn=document.createElement("button");tplDelBtn.textContent="삭제";tplDelBtn.style.cssText="font-size:11px;padding:4px 8px;border-radius:3px;background:transparent;border:1px solid #d66;color:#d66;cursor:pointer;";const tplResetBtn=document.createElement("button");tplResetBtn.textContent="초기화";tplResetBtn.style.cssText="font-size:11px;padding:4px 8px;border-radius:3px;background:transparent;border:1px solid #285;color:#4a9;cursor:pointer;margin-left:auto;";tplSelectWrap.appendChild(tplSelect);tplSelectWrap.appendChild(tplRenameBtn);tplSelectWrap.appendChild(tplDelBtn);tplSelectWrap.appendChild(tplResetBtn);nd.appendChild(tplSelectWrap);const promptStyle=FIELD_STYLE+"height:150px;font-family:monospace;resize:vertical;margin-bottom:12px;";const mkLabel=txt=>{const l=document.createElement("div");l.textContent=txt;l.style.cssText="font-size:12px;color:#ccc;margin-bottom:4px;";nd.appendChild(l)};mkLabel("출력 형식(JSON)");const taSchema=document.createElement("textarea");taSchema.style.cssText=promptStyle;nd.appendChild(taSchema);mkLabel("새 로어 추출 지시문");const ta1=document.createElement("textarea");ta1.style.cssText=promptStyle;nd.appendChild(ta1);mkLabel("기존 로어 참고 지시문");const ta2=document.createElement("textarea");ta2.style.cssText=promptStyle;nd.appendChild(ta2);const renderTplOptions=()=>{tplSelect.innerHTML="";(settings.config.templates||[]).forEach(t=>{const opt=document.createElement("option");opt.value=t.id;opt.textContent=t.name+(t.isDefault?" (기본)":"");tplSelect.appendChild(opt)});tplSelect.value=settings.config.activeTemplateId||"default";const activeTpl=settings.getActiveTemplate();tplRenameBtn.style.display=activeTpl.isDefault?"none":"block";tplDelBtn.style.display=activeTpl.isDefault?"none":"block";taSchema.value=activeTpl.schema;taSchema.disabled=activeTpl.isDefault;ta1.value=activeTpl.promptWithoutDb;ta1.disabled=activeTpl.isDefault;ta2.value=activeTpl.promptWithDb;ta2.disabled=activeTpl.isDefault};newTplBtn.onclick=()=>{const name=prompt("새 템플릿 이름:");if(!name)return;const newId="tpl_"+Date.now();const active=settings.getActiveTemplate();settings.config.templates.push({id:newId,name:name,isDefault:false,schema:active.schema,promptWithoutDb:active.promptWithoutDb,promptWithDb:active.promptWithDb});settings.config.activeTemplateId=newId;settings.save();renderTplOptions()};tplResetBtn.onclick=()=>{const activeTpl=settings.getActiveTemplate();if(activeTpl.isDefault){alert("기본 템플릿은 수정 불가.");return}if(!confirm("["+activeTpl.name+"] 기본 템플릿 내용으로 초기화?"))return;const defaultTpl=(settings.config.templates||[]).find(t=>t.isDefault);if(!defaultTpl)return;const idx=settings.config.templates.findIndex(t=>t.id===activeTpl.id);if(idx===-1)return;settings.config.templates[idx].schema=defaultTpl.schema;settings.config.templates[idx].promptWithoutDb=defaultTpl.promptWithoutDb;settings.config.templates[idx].promptWithDb=defaultTpl.promptWithDb;settings.save();renderTplOptions()};tplSelect.onchange=()=>{settings.config.activeTemplateId=tplSelect.value;settings.save();renderTplOptions()};tplRenameBtn.onclick=()=>{const activeTpl=settings.getActiveTemplate();if(activeTpl.isDefault)return;const newName=prompt("템플릿 이름:",activeTpl.name);if(newName){const idx=settings.config.templates.findIndex(t=>t.id===activeTpl.id);if(idx!==-1){settings.config.templates[idx].name=newName.trim();settings.save();renderTplOptions()}}};tplDelBtn.onclick=()=>{const activeId=settings.config.activeTemplateId;const activeTpl=settings.getActiveTemplate();if(activeTpl.isDefault)return;if(confirm("["+activeTpl.name+"] 템플릿 삭제?")){settings.config.templates=settings.config.templates.filter(t=>t.id!==activeId);settings.config.activeTemplateId="default";settings.save();renderTplOptions()}};const saveTpl=(key,val)=>{const id=settings.config.activeTemplateId;const idx=(settings.config.templates||[]).findIndex(t=>t.id===id);if(idx!==-1&&!settings.config.templates[idx].isDefault){settings.config.templates[idx][key]=val;settings.save()}};taSchema.onchange=()=>saveTpl("schema",taSchema.value);ta1.onchange=()=>saveTpl("promptWithoutDb",ta1.value);ta2.onchange=()=>saveTpl("promptWithDb",ta2.value);renderTplOptions()}});panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const title=document.createElement("div");title.textContent="후보 재정렬/응답 교정 프롬프트";title.style.cssText="font-size:14px;color:#4a9;font-weight:bold;margin-bottom:8px;";nd.appendChild(title);addPromptArea(nd,"후보 재정렬 지시문",settings.config.rerankPrompt||C.DEFAULTS.rerankPrompt,v=>{settings.config.rerankPrompt=v;settings.save()},{height:110,reset:()=>C.DEFAULTS.rerankPrompt});addPromptArea(nd,"응답 교정 지시문",settings.config.refinerCustomPrompt||"",v=>{settings.config.refinerCustomPrompt=v;settings.config.refinerUseDynamic=false;settings.save()},{height:180});const note=document.createElement("div");note.textContent="응답 교정 템플릿/체크박스 선택은 AI 응답 교정 화면에서도 가능함.";note.style.cssText="font-size:11px;color:#888;line-height:1.4;";nd.appendChild(note)}})}_w.__LoreInj.registerSubMenu("api",function(modal){modal.createSubMenu("API 설정",m=>{m.replaceContentPanel(panel=>{panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const t=document.createElement("div");t.textContent="API 연결";t.style.cssText="font-size:13px;color:#ccc;font-weight:bold;margin-bottom:8px;";nd.appendChild(t);const apiSummary=document.createElement("div");const apiTypeLabel=(settings.config.autoExtApiType||"key")==="vertex"?"Vertex JSON":(settings.config.autoExtApiType||"key")==="firebase"?"Firebase":"API Key";apiSummary.textContent="현재 방식: "+apiTypeLabel+" · 추출/정리, 변환, 중요 장면 추출이 이 연결 사용함.";apiSummary.style.cssText="font-size:11px;color:#888;margin-bottom:8px;line-height:1.4;";nd.appendChild(apiSummary);C.createApiInput(settings.config,"autoExt",nd,()=>settings.save());const testRow=document.createElement("div");testRow.style.cssText="margin:12px 0 16px;display:flex;gap:8px;align-items:center;";const testBtn=document.createElement("button");testBtn.textContent="API 키 테스트";testBtn.style.cssText="padding:6px 16px;font-size:12px;border-radius:4px;cursor:pointer;background:#258;color:#fff;border:1px solid #258;font-weight:bold;";const testResult=document.createElement("span");testResult.style.cssText="font-size:12px;color:#888;word-break:break-all;";testBtn.onclick=async()=>{const apiType=settings.config.autoExtApiType||"key";const missing=apiType==="vertex"?!settings.config.autoExtVertexJson:apiType==="firebase"?!settings.config.autoExtFirebaseScript:!settings.config.autoExtKey;if(missing){alert(apiType==="vertex"?"Vertex JSON 필요.":apiType==="firebase"?"Firebase 설정 필요.":"API 키 필요.");return}testBtn.disabled=true;testResult.textContent="테스트 중...";try{const r=await C.callGeminiApi('Say "OK" in one word.',{apiType:settings.config.autoExtApiType,key:settings.config.autoExtKey,vertexJson:settings.config.autoExtVertexJson,vertexLocation:settings.config.autoExtVertexLocation,vertexProjectId:settings.config.autoExtVertexProjectId,firebaseScript:settings.config.autoExtFirebaseScript,model:settings.config.autoExtModel,maxRetries:0,costContext:{feature:"apiTest",chatKey:"global"}});testResult.textContent=r.text?"✅ 성공: "+r.text.trim().slice(0,50):"❌ 실패: "+r.error;testResult.style.color=r.text?"#4a9":"#d66"}catch(e){testResult.textContent="❌ 오류: "+e.message;testResult.style.color="#d66"}testBtn.disabled=false};testRow.appendChild(testBtn);testRow.appendChild(testResult);nd.appendChild(testRow);const modelHead=document.createElement("div");modelHead.textContent="모델 선택";modelHead.style.cssText="font-size:13px;color:#ccc;font-weight:bold;margin:14px 0 8px;padding-top:10px;border-top:1px solid #333;";nd.appendChild(modelHead);const modelNote=document.createElement("div");modelNote.textContent="API를 쓰는 기능별 모델을 여기서 한 번에 관리함. 프롬프트 내용은 프롬프트 관리 메뉴에서 수정함.";modelNote.style.cssText="font-size:11px;color:#888;margin-bottom:8px;line-height:1.4;";nd.appendChild(modelNote);addSelect(nd,"추출/정리용 모델",settings.config.autoExtModel||"gemini-3-flash-preview",[["Gemini 3.x",[["3.5 Flash","gemini-3.5-flash"],["3.0 Flash","gemini-3-flash-preview"],["3.1 Pro","gemini-3.1-pro-preview"]]],["Gemini 2.x",[["2.5 Pro","gemini-2.5-pro"],["2.0 Flash","gemini-2.0-flash"]]],["기타",[["직접 입력","_custom"]]]],v=>{settings.config.autoExtModel=v;settings.save()},{customKey:"autoExtCustomModel"});addSelect(nd,"후보 재정렬 모델",settings.config.rerankModel||"gemini-3-flash-preview",[["Gemini",[["3.1 Flash Lite (추천)","gemini-3.1-flash-lite-preview"],["3.5 Flash","gemini-3.5-flash"],["3.0 Flash","gemini-3-flash-preview"],["2.5 Flash Lite","gemini-2.5-flash-lite"]]]],v=>{settings.config.rerankModel=v;settings.save()});const judgeCtl=addSelect(nd,"과거 장면 판단 모델",settings.config.temporalRecallJudgeModel||"gemini-3.1-flash-lite-preview",[["Gemini 3.x (추천)",[["3.1 Flash Lite (기본)","gemini-3.1-flash-lite-preview"],["3.5 Flash","gemini-3.5-flash"],["3.0 Flash","gemini-3-flash-preview"]]],["Gemini 2.x",[["2.5 Flash Lite","gemini-2.5-flash-lite"],["2.0 Flash","gemini-2.0-flash"]]],["기타",[["직접 입력","_custom"]]]],v=>{settings.config.temporalRecallJudgeModel=v;settings.save()},{customKey:"temporalRecallJudgeCustomModel"});addSelect(nd,"응답 교정 모델",settings.config.refinerModel!==undefined?settings.config.refinerModel:"",[["기본 LLM과 동일",[["기본 LLM 사용",""]]],["Gemini 3.x",[["3.5 Flash","gemini-3.5-flash"],["3.0 Flash","gemini-3-flash-preview"],["3.1 Flash Lite","gemini-3.1-flash-lite-preview"],["3.1 Pro","gemini-3.1-pro-preview"]]],["Gemini 2.x",[["2.5 Pro","gemini-2.5-pro"],["2.5 Flash","gemini-2.5-flash"],["2.5 Flash Lite","gemini-2.5-flash-lite"],["2.0 Flash","gemini-2.0-flash"]]],["기타",[["직접 입력","_custom"]]]],v=>{settings.config.refinerModel=v;settings.save()},{customKey:"refinerCustomModel"});const rl=document.createElement("div");rl.textContent="생각 깊이";rl.style.cssText="font-size:11px;color:#999;margin:10px 0 4px;";nd.appendChild(rl);const rs=document.createElement("select");rs.style.cssText=FIELD_STYLE;[["Off","off"],["Minimal (256)","minimal"],["Low (1024)","low"],["Medium (2048)","medium"],["High (4096)","high"],["Budget (사용자 지정)","budget"]].forEach(([l,v])=>{const o=document.createElement("option");o.value=v;o.textContent=l;rs.appendChild(o)});rs.value=settings.config.autoExtReasoning||"medium";nd.appendChild(rs);const bl=document.createElement("div");bl.textContent="생각 예산";bl.style.cssText="font-size:11px;color:#666;margin-bottom:4px;margin-top:8px;"+(rs.value==="budget"?"":"display:none;");const bi=document.createElement("input");bi.type="number";bi.value=settings.config.autoExtBudget||2048;bi.style.cssText=FIELD_STYLE+(rs.value==="budget"?"":"display:none;");bi.onchange=()=>{settings.config.autoExtBudget=parseInt(bi.value)||2048;settings.save()};rs.onchange=()=>{settings.config.autoExtReasoning=rs.value;settings.save();const isB=rs.value==="budget";bl.style.display=isB?"":"none";bi.style.display=isB?"":"none"};nd.appendChild(bl);nd.appendChild(bi);const jrl=document.createElement("div");jrl.textContent="과거 장면 판단 생각 깊이";jrl.style.cssText="font-size:11px;color:#999;margin:10px 0 4px;";nd.appendChild(jrl);const jrs=document.createElement("select");jrs.style.cssText=FIELD_STYLE;[["Minimal (권장)","minimal"],["Low","low"],["Medium","medium"],["High","high"]].forEach(([l,v])=>{const o=document.createElement("option");o.value=v;o.textContent=l;jrs.appendChild(o)});jrs.value=settings.config.temporalRecallJudgeReasoning||"minimal";nd.appendChild(jrs);const proWarn=document.createElement("div");proWarn.style.cssText="font-size:10px;color:#d96;margin-top:4px;display:none;";proWarn.textContent="주의: Pro 모델은 minimal 미지원. low 이상 권장.";nd.appendChild(proWarn);const refreshProWarn=()=>{const m=judgeCtl.sel.value==="_custom"?judgeCtl.customInput?.value||"":judgeCtl.sel.value;proWarn.style.display=String(m).includes("pro")&&jrs.value==="minimal"?"":"none"};judgeCtl.sel.addEventListener("change",refreshProWarn);if(judgeCtl.customInput)judgeCtl.customInput.addEventListener("change",refreshProWarn);jrs.onchange=()=>{settings.config.temporalRecallJudgeReasoning=jrs.value;settings.save();refreshProWarn()};refreshProWarn()}})},"API 설정")});modal.createSubMenu("프롬프트 관리",m=>{m.replaceContentPanel(panel=>renderPromptSettings(panel),"프롬프트 관리")})});_w.__LoreInj.__subApiLoaded=true})();(async function(){const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const deadline=Date.now()+15e3;while(!(_w.__LoreInj&&_w.__LoreInj.__settingsLoaded)&&Date.now()setTimeout(r,50));if(_w.__LoreInj.__subHelpLoaded)return;const{C:C}=_w.__LoreInj;_w.__LoreInj.registerSubMenu=_w.__LoreInj.registerSubMenu||function(){};const HELP_QUICK_START={title:"제작자: 로컬AI",text:"[핵심 기능]\n1. 현재 장면에 맞는 로어를 찾아 대화에 자동 삽입함.\n2. 대화를 주기적으로 읽어 로어팩에 기억을 추가함.\n3. 중요한 사건/약속은 별도 장면 기억으로 저장해 나중에 다시 불러옴.\n\n[로드 방식]\n메인화면에서는 무거운 모듈 로드 안 함. 채팅/에피소드 주소로 들어가거나 메인에서 자연스럽게 이동하면 그때 UI 로드함.\n\n[용어]\n로어: 요약본/기억\n의미 검색: 단어가 달라도 비슷한 의미의 로어를 찾는 기능\n변경분만 저장: 기존 로어 전체 대신 바뀐 부분만 받아 출력 토큰 줄이는 기능"};const HELP_ITEMS=[{title:"로어 설정",sections:[{label:"[빠른 설정]",text:"처음 쓰면 기본 추천 권장.\n수동 검색은 API 호출 최소화용.\n정밀은 후보 재정렬/응답 교정까지 쓰는 장문 RP용."},{label:"[인젝션/압축]",text:"로어 인젝션 활성화\n대화에 로어 자동 삽입함. 보통 ON 권장.\n\n적응형 로어 압축\n삽입 공간이 부족하면 로어 길이를 자동으로 줄임.\n자동: 남은 공간에 맞춤\n길게: 자세히 넣음\n짧게: 핵심만 넣음\n아주 짧게: 최소 정보만 넣음"},{label:"[검색 & 감지]",text:"의미로 찾기\n단어가 달라도 관련 로어 찾음. API/검색 준비 필요.\n\n의미 검색 모델\n모델을 바꾸면 기존 로어 검색 준비 재실행 권장.\n\n추출 후 검색 준비\n새 로어를 의미 검색용으로 자동 준비함.\n\n오래된 정보도 가끔 넣기\n직접 관련이 약해도 중요한 과거 정보를 주기적으로 넣음.\n\nAI로 후보 다시 고르기\n검색 후보를 현재 장면 기준으로 다시 정렬함. 정확도는 오르지만 지연/API 비용 증가."},{label:"[추가 정보/출력]",text:"호칭 정보\n캐릭터 간 호칭 정보 함께 전달함.\n\n첫 만남/재회 관리\n처음 만나는지, 오랜만에 다시 만나는지 자동 전달함.\n\n출력 포맷\n로어를 어떤 접두사/접미사로 감쌀지 정함."}]},{title:"추출/변환 설정",sections:[{label:"[자동/수동 추출]",text:"자동 대화 정리\n정해진 턴마다 대화를 읽어 로어팩에 저장함.\n\n수동 추출 실행\n자동 추출 설정값 그대로 즉시 실행함. 일반 로어 추출, 검색 준비, 중요 장면 추출까지 끝난 뒤 완료 표시함.\n\n저장할 로어팩\n현재 채팅에서 추출 결과가 들어갈 로어팩 이름."},{label:"[변경분만 저장]",text:"ON/OFF 모두 같은 대화와 기존 로어 요약을 입력으로 보냄.\nON: 바뀐 부분만 받아 기존 로어에 반영함. 변화 없으면 저장/검색 준비 건너뜀.\nOFF: 갱신된 전체 로어를 받아 병합함.\n\n즉, 입력 토큰은 거의 같아야 하고 출력 토큰만 달라지는 구조."},{label:"[중요 장면 기억하기]",text:"일반 로어 추출과 별도 API 호출로 실행함.\n사건, 약속, 관계 변화처럼 나중에 다시 참고할 장면을 저장함.\n변경분만 저장 ON이면 중요 장면도 바뀐 부분만 받아 반영함."},{label:"[삽입 옵션]",text:"삽입 쿨타임 사용\n같은 로어가 너무 자주 들어가지 않게 막음.\n\n삽입 쿨타임(턴)\n같은 로어를 다시 넣기 전 기다릴 턴 수.\n\n오래된 정보도 가끔 넣기\nOFF면 현재 장면과 직접 관련 있는 로어 중심으로만 넣음."},{label:"[과거 장면 불러오기 판단]",text:"저장된 중요 장면 중 지금 대화에 맞는 것을 고름.\n이 화면에서는 ON/OFF, 제한 시간, 검토할 장면 수만 조절함.\n모델과 생각 깊이는 API 설정에서 관리함."},{label:"[전체 로그 일괄 추출]",text:"이미 긴 채팅을 처음 정리할 때 쓰는 기능.\n대화를 배치로 나눠 여러 번 요약함.\n배치 크기: 한 번에 읽을 턴 수\n오버랩: 흐름 끊김 방지용 중복 턴 수\n재시도: API 실패 시 다시 시도할 횟수"},{label:"[지식 변환]",text:"URL/텍스트를 로어팩으로 변환함.\nAPI 설정의 추출/정리용 모델 사용함.\n생성된 로어팩은 자동 활성화함."}]},{title:"API 설정",sections:[{label:"[API 연결]",text:"지원 방식은 3종.\nAPI Key: 가장 단순함. 처음 설정 권장.\nVertex JSON: 서비스 계정 JSON 사용함.\nFirebase: Firebase 설정 스크립트 사용함. 의미 검색 준비는 별도 Gemini API Key 필요. Google AI Studio에서 무료 키 발급 가능."},{label:"[모델 선택]",text:"API를 쓰는 모델 선택은 여기서 관리함.\n\n추출/정리용 모델\n자동 추출, 수동 추출, 지식 변환, 중요 장면 추출에 사용함.\n\n후보 재정렬 모델\n검색된 로어 후보를 다시 고를 때 사용함.\n\n과거 장면 판단 모델\n저장된 중요 장면 중 지금 대화에 맞는 것을 고를 때 사용함.\n\n응답 교정 모델\nAI 응답을 검수/수정할 때 사용함. 비워두면 기본 LLM 사용."},{label:"[비용 표시]",text:"API 응답에 usageMetadata가 있으면 실제 토큰 사용량 기준으로 계산함.\n없으면 글자 수 기반 추정값 사용함.\n표시 비용은 디버그/비교용이며, 실제 청구액은 Google 계정의 청구 정책/환율/세금에 따라 달라질 수 있음."},{label:"[고급 지시문]",text:"프롬프트 입력 공간은 프롬프트 관리 메뉴로 모음.\n추출 템플릿, 후보 재정렬 지시문, 응답 교정 지시문을 한 곳에서 수정함.\nAPI 설정은 모델/연결 중심으로 유지함."}]},{title:"로어 관리",sections:[{label:"[목록]",text:"로어를 켜고 끄거나 수정/삭제함.\n임베딩 마크는 의미 검색 준비 여부.\n앵커는 자동 갱신에서 보호하는 고정 로어."},{label:"[파일]",text:"로어팩 가져오기/내보내기용.\n활성화한 로어팩만 대화에 주입됨.\n0개가 된 팩은 정리 대상."},{label:"[병합]",text:"활성 로어팩 안의 유사한 로어를 통합함.\n키워드 병합은 API 없음.\nLLM 요약 병합은 API 비용 발생."},{label:"[스냅샷]",text:"자동/수동 추출 전후 백업본.\n추출 결과가 마음에 안 들 때 복구용으로 사용함."}]},{title:"AI 응답 교정",sections:[{label:"[핵심 기능]",text:"AI 답변이 로어와 어긋나거나 상태/호칭/설정이 누락된 경우 교정함.\n수동 검수는 최근 답변만 즉시 확인함."},{label:"[설정]",text:"자동 반영 ON: 교정 결과를 바로 적용함.\n자동 반영 OFF: 교정 전 확인창 표시함.\n로어 검색 모드는 키워드 또는 의미 검색 중 선택함.\n검수 템플릿은 체크한 항목만 프롬프트에 넣을 수 있음."}]},{title:"실행 로그/세션",sections:[{label:"[실행 로그]",text:"주입, 추출, 교정, 모순 기록 확인용.\nAPI 비용도 모델/기능/채팅별로 볼 수 있음.\n비용은 비교용으로 보고, 실제 청구 확인은 Google 결제 내역 기준."},{label:"[현재 세션 상태]",text:"현재 채팅에서 어떤 로어가 점수를 받았는지, 재주입 쿨다운이 어떻게 걸렸는지 확인/초기화함."}]}];_w.__LoreInj.registerSubMenu("help",function(modal){modal.createSubMenu("도움말",m=>{m.replaceContentPanel(panel=>{const addHelp=(title,sections)=>{panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const head=document.createElement("div");head.style.cssText="display:flex;align-items:center;gap:8px;cursor:pointer;padding:2px 0;";const arrow=document.createElement("span");arrow.textContent="▶";arrow.style.cssText="font-size:11px;color:#888;width:10px;";const tt=document.createElement("div");tt.textContent=title||"도움말";tt.style.cssText="font-size:13px;color:#4a9;font-weight:bold;flex:1;";head.appendChild(arrow);head.appendChild(tt);const body=document.createElement("div");body.style.cssText="display:none;padding:10px 2px 4px 2px;border-top:1px dashed #333;margin-top:6px;width:100%;box-sizing:border-box;";(sections||[]).forEach(s=>{const lbl=document.createElement("div");lbl.textContent=s.label||"내용";lbl.style.cssText="font-size:11px;color:#888;font-weight:bold;margin-top:10px;margin-bottom:3px;letter-spacing:.5px;";const txt=document.createElement("div");txt.textContent=s.text||"";txt.style.cssText="font-size:12px;color:#ccc;line-height:1.75;word-break:keep-all;white-space:pre-line;";body.appendChild(lbl);body.appendChild(txt)});head.onclick=()=>{const open=body.style.display!=="none";body.style.display=open?"none":"block";arrow.textContent=open?"▶":"▼"};nd.appendChild(head);nd.appendChild(body)}})};panel.addBoxedField("","",{onInit:nd=>{C.setFullWidth(nd);const t=document.createElement("div");t.textContent=HELP_QUICK_START.title||"빠른 시작";t.style.cssText="font-size:14px;color:#ccc;font-weight:bold;margin-bottom:6px;";const d=document.createElement("div");d.textContent=HELP_QUICK_START.text||"";d.style.cssText="font-size:12px;color:#ccc;line-height:1.8;word-break:keep-all;white-space:pre-line;";nd.appendChild(t);nd.appendChild(d)}});HELP_ITEMS.forEach(item=>addHelp(item.title,item.sections||[]))},"기능 안내")})});_w.__LoreInj.__subHelpLoaded=true})();(async function(){"use strict";const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const _ls=_w.localStorage;function isChatPath(){const fn=_w.__LoreInj&&_w.__LoreInj.isChatPath;if(typeof fn==="function"){try{return!!fn()}catch(_){}}return/\/characters\/[a-f0-9]+\/chats\/[a-f0-9]+/.test(location.pathname)||/\/stories\/[a-f0-9]+\/episodes\/[a-f0-9]+/.test(location.pathname)||/\/u\/[a-f0-9]+\/c\/[a-f0-9]+/.test(location.pathname)}function reloadOnSpaChatRoute(){if(!_w.__LoreInj||typeof _w.__LoreInj.runWhenChatRoute!=="function")return;_w.__LoreInj.runWhenChatRoute(()=>{if(_w.__LoreInj.__uiLoaded||_w.__LoreInj.__spaChatReloading)return;_w.__LoreInj.__spaChatReloading=true;console.log("[LoreInj:6-ui] SPA chat route detected after light boot: reload for full bootstrap");setTimeout(()=>location.reload(),50)})}async function showBootErrorBadge(gate){if(document.readyState==="loading")await new Promise(r=>document.addEventListener("DOMContentLoaded",r));if(document.getElementById("lore-inj-boot-error"))return;const btn=document.createElement("button");btn.id="lore-inj-boot-error";btn.textContent="Lore Injector 로딩 진단";btn.style.cssText="position:fixed;right:12px;bottom:12px;z-index:999999;background:#833;color:#fff;border:0;border-radius:8px;padding:8px 10px;font-size:12px;box-shadow:0 2px 8px rgba(0,0,0,.35);";btn.onclick=()=>{const L=_w.__LoreInj||{};const payload={gate:gate,moduleStatus:L.moduleStatus||{},missingSubs:L.missingSubs||[],menuOrder:L.__menuOrder||null,route:L.route||null};alert(JSON.stringify(payload,null,2).slice(0,3e3))};document.body.appendChild(btn)}async function waitForModalManager(timeoutMs=1e4){const until=Date.now()+timeoutMs;while(Date.now()setTimeout(r,100))}return null}if(_w.__LoreInjReady&&typeof _w.__LoreInjReady.then==="function"){const gate=await _w.__LoreInjReady;if(!gate||gate.ok!==true){console.error("[LoreInj:6-ui] ready 게이트 실패, UI 마운트 중단:",gate);await showBootErrorBadge(gate);return}}else{const _deadline=Date.now()+15e3;const _subs=["__interceptorLoaded","__constLoaded","__settingsLoaded","__extractLoaded","__injectLoaded","__subMainLoaded","__subLoreLoaded","__subMergeLoaded","__subSnapshotLoaded","__subFileLoaded","__subExtractLoaded","__subRefinerLoaded","__subLogLoaded","__subSessionLoaded","__subApiLoaded","__subHelpLoaded"];while(Date.now()<_deadline){const L=_w.__LoreInj;if(L&&_subs.every(k=>L[k]))break;await new Promise(r=>setTimeout(r,50))}const L=_w.__LoreInj;if(!L||!_subs.every(k=>L[k])){console.error("[LoreInj:6-ui] 폴백 대기 타임아웃, UI 마운트 중단");await showBootErrorBadge({ok:false,reason:"fallback-timeout"});return}}if(!isChatPath()){console.log("[LoreInj:6-ui] non-chat route: UI bootstrap skipped");reloadOnSpaChatRoute();return}if(document.readyState==="loading")await new Promise(r=>document.addEventListener("DOMContentLoaded",r));if(_w.__LoreInj.__uiLoaded)return;const{C:C,R:R,settings:settings}=_w.__LoreInj;const VER=_w.__LoreInj.VER;const MM=await waitForModalManager(1e4);if(!MM){console.error("[LoreInj:6-ui] ModalManager 미로드");_w.__LoreInj?.markFailed?.("ui","ModalManager missing");await showBootErrorBadge({ok:false,reason:"ModalManager missing"});return}const modal=MM.getOrCreateManager("c2");if(!modal||typeof modal.createMenu!=="function"){console.error("[LoreInj:6-ui] modal.createMenu 없음");_w.__LoreInj?.markFailed?.("ui","modal.createMenu missing");await showBootErrorBadge({ok:false,reason:"modal.createMenu missing"});return}if(_w.__LoreInj.setupSubMenus){_w.__LoreInj.setupSubMenus(modal)}function __updateModalMenu(){const modalEl=document.getElementById("web-modal");if(modalEl&&!document.getElementById("chasm-decentral-menu")){const itemFound=modalEl.getElementsByTagName("a");for(let item of itemFound){if(item.getAttribute("href")==="/setting"){const clonedElement=item.cloneNode(true);clonedElement.id="chasm-decentral-menu";const textElement=clonedElement.getElementsByTagName("span")[0];if(textElement)textElement.innerText="결정화 캐즘";clonedElement.setAttribute("href","javascript: void(0)");clonedElement.onclick=event=>{event.preventDefault();event.stopPropagation();MM.getOrCreateManager("c2").display(document.body.getAttribute("data-theme")!=="light")};item.parentElement?.append(clonedElement);break}}}}async function injectBannerButton(){const selected=document.getElementsByClassName("burner-button");if(selected&&selected.length>0)return;try{const isStory=/\/stories\/[a-f0-9]+\/episodes\/[a-f0-9]+/.test(location.pathname)||/\/u\/[a-f0-9]+\/c\/[a-f0-9]+/.test(location.pathname);const topPanel=document.getElementsByClassName(isStory?"css-1c5w7et":"css-l8r172");if(topPanel&&topPanel.length>0){const topContainer=topPanel[0].childNodes[topPanel.length-1]?.getElementsByTagName("div");if(!topContainer||topContainer.length<=0)return;const topList=topContainer[0].children[0].children;const top=topList[topList.length-1];if(!top)return;const buttonCloned=document.createElement("button");buttonCloned.innerHTML="

";buttonCloned.style.cssText="margin-right: 10px";buttonCloned.className="burner-button";const textNode=buttonCloned.getElementsByTagName("p");top.insertBefore(buttonCloned,top.childNodes[0]);textNode[0].innerText="🔥 Chasm Tools";buttonCloned.removeAttribute("onClick");buttonCloned.addEventListener("click",()=>{MM.getOrCreateManager("c2").display(document.body.getAttribute("data-theme")!=="light")})}}catch(e){}}async function doInjection(){if(!/\/characters\/[a-f0-9]+\/chats\/[a-f0-9]+/.test(location.pathname)&&!/\/stories\/[a-f0-9]+\/episodes\/[a-f0-9]+/.test(location.pathname)&&!/\/u\/[a-f0-9]+\/c\/[a-f0-9]+/.test(location.pathname))return;await injectBannerButton()}function __doModalMenuInit(){if(document.c2InjectorModalInit)return;document.c2InjectorModalInit=true;let _menuDebounceTimer=0;const _debouncedUpdateMenu=()=>{if(_menuDebounceTimer)return;_menuDebounceTimer=setTimeout(()=>{_menuDebounceTimer=0;__updateModalMenu()},200)};if(typeof GenericUtil!=="undefined"&&GenericUtil.attachObserver){GenericUtil.attachObserver(document,_debouncedUpdateMenu)}else{const observer=new MutationObserver(_debouncedUpdateMenu);observer.observe(document.body,{childList:true,subtree:true})}if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",doInjection);window.addEventListener("load",doInjection)}else{doInjection()}setInterval(doInjection,2e3)}document.getElementById("lore-inj-gear-btn")?.remove();__doModalMenuInit();Object.assign(_w.__LoreInj,{__uiLoaded:true});console.log("[LoreInj:6-ui] UI loaded (Chasm Tools banner attached)")})();(function(){"use strict";const _w=typeof unsafeWindow!=="undefined"?unsafeWindow:window;const L=_w.__LoreInj=_w.__LoreInj||{};const TIMEOUT_MS=45e3;const POLL_MS=50;const deadline=Date.now()+TIMEOUT_MS;const requiredCore=["__interceptorLoaded","__constLoaded","__settingsLoaded","__extractLoaded","__injectLoaded","__inject6Loaded"];const requiredSubs=["__subMainLoaded","__subLoreLoaded","__subMergeLoaded","__subSnapshotLoaded","__subFileLoaded","__subExtractLoaded","__subRefinerLoaded","__subLogLoaded","__subSessionLoaded","__subApiLoaded","__subHelpLoaded"];const requiredAll=requiredCore.concat(requiredSubs);let settled=false;function settle(payload){if(settled)return;settled=true;try{_w.__LoreInjReady.__resolve(payload)}catch(_){}try{_w.dispatchEvent(new CustomEvent("LoreInj:ready",{detail:payload}))}catch(_){}}function check(){const state=_w.__LoreInj;if(!state){if(Date.now()!state[k]);const missingSubs=requiredSubs.filter(k=>!state[k]);if(missingCore.length===0&&!state.__gateCoreReadyAt)state.__gateCoreReadyAt=Date.now();if(missingCore.length===0&&missingSubs.length===0){state.allReady=true;state.universalBundle=true;console.log("[LoreInj universal "+(state.VER||L.chatBootstrapVersion)+"] gate passed");settle({ok:true,ver:state.VER||L.chatBootstrapVersion,universal:true});return}if(Date.now()!state[k]);console.error("[LoreInj universal] gate timeout:",missing);settle({ok:false,reason:"timeout",missing:missing,missingCore:missingCore,missingSubs:missingSubs,universal:true})}check()})();