diff --git a/src/lib/ChatScreens/DefaultChatScreen.svelte b/src/lib/ChatScreens/DefaultChatScreen.svelte index 7e188f32..ff5c8323 100644 --- a/src/lib/ChatScreens/DefaultChatScreen.svelte +++ b/src/lib/ChatScreens/DefaultChatScreen.svelte @@ -77,7 +77,7 @@ cha.push({ role: 'user', - data: processScript(char,messageInput,'editinput'), + data: await processScript(char,messageInput,'editinput'), time: Date.now() }) } diff --git a/src/lib/ChatScreens/Suggestion.svelte b/src/lib/ChatScreens/Suggestion.svelte index 5c735401..78868f2c 100644 --- a/src/lib/ChatScreens/Suggestion.svelte +++ b/src/lib/ChatScreens/Suggestion.svelte @@ -41,7 +41,7 @@ } - const unsub = doingChat.subscribe((v) => { + const unsub = doingChat.subscribe(async (v) => { if(v) { progress=false abortController?.abort() @@ -55,7 +55,7 @@ const firstMsg:string = currentChar.firstMsgIndex === -1 ? currentChar.firstMessage : currentChar.alternateGreetings[currentChar.firstMsgIndex] messages.push({ role: 'char', - data: processScript(currentChar, + data: await processScript(currentChar, replacePlaceholders(firstMsg, currentChar.name), 'editprocess') }) diff --git a/src/ts/parser.ts b/src/ts/parser.ts index 63db08d5..68c7074d 100644 --- a/src/ts/parser.ts +++ b/src/ts/parser.ts @@ -4,7 +4,7 @@ import { Marked } from 'marked'; import { DataBase, type Database, type Message, type character, type customscript, type groupChat } from './storage/database'; import { getFileSrc } from './storage/globalApi'; -import { processScript, processScriptFull } from './process/scripts'; +import { processScriptFull } from './process/scripts'; import { get } from 'svelte/store'; import css from '@adobe/css-tools' import { selectedCharID } from './stores'; @@ -102,6 +102,7 @@ export interface simpleCharacterArgument{ additionalAssets?: [string, string, string][] customscript: customscript[] chaId: string, + virtualscript?: string } @@ -115,7 +116,7 @@ export async function ParseMarkdown(data:string, charArg:(simpleCharacterArgumen firstParsed = data } if(char){ - data = processScriptFull(char, data, 'editdisplay', chatID).data + data = (await processScriptFull(char, data, 'editdisplay', chatID)).data } if(firstParsed !== data && char && char.type !== 'group'){ data = await parseAdditionalAssets(data, char, mode) diff --git a/src/ts/plugins/embedscript.ts b/src/ts/plugins/embedscript.ts new file mode 100644 index 00000000..9d9a3ee2 --- /dev/null +++ b/src/ts/plugins/embedscript.ts @@ -0,0 +1,102 @@ +import { get } from 'svelte/store' +import type { ScriptMode } from '../process/scripts' +import myWorkerUrl from './embedworker?worker&url' +import { DataBase } from '../storage/database' +import { selectedCharID } from '../stores' +import { cloneDeep } from 'lodash' + +let worker = new Worker(new URL(myWorkerUrl), {type: 'module'}) + +let results:{ + id: string, + result: any +}[] = [] + +let workerFunctions: { + [key:string]: (...args:any[])=> Promise +} = {} + +worker.onmessage = ({data}) => { + if(data.type === 'api'){ + workerFunctions[data.name](...data.args).then((result)=>{ + worker.postMessage({ + type: 'result', + id: data.id, + result + }) + }) + } + else{ + results.push(data) + } +} + +function addWorkerFunction(name:string, func: (...args:any[])=> Promise){ + workerFunctions[name] = func + worker.postMessage({ + type: 'api', + name + }) +} + + +function runVirtualJS(code:string){ + const id = `id${Math.random()}`.replace('.','') + worker.postMessage({ + id,code + }) + let startTime = performance.now() + return new Promise((resolve,reject)=>{ + const interval = setInterval(()=>{ + const result = results.find(r=>r.id === id) + console.log(performance.now() - startTime ) + if(result){ + clearInterval(interval) + resolve(result.result) + } + else if(performance.now() - startTime > 400){ + clearInterval(interval) + //restart worker + worker.terminate() + worker = new Worker(new URL('./worker.ts', import.meta.url), {type: 'module'}) + reject('timeout') + } + },100) + }) +} + +addWorkerFunction('getCharacter', async () => { + const db = get(DataBase) + const selectedChar = get(selectedCharID) + return cloneDeep(db.characters[selectedChar]) +}) + +export async function runCharacterJS(arg:{ + code: string, + mode: ScriptMode + data: string +}):Promise{ + try { + const codes = { + "editinput": 'editInput', + "editoutput": 'editOutput', + "editprocess": 'editProcess', + "editdisplay": 'editDisplay', + } as const + + const runCode = codes[arg.mode] + const result = await runVirtualJS(`${arg.code}\n${runCode}(${JSON.stringify(arg.data)})`) + + if(!result){ + return arg.data + } + + return result.toString() + } catch (error) { + if(arg.mode !== 'editprocess'){ + return `Error: ${error}` + } + return arg.data + } + +} \ No newline at end of file diff --git a/src/ts/plugins/embedworker.ts b/src/ts/plugins/embedworker.ts new file mode 100644 index 00000000..9d16627f --- /dev/null +++ b/src/ts/plugins/embedworker.ts @@ -0,0 +1,142 @@ +let globaly = globalThis + +const whitelist = [ + "Array", + "ArrayBuffer", + "BigInt", + "BigInt64Array", + "BigUint64Array", + "Boolean", + "DataView", + "Date", + "Error", + "EvalError", + "Float32Array", + "Float64Array", + "Function", + "Infinity", + "Int16Array", + "Int32Array", + "Int8Array", + "JSON", + "Map", + "Math", + "NaN", + "Number", + "Object", + "Promise", + "Proxy", + "RangeError", + "ReferenceError", + "Reflect", + "RegExp", + "Set", + "SharedArrayBuffer", + "String", + "Symbol", + "SyntaxError", + "TypeError", + "URIError", + "Uint16Array", + "Uint32Array", + "Uint8Array", + "Uint8ClampedArray", + "WeakMap", + "WeakSet", + "WebAssembly", + "console", + "decodeURI", + "decodeURIComponent", + "encodeURI", + "encodeURIComponent", + "escape", + "globalThis", + "isFinite", + "isNaN", + "null", + "parseFloat", + "parseInt", + "undefined", + "unescape", + "queueMicrotask", + "setTimeout", + "clearTimeout", + "setInterval", + "clearInterval", + "setImmediate", + "clearImmediate", + "atob", + "btoa", + "Headers", + "Request", + "Response", + "Blob", + "postMessage" +] + +const evaluation = global.eval + +Object.getOwnPropertyNames( global ).forEach( function( prop ) { + if( !whitelist.includes(prop) ) { + Object.defineProperty( global, prop, { + get : function() { + throw "Security Exception: cannot access "+prop; + return 1; + }, + configurable : false + }); + } +}); + +let workerResults:{ + id: string, + result: any +}[] = [] + +self.onmessage = async (event) => { + const da = event.data + if(da.type === 'result'){ + workerResults.push(da) + return + } + if(da.type === 'api'){ + //add api + Object.defineProperty( global, da.name, { + get : function() { + return function (...args:any[]) { + return new Promise((resolve)=>{ + const functionCallID = `id${Math.random()}`.replace('.','') + self.postMessage({ + type: 'api', + name: da.name, + id: functionCallID, + args + }) + const interval = setInterval(()=>{ + const result = workerResults.find(r=>r.id === functionCallID) + if(result){ + clearInterval(interval) + resolve(result.result) + } + },100) + }) + } + } + }); + return + } + try{ + const d = await evaluation(da.code) + self.postMessage({ + id: da.id, + result: d + }) + } + catch(e){ + console.error(e) + self.postMessage({ + id: da.id, + result: e + }) + } +} \ No newline at end of file diff --git a/src/ts/process/index.ts b/src/ts/process/index.ts index 17ccadac..bbc2450d 100644 --- a/src/ts/process/index.ts +++ b/src/ts/process/index.ts @@ -430,9 +430,9 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n const chat:OpenAIChat = { role: 'assistant', - content: processScript(nowChatroom, + content: await (processScript(nowChatroom, risuChatParser(firstMsg, {chara: currentChar, rmVar: true}), - 'editprocess') + 'editprocess')) } chats.push(chat) currentTokens += await tokenizer.tokenizeChat(chat) @@ -448,7 +448,7 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n } for(const msg of ms){ - let formedChat = processScript(nowChatroom,risuChatParser(msg.data, {chara: currentChar, rmVar: true}), 'editprocess') + let formedChat = await processScript(nowChatroom,risuChatParser(msg.data, {chara: currentChar, rmVar: true}), 'editprocess') let name = '' if(msg.role === 'char'){ if(msg.saying){ @@ -798,7 +798,7 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n if(db.cipherChat){ result = decipherChat(result) } - const result2 = processScriptFull(nowChatroom, reformatContent(prefix + result), 'editoutput', msgIndex) + const result2 = await processScriptFull(nowChatroom, reformatContent(prefix + result), 'editoutput', msgIndex) db.characters[selectedChar].chats[selectedChat].message[msgIndex].data = result2.data emoChanged = result2.emoChanged db.characters[selectedChar].reloadKeys += 1 @@ -832,11 +832,11 @@ export async function sendChat(chatProcessIndex = -1,arg:{chatAdditonalTokens?:n mess = decipherChat(result) } let msgIndex = db.characters[selectedChar].chats[selectedChat].message.length - let result2 = processScriptFull(nowChatroom, reformatContent(mess), 'editoutput', msgIndex) + let result2 = await processScriptFull(nowChatroom, reformatContent(mess), 'editoutput', msgIndex) if(i === 0 && arg.continue){ msgIndex -= 1 let beforeChat = db.characters[selectedChar].chats[selectedChat].message[msgIndex] - result2 = processScriptFull(nowChatroom, reformatContent(beforeChat.data + mess), 'editoutput', msgIndex) + result2 = await processScriptFull(nowChatroom, reformatContent(beforeChat.data + mess), 'editoutput', msgIndex) } result = result2.data emoChanged = result2.emoChanged diff --git a/src/ts/process/scripts.ts b/src/ts/process/scripts.ts index b9bc7e5b..51cd1fc4 100644 --- a/src/ts/process/scripts.ts +++ b/src/ts/process/scripts.ts @@ -7,14 +7,15 @@ import { language } from "src/lang"; import { selectSingleFile } from "../util"; import { risuChatParser as risuChatParserOrg, type simpleCharacterArgument } from "../parser"; import { autoMarkPlugin } from "../plugins/automark"; +import { runCharacterJS } from "../plugins/embedscript"; const dreg = /{{data}}/g const randomness = /\|\|\|/g -type ScriptMode = 'editinput'|'editoutput'|'editprocess'|'editdisplay' +export type ScriptMode = 'editinput'|'editoutput'|'editprocess'|'editdisplay' -export function processScript(char:character|groupChat, data:string, mode:ScriptMode){ - return processScriptFull(char, data, mode).data +export async function processScript(char:character|groupChat, data:string, mode:ScriptMode){ + return (await processScriptFull(char, data, mode)).data } export function exportRegex(){ @@ -54,13 +55,20 @@ export async function importRegex(){ } } -export function processScriptFull(char:character|groupChat|simpleCharacterArgument, data:string, mode:ScriptMode, chatID = -1){ +export async function processScriptFull(char:character|groupChat|simpleCharacterArgument, data:string, mode:ScriptMode, chatID = -1){ let db = get(DataBase) let emoChanged = false const scripts = (db.globalscript ?? []).concat(char.customscript) if(db.officialplugins.automark && mode === 'editdisplay'){ data = autoMarkPlugin(data) } + if(char.virtualscript){ + data = await runCharacterJS({ + code: char.virtualscript, + mode, + data, + }) + } if(scripts.length === 0){ return {data, emoChanged} } diff --git a/src/ts/storage/database.ts b/src/ts/storage/database.ts index 3f0a034a..e3d931fd 100644 --- a/src/ts/storage/database.ts +++ b/src/ts/storage/database.ts @@ -577,6 +577,7 @@ export interface character{ private?:boolean additionalText:string oaiVoice?:string + virtualscript?:string } @@ -618,6 +619,7 @@ export interface groupChat{ reloadKeys?:number backgroundCSS?:string oneAtTime?:boolean + virtualscript?:string } export interface botPreset{ diff --git a/src/ts/stores.ts b/src/ts/stores.ts index 96d4c28f..55ca50b1 100644 --- a/src/ts/stores.ts +++ b/src/ts/stores.ts @@ -46,7 +46,8 @@ function createSimpleCharacter(char:character|groupChat){ type: "simple", customscript: cloneDeep(char.customscript), chaId: char.chaId, - additionalAssets: char.additionalAssets + additionalAssets: char.additionalAssets, + virtualscript: char.virtualscript, } return simpleChar