From 406c2fc3c1452be59e97b55878c739cf09555c5f Mon Sep 17 00:00:00 2001 From: kwaroran Date: Wed, 8 Nov 2023 16:33:53 +0900 Subject: [PATCH] [feat] charajs apis --- src/etc/example-char.js | 137 ++++++++++++++++++++++++ src/main.ts | 2 + src/ts/parser.ts | 20 ++-- src/ts/plugins/embedscript.ts | 193 ++++++++++++++++++++++++++++++++-- src/ts/plugins/embedworker.ts | 34 +++--- src/ts/storage/database.ts | 2 + tsconfig.json | 2 +- 7 files changed, 359 insertions(+), 31 deletions(-) create mode 100644 src/etc/example-char.js diff --git a/src/etc/example-char.js b/src/etc/example-char.js new file mode 100644 index 00000000..87ec6f4e --- /dev/null +++ b/src/etc/example-char.js @@ -0,0 +1,137 @@ +//@use editInput +//@use editOutput +//@use editProcess +//@use editDisplay +//@use onButtonClick + +async function editInput(text){ + return text; +} + +async function editOutput(text){ + return text; +} + +async function editProcess(text){ + return text; +} + +async function onButtonClick(code){ + let fm = await getCharacterFirstMessage() + + if(code === 'calculate'){ + fm = calculateString(fm) + } + else if(code === 'clearResult'){ + fm = '0'; + } + else if(code.startsWith("click")){ + fm += code.substring(5); + } + else{ + fm += code; + } + setCharacterFirstMessage(fm); + +} + + +function calculateString(input) { + let numbers = input.split(/\+|\-|\*|\//).map(Number); + let operators = input.split(/[0-9]+/).filter(Boolean); + + let result = numbers[0]; + + for (let i = 0; i < operators.length; i++) { + switch (operators[i]) { + case '+': + result += numbers[i + 1]; + break; + case '-': + result -= numbers[i + 1]; + break; + case '*': + result *= numbers[i + 1]; + break; + case '/': + result /= numbers[i + 1]; + break; + default: + return "Error: Invalid operator"; + } + } + return result.toFixed(1); +} + +async function editDisplay(text){ + return ` +
+
${text}
+ + + + + + + + + + + + + + + + +
+ + `; +} + +async function edit(text){ + return text; + +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 01fbb6a9..0d622568 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import App from "./App.svelte"; import { loadData } from "./ts/storage/globalApi"; import { initHotkey } from "./ts/hotkey"; import { polyfill } from "./ts/polyfill"; +import { watchParamButton } from "./ts/plugins/embedscript"; polyfill() @@ -13,4 +14,5 @@ const app = new App({ loadData() initHotkey() +watchParamButton() export default app; \ No newline at end of file diff --git a/src/ts/parser.ts b/src/ts/parser.ts index 68c7074d..747b5630 100644 --- a/src/ts/parser.ts +++ b/src/ts/parser.ts @@ -45,13 +45,17 @@ DOMPurify.addHook("uponSanitizeElement", (node: HTMLElement, data) => { }); DOMPurify.addHook("uponSanitizeAttribute", (node, data) => { - if(data.attrName === 'style'){ - data.attrValue = data.attrValue.replace(/(absolute)|(z-index)|(fixed)/g, '') - } - if(data.attrName === 'class'){ - data.attrValue = data.attrValue.split(' ').map((v) => { - return "x-risu-" + v - }).join(' ') + switch(data.attrName){ + case 'style':{ + data.attrValue = data.attrValue.replace(/(absolute)|(z-index)|(fixed)/g, '') + break + } + case 'class':{ + data.attrValue = data.attrValue.split(' ').map((v) => { + return "x-risu-" + v + }).join(' ') + break + } } }) @@ -123,7 +127,7 @@ export async function ParseMarkdown(data:string, charArg:(simpleCharacterArgumen } return decodeStyle(DOMPurify.sanitize(mconverted.parse(encodeStyle(data)), { ADD_TAGS: ["iframe", "style", "risu-style"], - ADD_ATTR: ["allow", "allowfullscreen", "frameborder", "scrolling"], + ADD_ATTR: ["allow", "allowfullscreen", "frameborder", "scrolling", "risu-btn"], FORBID_ATTR: ["href"] })) } diff --git a/src/ts/plugins/embedscript.ts b/src/ts/plugins/embedscript.ts index 9d9a3ee2..abd44255 100644 --- a/src/ts/plugins/embedscript.ts +++ b/src/ts/plugins/embedscript.ts @@ -1,11 +1,14 @@ import { get } from 'svelte/store' import type { ScriptMode } from '../process/scripts' import myWorkerUrl from './embedworker?worker&url' -import { DataBase } from '../storage/database' +import { DataBase, type Chat, type character, type Message } from '../storage/database' import { selectedCharID } from '../stores' -import { cloneDeep } from 'lodash' +import { add, cloneDeep } from 'lodash' +import { sleep } from '../util' +import { characterFormatUpdate } from '../characters' +import { setDatabase } from '../storage/database' -let worker = new Worker(new URL(myWorkerUrl), {type: 'module'}) +let worker = new Worker(myWorkerUrl, {type: 'module'}) let results:{ id: string, @@ -49,7 +52,6 @@ function runVirtualJS(code:string){ 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) @@ -61,30 +63,184 @@ function runVirtualJS(code:string){ worker = new Worker(new URL('./worker.ts', import.meta.url), {type: 'module'}) reject('timeout') } - },100) + },10) }) } -addWorkerFunction('getCharacter', async () => { +addWorkerFunction('getChat', async () => { const db = get(DataBase) const selectedChar = get(selectedCharID) - return cloneDeep(db.characters[selectedChar]) + const char = db.characters[selectedChar] + return cloneDeep(char.chats[char.chatPage].message) }) +addWorkerFunction('setChat', async (data:Message[]) => { + const db = get(DataBase) + const selectedChar = get(selectedCharID) + let newChat:Message[] = [] + for(const dat of data){ + if(dat.role !== 'char' && dat.role !== 'user'){ + return false + } + if(typeof dat.data !== 'string'){ + return false + } + if(typeof dat.saying !== 'string'){ + return false + } + if(typeof dat.time !== 'number'){ + return false + } + if(typeof dat.chatId !== 'string'){ + return false + } + newChat.push({ + role: dat.role, + data: dat.data, + saying: dat.saying, + time: dat.time, + chatId: dat.chatId + }) + } + db.characters[selectedChar].chats[db.characters[selectedChar].chatPage].message = newChat + setDatabase(db) + return true +}) + +addWorkerFunction('getName', async () => { + const db = get(DataBase) + const selectedChar = get(selectedCharID) + const char = db.characters[selectedChar] + return char.name +}) + +addWorkerFunction('setName', async (data:string) => { + const db = get(DataBase) + const selectedChar = get(selectedCharID) + if(typeof data !== 'string'){ + return false + } + db.characters[selectedChar].name = data + setDatabase(db) + return true +}) + +addWorkerFunction('getDescription', async () => { + const db = get(DataBase) + const selectedChar = get(selectedCharID) + const char = db.characters[selectedChar] + if(char.type === 'group'){ + return '' + } + return char.desc +}) + +addWorkerFunction('setDescription', async (data:string) => { + const db = get(DataBase) + const selectedChar = get(selectedCharID) + const char =db.characters[selectedChar] + if(typeof data !== 'string'){ + return false + } + if(char.type === 'group'){ + return false + } + char.desc = data + db.characters[selectedChar] = char + setDatabase(db) + return true +}) + +addWorkerFunction('getCharacterFirstMessage', async () => { + const db = get(DataBase) + const selectedChar = get(selectedCharID) + const char = db.characters[selectedChar] + return char.firstMessage +}) + +addWorkerFunction('setCharacterFirstMessage', async (data:string) => { + const db = get(DataBase) + const selectedChar = get(selectedCharID) + const char = db.characters[selectedChar] + if(typeof data !== 'string'){ + return false + } + char.firstMessage = data + db.characters[selectedChar] = char + setDatabase(db) + return true +}) + +addWorkerFunction('getState', async (statename) => { + const db = get(DataBase) + const selectedChar = get(selectedCharID) + const char = db.characters[selectedChar] + const chat = char.chats[char.chatPage] + return (chat.scriptstate ?? {})[statename] +}) + +addWorkerFunction('setState', async (statename, data) => { + const db = get(DataBase) + const selectedChar = get(selectedCharID) + const char = db.characters[selectedChar] + const chat = char.chats[char.chatPage] + if(typeof statename !== 'string'){ + return false + } + if(typeof data !== 'string' && typeof data !== 'number' && typeof data !== 'boolean'){ + return false + } + if(!chat.scriptstate){ + chat.scriptstate = {} + } + chat.scriptstate[statename] = data + char.chats[char.chatPage] = chat + db.characters[selectedChar] = char + setDatabase(db) + return true +}) + + + +let lastCode = '' +let lastModeList:string[] = [] + export async function runCharacterJS(arg:{ - code: string, - mode: ScriptMode + code: string|null, + mode: ScriptMode|'onButtonClick' data: string }):Promise{ try { + if(arg.code === null){ + const db = get(DataBase) + const selectedChar = get(selectedCharID) + arg.code = db.characters[selectedChar].virtualscript + } const codes = { "editinput": 'editInput', "editoutput": 'editOutput', "editprocess": 'editProcess', "editdisplay": 'editDisplay', + 'onButtonClick': "onButtonClick" } as const + if(lastCode !== arg.code){ + lastModeList = [] + const codesplit = arg.code.split('\n') + for(let i = 0; i < codesplit.length; i++){ + const line = codesplit[i] + if(line.startsWith('//@use')){ + lastModeList.push(line.replace('//@use','').trim()) + } + } + lastCode = arg.code + } + const runCode = codes[arg.mode] + + if(!lastModeList.includes(runCode)){ + return arg.data + } const result = await runVirtualJS(`${arg.code}\n${runCode}(${JSON.stringify(arg.data)})`) if(!result){ @@ -99,4 +255,23 @@ export async function runCharacterJS(arg:{ return arg.data } +} + +export async function watchParamButton() { + while(true){ + const qs = document.querySelectorAll('*[risu-btn]:not([risu-btn-run="true"])') + for(let i = 0; i < qs.length; i++){ + const q = qs[i] + const code = q.getAttribute('risu-btn') + q.setAttribute('risu-btn-run','true') + q.addEventListener('click',async ()=>{ + await runCharacterJS({ + code: null, + mode: 'onButtonClick', + data: code + }) + }) + } + await sleep(100) + } } \ No newline at end of file diff --git a/src/ts/plugins/embedworker.ts b/src/ts/plugins/embedworker.ts index 9d16627f..1e47ee41 100644 --- a/src/ts/plugins/embedworker.ts +++ b/src/ts/plugins/embedworker.ts @@ -71,20 +71,27 @@ const whitelist = [ "Request", "Response", "Blob", - "postMessage" + "postMessage", + "Node", + "Element", + "Text", + "Comment", ] -const evaluation = global.eval +const evaluation = globaly.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 - }); +Object.getOwnPropertyNames( globaly ).forEach( function( prop ) { + if( (!whitelist.includes(prop)) && (!prop.startsWith('HTML')) && (!prop.startsWith('XML')) ) { + try { + Object.defineProperty( globaly, prop, { + get : function() { + throw "Security Exception: cannot access "+prop; + return 1; + }, + configurable : false + }); + } catch (error) { + } } }); @@ -93,6 +100,7 @@ let workerResults:{ result: any }[] = [] + self.onmessage = async (event) => { const da = event.data if(da.type === 'result'){ @@ -101,7 +109,7 @@ self.onmessage = async (event) => { } if(da.type === 'api'){ //add api - Object.defineProperty( global, da.name, { + Object.defineProperty( globaly, da.name, { get : function() { return function (...args:any[]) { return new Promise((resolve)=>{ @@ -118,7 +126,7 @@ self.onmessage = async (event) => { clearInterval(interval) resolve(result.result) } - },100) + },10) }) } } diff --git a/src/ts/storage/database.ts b/src/ts/storage/database.ts index e3d931fd..9400cc56 100644 --- a/src/ts/storage/database.ts +++ b/src/ts/storage/database.ts @@ -578,6 +578,7 @@ export interface character{ additionalText:string oaiVoice?:string virtualscript?:string + scriptstate?:{[key:string]:string|number|boolean} } @@ -699,6 +700,7 @@ export interface Chat{ lastMemory?:string suggestMessages?:string[] isStreaming?:boolean + scriptstate?:{[key:string]:string|number|boolean} } export interface Message{ diff --git a/tsconfig.json b/tsconfig.json index c69e8f57..70162e4f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ * of JS in `.svelte` files. */ "allowJs": true, - "checkJs": true, + "checkJs": false, "isolatedModules": true }, "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte", "public/sw.js"],