@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,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: 插件管理
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,28 @@
1
+ commands.plugin:
2
+ description: 插件管理
3
+
4
+ commands.plugin.install:
5
+ description: 安装插件
6
+ messages:
7
+ expect-name: 请输入插件名。
8
+ already-installed: 该插件已安装。
9
+ not-found: 未找到该插件。
10
+ success: 安装成功!
11
+
12
+ commands.plugin.uninstall:
13
+ description: 卸载插件
14
+ messages:
15
+ expect-name: 请输入插件名。
16
+ not-installed: 该插件未安装。
17
+ success: 卸载成功!
18
+
19
+ commands.plugin.upgrade:
20
+ description: 升级插件
21
+ options:
22
+ self: 升级 Koishi 本体
23
+ messages:
24
+ all-updated: 所有插件已是最新版本。
25
+ available: 有可用的依赖更新:
26
+ prompt: 输入「Y」升级全部依赖,输入「N」取消操作。
27
+ cancelled: 已取消操作。
28
+ success: 升级成功!
@@ -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,9 @@
1
+ registry:
2
+ $description: 插件源设置
3
+ endpoint: 插件的下载源。默认跟随当前项目的 npm config。
4
+ timeout: 获取插件数据的超时时间。
5
+ search:
6
+ $description: 搜索设置
7
+ endpoint: 用于搜索插件市场的网址。默认跟随插件源设置。
8
+ timeout: 搜索插件市场的超时时间。
9
+ proxyAgent: 用于搜索插件市场的代理。
@@ -0,0 +1,9 @@
1
+ registry:
2
+ $description: 插件源设置
3
+ endpoint: 插件的下载源。默认跟随当前项目的 npm config。
4
+ timeout: 获取插件数据的超时时间。
5
+ search:
6
+ $description: 搜索设置
7
+ endpoint: 用于搜索插件市场的网址。默认跟随插件源设置。
8
+ timeout: 搜索插件市场的超时时间。
9
+ proxyAgent: 用于搜索插件市场的代理。
@@ -0,0 +1,9 @@
1
+ registry:
2
+ $description: 插件源设置
3
+ endpoint: 插件的下载源。默认跟随当前项目的 npm config。
4
+ timeout: 获取插件数据的超时时间。
5
+ search:
6
+ $description: 搜索设置
7
+ endpoint: 用于搜索插件市场的网址。默认跟随插件源设置。
8
+ timeout: 搜索插件市场的超时时间。
9
+ proxyAgent: 用于搜索插件市场的代理。
@@ -0,0 +1,9 @@
1
+ registry:
2
+ $description: 插件源设置
3
+ endpoint: 插件的下载源。默认跟随当前项目的 npm config。
4
+ timeout: 获取插件数据的超时时间。
5
+ search:
6
+ $description: 搜索设置
7
+ endpoint: 用于搜索插件市场的网址。默认跟随插件源设置。
8
+ timeout: 搜索插件市场的超时时间。
9
+ proxyAgent: 用于搜索插件市场的代理。
@@ -0,0 +1,9 @@
1
+ registry:
2
+ $description: 插件源设置
3
+ endpoint: 插件的下载源。默认跟随当前项目的 npm config。
4
+ timeout: 获取插件数据的超时时间。
5
+ search:
6
+ $description: 搜索设置
7
+ endpoint: 用于搜索插件市场的网址。默认跟随插件源设置。
8
+ timeout: 搜索插件市场的超时时间。
9
+ proxyAgent: 用于搜索插件市场的代理。
@@ -0,0 +1,10 @@
1
+ registry:
2
+ $description: 插件源设置
3
+ endpoint: 插件的下载源。默认跟随当前项目的 npm config。
4
+ timeout: 获取插件数据的超时时间。
5
+
6
+ search:
7
+ $description: 搜索设置
8
+ endpoint: 用于搜索插件市场的网址。默认跟随插件源设置。
9
+ timeout: 搜索插件市场的超时时间。
10
+ proxyAgent: 用于搜索插件市场的代理。
@@ -0,0 +1,9 @@
1
+ registry:
2
+ $description: 外掛程式源設定
3
+ endpoint: 外掛程式的下載源。默認跟隨當前項目的 npm config。
4
+ timeout: 獲取外掛程式數據的超時時間。
5
+ search:
6
+ $description: 搜尋設定
7
+ endpoint: 用於搜尋外掛程式市場的網址。默認跟隨外掛程式源設置。
8
+ timeout: 搜尋外掛程式市場的超時時間。
9
+ proxyAgent: 用於搜尋外掛程式市場的代理。
@@ -0,0 +1,149 @@
1
+ import {
2
+ type Context,
3
+ type Dict,
4
+ type HTTP,
5
+ Schema,
6
+ Time,
7
+ } from "@koishi-ce/koishi";
8
+ import Scanner, {
9
+ type SearchObject,
10
+ type SearchResult,
11
+ } from "@koishi-ce/registry";
12
+ import { MarketProvider as BaseMarketProvider } from "../shared/index.ts";
13
+
14
+ class MarketProvider extends BaseMarketProvider {
15
+ private http?: HTTP;
16
+ private failed: string[] = [];
17
+ private scanner!: Scanner;
18
+ private fullCache: Dict<SearchObject> = {};
19
+ private tempCache: Dict<SearchObject> = {};
20
+ private flushData: () => void;
21
+
22
+ override config: MarketProvider.Config;
23
+
24
+ constructor(ctx: Context, config: MarketProvider.Config) {
25
+ super(ctx);
26
+ this.config = config;
27
+ if (config.endpoint) this.http = ctx.http.extend(config);
28
+ this.flushData = ctx.throttle(() => {
29
+ ctx.console.broadcast("market/patch", {
30
+ data: this.tempCache,
31
+ failed: this.failed.length,
32
+ total: this.scanner.total,
33
+ progress: this.scanner.progress,
34
+ });
35
+ this.tempCache = {};
36
+ }, 500);
37
+ }
38
+
39
+ override async start(refresh = false) {
40
+ this.failed = [];
41
+ this.fullCache = {};
42
+ this.tempCache = {};
43
+ if (refresh) this.ctx.installer.refresh(true);
44
+ await this.prepare();
45
+ super.start();
46
+ }
47
+
48
+ override async collect() {
49
+ const { timeout } = this.config;
50
+ const registry = this.ctx.installer.http;
51
+
52
+ this.failed = [];
53
+ this.scanner = new Scanner(registry.get);
54
+ if (this.http) {
55
+ const result = await this.http.get<SearchResult>("");
56
+ this.scanner.objects = result.objects.filter((object) => !object.ignored);
57
+ this.scanner.total = this.scanner.objects.length;
58
+ if (result.version !== undefined) this.scanner.version = result.version;
59
+ } else {
60
+ await this.scanner.collect(timeout !== undefined ? { timeout } : {});
61
+ }
62
+
63
+ if (!this.scanner.version) {
64
+ // npmmirror 的 404 需要静默忽略;origin 精确比对防止
65
+ // "https://registry.npmmirror.com.evil.io" 之类前缀伪造绕过
66
+ let isNpmmirror = false;
67
+ try {
68
+ isNpmmirror =
69
+ new URL(registry.config.endpoint ?? "").origin ===
70
+ "https://registry.npmmirror.com";
71
+ } catch {
72
+ // endpoint 缺失或非合法 URL:按非 npmmirror 处理
73
+ }
74
+ this.scanner.analyze({
75
+ version: "4",
76
+ onFailure: (name, reason) => {
77
+ this.failed.push(name);
78
+ if (isNpmmirror) {
79
+ if (
80
+ this.ctx.http.isError(reason) &&
81
+ reason.response?.status === 404
82
+ ) {
83
+ // ignore 404 error for npmmirror
84
+ }
85
+ }
86
+ },
87
+ onRegistry: (registry, versions) => {
88
+ this.ctx.installer.setPackage(registry.name, versions);
89
+ },
90
+ onSuccess: (object, _versions) => {
91
+ // npmmirror lacks `links` field
92
+ object.package.links ||= {
93
+ npm: `${registry.config.endpoint?.replace("registry.", "www.") ?? ""}/package/${object.package.name}`,
94
+ };
95
+ this.fullCache[object.package.name] = this.tempCache[
96
+ object.package.name
97
+ ] = object;
98
+ },
99
+ after: () => this.flushData(),
100
+ });
101
+ }
102
+
103
+ return undefined;
104
+ }
105
+
106
+ override async get() {
107
+ await this.prepare();
108
+ if (this._error) return { data: {}, failed: 0, total: 0, progress: 0 };
109
+ const gravatar = process.env["GRAVATAR_MIRROR"];
110
+ return this.scanner.version
111
+ ? {
112
+ registry: this.ctx.installer.endpoint,
113
+ data: Object.fromEntries(
114
+ this.scanner.objects.map((item) => [item.package.name, item]),
115
+ ),
116
+ failed: 0,
117
+ total: this.scanner.total,
118
+ progress: this.scanner.total,
119
+ gravatar,
120
+ }
121
+ : {
122
+ registry: this.ctx.installer.endpoint,
123
+ data: this.fullCache,
124
+ failed: this.failed.length,
125
+ total: this.scanner.total,
126
+ progress: this.scanner.progress,
127
+ gravatar,
128
+ };
129
+ }
130
+
131
+ // erasableSyntaxOnly 禁止含运行时值的 namespace,
132
+ // 原 namespace 内的 Config 常量移到此处的静态字段,对外形状不变
133
+ static Config: Schema<MarketProvider.Config> = Schema.object({
134
+ endpoint: Schema.string().role("link"),
135
+ timeout: Schema.number()
136
+ .role("time")
137
+ .default(Time.second * 30),
138
+ proxyAgent: Schema.string().role("link"),
139
+ });
140
+ }
141
+
142
+ declare namespace MarketProvider {
143
+ export interface Config {
144
+ endpoint?: string;
145
+ timeout?: number;
146
+ }
147
+ }
148
+
149
+ export default MarketProvider;
@@ -0,0 +1,73 @@
1
+ import { DataService } from "@koishi-ce/console";
2
+ import {
3
+ type Awaitable,
4
+ type Context,
5
+ type Dict,
6
+ Logger,
7
+ Time,
8
+ } from "@koishi-ce/koishi";
9
+ import type { SearchObject, SearchResult } from "@koishi-ce/registry";
10
+
11
+ declare module "@koishi-ce/console" {
12
+ interface Events {
13
+ "market/refresh"(): void;
14
+ }
15
+
16
+ namespace Console {
17
+ interface Services {
18
+ market: MarketProvider;
19
+ }
20
+ }
21
+ }
22
+
23
+ const logger = new Logger("market");
24
+
25
+ export abstract class MarketProvider extends DataService<MarketProvider.Payload> {
26
+ private _task: Promise<SearchResult | undefined> | null = null;
27
+ private _timestamp = 0;
28
+ protected _error: unknown;
29
+
30
+ constructor(ctx: Context) {
31
+ super(ctx, "market", { authority: 4 });
32
+
33
+ ctx.console.addListener("market/refresh", () => this.start(true), {
34
+ authority: 4,
35
+ });
36
+
37
+ ctx.on("console/connection", async (client) => {
38
+ if (!ctx.console.clients[client.id]) return;
39
+ if (Date.now() - this._timestamp <= Time.hour * 12) return;
40
+ if (await this.ctx.serial("console/intercept", client, { authority: 4 }))
41
+ return;
42
+ this.start();
43
+ });
44
+ }
45
+
46
+ override start(_refresh = false): Awaitable<void> {
47
+ this._task = null;
48
+ this._error = null;
49
+ this._timestamp = Date.now();
50
+ this.refresh();
51
+ }
52
+
53
+ abstract collect(): Promise<SearchResult | undefined>;
54
+
55
+ async prepare(): Promise<SearchResult | undefined> {
56
+ return (this._task ||= this.collect().catch((error: unknown) => {
57
+ logger.warn(error);
58
+ this._error = error;
59
+ return undefined;
60
+ }));
61
+ }
62
+ }
63
+
64
+ export namespace MarketProvider {
65
+ export interface Payload {
66
+ registry?: string | undefined;
67
+ data: Dict<SearchObject>;
68
+ total: number;
69
+ failed: number;
70
+ progress: number;
71
+ gravatar?: string | undefined;
72
+ }
73
+ }