@koishi-ce/plugin-market 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,229 @@
1
+ import { resolve } from "node:path";
2
+ import { type Context, type Dict, pick, Schema } from "@koishi-ce/koishi";
3
+ import type { DependencyMetaKey, RemotePackage } from "@koishi-ce/registry";
4
+ import { gt } from "semver";
5
+ import { DependencyProvider, RegistryProvider } from "./deps.ts";
6
+ import Installer from "./installer.ts";
7
+ import messageZhCN from "./locales/message.zh-CN.yml";
8
+ import schemaZhCN from "./locales/schema.zh-CN.yml";
9
+ import MarketProvider from "./market.ts";
10
+
11
+ export * from "../shared/index.ts";
12
+
13
+ export { DependencyProvider, Installer, RegistryProvider };
14
+
15
+ declare module "@koishi-ce/koishi" {
16
+ interface Context {
17
+ installer: Installer;
18
+ }
19
+ }
20
+
21
+ declare module "@koishi-ce/console" {
22
+ namespace Console {
23
+ interface Services {
24
+ dependencies: DependencyProvider;
25
+ registry: RegistryProvider;
26
+ }
27
+ }
28
+
29
+ interface Events {
30
+ "market/install"(deps: Dict<string>, forced?: boolean): Promise<number>;
31
+ "market/registry"(
32
+ names: string[],
33
+ ): Promise<Dict<Dict<Pick<RemotePackage, DependencyMetaKey>>>>;
34
+ }
35
+ }
36
+
37
+ export const name = "market";
38
+ export const inject = ["http"];
39
+
40
+ export const usage = `
41
+ 如果插件市场页面提示「无法连接到插件市场」,则可以选择一个 Koishi 社区提供的镜像地址,填入下方对应的配置项中。
42
+
43
+ ## 插件市场(填入 search.endpoint)
44
+
45
+ - Koishi(全球):https://registry.koishi.chat/index.json
46
+ - [t4wefan](https://k.ilharp.cc/2611)(大陆):https://registry.koishi.t4wefan.pub/index.json
47
+ - [Lipraty](https://k.ilharp.cc/3530)(大陆):https://koi.nyan.zone/registry/index.json
48
+ - [itzdrli](https://k.ilharp.cc/9975)(全球):https://kp.itzdrli.cc
49
+ - [Q78KG](https://k.ilharp.cc/10042)(全球):https://koishi-registry.yumetsuki.moe/index.json
50
+
51
+ 要浏览更多社区镜像,请访问 [Koishi 论坛上的镜像一览](https://k.ilharp.cc/4000)。`;
52
+
53
+ // ## 软件源(填入 npmRegistryServer)
54
+
55
+ // - 淘宝(大陆):https://registry.npmmirror.com
56
+ // - 腾讯(大陆):https://mirrors.cloud.tencent.com/npm
57
+ // - npm(全球):https://registry.npmjs.org
58
+ // - yarn(全球):https://registry.yarnpkg.com
59
+
60
+ export interface Config {
61
+ registry?: Installer.Config;
62
+ search?: MarketProvider.Config;
63
+ }
64
+
65
+ export const Config: Schema<Config> = Schema.object({
66
+ registry: Installer.Config,
67
+ search: MarketProvider.Config,
68
+ }).i18n({
69
+ "zh-CN": schemaZhCN,
70
+ });
71
+
72
+ export function apply(ctx: Context, config: Config) {
73
+ if (!ctx.loader?.writable) {
74
+ return ctx
75
+ .logger("app")
76
+ .warn(
77
+ "@koishijs/plugin-market is only available for json/yaml config file",
78
+ );
79
+ }
80
+
81
+ ctx.plugin(Installer, config.registry);
82
+
83
+ ctx.inject(["installer"], (ctx) => {
84
+ ctx.i18n.define("zh-CN", messageZhCN);
85
+
86
+ ctx
87
+ .command("plugin.install <name>", { authority: 4 })
88
+ .alias(".i")
89
+ .action(async ({ session }, name) => {
90
+ if (!session) return;
91
+ if (!name) return session.text(".expect-name");
92
+
93
+ // check local dependencies
94
+ const names = ctx.installer.resolveName(name);
95
+ const deps = await ctx.installer.getDeps();
96
+ if (names.find((name) => deps[name]))
97
+ return session.text(".already-installed");
98
+
99
+ // find proper version
100
+ const result = await ctx.installer.findVersion(names);
101
+ if (!result) return session.text(".not-found");
102
+
103
+ // set restart message
104
+ ctx.loader.envData.message = {
105
+ ...pick(session, ["sid", "channelId", "guildId", "isDirect"]),
106
+ content: session.text(".success"),
107
+ };
108
+ await ctx.installer.install(result);
109
+ ctx.loader.envData.message = null;
110
+ return session.text(".success");
111
+ });
112
+
113
+ ctx
114
+ .command("plugin.uninstall <name>", { authority: 4 })
115
+ .alias(".r")
116
+ .action(async ({ session }, name) => {
117
+ if (!session) return;
118
+ if (!name) return session.text(".expect-name");
119
+
120
+ // check local dependencies
121
+ const names = ctx.installer.resolveName(name);
122
+ const deps = await ctx.installer.getDeps();
123
+ const installed = names.find((name) => deps[name]);
124
+ if (!installed) return session.text(".not-installed");
125
+
126
+ await ctx.installer.install({ [installed]: null });
127
+ return session.text(".success");
128
+ });
129
+
130
+ ctx
131
+ .command("plugin.upgrade [name...]", { authority: 4 })
132
+ .alias(".update", ".up")
133
+ .option("self", "-s, --koishi")
134
+ .action(async ({ session, options }, ...names) => {
135
+ if (!session) return;
136
+
137
+ async function getPackages(names: string[]) {
138
+ if (!names.length) return Object.keys(deps);
139
+ const resolved = names
140
+ .map((name) => {
141
+ const names = ctx.installer.resolveName(name);
142
+ return names.find((name) => deps[name]);
143
+ })
144
+ .filter((name): name is string => name !== undefined);
145
+ if (options?.self) resolved.push("koishi");
146
+ return resolved;
147
+ }
148
+
149
+ // refresh dependencies
150
+ ctx.installer.refresh(true);
151
+ const deps = await ctx.installer.getDeps();
152
+ names = (
153
+ await getPackages(names.filter((name): name is string => !!name))
154
+ ).filter((name) => {
155
+ const { latest, resolved, invalid } = deps[name] ?? {};
156
+ if (latest === undefined || resolved === undefined) return false;
157
+ try {
158
+ return !invalid && gt(latest, resolved);
159
+ } catch {
160
+ return false;
161
+ }
162
+ });
163
+ if (!names.length) return session.text(".all-updated");
164
+
165
+ const output = names.map((name) => {
166
+ const { latest, resolved } = deps[name] ?? {};
167
+ return `${name}: ${resolved} -> ${latest}`;
168
+ });
169
+ output.unshift(session.text(".available"));
170
+ output.push(session.text(".prompt"));
171
+ await session.send(output.join("\n"));
172
+ const result = await session.prompt();
173
+ const answer = result?.trim();
174
+ if (answer !== "Y" && answer !== "y") {
175
+ return session.text(".cancelled");
176
+ }
177
+
178
+ ctx.loader.envData.message = {
179
+ ...pick(session, ["sid", "channelId", "guildId", "isDirect"]),
180
+ content: session.text(".success"),
181
+ };
182
+ await ctx.installer.install(
183
+ names.reduce<Dict<string | null>>((result, name) => {
184
+ const latest = deps[name]?.latest;
185
+ if (latest !== undefined) result[name] = latest;
186
+ return result;
187
+ }, {}),
188
+ );
189
+ ctx.loader.envData.message = null;
190
+ return session.text(".success");
191
+ });
192
+ });
193
+
194
+ ctx.inject(["console", "installer"], (ctx) => {
195
+ ctx.plugin(DependencyProvider);
196
+ ctx.plugin(RegistryProvider);
197
+ ctx.plugin(MarketProvider, config.search);
198
+
199
+ ctx.console.addEntry({
200
+ dev: resolve(__dirname, "../../client/index.ts"),
201
+ prod: resolve(__dirname, "../../dist"),
202
+ });
203
+
204
+ ctx.console.addListener(
205
+ "market/install",
206
+ async (deps, forced) => {
207
+ const code = await ctx.installer.install(deps, forced);
208
+ ctx.get("console")?.refresh("dependencies");
209
+ ctx.get("console")?.refresh("registry");
210
+ ctx.get("console")?.refresh("packages");
211
+ return code;
212
+ },
213
+ { authority: 4 },
214
+ );
215
+
216
+ ctx.console.addListener(
217
+ "market/registry",
218
+ async (names) => {
219
+ const meta = await Promise.all(
220
+ names.map((name) => ctx.installer.getPackage(name)),
221
+ );
222
+ return Object.fromEntries(
223
+ meta.map((meta, index) => [names[index], meta]),
224
+ );
225
+ },
226
+ { authority: 4 },
227
+ );
228
+ });
229
+ }
@@ -0,0 +1,388 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { resolve } from "node:path";
4
+ import type {} from "@koishi-ce/console";
5
+ import {
6
+ type Context,
7
+ type Dict,
8
+ defineProperty,
9
+ type HTTP,
10
+ Logger,
11
+ pick,
12
+ Schema,
13
+ Service,
14
+ Time,
15
+ valueMap,
16
+ } from "@koishi-ce/koishi";
17
+ import type {} from "@koishi-ce/loader";
18
+ import type {} from "@koishi-ce/plugin-market";
19
+ import Scanner, {
20
+ type DependencyMetaKey,
21
+ type PackageJson,
22
+ type Registry,
23
+ type RemotePackage,
24
+ } from "@koishi-ce/registry";
25
+ import spawn from "execa";
26
+ import getRegistry from "get-registry";
27
+ import pMap from "p-map";
28
+ import { compare, satisfies, valid } from "semver";
29
+
30
+ const logger = new Logger("market");
31
+
32
+ // 经 createRequire 加载 CJS 包并就地断言签名:不走 ESM 导入互操作,
33
+ // 规避多包合并类型检查(大一统 tsconfig)下 export = 交织失效问题
34
+ const whichPMRuns = createRequire(import.meta.url)("which-pm-runs") as () =>
35
+ | undefined
36
+ | { name: string; version: string };
37
+
38
+ export interface Dependency {
39
+ /**
40
+ * requested semver range
41
+ * @example `^1.2.3` -> `1.2.3`
42
+ */
43
+ request: string;
44
+ /**
45
+ * installed package version
46
+ * @example `1.2.5`
47
+ */
48
+ resolved?: string | undefined;
49
+ /** whether it is a workspace package */
50
+ workspace?: boolean | undefined;
51
+ /** valid (unsupported) syntax */
52
+ invalid?: boolean | undefined;
53
+ /** latest version */
54
+ latest?: string | undefined;
55
+ }
56
+
57
+ export interface YarnLog {
58
+ type: "warning" | "info" | "error" | string;
59
+ name: number | null;
60
+ displayName: string;
61
+ indent?: string;
62
+ data: string;
63
+ }
64
+
65
+ const levelMap = {
66
+ info: "info",
67
+ warning: "debug",
68
+ error: "warn",
69
+ } as const;
70
+
71
+ export interface LocalPackage extends PackageJson {
72
+ private?: boolean;
73
+ $workspace?: boolean;
74
+ /** loadManifest 归一化保证 dependencies 必有 */
75
+ dependencies: Record<string, string>;
76
+ }
77
+
78
+ export function loadManifest(name: string) {
79
+ const filename = require.resolve(`${name}/package.json`);
80
+ const meta: LocalPackage = JSON.parse(readFileSync(filename, "utf8"));
81
+ meta.dependencies ||= {};
82
+ defineProperty(meta, "$workspace", !filename.includes("node_modules"));
83
+ return meta;
84
+ }
85
+
86
+ function getVersions(versions: RemotePackage[]) {
87
+ return Object.fromEntries(
88
+ versions
89
+ .map(
90
+ (item) =>
91
+ [
92
+ item.version,
93
+ pick(item, [
94
+ "peerDependencies",
95
+ "peerDependenciesMeta",
96
+ "deprecated",
97
+ ]),
98
+ ] as const,
99
+ )
100
+ .sort(([a], [b]) => compare(b, a)),
101
+ );
102
+ }
103
+
104
+ class Installer extends Service {
105
+ declare http: HTTP;
106
+ declare endpoint: string | undefined;
107
+ public fullCache: Dict<Dict<Pick<RemotePackage, DependencyMetaKey>>> = {};
108
+ public tempCache: Dict<Dict<Pick<RemotePackage, DependencyMetaKey>>> = {};
109
+
110
+ private pkgTasks: Dict<
111
+ Promise<Dict<Pick<RemotePackage, DependencyMetaKey>>>
112
+ > = {};
113
+ private agent = whichPMRuns();
114
+ private manifest: LocalPackage;
115
+ private declare depTask: Promise<Dict<Dependency>>;
116
+ private flushData: () => void;
117
+
118
+ override config: Installer.Config;
119
+
120
+ constructor(ctx: Context, config: Installer.Config) {
121
+ super(ctx, "installer");
122
+ this.config = config;
123
+ this.manifest = loadManifest(this.cwd);
124
+ this.flushData = ctx.throttle(() => {
125
+ ctx.get("console")?.broadcast("market/registry", this.tempCache);
126
+ this.tempCache = {};
127
+ }, 500);
128
+ }
129
+
130
+ get cwd() {
131
+ return this.ctx.baseDir;
132
+ }
133
+
134
+ override async start() {
135
+ const { endpoint, timeout } = this.config;
136
+ this.endpoint = endpoint ?? (await getRegistry());
137
+ const options: HTTP.Config = {};
138
+ if (this.endpoint) options.endpoint = this.endpoint;
139
+ if (timeout !== undefined) options.timeout = timeout;
140
+ this.http = this.ctx.http.extend(options);
141
+ }
142
+
143
+ resolveName(name: string) {
144
+ if (name.startsWith("@koishijs/plugin-")) return [name];
145
+ if (name.match(/(^|\/)koishi-plugin-/)) return [name];
146
+ if (name[0] === "@") {
147
+ const [left, right] = name.split("/");
148
+ return [`${left}/koishi-plugin-${right}`];
149
+ } else {
150
+ return [`@koishijs/plugin-${name}`, `koishi-plugin-${name}`];
151
+ }
152
+ }
153
+
154
+ async findVersion(names: string[]) {
155
+ const entries = await Promise.all(
156
+ names.map(async (name) => {
157
+ try {
158
+ const versions = Object.entries(await this.getPackage(name));
159
+ const [latest] = versions;
160
+ if (!latest) return undefined;
161
+ return { [name]: latest[0] };
162
+ } catch {
163
+ return undefined;
164
+ }
165
+ }),
166
+ );
167
+ return entries.find((entry): entry is Dict<string> => entry !== undefined);
168
+ }
169
+
170
+ private async _getPackage(name: string) {
171
+ try {
172
+ const registry = await this.http.get<Registry>(`/${name}`);
173
+ const versions = getVersions(
174
+ Object.values(registry.versions).filter((remote) => {
175
+ if (name === "koishi") return satisfies(remote.version, "4");
176
+ return !Scanner.isPlugin(name) || Scanner.isCompatible("4", remote);
177
+ }),
178
+ );
179
+ this.fullCache[name] = this.tempCache[name] = versions;
180
+ this.flushData();
181
+ return versions;
182
+ } catch (error) {
183
+ logger.warn(error);
184
+ return {};
185
+ }
186
+ }
187
+
188
+ setPackage(name: string, versions: RemotePackage[]) {
189
+ this.fullCache[name] = this.tempCache[name] = getVersions(versions);
190
+ this.flushData();
191
+ this.pkgTasks[name] = Promise.resolve(this.fullCache[name]);
192
+ }
193
+
194
+ getPackage(name: string) {
195
+ return (this.pkgTasks[name] ||= this._getPackage(name));
196
+ }
197
+
198
+ private async _getDeps() {
199
+ const result = valueMap(this.manifest.dependencies, (request) => {
200
+ return { request: request.replace(/^[~^]/, "") } as Dependency;
201
+ });
202
+ await pMap(
203
+ Object.keys(result),
204
+ async (name) => {
205
+ const dep = result[name];
206
+ if (!dep) return;
207
+ try {
208
+ // some dependencies may be left with no local installation
209
+ const meta = loadManifest(name);
210
+ dep.resolved = meta.version;
211
+ dep.workspace = meta.$workspace;
212
+ if (meta.$workspace) return;
213
+ } catch {}
214
+
215
+ if (!valid(dep.request)) {
216
+ dep.invalid = true;
217
+ }
218
+
219
+ const versions = await this.getPackage(name);
220
+ if (versions) dep.latest = Object.keys(versions)[0];
221
+ },
222
+ { concurrency: 10 },
223
+ );
224
+ return result;
225
+ }
226
+
227
+ getDeps() {
228
+ return (this.depTask ||= this._getDeps());
229
+ }
230
+
231
+ refreshData() {
232
+ this.ctx.get("console")?.refresh("registry");
233
+ this.ctx.get("console")?.refresh("packages");
234
+ }
235
+
236
+ refresh(refresh = false) {
237
+ this.pkgTasks = {};
238
+ this.fullCache = {};
239
+ this.tempCache = {};
240
+ this.depTask = this._getDeps();
241
+ if (!refresh) return;
242
+ this.refreshData();
243
+ }
244
+
245
+ async exec(args: string[]) {
246
+ const name = this.agent?.name ?? "npm";
247
+ const useJson = name === "yarn" && (this.agent?.version ?? "1") >= "2";
248
+ if (name !== "yarn") args.unshift("install");
249
+ return new Promise<number>((resolve) => {
250
+ if (useJson) args.push("--json");
251
+ const child = spawn(name, args, { cwd: this.cwd });
252
+ child.on("exit", (code) => resolve(code ?? -1));
253
+ child.on("error", () => resolve(-1));
254
+
255
+ let stderr = "";
256
+ child.stderr?.on("data", (data) => {
257
+ data = stderr + data.toString();
258
+ const lines = data.split("\n");
259
+ stderr = lines.pop() ?? "";
260
+ for (const line of lines) {
261
+ logger.warn(line);
262
+ }
263
+ });
264
+
265
+ let stdout = "";
266
+ child.stdout?.on("data", (data) => {
267
+ data = stdout + data.toString();
268
+ const lines = data.split("\n");
269
+ stdout = lines.pop() ?? "";
270
+ for (const line of lines) {
271
+ if (!useJson || line[0] !== "{") {
272
+ logger.info(line);
273
+ continue;
274
+ }
275
+ try {
276
+ const { type, data } = JSON.parse(line) as YarnLog;
277
+ const level =
278
+ type in levelMap ? levelMap[type as keyof typeof levelMap] : null;
279
+ (level ? logger[level] : logger.info)(data);
280
+ } catch (error) {
281
+ logger.warn(line);
282
+ logger.warn(error);
283
+ }
284
+ }
285
+ });
286
+ });
287
+ }
288
+
289
+ async override(deps: Dict<string | null>) {
290
+ const filename = resolve(this.cwd, "package.json");
291
+ for (const key in deps) {
292
+ if (deps[key]) {
293
+ this.manifest.dependencies[key] = deps[key];
294
+ } else {
295
+ delete this.manifest.dependencies[key];
296
+ }
297
+ }
298
+ this.manifest.dependencies = Object.fromEntries(
299
+ Object.entries(this.manifest.dependencies).sort((a, b) =>
300
+ a[0].localeCompare(b[0]),
301
+ ),
302
+ );
303
+ await Bun.write(filename, `${JSON.stringify(this.manifest, null, 2)}\n`);
304
+ }
305
+
306
+ private _install() {
307
+ const args: string[] = [];
308
+ if (this.endpoint) {
309
+ args.push("--registry", this.endpoint);
310
+ }
311
+ return this.exec(args);
312
+ }
313
+
314
+ private _getLocalDeps(override: Dict<string | null>) {
315
+ return valueMap(override, (request, name) => {
316
+ const dep = { request } as Dependency;
317
+ try {
318
+ const meta = loadManifest(name);
319
+ dep.resolved = meta.version;
320
+ dep.workspace = meta.$workspace;
321
+ } catch {}
322
+ return dep;
323
+ });
324
+ }
325
+
326
+ async install(deps: Dict<string | null>, forced?: boolean) {
327
+ const localDeps = this._getLocalDeps(deps);
328
+ await this.override(deps);
329
+
330
+ let shouldInstall = forced === true;
331
+ for (const name in deps) {
332
+ const request = deps[name];
333
+ const local = localDeps[name];
334
+ if (
335
+ local?.workspace ||
336
+ (request &&
337
+ local?.resolved &&
338
+ satisfies(local.resolved, request, { includePrerelease: true }))
339
+ )
340
+ continue;
341
+ shouldInstall = true;
342
+ break;
343
+ }
344
+
345
+ if (shouldInstall) {
346
+ const code = await this._install();
347
+ if (code) return code;
348
+ }
349
+
350
+ this.refresh();
351
+ const newDeps = await this.getDeps();
352
+ for (const name in localDeps) {
353
+ const local = localDeps[name];
354
+ const newDep = newDeps[name];
355
+ if (!local || !newDep || local.workspace) continue;
356
+ if (newDep.resolved === local.resolved) continue;
357
+ try {
358
+ if (!(require.resolve(name) in require.cache)) continue;
359
+ } catch (error) {
360
+ // FIXME https://github.com/koishijs/webui/issues/273
361
+ // I have no idea why this happens and how to fix it.
362
+ logger.error(error);
363
+ }
364
+ this.ctx.loader.fullReload();
365
+ }
366
+ this.refreshData();
367
+
368
+ return 0;
369
+ }
370
+
371
+ // erasableSyntaxOnly 禁止含运行时值的 namespace,
372
+ // 原 namespace 内的 Config 常量移到此处的静态字段,对外形状不变
373
+ static Config: Schema<Installer.Config> = Schema.object({
374
+ endpoint: Schema.string().role("link"),
375
+ timeout: Schema.number()
376
+ .role("time")
377
+ .default(Time.second * 5),
378
+ }); // TODO .hidden()
379
+ }
380
+
381
+ declare namespace Installer {
382
+ export interface Config {
383
+ endpoint?: string;
384
+ timeout?: number;
385
+ }
386
+ }
387
+
388
+ export default Installer;
@@ -0,0 +1,25 @@
1
+ commands.plugin:
2
+ description: 插件管理
3
+ commands.plugin.install:
4
+ description: 安装插件
5
+ messages:
6
+ expect-name: 请输入插件名。
7
+ already-installed: 该插件已安装。
8
+ not-found: 未找到该插件。
9
+ success: 安装成功!
10
+ commands.plugin.uninstall:
11
+ description: 卸载插件
12
+ messages:
13
+ expect-name: 请输入插件名。
14
+ not-installed: 该插件未安装。
15
+ success: 卸载成功!
16
+ commands.plugin.upgrade:
17
+ description: 升级插件
18
+ options:
19
+ self: 升级 Koishi 本体
20
+ messages:
21
+ all-updated: 所有插件已是最新版本。
22
+ available: 有可用的依赖更新:
23
+ prompt: 输入「Y」升级全部依赖,输入「N」取消操作。
24
+ cancelled: 已取消操作。
25
+ success: 升级成功!
@@ -0,0 +1,25 @@
1
+ commands.plugin:
2
+ description: Plugin management
3
+ commands.plugin.install:
4
+ description: Install Plugins
5
+ messages:
6
+ expect-name: 请输入插件名。
7
+ already-installed: 该插件已安装。
8
+ not-found: 未找到该插件。
9
+ success: Installation Successful!
10
+ commands.plugin.uninstall:
11
+ description: Uninstall plugin
12
+ messages:
13
+ expect-name: 请输入插件名。
14
+ not-installed: 该插件未安装。
15
+ success: 卸载成功!
16
+ commands.plugin.upgrade:
17
+ description: Upgrade Plugin
18
+ options:
19
+ self: Upgrade Koishi core
20
+ messages:
21
+ all-updated: 所有插件已是最新版本。
22
+ available: 有可用的依赖更新:
23
+ prompt: 输入「Y」升级全部依赖,输入「N」取消操作。
24
+ cancelled: Operation canceled.
25
+ success: Upgrade Successful!
@@ -0,0 +1,25 @@
1
+ commands.plugin:
2
+ description: 插件管理
3
+ commands.plugin.install:
4
+ description: Installation de plugins
5
+ messages:
6
+ expect-name: 请输入插件名。
7
+ already-installed: 该插件已安装。
8
+ not-found: 未找到该插件。
9
+ success: 安装成功!
10
+ commands.plugin.uninstall:
11
+ description: 卸载插件
12
+ messages:
13
+ expect-name: 请输入插件名。
14
+ not-installed: 该插件未安装。
15
+ success: 卸载成功!
16
+ commands.plugin.upgrade:
17
+ description: 升级插件
18
+ options:
19
+ self: 升级 Koishi 本体
20
+ messages:
21
+ all-updated: 所有插件已是最新版本。
22
+ available: 有可用的依赖更新:
23
+ prompt: 输入「Y」升级全部依赖,输入「N」取消操作。
24
+ cancelled: 已取消操作。
25
+ success: 升级成功!