From 11cc9cbc7caf5c03c5f30b722995b81fc160615e Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Mon, 13 Apr 2020 03:23:23 +0900 Subject: [PATCH] Resolve #5755 --- locales/ja-JP.yml | 6 +++ migration/1586708940386-pageAiScript.ts | 14 +++++ package.json | 2 +- src/client/components/page/page.button.vue | 4 +- src/client/components/page/page.counter.vue | 2 +- .../components/page/page.number-input.vue | 2 +- .../components/page/page.radio-button.vue | 2 +- src/client/components/page/page.switch.vue | 2 +- .../components/page/page.text-input.vue | 2 +- .../components/page/page.textarea-input.vue | 2 +- src/client/components/page/page.vue | 52 ++++++++++++++++--- .../page-editor/els/page-editor.el.button.vue | 13 +++-- .../page-editor/els/page-editor.el.if.vue | 10 ++-- .../els/page-editor.el.section.vue | 4 +- .../page-editor/els/page-editor.el.text.vue | 5 +- .../els/page-editor.el.textarea.vue | 1 + .../pages/page-editor/page-editor.blocks.vue | 4 +- .../page-editor/page-editor.script-block.vue | 28 +++++----- src/client/pages/page-editor/page-editor.vue | 35 +++++++------ src/client/pages/scratchpad.vue | 2 +- src/client/scripts/aoiscript/evaluator.ts | 37 ++++++++++++- src/client/scripts/aoiscript/index.ts | 1 + src/client/scripts/create-aiscript-env.ts | 3 ++ src/models/entities/page.ts | 6 +++ src/models/repositories/page.ts | 1 + src/server/api/endpoints/pages/create.ts | 5 ++ src/server/api/endpoints/pages/update.ts | 5 ++ yarn.lock | 8 +-- 28 files changed, 195 insertions(+), 63 deletions(-) create mode 100644 migration/1586708940386-pageAiScript.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 0827091532..848bf4bb43 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -481,6 +481,8 @@ descendingOrder: "降順" scratchpad: "スクラッチパッド" scratchpadDescription: "スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。" output: "出力" +script: "スクリプト" +disablePagesScript: "Pagesのスクリプトを無効にする" _theme: explore: "テーマを探す" @@ -813,6 +815,9 @@ _pages: message: "押したときに表示するメッセージ" variable: "送信する変数" no-variable: "なし" + callAiScript: "AiScript呼び出し" + _callAiScript: + functionName: "関数名" radioButton: "選択肢" _radioButton: @@ -975,6 +980,7 @@ _pages: _splitStrByLine: arg1: "テキスト" ref: "変数" + aiScriptVar: "AiScript変数" fn: "関数" _fn: slots: "スロット" diff --git a/migration/1586708940386-pageAiScript.ts b/migration/1586708940386-pageAiScript.ts new file mode 100644 index 0000000000..fdd6e76b9b --- /dev/null +++ b/migration/1586708940386-pageAiScript.ts @@ -0,0 +1,14 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class pageAiScript1586708940386 implements MigrationInterface { + name = 'pageAiScript1586708940386' + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(`ALTER TABLE "page" ADD "script" character varying(16384) NOT NULL DEFAULT ''`, undefined); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(`ALTER TABLE "page" DROP COLUMN "script"`, undefined); + } + +} diff --git a/package.json b/package.json index 98ca48014c..8fa0129cb9 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@koa/cors": "3.0.0", "@koa/multer": "2.0.2", "@koa/router": "8.0.8", - "@syuilo/aiscript": "0.1.2", + "@syuilo/aiscript": "0.1.4", "@types/bcryptjs": "2.4.2", "@types/bull": "3.12.1", "@types/cbor": "5.0.0", diff --git a/src/client/components/page/page.button.vue b/src/client/components/page/page.button.vue index eeb56d5eca..148fdc8e9c 100644 --- a/src/client/components/page/page.button.vue +++ b/src/client/components/page/page.button.vue @@ -28,7 +28,7 @@ export default Vue.extend({ text: this.script.interpolate(this.value.content) }); } else if (this.value.action === 'resetRandom') { - this.script.aiScript.updateRandomSeed(Math.random()); + this.script.aoiScript.updateRandomSeed(Math.random()); this.script.eval(); } else if (this.value.action === 'pushEvent') { this.$root.api('page-push', { @@ -43,6 +43,8 @@ export default Vue.extend({ type: 'success', text: this.script.interpolate(this.value.message) }); + } else if (this.value.action === 'callAiScript') { + this.script.callAiScript(this.value.fn); } } } diff --git a/src/client/components/page/page.counter.vue b/src/client/components/page/page.counter.vue index 781a1bd549..f7557c003a 100644 --- a/src/client/components/page/page.counter.vue +++ b/src/client/components/page/page.counter.vue @@ -27,7 +27,7 @@ export default Vue.extend({ }, watch: { v() { - this.script.aiScript.updatePageVar(this.value.name, this.v); + this.script.aoiScript.updatePageVar(this.value.name, this.v); this.script.eval(); } }, diff --git a/src/client/components/page/page.number-input.vue b/src/client/components/page/page.number-input.vue index 9ee2730fac..9ea1ebb642 100644 --- a/src/client/components/page/page.number-input.vue +++ b/src/client/components/page/page.number-input.vue @@ -27,7 +27,7 @@ export default Vue.extend({ }, watch: { v() { - this.script.aiScript.updatePageVar(this.value.name, this.v); + this.script.aoiScript.updatePageVar(this.value.name, this.v); this.script.eval(); } } diff --git a/src/client/components/page/page.radio-button.vue b/src/client/components/page/page.radio-button.vue index fda0a03927..dd5cbcbded 100644 --- a/src/client/components/page/page.radio-button.vue +++ b/src/client/components/page/page.radio-button.vue @@ -28,7 +28,7 @@ export default Vue.extend({ }, watch: { v() { - this.script.aiScript.updatePageVar(this.value.name, this.v); + this.script.aoiScript.updatePageVar(this.value.name, this.v); this.script.eval(); } } diff --git a/src/client/components/page/page.switch.vue b/src/client/components/page/page.switch.vue index 416c36e9ad..79d871df8f 100644 --- a/src/client/components/page/page.switch.vue +++ b/src/client/components/page/page.switch.vue @@ -27,7 +27,7 @@ export default Vue.extend({ }, watch: { v() { - this.script.aiScript.updatePageVar(this.value.name, this.v); + this.script.aoiScript.updatePageVar(this.value.name, this.v); this.script.eval(); } } diff --git a/src/client/components/page/page.text-input.vue b/src/client/components/page/page.text-input.vue index fcc181d673..843d541de6 100644 --- a/src/client/components/page/page.text-input.vue +++ b/src/client/components/page/page.text-input.vue @@ -27,7 +27,7 @@ export default Vue.extend({ }, watch: { v() { - this.script.aiScript.updatePageVar(this.value.name, this.v); + this.script.aoiScript.updatePageVar(this.value.name, this.v); this.script.eval(); } } diff --git a/src/client/components/page/page.textarea-input.vue b/src/client/components/page/page.textarea-input.vue index d1cf9813c4..5ba22e7c58 100644 --- a/src/client/components/page/page.textarea-input.vue +++ b/src/client/components/page/page.textarea-input.vue @@ -27,7 +27,7 @@ export default Vue.extend({ }, watch: { v() { - this.script.aiScript.updatePageVar(this.value.name, this.v); + this.script.aoiScript.updatePageVar(this.value.name, this.v); this.script.eval(); } } diff --git a/src/client/components/page/page.vue b/src/client/components/page/page.vue index 977d384b30..0f1769fc8f 100644 --- a/src/client/components/page/page.vue +++ b/src/client/components/page/page.vue @@ -6,30 +6,57 @@ <script lang="ts"> import Vue from 'vue'; -import i18n from '../../i18n'; +import { AiScript, parse, values } from '@syuilo/aiscript'; import { faHeart as faHeartS } from '@fortawesome/free-solid-svg-icons'; import { faHeart } from '@fortawesome/free-regular-svg-icons'; +import i18n from '../../i18n'; import XBlock from './page.block.vue'; import { ASEvaluator } from '../../scripts/aoiscript/evaluator'; import { collectPageVars } from '../../scripts/collect-page-vars'; import { url } from '../../config'; class Script { - public aiScript: ASEvaluator; + public aoiScript: ASEvaluator; private onError: any; public vars: Record<string, any>; public page: Record<string, any>; - constructor(page, aiScript, onError) { + constructor(page, aoiScript, onError, cb) { this.page = page; - this.aiScript = aiScript; + this.aoiScript = aoiScript; this.onError = onError; - this.eval(); + + if (this.page.script) { + let ast; + try { + ast = parse(this.page.script); + } catch (e) { + console.error(e); + /*this.$root.dialog({ + type: 'error', + text: 'Syntax error :(' + });*/ + return; + } + this.aoiScript.aiscript.exec(ast).then(() => { + this.eval(); + cb(); + }).catch(e => { + console.error(e); + /*this.$root.dialog({ + type: 'error', + text: e + });*/ + }); + } else { + this.eval(); + cb(); + } } public eval() { try { - this.vars = this.aiScript.evaluateVars(); + this.vars = this.aoiScript.evaluateVars(); } catch (e) { this.onError(e); } @@ -42,6 +69,10 @@ class Script { return v == null ? 'NULL' : v.toString(); }); } + + public callAiScript(fn: string) { + this.aoiScript.aiscript.execFn(this.aoiScript.aiscript.scope.get(fn), []); + } } export default Vue.extend({ @@ -67,14 +98,21 @@ export default Vue.extend({ created() { const pageVars = this.getPageVars(); - this.script = new Script(this.page, new ASEvaluator(this.page.variables, pageVars, { + + const s = new Script(this.page, new ASEvaluator(this, this.page.variables, pageVars, { randomSeed: Math.random(), visitor: this.$store.state.i, page: this.page, url: url }), e => { console.dir(e); + }, () => { + this.script = s; }); + + s.aoiScript.aiscript.scope.opts.onUpdated = (name, value) => { + s.eval(); + }; }, methods: { diff --git a/src/client/pages/page-editor/els/page-editor.el.button.vue b/src/client/pages/page-editor/els/page-editor.el.button.vue index 8e74124b79..508d77a7a0 100644 --- a/src/client/pages/page-editor/els/page-editor.el.button.vue +++ b/src/client/pages/page-editor/els/page-editor.el.button.vue @@ -10,6 +10,7 @@ <option value="dialog">{{ $t('_pages.blocks._button._action.dialog') }}</option> <option value="resetRandom">{{ $t('_pages.blocks._button._action.resetRandom') }}</option> <option value="pushEvent">{{ $t('_pages.blocks._button._action.pushEvent') }}</option> + <option value="callAiScript">{{ $t('_pages.blocks._button._action.callAiScript') }}</option> </mk-select> <template v-if="value.action === 'dialog'"> <mk-input v-model="value.content"><span>{{ $t('_pages.blocks._button._action._dialog.content') }}</span></mk-input> @@ -20,15 +21,18 @@ <mk-select v-model="value.var"> <template #label>{{ $t('_pages.blocks._button._action._pushEvent.variable') }}</template> <option :value="null">{{ $t('_pages.blocks._button._action._pushEvent.no-variable') }}</option> - <option v-for="v in aiScript.getVarsByType()" :value="v.name">{{ v.name }}</option> + <option v-for="v in aoiScript.getVarsByType()" :value="v.name">{{ v.name }}</option> <optgroup :label="$t('_pages.script.pageVariables')"> - <option v-for="v in aiScript.getPageVarsByType()" :value="v">{{ v }}</option> + <option v-for="v in aoiScript.getPageVarsByType()" :value="v">{{ v }}</option> </optgroup> <optgroup :label="$t('_pages.script.enviromentVariables')"> - <option v-for="v in aiScript.getEnvVarsByType()" :value="v">{{ v }}</option> + <option v-for="v in aoiScript.getEnvVarsByType()" :value="v">{{ v }}</option> </optgroup> </mk-select> </template> + <template v-else-if="value.action === 'callAiScript'"> + <mk-input v-model="value.fn"><span>{{ $t('_pages.blocks._button._action._callAiScript.functionName') }}</span></mk-input> + </template> </section> </x-container> </template> @@ -53,7 +57,7 @@ export default Vue.extend({ value: { required: true }, - aiScript: { + aoiScript: { required: true, }, }, @@ -72,6 +76,7 @@ export default Vue.extend({ if (this.value.message == null) Vue.set(this.value, 'message', null); if (this.value.primary == null) Vue.set(this.value, 'primary', false); if (this.value.var == null) Vue.set(this.value, 'var', null); + if (this.value.fn == null) Vue.set(this.value, 'fn', null); }, }); </script> diff --git a/src/client/pages/page-editor/els/page-editor.el.if.vue b/src/client/pages/page-editor/els/page-editor.el.if.vue index 5e531a7ab5..0c40e41d8a 100644 --- a/src/client/pages/page-editor/els/page-editor.el.if.vue +++ b/src/client/pages/page-editor/els/page-editor.el.if.vue @@ -10,16 +10,16 @@ <section class="romcojzs"> <mk-select v-model="value.var"> <template #label>{{ $t('_pages.blocks._if.variable') }}</template> - <option v-for="v in aiScript.getVarsByType('boolean')" :value="v.name">{{ v.name }}</option> + <option v-for="v in aoiScript.getVarsByType('boolean')" :value="v.name">{{ v.name }}</option> <optgroup :label="$t('_pages.script.pageVariables')"> - <option v-for="v in aiScript.getPageVarsByType('boolean')" :value="v">{{ v }}</option> + <option v-for="v in aoiScript.getPageVarsByType('boolean')" :value="v">{{ v }}</option> </optgroup> <optgroup :label="$t('_pages.script.enviromentVariables')"> - <option v-for="v in aiScript.getEnvVarsByType('boolean')" :value="v">{{ v }}</option> + <option v-for="v in aoiScript.getEnvVarsByType('boolean')" :value="v">{{ v }}</option> </optgroup> </mk-select> - <x-blocks class="children" v-model="value.children" :ai-script="aiScript"/> + <x-blocks class="children" v-model="value.children" :aoi-script="aoiScript"/> </section> </x-container> </template> @@ -45,7 +45,7 @@ export default Vue.extend({ value: { required: true }, - aiScript: { + aoiScript: { required: true, }, }, diff --git a/src/client/pages/page-editor/els/page-editor.el.section.vue b/src/client/pages/page-editor/els/page-editor.el.section.vue index 8de796e6d6..97f063a287 100644 --- a/src/client/pages/page-editor/els/page-editor.el.section.vue +++ b/src/client/pages/page-editor/els/page-editor.el.section.vue @@ -11,7 +11,7 @@ </template> <section class="ilrvjyvi"> - <x-blocks class="children" v-model="value.children" :ai-script="aiScript"/> + <x-blocks class="children" v-model="value.children" :aoi-script="aoiScript"/> </section> </x-container> </template> @@ -37,7 +37,7 @@ export default Vue.extend({ value: { required: true }, - aiScript: { + aoiScript: { required: true, }, }, diff --git a/src/client/pages/page-editor/els/page-editor.el.text.vue b/src/client/pages/page-editor/els/page-editor.el.text.vue index 00b6cd8a36..c6722236eb 100644 --- a/src/client/pages/page-editor/els/page-editor.el.text.vue +++ b/src/client/pages/page-editor/els/page-editor.el.text.vue @@ -2,7 +2,7 @@ <x-container @remove="() => $emit('remove')" :draggable="true"> <template #header><fa :icon="faAlignLeft"/> {{ $t('_pages.blocks.text') }}</template> - <section class="ihymsbbe"> + <section class="vckmsadr"> <textarea v-model="value.text"></textarea> </section> </x-container> @@ -40,7 +40,7 @@ export default Vue.extend({ </script> <style lang="scss" scoped> -.ihymsbbe { +.vckmsadr { > textarea { display: block; -webkit-appearance: none; @@ -55,6 +55,7 @@ export default Vue.extend({ background: transparent; color: var(--fg); font-size: 14px; + box-sizing: border-box; } } </style> diff --git a/src/client/pages/page-editor/els/page-editor.el.textarea.vue b/src/client/pages/page-editor/els/page-editor.el.textarea.vue index fd75849684..d31da5dfa3 100644 --- a/src/client/pages/page-editor/els/page-editor.el.textarea.vue +++ b/src/client/pages/page-editor/els/page-editor.el.textarea.vue @@ -55,6 +55,7 @@ export default Vue.extend({ background: transparent; color: var(--fg); font-size: 14px; + box-sizing: border-box; } } </style> diff --git a/src/client/pages/page-editor/page-editor.blocks.vue b/src/client/pages/page-editor/page-editor.blocks.vue index 4d7293231f..bfc75cada4 100644 --- a/src/client/pages/page-editor/page-editor.blocks.vue +++ b/src/client/pages/page-editor/page-editor.blocks.vue @@ -1,6 +1,6 @@ <template> <x-draggable tag="div" :list="blocks" handle=".drag-handle" :group="{ name: 'blocks' }" animation="150" swap-threshold="0.5"> - <component v-for="block in blocks" :is="'x-' + block.type" :value="block" @input="updateItem" @remove="() => removeItem(block)" :key="block.id" :ai-script="aiScript"/> + <component v-for="block in blocks" :is="'x-' + block.type" :value="block" @input="updateItem" @remove="() => removeItem(block)" :key="block.id" :aoi-script="aoiScript"/> </x-draggable> </template> @@ -31,7 +31,7 @@ export default Vue.extend({ type: Array, required: true }, - aiScript: { + aoiScript: { required: true, }, }, diff --git a/src/client/pages/page-editor/page-editor.script-block.vue b/src/client/pages/page-editor/page-editor.script-block.vue index 4f30b7136b..7e3bbf0c88 100644 --- a/src/client/pages/page-editor/page-editor.script-block.vue +++ b/src/client/pages/page-editor/page-editor.script-block.vue @@ -2,7 +2,7 @@ <x-container :removable="removable" @remove="() => $emit('remove')" :error="error" :warn="warn" :draggable="draggable"> <template #header><fa v-if="icon" :icon="icon"/> <template v-if="title">{{ title }} <span class="turmquns" v-if="typeText">({{ typeText }})</span></template><template v-else-if="typeText">{{ typeText }}</template></template> <template #func> - <button @click="changeType()"> + <button @click="changeType()" class="_button"> <fa :icon="faPencilAlt"/> </button> </template> @@ -24,30 +24,33 @@ </section> <section v-else-if="value.type === 'ref'" class="hpdwcrvs"> <select v-model="value.value"> - <option v-for="v in aiScript.getVarsByType(getExpectedType ? getExpectedType() : null).filter(x => x.name !== name)" :value="v.name">{{ v.name }}</option> + <option v-for="v in aoiScript.getVarsByType(getExpectedType ? getExpectedType() : null).filter(x => x.name !== name)" :value="v.name">{{ v.name }}</option> <optgroup :label="$t('_pages.script.argVariables')"> <option v-for="v in fnSlots" :value="v.name">{{ v.name }}</option> </optgroup> <optgroup :label="$t('_pages.script.pageVariables')"> - <option v-for="v in aiScript.getPageVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option> + <option v-for="v in aoiScript.getPageVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option> </optgroup> <optgroup :label="$t('_pages.script.enviromentVariables')"> - <option v-for="v in aiScript.getEnvVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option> + <option v-for="v in aoiScript.getEnvVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option> </optgroup> </select> </section> + <section v-else-if="value.type === 'aiScriptVar'" class="tbwccoaw"> + <input v-model="value.value"/> + </section> <section v-else-if="value.type === 'fn'" class="" style="padding:0 16px 16px 16px;"> <mk-textarea v-model="slots"> <span>{{ $t('_pages.script.blocks._fn.slots') }}</span> <template #desc>{{ $t('_pages.script.blocks._fn.slots-info') }}</template> </mk-textarea> - <x-v v-if="value.value.expression" v-model="value.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :ai-script="aiScript" :fn-slots="value.value.slots" :name="name"/> + <x-v v-if="value.value.expression" v-model="value.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :aoi-script="aoiScript" :fn-slots="value.value.slots" :name="name"/> </section> <section v-else-if="value.type.startsWith('fn:')" class="" style="padding:16px;"> - <x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="aiScript.getVarByName(value.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :ai-script="aiScript" :name="name" :key="i"/> + <x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="aoiScript.getVarByName(value.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :aoi-script="aoiScript" :name="name" :key="i"/> </section> <section v-else class="" style="padding:16px;"> - <x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="$t(`_pages.script.blocks._${value.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :ai-script="aiScript" :name="name" :fn-slots="fnSlots" :key="i"/> + <x-v v-for="(x, i) in value.args" v-model="value.args[i]" :title="$t(`_pages.script.blocks._${value.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :aoi-script="aoiScript" :name="name" :fn-slots="fnSlots" :key="i"/> </section> </x-container> </template> @@ -85,7 +88,7 @@ export default Vue.extend({ required: false, default: false }, - aiScript: { + aoiScript: { required: true, }, name: { @@ -153,7 +156,7 @@ export default Vue.extend({ if (this.value.type && this.value.type.startsWith('fn:')) { const fnName = this.value.type.split(':')[1]; - const fn = this.aiScript.getVarByName(fnName); + const fn = this.aoiScript.getVarByName(fnName); const empties = []; for (let i = 0; i < fn.value.slots.length; i++) { @@ -199,9 +202,9 @@ export default Vue.extend({ deep: true }); - this.$watch('aiScript.variables', () => { + this.$watch('aoiScript.variables', () => { if (this.type != null && this.value) { - this.error = this.aiScript.typeCheck(this.value); + this.error = this.aoiScript.typeCheck(this.value); } }, { deep: true @@ -223,7 +226,7 @@ export default Vue.extend({ }, _getExpectedType(slot: number) { - return this.aiScript.getExpectedType(this.value, slot); + return this.aoiScript.getExpectedType(this.value, slot); } } }); @@ -258,6 +261,7 @@ export default Vue.extend({ font-size: 16px; background: transparent; color: var(--fg); + box-sizing: border-box; } > textarea { diff --git a/src/client/pages/page-editor/page-editor.vue b/src/client/pages/page-editor/page-editor.vue index b3350e0c63..6177663b71 100644 --- a/src/client/pages/page-editor/page-editor.vue +++ b/src/client/pages/page-editor/page-editor.vue @@ -46,7 +46,7 @@ </div> </template> - <x-blocks class="content" v-model="content" :ai-script="aiScript"/> + <x-blocks class="content" v-model="content" :aoi-script="aoiScript"/> <mk-button @click="add()" v-if="!readonly"><fa :icon="faPlus"/></mk-button> </section> @@ -62,7 +62,7 @@ @input="v => updateVariable(v)" @remove="() => removeVariable(variable)" :key="variable.name" - :ai-script="aiScript" + :aoi-script="aoiScript" :name="variable.name" :title="variable.name" :draggable="true" @@ -73,11 +73,10 @@ </div> </mk-container> - <mk-container :body-togglable="true" :expanded="false"> - <template #header><fa :icon="faCode"/> {{ $t('_pages.inspector') }}</template> - <div style="padding:0 32px 32px 32px;"> - <mk-textarea :value="JSON.stringify(content, null, 2)" readonly tall>{{ $t('_pages.content') }}</mk-textarea> - <mk-textarea :value="JSON.stringify(variables, null, 2)" readonly tall>{{ $t('_pages.variables') }}</mk-textarea> + <mk-container :body-togglable="true" :expanded="true"> + <template #header><fa :icon="faCode"/> {{ $t('script') }}</template> + <div> + <prism-editor v-model="script" :line-numbers="false" language="js"/> </div> </mk-container> </div> @@ -86,6 +85,9 @@ <script lang="ts"> import Vue from 'vue'; import * as XDraggable from 'vuedraggable'; +import "prismjs"; +import "prismjs/themes/prism.css"; +import PrismEditor from 'vue-prism-editor'; import { faICursor, faPlus, faMagic, faCog, faCode, faExternalLinkSquareAlt } from '@fortawesome/free-solid-svg-icons'; import { faSave, faStickyNote, faTrashAlt } from '@fortawesome/free-regular-svg-icons'; import { v4 as uuid } from 'uuid'; @@ -108,7 +110,7 @@ export default Vue.extend({ i18n, components: { - XDraggable, XVariable, XBlocks, MkTextarea, MkContainer, MkButton, MkSelect, MkSwitch, MkInput + XDraggable, XVariable, XBlocks, MkTextarea, MkContainer, MkButton, MkSelect, MkSwitch, MkInput, PrismEditor }, props: { @@ -143,7 +145,8 @@ export default Vue.extend({ alignCenter: false, hideTitleWhenPinned: false, variables: [], - aiScript: null, + aoiScript: null, + script: '', showOptions: false, url, faPlus, faICursor, faSave, faStickyNote, faMagic, faCog, faTrashAlt, faExternalLinkSquareAlt, faCode @@ -163,14 +166,14 @@ export default Vue.extend({ }, async created() { - this.aiScript = new ASTypeChecker(); + this.aoiScript = new ASTypeChecker(); this.$watch('variables', () => { - this.aiScript.variables = this.variables; + this.aoiScript.variables = this.variables; }, { deep: true }); this.$watch('content', () => { - this.aiScript.pageVars = collectPageVars(this.content); + this.aoiScript.pageVars = collectPageVars(this.content); }, { deep: true }); if (this.initPageId) { @@ -193,6 +196,7 @@ export default Vue.extend({ this.currentName = this.page.name; this.summary = this.page.summary; this.font = this.page.font; + this.script = this.page.script; this.hideTitleWhenPinned = this.page.hideTitleWhenPinned; this.alignCenter = this.page.alignCenter; this.content = this.page.content; @@ -223,6 +227,7 @@ export default Vue.extend({ name: this.name.trim(), summary: this.summary, font: this.font, + script: this.script, hideTitleWhenPinned: this.hideTitleWhenPinned, alignCenter: this.alignCenter, content: this.content, @@ -317,7 +322,7 @@ export default Vue.extend({ name = name.trim(); - if (this.aiScript.isUsedName(name)) { + if (this.aoiScript.isUsedName(name)) { this.$root.dialog({ type: 'error', text: this.$t('_pages.variableNameIsAlreadyUsed') @@ -382,7 +387,7 @@ export default Vue.extend({ } else { list.push({ category: block.category, - label: this.$t(`script.categories.${block.category}`), + label: this.$t(`_pages.script.categories.${block.category}`), items: [{ value: block.type, text: this.$t(`_pages.script.blocks.${block.type}`) @@ -394,7 +399,7 @@ export default Vue.extend({ const userFns = this.variables.filter(x => x.type === 'fn'); if (userFns.length > 0) { list.unshift({ - label: this.$t(`script.categories.fn`), + label: this.$t(`_pages.script.categories.fn`), items: userFns.map(v => ({ value: 'fn:' + v.name, text: v.name diff --git a/src/client/pages/scratchpad.vue b/src/client/pages/scratchpad.vue index 63ce5d99d7..e48beababa 100644 --- a/src/client/pages/scratchpad.vue +++ b/src/client/pages/scratchpad.vue @@ -23,9 +23,9 @@ <script lang="ts"> import Vue from 'vue'; +import { faTerminal, faPlay } from '@fortawesome/free-solid-svg-icons'; import "prismjs"; import "prismjs/themes/prism.css"; -import { faTerminal, faPlay } from '@fortawesome/free-solid-svg-icons'; import PrismEditor from 'vue-prism-editor'; import { AiScript, parse, utils, values } from '@syuilo/aiscript'; import i18n from '../i18n'; diff --git a/src/client/scripts/aoiscript/evaluator.ts b/src/client/scripts/aoiscript/evaluator.ts index 2e952da404..8d8a5b2e08 100644 --- a/src/client/scripts/aoiscript/evaluator.ts +++ b/src/client/scripts/aoiscript/evaluator.ts @@ -2,6 +2,8 @@ import autobind from 'autobind-decorator'; import * as seedrandom from 'seedrandom'; import { Variable, PageVar, envVarsDef, funcDefs, Block, isFnBlock } from '.'; import { version } from '../../config'; +import { AiScript, utils, parse, values } from '@syuilo/aiscript'; +import { createAiScriptEnv } from '../create-aiscript-env'; type Fn = { slots: string[]; @@ -15,15 +17,41 @@ export class ASEvaluator { private variables: Variable[]; private pageVars: PageVar[]; private envVars: Record<keyof typeof envVarsDef, any>; + public aiscript: AiScript; + private pageVarUpdatedCallback; private opts: { randomSeed: string; visitor?: any; page?: any; url?: string; }; - constructor(variables: Variable[], pageVars: PageVar[], opts: ASEvaluator['opts']) { + constructor(vm: any, variables: Variable[], pageVars: PageVar[], opts: ASEvaluator['opts']) { this.variables = variables; this.pageVars = pageVars; this.opts = opts; + this.aiscript = new AiScript({ ...createAiScriptEnv(vm, { + storageKey: 'pages:' + opts.page.id + }), ...{ + 'MkPages:updated': values.FN_NATIVE(([callback]) => { + this.pageVarUpdatedCallback = callback; + }) + }}, { + in: (q) => { + return new Promise(ok => { + vm.$root.dialog({ + title: q, + input: {} + }).then(({ canceled, result: a }) => { + ok(a); + }); + }); + }, + out: (value) => { + console.log(value); + }, + log: (type, params) => { + }, + maxStep: 16384 + }); const date = new Date(); @@ -50,6 +78,9 @@ export class ASEvaluator { const pageVar = this.pageVars.find(v => v.name === name); if (pageVar !== undefined) { pageVar.value = value; + if (this.pageVarUpdatedCallback) { + this.aiscript.execFn(this.pageVarUpdatedCallback, [values.STR(name), utils.jsToVal(value)]); + } } else { throw new AoiScriptError(`No such page var '${name}'`); } @@ -110,6 +141,10 @@ export class ASEvaluator { return scope.getState(block.value); } + if (block.type === 'aiScriptVar') { + return utils.valToJs(this.aiscript.scope.get(block.value)); + } + if (isFnBlock(block)) { // ユーザー関数定義 return { slots: block.value.slots.map(x => x.name), diff --git a/src/client/scripts/aoiscript/index.ts b/src/client/scripts/aoiscript/index.ts index 42d67b3fad..e6de5faaae 100644 --- a/src/client/scripts/aoiscript/index.ts +++ b/src/client/scripts/aoiscript/index.ts @@ -95,6 +95,7 @@ export const literalDefs: Record<string, { out: any; category: string; icon: any textList: { out: 'stringArray', category: 'value', icon: faList, }, number: { out: 'number', category: 'value', icon: faSortNumericUp, }, ref: { out: null, category: 'value', icon: faMagic, }, + aiScriptVar: { out: null, category: 'value', icon: faMagic, }, fn: { out: 'function', category: 'value', icon: faSquareRootAlt, }, }; diff --git a/src/client/scripts/create-aiscript-env.ts b/src/client/scripts/create-aiscript-env.ts index 3f7b0c80b5..5a492c64dd 100644 --- a/src/client/scripts/create-aiscript-env.ts +++ b/src/client/scripts/create-aiscript-env.ts @@ -1,6 +1,7 @@ import { utils, values } from '@syuilo/aiscript'; export function createAiScriptEnv(vm, opts) { + let apiRequests = 0; return { USER_ID: values.STR(vm.$store.state.i.id), USER_USERNAME: values.STR(vm.$store.state.i.username), @@ -21,6 +22,8 @@ export function createAiScriptEnv(vm, opts) { return confirm.canceled ? values.FALSE : values.TRUE }), 'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => { + apiRequests++; + if (apiRequests > 16) return values.NULL; const res = await vm.$root.api(ep.value, utils.valToJs(param), token || null); return utils.jsToVal(res); }), diff --git a/src/models/entities/page.ts b/src/models/entities/page.ts index 2163f9997f..ed0411a3d0 100644 --- a/src/models/entities/page.ts +++ b/src/models/entities/page.ts @@ -85,6 +85,12 @@ export class Page { }) public variables: Record<string, any>[]; + @Column('varchar', { + length: 16384, + default: '' + }) + public script: string; + /** * public ... 公開 * followers ... フォロワーのみ diff --git a/src/models/repositories/page.ts b/src/models/repositories/page.ts index cff42ddefd..662c41905f 100644 --- a/src/models/repositories/page.ts +++ b/src/models/repositories/page.ts @@ -74,6 +74,7 @@ export class PageRepository extends Repository<Page> { hideTitleWhenPinned: page.hideTitleWhenPinned, alignCenter: page.alignCenter, font: page.font, + script: page.script, eyeCatchingImageId: page.eyeCatchingImageId, eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : null, attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles)), diff --git a/src/server/api/endpoints/pages/create.ts b/src/server/api/endpoints/pages/create.ts index 11e476242e..6d41a4afeb 100644 --- a/src/server/api/endpoints/pages/create.ts +++ b/src/server/api/endpoints/pages/create.ts @@ -44,6 +44,10 @@ export const meta = { validator: $.arr($.obj()) }, + script: { + validator: $.str, + }, + eyeCatchingImageId: { validator: $.optional.nullable.type(ID), }, @@ -115,6 +119,7 @@ export default define(meta, async (ps, user) => { summary: ps.summary, content: ps.content, variables: ps.variables, + script: ps.script, eyeCatchingImageId: eyeCatchingImage ? eyeCatchingImage.id : null, userId: user.id, visibility: 'public', diff --git a/src/server/api/endpoints/pages/update.ts b/src/server/api/endpoints/pages/update.ts index a0fed28891..2d93dd4ae4 100644 --- a/src/server/api/endpoints/pages/update.ts +++ b/src/server/api/endpoints/pages/update.ts @@ -51,6 +51,10 @@ export const meta = { validator: $.arr($.obj()) }, + script: { + validator: $.str, + }, + eyeCatchingImageId: { validator: $.optional.nullable.type(ID), }, @@ -132,6 +136,7 @@ export default define(meta, async (ps, user) => { summary: ps.name === undefined ? page.summary : ps.summary, content: ps.content, variables: ps.variables, + script: ps.script, alignCenter: ps.alignCenter === undefined ? page.alignCenter : ps.alignCenter, hideTitleWhenPinned: ps.hideTitleWhenPinned === undefined ? page.hideTitleWhenPinned : ps.hideTitleWhenPinned, font: ps.font === undefined ? page.font : ps.font, diff --git a/yarn.lock b/yarn.lock index 2d79d7d947..598ea8ee5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -144,10 +144,10 @@ dependencies: type-detect "4.0.8" -"@syuilo/aiscript@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@syuilo/aiscript/-/aiscript-0.1.2.tgz#65c42793c38707d862b3a64f5edc845789372ade" - integrity sha512-W0G/JuVkD9jARPhKFaaHp+59Iv+2LapQ2zKjM08hoB/6hEzHjis0uRbw07TXyughQb17iU452rp1gJEUkXV3Mg== +"@syuilo/aiscript@0.1.4": + version "0.1.4" + resolved "https://registry.yarnpkg.com/@syuilo/aiscript/-/aiscript-0.1.4.tgz#ff027552f32990ae3e29145ce6efe0a7a516b442" + integrity sha512-SMDlBInsGTL3DOe0U394X7na0N6ryYg0RGQPPtCVhXkJpVDZiaqUe5vDO+DkRyuRlkmBbN82LWToou19j/Uv8g== dependencies: autobind-decorator "2.4.0" chalk "4.0.0"