This commit is contained in:
_ 2023-11-17 22:45:15 +09:00 committed by GitHub
commit e8401a6328
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 501 additions and 339 deletions

View File

@ -15,6 +15,10 @@
## 2023.11.1
### General
- Feat: プラグインのスクリプトとデータを端末間で同期をできるようになりました
- 既存のプラグインはローカルに保存されています。
- ローカルのプラグインはプラグインの管理にあるボタンからアカウントに移動できます。
- `Mk:load`と`Mk:save`の引数にオプションオブジェクト`option?: { toAccount?: bool }`が追加され、`true`にするとアカウントでデータを共有できます。(デフォルトは`false`)
- Feat: 管理者がコントロールパネルからメールアドレスの照会を行えるようになりました
- Enhance: ローカリゼーションの更新
- Enhance: 依存関係の更新

9
locales/index.d.ts vendored
View File

@ -1161,6 +1161,15 @@ export interface Locale {
"signupPendingError": string;
"cwNotationRequired": string;
"doReaction": string;
"pluginSyncSettings": string;
"pluginSyncSettingsInfo": string;
"duplicateSyncedPlugin": string;
"overrideSourceCodeOnly": string;
"syncSetting": string;
"syncing": string;
"movePluginToAccount": string;
"movePluginToAccountConfirm": string;
"overridePluginConfirm": string;
"_announcement": {
"forExistingUsers": string;
"forExistingUsersDescription": string;

View File

@ -1158,6 +1158,15 @@ useGroupedNotifications: "通知をグルーピングして表示する"
signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。"
cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。"
doReaction: "リアクションする"
pluginSyncSettings: "プラグインの同期設定"
pluginSyncSettingsInfo: "このプラグインをローカルのみにインストールするか他端末と同期するかを選択できます。"
duplicateSyncedPlugin: "このプラグインは同期されているプラグインと重複しています。"
overrideSourceCodeOnly: "コードのみを上書きする"
syncSetting: "同期設定"
syncing: "他端末と同期"
movePluginToAccount: "アカウントに移行"
movePluginToAccountConfirm: "プラグインをアカウントに移行して他端末と同期しますか?プラグインとプラグイン設定以外は引き継がれません。"
overridePluginConfirm: "すでにプラグインがアカウントに存在します。上書きしますか?アカウントにあるプラグインデータは削除されます。"
_announcement:
forExistingUsers: "既存ユーザーのみ"

View File

@ -131,7 +131,7 @@ export class RegistryApiService {
}
@bindThis
public async remove(userId: MiUser['id'], domain: string | null, scope: string[], key: string) {
public async remove(userId: MiUser['id'], domain: string | null, scope: string[], key?: string) {
const query = this.registryItemsRepository.createQueryBuilder().delete();
if (domain) {
query.where('domain = :domain', { domain: domain });
@ -139,7 +139,9 @@ export class RegistryApiService {
query.where('domain IS NULL');
}
query.andWhere('userId = :userId', { userId: userId });
query.andWhere('key = :key', { key: key });
if (key) {
query.andWhere('key = :key', { key: key });
}
query.andWhere('scope = :scope', { scope: scope });
await query.execute();

View File

@ -231,6 +231,7 @@ import * as ep___i_registry_get from './endpoints/i/registry/get.js';
import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js';
import * as ep___i_registry_keys from './endpoints/i/registry/keys.js';
import * as ep___i_registry_remove from './endpoints/i/registry/remove.js';
import * as ep___i_registry_removeAllKeysInScope from './endpoints/i/registry/remove-all-keys-in-scope.js';
import * as ep___i_registry_scopesWithDomain from './endpoints/i/registry/scopes-with-domain.js';
import * as ep___i_registry_set from './endpoints/i/registry/set.js';
import * as ep___i_revokeToken from './endpoints/i/revoke-token.js';
@ -590,6 +591,7 @@ const $i_registry_get: Provider = { provide: 'ep:i/registry/get', useClass: ep__
const $i_registry_keysWithType: Provider = { provide: 'ep:i/registry/keys-with-type', useClass: ep___i_registry_keysWithType.default };
const $i_registry_keys: Provider = { provide: 'ep:i/registry/keys', useClass: ep___i_registry_keys.default };
const $i_registry_remove: Provider = { provide: 'ep:i/registry/remove', useClass: ep___i_registry_remove.default };
const $i_registry_removeAllKeysInScope: Provider = { provide: 'ep:i/registry/remove-all-keys-in-scope', useClass: ep___i_registry_removeAllKeysInScope.default };
const $i_registry_scopesWithDomain: Provider = { provide: 'ep:i/registry/scopes-with-domain', useClass: ep___i_registry_scopesWithDomain.default };
const $i_registry_set: Provider = { provide: 'ep:i/registry/set', useClass: ep___i_registry_set.default };
const $i_revokeToken: Provider = { provide: 'ep:i/revoke-token', useClass: ep___i_revokeToken.default };
@ -953,6 +955,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_registry_keysWithType,
$i_registry_keys,
$i_registry_remove,
$i_registry_removeAllKeysInScope,
$i_registry_scopesWithDomain,
$i_registry_set,
$i_revokeToken,
@ -1310,6 +1313,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$i_registry_keysWithType,
$i_registry_keys,
$i_registry_remove,
$i_registry_removeAllKeysInScope,
$i_registry_scopesWithDomain,
$i_registry_set,
$i_revokeToken,

View File

@ -231,6 +231,7 @@ import * as ep___i_registry_get from './endpoints/i/registry/get.js';
import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js';
import * as ep___i_registry_keys from './endpoints/i/registry/keys.js';
import * as ep___i_registry_remove from './endpoints/i/registry/remove.js';
import * as ep___i_registry_removeAllKeysInScope from './endpoints/i/registry/remove-all-keys-in-scope.js';
import * as ep___i_registry_scopesWithDomain from './endpoints/i/registry/scopes-with-domain.js';
import * as ep___i_registry_set from './endpoints/i/registry/set.js';
import * as ep___i_revokeToken from './endpoints/i/revoke-token.js';
@ -588,6 +589,7 @@ const eps = [
['i/registry/keys-with-type', ep___i_registry_keysWithType],
['i/registry/keys', ep___i_registry_keys],
['i/registry/remove', ep___i_registry_remove],
['i/registry/remove-all-keys-in-scope', ep___i_registry_removeAllKeysInScope],
['i/registry/scopes-with-domain', ep___i_registry_scopesWithDomain],
['i/registry/set', ep___i_registry_set],
['i/revoke-token', ep___i_revokeToken],

View File

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistryItemsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { RegistryApiService } from '@/core/RegistryApiService.js';
import { ApiError } from '../../../error.js';
export const meta = {
requireCredential: true,
errors: {},
} as const;
export const paramDef = {
type: 'object',
properties: {
scope: { type: 'array', default: [], items: {
type: 'string', pattern: /^[a-zA-Z0-9_]+$/.toString().slice(1, -1),
} },
domain: { type: 'string', nullable: true },
},
required: ['scope'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private registryApiService: RegistryApiService,
) {
super(meta, paramDef, async (ps, me, accessToken) => {
await this.registryApiService.remove(me.id, accessToken != null ? accessToken.id : (ps.domain ?? null), ps.scope);
});
}
}

View File

@ -59,8 +59,8 @@
"querystring": "0.2.1",
"rollup": "4.4.1",
"sanitize-html": "2.11.0",
"shiki": "^0.14.5",
"sass": "1.69.5",
"shiki": "^0.14.5",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.158.0",
@ -75,7 +75,8 @@
"vanilla-tilt": "1.8.1",
"vite": "4.5.0",
"vue": "3.3.8",
"vuedraggable": "next"
"vuedraggable": "next",
"xxhash-wasm": "1.0.2"
},
"devDependencies": {
"@storybook/addon-actions": "7.5.3",

View File

@ -19,6 +19,8 @@ import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js
import { mainRouter } from '@/router.js';
import { initializeSw } from '@/scripts/initialize-sw.js';
import { deckStore } from '@/ui/deck/deck-store.js';
import { getPluginList } from '@/plugin.js';
import { unisonReload } from '@/scripts/unison-reload.js';
export async function mainBoot() {
const { isClientUpdated } = await common(() => createApp(
@ -56,7 +58,11 @@ export async function mainBoot() {
}
});
for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
const plugins = ColdDeviceStorage.get('plugins').filter(p => p.active);
const accountPlugins = Object.values(await getPluginList()).filter(p => p.active);
plugins.push(...accountPlugins);
for (const plugin of plugins) {
import('@/plugin.js').then(async ({ install }) => {
// Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740
await new Promise(r => setTimeout(r, 0));
@ -273,6 +279,13 @@ export async function mainBoot() {
main.on('myTokenRegenerated', () => {
signout();
});
// プラグインに変更が入ったら自動でリロードする
stream.useChannel('main').on('registryUpdated', ({ scope, key }: { scope: string[], key: string, value: any }) => {
if (scope.length === 1 && scope[0] === 'client' && key === 'plugins') {
unisonReload();
}
});
}
// shortcut

View File

@ -0,0 +1,75 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="dialog"
:width="400"
:height="450"
:withOkButton="true"
:okButtonDisabled="false"
:canClose="false"
@close="dialog.close()"
@closed="$emit('closed')"
@ok="ok()"
>
<template #header>{{ i18n.ts.pluginSyncSettings }}</template>
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps_m">
<div>
<MkInfo>{{ i18n.ts.pluginSyncSettingsInfo }}</MkInfo>
</div>
<div v-if="isExistsFromAccount && localOrAccount === 'account'">
<MkInfo warn>{{ i18n.ts.duplicateSyncedPlugin }}</MkInfo>
<MkSwitch v-model="pluginOnlyOverride" :class="$style.switch">{{ i18n.ts.overrideSourceCodeOnly }}</MkSwitch>
</div>
<div>
<MkSelect v-model="localOrAccount">
<template #label>{{ i18n.ts.syncSetting }}</template>
<option value="local">{{ i18n.ts.localOnly }}</option>
<option value="account">{{ i18n.ts.syncing }}</option>
</MkSelect>
</div>
</div>
</MkSpacer>
</MkModalWindow>
</template>
<script lang="ts" setup>
import { } from 'vue';
import MkSwitch from './MkSwitch.vue';
import MkSelect from './MkSelect.vue';
import MkInfo from './MkInfo.vue';
import MkModalWindow from './MkModalWindow.vue';
import { i18n } from '@/i18n.js';
const props = defineProps<{
isExistsFromAccount: boolean;
}>();
const emit = defineEmits<{
(ev: 'closed'): void;
(ev: 'done', result: { name: string | null, permissions: string[] }): void;
}>();
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
let localOrAccount = $ref(props.isExistsFromAccount ? 'account' : 'local');
let pluginOnlyOverride = $ref(false);
function ok(): void {
emit('done', {
isLocal: localOrAccount === 'local',
pluginOnlyOverride,
});
dialog.close();
}
</script>
<style lang="scss" module>
.switch {
margin-top: 10px;
}
</style>

View File

@ -145,7 +145,10 @@ async function run() {
aiscript = new Interpreter({
...createAiScriptEnv({
storageKey: 'flash:' + flash.id,
storageMetadata: {
type: 'flash',
id: flash.id,
},
}),
...registerAsUiLib(components, (_root) => {
root.value = _root.value;

View File

@ -79,8 +79,10 @@ async function run() {
logs.value = [];
aiscript = new Interpreter(({
...createAiScriptEnv({
storageKey: 'widget',
token: $i?.token,
storageMetadata: {
type: 'widget',
},
}),
...registerAsUiLib(components, (_root) => {
root.value = _root.value;

View File

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_s">
<div v-for="plugin in plugins" :key="plugin.id" class="_panel _gaps_m" style="padding: 20px;">
<div class="_gaps_s">
<span style="display: flex; align-items: center;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span>
<span style="display: flex; align-items: center;"><b>{{ plugin.name }}</b><span v-if="plugin.fromAccount" :class="$style.fromAccount">{{ '同期' }}</span><span style="margin-left: auto;">v{{ plugin.version }}</span></span>
<MkSwitch :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ i18n.ts.makeActive }}</MkSwitch>
</div>
@ -38,6 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_buttons">
<MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="ti ti-settings"></i> {{ i18n.ts.settings }}</MkButton>
<MkButton v-if="!plugin.fromAccount" inline @click="moveToAccount(plugin)"><i class="ti ti-link"></i> {{ i18n.ts.movePluginToAccount }}</MkButton>
<MkButton inline danger @click="uninstall(plugin)"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton>
</div>
@ -74,11 +75,24 @@ import { ColdDeviceStorage } from '@/store.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { getPluginList } from '@/plugin.js';
import { toHash } from '@/scripts/xxhash.js';
import { savePluginToAccount } from '@/scripts/install-plugin.js';
const plugins = ref(ColdDeviceStorage.get('plugins'));
let plugins = Object.values(await getPluginList());
plugins.push(...ColdDeviceStorage.get('plugins'));
plugins = ref(plugins);
async function uninstall(plugin) {
ColdDeviceStorage.set('plugins', plugins.value.filter(x => x.id !== plugin.id));
if (plugin.fromAccount) {
let plugins = await getPluginList();
delete plugins[plugin.id];
await os.api('i/registry/remove-all-keys-in-scope', { scope: ['client', 'aiscript', 'plugins', plugin.id] });
await os.api('i/registry/set', { scope: ['client'], key: 'plugins', value: plugins });
} else {
const coldPlugins = ColdDeviceStorage.get('plugins');
ColdDeviceStorage.set('plugins', coldPlugins.filter(x => x.id !== plugin.id));
}
await os.apiWithDialog('i/revoke-token', {
token: plugin.token,
});
@ -87,6 +101,37 @@ async function uninstall(plugin) {
});
}
async function moveToAccount(plugin) {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts.movePluginToAccountConfirm,
});
if (canceled) return;
const hash = await toHash(plugin.name, plugin.author);
const plugins = await getPluginList();
if (Object.keys(plugins).some(v => v === hash)) {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts.overridePluginConfirm,
});
if (canceled) return;
await os.api('i/registry/remove-all-keys-in-scope', { scope: ['client', 'aiscript', 'plugins', hash] });
}
const coldPlugins = ColdDeviceStorage.get('plugins');
ColdDeviceStorage.set('plugins', coldPlugins.filter(x => x.id !== plugin.id));
plugin.id = hash;
plugin.fromAccount = true;
plugins[hash] = plugin;
await os.api('i/registry/set', { scope: ['client'], key: 'plugins', value: plugins });
}
function copy(plugin) {
copyToClipboard(plugin.src ?? '');
os.success();
@ -102,9 +147,15 @@ async function config(plugin) {
const { canceled, result } = await os.form(plugin.name, config);
if (canceled) return;
const coldPlugins = ColdDeviceStorage.get('plugins');
coldPlugins.find(p => p.id === plugin.id)!.configData = result;
ColdDeviceStorage.set('plugins', coldPlugins);
if (plugin.fromAccount) {
let plugins = await getPluginList();
plugins[plugin.id].configData = result;
await os.api('i/registry/set', { scope: ['client'], key: 'plugins', value: plugins });
} else {
const coldPlugins = ColdDeviceStorage.get('plugins');
coldPlugins.find(p => p.id === plugin.id)!.configData = result;
ColdDeviceStorage.set('plugins', coldPlugins);
}
nextTick(() => {
location.reload();
@ -130,3 +181,15 @@ definePageMetadata({
icon: 'ti ti-plug',
});
</script>
<style lang="scss" module>
.fromAccount {
margin-left: 0.7em;
font-size: 65%;
padding: 2px 3px;
color: var(--accent);
border: solid 1px var(--accent);
border-radius: 4px;
vertical-align: top;
}
</style>

View File

@ -5,19 +5,30 @@
import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
import { createAiScriptEnv } from '@/scripts/aiscript/api.js';
import { inputText } from '@/os.js';
import { inputText, api } from '@/os.js';
import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors } from '@/store.js';
import { $i } from './account.js';
const parser = new Parser();
const pluginContexts = new Map<string, Interpreter>();
export async function getPluginList(): Promise<Record<string, Plugin>> {
if ($i == null) return {};
try {
return await api('i/registry/get', { scope: ['client'], key: 'plugins' });
} catch (err) {
if (err.code === 'NO_SUCH_KEY') return {};
throw err;
}
}
export async function install(plugin: Plugin): Promise<void> {
// 後方互換性のため
if (plugin.src == null) return;
const aiscript = new Interpreter(createPluginEnv({
plugin: plugin,
storageKey: 'plugins:' + plugin.id,
}), {
in: (q): Promise<string> => {
return new Promise(ok => {
@ -51,14 +62,14 @@ export async function install(plugin: Plugin): Promise<void> {
console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
}
function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<string, values.Value> {
function createPluginEnv(opts: { plugin: Plugin; }): Record<string, values.Value> {
const config = new Map<string, values.Value>();
for (const [k, v] of Object.entries(opts.plugin.config ?? {})) {
config.set(k, utils.jsToVal(typeof opts.plugin.configData[k] !== 'undefined' ? opts.plugin.configData[k] : v.default));
}
return {
...createAiScriptEnv({ ...opts, token: opts.plugin.token }),
...createAiScriptEnv({ token: opts.plugin.token, storageMetadata: { type: 'plugins', id: opts.plugin.id, fromAccount: opts.plugin.fromAccount } }),
//#region Deprecated
'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
utils.assertString(title);

View File

@ -6,12 +6,12 @@
import { utils, values } from '@syuilo/aiscript';
import * as os from '@/os.js';
import { $i } from '@/account.js';
import { miLocalStorage } from '@/local-storage.js';
import { customEmojis } from '@/custom-emojis.js';
import { url, lang } from '@/config.js';
import { nyaize } from '@/scripts/nyaize.js';
import { StorageMetadata, loadScriptStorage, saveScriptStorage } from './storage.js';
export function createAiScriptEnv(opts) {
export function createAiScriptEnv(opts: { token: string; storageMetadata: StorageMetadata; }) {
return {
USER_ID: $i ? values.STR($i.id) : values.NULL,
USER_NAME: $i ? values.STR($i.name) : values.NULL,
@ -60,14 +60,35 @@ export function createAiScriptEnv(opts) {
return values.ERROR('request_failed', utils.jsToVal(err));
});
}),
'Mk:save': values.FN_NATIVE(([key, value]) => {
'Mk:save': values.FN_NATIVE(async ([key, value, option]) => {
utils.assertString(key);
miLocalStorage.setItem(`aiscript:${opts.storageKey}:${key.value}`, JSON.stringify(utils.valToJs(value)));
return values.NULL;
if (option) {
utils.assertObject(option);
if (option.value.has('toAccount')) {
utils.assertBoolean(option.value.get('toAccount'));
}
}
const saveToAccount = option && option.value.has('toAccount') ? option.value.get('toAccount').value : false;
return saveScriptStorage(saveToAccount, opts.storageMetadata, key.value, utils.valToJs(value)).then(() => {
return values.NULL;
}, err => {
return values.ERROR('request_failed', utils.jsToVal(err));
});
}),
'Mk:load': values.FN_NATIVE(([key]) => {
'Mk:load': values.FN_NATIVE(async ([key, option]) => {
utils.assertString(key);
return utils.jsToVal(JSON.parse(miLocalStorage.getItem(`aiscript:${opts.storageKey}:${key.value}`)));
if (option) {
utils.assertObject(option);
if (option.value.has('toAccount')) {
utils.assertBoolean(option.value.get('toAccount'));
}
}
const loadToAccount = option && option.value.has('toAccount') ? option.value.get('toAccount').value : false;
return loadScriptStorage(loadToAccount, opts.storageMetadata, key.value).then(res => {
return utils.jsToVal(res);
}, err => {
return values.ERROR('request_failed', utils.jsToVal(err));
});
}),
'Mk:url': values.FN_NATIVE(() => {
return values.STR(window.location.href);

View File

@ -0,0 +1,43 @@
import { api } from '@/os.js';
import { miLocalStorage } from '@/local-storage.js';
import { $i } from '@/account.js';
type ScriptType = 'widget' | 'plugins' | 'flash';
export type StorageMetadata = { type: ScriptType; id?: string; fromAccount?: boolean };
export async function loadScriptStorage(toAccount: boolean, storageMetadata: StorageMetadata, key: string) {
let value: string | null;
if ($i && toAccount && (storageMetadata.type !== 'plugins' || (storageMetadata.type === 'plugins' && storageMetadata.fromAccount))) {
if (storageMetadata.type === 'widget') {
value = await api('i/registry/get', { scope: ['client', 'aiscript', storageMetadata.type], key: key });
} else {
value = await api('i/registry/get', { scope: ['client', 'aiscript', storageMetadata.type, storageMetadata.id!], key: key });
}
} else {
if (storageMetadata.type === 'widget') {
value = miLocalStorage.getItem(`aiscript:${storageMetadata.type}:${key}`);
} else {
value = miLocalStorage.getItem(`aiscript:${storageMetadata.type}:${storageMetadata.id!}:${key}`);
}
}
if (value === null) return null;
return JSON.parse(value);
}
export async function saveScriptStorage(toAccount: boolean, storageMetadata: StorageMetadata, key: string, value: any) {
const jsonValue = JSON.stringify(value);
if ($i && toAccount && (storageMetadata.type !== 'plugins' || (storageMetadata.type === 'plugins' && storageMetadata.fromAccount))) {
if (storageMetadata.type === 'widget') {
await api('i/registry/set', { scope: ['client', 'aiscript', storageMetadata.type], key: key, value: jsonValue });
} else {
await api('i/registry/set', { scope: ['client', 'aiscript', storageMetadata.type, storageMetadata.id!], key: key, value: jsonValue });
}
} else {
if (storageMetadata.type === 'widget') {
miLocalStorage.setItem(`aiscript:${storageMetadata.type}:${key}`, jsonValue);
} else {
miLocalStorage.setItem(`aiscript:${storageMetadata.type}:${storageMetadata.id!}:${key}`, jsonValue);
}
}
}

View File

@ -11,6 +11,8 @@ import type { Plugin } from '@/store.js';
import { ColdDeviceStorage } from '@/store.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { getPluginList } from '@/plugin.js';
import { toHash } from './xxhash.js';
export type AiScriptPluginMeta = {
name: string;
@ -19,6 +21,7 @@ export type AiScriptPluginMeta = {
description?: string;
permissions?: string[];
config?: Record<string, any>;
id?: string;
};
const parser = new Parser();
@ -36,9 +39,37 @@ export function savePlugin({ id, meta, src, token }: {
configData: {},
token: token,
src: src,
fromAccount: false,
} as Plugin));
}
async function savePluginToAccount(pluginOnlyOverride: boolean, { id, meta, src, token }: {
id: string;
meta: AiScriptPluginMeta;
src: string;
token: string;
}) {
const plugins = await getPluginList();
// pluginOnlyOverrideがtrueになっているということはすでに重複していることが確定している
const configData = pluginOnlyOverride ? plugins[id].configData : {};
const pluginToken = pluginOnlyOverride ? plugins[id].token : token;
plugins[id] = {
...meta,
id,
active: true,
configData,
token: pluginToken,
src: src,
fromAccount: true,
} as Plugin;
if (!pluginOnlyOverride) {
await os.api('i/registry/remove-all-keys-in-scope', { scope: ['client', 'aiscript', 'plugins', id] });
}
await os.api('i/registry/set', { scope: ['client'], key: 'plugins', value: plugins });
}
export function isSupportedAiScriptVersion(version: string): boolean {
try {
return (compareVersions(version, '0.12.0') >= 0);
@ -76,11 +107,15 @@ export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta>
throw new Error('Metadata not found');
}
const { name, version, author, description, permissions, config } = metadata;
const { name, version, author, description, permissions, config, id } = metadata;
if (name == null || version == null || author == null) {
throw new Error('Required property not found');
}
if (id != null && !/^[a-zA-Z0-9_]+$/.test(id)) {
throw new Error('Invalid id format.');
}
return {
name,
version,
@ -88,6 +123,7 @@ export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta>
description,
permissions,
config,
id,
};
}
@ -101,7 +137,21 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
realMeta = meta;
}
const token = realMeta.permissions == null || realMeta.permissions.length === 0 ? null : await new Promise((res, rej) => {
const plugins = Object.keys(await getPluginList());
const pluginHash = await toHash(realMeta.name, realMeta.author);
const { isLocal, pluginOnlyOverride } = await new Promise((res, rej) => {
const pluginCheckId = realMeta.id ?? pluginHash;
os.popup(defineAsyncComponent(() => import('@/components/MkPluginSelectSaveWindow.vue')), {
isExistsFromAccount: plugins.some(v => v === pluginCheckId)
}, {
done: result => {
res(result);
},
}, 'closed');
});
const token = realMeta.permissions == null || realMeta.permissions.length === 0 || pluginOnlyOverride ? null : await new Promise((res, rej) => {
os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {
title: i18n.ts.tokenRequested,
information: i18n.ts.pluginTokenRequestedDescription,
@ -120,10 +170,19 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
}, 'closed');
});
savePlugin({
id: uuid(),
meta: realMeta,
token,
src: code,
});
if (isLocal) {
savePlugin({
id: realMeta.id ?? uuid(),
meta: realMeta,
token,
src: code,
});
} else {
await savePluginToAccount(pluginOnlyOverride, {
id: realMeta.id ?? pluginHash,
meta: realMeta,
token,
src: code,
});
}
}

View File

@ -0,0 +1,6 @@
import xxhash from 'xxhash-wasm';
export async function toHash(name: string, author: string) {
const { h32ToString } = await xxhash();
return h32ToString(author + name);
}

View File

@ -426,6 +426,7 @@ export type Plugin = {
author?: string;
description?: string;
permissions?: string[];
fromAccount: boolean;
};
interface Watcher {

View File

@ -66,8 +66,10 @@ const logs = ref<{
const run = async () => {
logs.value = [];
const aiscript = new Interpreter(createAiScriptEnv({
storageKey: 'widget',
token: $i?.token,
storageMetadata: {
type: 'widget',
},
}), {
in: (q) => {
return new Promise(ok => {

View File

@ -57,8 +57,10 @@ const components: Ref<AsUiComponent>[] = $ref([]);
async function run() {
const aiscript = new Interpreter({
...createAiScriptEnv({
storageKey: 'widget',
token: $i?.token,
storageMetadata: {
type: 'widget',
},
}),
...registerAsUiLib(components, (_root) => {
root.value = _root.value;

View File

@ -53,8 +53,10 @@ const parser = new Parser();
const run = async () => {
const aiscript = new Interpreter(createAiScriptEnv({
storageKey: 'widget',
token: $i?.token,
storageMetadata: {
type: 'widget',
},
}), {
in: (q) => {
return new Promise(ok => {

File diff suppressed because it is too large Load Diff