node-karin 1.15.5 → 1.16.1

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.
Files changed (89) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/adapter-BqlH3u3X.mjs +218 -0
  3. package/dist/app-DdMQbBEY.mjs +4109 -0
  4. package/dist/cache-CPcPeo6N.mjs +163 -0
  5. package/dist/chunk-NzVPYdc1.mjs +21 -0
  6. package/dist/cli/index.cjs +10900 -1
  7. package/dist/cli/index.d.ts +1 -1
  8. package/dist/cli/index.mjs +10770 -10224
  9. package/dist/file-ZGuqNDd-.mjs +15987 -0
  10. package/dist/file-dGy9of8-.mjs +268 -0
  11. package/dist/fsSync-Cf5MWILk.mjs +65 -0
  12. package/dist/index.d.ts +12235 -12738
  13. package/dist/index.mjs +2054 -25247
  14. package/dist/internal-DupfycKE.mjs +597 -0
  15. package/dist/kv-DZp4UIxg.mjs +192 -0
  16. package/dist/module/art-template.d.ts +2 -13
  17. package/dist/module/art-template.mjs +3 -1
  18. package/dist/module/axios.d.ts +3 -2
  19. package/dist/module/axios.mjs +5 -2
  20. package/dist/module/chalk.d.ts +3 -2
  21. package/dist/module/chalk.mjs +5 -2
  22. package/dist/module/chokidar.d.ts +3 -2
  23. package/dist/module/chokidar.mjs +5 -2
  24. package/dist/module/express.d.ts +2 -1
  25. package/dist/module/express.mjs +3 -1
  26. package/dist/module/lodash.d.ts +2 -1
  27. package/dist/module/lodash.mjs +3 -1
  28. package/dist/module/log4js.d.ts +3 -2
  29. package/dist/module/log4js.mjs +5 -2
  30. package/dist/module/moment.d.ts +2 -1
  31. package/dist/module/moment.mjs +3 -1
  32. package/dist/module/node-schedule.d.ts +3 -2
  33. package/dist/module/node-schedule.mjs +5 -2
  34. package/dist/module/redis.d.ts +3 -2
  35. package/dist/module/redis.mjs +5 -2
  36. package/dist/module/sqlite3.d.ts +3 -2
  37. package/dist/module/sqlite3.mjs +5 -2
  38. package/dist/module/ws.d.ts +3 -2
  39. package/dist/module/ws.mjs +5 -2
  40. package/dist/module/yaml.d.ts +3 -2
  41. package/dist/module/yaml.mjs +5 -2
  42. package/dist/queue-CnKedaZA.mjs +70 -0
  43. package/dist/redis-aLJ7wbJH.mjs +1556 -0
  44. package/dist/render-DPqueDZr.mjs +170 -0
  45. package/dist/root.d.ts +46 -46
  46. package/dist/root.mjs +136 -93
  47. package/dist/router-zPSN9-tY.mjs +124 -0
  48. package/dist/server-DT64D-m-.mjs +38 -0
  49. package/dist/snapka-BTlnZOyI.mjs +450 -0
  50. package/dist/sqlite-Dcj9jlW9.mjs +307 -0
  51. package/dist/start/app.d.ts +1 -1
  52. package/dist/start/app.mjs +14 -7
  53. package/dist/start/index.d.ts +1 -1
  54. package/dist/start/index.mjs +325 -656
  55. package/dist/template-Djk6y0uC.mjs +133 -0
  56. package/dist/terminalManager-Lxa8Sm06.mjs +783 -0
  57. package/dist/uptime-C121X_rq.mjs +210 -0
  58. package/dist/web/{CompressaPRO-GX.woff2.br → CompressaPRO-GX.woff2} +0 -0
  59. package/dist/web/assets/css/style-CBB8wM_W.css +14880 -0
  60. package/dist/web/assets/js/entry-Blf4Trpx.js +258540 -0
  61. package/dist/web/{googleapis.woff2.br → googleapis.woff2} +0 -0
  62. package/dist/web/index.html +2 -15
  63. package/dist/web/karin.png +0 -0
  64. package/dist/web/sha256.min.js +9 -0
  65. package/dist/ws-BLDoC2gV.mjs +80 -0
  66. package/dist/ws-CcoWd3Ar.mjs +106 -0
  67. package/package.json +7 -7
  68. package/dist/global.d.d.ts +0 -68
  69. package/dist/types-hAhbXJDZ.d.ts +0 -109
  70. package/dist/web/assets/css/components-ep7vm38G.css +0 -1
  71. package/dist/web/assets/css/index-Dadvd9mn.css.br +0 -0
  72. package/dist/web/assets/css/vendor-editor-CFbL2ovg.css.br +0 -0
  73. package/dist/web/assets/css/vendor-others-ZgkIHsf0.css +0 -1
  74. package/dist/web/assets/js/components-CU2xw4lY.js.br +0 -0
  75. package/dist/web/assets/js/entry-Dvb7eYLE.js.br +0 -0
  76. package/dist/web/assets/js/hooks-CRfhs4ON.js.br +0 -0
  77. package/dist/web/assets/js/page-404.tsx-DYMd_RI_.js +0 -1
  78. package/dist/web/assets/js/page-dashboard-CG60V_Z-.js.br +0 -0
  79. package/dist/web/assets/js/page-loading.tsx-wY8a9me3.js.br +0 -0
  80. package/dist/web/assets/js/page-login.tsx-B54ZOEZB.js.br +0 -0
  81. package/dist/web/assets/js/utils-C9nWTSuo.js +0 -2
  82. package/dist/web/assets/js/vendor-editor-BmqYP7lh.js.br +0 -0
  83. package/dist/web/assets/js/vendor-heroui-ClBCy2zk.js.br +0 -0
  84. package/dist/web/assets/js/vendor-others-6GiMrjd4.js.br +0 -0
  85. package/dist/web/assets/js/vendor-react-Dc9jdQiK.js.br +0 -0
  86. package/dist/web/assets/js/vendor-ui-utils-D0xkboLL.js.br +0 -0
  87. package/dist/web/assets/js/vendor-visual-saF8KLH_.js.br +0 -0
  88. package/dist/web/karin.png.br +0 -0
  89. package/dist/web/sha256.min.js.br +0 -0
@@ -0,0 +1,4109 @@
1
+ import { t as __exportAll } from "./chunk-NzVPYdc1.mjs";
2
+ import { consolePath, karinPathConfig, karinPathPlugins, logsPath } from "./root.mjs";
3
+ import { $n as setConfig, Ai as isPnpm10, Di as requireFileSync, Fn as updatePkg, In as importModule, Ln as imports, N as getBotCount, Ni as isWorkspace, Rn as isClass, Wn as isLocalRequest, _ as taskSystem, _n as createPluginMismatchReporter, a as getPrivatesFileData, ar as getSecretOrPrivateKey, br as createUnauthorizedResponse, c as config, cr as verifyRefreshToken, dr as createBadRequestResponse, er as updateLevel, fr as createForbiddenResponse, gn as getPluginsInfo, gr as createRefreshTokenExpiredResponse, hn as getPlugins, hr as createPayloadTooLargeResponse, i as getRenderCfg, ir as createJwt, ji as isTs, ki as isDev, mr as createNotFoundResponse, n as redis, nr as getEnv, o as getGroupsFileData, or as refreshAccessToken, pr as createMethodNotAllowedResponse, r as pm2, rr as writeEnv, u as adapter, vr as createServerErrorResponse, wn as restartDirect, yn as satisfies, yr as createSuccessResponse } from "./file-ZGuqNDd-.mjs";
4
+ import { a as mkdirSync } from "./fsSync-Cf5MWILk.mjs";
5
+ import { c as downloadFile, o as createPluginDir } from "./file-dGy9of8-.mjs";
6
+ import { S as cache, d as WS_CLOSE_ONEBOT, f as WS_CLOSE_PUPPETEER, g as WS_CONNECTION_PUPPETEER, h as WS_CONNECTION_ONEBOT, n as errorHandler, r as listeners, t as statusListener, w as formatPath } from "./internal-DupfycKE.mjs";
7
+ import { f as exec$3 } from "./uptime-C121X_rq.mjs";
8
+ import { $ as TASK_LOGS_ROUTER, A as GET_TERMINAL_LIST_ROUTER, B as PLUGIN_ADMIN_ROUTER, C as GET_PLUGIN_CONFIG_ROUTER, D as GET_PLUGIN_MARKET_LIST_ROUTER, F as INSTALL_WEBUI_PLUGIN_ROUTER, G as SAVE_PLUGIN_CONFIG_ROUTER, H as RESTART_ROUTER, I as IS_PLUGIN_CONFIG_EXIST_ROUTER, J as SYSTEM_STATUS_KARIN_ROUTER, K as SET_LOG_LEVEL_ROUTER, L as LOGIN_ROUTER, M as GET_WEBUI_PLUGIN_LIST_ROUTER, N as GET_WEBUI_PLUGIN_VERSIONS_ROUTER, O as GET_TASK_LIST_ROUTER, P as INSTALL_PLUGIN_ROUTER, Q as TASK_LIST_ROUTER, R as MANAGE_DEPENDENCIES_ROUTER, T as GET_PLUGIN_LIST_PLUGIN_ADMIN_ROUTER, U as SAVE_CONFIG_ROUTER, V as REFRESH_ROUTER, W as SAVE_NPMRC_ROUTER, X as SYSTEM_STATUS_WS_ROUTER, Y as SYSTEM_STATUS_ROUTER, Z as TASK_DELETE_ROUTER, _ as GET_NETWORK_STATUS_ROUTER, a as CONSOLE_ROUTER, at as UPDATE_TASK_STATUS_ROUTER, b as GET_NPM_CONFIG_ROUTER, c as GET_BOTS_ROUTER, d as GET_LOADED_COMMAND_PLUGIN_CACHE_LIST_ROUTER, et as TASK_RUN_ROUTER, f as GET_LOCAL_PLUGIN_FRONTEND_LIST_ROUTER, g as GET_LOG_ROUTER, h as GET_LOG_FILE_ROUTER, i as CLOSE_TERMINAL_ROUTER, k as GET_TASK_STATUS_ROUTER, l as GET_CONFIG_ROUTER, m as GET_LOG_FILE_LIST_ROUTER, nt as UNINSTALL_WEBUI_PLUGIN_ROUTER, o as CREATE_TERMINAL_ROUTER, ot as UPDATE_WEBUI_PLUGIN_VERSION_ROUTER, p as GET_LOCAL_PLUGIN_LIST_ROUTER, r as CHECK_PLUGIN_ROUTER, rt as UPDATE_CORE_ROUTER, s as EXIT_ROUTER, t as BASE_ROUTER, tt as UNINSTALL_PLUGIN_ROUTER, u as GET_DEPENDENCIES_LIST_ROUTER, v as GET_NPMRC_LIST_ROUTER, y as GET_NPM_BASE_CONFIG_ROUTER, z as PING_ROUTER } from "./router-zPSN9-tY.mjs";
9
+ import { c as ini, f as getFastRegistry, h as raceRequest, i as initialize, n as createTerminal, o as auth, p as getPackageJson, r as getTerminalList, t as closeTerminal, u as getFastGithub } from "./terminalManager-Lxa8Sm06.mjs";
10
+ import { n as redis$1 } from "./redis-aLJ7wbJH.mjs";
11
+ import "./kv-DZp4UIxg.mjs";
12
+ import path, { join } from "node:path";
13
+ import util from "node:util";
14
+ import path$1 from "path";
15
+ import fs, { existsSync, promises } from "node:fs";
16
+ import { exec, spawn } from "node:child_process";
17
+ import { URL, pathToFileURL } from "node:url";
18
+ import axios, { AxiosError } from "axios";
19
+ import lodash from "lodash";
20
+ import { homedir } from "node:os";
21
+ import { spawn as spawn$1 } from "child_process";
22
+ import schedule from "node-schedule";
23
+ import fs$1 from "fs";
24
+ import moment from "moment";
25
+ import express, { Router } from "express";
26
+ import { createServer } from "node:http";
27
+
28
+ //#region src/env/key/redis.ts
29
+ /** redis: 插件市场缓存键名 */
30
+ const REDIS_PLUGIN_LIST_CACHE_KEY = "karin:market:plugin:list";
31
+ /** redis: 依赖列表缓存键名 */
32
+ const REDIS_DEPENDENCIES_LIST_CACHE_KEY = "karin:dependencies:list";
33
+ /** redis: 插件市场列表缓存键名 */
34
+ const REDIS_PLUGIN_MARKET_LIST_CACHE_KEY = "karin:market:plugin:list:v2";
35
+ /** redis: 本地插件列表缓存键名 */
36
+ const REDIS_LOCAL_PLUGIN_LIST_CACHE_KEY = "karin:local:plugin:list";
37
+ /** redis: 本地插件列表缓存键名 用于前端显示插件简约列表 */
38
+ const REDIS_LOCAL_PLUGIN_LIST_CACHE_KEY_FRONTEND = "karin:local:plugin:list:frontend";
39
+ /** redis: 依赖列表缓存过期时间 24小时 */
40
+ const REDIS_DEPENDENCIES_LIST_CACHE_EXPIRE = 1440 * 60;
41
+ /** redis: 插件市场列表缓存过期时间 24小时 */
42
+ const REDIS_PLUGIN_MARKET_LIST_CACHE_EXPIRE = 1440 * 60;
43
+ /** redis: 本地插件列表缓存过期时间 24小时 */
44
+ const REDIS_PLUGIN_LIST_CACHE_EXPIRE = 1440 * 60;
45
+ /** redis: 本地插件列表缓存过期时间 24小时 用于前端显示插件简约列表 */
46
+ const REDIS_LOCAL_PLUGIN_LIST_CACHE_EXPIRE_FRONTEND = 1440 * 60;
47
+
48
+ //#endregion
49
+ //#region src/env/key/event.ts
50
+ /** Bot上线事件 也就是初始化完成了... */
51
+ const ONLINE = "online";
52
+
53
+ //#endregion
54
+ //#region src/utils/git/exec.ts
55
+ /**
56
+ * 执行命令
57
+ * @param cmd 命令
58
+ * @param options 选项
59
+ * @returns 执行结果
60
+ */
61
+ const exec$2 = (cmd, options) => {
62
+ return new Promise((resolve) => {
63
+ const timeout = (options?.timeout || 30) * 1e3;
64
+ const timer = setTimeout(() => {
65
+ resolve({
66
+ status: false,
67
+ error: /* @__PURE__ */ new Error("命令执行超时"),
68
+ stdout: "",
69
+ stderr: "命令执行超时"
70
+ });
71
+ }, timeout);
72
+ exec(cmd, options, (error, stdout, stderr) => {
73
+ clearTimeout(timer);
74
+ stderr = stderr.toString().trim();
75
+ stdout = stdout.toString().trim();
76
+ resolve({
77
+ status: !error,
78
+ error,
79
+ stdout,
80
+ stderr
81
+ });
82
+ }).stdin?.write("\n");
83
+ });
84
+ };
85
+ /**
86
+ * 返回值处理
87
+ * @param data 执行结果
88
+ * @param callback 回调函数
89
+ * @returns 执行结果
90
+ */
91
+ const handleReturn$1 = (data, callback) => {
92
+ if (data.error || typeof data.stdout !== "string") throw data.error || /* @__PURE__ */ new Error("stdout 类型错误");
93
+ if (callback) return callback();
94
+ return data.stdout;
95
+ };
96
+
97
+ //#endregion
98
+ //#region src/utils/git/branch.ts
99
+ /**
100
+ * 2025年4月8日22:35:19
101
+ * 此文件较为特殊,需要同时在core内部和cli使用,因此需要保持解耦。
102
+ */
103
+ /**
104
+ * 获取本地分支列表
105
+ * @param cwd 仓库路径 默认当前目录
106
+ * @returns 本地分支列表 包含默认分支和分支列表
107
+ * @throws 执行发生错误 例如stdout类型错误
108
+ */
109
+ const getLocalBranches = async (cwd = process.cwd()) => {
110
+ const data = await exec$2("git --no-pager branch", { cwd });
111
+ return handleReturn$1(data, () => {
112
+ /** 默认分支 */
113
+ let defaultBranch = "";
114
+ const list = data.stdout.split("\n").map((line) => {
115
+ line = line.trim();
116
+ if (line.startsWith("*")) {
117
+ line = line.replace("*", "").trim();
118
+ defaultBranch = line;
119
+ }
120
+ return line;
121
+ });
122
+ return {
123
+ defaultBranch,
124
+ list
125
+ };
126
+ });
127
+ };
128
+ /**
129
+ * 获取本地默认分支
130
+ * @param cwd 仓库路径 默认当前目录
131
+ * @returns 默认分支
132
+ */
133
+ const getDefaultBranch = async (cwd = process.cwd()) => {
134
+ const { defaultBranch } = await getLocalBranches(cwd);
135
+ return defaultBranch;
136
+ };
137
+ /**
138
+ * 获取git仓库远程分支列表
139
+ * @param cwd 仓库路径
140
+ * @returns 远程分支列表 包含哈希和分支名称
141
+ * @throws 执行发生错误 例如stdout类型错误
142
+ */
143
+ const getRemoteBranches = async (cwd) => {
144
+ const data = await exec$2("git ls-remote --heads origin", { cwd });
145
+ return handleReturn$1(data, () => {
146
+ return data.stdout.split("\n").map((line) => {
147
+ const [hash, ref] = line.trim().split(/\s+/).map((v) => v.trim());
148
+ return {
149
+ branch: ref.replace("refs/heads/", ""),
150
+ short: hash.slice(0, 7),
151
+ hash
152
+ };
153
+ });
154
+ });
155
+ };
156
+ /**
157
+ * 获取本地最新提交哈希
158
+ * @param cwd 仓库路径
159
+ * @param options.branch 分支名称 默认使用当前分支
160
+ * @param options.short 是否返回短哈希 默认返回长哈希
161
+ * @returns 最新提交哈希
162
+ */
163
+ const getLocalCommitHash = async (cwd, options) => {
164
+ const branch = options?.branch || "HEAD";
165
+ return handleReturn$1(await exec$2(`git rev-parse ${options?.short ? "--short" : ""} ${branch}`, { cwd }));
166
+ };
167
+ /**
168
+ * 获取远程最新提交哈希
169
+ * @description 分支名称支持不带origin/前缀 会自动添加
170
+ * @param cwd 仓库路径
171
+ * @param options.branch 分支名称 默认值`origin/HEAD`
172
+ * @param options.short 是否返回短哈希 默认返回长哈希
173
+ * @returns 远程最新提交哈希
174
+ */
175
+ const getRemoteCommitHash = async (cwd, options) => {
176
+ let branch = options?.branch || "origin/HEAD";
177
+ if (!branch.startsWith("origin/")) branch = `origin/${branch}`;
178
+ return handleReturn$1(await exec$2(`git rev-parse ${options?.short ? "--short" : ""} ${branch}`, { cwd }));
179
+ };
180
+
181
+ //#endregion
182
+ //#region src/utils/git/pull.ts
183
+ /**
184
+ * 构建pull返回值
185
+ * @param status 是否成功
186
+ * @param currentHash 当前哈希
187
+ * @param remoteHash 远程哈希
188
+ * @param data 更新信息
189
+ * @param prefix data前缀
190
+ * @returns 返回值
191
+ */
192
+ const pullResult = (status, currentHash, remoteHash, data, prefix = "") => {
193
+ return {
194
+ status,
195
+ data: typeof data === "string" ? data : `${prefix}${data.message || data.stack || "未知错误"}`,
196
+ hash: {
197
+ before: currentHash,
198
+ after: remoteHash
199
+ }
200
+ };
201
+ };
202
+ /**
203
+ * 拉取git仓库 俗称`更新git插件`
204
+ * @param cwd 工作目录 默认当前目录
205
+ * @param options 选项
206
+ * @returns 拉取结果
207
+ */
208
+ const gitPull = async (cwd, options = {}) => {
209
+ try {
210
+ /** 先记录当前哈希 */
211
+ const currentHash = await getLocalCommitHash(cwd);
212
+ /** 获取远程哈希 */
213
+ const remoteHash = await getRemoteCommitHash(cwd);
214
+ if (currentHash === remoteHash) return pullResult(false, currentHash, remoteHash, "当前已经是最新版本");
215
+ /** 如果是强制拉取 则直接丢弃全部修改 将分支强制与远程分支同步 */
216
+ if (options.force) {
217
+ const remote = options.remote || "origin/HEAD";
218
+ const fetchResult = await exec$2("git fetch origin", {
219
+ cwd,
220
+ ...options
221
+ });
222
+ if (fetchResult.error) return pullResult(false, currentHash, remoteHash, fetchResult.error, "同步远程分支失败: ");
223
+ const resetResult = await exec$2(`git reset --hard ${remote}`, {
224
+ cwd,
225
+ ...options
226
+ });
227
+ if (resetResult.error) return pullResult(false, currentHash, remoteHash, resetResult.error, "强制同步远程分支失败: ");
228
+ return pullResult(true, currentHash, await getLocalCommitHash(cwd), "本地分支已强制与远程分支同步");
229
+ }
230
+ const { error } = await exec$2(options.customCmd || "git pull", {
231
+ ...options,
232
+ cwd
233
+ });
234
+ if (error) return pullResult(false, currentHash, remoteHash, error, "更新失败: ");
235
+ return pullResult(true, currentHash, await getLocalCommitHash(cwd), "更新成功");
236
+ } catch (error) {
237
+ return pullResult(false, "", "", error.message, "发生错误: ");
238
+ }
239
+ };
240
+
241
+ //#endregion
242
+ //#region src/plugin/tools.ts
243
+ /**
244
+ * 创建日志方法
245
+ * @param enable 是否启用
246
+ * @param isBot 是否为bot
247
+ */
248
+ const createLogger = (enable, isBot) => {
249
+ if (isBot) return enable === false ? (id, log) => logger.bot("debug", id, log) : (id, log) => logger.bot("mark", id, log);
250
+ return enable === false ? (log) => logger.debug(log) : (log) => logger.mark(log);
251
+ };
252
+ /**
253
+ * 创建插件文件对象
254
+ * @param type - 文件类型
255
+ */
256
+ const createFile$1 = (type, name) => {
257
+ return {
258
+ absPath: "",
259
+ basename: "",
260
+ dirname: "",
261
+ method: "",
262
+ type,
263
+ name
264
+ };
265
+ };
266
+ /**
267
+ * 创建插件pkg对象
268
+ */
269
+ const createPkg = () => {
270
+ return {
271
+ name: "",
272
+ apps: [],
273
+ dir: "",
274
+ id: -1,
275
+ pkgData: {},
276
+ pkgPath: "",
277
+ type: "app",
278
+ allApps: []
279
+ };
280
+ };
281
+
282
+ //#endregion
283
+ //#region src/plugin/admin/load.ts
284
+ /** 插件ID */
285
+ let seq = 0;
286
+ /**
287
+ * 加载插件包
288
+ * @param pkg 插件包
289
+ * @param allPromises 所有Promise
290
+ *
291
+ * @description 此处所有的加载都是异步的 所以需要传入Promise数组
292
+ */
293
+ const pkgLoads = async (pkg, allPromises) => {
294
+ /** 验证版本兼容性 */
295
+ const reporter = createPluginMismatchReporter();
296
+ let isCompatible = true;
297
+ let shouldLoad = true;
298
+ if (pkg.type !== "app") {
299
+ /** 版本范围优先级:karin.engines > engines.karin > engines['node-karin'] */
300
+ const ignoreEngines = pkg.pkgData?.karin?.ignoreEngines === true;
301
+ const preferred = typeof pkg.pkgData?.karin?.engines === "string" ? String(pkg.pkgData.karin.engines).trim() : "";
302
+ let fallback = "";
303
+ if (!preferred) {
304
+ if (typeof pkg.pkgData?.engines?.karin === "string") fallback = String(pkg.pkgData.engines.karin).trim();
305
+ else if (typeof pkg.pkgData?.engines?.["node-karin"] === "string") fallback = String(pkg.pkgData.engines["node-karin"]).trim();
306
+ }
307
+ const range = preferred || fallback;
308
+ isCompatible = !range || satisfies(range, process.env.KARIN_VERSION);
309
+ if (range && !isCompatible) {
310
+ if (!ignoreEngines) {
311
+ /** 未忽略:打印日志并禁止加载 */
312
+ reporter.add(pkg.name, range);
313
+ await reporter.flush(true, process.env.KARIN_VERSION);
314
+ shouldLoad = false;
315
+ }
316
+ }
317
+ }
318
+ /** 如果不应该加载,直接返回 */
319
+ if (!shouldLoad) return;
320
+ pkg.id = ++seq;
321
+ cache.index[pkg.id] = pkg;
322
+ const files = [];
323
+ if (pkg.type === "app") files.push("config", "data", "resources");
324
+ else if (pkg.pkgData.karin?.files && Array.isArray(pkg.pkgData.karin?.files)) files.push(...pkg.pkgData.karin.files);
325
+ /** 创建插件基本文件夹 - 这个需要立即执行 */
326
+ await createPluginDir(pkg.name, files);
327
+ /** 收集入口文件加载的Promise:仅非app类型插件执行入口 */
328
+ if (pkg.type !== "app" && shouldLoad) {
329
+ const main = pkg.type === "npm" || !isTs() ? await loadMainFile(pkg, pkg.pkgData?.main) : await loadMainFile(pkg, pkg.pkgData?.karin?.main);
330
+ if (main && main.KARIN_PLUGIN_INIT) try {
331
+ await main.KARIN_PLUGIN_INIT();
332
+ logger.debug(`[load][${pkg.name}] 插件执行KARIN_PLUGIN_INIT函数成功`);
333
+ } catch (error) {
334
+ logger.error(new Error(`[load][${pkg.name}] 插件执行KARIN_PLUGIN_INIT函数失败`, { cause: error }));
335
+ }
336
+ }
337
+ /** 收集所有app加载的Promise */
338
+ shouldLoad && pkg.apps.forEach((app) => {
339
+ const promise = async () => {
340
+ pkgCache(await pkgLoadModule(pkg.name, app), pkg, app);
341
+ };
342
+ allPromises.push(promise());
343
+ });
344
+ /** 静态资源目录处理 */
345
+ if (pkg.type !== "app" && pkg?.pkgData?.karin?.static) {
346
+ const list = Array.isArray(pkg.pkgData.karin.static) ? pkg.pkgData.karin.static : [pkg.pkgData.karin.static];
347
+ cache.static.push(...list.map((file) => path.resolve(pkg.dir, file)));
348
+ } else {
349
+ /** 如果没有配置 默认使用 resource、resources 目录 */
350
+ cache.static.push(path.resolve(pkg.dir, "resource"));
351
+ cache.static.push(path.resolve(pkg.dir, "resources"));
352
+ }
353
+ };
354
+ /**
355
+ * 加载入口文件
356
+ * @param pkg 插件包
357
+ * @param dir 入口文件路径
358
+ */
359
+ const loadMainFile = async (pkg, dir) => {
360
+ if (!dir) return;
361
+ const file = path.join(pkg.dir, dir);
362
+ if (fs.existsSync(file)) return pkgLoadModule(pkg.name, file);
363
+ return null;
364
+ };
365
+ /**
366
+ * 加载模块 ts、js
367
+ * @param name 插件名称
368
+ * @param file 文件路径
369
+ * @param isRefresh 是否刷新
370
+ */
371
+ const pkgLoadModule = async (name, file, isRefresh = false) => {
372
+ const { status, data } = await importModule(file, isRefresh);
373
+ if (status) return data;
374
+ logger.debug(new Error(`加载模块失败: ${name} ${file}`, { cause: data }));
375
+ errorHandler.loaderPlugin(name, file, data);
376
+ return {};
377
+ };
378
+ /**
379
+ * 判断是否为指定类型
380
+ * @param val 插件方法
381
+ * @param type 插件类型
382
+ */
383
+ const isType = (val, type) => {
384
+ return val.file?.type === type;
385
+ };
386
+ /**
387
+ * 缓存插件
388
+ * @param result 插件导入结果
389
+ * @param info 插件信息
390
+ */
391
+ const pkgCache = (result, pkg, app) => {
392
+ const cacheHandler = (val, key) => {
393
+ if (typeof val !== "object") return;
394
+ if (!val?.pkg || !val.file) return;
395
+ val.pkg = pkg;
396
+ val.file = createFile(app, val.file.type, key, val.file.name);
397
+ if (isType(val, "accept")) {
398
+ cache.count.accept++;
399
+ cache.accept.push(val);
400
+ return;
401
+ }
402
+ if (isType(val, "command")) {
403
+ cache.count.command++;
404
+ cache.command.push(val);
405
+ return;
406
+ }
407
+ if (isType(val, "button")) {
408
+ cache.count.button++;
409
+ cache.button.push(val);
410
+ return;
411
+ }
412
+ if (isType(val, "handler")) {
413
+ if (!cache.handler[val.key]) {
414
+ cache.count.handler.key++;
415
+ cache.handler[val.key] = [];
416
+ }
417
+ cache.count.handler.fnc++;
418
+ cache.handler[val.key].push(val);
419
+ return;
420
+ }
421
+ if (isType(val, "task")) {
422
+ val.schedule = schedule.scheduleJob(val.cron, async () => {
423
+ try {
424
+ if (val.type === "skip" && val.running) {
425
+ val.log(`[定时任务][${val.name}][${val.cron}]: 上一次任务未完成,跳过本次执行`);
426
+ return;
427
+ }
428
+ val.running = true;
429
+ val.log(`[定时任务][${val.name}][${val.cron}]: 开始执行`);
430
+ const result = val.fnc();
431
+ if (util.types.isPromise(result)) await result;
432
+ val.log(`[定时任务][${val.name}][${val.cron}]: 执行完成`);
433
+ } catch (error) {
434
+ errorHandler.taskStart(val.name, val.name, error);
435
+ } finally {
436
+ val.running = false;
437
+ }
438
+ });
439
+ cache.count.task++;
440
+ cache.task.push(val);
441
+ }
442
+ };
443
+ for (const key of Object.keys(result)) {
444
+ if (key === "default") continue;
445
+ if (typeof result[key] === "function") {
446
+ if (!isClass(result[key])) continue;
447
+ cacheClassPlugin(result[key], pkg, app, key);
448
+ continue;
449
+ }
450
+ const data = result[key];
451
+ /** 支持导出数组 */
452
+ for (const val of Array.isArray(data) ? data : [data]) cacheHandler(val, key);
453
+ }
454
+ };
455
+ /**
456
+ * 处理导入的模块
457
+ * @param app app文件绝对路径
458
+ * @param type 插件类型
459
+ * @param method 插件方法名称
460
+ */
461
+ const createFile = (app, type, method, name) => {
462
+ return {
463
+ absPath: app,
464
+ get dirname() {
465
+ return path.dirname(this.absPath);
466
+ },
467
+ get basename() {
468
+ return path.basename(this.absPath);
469
+ },
470
+ type,
471
+ method,
472
+ name: name || type
473
+ };
474
+ };
475
+ /**
476
+ * 缓存类命令插件
477
+ * @param result 插件导入结果
478
+ * @param info 插件信息
479
+ * @param pkg 插件包信息
480
+ * @param app app文件绝对路径
481
+ * @param key 插件方法名称
482
+ */
483
+ const cacheClassPlugin = (Method, pkg, app, _) => {
484
+ const command = new Method();
485
+ if (!command.name) {
486
+ logger.error(`[load][${app}] plugin.name 不能为空`);
487
+ return;
488
+ }
489
+ if (!command.rule || !Array.isArray(command.rule) || command.rule?.length === 0) {
490
+ logger.error(`[load][${app}] ${command.name} plugin.rule 不能为空`);
491
+ return;
492
+ }
493
+ command.rule.forEach((v) => {
494
+ /** 没有对应方法跳过 */
495
+ if (!(v.fnc in command)) return;
496
+ /** 没有正则跳过 */
497
+ if (typeof v.reg !== "string" && !(v.reg instanceof RegExp)) return;
498
+ cache.command.push({
499
+ pkg,
500
+ type: "class",
501
+ log: createLogger(v.log, true),
502
+ adapter: v.adapter || [],
503
+ dsbAdapter: v.dsbAdapter || [],
504
+ Cls: Method,
505
+ reg: v.reg instanceof RegExp ? v.reg : new RegExp(v.reg),
506
+ permission: v.permission || "all",
507
+ event: v.event || command.event || "message",
508
+ priority: v.priority || 1e4,
509
+ file: createFile(app, "command", v.fnc, command.name),
510
+ authFailMsg: v.authFailMsg || true
511
+ });
512
+ });
513
+ };
514
+ /**
515
+ * 根据文件路径查找对应的插件包
516
+ */
517
+ const findPkgByFile = (file) => {
518
+ file = formatPath(file);
519
+ return Object.values(cache.index).find((pkg) => pkg.apps.includes(file) || pkg.allApps.some((dir) => file.startsWith(dir)) || pkg.type === "app" && path.normalize(file).startsWith(path.normalize(pkg.dir))) || null;
520
+ };
521
+ /**
522
+ * 排序插件
523
+ * @description 按照插件的优先度从小到大进行排序插件
524
+ */
525
+ const pkgSort = () => {
526
+ cache.accept = lodash.sortBy(cache.accept, ["rank"], ["asc"]);
527
+ cache.command = lodash.sortBy(cache.command, ["rank"], ["asc"]);
528
+ cache.task = lodash.sortBy(cache.task, ["rank"], ["asc"]);
529
+ cache.button = lodash.sortBy(cache.button, ["rank"], ["asc"]);
530
+ for (const key of Object.keys(cache.handler)) cache.handler[key] = lodash.sortBy(cache.handler[key], ["rank"], ["asc"]);
531
+ };
532
+ /**
533
+ * 热加载一个插件包
534
+ * @version 1.8.0
535
+ * @param type 插件类型
536
+ * @param name 插件名称
537
+ */
538
+ const pkgHotReload = async (type, name) => {
539
+ /** 收集所有插件加载的Promise */
540
+ const allPromises = [];
541
+ const pkg = await getPluginsInfo([`${type}:${name}`], true, true);
542
+ if (pkg.length === 0) throw new Error(`[load][${type}:${name}] 插件不存在`);
543
+ await pkgLoads(pkg[0], allPromises);
544
+ await Promise.allSettled(allPromises);
545
+ /** 回收缓存 */
546
+ allPromises.length = 0;
547
+ /** 排序 */
548
+ pkgSort();
549
+ };
550
+
551
+ //#endregion
552
+ //#region src/server/log/index.ts
553
+ /**
554
+ * 日志中间件
555
+ */
556
+ const logMiddleware = async (req, _, next) => {
557
+ logger.debug(`[express] 收到请求:
558
+ method: ${req.method}\nip: ${req.ip}\npath: ${req.path}\nheaders: ${JSON.stringify(req.headers)}\nbody: ${JSON.stringify(req.body)}\n`);
559
+ next();
560
+ };
561
+
562
+ //#endregion
563
+ //#region src/server/auth/middleware.ts
564
+ /**
565
+ * 鉴权中间件
566
+ * @param req 请求
567
+ * @param res 响应
568
+ * @param next 下一个中间件
569
+ */
570
+ const authMiddleware = async (req, res, next) => {
571
+ /** 白名单 */
572
+ if (req.path === "/ping" || req.path === "/login" || req.path === "/refresh" || req.path.startsWith("/console")) {
573
+ next();
574
+ return;
575
+ }
576
+ if (req.method === "POST") {
577
+ if (!await auth.postAuth(req, res)) return;
578
+ } else if (req.method === "GET") {
579
+ if (!await auth.getAuth(req, res)) return;
580
+ } else {
581
+ createMethodNotAllowedResponse(res);
582
+ return;
583
+ }
584
+ next();
585
+ };
586
+
587
+ //#endregion
588
+ //#region src/server/auth/login.ts
589
+ /** 登录限制配置 */
590
+ const IP_LIMIT_CONFIG = {
591
+ /** 最大尝试次数 */
592
+ maxAttempts: 5,
593
+ /** 时间窗口 */
594
+ timeWindow: 300 * 1e3,
595
+ /** 封禁时长 */
596
+ blockDuration: 1800 * 1e3
597
+ };
598
+ /** IP记录 */
599
+ const ipRecords = /* @__PURE__ */ new Map();
600
+ /**
601
+ * 清理过期的IP记录
602
+ */
603
+ const cleanupIPRecords = () => {
604
+ const now = Date.now();
605
+ for (const [ip, record] of ipRecords.entries()) if (record.blockedUntil && record.blockedUntil < now || now - record.firstAttempt > IP_LIMIT_CONFIG.timeWindow) ipRecords.delete(ip);
606
+ };
607
+ /** 每10分钟清理一次过期记录 */
608
+ setInterval(cleanupIPRecords, 600 * 1e3);
609
+ /**
610
+ * 检查IP是否被封禁
611
+ * @param clientIP 客户端IP
612
+ * @param res 响应
613
+ * @returns 是否被封禁
614
+ */
615
+ const checkIPBlocked = (clientIP, res) => {
616
+ const now = Date.now();
617
+ const ipRecord = ipRecords.get(clientIP) || {
618
+ attempts: 0,
619
+ firstAttempt: now
620
+ };
621
+ if (ipRecord.blockedUntil && ipRecord.blockedUntil > now) {
622
+ /** 在封禁期间继续尝试,延长封禁时间 */
623
+ const additionalAttempts = ipRecord.attempts + 1 - IP_LIMIT_CONFIG.maxAttempts;
624
+ ipRecord.blockedUntil = now + Math.min(IP_LIMIT_CONFIG.blockDuration * additionalAttempts, 1440 * 60 * 1e3);
625
+ ipRecord.attempts++;
626
+ ipRecords.set(clientIP, ipRecord);
627
+ const remainingTime = Math.ceil((ipRecord.blockedUntil - now) / 1e3 / 60);
628
+ createForbiddenResponse(res, `登录尝试次数过多,请在${remainingTime}分钟后重试`);
629
+ logger.warn(`${logger.red("login")}: ${clientIP} 继续尝试登录, 当前尝试次数:${ipRecord.attempts}, 封禁时间延长至${remainingTime}分钟`);
630
+ return {
631
+ isBlocked: true,
632
+ ipRecord
633
+ };
634
+ }
635
+ /** 检查是否需要重置计数器 */
636
+ if (now - ipRecord.firstAttempt > IP_LIMIT_CONFIG.timeWindow) {
637
+ ipRecord.attempts = 0;
638
+ ipRecord.firstAttempt = now;
639
+ }
640
+ return {
641
+ isBlocked: false,
642
+ ipRecord
643
+ };
644
+ };
645
+ /**
646
+ * 处理登录失败
647
+ * @param clientIP 客户端IP
648
+ * @param ipRecord IP记录
649
+ * @param res 响应
650
+ * @returns 处理结果
651
+ */
652
+ const handleLoginFailure = (clientIP, ipRecord, res) => {
653
+ const now = Date.now();
654
+ ipRecord.attempts++;
655
+ /** 检查是否超过最大尝试次数 */
656
+ if (ipRecord.attempts >= IP_LIMIT_CONFIG.maxAttempts) {
657
+ ipRecord.blockedUntil = now + IP_LIMIT_CONFIG.blockDuration;
658
+ ipRecords.set(clientIP, ipRecord);
659
+ const tips = `登录尝试次数过多,请在${Math.ceil(IP_LIMIT_CONFIG.blockDuration / 1e3 / 60)}分钟后重试`;
660
+ logger.warn(`${logger.red("login")}: ${clientIP} ${tips}`);
661
+ return createForbiddenResponse(res, tips);
662
+ }
663
+ ipRecords.set(clientIP, ipRecord);
664
+ logger.warn(`${logger.red("login")}: ${clientIP} 密码错误`);
665
+ return createBadRequestResponse(res, "密码错误");
666
+ };
667
+ /**
668
+ * 处理登录成功
669
+ * @param clientIP 客户端IP
670
+ * @param res 响应
671
+ * @returns 处理结果
672
+ */
673
+ const handleLoginSuccess = (clientIP, res) => {
674
+ /** 登录成功,重置该IP的记录 */
675
+ ipRecords.delete(clientIP);
676
+ const { userId, accessToken, refreshToken } = createJwt();
677
+ return createSuccessResponse(res, {
678
+ userId,
679
+ accessToken,
680
+ refreshToken
681
+ }, "登录成功");
682
+ };
683
+ /**
684
+ * 验证登录凭证
685
+ * @param authorization 授权
686
+ * @returns 是否有效
687
+ */
688
+ const validateCredentials = (authorization) => {
689
+ const token = authorization?.replace("Bearer ", "");
690
+ return token && token === getSecretOrPrivateKey();
691
+ };
692
+ /**
693
+ * 登录路由处理
694
+ */
695
+ const loginRouter = async (req, res) => {
696
+ try {
697
+ const clientIP = req.ip || req.socket.remoteAddress || "unknown";
698
+ logger.info(`${logger.green("login")}: ${clientIP} ${JSON.stringify(req.body)}`);
699
+ /** 检查IP限制 */
700
+ const { isBlocked, ipRecord } = checkIPBlocked(clientIP, res);
701
+ if (isBlocked) return;
702
+ /** 验证登录凭证 */
703
+ const { authorization } = req.body || {};
704
+ if (!validateCredentials(authorization)) {
705
+ handleLoginFailure(clientIP, ipRecord, res);
706
+ return;
707
+ }
708
+ handleLoginSuccess(clientIP, res);
709
+ } catch (error) {
710
+ logger.error(error);
711
+ createServerErrorResponse(res, error instanceof Error ? error.message : "服务器错误");
712
+ }
713
+ };
714
+
715
+ //#endregion
716
+ //#region src/server/auth/refresh.ts
717
+ /**
718
+ * 刷新访问令牌
719
+ * @param req 请求
720
+ * @param res 响应
721
+ * @returns 响应
722
+ */
723
+ const refreshRouter = async (req, res) => {
724
+ try {
725
+ const { accessToken, refreshToken } = req.body || {};
726
+ if (!accessToken || !refreshToken) return createBadRequestResponse(res);
727
+ const { status, data } = verifyRefreshToken(refreshToken);
728
+ if (!status) {
729
+ if (data.includes("过期")) return createRefreshTokenExpiredResponse(res);
730
+ return createUnauthorizedResponse(res, data);
731
+ }
732
+ const newAccessToken = refreshAccessToken(data);
733
+ logger.mark(`[refresh] 刷新访问令牌成功: ${accessToken} -> ${newAccessToken}`);
734
+ createSuccessResponse(res, { accessToken: newAccessToken }, "刷新成功");
735
+ } catch (error) {
736
+ logger.error(error);
737
+ createServerErrorResponse(res, error instanceof Error ? error.message : "服务器错误");
738
+ }
739
+ };
740
+
741
+ //#endregion
742
+ //#region src/server/config/index.ts
743
+ /**
744
+ * 获取配置
745
+ * @param req 请求
746
+ * @param res 响应
747
+ * @returns 配置
748
+ */
749
+ const getConfig = async (req, res) => {
750
+ const { type } = req.body;
751
+ if (type === "config") return createSuccessResponse(res, config());
752
+ if (type === "adapter") return createSuccessResponse(res, adapter());
753
+ if (type === "groups") return createSuccessResponse(res, getGroupsFileData(karinPathConfig));
754
+ if (type === "privates") return createSuccessResponse(res, getPrivatesFileData(karinPathConfig));
755
+ if (type === "render") return createSuccessResponse(res, getRenderCfg());
756
+ if (type === "redis") return createSuccessResponse(res, redis());
757
+ if (type === "pm2") return createSuccessResponse(res, pm2());
758
+ if (type === "env") return createSuccessResponse(res, getEnv());
759
+ return createBadRequestResponse(res, "无效的配置类型");
760
+ };
761
+ /**
762
+ * 保存配置
763
+ * @param req 请求
764
+ * @param res 响应
765
+ * @returns 保存配置
766
+ */
767
+ const saveConfig = async (req, res) => {
768
+ const list = [
769
+ "config",
770
+ "adapter",
771
+ "render",
772
+ "pm2",
773
+ "redis",
774
+ "groups",
775
+ "privates",
776
+ "env"
777
+ ];
778
+ const { type, data } = req.body;
779
+ const save = () => {
780
+ if (type === "env") {
781
+ writeEnv(Object.entries(data).map(([key, value]) => ({
782
+ key,
783
+ value: value.value,
784
+ comment: value.comment
785
+ })), void 0, true);
786
+ return true;
787
+ }
788
+ if (list.includes(type)) return setConfig(type, data);
789
+ return false;
790
+ };
791
+ try {
792
+ if (!save()) return createBadRequestResponse(res, "配置保存失败");
793
+ createSuccessResponse(res, "配置保存成功");
794
+ } catch (error) {
795
+ logger.error(error);
796
+ createServerErrorResponse(res, error.message);
797
+ }
798
+ };
799
+
800
+ //#endregion
801
+ //#region src/server/system/ping.ts
802
+ /**
803
+ * ping路由
804
+ */
805
+ const pingRouter = (_req, res) => {
806
+ createSuccessResponse(res, { ping: "pong" }, "成功");
807
+ };
808
+
809
+ //#endregion
810
+ //#region src/server/plugins/config.ts
811
+ /**
812
+ * 检查文件是否存在
813
+ * @param filepath 文件路径
814
+ * @returns 存在返回true,否则返回false
815
+ */
816
+ const fileExists = (filepath) => {
817
+ try {
818
+ return fs.existsSync(filepath);
819
+ } catch (error) {
820
+ return false;
821
+ }
822
+ };
823
+ /**
824
+ * 从package.json中获取web配置路径
825
+ * @param pkg package.json内容
826
+ * @param baseDir 插件根目录
827
+ * @returns web配置路径或null
828
+ */
829
+ const getWebConfigPathFromPkg = (pkg, baseDir) => {
830
+ if (!pkg.karin) return null;
831
+ /** 如果该插件处于node_modules中 视其为正式插件的环境 仅加载web */
832
+ if (baseDir.includes("node_modules")) {
833
+ if (pkg.karin.web) {
834
+ const configPath = path.join(baseDir, pkg.karin.web);
835
+ return fileExists(configPath) ? configPath : null;
836
+ }
837
+ return null;
838
+ }
839
+ let configPath = null;
840
+ if (isTs()) {
841
+ if (pkg.karin["ts-web"]) {
842
+ configPath = path.join(baseDir, pkg.karin["ts-web"]);
843
+ if (fileExists(configPath)) return configPath;
844
+ }
845
+ }
846
+ if (pkg.karin.web) {
847
+ configPath = path.join(baseDir, pkg.karin.web);
848
+ if (fileExists(configPath)) return configPath;
849
+ }
850
+ return null;
851
+ };
852
+ /**
853
+ * 获取NPM插件配置路径
854
+ * @param name 插件名称
855
+ * @returns 配置路径或null
856
+ */
857
+ const getNpmPluginConfigPath = (name) => {
858
+ const dir = path.join(process.cwd(), "node_modules", name);
859
+ if (fileExists(dir)) try {
860
+ const pkgPath = path.join(dir, "package.json");
861
+ if (fileExists(pkgPath)) {
862
+ const configPath = getWebConfigPathFromPkg(requireFileSync(pkgPath), dir);
863
+ if (configPath) return configPath;
864
+ }
865
+ } catch (error) {
866
+ return null;
867
+ }
868
+ /** 开发环境检查根目录 */
869
+ if (isDev()) try {
870
+ const rootPkgPath = path.join(process.cwd(), "package.json");
871
+ if (fileExists(rootPkgPath)) {
872
+ const pkg = requireFileSync(rootPkgPath);
873
+ if (pkg?.name === name) return getWebConfigPathFromPkg(pkg, process.cwd());
874
+ }
875
+ } catch (error) {
876
+ return null;
877
+ }
878
+ return null;
879
+ };
880
+ /**
881
+ * 获取本地插件配置路径
882
+ * @param name 插件名称
883
+ * @returns 配置路径或null
884
+ */
885
+ const getLocalPluginConfigPath = (name) => {
886
+ const pluginDir = path.join(process.cwd(), "plugins", name);
887
+ if (fileExists(pluginDir)) try {
888
+ const pkgPath = path.join(pluginDir, "package.json");
889
+ if (fileExists(pkgPath)) {
890
+ const configPath = getWebConfigPathFromPkg(requireFileSync(pkgPath), pluginDir);
891
+ if (configPath) return configPath;
892
+ }
893
+ } catch (error) {
894
+ return null;
895
+ }
896
+ try {
897
+ const pkgPath = path.join(process.cwd(), "package.json");
898
+ if (fileExists(pkgPath)) return getWebConfigPathFromPkg(requireFileSync(pkgPath), process.cwd());
899
+ } catch (error) {
900
+ return null;
901
+ }
902
+ return null;
903
+ };
904
+ /**
905
+ * 获取插件配置路径
906
+ * @param type 插件类型
907
+ * @param name 插件名称
908
+ * @returns 配置路径
909
+ */
910
+ const getConfigPath = (type, name) => {
911
+ try {
912
+ switch (type) {
913
+ case "npm": return getNpmPluginConfigPath(name);
914
+ case "git": return getLocalPluginConfigPath(name);
915
+ default: return null;
916
+ }
917
+ } catch (error) {
918
+ return null;
919
+ }
920
+ };
921
+ /**
922
+ * 加载插件配置
923
+ * @param configPath 配置路径
924
+ */
925
+ const loadConfig = async (configPath) => {
926
+ try {
927
+ return (await import(`${pathToFileURL(configPath).toString()}${isDev() ? "?t=" + Date.now() : ""}`)).default;
928
+ } catch (error) {
929
+ throw new Error(`加载插件配置失败: ${error instanceof Error ? error.message : String(error)}`);
930
+ }
931
+ };
932
+ /**
933
+ * 传入type id 返回web.config配置
934
+ * @param type 插件类型
935
+ * @param id 插件id
936
+ * @param fnc 配置文件不符合要求时回调
937
+ * @returns web.config配置
938
+ */
939
+ const getWebConfig$1 = async (type, id, _) => {
940
+ /** 只支持git npm */
941
+ if (!["git", "npm"].includes(type)) return null;
942
+ const webConfig = getConfigPath(type, id);
943
+ if (!webConfig) return null;
944
+ if (path.basename(webConfig, path.extname(webConfig)) !== "web.config") return null;
945
+ const result = await loadConfig(webConfig);
946
+ /** 检查一下version description是否存在 不存在则从插件对应的package.json中获取 */
947
+ if (!result.info.version || !result.info.description) {
948
+ let dir = "";
949
+ if (type === "npm") dir = path.join(process.cwd(), "node_modules", id);
950
+ else {
951
+ dir = path.join(process.cwd(), "plugins", id);
952
+ if (!fs.existsSync(dir)) dir = process.cwd();
953
+ }
954
+ const pkg = requireFileSync(path.join(dir, "package.json"));
955
+ if (!pkg || pkg.name !== id) {
956
+ if (!result.info.version) result.info.version = "";
957
+ if (!result.info.description) result.info.description = "";
958
+ } else {
959
+ if (!result.info.version && pkg.version !== void 0) result.info.version = pkg.version;
960
+ else result.info.version = "";
961
+ if (!result.info.description) result.info.description = "";
962
+ }
963
+ }
964
+ return result;
965
+ };
966
+ /**
967
+ * 标准化插件作者字段
968
+ * @param author 插件作者
969
+ * @returns 标准化后的作者
970
+ */
971
+ const normalizeAuthor = (author) => {
972
+ const list = [];
973
+ if (Array.isArray(author)) list.push(...author);
974
+ else if (author) list.push(author);
975
+ return list;
976
+ };
977
+ /**
978
+ * 解析自定义配置页面
979
+ * @param config 插件web配置
980
+ */
981
+ const resolveConfigPage = async (config) => {
982
+ if (!("page" in config) || !config.page) return void 0;
983
+ const page = await (typeof config.page === "function" ? config.page() : config.page);
984
+ if (!page || typeof page.url !== "string" || !page.url.trim()) throw new Error("插件自定义配置页面缺少有效的 url");
985
+ return {
986
+ ...page,
987
+ url: normalizeConfigPageUrl(page.url)
988
+ };
989
+ };
990
+ /**
991
+ * 标准化自定义配置页面地址。
992
+ * @param rawUrl 页面地址
993
+ * @returns 标准化后的页面地址
994
+ */
995
+ const normalizeConfigPageUrl = (rawUrl) => {
996
+ const url = rawUrl.trim();
997
+ if (url.startsWith("/") && !url.startsWith("//")) return url;
998
+ try {
999
+ const parsed = new URL(url);
1000
+ if (parsed.protocol === "http:" || parsed.protocol === "https:") return parsed.toString();
1001
+ } catch {}
1002
+ throw new Error("插件自定义配置页面 url 仅支持以 / 开头的同源路径或 http(s) 外部地址");
1003
+ };
1004
+ /**
1005
+ * 获取插件配置 不存在则返回null
1006
+ * @param req 请求
1007
+ * @param res 响应
1008
+ */
1009
+ const pluginGetConfig = async (req, res) => {
1010
+ const options = req.body;
1011
+ if (!options.name) {
1012
+ createServerErrorResponse(res, "参数错误");
1013
+ return;
1014
+ }
1015
+ const type = await getPluginType(options.name);
1016
+ if (!type) return createServerErrorResponse(res, "参数错误");
1017
+ const config = await getWebConfig$1(type, options.name, () => {
1018
+ logger.error(`[plugin] 插件${options.name}的web配置文件名称不正确: 需要以 web.config 命名`);
1019
+ createSuccessResponse(res, null);
1020
+ });
1021
+ if (!config) return createServerErrorResponse(res, "参数错误");
1022
+ let page;
1023
+ try {
1024
+ page = await resolveConfigPage(config);
1025
+ } catch (error) {
1026
+ return createServerErrorResponse(res, error.message);
1027
+ }
1028
+ const list = [];
1029
+ if (typeof config.components === "function") {
1030
+ let result = config.components();
1031
+ result = util.types.isPromise(result) ? await result : result;
1032
+ result.forEach((item) => {
1033
+ if (typeof item?.toJSON === "function") list.push(item.toJSON());
1034
+ else if (typeof item === "object" && item !== null) list.push(item);
1035
+ });
1036
+ } else if (!page) return createServerErrorResponse(res, "该插件未提供默认组件配置函数或自定义配置页面");
1037
+ createSuccessResponse(res, {
1038
+ options: list,
1039
+ page,
1040
+ info: {
1041
+ ...config.info,
1042
+ author: normalizeAuthor(config.info.author)
1043
+ }
1044
+ });
1045
+ };
1046
+ /**
1047
+
1048
+ * 保存插件配置
1049
+ */
1050
+ const pluginSaveConfig = async (req, res) => {
1051
+ const { name, config } = req.body;
1052
+ const type = await getPluginType(name);
1053
+ if (!type) return createServerErrorResponse(res, "参数错误");
1054
+ const configPath = getConfigPath(type, name);
1055
+ if (!configPath) return createServerErrorResponse(res, "参数错误");
1056
+ const webConfig = await loadConfig(configPath);
1057
+ if ("page" in webConfig && webConfig.page) return createServerErrorResponse(res, "自定义配置页面不支持 Karin 通用保存接口");
1058
+ const { save } = webConfig;
1059
+ if (typeof save !== "function") return createServerErrorResponse(res, "该插件未提供默认组件保存函数");
1060
+ const result = save(config);
1061
+ createSuccessResponse(res, (util.types.isPromise(result) ? await result : result) || {
1062
+ success: true,
1063
+ message: "没有返回值哦 φ(>ω<*) "
1064
+ });
1065
+ };
1066
+ /**
1067
+ * 判断插件是否存在配置
1068
+ */
1069
+ const pluginIsConfigExist = async (req, res) => {
1070
+ const name = req.body.name;
1071
+ const type = await getPluginType(name);
1072
+ if (!name || !type) return createServerErrorResponse(res, "参数错误");
1073
+ createSuccessResponse(res, typeof getConfigPath(type, name) === "string");
1074
+ };
1075
+ /**
1076
+ * 获取插件的类型
1077
+ * @param name 插件名称
1078
+ */
1079
+ const getPluginType = async (name) => {
1080
+ const list = await getPlugins("all", false);
1081
+ const npmName = `npm:${name}`;
1082
+ const gitName = `git:${name}`;
1083
+ const rootName = `root:${name}`;
1084
+ for (const item of list) {
1085
+ if (item === npmName) return "npm";
1086
+ if (item === gitName) return "git";
1087
+ if (item === rootName) return "git";
1088
+ }
1089
+ return null;
1090
+ };
1091
+
1092
+ //#endregion
1093
+ //#region src/server/plugins/local.ts
1094
+ /**
1095
+ * 获取本地已安装插件列表
1096
+ */
1097
+ const pluginGetLocalList = async (req, res) => {
1098
+ const { isForce } = req.body;
1099
+ const [npm, git] = await Promise.all([getPlugins("npm", true, isForce ?? false), getPlugins("git", true, isForce ?? false)]);
1100
+ const list = [...npm, ...git];
1101
+ const result = [];
1102
+ const promise = [];
1103
+ list.forEach((val) => promise.push(isWebConfigPlugin(val, result)));
1104
+ await Promise.all(promise);
1105
+ createSuccessResponse(res, result);
1106
+ };
1107
+ /**
1108
+ * 判断插件是否存在`web.config`
1109
+ */
1110
+ const isWebConfigPlugin = async (val, result) => {
1111
+ const pkg = val.pkgData;
1112
+ if (val.type === "npm") {
1113
+ if (!pkg.karin?.web) return;
1114
+ return getWebConfigPlugins(pkg, val, result);
1115
+ }
1116
+ if (isTs()) {
1117
+ if (!pkg.karin?.["ts-web"]) return;
1118
+ return getWebConfigPlugins(pkg, val, result);
1119
+ }
1120
+ };
1121
+ /**
1122
+ * 获取拥有web.config的插件配置
1123
+ */
1124
+ const getWebConfigPlugins = async (pkg, val, result) => {
1125
+ let config = null;
1126
+ config = await getWebConfig$1(val.type, val.name);
1127
+ /** 开发环境下兼容获取根目录的 */
1128
+ if (!config && isDev() && val.type === "npm") config = await getWebConfig$1("git", val.name);
1129
+ if (!config || !config.info) return;
1130
+ result.push({
1131
+ ...config.info,
1132
+ id: val.name,
1133
+ version: config.info.version ?? pkg.version,
1134
+ description: config.info.description ?? pkg.description,
1135
+ hasConfig: true,
1136
+ type: val.type,
1137
+ author: normalizeAuthor(config.info.author)
1138
+ });
1139
+ };
1140
+
1141
+ //#endregion
1142
+ //#region src/server/console/index.ts
1143
+ /** 允许的文件类型及其对应的 Content-Type */
1144
+ const ALLOWED_TYPES = {
1145
+ ".png": "image/png",
1146
+ ".jpg": "image/jpeg",
1147
+ ".jpeg": "image/jpeg",
1148
+ ".gif": "image/gif",
1149
+ ".mp3": "audio/mpeg",
1150
+ ".mp4": "video/mp4",
1151
+ ".wav": "audio/wav",
1152
+ ".webp": "image/webp",
1153
+ ".json": "application/json",
1154
+ ".txt": "text/plain",
1155
+ ".html": "text/html",
1156
+ ".css": "text/css"
1157
+ };
1158
+ /** 最大文件大小 (1024MB) */
1159
+ const MAX_FILE_SIZE = 1024 * 1024 * 1024;
1160
+ /**
1161
+ * console适配器路由
1162
+ */
1163
+ const consoleRouter = async (req, res) => {
1164
+ try {
1165
+ const cfg = adapter();
1166
+ let url = decodeURIComponent(req.path).replace(/\/+/g, "/").replace(/^\/+|\/+$/g, "");
1167
+ url = url.split("/").pop() || "";
1168
+ if (!url) return createBadRequestResponse(res, "文件名不能为空");
1169
+ /** 防止路径穿越 */
1170
+ if (url.includes("..") || url.includes("~") || !url.match(/^[a-zA-Z0-9-_.]+$/)) return createForbiddenResponse(res, "非法请求");
1171
+ const ext = path.extname(url).toLowerCase();
1172
+ if (!ALLOWED_TYPES[ext]) return createBadRequestResponse(res, "不支持的文件类型");
1173
+ const isLocal = await isLocalRequest(req);
1174
+ if (cfg.console.isLocal) {
1175
+ if (!isLocal) return createForbiddenResponse(res, "非法请求");
1176
+ } else {
1177
+ if (!cfg.console.token) return createServerErrorResponse(res, "缺少 token 配置");
1178
+ const token = req.query.token;
1179
+ if (!token || token !== cfg.console.token) return createForbiddenResponse(res, "无效的 token");
1180
+ }
1181
+ const file = path.join(consolePath, url);
1182
+ try {
1183
+ /** 组合路径之后 判断一下文件是否处于 consolePath 目录下 */
1184
+ if (!file.startsWith(consolePath)) return createForbiddenResponse(res, "非法请求");
1185
+ if ((await promises.stat(file)).size > MAX_FILE_SIZE) return createPayloadTooLargeResponse(res, "文件过大");
1186
+ } catch {
1187
+ return createNotFoundResponse(res, "文件不存在");
1188
+ }
1189
+ const data = await promises.readFile(file);
1190
+ res.setHeader("Content-Type", ALLOWED_TYPES[ext]);
1191
+ res.setHeader("Content-Length", data.length);
1192
+ res.setHeader("X-Content-Type-Options", "nosniff");
1193
+ res.send(data);
1194
+ } catch (error) {
1195
+ console.error("Console router error:", error);
1196
+ return createServerErrorResponse(res, "服务器错误");
1197
+ }
1198
+ };
1199
+
1200
+ //#endregion
1201
+ //#region src/server/system/botList.ts
1202
+ /**
1203
+ * 获取所有bot列表
1204
+ */
1205
+ const getBotsRouter = async (_, res) => {
1206
+ createSuccessResponse(res, getBotCount());
1207
+ };
1208
+
1209
+ //#endregion
1210
+ //#region src/server/system/update.ts
1211
+ /**
1212
+ * 更新karin
1213
+ */
1214
+ const updateCoreRouter = async (_req, res) => {
1215
+ console.log("收到更新请求");
1216
+ const result = await updatePkg("node-karin");
1217
+ if (result.status === "ok") createSuccessResponse(res, result, "更新成功");
1218
+ else createServerErrorResponse(res, "更新失败,请检查日志");
1219
+ };
1220
+
1221
+ //#endregion
1222
+ //#region src/server/system/network.ts
1223
+ let network = null;
1224
+ /**
1225
+ * 获取网络状态
1226
+ */
1227
+ const networkStatusRouter = async (_req, res) => {
1228
+ try {
1229
+ if (typeof network !== "function") {
1230
+ const { calculateNetworkSpeed } = await import("@karinjs/plugin-webui-network-monitor");
1231
+ network = calculateNetworkSpeed;
1232
+ }
1233
+ createSuccessResponse(res, await network());
1234
+ } catch (error) {
1235
+ logger.debug(error);
1236
+ createServerErrorResponse(res, "@karinjs/plugin-webui-network-monitor 插件未安装!");
1237
+ }
1238
+ };
1239
+
1240
+ //#endregion
1241
+ //#region src/server/system/manage.ts
1242
+ /**
1243
+ * 重启karin
1244
+ */
1245
+ const restartRouter = async (req, res) => {
1246
+ try {
1247
+ const { isPm2 = false, reloadDeps = false } = req.body;
1248
+ restartDirect({
1249
+ isPm2,
1250
+ reloadDeps
1251
+ });
1252
+ createSuccessResponse(res, null, "重启指令发送成功");
1253
+ } catch (error) {
1254
+ createServerErrorResponse(res, error.message);
1255
+ }
1256
+ };
1257
+ /**
1258
+ * 退出karin
1259
+ */
1260
+ const exitRouter = async (_req, res) => {
1261
+ logger.mark("收到退出请求,正在退出...");
1262
+ createSuccessResponse(res, null, "退出指令发送成功");
1263
+ const { processExit } = await import("./index.mjs").then((n) => n.t);
1264
+ await processExit(0, true);
1265
+ };
1266
+
1267
+ //#endregion
1268
+ //#region src/server/system/info.ts
1269
+ const wsOneBotSet = /* @__PURE__ */ new Set();
1270
+ const wsPuppeteerSet = /* @__PURE__ */ new Set();
1271
+ listeners.on(WS_CONNECTION_ONEBOT, (socket) => {
1272
+ wsOneBotSet.add(socket);
1273
+ });
1274
+ listeners.on(WS_CONNECTION_PUPPETEER, (socket) => {
1275
+ wsPuppeteerSet.add(socket);
1276
+ });
1277
+ listeners.on(WS_CLOSE_ONEBOT, (socket) => {
1278
+ wsOneBotSet.delete(socket);
1279
+ });
1280
+ listeners.on(WS_CLOSE_PUPPETEER, (socket) => {
1281
+ wsPuppeteerSet.delete(socket);
1282
+ });
1283
+ /**
1284
+ * 系统信息路由
1285
+ */
1286
+ const statusRouter = (_req, res) => {
1287
+ createSuccessResponse(res, {
1288
+ name: "karin",
1289
+ pid: process.pid,
1290
+ pm2_id: process.env.pm_id || "",
1291
+ uptime: process.uptime(),
1292
+ version: process.env.KARIN_VERSION,
1293
+ karin_dev: isDev(),
1294
+ karin_lang: process.env.RUNTIME === "tsx" ? "ts" : "js",
1295
+ karin_runtime: process.env.RUNTIME,
1296
+ platform: process.platform,
1297
+ arch: process.arch
1298
+ }, "成功");
1299
+ };
1300
+ const infoRouter = async (_req, res) => {
1301
+ createSuccessResponse(res, {
1302
+ onebot: Array.from(wsOneBotSet).map((ws) => {
1303
+ return {
1304
+ readyState: ws.readyState,
1305
+ url: ws.url,
1306
+ protocol: ws.protocol
1307
+ };
1308
+ }),
1309
+ puppeteer: Array.from(wsPuppeteerSet).map((ws) => {
1310
+ return {
1311
+ readyState: ws.readyState,
1312
+ url: ws.url,
1313
+ protocol: ws.protocol
1314
+ };
1315
+ })
1316
+ });
1317
+ };
1318
+ /**
1319
+ * 系统状态实时路由
1320
+ */
1321
+ const systemStatusRealTimeHandler = async (req, res) => {
1322
+ res.setHeader("Content-Type", "text/event-stream");
1323
+ res.setHeader("Connection", "keep-alive");
1324
+ const sendStatus = (status) => {
1325
+ try {
1326
+ res.write(`data: ${JSON.stringify(status)}\n\n`);
1327
+ } catch (e) {
1328
+ logger.error(`An error occurred when writing sendStatus data to client: ${e}`);
1329
+ }
1330
+ };
1331
+ statusListener.on("statusUpdate", sendStatus);
1332
+ req.on("close", () => {
1333
+ statusListener.off("statusUpdate", sendStatus);
1334
+ res.end("data: end\n\n");
1335
+ });
1336
+ };
1337
+
1338
+ //#endregion
1339
+ //#region src/server/log/getLog.ts
1340
+ /**
1341
+ * 检查是否为标准的YYYY-MM-DD格式
1342
+ * @param date - 日期字符串
1343
+ * @returns 是否为标准的YYYY-MM-DD格式
1344
+ */
1345
+ const isStandardDate = (date) => {
1346
+ return typeof date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(date);
1347
+ };
1348
+ /**
1349
+ * 临时修改当前日志等级 重启后恢复
1350
+ */
1351
+ const logLevelRouter = async (req, res) => {
1352
+ const level = req.body?.level;
1353
+ if (!level || ![
1354
+ "trace",
1355
+ "debug",
1356
+ "info",
1357
+ "warn",
1358
+ "error",
1359
+ "fatal"
1360
+ ].includes(level)) return createBadRequestResponse(res, "参数错误");
1361
+ updateLevel(level);
1362
+ createSuccessResponse(res, null, "修改成功");
1363
+ };
1364
+ /**
1365
+ * 当前活跃的连接数
1366
+ */
1367
+ let activeConnections$1 = 0;
1368
+ /**
1369
+ * 获取当前日志
1370
+ * 支持查询指定日期的日志,如果没有指定则获取当天的
1371
+ */
1372
+ const getLogRouter = async (req, res) => {
1373
+ const maxConnections = Number(process.env.LOG_API_MAX_CONNECTIONS) || 5;
1374
+ const MAX_CHUNK_SIZE = Number(process.env.LOG_API_MAX_CHUNK_SIZE) || 1024 * 1024;
1375
+ /** 检查连接数是否达到上限 */
1376
+ if (activeConnections$1 >= maxConnections) return createBadRequestResponse(res, "当前连接数已达到上限,请稍后重试");
1377
+ activeConnections$1++;
1378
+ /** 获取日期 */
1379
+ const date = moment();
1380
+ /** 检查日期是否有效 */
1381
+ if (!date.isValid()) {
1382
+ activeConnections$1--;
1383
+ return createBadRequestResponse(res, "日期格式错误");
1384
+ }
1385
+ /** 日志文件路径 */
1386
+ const file = path.join(logsPath, `logger.${date.format("YYYY-MM-DD")}.log`);
1387
+ /** 响应头 */
1388
+ res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
1389
+ /** 设置不缓存 */
1390
+ res.setHeader("Cache-Control", "no-cache");
1391
+ /** 设置长连接 */
1392
+ res.setHeader("Connection", "keep-alive");
1393
+ /** 禁用 Nginx 缓冲 */
1394
+ res.setHeader("X-Accel-Buffering", "no");
1395
+ let position = 0;
1396
+ let isStreaming = false;
1397
+ const isEventSource = req.headers.accept === "text/event-stream";
1398
+ const heartbeat = setInterval(() => {
1399
+ if (isEventSource) res.write(":heartbeat\n\n");
1400
+ }, 3e4);
1401
+ const tailFile = () => {
1402
+ if (isStreaming) return;
1403
+ fs.stat(file, (err, stats) => {
1404
+ if (err) {
1405
+ logger.error("读取日志文件状态错误:", err);
1406
+ return;
1407
+ }
1408
+ if (position > stats.size)
1409
+ /** 文件被截断,重置位置 */
1410
+ position = 0;
1411
+ if (position < stats.size) {
1412
+ /** 有新内容,读取并发送 */
1413
+ isStreaming = true;
1414
+ const endPosition = Math.min(position + MAX_CHUNK_SIZE, stats.size);
1415
+ const stream = fs.createReadStream(file, {
1416
+ start: position,
1417
+ end: endPosition - 1,
1418
+ encoding: "utf-8",
1419
+ highWaterMark: 64 * 1024
1420
+ });
1421
+ /** 监听数据流 */
1422
+ stream.on("data", (data) => {
1423
+ const lines = data.toString().split("\n");
1424
+ for (const line of lines) if (line) if (isEventSource) res.write(`data: ${line}\n\n`);
1425
+ else res.write(`${line}\n`);
1426
+ });
1427
+ /** 文件读取完毕 */
1428
+ stream.on("end", () => {
1429
+ position = endPosition;
1430
+ isStreaming = false;
1431
+ });
1432
+ /** 监听错误 */
1433
+ stream.on("error", (error) => {
1434
+ logger.error("读取日志文件错误:", error.message);
1435
+ isStreaming = false;
1436
+ });
1437
+ }
1438
+ });
1439
+ };
1440
+ /** 首次读取 */
1441
+ tailFile();
1442
+ /** 定期检查文件变化 */
1443
+ const interval = setInterval(tailFile, 1e3);
1444
+ req.on("close", () => {
1445
+ clearInterval(interval);
1446
+ clearInterval(heartbeat);
1447
+ activeConnections$1--;
1448
+ });
1449
+ req.on("error", () => {
1450
+ clearInterval(interval);
1451
+ clearInterval(heartbeat);
1452
+ activeConnections$1--;
1453
+ });
1454
+ };
1455
+ /**
1456
+ * 获取日志文件列表
1457
+ */
1458
+ const getLogFileListRouter = async (_, res) => {
1459
+ createSuccessResponse(res, fs.readdirSync(logsPath).filter((file) => file.startsWith("logger.") && file.endsWith(".log")).map((file) => file.replace("logger.", "").replace(".log", "")), "成功");
1460
+ };
1461
+ /**
1462
+ * 获取指定日志文件
1463
+ * @description 此接口一次性返回所有日志文件内容
1464
+ */
1465
+ const getLogFileRouter = async (req, res) => {
1466
+ const file = req.query.file;
1467
+ if (!isStandardDate(file)) return createBadRequestResponse(res, "日期格式错误");
1468
+ const filePath = path.join(logsPath, `logger.${file}.log`);
1469
+ if (!fs.existsSync(filePath)) return createBadRequestResponse(res, "日志文件不存在");
1470
+ const stats = fs.statSync(filePath);
1471
+ const FILE_SIZE_LIMIT = Number(process.env.LOG_FILE_SIZE_LIMIT) || 10 * 1024 * 1024;
1472
+ if (stats.size > FILE_SIZE_LIMIT) return createBadRequestResponse(res, "日志文件过大,请使用流式接口获取或下载文件");
1473
+ createSuccessResponse(res, fs.readFileSync(filePath, "utf-8"), "成功");
1474
+ };
1475
+
1476
+ //#endregion
1477
+ //#region src/server/plugins/cache.ts
1478
+ const CACHE_KEY = "karin:web:plugin:list";
1479
+ /**
1480
+ * 删除插件列表缓存
1481
+ */
1482
+ const deletePluginListCache = async () => {
1483
+ try {
1484
+ const { redis } = await import("./redis-aLJ7wbJH.mjs").then((n) => n.r);
1485
+ await redis.del(CACHE_KEY);
1486
+ } catch (error) {
1487
+ logger.error("删除插件列表缓存失败:", error);
1488
+ }
1489
+ };
1490
+
1491
+ //#endregion
1492
+ //#region src/server/plugins/install.ts
1493
+ /** 全局任务队列 */
1494
+ const taskQueue = /* @__PURE__ */ new Map();
1495
+ /**
1496
+ * 执行命令并实时获取输出
1497
+ * @param command 命令
1498
+ * @param args 参数
1499
+ * @param task 任务
1500
+ */
1501
+ const spawnCommand = (command, args, task) => {
1502
+ if (isWorkspace()) args.push("-w");
1503
+ return new Promise((resolve, reject) => {
1504
+ const child = spawn$1(command, args, {
1505
+ stdio: [
1506
+ "ignore",
1507
+ "pipe",
1508
+ "pipe"
1509
+ ],
1510
+ shell: true,
1511
+ cwd: process.cwd()
1512
+ });
1513
+ child.stdout.on("data", (data) => {
1514
+ data.toString().split("\n").filter(Boolean).forEach((line) => {
1515
+ task.logs.push(line);
1516
+ });
1517
+ });
1518
+ child.stderr.on("data", (data) => {
1519
+ data.toString().split("\n").filter(Boolean).forEach((line) => {
1520
+ task.logs.push(`[错误] ${line}`);
1521
+ });
1522
+ });
1523
+ child.on("close", (code) => {
1524
+ if (code === 0) resolve();
1525
+ else reject(/* @__PURE__ */ new Error(`命令执行失败,退出码: ${code}`));
1526
+ });
1527
+ child.on("error", (error) => reject(error));
1528
+ });
1529
+ };
1530
+ /**
1531
+ * 安装 NPM 插件
1532
+ * @param task 任务
1533
+ */
1534
+ const installNpmPlugin = async (task) => {
1535
+ task.logs.push(`开始安装 NPM 插件: ${task.name}`);
1536
+ task.logs.push("正在解析依赖...");
1537
+ await spawnCommand("pnpm", [
1538
+ "add",
1539
+ task.name,
1540
+ "--save"
1541
+ ], task);
1542
+ };
1543
+ /**
1544
+ * 安装 Git 插件
1545
+ * @param task 任务
1546
+ * @param url 下载地址
1547
+ */
1548
+ const installGitPlugin = async (task, url) => {
1549
+ if (!url) throw new Error("Git 插件需要提供仓库地址");
1550
+ const pluginDir = path.join(process.cwd(), "plugins", task.name);
1551
+ task.logs.push(`开始克隆仓库: ${url}`);
1552
+ task.logs.push(`目标目录: ${pluginDir}`);
1553
+ await fs.promises.mkdir(pluginDir, { recursive: true });
1554
+ /** git clone --depth=1 https://github.com/ikenxuan/karin-plugin-kkk.git ./plugins/karin-plugin-kkk/ */
1555
+ await spawnCommand("git", [
1556
+ "clone",
1557
+ "--depth=1",
1558
+ url,
1559
+ `./plugins/${task.name}`
1560
+ ], task);
1561
+ /** 检查是否有 package.json */
1562
+ const pkgPath = path.join(pluginDir, "package.json");
1563
+ if (fs.existsSync(pkgPath)) {
1564
+ task.logs.push("检测到 package.json,开始安装依赖...");
1565
+ await spawnCommand("pnpm", ["install"], task);
1566
+ }
1567
+ };
1568
+ /**
1569
+ * 安装 App 插件
1570
+ * @param task 任务
1571
+ * @param url 下载地址
1572
+ */
1573
+ const installAppPlugin = async (task, url) => {
1574
+ if (!url) throw new Error("App 插件需要提供下载地址");
1575
+ /** 非js、ts不允许下载 */
1576
+ if (!url.endsWith(".js") && !url.endsWith(".ts")) throw new Error("非js、ts不允许下载");
1577
+ const pluginDir = path.join(process.cwd(), "plugins", "karin-plugin-example");
1578
+ task.logs.push(`开始下载插件: ${url}`);
1579
+ task.logs.push(`目标目录: ${pluginDir}`);
1580
+ await fs.promises.mkdir(pluginDir, { recursive: true });
1581
+ /** 使用 curl 下载文件,以获取下载进度 */
1582
+ await spawnCommand("curl", [
1583
+ "-L",
1584
+ "-o",
1585
+ pluginDir,
1586
+ url
1587
+ ], task);
1588
+ };
1589
+ /**
1590
+ * 执行插件安装任务
1591
+ * @param task 任务
1592
+ * @param url 下载地址
1593
+ */
1594
+ const installPluginTask = async (task, url) => {
1595
+ try {
1596
+ task.status = "running";
1597
+ task.logs.push(`开始安装插件: ${task.name}`);
1598
+ task.logs.push(`插件类型: ${task.type}`);
1599
+ task.logs.push("-------------------");
1600
+ switch (task.type) {
1601
+ case "npm":
1602
+ await installNpmPlugin(task);
1603
+ break;
1604
+ case "git":
1605
+ await installGitPlugin(task, url);
1606
+ break;
1607
+ case "app":
1608
+ await installAppPlugin(task, url);
1609
+ break;
1610
+ default: throw new Error("不支持的插件类型");
1611
+ }
1612
+ task.logs.push("-------------------");
1613
+ task.logs.push("🎉 安装完成!");
1614
+ task.status = "completed";
1615
+ /** 清除插件列表缓存 */
1616
+ await deletePluginListCache();
1617
+ } catch (error) {
1618
+ task.status = "failed";
1619
+ task.error = error.message;
1620
+ task.logs.push("-------------------");
1621
+ task.logs.push(`❌ 安装失败: ${error.message}`);
1622
+ throw error;
1623
+ }
1624
+ };
1625
+ /**
1626
+ * 安装插件
1627
+ */
1628
+ const pluginInstall = async (req, res) => {
1629
+ try {
1630
+ const { name, type, url } = req.body;
1631
+ const taskId = `${type}-${name}-${Date.now()}`;
1632
+ if (Array.from(taskQueue.values()).find((task) => task.name === name && task.status === "running")) return createServerErrorResponse(res, "该插件正在安装中");
1633
+ /** 创建新任务 */
1634
+ const task = {
1635
+ id: taskId,
1636
+ name,
1637
+ type,
1638
+ status: "pending",
1639
+ logs: [],
1640
+ minimized: false
1641
+ };
1642
+ taskQueue.set(taskId, task);
1643
+ /** 异步执行安装 */
1644
+ installPluginTask(task, url).catch((error) => {
1645
+ task.status = "failed";
1646
+ task.error = error.message;
1647
+ task.logs.push(`安装失败: ${error.message}`);
1648
+ });
1649
+ createSuccessResponse(res, { taskId });
1650
+ } catch (error) {
1651
+ createServerErrorResponse(res, error.message);
1652
+ logger.error(error);
1653
+ }
1654
+ };
1655
+ /**
1656
+ * 卸载 NPM 插件
1657
+ */
1658
+ const uninstallNpmPlugin = async (task) => {
1659
+ task.logs.push(`开始卸载 NPM 插件: ${task.name}`);
1660
+ await spawnCommand("pnpm", ["rm", task.name], task);
1661
+ };
1662
+ /**
1663
+ * 卸载 Git 插件
1664
+ * @param task 任务
1665
+ */
1666
+ const uninstallGitPlugin = async (task) => {
1667
+ const pluginDir = path.join(process.cwd(), "plugins", task.name);
1668
+ task.logs.push(`开始删除插件目录: ${pluginDir}`);
1669
+ /** 删除插件目录 */
1670
+ await fs.promises.rm(pluginDir, {
1671
+ recursive: true,
1672
+ force: true
1673
+ });
1674
+ task.logs.push("插件目录已删除");
1675
+ /** 清理依赖缓存 */
1676
+ task.logs.push("正在清理依赖缓存...");
1677
+ await spawnCommand("pnpm", ["install", "-P"], task);
1678
+ };
1679
+ /**
1680
+ * 执行插件卸载任务
1681
+ * @param task 任务
1682
+ */
1683
+ const uninstallPluginTask = async (task) => {
1684
+ try {
1685
+ task.status = "running";
1686
+ task.logs.push(`开始卸载插件: ${task.name}`);
1687
+ task.logs.push(`插件类型: ${task.type}`);
1688
+ task.logs.push("-------------------");
1689
+ switch (task.type) {
1690
+ case "npm":
1691
+ await uninstallNpmPlugin(task);
1692
+ break;
1693
+ case "git":
1694
+ await uninstallGitPlugin(task);
1695
+ break;
1696
+ default: throw new Error("不支持卸载该类型的插件");
1697
+ }
1698
+ task.logs.push("-------------------");
1699
+ task.logs.push("🎉 卸载完成!");
1700
+ task.logs.push("⚠️ 建议重启 Bot 以使更改生效");
1701
+ task.status = "completed";
1702
+ /** 清除插件列表缓存 */
1703
+ await deletePluginListCache();
1704
+ } catch (error) {
1705
+ task.status = "failed";
1706
+ task.error = error.message;
1707
+ task.logs.push("-------------------");
1708
+ task.logs.push(`❌ 卸载失败: ${error.message}`);
1709
+ throw error;
1710
+ }
1711
+ };
1712
+ /**
1713
+ * 卸载插件
1714
+ */
1715
+ const pluginUninstall = async (req, res) => {
1716
+ try {
1717
+ const { name, type } = req.body;
1718
+ const taskId = `uninstall-${type}-${name}-${Date.now()}`;
1719
+ if (Array.from(taskQueue.values()).find((task) => task.name === name && task.status === "running")) return createServerErrorResponse(res, "该插件正在卸载中");
1720
+ /** 创建新任务 */
1721
+ const task = {
1722
+ id: taskId,
1723
+ name,
1724
+ type,
1725
+ status: "pending",
1726
+ logs: [],
1727
+ minimized: false
1728
+ };
1729
+ taskQueue.set(taskId, task);
1730
+ /** 异步执行卸载 */
1731
+ uninstallPluginTask(task).catch((error) => {
1732
+ task.status = "failed";
1733
+ task.error = error.message;
1734
+ task.logs.push(`卸载失败: ${error.message}`);
1735
+ });
1736
+ createSuccessResponse(res, { taskId });
1737
+ } catch (error) {
1738
+ createServerErrorResponse(res, error.message);
1739
+ logger.error(error);
1740
+ }
1741
+ };
1742
+ /**
1743
+ * 获取任务状态
1744
+ */
1745
+ const pluginGetTaskStatus = (req, res) => {
1746
+ try {
1747
+ const { taskId } = req.body;
1748
+ const task = taskQueue.get(taskId);
1749
+ if (!task) return createServerErrorResponse(res, "任务不存在");
1750
+ createSuccessResponse(res, task);
1751
+ } catch (error) {
1752
+ createServerErrorResponse(res, error.message);
1753
+ logger.error(error);
1754
+ }
1755
+ };
1756
+ /** 清理已完成的任务(保留作为备用清理机制) */
1757
+ const cleanupTasks = () => {
1758
+ const THIRTY_MINUTES = 1800 * 1e3;
1759
+ for (const [taskId, task] of taskQueue.entries()) if ((task.status === "completed" || task.status === "failed") && Date.now() - parseInt(taskId.split("-").pop() || "0") > THIRTY_MINUTES) taskQueue.delete(taskId);
1760
+ };
1761
+ /**
1762
+ * 获取任务列表
1763
+ */
1764
+ const pluginGetTaskList = (_req, res) => {
1765
+ /** 清理过期任务 */
1766
+ cleanupTasks();
1767
+ createSuccessResponse(res, Array.from(taskQueue.values()));
1768
+ };
1769
+ /**
1770
+ * 更新任务状态(最小化/恢复)
1771
+ */
1772
+ const pluginUpdateTaskStatus = (req, res) => {
1773
+ try {
1774
+ const { taskId, minimized } = req.body;
1775
+ const task = taskQueue.get(taskId);
1776
+ if (!task) return createServerErrorResponse(res, "任务不存在");
1777
+ task.minimized = minimized;
1778
+ createSuccessResponse(res, task);
1779
+ } catch (error) {
1780
+ createServerErrorResponse(res, error.message);
1781
+ logger.error(error);
1782
+ }
1783
+ };
1784
+
1785
+ //#endregion
1786
+ //#region src/server/pty/index.ts
1787
+ /**
1788
+ * 创建终端
1789
+ * @param req 请求
1790
+ * @param res 响应
1791
+ */
1792
+ const createTerminalHandler = async (req, res) => {
1793
+ try {
1794
+ const { cols, rows, shell, name } = req.body;
1795
+ const { id } = await createTerminal(name, shell, cols, rows);
1796
+ return createSuccessResponse(res, { id });
1797
+ } catch (error) {
1798
+ logger.error(`[terminal] 创建终端失败: ${req.body.shell}`);
1799
+ logger.error(error);
1800
+ return createServerErrorResponse(res, `创建终端失败: ${error.message}`);
1801
+ }
1802
+ };
1803
+ /**
1804
+ * 获取终端列表
1805
+ * @param _ 请求
1806
+ * @param res 响应
1807
+ */
1808
+ const getTerminalListHandler = (_, res) => {
1809
+ try {
1810
+ return createSuccessResponse(res, getTerminalList());
1811
+ } catch (error) {
1812
+ logger.error("[terminal] 获取终端列表失败");
1813
+ logger.error(error);
1814
+ return createServerErrorResponse(res, `获取终端列表失败: ${error.message}`);
1815
+ }
1816
+ };
1817
+ /**
1818
+ * 关闭终端
1819
+ * @param req 请求
1820
+ * @param res 响应
1821
+ */
1822
+ const closeTerminalHandler = (req, res) => {
1823
+ try {
1824
+ const id = req.body.id;
1825
+ if (!id) return createServerErrorResponse(res, "ID不能为空");
1826
+ closeTerminal(id);
1827
+ return createSuccessResponse(res, {});
1828
+ } catch (error) {
1829
+ logger.error(`[terminal] 关闭终端失败: ${req.body.id}`);
1830
+ logger.error(error);
1831
+ return createServerErrorResponse(res, `关闭终端失败: ${error.message}`);
1832
+ }
1833
+ };
1834
+
1835
+ //#endregion
1836
+ //#region src/server/system/check.ts
1837
+ /**
1838
+ * 检查是否安装了指定的npm包
1839
+ */
1840
+ const checkPlugin = async (req, res) => {
1841
+ try {
1842
+ await import(req.body.name);
1843
+ return createSuccessResponse(res, { installed: true }, "已安装");
1844
+ } catch {
1845
+ return createSuccessResponse(res, { installed: false }, "未安装");
1846
+ }
1847
+ };
1848
+
1849
+ //#endregion
1850
+ //#region src/server/plugins/webui.ts
1851
+ /** 版本缓存对象,键为插件名,值为缓存数据和时间戳 */
1852
+ const versionCache = {};
1853
+ /** 缓存有效期(毫秒) */
1854
+ const CACHE_TTL$1 = 120 * 1e3;
1855
+ /**
1856
+ * 插件列表
1857
+ */
1858
+ const plugins = [{
1859
+ name: "@karinjs/node-pty",
1860
+ installed: false,
1861
+ description: "提供终端功能支持,允许您在WebUI中使用命令行终端。需要注意,此插件存在风险,安装后可运行一切命令。"
1862
+ }, {
1863
+ name: "@karinjs/plugin-webui-network-monitor",
1864
+ installed: false,
1865
+ description: "网络监控插件,提供网络流量、连接等监控功能,可视化展示系统网络状态。"
1866
+ }];
1867
+ /**
1868
+ * 安装webui插件
1869
+ */
1870
+ const installWebui = async (req, res) => {
1871
+ try {
1872
+ const { name } = req.body;
1873
+ if (!name) return createServerErrorResponse(res, "name不能为空");
1874
+ if (!plugins.some((p) => p.name === name)) return createServerErrorResponse(res, "非法插件");
1875
+ const result = await exec$3(`pnpm install ${name}${isWorkspace() ? " -w" : ""}`);
1876
+ if (name === "@karinjs/node-pty") await initialize();
1877
+ if (result.error) logger.error(new Error(`安装webui插件发生错误: ${name}`, { cause: result.error }));
1878
+ logger.mark(`[webui] 安装 ${name} 插件成功`);
1879
+ return createSuccessResponse(res, {
1880
+ status: result.status,
1881
+ data: result.status ? "安装成功" : result.error?.message || "安装失败"
1882
+ });
1883
+ } catch (error) {
1884
+ logger.error(`[webui] 安装webui插件失败: ${req.body.name}`);
1885
+ logger.error(error);
1886
+ return createServerErrorResponse(res, `安装webui插件失败: ${error.message}`);
1887
+ }
1888
+ };
1889
+ /**
1890
+ * 卸载webui插件
1891
+ */
1892
+ const uninstallWebui = async (req, res) => {
1893
+ try {
1894
+ const { name } = req.body;
1895
+ if (!name) return createServerErrorResponse(res, "name不能为空");
1896
+ if (!plugins.some((p) => p.name === name)) return createServerErrorResponse(res, "非法插件");
1897
+ const result = await exec$3(`pnpm uninstall ${name}`);
1898
+ logger.mark(`[webui] 卸载 ${name} 插件成功`);
1899
+ return createSuccessResponse(res, {
1900
+ status: result.status,
1901
+ data: result.status ? "卸载成功" : result.error?.message || "卸载失败"
1902
+ });
1903
+ } catch (error) {
1904
+ logger.error(`[webui] 卸载webui插件失败: ${req.body.name}`);
1905
+ logger.error(error);
1906
+ return createServerErrorResponse(res, `卸载webui插件失败: ${error.message}`);
1907
+ }
1908
+ };
1909
+ /**
1910
+ * 获取webui插件列表
1911
+ */
1912
+ const getWebuiPluginList = async (_, res) => {
1913
+ try {
1914
+ const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), "package.json"), "utf-8"));
1915
+ return createSuccessResponse(res, plugins.map((plugin) => {
1916
+ const version = pkg.dependencies?.[plugin.name] || pkg.devDependencies?.[plugin.name] || pkg.peerDependencies?.[plugin.name] || null;
1917
+ return {
1918
+ ...plugin,
1919
+ installed: typeof version === "string",
1920
+ version
1921
+ };
1922
+ }));
1923
+ } catch (error) {
1924
+ logger.error("[webui] 获取插件列表失败");
1925
+ logger.error(error);
1926
+ return createServerErrorResponse(res, `获取插件列表失败: ${error.message}`);
1927
+ }
1928
+ };
1929
+ /**
1930
+ * 获取WebUI插件可用版本
1931
+ */
1932
+ const getWebuiPluginVersions = async (req, res) => {
1933
+ try {
1934
+ const { name } = req.body;
1935
+ if (!name) return createServerErrorResponse(res, "name不能为空");
1936
+ if (!plugins.some((p) => p.name === name)) return createServerErrorResponse(res, "非法插件");
1937
+ const now = Date.now();
1938
+ const cachedData = versionCache[name];
1939
+ if (cachedData && now - cachedData.timestamp < CACHE_TTL$1) {
1940
+ logger.debug(`[webui] 使用缓存的版本信息: ${name}`);
1941
+ return createSuccessResponse(res, cachedData.data);
1942
+ }
1943
+ /**
1944
+ * 获取当前安装的版本(如果已安装)
1945
+ */
1946
+ let currentVersion = null;
1947
+ const pkgPath = path.join(process.cwd(), "node_modules", name, "package.json");
1948
+ if (fs.existsSync(pkgPath)) try {
1949
+ const pkgContent = fs.readFileSync(pkgPath, "utf-8");
1950
+ currentVersion = JSON.parse(pkgContent).version;
1951
+ } catch (error) {
1952
+ logger.error(`[webui] 读取插件版本失败: ${name}`);
1953
+ }
1954
+ /**
1955
+ * 从npm获取可用版本
1956
+ */
1957
+ logger.info(`[webui] 获取插件 ${name} 的版本信息`);
1958
+ const result = await exec$3(`npm view ${name} versions --json`);
1959
+ if (!result.status) return createServerErrorResponse(res, `获取版本信息失败: ${result.error?.message}`);
1960
+ try {
1961
+ let versions = JSON.parse(result.stdout);
1962
+ versions = Array.isArray(versions) ? versions : [versions];
1963
+ /** 反转数组,让最新版本排在前面 */
1964
+ versions.reverse();
1965
+ /** 限制返回的版本数量为20个 */
1966
+ const limitedVersions = versions.slice(0, 20);
1967
+ const responseData = {
1968
+ currentVersion,
1969
+ availableVersions: limitedVersions,
1970
+ hasMoreVersions: versions.length > 20
1971
+ };
1972
+ versionCache[name] = {
1973
+ data: responseData,
1974
+ timestamp: now
1975
+ };
1976
+ return createSuccessResponse(res, responseData);
1977
+ } catch (error) {
1978
+ return createServerErrorResponse(res, `解析版本信息失败: ${error.message}`);
1979
+ }
1980
+ } catch (error) {
1981
+ logger.error(`[webui] 获取插件版本失败: ${req.body.name}`);
1982
+ logger.error(error);
1983
+ return createServerErrorResponse(res, `获取插件版本失败: ${error.message}`);
1984
+ }
1985
+ };
1986
+ /**
1987
+ * 更新WebUI插件到指定版本
1988
+ */
1989
+ const updateWebuiPluginVersion = async (req, res) => {
1990
+ try {
1991
+ const { name, version } = req.body;
1992
+ if (!name || !version) return createServerErrorResponse(res, "name和version不能为空");
1993
+ if (!plugins.some((p) => p.name === name)) return createServerErrorResponse(res, "非法插件");
1994
+ const result = await exec$3(`pnpm install ${name}@${version}${isWorkspace() ? " -w" : ""}`);
1995
+ return createSuccessResponse(res, {
1996
+ status: result.status,
1997
+ data: result.status ? "更新成功" : result.error?.message || "更新失败"
1998
+ });
1999
+ } catch (error) {
2000
+ logger.error(`[webui] 更新插件版本失败: ${req.body.name}@${req.body.version}`);
2001
+ logger.error(error);
2002
+ return createServerErrorResponse(res, `更新插件版本失败: ${error.message}`);
2003
+ }
2004
+ };
2005
+
2006
+ //#endregion
2007
+ //#region src/server/plugins/admin/tool.ts
2008
+ /**
2009
+ * 验证插件请求基本参数
2010
+ *
2011
+ * 检查插件操作请求的必要参数是否有效:
2012
+ * 1. 验证名称和目标不为空
2013
+ * 2. 验证插件类型是否在允许的类型列表中
2014
+ *
2015
+ * @param res - 响应对象,用于返回错误信息
2016
+ * @param name - 插件名称
2017
+ * @param target - 插件目标
2018
+ * @param pluginType - 插件类型 (npm/git/app)
2019
+ * @param allowedTypes - 允许的插件类型列表
2020
+ * @returns 验证是否通过
2021
+ */
2022
+ const validatePluginRequest = (res, name, target, pluginType, allowedTypes) => {
2023
+ if (!name || !target) {
2024
+ createBadRequestResponse(res, "无效请求: 插件名称或任务名称不能为空");
2025
+ return false;
2026
+ }
2027
+ if (!allowedTypes.includes(pluginType)) {
2028
+ createBadRequestResponse(res, "无效请求: 插件类型错误");
2029
+ return false;
2030
+ }
2031
+ return true;
2032
+ };
2033
+ /**
2034
+ * 启动命令子进程
2035
+ *
2036
+ * 创建一个子进程来执行指定的命令,并通过回调函数处理输出。
2037
+ * 处理标准输出、标准错误、进程结束和错误事件。
2038
+ *
2039
+ * @param command - 要执行的命令(如'npm'、'git')
2040
+ * @param args - 命令参数数组
2041
+ * @param options - 进程选项,包括工作目录和环境变量
2042
+ * @param emitLog - 日志回调函数,用于处理进程的输出信息
2043
+ * @returns 创建的子进程对象
2044
+ */
2045
+ const spawnProcess = (command, args, options = {}, emitLog, pnpm) => {
2046
+ return new Promise((resolve) => {
2047
+ const proc = spawn(command, args, {
2048
+ shell: true,
2049
+ cwd: options.cwd || process.cwd(),
2050
+ env: {
2051
+ ...process.env,
2052
+ ...options.env
2053
+ }
2054
+ });
2055
+ proc.stdout.on("data", (data) => {
2056
+ const message = data.toString();
2057
+ logger.debug(message);
2058
+ emitLog(message);
2059
+ });
2060
+ proc.stderr.on("data", (data) => {
2061
+ const message = data.toString();
2062
+ if (message.includes("ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF")) pnpm && pnpm();
2063
+ logger.debug(message);
2064
+ emitLog(message);
2065
+ });
2066
+ proc.on("close", (code) => {
2067
+ proc.kill();
2068
+ emitLog(`执行完成,退出码: ${code}`);
2069
+ resolve(true);
2070
+ });
2071
+ proc.on("error", (error) => {
2072
+ logger.debug(error);
2073
+ emitLog(`执行失败: ${error.message}`);
2074
+ logger.error(error);
2075
+ resolve(error);
2076
+ });
2077
+ return proc;
2078
+ });
2079
+ };
2080
+ /**
2081
+ * 处理安装、更新依赖响应
2082
+ * @description 安装、更新依赖接口只允许返回200响应
2083
+ * @param res - 响应对象
2084
+ * @param success - 是否成功
2085
+ * @param message - 消息
2086
+ * @param taskId - 任务ID
2087
+ * @returns 操作响应
2088
+ */
2089
+ const handleReturn = (res, success, message, taskId) => {
2090
+ if (taskId) return createSuccessResponse(res, {
2091
+ success,
2092
+ message,
2093
+ taskId
2094
+ });
2095
+ return createSuccessResponse(res, {
2096
+ success,
2097
+ message
2098
+ });
2099
+ };
2100
+
2101
+ //#endregion
2102
+ //#region src/server/plugins/admin/update.ts
2103
+ /**
2104
+ * 更新插件
2105
+ *
2106
+ * 负责处理插件更新请求,支持两种更新模式:
2107
+ * 1. 更新所有插件 (isAll=true)
2108
+ * 2. 更新单个指定插件
2109
+ *
2110
+ * 更新过程:
2111
+ * 1. 验证请求参数
2112
+ * 2. 处理全部更新或单个更新逻辑
2113
+ * 3. 创建并启动更新任务
2114
+ * 4. 返回任务ID和操作结果
2115
+ *
2116
+ * @param res - 响应对象
2117
+ * @param data - 更新数据,包含插件名称、目标、类型等
2118
+ * @param ip - 操作者IP地址
2119
+ * @returns 操作响应
2120
+ */
2121
+ const update = async (res, data, ip = "0.0.0.0") => {
2122
+ if (!Array.isArray(data.target) || data.target.length < 1 && !data.isAll) return handleReturn(res, false, "无效请求: 插件目标错误");
2123
+ /** 更新全部插件 */
2124
+ if (data.isAll) return handleReturn(res, true, "更新任务已创建,请通过taskId执行任务", await taskSystem.add({
2125
+ type: "update",
2126
+ name: data.name,
2127
+ target: "all",
2128
+ operatorIp: ip
2129
+ }, async (options, emitLog) => {
2130
+ await updateAll(options, emitLog, data.isAll);
2131
+ await taskSystem.update.logs(options.id, "任务执行成功");
2132
+ return true;
2133
+ }));
2134
+ const performUpdate = async (_, log) => {
2135
+ const npm = [];
2136
+ const git = [];
2137
+ /** 记录不存在的插件 */
2138
+ const notExist = [];
2139
+ const list = await getPlugins("all", false, true);
2140
+ for (const item of data.target) {
2141
+ if (item.type === "npm") {
2142
+ list.includes(`${item.type}:${item.name}`) ? npm.push({
2143
+ name: item.name,
2144
+ version: item.version || "latest"
2145
+ }) : notExist.push(item.name);
2146
+ continue;
2147
+ }
2148
+ if (item.type === "git") {
2149
+ const force = typeof item.force === "boolean" ? item.force : false;
2150
+ list.includes(`${item.type}:${item.name}`) ? git.push({
2151
+ name: item.name,
2152
+ version: item.version || "latest",
2153
+ force
2154
+ }) : notExist.push(item.name);
2155
+ continue;
2156
+ }
2157
+ notExist.push(item.name);
2158
+ }
2159
+ if (npm.length > 0) await spawnProcess("pnpm", [
2160
+ "update",
2161
+ ...npm.map((item) => `${item.name}@${item.version}`),
2162
+ "--save"
2163
+ ], { timeout: 60 * 1e3 }, log);
2164
+ for (const item of git) {
2165
+ const { name, force } = item;
2166
+ const result = await gitPull(path.join(karinPathPlugins, name), {
2167
+ force,
2168
+ timeout: 60 * 1e3
2169
+ });
2170
+ if (result.status) log(`更新 ${name}(git) 插件成功: ${result.hash.before} -> ${result.hash.after}`);
2171
+ else log(`更新 ${name}(git) 插件失败: ${result.data}`);
2172
+ }
2173
+ return true;
2174
+ };
2175
+ return handleReturn(res, true, "更新任务已创建,请通过taskId执行任务", await taskSystem.add({
2176
+ type: "update",
2177
+ name: data.name,
2178
+ target: data.target.map((item) => `${item.type}:${item.name}`).join(","),
2179
+ operatorIp: ip
2180
+ }, async (options, emitLog) => {
2181
+ try {
2182
+ await performUpdate(options, emitLog);
2183
+ await taskSystem.update.logs(options.id, "任务执行成功");
2184
+ return true;
2185
+ } catch (error) {
2186
+ await taskSystem.update.logs(options.id, `任务执行失败: ${error.message}`);
2187
+ return false;
2188
+ }
2189
+ }));
2190
+ };
2191
+ /**
2192
+ * 更新全部插件
2193
+ *
2194
+ * 同时更新所有npm和git类型的插件:
2195
+ * 1. 获取所有插件列表并按类型分类
2196
+ * 2. 对npm插件执行批量更新
2197
+ * 3. 对git插件逐个执行更新
2198
+ * 4. 记录更新过程的日志
2199
+ *
2200
+ * @param _ - 任务参数,包含任务ID和相关信息
2201
+ * @param log - 日志函数,用于记录更新进度和结果
2202
+ * @returns 操作是否成功
2203
+ */
2204
+ const updateAll = async (_, log, options) => {
2205
+ /**
2206
+ * 将插件列表按类型分类
2207
+ * @returns 分类后的插件列表 {npm: string[], git: string[]}
2208
+ */
2209
+ const categorizePlugins = async () => {
2210
+ const list = await getPlugins("all", true, true);
2211
+ const git = [];
2212
+ const npm = ["node-karin"];
2213
+ for (const item of list) if (item.type === "npm") npm.push(item.name);
2214
+ else if (item.type === "git") git.push(item.name);
2215
+ return {
2216
+ npm,
2217
+ git
2218
+ };
2219
+ };
2220
+ /**
2221
+ * 更新所有NPM插件
2222
+ * @param npmPlugins - NPM插件列表
2223
+ */
2224
+ const updateNpmPlugins = async (npmPlugins) => {
2225
+ if (npmPlugins.length === 0) return;
2226
+ log(`* 开始更新NPM插件,共${npmPlugins.length}个`);
2227
+ const args = [
2228
+ "update",
2229
+ npmPlugins.join("@latest "),
2230
+ "--save"
2231
+ ];
2232
+ if (isWorkspace()) args.push("-w");
2233
+ await spawnProcess("pnpm", args, {}, log);
2234
+ };
2235
+ /**
2236
+ * 更新单个Git插件
2237
+ * @param pluginName - 插件名称
2238
+ * @returns 更新结果的日志消息
2239
+ */
2240
+ const updateGitPlugin = async (pluginName) => {
2241
+ const result = await gitPull(path.join(karinPathPlugins, pluginName), {
2242
+ force: options?.force,
2243
+ timeout: 60 * 1e3
2244
+ });
2245
+ if (result.status) return `更新 ${pluginName}(git) 插件成功: ${result.hash.before} -> ${result.hash.after}`;
2246
+ return `更新 ${pluginName}(git) 插件失败: ${result.data}`;
2247
+ };
2248
+ /**
2249
+ * 更新所有Git插件
2250
+ * @param gitPlugins - Git插件列表
2251
+ */
2252
+ const updateGitPlugins = async (gitPlugins) => {
2253
+ if (gitPlugins.length === 0) return;
2254
+ log(`* 开始更新Git插件,共${gitPlugins.length}个`);
2255
+ for (const item of gitPlugins) log(await updateGitPlugin(item));
2256
+ log("全部git插件更新完成");
2257
+ };
2258
+ const { npm, git } = await categorizePlugins();
2259
+ try {
2260
+ await updateNpmPlugins(npm);
2261
+ await updateGitPlugins(git);
2262
+ } catch (error) {
2263
+ log(`* 发生错误: ${error instanceof Error ? error.message : String(error)}`);
2264
+ }
2265
+ return true;
2266
+ };
2267
+
2268
+ //#endregion
2269
+ //#region src/plugin/system/market.ts
2270
+ /** 插件源地址列表 */
2271
+ const PLUGIN_SOURCES = [
2272
+ "https://registry.npmmirror.com/@karinjs/plugins-list/latest",
2273
+ "https://registry.npmjs.com/@karinjs/plugins-list/latest",
2274
+ "https://mirrors.cloud.tencent.com/npm/@karinjs/plugins-list/latest"
2275
+ ];
2276
+ /** 缓存有效期 默认12小时 */
2277
+ const CACHE_TTL = process.env.PLUGIN_MARKET_CACHE_TTL ? Number(process.env.PLUGIN_MARKET_CACHE_TTL) : 720 * 60;
2278
+ /**
2279
+ * 获取插件市场
2280
+ * @param forceUpdate - 是否强制从远程获取,忽略缓存
2281
+ * @returns 返回最先成功响应的插件列表
2282
+ */
2283
+ const getPluginMarket = async (forceUpdate = false) => {
2284
+ const { redis } = await import("./redis-aLJ7wbJH.mjs").then((n) => n.r);
2285
+ if (!forceUpdate) {
2286
+ const cachedData = await redis.get(REDIS_PLUGIN_LIST_CACHE_KEY);
2287
+ if (cachedData) {
2288
+ const data = JSON.parse(cachedData);
2289
+ if (data) return data;
2290
+ }
2291
+ }
2292
+ const results = await raceRequest(PLUGIN_SOURCES, { method: "get" });
2293
+ if (!results) throw new Error("无法从任何源获取插件列表");
2294
+ await redis.set(REDIS_PLUGIN_LIST_CACHE_KEY, JSON.stringify(results.data), { EX: CACHE_TTL });
2295
+ logger.debug(`[插件列表] 数据已缓存,有效期${CACHE_TTL}秒`);
2296
+ return results.data;
2297
+ };
2298
+
2299
+ //#endregion
2300
+ //#region src/server/plugins/admin/installMarket.ts
2301
+ /**
2302
+ * 插件市场安装
2303
+ * @param res - 响应对象
2304
+ * @param data - 安装数据,包含插件名称、目标、类型等
2305
+ * @param ip - 操作者IP地址
2306
+ * @returns 操作响应
2307
+ */
2308
+ const installMarket = async (res, data, ip) => {
2309
+ const plugin = (await getPluginMarket(true)).plugins.find((item) => item.name === data.target);
2310
+ if (!plugin) return handleReturn(res, false, "插件包不存在");
2311
+ if (data.pluginType === "app" && plugin.type === "app") return installApp$1(res, plugin, data, ip);
2312
+ if (data.pluginType === "npm" && plugin.type === "npm") {
2313
+ if (Array.isArray(plugin.allowBuild) && plugin.allowBuild.length) data.allowBuild = plugin.allowBuild;
2314
+ return installNpm$1(res, plugin, data, ip);
2315
+ }
2316
+ if (data.pluginType === "git" && plugin.type === "git") return installGit$1(res, plugin, data, ip);
2317
+ };
2318
+ /**
2319
+ * 插件市场 安装NPM类型的插件
2320
+ * @param res - 响应对象
2321
+ * @param plugin - 插件信息
2322
+ * @param data - 安装数据,包含插件名称、目标、类型等
2323
+ * @param ip - 操作者IP地址
2324
+ * @returns 操作响应
2325
+ */
2326
+ const installNpm$1 = async (res, _, data, ip) => {
2327
+ return handleReturn(res, true, "安装任务已创建,请通过taskId执行任务", await taskSystem.add({
2328
+ type: "install",
2329
+ name: data.name,
2330
+ target: data.target,
2331
+ operatorIp: ip
2332
+ }, async (_, emitLog) => {
2333
+ const args = [
2334
+ "add",
2335
+ data.target,
2336
+ "--save"
2337
+ ];
2338
+ if (isWorkspace()) args.push("-w");
2339
+ if (Array.isArray(data.allowBuild) && data.allowBuild.length && isPnpm10()) data.allowBuild.forEach((pkg) => args.unshift(`--allow-build=${pkg}`));
2340
+ /** 处理 ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF 错误 */
2341
+ let IS_ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF = false;
2342
+ await spawnProcess("pnpm", args, {}, emitLog, () => {
2343
+ IS_ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF = true;
2344
+ });
2345
+ if (IS_ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF) {
2346
+ emitLog("检测到 ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF 错误,尝试修复...");
2347
+ emitLog("执行 pnpm install -f 强制重建模块目录");
2348
+ /** 先执行 pnpm install -f 强制重建模块目录 */
2349
+ await spawnProcess("pnpm", ["install", "-f"], {}, emitLog);
2350
+ emitLog("模块目录重建完成,重新尝试安装插件");
2351
+ /** 重新尝试安装 */
2352
+ await spawnProcess("pnpm", args, {}, emitLog);
2353
+ emitLog("安装完成,尝试加载插件");
2354
+ }
2355
+ await pkgHotReload("npm", data.target);
2356
+ return true;
2357
+ }));
2358
+ };
2359
+ /**
2360
+ * 插件市场 安装Git类型的插件
2361
+ * @param res - 响应对象
2362
+ * @param plugin - 插件信息
2363
+ * @param data - 安装数据,包含插件名称、目标、类型等
2364
+ * @param ip - 操作者IP地址
2365
+ * @returns 操作响应
2366
+ */
2367
+ const installGit$1 = async (res, plugin, data, ip) => {
2368
+ /** 竞速 */
2369
+ const urls = [];
2370
+ plugin.repo.forEach((v) => {
2371
+ if (!v.type.includes("git")) return;
2372
+ urls.push(v.url);
2373
+ });
2374
+ const repo = await raceRequest(urls, {
2375
+ method: "HEAD",
2376
+ timeout: 5e3
2377
+ });
2378
+ if (repo?.status !== 200) return handleReturn(res, false, "测试访问仓库失败,请检查当前网络环境是否正常");
2379
+ return handleReturn(res, true, "安装任务已创建,请通过taskId执行任务", await taskSystem.add({
2380
+ type: "install",
2381
+ name: data.name,
2382
+ target: data.target,
2383
+ operatorIp: ip
2384
+ }, async (_, emitLog) => {
2385
+ await spawnProcess("git", [
2386
+ "clone",
2387
+ "--depth=1",
2388
+ repo.config.url,
2389
+ `./plugins/${plugin.name}`
2390
+ ], {}, emitLog);
2391
+ await pkgHotReload("git", plugin.name);
2392
+ return true;
2393
+ }));
2394
+ };
2395
+ /**
2396
+ * 插件市场 安装app类型的插件
2397
+ * @param res - 响应对象
2398
+ * @param plugin - 插件信息
2399
+ * @param data - 安装数据,包含插件名称、目标、类型等
2400
+ * @param ip - 操作者IP地址
2401
+ * @returns 操作响应
2402
+ */
2403
+ const installApp$1 = async (res, plugin, data, ip) => {
2404
+ if (!data.urls || !Array.isArray(data.urls)) return handleReturn(res, false, "app插件名称不能为空");
2405
+ /** 排除掉files中 不存在插件市场的文件 */
2406
+ let urls = plugin.files.filter((item) => data.urls.includes(item.url));
2407
+ if (!urls.length) return handleReturn(res, false, "请传递正确的app插件名称");
2408
+ let isRace = false;
2409
+ /** 对每个url进行判断 如果是github的 竞速换源 */
2410
+ for (const app of urls) if (app.url.startsWith("https://raw.githubusercontent.com")) {
2411
+ isRace = true;
2412
+ break;
2413
+ }
2414
+ if (isRace) {
2415
+ const result = await getFastGithub("raw");
2416
+ urls = urls.map((item) => {
2417
+ return {
2418
+ ...item,
2419
+ url: item.url.startsWith("https://raw.githubusercontent.com") ? result.raw(item.url) : item.url
2420
+ };
2421
+ });
2422
+ }
2423
+ return handleReturn(res, true, "安装任务已创建,请通过taskId执行任务", await taskSystem.add({
2424
+ type: "install",
2425
+ name: data.name,
2426
+ target: data.target,
2427
+ operatorIp: ip
2428
+ }, async (_, emitLog) => {
2429
+ const msg = ["安装任务执行中"];
2430
+ /** 插件目录 统一下载到这里方便管理 */
2431
+ const dir = path.join(karinPathPlugins, "karin-plugin-example");
2432
+ mkdirSync(dir);
2433
+ emitLog("开始下载插件文件...");
2434
+ await Promise.all(urls.map(async (app) => {
2435
+ const filename = path.basename(app.url);
2436
+ const fileUrl = path.join(dir, filename);
2437
+ emitLog(`正在下载: ${filename}`);
2438
+ const result = await downloadFile(app.url, fileUrl);
2439
+ if (!result.success) {
2440
+ let err = `${filename} 下载失败: `;
2441
+ if (result.data instanceof AxiosError) err += result.data.message;
2442
+ else if (result.data instanceof Error) err += result.data.message || result.data.stack || "未知错误";
2443
+ else err += String(result.data);
2444
+ logger.error(`[install] 下载app插件失败:\n url: ${app.url}\n message: ${err}`);
2445
+ msg.push(err);
2446
+ emitLog(err);
2447
+ return;
2448
+ }
2449
+ msg.push(`${app.url} 下载成功`);
2450
+ emitLog(`${app.url} 下载成功`);
2451
+ }));
2452
+ emitLog("安装完成");
2453
+ return true;
2454
+ }));
2455
+ };
2456
+
2457
+ //#endregion
2458
+ //#region src/server/plugins/admin/installCustom.ts
2459
+ /**
2460
+ * 自定义安装
2461
+ * @param res - 响应对象
2462
+ * @param data - 安装数据,包含插件名称、目标、类型等
2463
+ * @param ip - 操作者IP地址
2464
+ * @returns 操作响应
2465
+ */
2466
+ const installCustom = async (res, data, ip) => {
2467
+ if (data.pluginType === "app") return installApp(res, data, ip);
2468
+ if (data.pluginType === "npm") return installNpm(res, data, ip);
2469
+ if (data.pluginType === "git") return installGit(res, data, ip);
2470
+ };
2471
+ /**
2472
+ * 自定义安装NPM类型的插件
2473
+ * @param res - 响应对象
2474
+ * @param data - 安装数据,包含插件名称、目标、类型等
2475
+ * @param ip - 操作者IP地址
2476
+ * @returns 操作响应
2477
+ */
2478
+ const installNpm = async (res, data, ip) => {
2479
+ return handleReturn(res, true, "安装任务已创建,请通过taskId执行任务", await taskSystem.add({
2480
+ type: "install",
2481
+ name: data.name,
2482
+ target: data.target,
2483
+ operatorIp: ip
2484
+ }, async (_, emitLog) => {
2485
+ /** 包名 */
2486
+ let pkg = data.target;
2487
+ /** 自定义版本版本 */
2488
+ if (data.version) pkg += `@${data.version}`;
2489
+ const args = [
2490
+ "add",
2491
+ pkg,
2492
+ "--save"
2493
+ ];
2494
+ if (isWorkspace()) args.push("-w");
2495
+ if (data.registry) args.push(`--registry=${data.registry}`);
2496
+ if (Array.isArray(data.allowBuild) && data.allowBuild.length && isPnpm10()) data.allowBuild.forEach((pkg) => args.unshift(`--allow-build=${pkg}`));
2497
+ /** 处理 ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF 错误 */
2498
+ let IS_ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF = false;
2499
+ await spawnProcess("pnpm", args, {}, emitLog, () => {
2500
+ IS_ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF = true;
2501
+ });
2502
+ if (IS_ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF) {
2503
+ emitLog("检测到 ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF 错误,尝试修复...");
2504
+ emitLog("执行 pnpm install -f 强制重建模块目录");
2505
+ /** 先执行 pnpm install -f 强制重建模块目录 */
2506
+ await spawnProcess("pnpm", ["install", "-f"], {}, emitLog);
2507
+ emitLog("模块目录重建完成,重新尝试安装插件");
2508
+ /** 重新尝试安装 */
2509
+ await spawnProcess("pnpm", args, {}, emitLog);
2510
+ emitLog("安装完成,尝试加载插件");
2511
+ }
2512
+ await pkgHotReload("npm", data.target);
2513
+ return true;
2514
+ }));
2515
+ };
2516
+ /**
2517
+ * 自定义安装Git类型的插件
2518
+ * @param res - 响应对象
2519
+ * @param data - 安装数据,包含插件名称、目标、类型等
2520
+ * @param ip - 操作者IP地址
2521
+ * @returns 操作响应
2522
+ */
2523
+ const installGit = async (res, data, ip) => {
2524
+ const pkgName = data.target || path.basename(data.repo).replace(".git", "");
2525
+ if (!pkgName.startsWith("karin-plugin-")) return handleReturn(res, false, "插件名称必须以karin-plugin-开头");
2526
+ return handleReturn(res, true, "安装任务已创建,请通过taskId执行任务", await taskSystem.add({
2527
+ type: "install",
2528
+ name: data.name,
2529
+ target: data.target,
2530
+ operatorIp: ip
2531
+ }, async (_, emitLog) => {
2532
+ await spawnProcess("git", [
2533
+ data.branch ? `-b ${data.branch}` : "",
2534
+ "clone",
2535
+ "--depth=1",
2536
+ data.repo,
2537
+ `./plugins/${pkgName}`
2538
+ ], {}, emitLog);
2539
+ await pkgHotReload("git", pkgName);
2540
+ return true;
2541
+ }));
2542
+ };
2543
+ /**
2544
+ * 自定义安装app类型的插件
2545
+ * @param res - 响应对象
2546
+ * @param data - 安装数据,包含插件名称、目标、类型等
2547
+ * @param ip - 操作者IP地址
2548
+ * @returns 操作响应
2549
+ */
2550
+ const installApp = async (res, data, ip) => {
2551
+ if (!data.jsUrl) return handleReturn(res, false, "jsUrl不能为空");
2552
+ /** 下载后的文件名称 */
2553
+ let filename = data.target || path.basename(data.jsUrl);
2554
+ if (!filename.endsWith(".js")) filename += ".js";
2555
+ return handleReturn(res, true, "安装任务已创建,请通过taskId执行任务", await taskSystem.add({
2556
+ type: "install",
2557
+ name: data.name,
2558
+ target: data.target,
2559
+ operatorIp: ip
2560
+ }, async (_, emitLog) => {
2561
+ /** 插件目录 统一下载到这里方便管理 */
2562
+ const dir = path.join(karinPathPlugins, "karin-plugin-example");
2563
+ const fileUrl = path.join(dir, filename);
2564
+ mkdirSync(dir);
2565
+ emitLog(`开始下载插件文件: ${filename}`);
2566
+ const result = await downloadFile(data.jsUrl, fileUrl);
2567
+ if (!result.success) {
2568
+ let msg = "app插件下载失败: ";
2569
+ if (result.data instanceof AxiosError) msg += result.data.message;
2570
+ else if (result.data instanceof Error) msg += result.data.message || result.data.stack || "未知错误";
2571
+ else msg += String(result.data);
2572
+ logger.error(`[install] 下载app插件失败:\n url: ${data.jsUrl}\n message: ${msg}`);
2573
+ emitLog(msg);
2574
+ return false;
2575
+ }
2576
+ emitLog(`下载完成: ${data.jsUrl}`);
2577
+ return true;
2578
+ }));
2579
+ };
2580
+
2581
+ //#endregion
2582
+ //#region src/server/plugins/admin/install.ts
2583
+ /**
2584
+ * 安装插件
2585
+ * @param res - 响应对象
2586
+ * @param data - 更新数据,包含插件名称、目标、类型等
2587
+ * @param ip - 操作者IP地址
2588
+ * @returns 操作响应
2589
+ */
2590
+ const install = async (res, data, ip = "0.0.0.0") => {
2591
+ if (!validatePluginRequest(res, data.name, data.target, data.pluginType, [
2592
+ "npm",
2593
+ "git",
2594
+ "app"
2595
+ ])) return;
2596
+ if (data.source === "market") return installMarket(res, data, ip);
2597
+ if (data.source === "custom") return installCustom(res, data, ip);
2598
+ return handleReturn(res, false, "无效的安装来源");
2599
+ };
2600
+
2601
+ //#endregion
2602
+ //#region src/server/plugins/admin/uninstall.ts
2603
+ /**
2604
+ * 卸载插件
2605
+ *
2606
+ * 根据插件类型执行不同的卸载操作:
2607
+ * - npm: 使用pnpm remove命令卸载npm包
2608
+ * - git: 删除插件目录
2609
+ * - app: 删除特定的应用文件
2610
+ *
2611
+ * 过程:
2612
+ * 1. 验证请求参数
2613
+ * 2. 检查插件是否存在
2614
+ * 3. 创建并执行卸载任务
2615
+ * 4. 返回操作结果
2616
+ *
2617
+ * @param res - 响应对象
2618
+ * @param name - 插件名称
2619
+ * @param target - 插件目标
2620
+ * @param pluginType - 插件类型 (npm/git/app)
2621
+ * @param operatorIp - 操作者IP地址
2622
+ * @returns 操作响应
2623
+ */
2624
+ const uninstall = async (res, name, target, operatorIp = "0.0.0.0") => {
2625
+ if (!Array.isArray(target) || target.length < 1) return handleReturn(res, false, "无效请求: 插件目标错误");
2626
+ /**
2627
+ * 执行不同类型插件的卸载操作
2628
+ *
2629
+ * 根据插件类型选择适当的卸载方法:
2630
+ * - npm包:使用pnpm remove
2631
+ * - git仓库:删除整个目录
2632
+ * - app应用:删除特定文件
2633
+ *
2634
+ * @returns 操作结果对象
2635
+ */
2636
+ const performUninstall = async (emitLog) => {
2637
+ const npm = [];
2638
+ const git = [];
2639
+ const app = [];
2640
+ /** 记录不存在的插件 */
2641
+ const notExist = [];
2642
+ /** 本地已安装插件列表 */
2643
+ const list = await getPlugins("all");
2644
+ target.forEach(async (v) => {
2645
+ if (v.type === "npm") {
2646
+ list.includes(`${v.type}:${v.name}`) ? npm.push(v.name) : notExist.push(v.name);
2647
+ return;
2648
+ }
2649
+ if (v.type === "git") {
2650
+ list.includes(`${v.type}:${v.name}`) ? git.push(v.name) : notExist.push(v.name);
2651
+ return;
2652
+ }
2653
+ if (v.type === "app") {
2654
+ /** 等下直接判断路径更快 因为还需要判断路径穿越的问题 */
2655
+ app.push(v.name);
2656
+ return;
2657
+ }
2658
+ notExist.push(v.name);
2659
+ });
2660
+ await spawnProcess("pnpm", ["remove", ...npm], { timeout: 60 * 1e3 }, emitLog);
2661
+ /** 执行完成之后 检查package.json是否还存在这些依赖 */
2662
+ const pkg = path.join(process.cwd(), "package.json");
2663
+ if (fs.existsSync(pkg)) {
2664
+ const content = fs.readFileSync(pkg, "utf-8");
2665
+ const data = JSON.parse(content);
2666
+ const delDep = (obj) => {
2667
+ Object.keys(obj).forEach((key) => {
2668
+ if (npm.includes(key)) delete obj[key];
2669
+ });
2670
+ };
2671
+ delDep(data.dependencies);
2672
+ delDep(data.devDependencies);
2673
+ delDep(data.peerDependencies);
2674
+ fs.writeFileSync(pkg, JSON.stringify(data, null, 2), "utf-8");
2675
+ }
2676
+ /** tips: 不要使用异步 */
2677
+ for (const v of git) {
2678
+ emitLog("-----------------------");
2679
+ emitLog(`开始卸载 git 插件: ${v}`);
2680
+ if (v.includes("..")) {
2681
+ emitLog(`卸载 ${v} 失败: 文件名称存在路径穿越风险`);
2682
+ continue;
2683
+ }
2684
+ /** 判断git文件夹是否存在 */
2685
+ const dir = path.join(karinPathPlugins, v);
2686
+ if (!fs.existsSync(path.join(dir, ".git"))) {
2687
+ emitLog(`卸载 ${v} 失败: 非git仓库`);
2688
+ continue;
2689
+ }
2690
+ try {
2691
+ await fs.promises.rm(dir, {
2692
+ recursive: true,
2693
+ force: true
2694
+ });
2695
+ emitLog(`卸载 ${v} 成功`);
2696
+ } catch (error) {
2697
+ emitLog(`卸载 ${v} 失败: ${error.message}`);
2698
+ }
2699
+ emitLog("-----------------------\n\n");
2700
+ }
2701
+ for (const v of app) {
2702
+ emitLog("-----------------------");
2703
+ emitLog(`开始卸载 app 插件: ${v}`);
2704
+ if (v.includes("..")) {
2705
+ emitLog(`卸载 ${v} 失败: 文件名称存在路径穿越风险`);
2706
+ continue;
2707
+ }
2708
+ /** 判断app文件夹是否存在 */
2709
+ const arr = v.split("/");
2710
+ const [pkg, file] = arr;
2711
+ if (arr.length !== 2 || !pkg || !file) {
2712
+ emitLog(`卸载 ${v} 失败: 格式错误`);
2713
+ continue;
2714
+ }
2715
+ if (!list.includes(`app:${pkg}`)) {
2716
+ emitLog(`卸载 ${v} 失败: 插件不存在`);
2717
+ continue;
2718
+ }
2719
+ const dir = path.join(karinPathPlugins, pkg);
2720
+ if (!fs.existsSync(path.join(dir, file))) {
2721
+ emitLog(`卸载 ${v} 失败: 文件不存在`);
2722
+ continue;
2723
+ }
2724
+ /** 判断后缀 必须是.js .mjs .cjs .ts .cts .mts */
2725
+ const ext = path.extname(file);
2726
+ if (![
2727
+ ".js",
2728
+ ".mjs",
2729
+ ".cjs",
2730
+ ".ts",
2731
+ ".cts",
2732
+ ".mts"
2733
+ ].includes(ext)) {
2734
+ emitLog(`卸载 ${v} 失败: 错误的文件类型`);
2735
+ continue;
2736
+ }
2737
+ try {
2738
+ /** 这里是文件 不能使用rm函数 */
2739
+ await fs.promises.unlink(path.join(dir, file));
2740
+ emitLog(`卸载 ${v} 成功`);
2741
+ } catch (error) {
2742
+ emitLog(`卸载 ${v} 失败: ${error.message}`);
2743
+ }
2744
+ emitLog("-----------------------\n\n");
2745
+ }
2746
+ if (notExist.length) {
2747
+ notExist.unshift("以下插件不存在:");
2748
+ emitLog(notExist.join("\n") + "\n\n");
2749
+ }
2750
+ emitLog("卸载任务完成");
2751
+ };
2752
+ return handleReturn(res, true, "卸载任务已创建,请通过taskId执行任务", await taskSystem.add({
2753
+ type: "uninstall",
2754
+ name,
2755
+ target: target.map((v) => `${v.type}:${v.name}`).join(","),
2756
+ operatorIp
2757
+ }, async (options, emitLog) => {
2758
+ try {
2759
+ await performUninstall(emitLog);
2760
+ await taskSystem.update.logs(options.id, "任务执行成功");
2761
+ return true;
2762
+ } catch (error) {
2763
+ await taskSystem.update.logs(options.id, `任务执行失败: ${error.message}`);
2764
+ return false;
2765
+ }
2766
+ }));
2767
+ };
2768
+
2769
+ //#endregion
2770
+ //#region src/server/plugins/admin/router.ts
2771
+ /**
2772
+ * 插件管理路由
2773
+ *
2774
+ * 处理插件相关的API请求,包括:
2775
+ * - 更新插件:更新已安装的插件到最新版本
2776
+ * - 卸载插件:移除指定的插件
2777
+ * - 安装插件:安装新的插件
2778
+ *
2779
+ * @param req - 请求对象,包含插件操作类型和相关参数
2780
+ * @param res - 响应对象,用于返回操作结果
2781
+ * @returns 响应结果
2782
+ */
2783
+ const pluginAdminRouter = (req, res) => {
2784
+ if (req.body.type === "uninstall")
2785
+ /**
2786
+ * @description 此处的所有api都必须给予响应并且符合以下格式
2787
+ * @example
2788
+ * ```json
2789
+ * {
2790
+ * "success": true,
2791
+ * "message": "卸载成功"
2792
+ * }
2793
+ *
2794
+ * {
2795
+ * "success": false,
2796
+ * "message": "卸载失败"
2797
+ * }
2798
+ * ```
2799
+ */
2800
+ return uninstall(res, req.body.name, req.body.target, req.ip);
2801
+ if (req.body.type === "update")
2802
+ /**
2803
+ * @description 此处的所有api都必须给予响应并且符合以下格式
2804
+ * @example
2805
+ * ```json
2806
+ * {
2807
+ * "success": true,
2808
+ * "message": "更新任务已创建",
2809
+ * "taskId": "1234567890"
2810
+ * }
2811
+ *
2812
+ * {
2813
+ * "success": false,
2814
+ * "message": "更新失败"
2815
+ * }
2816
+ * ```
2817
+ */
2818
+ return update(res, req.body, req.ip);
2819
+ if (req.body.type !== "install") return handleReturn(res, false, "无效请求: 插件类型错误");
2820
+ /**
2821
+ * @description 此处的所有api都必须给予响应并且符合以下格式
2822
+ * @example
2823
+ * ```json
2824
+ * {
2825
+ * "success": true,
2826
+ * "message": "安装成功"
2827
+ * }
2828
+ *
2829
+ * {
2830
+ * "success": false,
2831
+ * "message": "安装失败"
2832
+ * }
2833
+ *
2834
+ * {
2835
+ * "success": true,
2836
+ * "message": "安装任务已创建",
2837
+ * "taskId": "1234567890"
2838
+ * }
2839
+ * ```
2840
+ */
2841
+ return install(res, req.body, req.ip);
2842
+ };
2843
+
2844
+ //#endregion
2845
+ //#region src/utils/npm/registry.ts
2846
+ /**
2847
+ * 获取registry
2848
+ */
2849
+ const getRegistry = async () => {
2850
+ if (process.env.npm_config_registry) return process.env.npm_config_registry;
2851
+ const registry = await exec$3("npm config get registry");
2852
+ process.env.npm_config_registry = registry.stdout;
2853
+ return registry.stdout;
2854
+ };
2855
+ /**
2856
+ * 获取一个npm包registry配置
2857
+ * @param name - 包名
2858
+ * @returns registry
2859
+ */
2860
+ const getNpmRegistry = async (name) => {
2861
+ /** 获取npm源 */
2862
+ const registry = await getRegistry();
2863
+ return (await axios.get(`${registry}/${name}`)).data;
2864
+ };
2865
+ /**
2866
+ * 获取一个npm包的最新版本
2867
+ * @param name - 包名
2868
+ * @returns 最新版本
2869
+ */
2870
+ const getNpmLatestVersion = async (name) => {
2871
+ try {
2872
+ return (await getNpmRegistry(name))["dist-tags"].latest;
2873
+ } catch (error) {
2874
+ logger.debug(new Error(`获取${name}最新版本失败`, { cause: error }));
2875
+ return null;
2876
+ }
2877
+ };
2878
+ /** 初始化一下 防止并发 */
2879
+ getRegistry();
2880
+
2881
+ //#endregion
2882
+ //#region src/server/dependencies/list.ts
2883
+ /**
2884
+ * 获取项目依赖列表
2885
+ */
2886
+ const getDependenciesListRouter = async (req, res) => {
2887
+ try {
2888
+ const cache = await getCache$1(req, res);
2889
+ if (cache) return createSuccessResponse(res, cache);
2890
+ const { stdout, error } = await exec$3("pnpm list --depth=0 --json");
2891
+ if (error) return createBadRequestResponse(res, error.message);
2892
+ /** 并发请求 */
2893
+ const promises = [];
2894
+ /** 返回给前端的依赖列表 */
2895
+ let list = [];
2896
+ /** package.json */
2897
+ const pkg = requireFileSync("./package.json", { force: true });
2898
+ /** 当前依赖列表 */
2899
+ const dependencies = JSON.parse(stdout);
2900
+ /** 遍历当前依赖列表 转换格式 */
2901
+ for (const dependency of dependencies) Object.entries(dependency).forEach(([key, value]) => {
2902
+ if (typeof value !== "object") return;
2903
+ Object.entries(value).forEach(([k, v]) => {
2904
+ promises.push(getDependenciesInfo(pkg, list, k, v, key));
2905
+ });
2906
+ });
2907
+ await Promise.allSettled(promises);
2908
+ const npmPlugin = await getPlugins("npm");
2909
+ list.forEach((item) => {
2910
+ item.isKarinPlugin = npmPlugin.some((plugin) => plugin === `npm:${item.name}`);
2911
+ });
2912
+ /**
2913
+ * 依赖类型权重映射
2914
+ * 权重越小优先级越高
2915
+ */
2916
+ const typeWeightMap = {
2917
+ dependencies: 1,
2918
+ devDependencies: 2,
2919
+ peerDependencies: 3,
2920
+ optionalDependencies: 4,
2921
+ unsavedDependencies: 5
2922
+ };
2923
+ list = lodash.sortBy(list, [
2924
+ (item) => !(item.name === "node-karin"),
2925
+ "isKarinPlugin",
2926
+ (item) => typeWeightMap[item.type],
2927
+ "name"
2928
+ ]);
2929
+ await setCache$1(list);
2930
+ return createSuccessResponse(res, list);
2931
+ } catch (error) {
2932
+ logger.error("[getDependenciesListRouter]", error);
2933
+ return createServerErrorResponse(res, error instanceof Error ? error.message : String(error));
2934
+ }
2935
+ };
2936
+ /**
2937
+ * 获取依赖信息
2938
+ * @param pkg - 项目package.json
2939
+ * @param list - 依赖列表
2940
+ * @param key - 依赖名称
2941
+ * @param value - 依赖信息
2942
+ * @param type - 依赖类型
2943
+ */
2944
+ const getDependenciesInfo = async (pkg, list, key, value, type) => {
2945
+ if (!key || !value?.version) return;
2946
+ try {
2947
+ /** 获取版本列表 */
2948
+ const registry = await getNpmRegistry(value.from);
2949
+ /** 获取最新的15个版本 数组最后就是最新的版本 不足15个就返回全部 */
2950
+ const latest = Object.keys(registry.versions).slice(-15).filter(Boolean);
2951
+ let packageValue = "";
2952
+ /** type以pkg中的为准 */
2953
+ if (pkg.dependencies?.[key]) {
2954
+ type = "dependencies";
2955
+ packageValue = pkg.dependencies?.[key];
2956
+ } else if (pkg.devDependencies?.[key]) {
2957
+ type = "devDependencies";
2958
+ packageValue = pkg.devDependencies?.[key];
2959
+ } else if (pkg.peerDependencies?.[key]) {
2960
+ type = "peerDependencies";
2961
+ packageValue = pkg.peerDependencies?.[key];
2962
+ } else if (pkg.optionalDependencies?.[key]) {
2963
+ type = "optionalDependencies";
2964
+ packageValue = pkg.optionalDependencies?.[key];
2965
+ }
2966
+ list.push({
2967
+ name: key,
2968
+ type,
2969
+ from: value.from,
2970
+ current: value.version,
2971
+ isKarinPlugin: false,
2972
+ latest,
2973
+ packageValue
2974
+ });
2975
+ } catch (error) {
2976
+ logger.debug(`[getDependenciesInfo] 获取${key}的版本信息失败`, error);
2977
+ }
2978
+ };
2979
+ /**
2980
+ * 获取缓存
2981
+ * @param req - 请求
2982
+ * @param res - 响应
2983
+ */
2984
+ const getCache$1 = async (req, _) => {
2985
+ if (req.body?.force) return null;
2986
+ const { redis } = await import("./redis-aLJ7wbJH.mjs").then((n) => n.r);
2987
+ const cache = await redis.get(REDIS_DEPENDENCIES_LIST_CACHE_KEY);
2988
+ if (cache) {
2989
+ const data = JSON.parse(cache);
2990
+ if (!Array.isArray(data)) {
2991
+ await redis.del(REDIS_DEPENDENCIES_LIST_CACHE_KEY);
2992
+ return null;
2993
+ }
2994
+ return data;
2995
+ }
2996
+ return null;
2997
+ };
2998
+ /**
2999
+ * 设置缓存
3000
+ * @param data - 依赖列表
3001
+ */
3002
+ const setCache$1 = async (data) => {
3003
+ const { redis } = await import("./redis-aLJ7wbJH.mjs").then((n) => n.r);
3004
+ await redis.set(REDIS_DEPENDENCIES_LIST_CACHE_KEY, JSON.stringify(data), { EX: REDIS_DEPENDENCIES_LIST_CACHE_EXPIRE });
3005
+ };
3006
+
3007
+ //#endregion
3008
+ //#region src/server/dependencies/manage.ts
3009
+ /**
3010
+ * 依赖管理路由
3011
+ *
3012
+ * 处理依赖相关的API请求,包括:
3013
+ * - 安装依赖:安装指定版本的依赖
3014
+ * - 删除依赖:删除指定的依赖
3015
+ *
3016
+ * @param req - 请求对象,包含操作类型和依赖参数
3017
+ * @param res - 响应对象,用于返回操作结果
3018
+ * @returns 响应结果
3019
+ */
3020
+ const manageDependenciesRouter = async (req, res) => {
3021
+ const { type, data } = req.body;
3022
+ if (type === "add")
3023
+ /**
3024
+ * @description 此处的响应格式符合以下格式
3025
+ * @example
3026
+ * ```json
3027
+ * {
3028
+ * "success": true,
3029
+ * "message": "添加任务已创建",
3030
+ * "taskId": "1234567890"
3031
+ * }
3032
+ *
3033
+ * {
3034
+ * "success": false,
3035
+ * "message": "添加失败"
3036
+ * }
3037
+ * ```
3038
+ */
3039
+ return await addDependencies(res, data, req.ip);
3040
+ if (type === "upgrade")
3041
+ /**
3042
+ * @description 此处的响应格式符合以下格式
3043
+ * @example
3044
+ * ```json
3045
+ * {
3046
+ * "success": true,
3047
+ * "message": "安装任务已创建",
3048
+ * "taskId": "1234567890"
3049
+ * }
3050
+ *
3051
+ * {
3052
+ * "success": false,
3053
+ * "message": "安装失败"
3054
+ * }
3055
+ * ```
3056
+ */
3057
+ return await installDependencies(res, data, req.ip);
3058
+ if (type === "remove")
3059
+ /**
3060
+ * @description 此处的响应格式符合以下格式
3061
+ * @example
3062
+ * ```json
3063
+ * {
3064
+ * "success": true,
3065
+ * "message": "删除任务已创建",
3066
+ * "taskId": "1234567890"
3067
+ * }
3068
+ *
3069
+ * {
3070
+ * "success": false,
3071
+ * "message": "删除失败"
3072
+ * }
3073
+ * ```
3074
+ */
3075
+ return await removeDependencies(res, data, req.ip);
3076
+ return handleReturn(res, false, "无效请求:不支持的操作类型");
3077
+ };
3078
+ /**
3079
+ * 安装依赖
3080
+ *
3081
+ * @param res - 响应对象
3082
+ * @param dependencies - 依赖列表,包含名称和版本
3083
+ * @param ip - 操作者IP地址
3084
+ * @returns 操作响应
3085
+ */
3086
+ const installDependencies = async (res, dependencies, ip) => {
3087
+ try {
3088
+ const packagesToInstall = dependencies.map((dep) => `${dep.name}@${dep.version || "latest"}`).join(" ");
3089
+ return handleReturn(res, true, "安装任务已创建", await taskSystem.add({
3090
+ type: "install-dependencies",
3091
+ name: "安装依赖",
3092
+ target: packagesToInstall,
3093
+ operatorIp: ip
3094
+ }, async (_, emitLog) => {
3095
+ const args = ["install", ...packagesToInstall.split(" ")];
3096
+ if (isWorkspace()) args.push("-w");
3097
+ await spawnProcess("pnpm", args, {}, emitLog);
3098
+ logger.mark(`安装依赖 ${logger.green(packagesToInstall)} 完成`);
3099
+ return true;
3100
+ }));
3101
+ } catch (error) {
3102
+ logger.error("[installDependencies]", error);
3103
+ logger.mark(`安装依赖 ${logger.red(dependencies.join(" "))} 失败`);
3104
+ return handleReturn(res, false, `安装失败: ${error instanceof Error ? error.message : String(error)}`);
3105
+ }
3106
+ };
3107
+ /**
3108
+ * 删除依赖
3109
+ *
3110
+ * @param res - 响应对象
3111
+ * @param dependencies - 依赖列表
3112
+ * @param ip - 操作者IP地址
3113
+ * @returns 操作响应
3114
+ */
3115
+ const removeDependencies = async (res, dependencies, ip) => {
3116
+ try {
3117
+ const packagesToRemove = dependencies.join(" ");
3118
+ return handleReturn(res, true, "删除任务已创建", await taskSystem.add({
3119
+ type: "remove-dependencies",
3120
+ name: "删除依赖",
3121
+ target: packagesToRemove,
3122
+ operatorIp: ip
3123
+ }, async (_, emitLog) => {
3124
+ const args = ["remove", ...packagesToRemove.split(" ")];
3125
+ if (isWorkspace()) args.push("-w");
3126
+ await spawnProcess("pnpm", args, {}, emitLog);
3127
+ logger.mark(`删除依赖 ${logger.yellow(packagesToRemove)} 完成`);
3128
+ return true;
3129
+ }));
3130
+ } catch (error) {
3131
+ logger.error("[removeDependencies]", error);
3132
+ logger.mark(`删除依赖 ${logger.red(dependencies.join(" "))} 失败`);
3133
+ return handleReturn(res, false, `删除失败: ${error instanceof Error ? error.message : String(error)}`);
3134
+ }
3135
+ };
3136
+ /**
3137
+ * 添加依赖
3138
+ *
3139
+ * @param res - 响应对象
3140
+ * @param dependencies - 依赖列表
3141
+ * @param ip - 操作者IP地址
3142
+ */
3143
+ const addDependencies = async (res, dependencies, ip) => {
3144
+ try {
3145
+ if (!dependencies.name || !dependencies.location) return handleReturn(res, false, "无效请求:缺少必要参数");
3146
+ const name = `${dependencies.name}@${dependencies.version || "latest"}`;
3147
+ return handleReturn(res, true, "添加任务已创建", await taskSystem.add({
3148
+ type: "add-dependencies",
3149
+ name: "依赖新增",
3150
+ target: name,
3151
+ operatorIp: ip
3152
+ }, async (_, emitLog) => {
3153
+ const args = ["add", name];
3154
+ if (Array.isArray(dependencies.allowBuild) && dependencies.allowBuild.length && isPnpm10()) dependencies.allowBuild.forEach((pkg) => args.unshift(`--allow-build=${pkg}`));
3155
+ if (dependencies.location === "devDependencies") args.push("-D");
3156
+ else if (dependencies.location === "optionalDependencies") args.push("-O");
3157
+ if (isWorkspace()) args.push("-w");
3158
+ await spawnProcess("pnpm", args, {}, emitLog);
3159
+ /**
3160
+ * @version 1.9.9
3161
+ * 如果新增的依赖存在对等依赖 需要再执行一次pnpm install
3162
+ */
3163
+ const depDir = path.join(process.cwd(), "packages", dependencies.name, "package.json");
3164
+ try {
3165
+ if (!JSON.parse(fs.readFileSync(depDir, "utf-8")).peerDependencies) return true;
3166
+ await spawnProcess("pnpm", ["install"], {}, emitLog);
3167
+ } catch (error) {
3168
+ logger.error("[addDependencies]", error);
3169
+ emitLog(util.format(error));
3170
+ }
3171
+ logger.mark(`新增依赖 ${logger.green(dependencies.name)} 完成`);
3172
+ return true;
3173
+ }));
3174
+ } catch (error) {
3175
+ logger.error("[addDependencies]", error);
3176
+ logger.mark(`新增依赖 ${logger.red(dependencies.name)} 失败`);
3177
+ return handleReturn(res, false, `添加失败: ${error instanceof Error ? error.message : String(error)}`);
3178
+ }
3179
+ };
3180
+
3181
+ //#endregion
3182
+ //#region src/server/task/list.ts
3183
+ const taskStatusMap = {
3184
+ pending: "待执行(pending)",
3185
+ running: "执行中(running)",
3186
+ success: "成功(success)",
3187
+ failed: "失败(failed)",
3188
+ canceled: "已取消(canceled)",
3189
+ timeout: "超时(timeout)"
3190
+ };
3191
+ /** 活跃的SSE连接计数 */
3192
+ const activeConnections = /* @__PURE__ */ new Map();
3193
+ /** 最大允许的同时连接数 */
3194
+ const MAX_CONNECTIONS = 3;
3195
+ /**
3196
+ * 获取任务列表
3197
+ */
3198
+ const taskListRouter = async (_req, res) => {
3199
+ createSuccessResponse(res, await taskSystem.list());
3200
+ };
3201
+ /**
3202
+ * 执行任务
3203
+ * 返回SSE长连接,实时推送任务执行日志和状态
3204
+ */
3205
+ const taskRunRouter = async (req, res) => {
3206
+ const id = req.query.id;
3207
+ const lastIndex = req.query.lastIndex;
3208
+ if (typeof id !== "string" || id.length === 0) return createServerErrorResponse(res, "id 为空");
3209
+ const task = await taskSystem.get(id);
3210
+ if (!task) return createServerErrorResponse(res, "任务不存在");
3211
+ /** 获取当前活跃连接总数 */
3212
+ const totalConnections = [...activeConnections.values()].reduce((sum, connection) => sum + connection.count, 0);
3213
+ /** 如果不是断线重连并且超过最大连接数,则拒绝新连接 */
3214
+ if (!lastIndex && totalConnections >= MAX_CONNECTIONS) return createServerErrorResponse(res, `服务器负载过高,当前连接数已达上限(${MAX_CONNECTIONS}),请稍后重试`);
3215
+ /** 检查任务是否存在 */
3216
+ if (await taskSystem.exists(task.type, task.target, ["running"])) return createServerErrorResponse(res, "已有相同任务正在执行,请勿重复创建任务...");
3217
+ try {
3218
+ /** 设置SSE响应头 */
3219
+ res.setHeader("Content-Type", "text/event-stream");
3220
+ /** 设置缓存为不缓存 */
3221
+ res.setHeader("Cache-Control", "no-cache");
3222
+ /** 设置连接为长连接 */
3223
+ res.setHeader("Connection", "keep-alive");
3224
+ /** 立即发送响应头 */
3225
+ res.flushHeaders();
3226
+ /** 获取或初始化任务连接信息 */
3227
+ if (!activeConnections.has(id)) activeConnections.set(id, {
3228
+ count: 0,
3229
+ logs: [],
3230
+ status: task.status
3231
+ });
3232
+ /** 更新连接计数 */
3233
+ const connectionInfo = activeConnections.get(id);
3234
+ connectionInfo.count++;
3235
+ /** 如果是重连请求,先发送之前缓存的日志 */
3236
+ if (lastIndex && connectionInfo.logs.length > 0) {
3237
+ const lastPos = parseInt(lastIndex, 10) || 0;
3238
+ if (lastPos < connectionInfo.logs.length) connectionInfo.logs.slice(lastPos).forEach((log) => res.write(`data: ${log}\n\n`));
3239
+ } else res.write("data: 任务创建成功: 开始执行...\n\n");
3240
+ /** sse是否处于运行状态 */
3241
+ let sseOpen = true;
3242
+ req.on("close", () => {
3243
+ sseOpen = false;
3244
+ /** 更新连接计数 */
3245
+ if (activeConnections.has(id)) {
3246
+ const info = activeConnections.get(id);
3247
+ info.count = Math.max(0, info.count - 1);
3248
+ /** 如果没有活跃连接且任务已完成,清理缓存 */
3249
+ if (info.count === 0 && info.status !== "running" && info.status !== "pending") activeConnections.delete(id);
3250
+ }
3251
+ logger.debug(`[task][${id}] 客户端断开连接,当前连接数: ${activeConnections.get(id)?.count || 0}`);
3252
+ });
3253
+ taskSystem.run(id, (log) => {
3254
+ /** 缓存日志 */
3255
+ if (activeConnections.has(id)) activeConnections.get(id).logs.push(log);
3256
+ /** 发送日志到客户端 */
3257
+ sseOpen && res.write(`data: ${log}\n\n`);
3258
+ }, (status) => {
3259
+ const tips = taskStatusMap[status];
3260
+ logger.debug(`[task][${id}] 状态变更: ${tips}`);
3261
+ /** 更新任务状态 */
3262
+ if (activeConnections.has(id)) activeConnections.get(id).status = status;
3263
+ if (!sseOpen) {
3264
+ logger.debug(`[task][${id}] sse已关闭 停止发送日志`);
3265
+ return;
3266
+ }
3267
+ res.write(`data: 任务状态变更: ${tips}\n\n`);
3268
+ if (status !== "running" && status !== "pending") {
3269
+ res.write("data: 任务执行完成,结束连接\n\n");
3270
+ res.end("data: end\n\n");
3271
+ /** 如果没有活跃连接,清理缓存 */
3272
+ if (activeConnections.has(id) && activeConnections.get(id).count <= 1) activeConnections.delete(id);
3273
+ }
3274
+ });
3275
+ } catch (error) {
3276
+ /** 更新连接计数 */
3277
+ if (activeConnections.has(id)) activeConnections.get(id).count = Math.max(0, activeConnections.get(id).count - 1);
3278
+ if (res.headersSent) {
3279
+ res.write(`data: ${error instanceof Error ? error.message : String(error)} \n\n`);
3280
+ res.end("data: end\n\n");
3281
+ return;
3282
+ }
3283
+ createServerErrorResponse(res, error instanceof Error ? error.message : String(error));
3284
+ }
3285
+ };
3286
+ /**
3287
+ * 获取任务日志
3288
+ * 根据任务状态提供不同的响应:
3289
+ * - 任务运行中: 返回SSE长连接实时获取日志
3290
+ * - 任务已完成: 直接返回完整日志
3291
+ */
3292
+ const taskLogsRouter = async (req, res) => {
3293
+ const { id } = req.params;
3294
+ if (!id || typeof id !== "string" || id.length === 0) return createServerErrorResponse(res, "任务ID不能为空");
3295
+ try {
3296
+ const task = await taskSystem.get(id);
3297
+ if (!task) return createServerErrorResponse(res, "任务不存在");
3298
+ /** 任务非运行中或非待执行,直接返回日志 */
3299
+ if (task.status === "running" || task.status === "pending") return createSuccessResponse(res, { logs: await taskSystem.logs(id) || "" });
3300
+ /** 设置SSE响应头 */
3301
+ res.setHeader("Content-Type", "text/event-stream");
3302
+ res.setHeader("Cache-Control", "no-cache");
3303
+ res.setHeader("Connection", "keep-alive");
3304
+ res.flushHeaders();
3305
+ /** sse是否处于运行状态 */
3306
+ let sseOpen = true;
3307
+ let timer = null;
3308
+ req.on("close", () => {
3309
+ sseOpen = false;
3310
+ timer && clearInterval(timer);
3311
+ logger.debug(`[task][${id}] sse已关闭 停止发送日志`);
3312
+ });
3313
+ res.write(`data: ${[
3314
+ `当前状态: ${taskStatusMap[task.status]}`,
3315
+ `ID: ${task.id}`,
3316
+ `名称: ${task.name}`,
3317
+ `类型: ${task.type}`,
3318
+ `目标: ${task.target}`,
3319
+ `创建时间: ${task.createTime}`,
3320
+ `更新时间: ${task.updateTime}`
3321
+ ].join("\n")} \n\n`);
3322
+ timer = setInterval(async () => {
3323
+ try {
3324
+ const info = await taskSystem.get(id);
3325
+ if (!info) {
3326
+ timer && clearInterval(timer);
3327
+ sseOpen && res.write("data: 任务不存在,无法获取任务详情 \n\n");
3328
+ return res.end("data: end\n\n");
3329
+ }
3330
+ /** 如果状态发生变化 则说明任务已结束 */
3331
+ if (info.status !== task.status) {
3332
+ info.logs.split("\n").forEach((log) => res.write(`data: ${log} \n\n`));
3333
+ timer && clearInterval(timer);
3334
+ return res.end("data: end\n\n");
3335
+ }
3336
+ sseOpen && res.write("data: 任务执行中,请耐心等待... \n\n");
3337
+ } catch (error) {
3338
+ logger.error(new Error("sse获取日志失败", { cause: error }));
3339
+ timer && clearInterval(timer);
3340
+ sseOpen && res.write("data: 获取日志失败 \n\n");
3341
+ return res.end("data: end\n\n");
3342
+ }
3343
+ }, 1e3);
3344
+ } catch (error) {
3345
+ logger.error(new Error("sse获取日志失败", { cause: error }));
3346
+ createServerErrorResponse(res, error instanceof Error ? error.message : String(error));
3347
+ }
3348
+ };
3349
+ /**
3350
+ * 删除任务记录
3351
+ */
3352
+ const taskDeleteRouter = async (req, res) => {
3353
+ const { id } = req.params;
3354
+ if (!id || typeof id !== "string" || id.length === 0) return createServerErrorResponse(res, "任务ID不能为空");
3355
+ try {
3356
+ await taskSystem.delete(id);
3357
+ createSuccessResponse(res, "任务记录删除成功");
3358
+ } catch (error) {
3359
+ createServerErrorResponse(res, error instanceof Error ? error.message : String(error));
3360
+ }
3361
+ };
3362
+
3363
+ //#endregion
3364
+ //#region src/utils/npm/npmrc.ts
3365
+ /**
3366
+ * 获取 .npmrc 文件路径
3367
+ * @param type - 类型 global 全局 project 项目
3368
+ * @param isCheck - 是否校验文件存在 不存在返回null
3369
+ */
3370
+ const getNpmrcPath = async (type, isCheck) => {
3371
+ let dir = "";
3372
+ if (type === "global") {
3373
+ const { stdout } = await exec$3("npm config get userconfig");
3374
+ dir = stdout.trim();
3375
+ } else dir = join(process.cwd(), ".npmrc");
3376
+ if (isCheck && !existsSync(dir)) return null;
3377
+ return dir;
3378
+ };
3379
+ /**
3380
+ * 获取 pnpm 配置文件的路径
3381
+ * @param isCheck - 是否校验文件存在 不存在返回null
3382
+ * - docs: {@link https://pnpm.io/zh/cli/config}
3383
+ */
3384
+ const getPnpmConfigPath = (isCheck) => {
3385
+ let dir = "";
3386
+ if (process.env.XDG_CONFIG_HOME) dir = join(process.env.XDG_CONFIG_HOME, "pnpm", "rc");
3387
+ const homeDir = homedir();
3388
+ const platform = process.platform;
3389
+ if (platform === "win32") dir = join(homeDir, "AppData", "Local", "pnpm", "config", "rc");
3390
+ else if (platform === "darwin") dir = join(homeDir, "Library", "preferences", "pnpm", "rc");
3391
+ else dir = join(homeDir, ".config", "pnpm", "rc");
3392
+ if (isCheck && !existsSync(dir)) return null;
3393
+ return dir;
3394
+ };
3395
+ /**
3396
+ * 获取npm、pnpm配置文件列表
3397
+ */
3398
+ const getNpmConfigList = async () => {
3399
+ const list = [];
3400
+ const npmrc = await getNpmrcPath("global", true);
3401
+ const npmrcProject = await getNpmrcPath("project", true);
3402
+ const pnpmrc = getPnpmConfigPath(true);
3403
+ if (npmrc) list.push({
3404
+ path: npmrc,
3405
+ type: "global",
3406
+ description: "npm全局配置"
3407
+ });
3408
+ if (pnpmrc) list.push({
3409
+ path: pnpmrc,
3410
+ type: "pnpm",
3411
+ description: "pnpm全局配置"
3412
+ });
3413
+ if (npmrcProject) list.push({
3414
+ path: npmrcProject,
3415
+ type: "project",
3416
+ description: "当前项目配置"
3417
+ });
3418
+ return list;
3419
+ };
3420
+ /**
3421
+ * 传入key获取对应的npm配置
3422
+ * @param keys - 配置key
3423
+ * @returns 配置key和对应的配置值
3424
+ */
3425
+ const getNpmConfig = async (keys) => {
3426
+ if (typeof keys === "string") {
3427
+ const { stdout } = await exec$3(`npm config get ${keys}`, { env: {} });
3428
+ return stdout.trim();
3429
+ }
3430
+ const listResult = [];
3431
+ for (const key of keys) {
3432
+ const { stdout: value } = await exec$3(`npm config get ${key}`);
3433
+ listResult.push({
3434
+ key,
3435
+ value: value.trim()
3436
+ });
3437
+ }
3438
+ return listResult;
3439
+ };
3440
+ /**
3441
+ * 设置npmrc文件的配置
3442
+ * @param key - 配置项
3443
+ * @param value - 配置值
3444
+ */
3445
+ const setNpmConfig = async (key, value) => {
3446
+ await exec$3(`npm config set "${key}" "${value}"`);
3447
+ };
3448
+
3449
+ //#endregion
3450
+ //#region src/server/dependencies/config.ts
3451
+ /**
3452
+ * 获取.npmrc文件列表
3453
+ */
3454
+ const getNpmrcListRouter = async (_, res) => {
3455
+ try {
3456
+ return createSuccessResponse(res, await getNpmConfigList());
3457
+ } catch (error) {
3458
+ logger.error("[getNpmrcListRouter]", error);
3459
+ return createServerErrorResponse(res, error instanceof Error ? error.message : String(error));
3460
+ }
3461
+ };
3462
+ /**
3463
+ * 获取npm config文件内容
3464
+ */
3465
+ const getNpmrcContentRouter = async (req, res) => {
3466
+ try {
3467
+ const { path } = req.body;
3468
+ if (!(await getNpmConfigList()).find((item) => item.path === path)) return createBadRequestResponse(res, "文件不存在");
3469
+ return createSuccessResponse(res, ini.read(path));
3470
+ } catch (error) {
3471
+ logger.error("[getNpmrcContentRouter]", error);
3472
+ return createServerErrorResponse(res, error instanceof Error ? error.message : String(error));
3473
+ }
3474
+ };
3475
+ /**
3476
+ * 获取registry、proxy、https-proxy配置
3477
+ */
3478
+ const getNpmBaseConfigRouter = async (_, res) => {
3479
+ try {
3480
+ const [registry, proxy, httpsProxy] = await Promise.all([
3481
+ getNpmConfig("registry"),
3482
+ getNpmConfig("proxy"),
3483
+ getNpmConfig("https-proxy")
3484
+ ]);
3485
+ return createSuccessResponse(res, {
3486
+ registry,
3487
+ proxy,
3488
+ "https-proxy": httpsProxy
3489
+ });
3490
+ } catch (error) {
3491
+ logger.error("[getNpmConfigRouter]", error);
3492
+ return createServerErrorResponse(res, error instanceof Error ? error.message : String(error));
3493
+ }
3494
+ };
3495
+ /**
3496
+ * 保存npmrc文件
3497
+ */
3498
+ const saveNpmrcRouter = async (req, res) => {
3499
+ try {
3500
+ const { path, content, baseConfig } = req.body;
3501
+ const list = await getNpmConfigList();
3502
+ const promises = [];
3503
+ if (baseConfig.registry) promises.push(setNpmConfig("registry", baseConfig.registry));
3504
+ if (baseConfig.proxy) promises.push(setNpmConfig("proxy", baseConfig.proxy));
3505
+ if (baseConfig["https-proxy"]) promises.push(setNpmConfig("https-proxy", baseConfig["https-proxy"]));
3506
+ await Promise.all(promises);
3507
+ if (path && content) {
3508
+ if (!list.find((item) => item.path === path)) return createBadRequestResponse(res, "文件不存在");
3509
+ ini.write(content, path);
3510
+ }
3511
+ return createSuccessResponse(res, "保存成功");
3512
+ } catch (error) {
3513
+ logger.error("[saveNpmrcRouter]", error);
3514
+ return createServerErrorResponse(res, error instanceof Error ? error.message : String(error));
3515
+ }
3516
+ };
3517
+
3518
+ //#endregion
3519
+ //#region src/server/plugins/config/webConfig.ts
3520
+ /**
3521
+ * 传入插件名称 获取插件的web配置
3522
+ * @param name 插件名称
3523
+ */
3524
+ const getWebConfig = async (name) => {
3525
+ const plugin = (await getPlugins("all", true)).find((v) => v.name === name);
3526
+ if (!plugin) return defaultWebConfig();
3527
+ if (plugin.type === "npm" || plugin.type === "git") {
3528
+ const filepath = getWebConfigPath(plugin);
3529
+ if (!filepath) return defaultWebConfig();
3530
+ return getWebConfigMore(filepath);
3531
+ }
3532
+ return defaultWebConfig();
3533
+ };
3534
+ /**
3535
+ * 默认web.config参数
3536
+ */
3537
+ const defaultWebConfig = (exists, path, customComponent, defaultComponent, page, icon) => {
3538
+ return {
3539
+ exists: exists ?? false,
3540
+ path: path || "",
3541
+ customComponent: customComponent ?? false,
3542
+ defaultComponent: defaultComponent ?? false,
3543
+ page: page ?? false,
3544
+ icon: icon || {
3545
+ color: "",
3546
+ name: "star",
3547
+ size: 16
3548
+ }
3549
+ };
3550
+ };
3551
+ /**
3552
+ * 获取web.config文件绝对路径
3553
+ */
3554
+ const getWebConfigPath = (plugin) => {
3555
+ const dev = isDev();
3556
+ const pkg = plugin.pkgData;
3557
+ if (!pkg.karin?.web) return null;
3558
+ if (dev) {
3559
+ if (!pkg.karin["ts-web"]) return null;
3560
+ const filepath = path$1.join(plugin.dir, pkg.karin["ts-web"]);
3561
+ if (!fs$1.existsSync(filepath)) return null;
3562
+ return filepath;
3563
+ }
3564
+ const filepath = path$1.join(plugin.dir, pkg.karin.web);
3565
+ if (!fs$1.existsSync(filepath)) return null;
3566
+ return filepath;
3567
+ };
3568
+ /**
3569
+ * 存在web.config文件 获取更多信息
3570
+ * @param filepath web.config文件绝对路径
3571
+ */
3572
+ const getWebConfigMore = async (filepath) => {
3573
+ try {
3574
+ const web = await imports(filepath, {
3575
+ isImportDefault: true,
3576
+ isRefresh: isDev()
3577
+ });
3578
+ return defaultWebConfig(true, filepath, typeof web?.customComponent === "function", typeof web?.components === "function", typeof web?.page === "function" || typeof web?.page?.url === "string", web?.info?.icon || web?.icon);
3579
+ } catch (error) {
3580
+ logger.error(new Error("获取插件web.config文件失败", { cause: error }));
3581
+ return defaultWebConfig();
3582
+ }
3583
+ };
3584
+
3585
+ //#endregion
3586
+ //#region src/server/plugins/detail/list.ts
3587
+ const git = async (plugin) => {
3588
+ try {
3589
+ /** 本地最新提交哈希 */
3590
+ const version = await getLocalCommitHash(plugin.dir, { short: true });
3591
+ /** 远程最新提交哈希 */
3592
+ const latestHash = await getRemoteCommitHash(plugin.dir, { short: true });
3593
+ return {
3594
+ type: "git",
3595
+ id: plugin.pkgData.name,
3596
+ name: plugin.name,
3597
+ version,
3598
+ latestVersion: latestHash,
3599
+ webConfig: await getWebConfig(plugin.name)
3600
+ };
3601
+ } catch (error) {
3602
+ logger.debug(`获取插件${plugin.name}提交哈希失败`, { cache: error });
3603
+ return {
3604
+ type: "git",
3605
+ id: plugin.pkgData.name,
3606
+ name: plugin.name,
3607
+ version: "0.0.0",
3608
+ latestVersion: "0.0.0",
3609
+ webConfig: await getWebConfig(plugin.name)
3610
+ };
3611
+ }
3612
+ };
3613
+ const npm = async (plugin) => {
3614
+ return {
3615
+ type: "npm",
3616
+ id: plugin.pkgData.name,
3617
+ name: plugin.name,
3618
+ version: plugin.pkgData.version,
3619
+ latestVersion: await getNpmLatestVersion(plugin.pkgData.name) || "0.0.0",
3620
+ webConfig: defaultWebConfig()
3621
+ };
3622
+ };
3623
+ const app$1 = async (plugin) => {
3624
+ return plugin.apps.map((v) => {
3625
+ return {
3626
+ type: "app",
3627
+ id: plugin.name,
3628
+ name: `${plugin.name}/${path.basename(v)}`,
3629
+ version: "",
3630
+ latestVersion: "",
3631
+ webConfig: defaultWebConfig()
3632
+ };
3633
+ });
3634
+ };
3635
+ /**
3636
+ * 获取插件本地列表`(包含web.config相关配置)`
3637
+ * @param isRefresh 是否强制刷新
3638
+ * @returns 插件列表
3639
+ */
3640
+ const getPluginLocalList = async (isRefresh = false) => {
3641
+ if (!isRefresh) {
3642
+ const cachedData = await redis$1.get(REDIS_LOCAL_PLUGIN_LIST_CACHE_KEY);
3643
+ if (cachedData) return JSON.parse(cachedData);
3644
+ }
3645
+ const list = [];
3646
+ const plugin = await getPlugins("all", true);
3647
+ await Promise.all(plugin.map(async (plugin) => {
3648
+ if (plugin.type === "git") return list.push(await git(plugin));
3649
+ if (plugin.type === "npm") return list.push(await npm(plugin));
3650
+ if (plugin.type === "app") {
3651
+ const result = await app$1(plugin);
3652
+ return list.push(...result);
3653
+ }
3654
+ }));
3655
+ /** 更新Redis缓存,设置过期时间 */
3656
+ await redis$1.set(REDIS_LOCAL_PLUGIN_LIST_CACHE_KEY, JSON.stringify(list), { EX: REDIS_PLUGIN_LIST_CACHE_EXPIRE });
3657
+ return list;
3658
+ };
3659
+ /**
3660
+ * @webui 插件管理 获取插件列表Api
3661
+ */
3662
+ const getPluginListPluginAdmin = async (req, res) => {
3663
+ try {
3664
+ const { isRefresh = false } = req.body;
3665
+ createSuccessResponse(res, await getPluginLocalList(isRefresh));
3666
+ } catch (error) {
3667
+ createServerErrorResponse(res, error.message);
3668
+ logger.error(error);
3669
+ }
3670
+ };
3671
+ /**
3672
+ * @webui 获取已加载命令插件缓存信息列表
3673
+ */
3674
+ const getLoadedCommandPluginCacheList = async (_, res) => {
3675
+ try {
3676
+ const list = [];
3677
+ /**
3678
+ * @example
3679
+ * ```ts
3680
+ * {
3681
+ * 'karin-plugin-example': {
3682
+ * 'index.ts': ['fnc'],
3683
+ * }
3684
+ * }
3685
+ * ```
3686
+ */
3687
+ const map = {};
3688
+ cache.command.forEach((plugin) => {
3689
+ const { name: key } = plugin.pkg;
3690
+ if (!map[key]) map[key] = {};
3691
+ if (!map[key][plugin.file.basename]) map[key][plugin.file.basename] = [];
3692
+ map[key][plugin.file.basename].push({
3693
+ pluginName: plugin.file.name,
3694
+ method: plugin.file.method
3695
+ });
3696
+ });
3697
+ Object.keys(map).forEach((key) => {
3698
+ const files = [];
3699
+ Object.keys(map[key]).forEach((file) => {
3700
+ files.push({
3701
+ fileName: file,
3702
+ command: map[key][file]
3703
+ });
3704
+ });
3705
+ list.push({
3706
+ name: key,
3707
+ files
3708
+ });
3709
+ });
3710
+ createSuccessResponse(res, list);
3711
+ } catch (error) {
3712
+ createServerErrorResponse(res, error.message);
3713
+ logger.error(error);
3714
+ }
3715
+ };
3716
+ /**
3717
+ * @webui
3718
+ * 获取前端已安装插件简约列表
3719
+ */
3720
+ const getFrontendInstalledPluginList = async (req, res) => {
3721
+ const isRefresh = req.body.isRefresh || false;
3722
+ try {
3723
+ if (!isRefresh) {
3724
+ const cachedData = await redis$1.get(REDIS_LOCAL_PLUGIN_LIST_CACHE_KEY_FRONTEND);
3725
+ if (cachedData) return createSuccessResponse(res, JSON.parse(cachedData));
3726
+ }
3727
+ /** 复用同一个获取逻辑 */
3728
+ const [list, marketResult] = await Promise.all([getPlugins("all", false, isRefresh), getPluginMarket(isRefresh)]);
3729
+ const marketMap = {};
3730
+ marketResult.plugins.forEach((item) => {
3731
+ marketMap[item.name] = item;
3732
+ });
3733
+ const result = await Promise.all(list.map(async (item) => {
3734
+ const [type, name] = item.split(":");
3735
+ /** 是否在插件市场 */
3736
+ const market = marketMap[name];
3737
+ const webConfig = await getWebConfig(name);
3738
+ return {
3739
+ id: name,
3740
+ name,
3741
+ type,
3742
+ isMarketPlugin: !!market,
3743
+ description: market?.description || "",
3744
+ author: {
3745
+ name: market?.author[0].name || "",
3746
+ home: market?.author[0].home || "",
3747
+ avatar: market?.author[0].avatar || market?.author[0].home ? `${market?.author[0].home}.png` : ""
3748
+ },
3749
+ repoUrl: market?.repo[0].url || "",
3750
+ hasConfig: webConfig.defaultComponent || webConfig.page,
3751
+ hasCustomComponent: webConfig.customComponent,
3752
+ icon: webConfig?.icon
3753
+ };
3754
+ }));
3755
+ createSuccessResponse(res, result);
3756
+ /** 更新Redis缓存,设置过期时间 */
3757
+ await redis$1.set(REDIS_LOCAL_PLUGIN_LIST_CACHE_KEY_FRONTEND, JSON.stringify(result), { EX: REDIS_LOCAL_PLUGIN_LIST_CACHE_EXPIRE_FRONTEND });
3758
+ } catch (error) {
3759
+ createServerErrorResponse(res, error.message);
3760
+ logger.error(error);
3761
+ }
3762
+ };
3763
+
3764
+ //#endregion
3765
+ //#region src/server/plugins/market/index.ts
3766
+ /**
3767
+ * @webui 插件市场 获取插件列表
3768
+ */
3769
+ const getPluginMarketList = async (req, res) => {
3770
+ try {
3771
+ const list = [];
3772
+ const isForce = Boolean(req.body.refresh ?? false);
3773
+ if (!isForce) {
3774
+ const cache = await getCache();
3775
+ if (cache) return createSuccessResponse(res, JSON.parse(cache));
3776
+ }
3777
+ const [local, { plugins: market }] = await Promise.all([getPlugins("all", true), getPluginMarket(isForce)]);
3778
+ const registry = await getFastRegistry();
3779
+ handleLocalPlugins(list, local, market);
3780
+ await handleMarketPlugins(list, local, market, registry);
3781
+ setCache(list);
3782
+ return createSuccessResponse(res, list);
3783
+ } catch (error) {
3784
+ logger.error(error);
3785
+ return createServerErrorResponse(res, error.message);
3786
+ }
3787
+ };
3788
+ /**
3789
+ * 获取缓存
3790
+ */
3791
+ const getCache = async () => {
3792
+ const { redis } = await import("./redis-aLJ7wbJH.mjs").then((n) => n.r);
3793
+ return await redis.get(REDIS_PLUGIN_MARKET_LIST_CACHE_KEY);
3794
+ };
3795
+ /**
3796
+ * 设置缓存
3797
+ */
3798
+ const setCache = async (data) => {
3799
+ const { redis } = await import("./redis-aLJ7wbJH.mjs").then((n) => n.r);
3800
+ await redis.set(REDIS_PLUGIN_MARKET_LIST_CACHE_KEY, JSON.stringify(data), { EX: REDIS_PLUGIN_MARKET_LIST_CACHE_EXPIRE });
3801
+ };
3802
+ /**
3803
+ * 已安装但未在插件市场中的插件
3804
+ * @param list 插件市场列表
3805
+ * @param local 已安装插件列表
3806
+ * @param market 插件市场列表
3807
+ */
3808
+ const handleLocalPlugins = (list, local, market) => {
3809
+ const localPlugins = local.filter((plugin) => !market.some((m) => m.name === plugin.name));
3810
+ for (const plugin of localPlugins) {
3811
+ const author = getAuthorInfo();
3812
+ const local = {
3813
+ name: plugin.name,
3814
+ installed: true,
3815
+ type: plugin.type,
3816
+ version: "",
3817
+ description: "",
3818
+ home: ""
3819
+ };
3820
+ if (plugin.type !== "app") {
3821
+ const pkg = plugin.pkgData;
3822
+ local.version = pkg.version;
3823
+ local.description = pkg.description;
3824
+ local.home = pkg.homepage || pkg?.repository?.url;
3825
+ if (local.home) {
3826
+ const url = new URL(local.home);
3827
+ const [owner] = url.pathname.split("/").filter(Boolean);
3828
+ author.name = pkg.author || owner;
3829
+ author.home = `${url.origin}/${owner}`;
3830
+ if (url.hostname.includes("github") || url.hostname.includes("gitee")) author.avatar = `${url.origin}/${owner}.png`;
3831
+ }
3832
+ }
3833
+ list.push({
3834
+ type: "local",
3835
+ local,
3836
+ author
3837
+ });
3838
+ }
3839
+ };
3840
+ /**
3841
+ * 处理插件市场的插件
3842
+ * @param list 插件市场列表
3843
+ * @param local 已安装插件列表
3844
+ * @param market 插件市场列表
3845
+ * @param registry 注册源
3846
+ */
3847
+ const handleMarketPlugins = async (list, local, market, registry) => {
3848
+ await Promise.all(market.map(async (plugin) => {
3849
+ const data = {
3850
+ installed: false,
3851
+ type: plugin.type,
3852
+ version: "",
3853
+ name: plugin.name,
3854
+ description: plugin.description,
3855
+ home: plugin.home
3856
+ };
3857
+ /** 检查是否已经安装 返回安装的插件 */
3858
+ const pkg = local.find((p) => p.name === plugin.name);
3859
+ if (pkg) {
3860
+ data.installed = true;
3861
+ data.type = pkg.type;
3862
+ data.version = pkg.pkgData.version;
3863
+ } else data.version = await handlePluginVersion(plugin, registry);
3864
+ const author = getAuthorInfo();
3865
+ if (plugin.author?.length) {
3866
+ author.name = plugin.author[0].name;
3867
+ author.home = plugin.author[0].home;
3868
+ author.avatar = plugin.author[0].avatar || `${plugin.author[0].home}.png`;
3869
+ }
3870
+ list.push({
3871
+ type: "market",
3872
+ market: plugin,
3873
+ local: data,
3874
+ author
3875
+ });
3876
+ }));
3877
+ };
3878
+ /**
3879
+ * 获取默认作者信息
3880
+ */
3881
+ const getAuthorInfo = () => {
3882
+ /** 嘿嘿嘿,三月七天下第一可爱^_^ */
3883
+ return {
3884
+ name: "神秘的三月七",
3885
+ home: "",
3886
+ avatar: "https://bbs-static.miyoushe.com/static/2025/03/05/fed22237b02cf398ef993b98b83989b3_7330898026089067872.png"
3887
+ };
3888
+ };
3889
+ /**
3890
+ * 处理插件的版本
3891
+ * @param plugin 插件
3892
+ * @param registry 注册源
3893
+ * @returns 返回插件的版本
3894
+ */
3895
+ const handlePluginVersion = async (plugin, registry) => {
3896
+ if (plugin.type === "npm") {
3897
+ const result = await axios.get(`${registry}/${plugin.name}/latest`);
3898
+ if (result) return result.data.version;
3899
+ } else {
3900
+ const github = plugin.repo.find((r) => r.type === "github");
3901
+ if (github) {
3902
+ const [owner, repo] = new URL(github.url).pathname.split("/").filter(Boolean);
3903
+ const { version } = await getPackageJson(owner, repo);
3904
+ return version;
3905
+ }
3906
+ return "0.0.0";
3907
+ }
3908
+ };
3909
+
3910
+ //#endregion
3911
+ //#region src/server/router/index.ts
3912
+ /**
3913
+ * karin内部路由
3914
+ */
3915
+ const router = Router();
3916
+ /** 日志 */
3917
+ router.use(logMiddleware);
3918
+ /** 鉴权 */
3919
+ router.use(authMiddleware);
3920
+ /** 解析json */
3921
+ router.use(express.json());
3922
+ /** 登录 */
3923
+ router.post(LOGIN_ROUTER, loginRouter);
3924
+ /** 刷新令牌 */
3925
+ router.post(REFRESH_ROUTER, refreshRouter);
3926
+ /** 获取系统配置 */
3927
+ router.post(GET_CONFIG_ROUTER, getConfig);
3928
+ /** 保存系统配置 */
3929
+ router.post(SAVE_CONFIG_ROUTER, saveConfig);
3930
+ /** 获取日志 */
3931
+ router.get(GET_LOG_ROUTER, getLogRouter);
3932
+ /** 设置日志等级 */
3933
+ router.get(SET_LOG_LEVEL_ROUTER, logLevelRouter);
3934
+ /** 获取日志文件列表 */
3935
+ router.get(GET_LOG_FILE_LIST_ROUTER, getLogFileListRouter);
3936
+ /** 获取指定日志文件 */
3937
+ router.get(GET_LOG_FILE_ROUTER, getLogFileRouter);
3938
+ /** 退出karin */
3939
+ router.post(EXIT_ROUTER, exitRouter);
3940
+ /** 重启karin */
3941
+ router.post(RESTART_ROUTER, restartRouter);
3942
+ /** 获取网络状态 */
3943
+ router.get(GET_NETWORK_STATUS_ROUTER, networkStatusRouter);
3944
+ /** 更新karin */
3945
+ router.get(UPDATE_CORE_ROUTER, updateCoreRouter);
3946
+ /** 获取所有bot列表 */
3947
+ router.get(GET_BOTS_ROUTER, getBotsRouter);
3948
+ /** console适配器路由 */
3949
+ router.get(CONSOLE_ROUTER, consoleRouter);
3950
+ /** 系统ws连接 */
3951
+ router.get(SYSTEM_STATUS_WS_ROUTER, infoRouter);
3952
+ /** ping路由 */
3953
+ router.get(PING_ROUTER, pingRouter);
3954
+ /** karin状态 */
3955
+ router.get(SYSTEM_STATUS_KARIN_ROUTER, statusRouter);
3956
+ /** 系统状态 */
3957
+ router.get(SYSTEM_STATUS_ROUTER, systemStatusRealTimeHandler);
3958
+ /** 检查是否安装了指定的npm包 */
3959
+ router.post(CHECK_PLUGIN_ROUTER, checkPlugin);
3960
+ /** 获取插件配置 */
3961
+ router.post(GET_PLUGIN_CONFIG_ROUTER, pluginGetConfig);
3962
+ /** 保存插件配置 */
3963
+ router.post(SAVE_PLUGIN_CONFIG_ROUTER, pluginSaveConfig);
3964
+ /** 判断插件web.config是否存在 */
3965
+ router.post(IS_PLUGIN_CONFIG_EXIST_ROUTER, pluginIsConfigExist);
3966
+ /** 插件管理 */
3967
+ router.post(PLUGIN_ADMIN_ROUTER, pluginAdminRouter);
3968
+ /** 安装插件 */
3969
+ router.post(INSTALL_PLUGIN_ROUTER, pluginInstall);
3970
+ /** 卸载插件 */
3971
+ router.post(UNINSTALL_PLUGIN_ROUTER, pluginUninstall);
3972
+ /** 获取任务状态 */
3973
+ router.post(GET_TASK_STATUS_ROUTER, pluginGetTaskStatus);
3974
+ /** 获取任务列表 */
3975
+ router.post(GET_TASK_LIST_ROUTER, pluginGetTaskList);
3976
+ /** 更新任务状态 */
3977
+ router.post(UPDATE_TASK_STATUS_ROUTER, pluginUpdateTaskStatus);
3978
+ /** 获取已安装插件详情 */
3979
+ router.post(GET_LOCAL_PLUGIN_LIST_ROUTER, pluginGetLocalList);
3980
+ /** @version 1.8.0 获取已安装插件名称列表 */
3981
+ router.post(GET_PLUGIN_LIST_PLUGIN_ADMIN_ROUTER, getPluginListPluginAdmin);
3982
+ /** 创建终端 */
3983
+ router.post(CREATE_TERMINAL_ROUTER, createTerminalHandler);
3984
+ /** 获取终端列表 */
3985
+ router.get(GET_TERMINAL_LIST_ROUTER, getTerminalListHandler);
3986
+ /** 关闭终端 */
3987
+ router.post(CLOSE_TERMINAL_ROUTER, closeTerminalHandler);
3988
+ /** 获取webui插件列表 */
3989
+ router.get(GET_WEBUI_PLUGIN_LIST_ROUTER, getWebuiPluginList);
3990
+ /** 安装webui插件 */
3991
+ router.post(INSTALL_WEBUI_PLUGIN_ROUTER, installWebui);
3992
+ /** 卸载webui插件 */
3993
+ router.post(UNINSTALL_WEBUI_PLUGIN_ROUTER, uninstallWebui);
3994
+ /** 获取webui插件版本 */
3995
+ router.post(GET_WEBUI_PLUGIN_VERSIONS_ROUTER, getWebuiPluginVersions);
3996
+ /** 更新webui插件到指定版本 */
3997
+ router.post(UPDATE_WEBUI_PLUGIN_VERSION_ROUTER, updateWebuiPluginVersion);
3998
+ /** 获取依赖列表 */
3999
+ router.post(GET_DEPENDENCIES_LIST_ROUTER, getDependenciesListRouter);
4000
+ /** 依赖管理(安装/删除) */
4001
+ router.post(MANAGE_DEPENDENCIES_ROUTER, manageDependenciesRouter);
4002
+ /** 获取.npmrc文件列表 */
4003
+ router.post(GET_NPMRC_LIST_ROUTER, getNpmrcListRouter);
4004
+ /** 获取npm config文件内容 */
4005
+ router.post(GET_NPM_CONFIG_ROUTER, getNpmrcContentRouter);
4006
+ /** 获取npm registry、proxy、https-proxy配置 */
4007
+ router.post(GET_NPM_BASE_CONFIG_ROUTER, getNpmBaseConfigRouter);
4008
+ /** 保存npmrc文件 */
4009
+ router.post(SAVE_NPMRC_ROUTER, saveNpmrcRouter);
4010
+ /** 获取任务列表 */
4011
+ router.post(TASK_LIST_ROUTER, taskListRouter);
4012
+ /** 执行任务 */
4013
+ router.get(TASK_RUN_ROUTER, taskRunRouter);
4014
+ /** 获取任务日志 */
4015
+ router.post(TASK_LOGS_ROUTER, taskLogsRouter);
4016
+ /** 删除任务记录 */
4017
+ router.post(TASK_DELETE_ROUTER, taskDeleteRouter);
4018
+ /** @version 1.8.0 获取已加载命令插件缓存信息列表 */
4019
+ router.post(GET_LOADED_COMMAND_PLUGIN_CACHE_LIST_ROUTER, getLoadedCommandPluginCacheList);
4020
+ /** @version 1.8.0 获取插件市场列表 */
4021
+ router.post(GET_PLUGIN_MARKET_LIST_ROUTER, getPluginMarketList);
4022
+ /** @version 1.8.0 获取本地插件列表 用于插件索引页面渲染简约列表 */
4023
+ router.post(GET_LOCAL_PLUGIN_FRONTEND_LIST_ROUTER, getFrontendInstalledPluginList);
4024
+
4025
+ //#endregion
4026
+ //#region src/server/system/root.ts
4027
+ /**
4028
+ * 根路由
4029
+ */
4030
+ const rootRouter = async (_req, res) => {
4031
+ createSuccessResponse(res, null, "雪霁银妆素,桔高映琼枝。");
4032
+ };
4033
+
4034
+ //#endregion
4035
+ //#region src/server/app/app.ts
4036
+ var app_exports = /* @__PURE__ */ __exportAll({
4037
+ app: () => app,
4038
+ initExpress: () => initExpress,
4039
+ server: () => server
4040
+ });
4041
+ /**
4042
+ * @public
4043
+ * @description express 服务
4044
+ */
4045
+ const app = express();
4046
+ /**
4047
+ * @public
4048
+ * @description http 服务
4049
+ */
4050
+ const server = createServer(app);
4051
+ /**
4052
+ * 监听端口
4053
+ * @param port 监听端口
4054
+ * @param host 监听地址
4055
+ */
4056
+ const listen = (port, host) => {
4057
+ server.listen(port, host, () => {
4058
+ logger.info(`[server] express 正在监听: http://${host}:${port}`);
4059
+ });
4060
+ listeners.once(ONLINE, () => {
4061
+ logger.info("\n--------------------^_^--------------------");
4062
+ logger.info(`[server] ${logger.yellow("WebUI 访问地址:")} ${logger.green(`http://127.0.0.1:${port}/web/login?token=${process.env.HTTP_AUTH_KEY}`)}`);
4063
+ logger.info(`[server] HTTP 鉴权秘钥: ${logger.green(process.env.HTTP_AUTH_KEY)}`);
4064
+ logger.info(`[server] WS 鉴权秘钥: ${logger.green(process.env.WS_SERVER_AUTH_KEY) || logger.yellow("没有设置鉴权秘钥")}`);
4065
+ logger.info("-------------------------------------------");
4066
+ logger.info(`[OneBot] ${logger.yellow("协议端连接地址:")} ${logger.green(`ws://127.0.0.1:${process.env.HTTP_PORT}`)}`);
4067
+ logger.info(`[puppet] 渲染器连接地址: ${logger.green(`ws://127.0.0.1:${process.env.HTTP_PORT}/puppeteer`)}`);
4068
+ logger.info("\n-------------------------------------------");
4069
+ });
4070
+ };
4071
+ /**
4072
+ * web 服务
4073
+ */
4074
+ const web = (dir) => {
4075
+ /** web静态文件目录 */
4076
+ const webDir = path.join(dir.karinDir, "dist/web");
4077
+ /** 静态文件 */
4078
+ app.use("/web", express.static(webDir));
4079
+ /** 沙盒数据 一般存储用户头像 */
4080
+ app.use("/sandbox/data", express.static(dir.sandboxDataPath));
4081
+ /** 沙盒临时文件 一般存储临时文件 */
4082
+ app.use("/sandbox/file", express.static(dir.sandboxTempPath));
4083
+ /** 处理 /web 路径下的所有请求,确保 SPA 路由可以正常工作 */
4084
+ app.get("/web/{*splat}", (_, res) => {
4085
+ res.sendFile("index.html", { root: path.resolve(webDir) });
4086
+ });
4087
+ /**
4088
+ * 不注册全局兜底重定向。
4089
+ * 插件可能会在启动后挂载自己的 Web 路由,例如 /my-plugin。
4090
+ * 如果这里把所有未命中的路径重定向到 /web,会导致插件 WebUI 被 Karin WebUI 吞掉。
4091
+ */
4092
+ };
4093
+ /**
4094
+ * @internal
4095
+ * @description 初始化express
4096
+ * @param dir root
4097
+ * @param port 监听端口
4098
+ * @param host 监听地址
4099
+ */
4100
+ const initExpress = async (dir, port, host) => {
4101
+ await import("./ws-BLDoC2gV.mjs");
4102
+ app.use(BASE_ROUTER, router);
4103
+ app.get("/", rootRouter);
4104
+ web(dir);
4105
+ listen(port, host);
4106
+ };
4107
+
4108
+ //#endregion
4109
+ export { getLocalCommitHash as _, authMiddleware as a, ONLINE as b, pkgLoadModule as c, createFile$1 as d, createLogger as f, getLocalBranches as g, getDefaultBranch as h, router as i, pkgLoads as l, gitPull as m, app_exports as n, findPkgByFile as o, createPkg as p, server as r, pkgCache as s, app as t, pkgSort as u, getRemoteBranches as v, getRemoteCommitHash as y };