@yunzai-ng/core 0.1.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.
- package/LICENSE +661 -0
- package/README.md +51 -0
- package/dist/adapter/accounts.d.ts +174 -0
- package/dist/adapter/accounts.d.ts.map +1 -0
- package/dist/adapter/accounts.js +653 -0
- package/dist/adapter/accounts.js.map +1 -0
- package/dist/adapter/bots.d.ts +142 -0
- package/dist/adapter/bots.d.ts.map +1 -0
- package/dist/adapter/bots.js +454 -0
- package/dist/adapter/bots.js.map +1 -0
- package/dist/adapter/host.d.ts +114 -0
- package/dist/adapter/host.d.ts.map +1 -0
- package/dist/adapter/host.js +90 -0
- package/dist/adapter/host.js.map +1 -0
- package/dist/adapter/login.d.ts +174 -0
- package/dist/adapter/login.d.ts.map +1 -0
- package/dist/adapter/login.js +345 -0
- package/dist/adapter/login.js.map +1 -0
- package/dist/adapter/registry.d.ts +102 -0
- package/dist/adapter/registry.d.ts.map +1 -0
- package/dist/adapter/registry.js +150 -0
- package/dist/adapter/registry.js.map +1 -0
- package/dist/config/core-config.d.ts +151 -0
- package/dist/config/core-config.d.ts.map +1 -0
- package/dist/config/core-config.js +338 -0
- package/dist/config/core-config.js.map +1 -0
- package/dist/config/schema.d.ts +449 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +871 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/store.d.ts +184 -0
- package/dist/config/store.d.ts.map +1 -0
- package/dist/config/store.js +428 -0
- package/dist/config/store.js.map +1 -0
- package/dist/config/yaml.d.ts +23 -0
- package/dist/config/yaml.d.ts.map +1 -0
- package/dist/config/yaml.js +108 -0
- package/dist/config/yaml.js.map +1 -0
- package/dist/http/client.d.ts +82 -0
- package/dist/http/client.d.ts.map +1 -0
- package/dist/http/client.js +658 -0
- package/dist/http/client.js.map +1 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/app.d.ts +244 -0
- package/dist/kernel/app.d.ts.map +1 -0
- package/dist/kernel/app.js +654 -0
- package/dist/kernel/app.js.map +1 -0
- package/dist/kernel/kv-sink.d.ts +34 -0
- package/dist/kernel/kv-sink.d.ts.map +1 -0
- package/dist/kernel/kv-sink.js +25 -0
- package/dist/kernel/kv-sink.js.map +1 -0
- package/dist/kernel/policy.d.ts +77 -0
- package/dist/kernel/policy.d.ts.map +1 -0
- package/dist/kernel/policy.js +99 -0
- package/dist/kernel/policy.js.map +1 -0
- package/dist/kernel/runtime.d.ts +117 -0
- package/dist/kernel/runtime.d.ts.map +1 -0
- package/dist/kernel/runtime.js +197 -0
- package/dist/kernel/runtime.js.map +1 -0
- package/dist/kernel/sql-sink.d.ts +36 -0
- package/dist/kernel/sql-sink.d.ts.map +1 -0
- package/dist/kernel/sql-sink.js +114 -0
- package/dist/kernel/sql-sink.js.map +1 -0
- package/dist/logger/format.d.ts +67 -0
- package/dist/logger/format.d.ts.map +1 -0
- package/dist/logger/format.js +218 -0
- package/dist/logger/format.js.map +1 -0
- package/dist/logger/index.d.ts +97 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +363 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/rotate.d.ts +47 -0
- package/dist/logger/rotate.d.ts.map +1 -0
- package/dist/logger/rotate.js +242 -0
- package/dist/logger/rotate.js.map +1 -0
- package/dist/message/segment.d.ts +255 -0
- package/dist/message/segment.d.ts.map +1 -0
- package/dist/message/segment.js +510 -0
- package/dist/message/segment.js.map +1 -0
- package/dist/message/split.d.ts +34 -0
- package/dist/message/split.d.ts.map +1 -0
- package/dist/message/split.js +79 -0
- package/dist/message/split.js.map +1 -0
- package/dist/message/target.d.ts +29 -0
- package/dist/message/target.d.ts.map +1 -0
- package/dist/message/target.js +34 -0
- package/dist/message/target.js.map +1 -0
- package/dist/pipeline/cooldown.d.ts +84 -0
- package/dist/pipeline/cooldown.d.ts.map +1 -0
- package/dist/pipeline/cooldown.js +94 -0
- package/dist/pipeline/cooldown.js.map +1 -0
- package/dist/pipeline/dispatch.d.ts +98 -0
- package/dist/pipeline/dispatch.d.ts.map +1 -0
- package/dist/pipeline/dispatch.js +323 -0
- package/dist/pipeline/dispatch.js.map +1 -0
- package/dist/pipeline/event.d.ts +130 -0
- package/dist/pipeline/event.d.ts.map +1 -0
- package/dist/pipeline/event.js +538 -0
- package/dist/pipeline/event.js.map +1 -0
- package/dist/pipeline/middleware.d.ts +63 -0
- package/dist/pipeline/middleware.d.ts.map +1 -0
- package/dist/pipeline/middleware.js +174 -0
- package/dist/pipeline/middleware.js.map +1 -0
- package/dist/pipeline/prompt.d.ts +80 -0
- package/dist/pipeline/prompt.d.ts.map +1 -0
- package/dist/pipeline/prompt.js +155 -0
- package/dist/pipeline/prompt.js.map +1 -0
- package/dist/pipeline/router.d.ts +98 -0
- package/dist/pipeline/router.d.ts.map +1 -0
- package/dist/pipeline/router.js +489 -0
- package/dist/pipeline/router.js.map +1 -0
- package/dist/platform/detect.d.ts +24 -0
- package/dist/platform/detect.d.ts.map +1 -0
- package/dist/platform/detect.js +178 -0
- package/dist/platform/detect.js.map +1 -0
- package/dist/platform/paths.d.ts +38 -0
- package/dist/platform/paths.d.ts.map +1 -0
- package/dist/platform/paths.js +129 -0
- package/dist/platform/paths.js.map +1 -0
- package/dist/platform/system.d.ts +71 -0
- package/dist/platform/system.d.ts.map +1 -0
- package/dist/platform/system.js +242 -0
- package/dist/platform/system.js.map +1 -0
- package/dist/plugin/context.d.ts +92 -0
- package/dist/plugin/context.d.ts.map +1 -0
- package/dist/plugin/context.js +573 -0
- package/dist/plugin/context.js.map +1 -0
- package/dist/plugin/define.d.ts +92 -0
- package/dist/plugin/define.d.ts.map +1 -0
- package/dist/plugin/define.js +82 -0
- package/dist/plugin/define.js.map +1 -0
- package/dist/plugin/discover.d.ts +106 -0
- package/dist/plugin/discover.d.ts.map +1 -0
- package/dist/plugin/discover.js +254 -0
- package/dist/plugin/discover.js.map +1 -0
- package/dist/plugin/events.d.ts +109 -0
- package/dist/plugin/events.d.ts.map +1 -0
- package/dist/plugin/events.js +209 -0
- package/dist/plugin/events.js.map +1 -0
- package/dist/plugin/hooks.d.ts +266 -0
- package/dist/plugin/hooks.d.ts.map +1 -0
- package/dist/plugin/hooks.js +84 -0
- package/dist/plugin/hooks.js.map +1 -0
- package/dist/plugin/host.d.ts +160 -0
- package/dist/plugin/host.d.ts.map +1 -0
- package/dist/plugin/host.js +546 -0
- package/dist/plugin/host.js.map +1 -0
- package/dist/plugin/market.d.ts +264 -0
- package/dist/plugin/market.d.ts.map +1 -0
- package/dist/plugin/market.js +700 -0
- package/dist/plugin/market.js.map +1 -0
- package/dist/plugin/services.d.ts +116 -0
- package/dist/plugin/services.d.ts.map +1 -0
- package/dist/plugin/services.js +213 -0
- package/dist/plugin/services.js.map +1 -0
- package/dist/plugin/tar.d.ts +65 -0
- package/dist/plugin/tar.d.ts.map +1 -0
- package/dist/plugin/tar.js +277 -0
- package/dist/plugin/tar.js.map +1 -0
- package/dist/render/registry.d.ts +127 -0
- package/dist/render/registry.d.ts.map +1 -0
- package/dist/render/registry.js +306 -0
- package/dist/render/registry.js.map +1 -0
- package/dist/scheduler/index.d.ts +67 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +279 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/server/api.d.ts +179 -0
- package/dist/server/api.d.ts.map +1 -0
- package/dist/server/api.js +602 -0
- package/dist/server/api.js.map +1 -0
- package/dist/server/auth.d.ts +83 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/auth.js +178 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/browse.d.ts +60 -0
- package/dist/server/browse.d.ts.map +1 -0
- package/dist/server/browse.js +187 -0
- package/dist/server/browse.js.map +1 -0
- package/dist/server/files.d.ts +27 -0
- package/dist/server/files.d.ts.map +1 -0
- package/dist/server/files.js +76 -0
- package/dist/server/files.js.map +1 -0
- package/dist/server/index.d.ts +189 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1123 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/table.d.ts +105 -0
- package/dist/server/table.d.ts.map +1 -0
- package/dist/server/table.js +289 -0
- package/dist/server/table.js.map +1 -0
- package/dist/store/index.d.ts +50 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +120 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/json.d.ts +73 -0
- package/dist/store/json.d.ts.map +1 -0
- package/dist/store/json.js +174 -0
- package/dist/store/json.js.map +1 -0
- package/dist/store/kv.d.ts +126 -0
- package/dist/store/kv.d.ts.map +1 -0
- package/dist/store/kv.js +232 -0
- package/dist/store/kv.js.map +1 -0
- package/dist/store/level.d.ts +103 -0
- package/dist/store/level.d.ts.map +1 -0
- package/dist/store/level.js +119 -0
- package/dist/store/level.js.map +1 -0
- package/dist/store/memory.d.ts +61 -0
- package/dist/store/memory.d.ts.map +1 -0
- package/dist/store/memory.js +94 -0
- package/dist/store/memory.js.map +1 -0
- package/dist/store/sql.d.ts +142 -0
- package/dist/store/sql.d.ts.map +1 -0
- package/dist/store/sql.js +246 -0
- package/dist/store/sql.js.map +1 -0
- package/dist/testing/fake.d.ts +109 -0
- package/dist/testing/fake.d.ts.map +1 -0
- package/dist/testing/fake.js +196 -0
- package/dist/testing/fake.js.map +1 -0
- package/dist/testing/index.d.ts +16 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +16 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/mock-adapter.d.ts +205 -0
- package/dist/testing/mock-adapter.d.ts.map +1 -0
- package/dist/testing/mock-adapter.js +309 -0
- package/dist/testing/mock-adapter.js.map +1 -0
- package/dist/util/deep.d.ts +108 -0
- package/dist/util/deep.d.ts.map +1 -0
- package/dist/util/deep.js +275 -0
- package/dist/util/deep.js.map +1 -0
- package/dist/util/defer.d.ts +107 -0
- package/dist/util/defer.d.ts.map +1 -0
- package/dist/util/defer.js +163 -0
- package/dist/util/defer.js.map +1 -0
- package/dist/util/dispose.d.ts +88 -0
- package/dist/util/dispose.d.ts.map +1 -0
- package/dist/util/dispose.js +143 -0
- package/dist/util/dispose.js.map +1 -0
- package/dist/util/duration.d.ts +28 -0
- package/dist/util/duration.d.ts.map +1 -0
- package/dist/util/duration.js +77 -0
- package/dist/util/duration.js.map +1 -0
- package/dist/util/fs.d.ts +110 -0
- package/dist/util/fs.d.ts.map +1 -0
- package/dist/util/fs.js +269 -0
- package/dist/util/fs.js.map +1 -0
- package/dist/util/id.d.ts +49 -0
- package/dist/util/id.d.ts.map +1 -0
- package/dist/util/id.js +87 -0
- package/dist/util/id.js.map +1 -0
- package/dist/util/lru.d.ts +74 -0
- package/dist/util/lru.d.ts.map +1 -0
- package/dist/util/lru.js +156 -0
- package/dist/util/lru.js.map +1 -0
- package/dist/util/queue.d.ts +64 -0
- package/dist/util/queue.d.ts.map +1 -0
- package/dist/util/queue.js +147 -0
- package/dist/util/queue.js.map +1 -0
- package/dist/util/text.d.ts +86 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +182 -0
- package/dist/util/text.js.map +1 -0
- package/package.json +44 -0
- package/src/adapter/accounts.ts +758 -0
- package/src/adapter/bots.ts +582 -0
- package/src/adapter/host.ts +224 -0
- package/src/adapter/login.ts +481 -0
- package/src/adapter/registry.ts +202 -0
- package/src/config/core-config.test.ts +60 -0
- package/src/config/core-config.ts +401 -0
- package/src/config/schema.test.ts +179 -0
- package/src/config/schema.ts +1036 -0
- package/src/config/store.test.ts +245 -0
- package/src/config/store.ts +506 -0
- package/src/config/yaml.ts +119 -0
- package/src/http/client.test.ts +568 -0
- package/src/http/client.ts +777 -0
- package/src/index.ts +106 -0
- package/src/kernel/app.test.ts +267 -0
- package/src/kernel/app.ts +843 -0
- package/src/kernel/kv-sink.ts +58 -0
- package/src/kernel/policy.ts +120 -0
- package/src/kernel/runtime.test.ts +542 -0
- package/src/kernel/runtime.ts +307 -0
- package/src/kernel/sql-sink.ts +154 -0
- package/src/logger/format.ts +259 -0
- package/src/logger/index.ts +427 -0
- package/src/logger/rotate.ts +265 -0
- package/src/message/segment.test.ts +335 -0
- package/src/message/segment.ts +569 -0
- package/src/message/split.ts +97 -0
- package/src/message/target.ts +48 -0
- package/src/pipeline/cooldown.ts +136 -0
- package/src/pipeline/dispatch.ts +419 -0
- package/src/pipeline/event.ts +735 -0
- package/src/pipeline/middleware.test.ts +426 -0
- package/src/pipeline/middleware.ts +199 -0
- package/src/pipeline/prompt.ts +212 -0
- package/src/pipeline/router.test.ts +432 -0
- package/src/pipeline/router.ts +575 -0
- package/src/platform/detect.ts +178 -0
- package/src/platform/paths.ts +147 -0
- package/src/platform/system.test.ts +102 -0
- package/src/platform/system.ts +289 -0
- package/src/plugin/context.test.ts +459 -0
- package/src/plugin/context.ts +757 -0
- package/src/plugin/define.test.ts +120 -0
- package/src/plugin/define.ts +142 -0
- package/src/plugin/discover.test.ts +253 -0
- package/src/plugin/discover.ts +353 -0
- package/src/plugin/events.test.ts +162 -0
- package/src/plugin/events.ts +266 -0
- package/src/plugin/hooks.ts +373 -0
- package/src/plugin/host.test.ts +696 -0
- package/src/plugin/host.ts +738 -0
- package/src/plugin/market.test.ts +781 -0
- package/src/plugin/market.ts +870 -0
- package/src/plugin/services.test.ts +135 -0
- package/src/plugin/services.ts +261 -0
- package/src/plugin/tar.test.ts +158 -0
- package/src/plugin/tar.ts +309 -0
- package/src/render/registry.test.ts +127 -0
- package/src/render/registry.ts +439 -0
- package/src/scheduler/index.ts +357 -0
- package/src/server/api.test.ts +802 -0
- package/src/server/api.ts +869 -0
- package/src/server/auth.ts +201 -0
- package/src/server/browse.test.ts +193 -0
- package/src/server/browse.ts +240 -0
- package/src/server/files.ts +97 -0
- package/src/server/index.test.ts +566 -0
- package/src/server/index.ts +1386 -0
- package/src/server/table.ts +351 -0
- package/src/store/index.ts +156 -0
- package/src/store/json.ts +198 -0
- package/src/store/kv.test.ts +307 -0
- package/src/store/kv.ts +256 -0
- package/src/store/level.ts +167 -0
- package/src/store/memory.ts +109 -0
- package/src/store/sql.test.ts +204 -0
- package/src/store/sql.ts +324 -0
- package/src/testing/fake.ts +297 -0
- package/src/testing/index.ts +15 -0
- package/src/testing/mock-adapter.ts +588 -0
- package/src/util/deep.ts +272 -0
- package/src/util/defer.ts +211 -0
- package/src/util/dispose.ts +169 -0
- package/src/util/duration.ts +82 -0
- package/src/util/fs.ts +279 -0
- package/src/util/id.ts +94 -0
- package/src/util/lru.ts +185 -0
- package/src/util/queue.ts +167 -0
- package/src/util/text.ts +189 -0
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:插件市场 —— 索引获取与缓存、插件安装、卸载与更新
|
|
3
|
+
* 依赖方向:依赖 http 客户端契约、logger 契约、`plugin/tar.ts` 与 `util/fs`;
|
|
4
|
+
* 不认识插件宿主,也不认识面板 —— 安装完成后由调用方决定何时加载
|
|
5
|
+
* 生命周期:随应用装配创建一次,索引缓存驻留内存并落盘一份,随应用停机一同丢弃
|
|
6
|
+
* 注意事项:四项与安全相关的约定,修改本文件前须一并考虑:
|
|
7
|
+
*
|
|
8
|
+
* 1) **写入范围限定在插件目录之内。** 插件名先经白名单校验,再经
|
|
9
|
+
* `joinWithin` 求值;两者任一不通过即拒绝,不做纠正后重试。
|
|
10
|
+
* 2) **先下载到临时目录,校验通过后再移入插件目录。** 中途失败时插件目录
|
|
11
|
+
* 保持原状,不会留下半个插件 —— 半个插件会在下次启动时被扫描到并加载失败。
|
|
12
|
+
* 3) **不自动执行包管理器。** 安装第三方代码的依赖等于执行其 install 脚本,
|
|
13
|
+
* 该动作须由用户在知情前提下自行发起;此处仅在返回值中标记需要安装依赖。
|
|
14
|
+
* 4) **索引为不可信输入。** 逐字段校验类型,缺字段的条目整条丢弃而非补默认值:
|
|
15
|
+
* 索引写错时应当表现为"该插件不出现在列表里",而不是出现一个装不上的条目。
|
|
16
|
+
*/
|
|
17
|
+
import { execFile } from "node:child_process";
|
|
18
|
+
import { cp, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
import { isDirectory, isFile } from "../util/fs.js";
|
|
21
|
+
import { extractTarGz, joinWithin, singleRoot } from "./tar.js";
|
|
22
|
+
/** 合法插件名:字母或数字开头,其余可含字母、数字、点、下划线与连字符 */
|
|
23
|
+
const NAME_RE = /^[a-z\d][a-z\d._-]*$/i;
|
|
24
|
+
/** 安装归档的体积上限 */
|
|
25
|
+
const MAX_ARCHIVE_BYTES = 64 * 1024 * 1024;
|
|
26
|
+
/**
|
|
27
|
+
* 克隆与下载的超时毫秒
|
|
28
|
+
*
|
|
29
|
+
* 与索引请求分开取值:索引是一份几十 KB 的 JSON,超时设短便于尽快切换到下一个源;
|
|
30
|
+
* 而一次插件传输可达数十 MB,在窄带链路上按索引超时衡量会把正常的下载判为失败。
|
|
31
|
+
*/
|
|
32
|
+
const TRANSFER_TIMEOUT_MS = 5 * 60 * 1000;
|
|
33
|
+
/** 只读本地仓库的 git 命令超时毫秒;不含网络往返,故与传输超时分开取值 */
|
|
34
|
+
const GIT_LOCAL_TIMEOUT_MS = 10_000;
|
|
35
|
+
/** 判定"看起来是一个插件"时接受的入口文件 */
|
|
36
|
+
const ENTRY_FILES = ["index.js", "index.mjs", "index.cjs", "dist/index.js", "dist/index.mjs"];
|
|
37
|
+
/** 应用镜像前缀的主机白名单 */
|
|
38
|
+
const MIRRORED_HOSTS = ["github.com", "raw.githubusercontent.com", "codeload.github.com"];
|
|
39
|
+
/**
|
|
40
|
+
* 校验插件名
|
|
41
|
+
*
|
|
42
|
+
* 该校验先于路径拼接执行。路径越界校验能挡住 `../`,但挡不住 `.git`、`node_modules`
|
|
43
|
+
* 这类落在目录之内却会破坏运行环境的名称,因此此处采用白名单而非黑名单。
|
|
44
|
+
* @param name 待校验的名称
|
|
45
|
+
* @returns 名称本身
|
|
46
|
+
* @throws 名称为空、含非法字符或为保留名时
|
|
47
|
+
*/
|
|
48
|
+
export function assertPluginName(name) {
|
|
49
|
+
if (!NAME_RE.test(name))
|
|
50
|
+
throw new Error(`插件名不合法:${name}`);
|
|
51
|
+
if (name === "node_modules" || name.startsWith("."))
|
|
52
|
+
throw new Error(`插件名为保留名:${name}`);
|
|
53
|
+
return name;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 比较两个版本号
|
|
57
|
+
*
|
|
58
|
+
* 仅比较点分数值段,忽略预发布标识。内核版本与 `minCore` 都由本项目自己产出,
|
|
59
|
+
* 不需要完整的 semver 语义,引入依赖的收益低于其体积。
|
|
60
|
+
* @param a 左操作数
|
|
61
|
+
* @param b 右操作数
|
|
62
|
+
* @returns a 大于 b 时为正,小于时为负,相等时为零
|
|
63
|
+
*/
|
|
64
|
+
export function compareVersion(a, b) {
|
|
65
|
+
const left = a.split("-")[0]?.split(".") ?? [];
|
|
66
|
+
const right = b.split("-")[0]?.split(".") ?? [];
|
|
67
|
+
for (let i = 0; i < Math.max(left.length, right.length); i++) {
|
|
68
|
+
const x = Number.parseInt(left[i] ?? "0", 10) || 0;
|
|
69
|
+
const y = Number.parseInt(right[i] ?? "0", 10) || 0;
|
|
70
|
+
if (x !== y)
|
|
71
|
+
return x - y;
|
|
72
|
+
}
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 对白名单主机的地址应用镜像前缀
|
|
77
|
+
*
|
|
78
|
+
* 只处理白名单内的主机:镜像站点通常只代理 GitHub,把任意地址都套上前缀会让
|
|
79
|
+
* 自建源无法访问,且更难排查 —— 用户看到的会是镜像站的 404 而不是自己的地址写错。
|
|
80
|
+
* @param url 原始地址
|
|
81
|
+
* @param mirror 镜像前缀,空串表示直连
|
|
82
|
+
* @returns 应用镜像后的地址
|
|
83
|
+
*/
|
|
84
|
+
export function applyMirror(url, mirror) {
|
|
85
|
+
const prefix = mirror.trim().replace(/\/+$/, "");
|
|
86
|
+
if (prefix === "")
|
|
87
|
+
return url;
|
|
88
|
+
let host;
|
|
89
|
+
try {
|
|
90
|
+
host = new URL(url).host;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return url;
|
|
94
|
+
}
|
|
95
|
+
if (!MIRRORED_HOSTS.includes(host))
|
|
96
|
+
return url;
|
|
97
|
+
return `${prefix}/${url}`;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* 把 git 仓库地址换算为 codeload 归档地址
|
|
101
|
+
*
|
|
102
|
+
* git 不可用时的退路。仅识别 GitHub:其归档地址可由仓库地址推导,而通用的
|
|
103
|
+
* git 服务端不存在统一的归档端点,推断得出的地址仅会导致一次无谓的失败请求。
|
|
104
|
+
* @param url 仓库地址
|
|
105
|
+
* @param branch 分支名,缺省取 HEAD
|
|
106
|
+
* @returns 归档地址;无法推导时 undefined
|
|
107
|
+
*/
|
|
108
|
+
export function tarballFromGit(url, branch = "HEAD") {
|
|
109
|
+
const match = /^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/.exec(url);
|
|
110
|
+
if (!match)
|
|
111
|
+
return undefined;
|
|
112
|
+
return `https://codeload.github.com/${match[1]}/${match[2]}/tar.gz/${branch}`;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 取一个字符串字段
|
|
116
|
+
* @param raw 记录
|
|
117
|
+
* @param key 字段名
|
|
118
|
+
* @returns 去除首尾空白后的值;类型不符或为空时 undefined
|
|
119
|
+
*/
|
|
120
|
+
function text(raw, key) {
|
|
121
|
+
const value = raw[key];
|
|
122
|
+
if (typeof value !== "string")
|
|
123
|
+
return undefined;
|
|
124
|
+
const trimmed = value.trim();
|
|
125
|
+
return trimmed === "" ? undefined : trimmed;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 解析安装来源
|
|
129
|
+
* @param raw 条目中的 `install` 字段
|
|
130
|
+
* @returns 安装来源;字段缺失或类型不符时 undefined
|
|
131
|
+
*/
|
|
132
|
+
function parseInstall(raw) {
|
|
133
|
+
if (typeof raw !== "object" || raw === null)
|
|
134
|
+
return undefined;
|
|
135
|
+
const record = raw;
|
|
136
|
+
const type = text(record, "type");
|
|
137
|
+
const url = text(record, "url");
|
|
138
|
+
if (url === undefined)
|
|
139
|
+
return undefined;
|
|
140
|
+
if (!/^https?:\/\//.test(url))
|
|
141
|
+
return undefined;
|
|
142
|
+
if (type !== "git" && type !== "tarball")
|
|
143
|
+
return undefined;
|
|
144
|
+
const branch = text(record, "branch");
|
|
145
|
+
return { type, url, ...(branch === undefined ? {} : { branch }) };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 把一条索引记录解析为条目
|
|
149
|
+
*
|
|
150
|
+
* 任一必填字段不合法即返回 undefined,由调用方整条丢弃。
|
|
151
|
+
* @param raw 索引记录
|
|
152
|
+
* @param source 该记录所属的索引地址
|
|
153
|
+
* @returns 条目;记录不合法时 undefined
|
|
154
|
+
*/
|
|
155
|
+
function parseEntry(raw, source) {
|
|
156
|
+
if (typeof raw !== "object" || raw === null)
|
|
157
|
+
return undefined;
|
|
158
|
+
const record = raw;
|
|
159
|
+
const name = text(record, "name");
|
|
160
|
+
if (name === undefined || !NAME_RE.test(name) || name.startsWith("."))
|
|
161
|
+
return undefined;
|
|
162
|
+
const install = parseInstall(record.install);
|
|
163
|
+
if (install === undefined)
|
|
164
|
+
return undefined;
|
|
165
|
+
const tags = Array.isArray(record.tags) ? record.tags.filter((tag) => typeof tag === "string") : [];
|
|
166
|
+
const author = text(record, "author");
|
|
167
|
+
const version = text(record, "version");
|
|
168
|
+
const homepage = text(record, "homepage");
|
|
169
|
+
const minCore = text(record, "minCore");
|
|
170
|
+
return {
|
|
171
|
+
name,
|
|
172
|
+
title: text(record, "title") ?? name,
|
|
173
|
+
description: text(record, "description") ?? "",
|
|
174
|
+
tags,
|
|
175
|
+
official: record.official === true,
|
|
176
|
+
install,
|
|
177
|
+
source,
|
|
178
|
+
...(author === undefined ? {} : { author }),
|
|
179
|
+
...(version === undefined ? {} : { version }),
|
|
180
|
+
...(homepage === undefined ? {} : { homepage }),
|
|
181
|
+
...(minCore === undefined ? {} : { minCore })
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* 解析一份索引文档
|
|
186
|
+
*
|
|
187
|
+
* 接受两种形状:`{ plugins: [...] }` 与顶层直接是数组。后者便于用户把一个手写的
|
|
188
|
+
* 数组挂到静态文件服务上作为私有源。
|
|
189
|
+
* @param raw 已解析的 JSON
|
|
190
|
+
* @param source 索引地址
|
|
191
|
+
* @returns 合法条目列表
|
|
192
|
+
* @throws 文档既不是数组也不含 `plugins` 数组时
|
|
193
|
+
*/
|
|
194
|
+
export function parseIndex(raw, source) {
|
|
195
|
+
const list = Array.isArray(raw)
|
|
196
|
+
? raw
|
|
197
|
+
: typeof raw === "object" && raw !== null && Array.isArray(raw.plugins)
|
|
198
|
+
? raw.plugins
|
|
199
|
+
: undefined;
|
|
200
|
+
if (list === undefined)
|
|
201
|
+
throw new Error("索引格式不符:期望数组或含 plugins 数组的对象");
|
|
202
|
+
const entries = [];
|
|
203
|
+
for (const item of list) {
|
|
204
|
+
const entry = parseEntry(item, source);
|
|
205
|
+
if (entry !== undefined)
|
|
206
|
+
entries.push(entry);
|
|
207
|
+
}
|
|
208
|
+
return entries;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* 调用本机 git 执行一次命令,`MarketDeps.git` 的缺省实现
|
|
212
|
+
*
|
|
213
|
+
* 参数以数组传递,不经 shell,因此索引里的地址不会被当作命令解释。
|
|
214
|
+
* `GIT_TERMINAL_PROMPT=0` 与 `GIT_ASKPASS` 一并关闭凭据提示:市场安装发生在
|
|
215
|
+
* 无人值守的请求处理过程中,弹出的提示无人应答,只会让请求挂到超时。
|
|
216
|
+
* @param args 命令参数
|
|
217
|
+
* @param cwd 工作目录
|
|
218
|
+
* @param timeout 超时毫秒
|
|
219
|
+
* @returns 标准输出
|
|
220
|
+
* @throws 命令不存在、超时或退出码非零时
|
|
221
|
+
*/
|
|
222
|
+
const runGit = (args, cwd, timeout) => {
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
execFile("git", [...args], { cwd, timeout, env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "echo" }, windowsHide: true }, (err, stdout, stderr) => {
|
|
225
|
+
if (err)
|
|
226
|
+
reject(new Error(`git ${args[0]} 失败:${stderr.trim() || err.message}`));
|
|
227
|
+
else
|
|
228
|
+
resolve(stdout);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
/**
|
|
233
|
+
* 插件市场
|
|
234
|
+
*
|
|
235
|
+
* 索引缓存同时驻留内存与磁盘:内存缓存服务于同一次运行内的反复浏览,磁盘缓存
|
|
236
|
+
* 服务于重启后的首次打开 —— 面板在无网络时也应当能列出上次看到的插件,
|
|
237
|
+
* 而不是显示一个空列表。
|
|
238
|
+
*/
|
|
239
|
+
export class PluginMarket {
|
|
240
|
+
/** 依赖 */
|
|
241
|
+
#deps;
|
|
242
|
+
/** 内存中的索引条目 */
|
|
243
|
+
#entries = [];
|
|
244
|
+
/** 内存索引的获取时间,0 表示尚未取到 */
|
|
245
|
+
#fetchedAt = 0;
|
|
246
|
+
/** 逐源结果,随索引一同更新 */
|
|
247
|
+
#sources = [];
|
|
248
|
+
/** 正在进行的索引获取,用于合并并发请求 */
|
|
249
|
+
#inflight;
|
|
250
|
+
/** git 可用性探测结果 */
|
|
251
|
+
#gitAvailable;
|
|
252
|
+
/** 执行 git 命令,未注入时调用本机 git */
|
|
253
|
+
#git;
|
|
254
|
+
/**
|
|
255
|
+
* @param deps 依赖
|
|
256
|
+
*/
|
|
257
|
+
constructor(deps) {
|
|
258
|
+
this.#deps = deps;
|
|
259
|
+
this.#git = deps.git ?? runGit;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* 列出市场中的插件
|
|
263
|
+
*
|
|
264
|
+
* 缓存未过期时不发起网络请求。安装状态每次都重新读取文件系统:用户可能在面板
|
|
265
|
+
* 之外手动删掉了插件目录,沿用缓存里的状态会让"卸载"按钮对着一个不存在的目录。
|
|
266
|
+
* @param force 忽略缓存,强制重新获取
|
|
267
|
+
* @returns 索引快照
|
|
268
|
+
*/
|
|
269
|
+
async list(force = false) {
|
|
270
|
+
const settings = this.#deps.settings();
|
|
271
|
+
const fresh = this.#fetchedAt > 0 && Date.now() - this.#fetchedAt < settings.cacheTtl;
|
|
272
|
+
const cached = !force && fresh;
|
|
273
|
+
if (force || !fresh)
|
|
274
|
+
await this.#refresh(force);
|
|
275
|
+
const plugins = [];
|
|
276
|
+
for (const entry of this.#entries) {
|
|
277
|
+
plugins.push({ ...entry, installed: await this.#isInstalled(entry.name) });
|
|
278
|
+
}
|
|
279
|
+
plugins.sort((a, b) => a.name.localeCompare(b.name));
|
|
280
|
+
return { fetchedAt: this.#fetchedAt, cached, sources: [...this.#sources], plugins };
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* 按名称取一个条目
|
|
284
|
+
* @param name 插件名
|
|
285
|
+
* @returns 条目;索引中没有时 undefined
|
|
286
|
+
*/
|
|
287
|
+
async entry(name) {
|
|
288
|
+
await this.list();
|
|
289
|
+
return this.#entries.find(item => item.name === name);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* 合并并发的索引获取
|
|
293
|
+
*
|
|
294
|
+
* 面板打开时会同时请求概览与插件列表,两者都会触发获取。此处让后到者复用
|
|
295
|
+
* 同一次网络请求,而不是各发一遍。
|
|
296
|
+
* @param force 忽略磁盘缓存
|
|
297
|
+
* @returns 获取完成
|
|
298
|
+
*/
|
|
299
|
+
async #refresh(force) {
|
|
300
|
+
if (this.#inflight !== undefined)
|
|
301
|
+
return this.#inflight;
|
|
302
|
+
const task = this.#fetchAll(force).finally(() => {
|
|
303
|
+
this.#inflight = undefined;
|
|
304
|
+
});
|
|
305
|
+
this.#inflight = task;
|
|
306
|
+
return task;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* 逐个索引获取并合并条目
|
|
310
|
+
*
|
|
311
|
+
* 同名插件以靠前的索引为准,因此用户可以把私有源放在官方源之前来覆盖某个条目。
|
|
312
|
+
* 全部索引都失败且没有缓存可用时,条目置空但仍记录获取时间:面板据 `sources`
|
|
313
|
+
* 展示失败原因,重试由用户显式触发,避免每次刷新页面都重复请求已知不可达的地址。
|
|
314
|
+
* @param force 忽略磁盘缓存
|
|
315
|
+
*/
|
|
316
|
+
async #fetchAll(force) {
|
|
317
|
+
const { sources, mirror, timeout, cacheTtl } = this.#deps.settings();
|
|
318
|
+
if (!force && this.#fetchedAt === 0) {
|
|
319
|
+
const cache = await this.#loadCache();
|
|
320
|
+
if (cache !== undefined && Date.now() - cache.fetchedAt < cacheTtl) {
|
|
321
|
+
this.#entries = cache.entries;
|
|
322
|
+
this.#fetchedAt = cache.fetchedAt;
|
|
323
|
+
this.#sources = [{ url: this.#deps.cacheFile, ok: true, count: cache.entries.length }];
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const results = [];
|
|
328
|
+
const merged = new Map();
|
|
329
|
+
for (const url of sources) {
|
|
330
|
+
try {
|
|
331
|
+
const raw = await this.#deps.http.get(applyMirror(url, mirror), {
|
|
332
|
+
responseType: "json",
|
|
333
|
+
timeout,
|
|
334
|
+
retry: 1
|
|
335
|
+
});
|
|
336
|
+
const entries = parseIndex(raw, url);
|
|
337
|
+
for (const item of entries)
|
|
338
|
+
if (!merged.has(item.name))
|
|
339
|
+
merged.set(item.name, item);
|
|
340
|
+
results.push({ url, ok: true, count: entries.length });
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
344
|
+
results.push({ url, ok: false, error, count: 0 });
|
|
345
|
+
this.#deps.logger.warn(`插件市场索引获取失败 ${url}:${error}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
this.#sources = results;
|
|
349
|
+
if (merged.size === 0 && results.every(item => !item.ok)) {
|
|
350
|
+
const cache = await this.#loadCache();
|
|
351
|
+
if (cache !== undefined) {
|
|
352
|
+
this.#entries = cache.entries;
|
|
353
|
+
this.#fetchedAt = cache.fetchedAt;
|
|
354
|
+
this.#deps.logger.warn("插件市场全部索引不可达,沿用上次缓存");
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
this.#entries = [...merged.values()];
|
|
359
|
+
this.#fetchedAt = Date.now();
|
|
360
|
+
await this.#saveCache();
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* 读取磁盘缓存
|
|
364
|
+
*
|
|
365
|
+
* 缓存文件按不可信输入对待并重新经 `parseIndex` 校验:文件可能被用户手工改动,
|
|
366
|
+
* 也可能是旧版本写下的、字段形状已经不同的内容。
|
|
367
|
+
* @returns 缓存内容;文件不存在或不可解析时 undefined
|
|
368
|
+
*/
|
|
369
|
+
async #loadCache() {
|
|
370
|
+
try {
|
|
371
|
+
const raw = JSON.parse(await readFile(this.#deps.cacheFile, "utf8"));
|
|
372
|
+
if (typeof raw !== "object" || raw === null)
|
|
373
|
+
return undefined;
|
|
374
|
+
const record = raw;
|
|
375
|
+
if (typeof record.fetchedAt !== "number")
|
|
376
|
+
return undefined;
|
|
377
|
+
const entries = parseIndex(record.entries ?? [], this.#deps.cacheFile);
|
|
378
|
+
return { fetchedAt: record.fetchedAt, entries };
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* 写入磁盘缓存
|
|
386
|
+
*
|
|
387
|
+
* 写入失败只记日志:缓存是加速手段,写不进去不应当让一次成功的索引获取失败。
|
|
388
|
+
*/
|
|
389
|
+
async #saveCache() {
|
|
390
|
+
const doc = { fetchedAt: this.#fetchedAt, entries: this.#entries };
|
|
391
|
+
try {
|
|
392
|
+
await mkdir(dirname(this.#deps.cacheFile), { recursive: true });
|
|
393
|
+
await writeFile(this.#deps.cacheFile, JSON.stringify(doc, undefined, 2), "utf8");
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
this.#deps.logger.debug(`插件市场缓存写入失败:${err instanceof Error ? err.message : String(err)}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* 判断插件目录下是否已存在同名目录
|
|
401
|
+
* @param name 插件名
|
|
402
|
+
* @returns 是否已安装
|
|
403
|
+
*/
|
|
404
|
+
async #isInstalled(name) {
|
|
405
|
+
try {
|
|
406
|
+
return await isDirectory(joinWithin(this.#deps.pluginsDir, name));
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* 安装一个插件
|
|
414
|
+
*
|
|
415
|
+
* 全过程在临时目录内完成,仅在校验通过后才移入插件目录,因此失败时插件目录
|
|
416
|
+
* 保持原状。安装完成后不加载插件:加载时机由调用方决定,面板据返回值提示用户
|
|
417
|
+
* 是否需要先安装依赖。
|
|
418
|
+
* @param name 插件名
|
|
419
|
+
* @param opts 可选参数
|
|
420
|
+
* @param opts.replace 目标已存在时先删除再安装
|
|
421
|
+
* @returns 安装结果
|
|
422
|
+
* @throws 名称不合法、索引中无此插件、内核版本不满足、目标已存在或取源失败时
|
|
423
|
+
*/
|
|
424
|
+
async install(name, opts = {}) {
|
|
425
|
+
assertPluginName(name);
|
|
426
|
+
const entry = await this.entry(name);
|
|
427
|
+
if (entry === undefined)
|
|
428
|
+
throw new Error(`插件市场中没有名为 ${name} 的插件`);
|
|
429
|
+
const target = joinWithin(this.#deps.pluginsDir, name);
|
|
430
|
+
const exists = await isDirectory(target);
|
|
431
|
+
if (exists && opts.replace !== true)
|
|
432
|
+
throw new Error(`插件 ${name} 已安装,如需覆盖请先卸载`);
|
|
433
|
+
if (entry.minCore !== undefined && compareVersion(this.#deps.coreVersion, entry.minCore) < 0) {
|
|
434
|
+
throw new Error(`插件 ${name} 要求内核版本不低于 ${entry.minCore},当前为 ${this.#deps.coreVersion}`);
|
|
435
|
+
}
|
|
436
|
+
await mkdir(this.#deps.tempDir, { recursive: true });
|
|
437
|
+
const staging = await mkdtemp(join(this.#deps.tempDir, `market-${name}-`));
|
|
438
|
+
try {
|
|
439
|
+
const { via, root } = await this.#fetch(entry, staging);
|
|
440
|
+
await this.#assertLooksLikePlugin(root, name);
|
|
441
|
+
const manifest = await this.#manifest(root);
|
|
442
|
+
const version = manifest?.version ?? entry.version ?? "0.0.0";
|
|
443
|
+
const needsDependencies = Object.keys(manifest?.dependencies ?? {}).length > 0 && !(await isDirectory(join(root, "node_modules")));
|
|
444
|
+
if (exists)
|
|
445
|
+
await rm(target, { recursive: true, force: true });
|
|
446
|
+
await mkdir(this.#deps.pluginsDir, { recursive: true });
|
|
447
|
+
await this.#move(root, target);
|
|
448
|
+
this.#deps.logger.info(`插件 ${name}@${version} 已安装至 ${target}`);
|
|
449
|
+
if (needsDependencies)
|
|
450
|
+
this.#deps.logger.warn(`插件 ${name} 声明了运行时依赖,需在其目录内自行执行包管理器安装`);
|
|
451
|
+
// 带 `.git` 的目录此后可就地拉取;归档装出来的每次更新都要整目录重下
|
|
452
|
+
return { name, dir: target, via, version, needsDependencies, updatable: via === "git" ? "pull" : "reinstall" };
|
|
453
|
+
}
|
|
454
|
+
finally {
|
|
455
|
+
await rm(staging, { recursive: true, force: true });
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* 卸载一个插件
|
|
460
|
+
*
|
|
461
|
+
* 只删除插件目录。配置文件与数据库另行存放,保留它们使得重新安装后原有配置仍然有效。
|
|
462
|
+
* @param name 插件名
|
|
463
|
+
* @returns 是否确实删除了目录
|
|
464
|
+
* @throws 名称不合法时
|
|
465
|
+
*/
|
|
466
|
+
async remove(name) {
|
|
467
|
+
assertPluginName(name);
|
|
468
|
+
const target = joinWithin(this.#deps.pluginsDir, name);
|
|
469
|
+
if (!(await isDirectory(target)))
|
|
470
|
+
return false;
|
|
471
|
+
await rm(target, { recursive: true, force: true });
|
|
472
|
+
this.#deps.logger.info(`插件 ${name} 目录已删除:${target}`);
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* 更新一个插件:目录已是 git 仓库时就地拉取,否则退回重新安装
|
|
477
|
+
* @param name 插件名
|
|
478
|
+
* @returns 安装结果
|
|
479
|
+
* @throws 与 `install` 相同;就地拉取失败时抛出 git 的错误
|
|
480
|
+
*/
|
|
481
|
+
async update(name) {
|
|
482
|
+
/*
|
|
483
|
+
* 优先就地拉取,因为重新安装会删掉目录里那份 `node_modules`
|
|
484
|
+
*
|
|
485
|
+
* 那是使用者按提示自己装的,动辄几十兆;删掉意味着每更新一次插件就要在国内网络下
|
|
486
|
+
* 重装一遍依赖,而更新的内容通常只是几行代码。就地拉取还保住目录内其他不属于仓库的
|
|
487
|
+
* 东西(插件写在安装目录下的缓存、使用者放进去的资源)—— 那些本不该放在那里,
|
|
488
|
+
* 但目录是使用者的,删它得有个理由,而「顺便」不是理由。
|
|
489
|
+
*
|
|
490
|
+
* 退回重新安装的三种情形都无从拉取:目录不是 git 仓库、本机没有 git、来源不是 git。
|
|
491
|
+
*/
|
|
492
|
+
assertPluginName(name);
|
|
493
|
+
const target = joinWithin(this.#deps.pluginsDir, name);
|
|
494
|
+
const pulled = await this.#tryPull(name, target);
|
|
495
|
+
return pulled ?? this.install(name, { replace: true });
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* 试着就地拉取一个插件
|
|
499
|
+
* @param name 插件名
|
|
500
|
+
* @param dir 插件安装目录
|
|
501
|
+
* @returns 拉取结果;不具备就地拉取条件(无 git、目录不是仓库、来源不是 git)时 undefined
|
|
502
|
+
* @throws 拉取过程本身失败时 —— 那意味着网络或仓库状态有问题,此时退回重新安装会把一次
|
|
503
|
+
* 可修复的失败变成一次目录删除
|
|
504
|
+
*/
|
|
505
|
+
async #tryPull(name, dir) {
|
|
506
|
+
/*
|
|
507
|
+
* 两处刻意的做法
|
|
508
|
+
*
|
|
509
|
+
* **`fetch` + `reset --hard` 而非 `pull`。** `pull` 会试图合并,而目录里的改动多半不是
|
|
510
|
+
* 有意维护的分叉(编辑器留下的文件、上次安装的残留);一次合并冲突会让目录停在半新半旧
|
|
511
|
+
* 的状态,且要求使用者懂 git 才能收拾。`reset --hard` 的语义是「以远端为准」,与「更新
|
|
512
|
+
* 插件」这个动作一致。
|
|
513
|
+
*
|
|
514
|
+
* **改动先 stash 再 reset,顺序即安全性。** 那些改动有时是刻意为之(改了一处硬编码的
|
|
515
|
+
* 地址),故留一份可取回的副本并在日志里说明取回办法 —— 直接丢掉等于替使用者做了他没
|
|
516
|
+
* 同意的决定。反过来先 reset 后 stash 就是数据丢失。
|
|
517
|
+
*/
|
|
518
|
+
if (!(await isDirectory(join(dir, ".git"))))
|
|
519
|
+
return undefined;
|
|
520
|
+
const entry = await this.entry(name);
|
|
521
|
+
if (entry === undefined)
|
|
522
|
+
throw new Error(`插件市场中没有名为 ${name} 的插件`);
|
|
523
|
+
if (entry.install.type !== "git")
|
|
524
|
+
return undefined;
|
|
525
|
+
if (!(await this.#hasGit()))
|
|
526
|
+
return undefined;
|
|
527
|
+
if (entry.minCore !== undefined && compareVersion(this.#deps.coreVersion, entry.minCore) < 0) {
|
|
528
|
+
throw new Error(`插件 ${name} 要求内核版本不低于 ${entry.minCore},当前为 ${this.#deps.coreVersion}`);
|
|
529
|
+
}
|
|
530
|
+
const before = (await this.#manifest(dir))?.version;
|
|
531
|
+
const head = async () => (await this.#git(["rev-parse", "HEAD"], dir, GIT_LOCAL_TIMEOUT_MS)).trim();
|
|
532
|
+
const wasAt = await head();
|
|
533
|
+
// 使用者改过的文件先暂存。`--include-untracked` 一并收进去:新增的文件同样会被
|
|
534
|
+
// reset 之后的 checkout 撞上,而它更可能是使用者自己放的东西
|
|
535
|
+
const dirty = (await this.#git(["status", "--porcelain"], dir, GIT_LOCAL_TIMEOUT_MS)).trim() !== "";
|
|
536
|
+
if (dirty) {
|
|
537
|
+
await this.#git(["stash", "push", "--include-untracked", "-m", `yunzai-ng 更新前自动暂存 ${new Date().toISOString()}`], dir, GIT_LOCAL_TIMEOUT_MS);
|
|
538
|
+
this.#deps.logger.warn(`插件 ${name} 目录内有未提交的改动,已暂存。如需取回:在该目录执行 git stash pop`);
|
|
539
|
+
}
|
|
540
|
+
const { mirror } = this.#deps.settings();
|
|
541
|
+
const branch = entry.install.branch;
|
|
542
|
+
/*
|
|
543
|
+
* 拉取时重新指定远端地址,不沿用目录里记着的那个
|
|
544
|
+
*
|
|
545
|
+
* 镜像前缀是可改的配置项:使用者装插件时用着某个镜像,此后换了一个,而目录里的
|
|
546
|
+
* origin 仍指向旧镜像。那个镜像可能已经关站,于是"更新"永远失败,且错误信息
|
|
547
|
+
* 指向一个使用者早已不用的地址。以索引里的地址为准,镜像每次现算。
|
|
548
|
+
*/
|
|
549
|
+
const url = applyMirror(entry.install.url, mirror);
|
|
550
|
+
const ref = branch ?? "HEAD";
|
|
551
|
+
await this.#git(["fetch", "--depth", "1", url, ref], dir, TRANSFER_TIMEOUT_MS);
|
|
552
|
+
await this.#git(["reset", "--hard", "FETCH_HEAD"], dir, GIT_LOCAL_TIMEOUT_MS);
|
|
553
|
+
const nowAt = await head();
|
|
554
|
+
const changed = nowAt !== wasAt;
|
|
555
|
+
const manifest = await this.#manifest(dir);
|
|
556
|
+
const version = manifest?.version ?? entry.version ?? "0.0.0";
|
|
557
|
+
const needsDependencies = Object.keys(manifest?.dependencies ?? {}).length > 0 && !(await isDirectory(join(dir, "node_modules")));
|
|
558
|
+
if (changed)
|
|
559
|
+
this.#deps.logger.info(`插件 ${name} 已就地更新至 ${version}(${wasAt.slice(0, 7)} → ${nowAt.slice(0, 7)})`);
|
|
560
|
+
else
|
|
561
|
+
this.#deps.logger.info(`插件 ${name} 已是最新版本 ${version}`);
|
|
562
|
+
if (needsDependencies)
|
|
563
|
+
this.#deps.logger.warn(`插件 ${name} 声明了运行时依赖,需在其目录内自行执行包管理器安装`);
|
|
564
|
+
return {
|
|
565
|
+
name,
|
|
566
|
+
dir,
|
|
567
|
+
via: "pull",
|
|
568
|
+
version,
|
|
569
|
+
needsDependencies,
|
|
570
|
+
changed,
|
|
571
|
+
// 就地拉取过一次,说明目录确实是 git 仓库,此后照旧走这条路
|
|
572
|
+
updatable: "pull",
|
|
573
|
+
...(before === undefined ? {} : { fromVersion: before })
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* 把插件内容取到临时目录
|
|
578
|
+
*
|
|
579
|
+
* git 优先:克隆得到的目录带 `.git`,用户此后可以自行 `git pull`,也便于切换分支。
|
|
580
|
+
* git 不可用时退回归档下载,GitHub 仓库地址可换算出 codeload 归档地址,
|
|
581
|
+
* 其余来源若只提供 git 则明确报错,而不是静默失败。
|
|
582
|
+
* @param entry 索引条目
|
|
583
|
+
* @param staging 临时目录
|
|
584
|
+
* @returns 取源方式与插件内容的根目录
|
|
585
|
+
* @throws git 与归档两条路都不可用,或归档为空时
|
|
586
|
+
*/
|
|
587
|
+
async #fetch(entry, staging) {
|
|
588
|
+
const { mirror } = this.#deps.settings();
|
|
589
|
+
if (entry.install.type === "git" && (await this.#hasGit())) {
|
|
590
|
+
const dest = join(staging, "repo");
|
|
591
|
+
const args = ["clone", "--depth", "1", "--single-branch"];
|
|
592
|
+
if (entry.install.branch !== undefined)
|
|
593
|
+
args.push("--branch", entry.install.branch);
|
|
594
|
+
args.push(applyMirror(entry.install.url, mirror), dest);
|
|
595
|
+
await this.#git(args, staging, TRANSFER_TIMEOUT_MS);
|
|
596
|
+
return { via: "git", root: dest };
|
|
597
|
+
}
|
|
598
|
+
const url = entry.install.type === "tarball"
|
|
599
|
+
? entry.install.url
|
|
600
|
+
: tarballFromGit(entry.install.url, entry.install.branch);
|
|
601
|
+
if (url === undefined)
|
|
602
|
+
throw new Error(`插件 ${entry.name} 仅提供 git 来源,而本机未安装 git`);
|
|
603
|
+
const bytes = await this.#deps.http.buffer(applyMirror(url, mirror), {
|
|
604
|
+
timeout: TRANSFER_TIMEOUT_MS,
|
|
605
|
+
retry: 1
|
|
606
|
+
});
|
|
607
|
+
if (bytes.byteLength > MAX_ARCHIVE_BYTES) {
|
|
608
|
+
throw new Error(`归档体积 ${bytes.byteLength} 字节超过上限 ${MAX_ARCHIVE_BYTES} 字节`);
|
|
609
|
+
}
|
|
610
|
+
const raw = join(staging, "raw");
|
|
611
|
+
const written = await extractTarGz(bytes, raw);
|
|
612
|
+
if (written.length === 0)
|
|
613
|
+
throw new Error(`归档 ${url} 中没有可写入的文件`);
|
|
614
|
+
const top = singleRoot(written);
|
|
615
|
+
return { via: "tarball", root: top === undefined ? raw : join(raw, top) };
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* 探测本机 git 可用性
|
|
619
|
+
*
|
|
620
|
+
* 结果缓存到进程结束。用户在运行期间安装 git 属于少见情形,重启即可重新探测,
|
|
621
|
+
* 不值得为此在每次安装时多执行一次子进程。
|
|
622
|
+
*
|
|
623
|
+
* **先建出临时目录再探测。** 它是这条命令的 cwd,而子进程的 cwd 不存在时
|
|
624
|
+
* execFile 报 ENOENT —— 那会被下面的 catch 记成「本机没有 git」并缓存到进程结束,
|
|
625
|
+
* 于是此后每一次安装都退回归档下载,而真实原因与 git 毫无关系。`install` 恰好在
|
|
626
|
+
* 调用本方法之前建过该目录,`update` 的就地拉取那条路没有,故不能依赖调用顺序。
|
|
627
|
+
* @returns git 是否可用
|
|
628
|
+
*/
|
|
629
|
+
async #hasGit() {
|
|
630
|
+
if (this.#gitAvailable !== undefined)
|
|
631
|
+
return this.#gitAvailable;
|
|
632
|
+
try {
|
|
633
|
+
await mkdir(this.#deps.tempDir, { recursive: true });
|
|
634
|
+
await this.#git(["--version"], this.#deps.tempDir, GIT_LOCAL_TIMEOUT_MS);
|
|
635
|
+
this.#gitAvailable = true;
|
|
636
|
+
}
|
|
637
|
+
catch {
|
|
638
|
+
this.#gitAvailable = false;
|
|
639
|
+
this.#deps.logger.debug("本机未检测到 git,插件安装改用归档下载");
|
|
640
|
+
}
|
|
641
|
+
return this.#gitAvailable;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* 校验取到的内容是否具备插件形态
|
|
645
|
+
*
|
|
646
|
+
* 该校验的目的是及早给出可理解的错误:地址写错时下载到的往往是一份文档仓库或
|
|
647
|
+
* 一个空目录,若不检查就移入插件目录,用户看到的会是下次启动时的一条加载失败。
|
|
648
|
+
* @param root 内容根目录
|
|
649
|
+
* @param name 插件名
|
|
650
|
+
* @throws 既无 package.json 也无入口文件时
|
|
651
|
+
*/
|
|
652
|
+
async #assertLooksLikePlugin(root, name) {
|
|
653
|
+
if (await isFile(join(root, "package.json")))
|
|
654
|
+
return;
|
|
655
|
+
for (const entry of ENTRY_FILES)
|
|
656
|
+
if (await isFile(join(root, entry)))
|
|
657
|
+
return;
|
|
658
|
+
throw new Error(`取到的内容不像插件:${name} 目录内既无 package.json 也无入口文件`);
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* 读取 package.json 中与安装相关的字段
|
|
662
|
+
* @param root 内容根目录
|
|
663
|
+
* @returns 版本与依赖声明;无 package.json 或不可解析时 undefined
|
|
664
|
+
*/
|
|
665
|
+
async #manifest(root) {
|
|
666
|
+
try {
|
|
667
|
+
const raw = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
|
|
668
|
+
if (typeof raw !== "object" || raw === null)
|
|
669
|
+
return undefined;
|
|
670
|
+
const record = raw;
|
|
671
|
+
const version = typeof record.version === "string" ? record.version : undefined;
|
|
672
|
+
const dependencies = typeof record.dependencies === "object" && record.dependencies !== null
|
|
673
|
+
? record.dependencies
|
|
674
|
+
: undefined;
|
|
675
|
+
return { ...(version === undefined ? {} : { version }), ...(dependencies === undefined ? {} : { dependencies }) };
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
return undefined;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* 把临时目录中的内容移到插件目录
|
|
683
|
+
*
|
|
684
|
+
* `rename` 在跨设备时会失败(EXDEV)—— 用户可能把临时目录配到另一个分区,
|
|
685
|
+
* 此时退回递归复制。复制成本高于改名,因此仅作为兜底而非默认路径。
|
|
686
|
+
* @param from 源目录
|
|
687
|
+
* @param to 目标目录
|
|
688
|
+
*/
|
|
689
|
+
async #move(from, to) {
|
|
690
|
+
try {
|
|
691
|
+
await rename(from, to);
|
|
692
|
+
}
|
|
693
|
+
catch (err) {
|
|
694
|
+
if (err.code !== "EXDEV")
|
|
695
|
+
throw err;
|
|
696
|
+
await cp(from, to, { recursive: true });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
//# sourceMappingURL=market.js.map
|