feat(client): AiScriptプラグインからAPIアクセスできるように
This commit is contained in:
		
							parent
							
								
									b9c5e95b85
								
							
						
					
					
						commit
						b39850de01
					
				
					 5 changed files with 107 additions and 20 deletions
				
			
		|  | @ -533,6 +533,8 @@ generateAccessToken: "アクセストークンの発行" | ||||||
| permission: "権限" | permission: "権限" | ||||||
| enableAll: "全て有効にする" | enableAll: "全て有効にする" | ||||||
| disableAll: "全て無効にする" | disableAll: "全て無効にする" | ||||||
|  | tokenRequested: "アカウントへのアクセス許可" | ||||||
|  | pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。" | ||||||
| 
 | 
 | ||||||
| _theme: | _theme: | ||||||
|   explore: "テーマを探す" |   explore: "テーマを探す" | ||||||
|  |  | ||||||
|  | @ -2,6 +2,9 @@ | ||||||
| <x-window ref="window" :width="400" :height="450" :no-padding="true" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="false" @ok="ok()" :can-close="false"> | <x-window ref="window" :width="400" :height="450" :no-padding="true" @closed="() => { $emit('closed'); destroyDom(); }" :with-ok-button="true" :ok-button-disabled="false" @ok="ok()" :can-close="false"> | ||||||
| 	<template #header>{{ title || $t('generateAccessToken') }}</template> | 	<template #header>{{ title || $t('generateAccessToken') }}</template> | ||||||
| 	<div class="ugkkpisj"> | 	<div class="ugkkpisj"> | ||||||
|  | 		<div> | ||||||
|  | 			<mk-info warn v-if="information">{{ information }}</mk-info> | ||||||
|  | 		</div> | ||||||
| 		<div> | 		<div> | ||||||
| 			<mk-input v-model="name">{{ $t('name') }}</mk-input> | 			<mk-input v-model="name">{{ $t('name') }}</mk-input> | ||||||
| 		</div> | 		</div> | ||||||
|  | @ -9,7 +12,7 @@ | ||||||
| 			<div style="margin-bottom: 16px;"><b>{{ $t('permission') }}</b></div> | 			<div style="margin-bottom: 16px;"><b>{{ $t('permission') }}</b></div> | ||||||
| 			<mk-button inline @click="disableAll">{{ $t('disableAll') }}</mk-button> | 			<mk-button inline @click="disableAll">{{ $t('disableAll') }}</mk-button> | ||||||
| 			<mk-button inline @click="enableAll">{{ $t('enableAll') }}</mk-button> | 			<mk-button inline @click="enableAll">{{ $t('enableAll') }}</mk-button> | ||||||
| 			<mk-switch v-for="kind in kinds" :key="kind" v-model="permissions[kind]">{{ $t(`_permissions.${kind}`) }}</mk-switch> | 			<mk-switch v-for="kind in (initialPermissions || kinds)" :key="kind" v-model="permissions[kind]">{{ $t(`_permissions.${kind}`) }}</mk-switch> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| </x-window> | </x-window> | ||||||
|  | @ -23,6 +26,7 @@ import MkInput from './ui/input.vue'; | ||||||
| import MkTextarea from './ui/textarea.vue'; | import MkTextarea from './ui/textarea.vue'; | ||||||
| import MkSwitch from './ui/switch.vue'; | import MkSwitch from './ui/switch.vue'; | ||||||
| import MkButton from './ui/button.vue'; | import MkButton from './ui/button.vue'; | ||||||
|  | import MkInfo from './ui/info.vue'; | ||||||
| 
 | 
 | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	components: { | 	components: { | ||||||
|  | @ -31,6 +35,7 @@ export default Vue.extend({ | ||||||
| 		MkTextarea, | 		MkTextarea, | ||||||
| 		MkSwitch, | 		MkSwitch, | ||||||
| 		MkButton, | 		MkButton, | ||||||
|  | 		MkInfo, | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	props: { | 	props: { | ||||||
|  | @ -38,20 +43,41 @@ export default Vue.extend({ | ||||||
| 			type: String, | 			type: String, | ||||||
| 			required: false, | 			required: false, | ||||||
| 			default: null | 			default: null | ||||||
|  | 		}, | ||||||
|  | 		information: { | ||||||
|  | 			type: String, | ||||||
|  | 			required: false, | ||||||
|  | 			default: null | ||||||
|  | 		}, | ||||||
|  | 		initialName: { | ||||||
|  | 			type: String, | ||||||
|  | 			required: false, | ||||||
|  | 			default: null | ||||||
|  | 		}, | ||||||
|  | 		initialPermissions: { | ||||||
|  | 			type: Array, | ||||||
|  | 			required: false, | ||||||
|  | 			default: null | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			name: null, | 			name: this.initialName, | ||||||
| 			permissions: {}, | 			permissions: {}, | ||||||
| 			kinds | 			kinds | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	created() { | 	created() { | ||||||
| 		for (const kind of this.kinds) { | 		if (this.initialPermissions) { | ||||||
| 			Vue.set(this.permissions, kind, false); | 			for (const kind of this.initialPermissions) { | ||||||
|  | 				Vue.set(this.permissions, kind, true); | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			for (const kind of this.kinds) { | ||||||
|  | 				Vue.set(this.permissions, kind, false); | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -30,7 +30,10 @@ | ||||||
| 					<div>{{ $t('description') }}:</div> | 					<div>{{ $t('description') }}:</div> | ||||||
| 					<div>{{ selectedPlugin.description }}</div> | 					<div>{{ selectedPlugin.description }}</div> | ||||||
| 				</div> | 				</div> | ||||||
| 				<mk-button @click="uninstall()" style="margin-top: 8px;"><fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</mk-button> | 				<div style="margin-top: 8px;"> | ||||||
|  | 					<mk-button @click="config()" inline v-if="selectedPlugin.config"><fa :icon="faCog"/> {{ $t('settings') }}</mk-button> | ||||||
|  | 					<mk-button @click="uninstall()" inline><fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</mk-button> | ||||||
|  | 				</div> | ||||||
| 			</template> | 			</template> | ||||||
| 		</details> | 		</details> | ||||||
| 	</div> | 	</div> | ||||||
|  | @ -39,7 +42,7 @@ | ||||||
| 
 | 
 | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload } from '@fortawesome/free-solid-svg-icons'; | import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog } from '@fortawesome/free-solid-svg-icons'; | ||||||
| import MkButton from '../../components/ui/button.vue'; | import MkButton from '../../components/ui/button.vue'; | ||||||
| import MkTextarea from '../../components/ui/textarea.vue'; | import MkTextarea from '../../components/ui/textarea.vue'; | ||||||
| import MkSelect from '../../components/ui/select.vue'; | import MkSelect from '../../components/ui/select.vue'; | ||||||
|  | @ -58,7 +61,7 @@ export default Vue.extend({ | ||||||
| 		return { | 		return { | ||||||
| 			script: '', | 			script: '', | ||||||
| 			selectedPluginId: null, | 			selectedPluginId: null, | ||||||
| 			faPlug, faSave, faTrashAlt, faFolderOpen, faDownload | 			faPlug, faSave, faTrashAlt, faFolderOpen, faDownload, faCog | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -70,7 +73,7 @@ export default Vue.extend({ | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	methods: { | 	methods: { | ||||||
| 		install() { | 		async install() { | ||||||
| 			let ast; | 			let ast; | ||||||
| 			try { | 			try { | ||||||
| 				ast = parse(this.script); | 				ast = parse(this.script); | ||||||
|  | @ -82,7 +85,6 @@ export default Vue.extend({ | ||||||
| 				return; | 				return; | ||||||
| 			} | 			} | ||||||
| 			const meta = AiScript.collectMetadata(ast); | 			const meta = AiScript.collectMetadata(ast); | ||||||
| 			console.log(meta); |  | ||||||
| 			if (meta == null) { | 			if (meta == null) { | ||||||
| 				this.$root.dialog({ | 				this.$root.dialog({ | ||||||
| 					type: 'error', | 					type: 'error', | ||||||
|  | @ -98,7 +100,7 @@ export default Vue.extend({ | ||||||
| 				}); | 				}); | ||||||
| 				return; | 				return; | ||||||
| 			} | 			} | ||||||
| 			const { id, name, version, author, description } = data; | 			const { id, name, version, author, description, permissions, config } = data; | ||||||
| 			if (id == null || name == null || version == null || author == null) { | 			if (id == null || name == null || version == null || author == null) { | ||||||
| 				this.$root.dialog({ | 				this.$root.dialog({ | ||||||
| 					type: 'error', | 					type: 'error', | ||||||
|  | @ -106,16 +108,40 @@ export default Vue.extend({ | ||||||
| 				}); | 				}); | ||||||
| 				return; | 				return; | ||||||
| 			} | 			} | ||||||
|  | 
 | ||||||
|  | 			const token = permissions == null || permissions.length === 0 ? null : await new Promise(async (res, rej) => { | ||||||
|  | 				this.$root.new(await import('../../components/token-generate-window.vue').then(m => m.default), { | ||||||
|  | 					title: this.$t('tokenRequested'), | ||||||
|  | 					information: this.$t('pluginTokenRequestedDescription'), | ||||||
|  | 					initialName: name, | ||||||
|  | 					initialPermissions: permissions | ||||||
|  | 				}).$on('ok', async ({ name, permissions }) => { | ||||||
|  | 					const { token } = await this.$root.api('miauth/gen-token', { | ||||||
|  | 						session: null, | ||||||
|  | 						name: name, | ||||||
|  | 						permission: permissions, | ||||||
|  | 					}); | ||||||
|  | 
 | ||||||
|  | 					res(token); | ||||||
|  | 				}); | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
| 			this.$store.commit('deviceUser/installPlugin', { | 			this.$store.commit('deviceUser/installPlugin', { | ||||||
| 				meta: { | 				meta: { | ||||||
| 					id, name, version, author, description | 					id, name, version, author, description, permissions, config | ||||||
| 				}, | 				}, | ||||||
| 				ast | 				token, | ||||||
|  | 				ast // TODO: astにはMapが含まれることがあり、MapはJSONとしてシリアライズできないのでバグる。どうにかする | ||||||
| 			}); | 			}); | ||||||
|  | 
 | ||||||
| 			this.$root.dialog({ | 			this.$root.dialog({ | ||||||
| 				type: 'success', | 				type: 'success', | ||||||
| 				iconOnly: true, autoClose: true | 				iconOnly: true, autoClose: true | ||||||
| 			}); | 			}); | ||||||
|  | 
 | ||||||
|  | 			this.$nextTick(() => { | ||||||
|  | 				location.reload(); | ||||||
|  | 			}); | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
| 		uninstall() { | 		uninstall() { | ||||||
|  | @ -124,6 +150,29 @@ export default Vue.extend({ | ||||||
| 				type: 'success', | 				type: 'success', | ||||||
| 				iconOnly: true, autoClose: true | 				iconOnly: true, autoClose: true | ||||||
| 			}); | 			}); | ||||||
|  | 			this.$nextTick(() => { | ||||||
|  | 				location.reload(); | ||||||
|  | 			}); | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		// TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする | ||||||
|  | 		async config() { | ||||||
|  | 			const config = this.selectedPlugin.config; | ||||||
|  | 			for (const key in this.selectedPlugin.configData) { | ||||||
|  | 				config[key].default = this.selectedPlugin.configData[key]; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			const { canceled, result } = await this.$root.form(this.selectedPlugin.name, config); | ||||||
|  | 			if (canceled) return; | ||||||
|  | 
 | ||||||
|  | 			this.$store.commit('deviceUser/configPlugin', { | ||||||
|  | 				id: this.selectedPluginId, | ||||||
|  | 				config: result | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 			this.$nextTick(() => { | ||||||
|  | 				location.reload(); | ||||||
|  | 			}); | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import { utils, values } from '@syuilo/aiscript'; | import { utils, values } from '@syuilo/aiscript'; | ||||||
|  | import { jsToVal } from '@syuilo/aiscript/built/interpreter/util'; | ||||||
| 
 | 
 | ||||||
| export function createAiScriptEnv(vm, opts) { | export function createAiScriptEnv(vm, opts) { | ||||||
| 	let apiRequests = 0; | 	let apiRequests = 0; | ||||||
|  | @ -26,7 +27,7 @@ export function createAiScriptEnv(vm, opts) { | ||||||
| 			if (token) utils.assertString(token); | 			if (token) utils.assertString(token); | ||||||
| 			apiRequests++; | 			apiRequests++; | ||||||
| 			if (apiRequests > 16) return values.NULL; | 			if (apiRequests > 16) return values.NULL; | ||||||
| 			const res = await vm.$root.api(ep.value, utils.valToJs(param), token ? token.value : null); | 			const res = await vm.$root.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token || null)); | ||||||
| 			return utils.jsToVal(res); | 			return utils.jsToVal(res); | ||||||
| 		}), | 		}), | ||||||
| 		'Mk:save': values.FN_NATIVE(([key, value]) => { | 		'Mk:save': values.FN_NATIVE(([key, value]) => { | ||||||
|  | @ -42,8 +43,14 @@ export function createAiScriptEnv(vm, opts) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function createPluginEnv(vm, opts) { | export function createPluginEnv(vm, opts) { | ||||||
|  | 	const config = new Map(); | ||||||
|  | 	for (const key in opts.plugin.config) { | ||||||
|  | 		const val = opts.plugin.configData[key] || opts.plugin.config[key].default; | ||||||
|  | 		config.set(key, jsToVal(val)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return { | 	return { | ||||||
| 		...createAiScriptEnv(vm, opts), | 		...createAiScriptEnv(vm, { ...opts, token: opts.plugin.token }), | ||||||
| 		'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => { | 		'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => { | ||||||
| 			vm.$store.commit('registerPostFormAction', { pluginId: opts.plugin.id, title: title.value, handler }); | 			vm.$store.commit('registerPostFormAction', { pluginId: opts.plugin.id, title: title.value, handler }); | ||||||
| 		}), | 		}), | ||||||
|  | @ -53,5 +60,6 @@ export function createPluginEnv(vm, opts) { | ||||||
| 		'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => { | 		'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => { | ||||||
| 			vm.$store.commit('registerNoteAction', { pluginId: opts.plugin.id, title: title.value, handler }); | 			vm.$store.commit('registerNoteAction', { pluginId: opts.plugin.id, title: title.value, handler }); | ||||||
| 		}), | 		}), | ||||||
|  | 		'Plugin:config': values.OBJ(config), | ||||||
| 	}; | 	}; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -587,13 +587,11 @@ export default () => new Vuex.Store({ | ||||||
| 				}, | 				}, | ||||||
| 				//#endregion
 | 				//#endregion
 | ||||||
| 
 | 
 | ||||||
| 				installPlugin(state, { meta, ast }) { | 				installPlugin(state, { meta, ast, token }) { | ||||||
| 					state.plugins.push({ | 					state.plugins.push({ | ||||||
| 						id: meta.id, | 						...meta, | ||||||
| 						name: meta.name, | 						configData: {}, | ||||||
| 						version: meta.version, | 						token: token, | ||||||
| 						author: meta.author, |  | ||||||
| 						description: meta.description, |  | ||||||
| 						ast: ast | 						ast: ast | ||||||
| 					}); | 					}); | ||||||
| 				}, | 				}, | ||||||
|  | @ -601,6 +599,10 @@ export default () => new Vuex.Store({ | ||||||
| 				uninstallPlugin(state, id) { | 				uninstallPlugin(state, id) { | ||||||
| 					state.plugins = state.plugins.filter(x => x.id != id); | 					state.plugins = state.plugins.filter(x => x.id != id); | ||||||
| 				}, | 				}, | ||||||
|  | 
 | ||||||
|  | 				configPlugin(state, { id, config }) { | ||||||
|  | 					state.plugins.find(p => p.id === id).configData = config; | ||||||
|  | 				}, | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue