mioku-service-webui 4.6.1 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/auth.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as crypto from "node:crypto";
2
+ import { logger } from "mioku";
2
3
  import type { Context, Next } from "hono";
3
4
  import type { AuthConfig } from "./types";
4
5
  import { AUTH_PATH, WEEK_MS, readJsonFile, writeJsonFile } from "./utils";
@@ -27,13 +28,79 @@ export function ensureAuthConfig(): AuthConfig {
27
28
  void notifyOwnersAuthTokenRefreshed(
28
29
  generated.token,
29
30
  generated.expiresAt,
30
- ).catch(() => {});
31
+ ).catch((err) => {
32
+ logger.warn(
33
+ `[webui] 通知主人密钥更新失败: ${err instanceof Error ? err.message : String(err)}`,
34
+ );
35
+ });
31
36
  return generated;
32
37
  }
33
38
 
34
39
  return existing;
35
40
  }
36
41
 
42
+ /**
43
+ * 距密钥过期多久时主动刷新(默认 6 小时)。
44
+ * 提前刷新的好处:即便长时间没有 API 请求 / 登录触发,密钥也会轮换并通知主人。
45
+ */
46
+ const AUTH_REFRESH_LEAD_MS = 6 * 60 * 60 * 1000;
47
+ /** 定时巡检周期(默认 60 秒)。 */
48
+ const AUTH_REFRESH_INTERVAL_MS = 60 * 1000;
49
+
50
+ let authRefreshTimer: NodeJS.Timeout | null = null;
51
+
52
+ /**
53
+ * 启动密钥定时巡检。即使没有前端访问 / 没有登录请求,
54
+ * 也会在密钥接近过期时主动刷新并通知主人。
55
+ */
56
+ export function startAuthRefreshTimer(): void {
57
+ if (authRefreshTimer) {
58
+ return;
59
+ }
60
+ // 启动时立即巡检一次,处理"服务运行时密钥刚好过期"的场景
61
+ try {
62
+ ensureAuthConfig();
63
+ } catch (err) {
64
+ logger.warn(
65
+ `[webui] 启动时巡检密钥失败: ${err instanceof Error ? err.message : String(err)}`,
66
+ );
67
+ }
68
+ authRefreshTimer = setInterval(() => {
69
+ try {
70
+ const now = Date.now();
71
+ const existing = readJsonFile<AuthConfig | null>(AUTH_PATH, null);
72
+ // 仅在接近过期 / 已过期时才触发刷新逻辑,避免无意义的写文件
73
+ if (
74
+ existing &&
75
+ existing.token &&
76
+ existing.expiresAt &&
77
+ existing.expiresAt - now > AUTH_REFRESH_LEAD_MS
78
+ ) {
79
+ return;
80
+ }
81
+ ensureAuthConfig();
82
+ } catch (err) {
83
+ logger.warn(
84
+ `[webui] 定时巡检密钥失败: ${err instanceof Error ? err.message : String(err)}`,
85
+ );
86
+ }
87
+ }, AUTH_REFRESH_INTERVAL_MS);
88
+ // unref 避免阻塞进程退出
89
+ if (typeof authRefreshTimer.unref === "function") {
90
+ authRefreshTimer.unref();
91
+ }
92
+ logger.info(
93
+ `[webui] 已启动密钥定时巡检(周期 ${AUTH_REFRESH_INTERVAL_MS / 1000}s,提前 ${AUTH_REFRESH_LEAD_MS / 60 / 60 / 1000}h 刷新)`,
94
+ );
95
+ }
96
+
97
+ export function stopAuthRefreshTimer(): void {
98
+ if (authRefreshTimer) {
99
+ clearInterval(authRefreshTimer);
100
+ authRefreshTimer = null;
101
+ }
102
+ }
103
+
37
104
  export function verifyAuthHeader(c: Context): boolean {
38
105
  const auth = c.req.header("authorization") || "";
39
106
  const token = auth.replace(/^Bearer\s+/i, "").trim();
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import matter from "gray-matter";
4
- import { logger } from "mioki";
4
+ import { logger } from "mioku";
5
5
 
6
6
  export interface ConfigField {
7
7
  key: string;
@@ -44,6 +44,14 @@ export function loadServiceConfigPage(serviceName: string): ConfigPageManifest |
44
44
  return loadConfigPage(serviceName, possiblePaths);
45
45
  }
46
46
 
47
+ export function loadAdapterConfigPage(adapterName: string): ConfigPageManifest | null {
48
+ const possiblePaths = [
49
+ path.join(process.cwd(), "node_modules", `mioku-adapter-${adapterName}`),
50
+ ];
51
+
52
+ return loadConfigPage(adapterName, possiblePaths);
53
+ }
54
+
47
55
  function loadConfigPage(
48
56
  packageName: string,
49
57
  possiblePaths: string[],
package/datasources.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { connectedBots, logger } from "mioki";
1
+ import { connectedBots, logger } from "mioku";
2
2
 
3
3
  export interface DatasourceOption {
4
4
  value: string;
package/index.ts CHANGED
@@ -1,13 +1,19 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { logger } from "mioki";
3
+ import { logger } from "mioku";
4
4
  import { Hono } from "hono";
5
5
  import { serve } from "@hono/node-server";
6
6
  import { serveStatic } from "@hono/node-server/serve-static";
7
7
  import { createNodeWebSocket } from "@hono/node-ws";
8
8
  import type { MiokuService } from "mioku";
9
9
  import type { WebUISettings } from "./types";
10
- import { ensureAuthConfig, loginWithToken, requireAuth } from "./auth";
10
+ import {
11
+ ensureAuthConfig,
12
+ loginWithToken,
13
+ requireAuth,
14
+ startAuthRefreshTimer,
15
+ stopAuthRefreshTimer,
16
+ } from "./auth";
11
17
  import {
12
18
  ensureWebUIDist,
13
19
  getWebUISettings,
@@ -23,6 +29,7 @@ import {
23
29
  createDBRoutes,
24
30
  createPluginConfigRoutes,
25
31
  createServiceConfigRoutes,
32
+ createAdapterConfigRoutes,
26
33
  createMemeRoutes,
27
34
  createManageRoutes,
28
35
  createStoreRoutes,
@@ -118,6 +125,7 @@ class WebUIRuntime {
118
125
  this.app.route("/api/db", createDBRoutes());
119
126
  this.app.route("/api/plugin-config", createPluginConfigRoutes());
120
127
  this.app.route("/api/service-config", createServiceConfigRoutes());
128
+ this.app.route("/api/adapter-config", createAdapterConfigRoutes());
121
129
  this.app.route("/api/meme", createMemeRoutes());
122
130
  this.app.route("/api/data-management", createDataManagementRoutes());
123
131
  this.app.route("/api/stranger", createStrangerRoutes());
@@ -250,9 +258,13 @@ const webUIService: MiokuService = {
250
258
  ensureAuthConfig();
251
259
  await ensureWebUIDist();
252
260
  await runtime.initRoutes();
261
+ // 启动密钥定时巡检:即便长时间没有前端访问 / 登录请求,
262
+ // 也会在密钥接近过期时主动刷新并通知主人。
263
+ startAuthRefreshTimer();
253
264
  },
254
265
 
255
266
  async dispose() {
267
+ stopAuthRefreshTimer();
256
268
  runtime.stop();
257
269
  },
258
270
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-service-webui",
3
- "version": "4.6.1",
3
+ "version": "4.8.0",
4
4
  "description": "Mioku WebUI 服务",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -22,15 +22,14 @@
22
22
  "adm-zip": "^0.5.16",
23
23
  "fflate": "^0.8.2",
24
24
  "gray-matter": "^4.0.3",
25
- "hono": "4.12.14"
25
+ "hono": "4.12.14",
26
+ "systeminformation": "^5.31.6"
26
27
  },
27
28
  "peerDependencies": {
28
- "mioku-service-ai": "^0.2.0",
29
- "mioki": "^0.16.0"
29
+ "mioku-service-ai": "^4.0.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/bun": "^1.3.14",
33
- "mioki": "^0.16.0",
34
33
  "mioku": "workspace:*",
35
34
  "mioku-service-ai": "workspace:*"
36
35
  }
package/port-utils.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
- import { logger } from "mioki";
3
+ import { logger } from "mioku";
4
4
 
5
5
  const execFileAsync = promisify(execFile);
6
6
 
@@ -1,12 +1,12 @@
1
1
  import { Hono } from "hono";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { logger } from "mioki";
4
+ import { logger } from "mioku";
5
5
  import { readJsonFile, writeJsonFile, NODE_MODULES_DIR } from "../utils";
6
6
 
7
7
  const ACCESS_CONFIG_PATH = path.resolve(
8
8
  process.cwd(),
9
- "config/boot/access-control.json",
9
+ "config/core/access-control.json",
10
10
  );
11
11
 
12
12
  const DEFAULT_CONFIG = {
@@ -0,0 +1,83 @@
1
+ import { Hono } from "hono";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { loadAdapterConfigPage } from "../config-page-loader";
5
+ import { getAdapterConfigs, updateAdapterConfig } from "../system";
6
+
7
+ function listAdaptersFromNodeModules(): string[] {
8
+ const modulesPath = path.join(process.cwd(), "node_modules");
9
+ if (!fs.existsSync(modulesPath)) return [];
10
+ const adapters: string[] = [];
11
+ const entries = fs.readdirSync(modulesPath, { withFileTypes: true });
12
+ for (const entry of entries) {
13
+ if (!entry.name.startsWith("mioku-adapter-")) continue;
14
+ const fullPath = path.join(modulesPath, entry.name);
15
+ let stat: fs.Stats;
16
+ try {
17
+ stat = fs.lstatSync(fullPath);
18
+ } catch {
19
+ continue;
20
+ }
21
+ if (!stat.isDirectory() && !stat.isSymbolicLink()) continue;
22
+ adapters.push(entry.name.replace(/^mioku-adapter-/, ""));
23
+ }
24
+ return adapters.sort((a, b) => a.localeCompare(b));
25
+ }
26
+
27
+ export function createAdapterConfigRoutes() {
28
+ const app = new Hono();
29
+
30
+ app.get("/overview", (c) => {
31
+ const adapters = listAdaptersFromNodeModules();
32
+ const overview = adapters
33
+ .map((name) => {
34
+ const manifest = loadAdapterConfigPage(name);
35
+ return {
36
+ name,
37
+ title: manifest?.title || name,
38
+ description: manifest?.description,
39
+ hasPage: manifest?.hasCustomPage || false,
40
+ };
41
+ })
42
+ .filter((a) => a.hasPage);
43
+
44
+ return c.json({ ok: true, data: overview });
45
+ });
46
+
47
+ app.get("/:name/page", (c) => {
48
+ const name = c.req.param("name");
49
+ const manifest = loadAdapterConfigPage(name);
50
+ if (!manifest) {
51
+ return c.json({ ok: true, data: null });
52
+ }
53
+
54
+ return c.json({
55
+ ok: true,
56
+ data: {
57
+ plugin: manifest.plugin,
58
+ title: manifest.title,
59
+ description: manifest.description,
60
+ markdown: manifest.markdown,
61
+ fields: manifest.fields,
62
+ hasCustomPage: manifest.hasCustomPage,
63
+ configs: {
64
+ [name]: getAdapterConfigs(name),
65
+ },
66
+ },
67
+ });
68
+ });
69
+
70
+ app.get("/:name", (c) => {
71
+ const name = c.req.param("name");
72
+ return c.json({ ok: true, data: getAdapterConfigs(name) });
73
+ });
74
+
75
+ app.put("/:name", async (c) => {
76
+ const name = c.req.param("name");
77
+ const body = await c.req.json();
78
+ const data = updateAdapterConfig(name, body);
79
+ return c.json({ ok: true, data });
80
+ });
81
+
82
+ return app;
83
+ }
package/routes/ai.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Hono } from "hono";
2
- import { logger } from "mioki";
2
+ import { logger } from "mioku";
3
3
  import { getChatConfig, updateChatConfig } from "../system";
4
4
  import { listStickerCatalog } from "../database";
5
5
  import aiService from "mioku-service-ai";
@@ -332,18 +332,42 @@ export function createAIRoutes() {
332
332
  modelId: body?.modelId,
333
333
  name: body?.name,
334
334
  capabilities: body?.capabilities,
335
+ thinkingLevel: body?.thinkingLevel,
335
336
  });
336
337
  return c.json({ ok: true, data: model });
337
338
  });
338
339
 
340
+ app.put("/models/:id", async (c) => {
341
+ const api = getAIApi();
342
+ if (!api?.setModelThinkingLevel) {
343
+ return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
344
+ }
345
+ const id = decodeURIComponent(c.req.param("id"));
346
+ const body = await c.req.json();
347
+ const rawLevel = body?.thinkingLevel;
348
+ const level =
349
+ rawLevel === null || rawLevel === undefined || rawLevel === ""
350
+ ? undefined
351
+ : String(rawLevel);
352
+ logger.info(`[webui-action] ai.model.thinking.update`, {
353
+ id,
354
+ level: level ?? "(default)",
355
+ });
356
+ const ok = api.setModelThinkingLevel(id, level as any);
357
+ return c.json({ ok });
358
+ });
359
+
339
360
  app.delete("/models/:id", (c) => {
340
361
  const api = getAIApi();
341
- if (!api?.removeCustomModel) {
362
+ if (!api?.removeModel && !api?.removeCustomModel) {
342
363
  return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
343
364
  }
344
365
  const id = decodeURIComponent(c.req.param("id"));
345
366
  logger.info(`[webui-action] ai.model.remove`, { id });
346
- return c.json({ ok: Boolean(api.removeCustomModel(id)) });
367
+ const ok = api.removeModel
368
+ ? Boolean(api.removeModel(id))
369
+ : Boolean(api.removeCustomModel(id));
370
+ return c.json({ ok });
347
371
  });
348
372
 
349
373
  app.get("/roles", (c) => {
@@ -354,6 +378,38 @@ export function createAIRoutes() {
354
378
  return c.json({ ok: true, data: api.getRoleBindings() });
355
379
  });
356
380
 
381
+ app.get("/fallback", (c) => {
382
+ const api = getAIApi();
383
+ const chain = api?.getMainFallbackChain?.() ?? [];
384
+ return c.json({
385
+ ok: true,
386
+ data: {
387
+ fallback: chain,
388
+ liveFallback: chain,
389
+ },
390
+ });
391
+ });
392
+
393
+ app.put("/fallback", async (c) => {
394
+ const api = getAIApi();
395
+ if (!api?.setMainFallbackChain) {
396
+ return c.json({ ok: false, error: "AI_SERVICE_UNAVAILABLE" }, 503);
397
+ }
398
+ const body = await c.req.json();
399
+ const raw = Array.isArray(body?.fallback) ? body.fallback : [];
400
+ const fallback = Array.from(
401
+ new Set(
402
+ raw
403
+ .map((id: unknown) => String(id || "").trim())
404
+ .filter((id: string) => id.includes("/") && id.length > 1),
405
+ ),
406
+ );
407
+ logger.info(`[webui-action] ai.fallback.update`, { count: fallback.length });
408
+ api.setMainFallbackChain(fallback);
409
+ const live = api.getMainFallbackChain?.() ?? fallback;
410
+ return c.json({ ok: true, data: { fallback: live, liveFallback: live } });
411
+ });
412
+
357
413
  app.put("/roles", async (c) => {
358
414
  const api = getAIApi();
359
415
  if (!api?.setRoleBinding) {
package/routes/config.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Hono } from "hono";
2
- import { logger } from "mioki";
2
+ import { logger } from "mioku";
3
3
  import {
4
4
  getMiokuConfig,
5
5
  updateMiokuConfig,
@@ -25,7 +25,7 @@ export function createConfigRoutes() {
25
25
  admins: body?.admins?.length,
26
26
  napcat: body?.napcat?.length,
27
27
  plugins: body?.plugins?.length,
28
- bootConfigured: Boolean(body?.boot),
28
+ coreConfigured: Boolean(body?.core),
29
29
  });
30
30
  const data = updateMiokuConfig(body);
31
31
  return c.json({ ok: true, data });
@@ -569,7 +569,7 @@ export function createDataManagementRoutes() {
569
569
 
570
570
  const file = await fs.promises.readFile(filePath);
571
571
  c.header("content-type", "application/zip");
572
- c.header("content-disposition", `attachment; filename="${path.basename(filePath)}"`);
572
+ c.header("content-disposition", `attachment); filename="${path.basename(filePath)}"`);
573
573
  return c.body(file);
574
574
  });
575
575
 
package/routes/index.ts CHANGED
@@ -2,6 +2,7 @@ export { createConfigRoutes } from "./config";
2
2
  export { createWebUISettingsRoutes } from "./webui";
3
3
  export { createAIRoutes } from "./ai";
4
4
  export { createDBRoutes, createPluginConfigRoutes, createMemeRoutes, createServiceConfigRoutes } from "./db";
5
+ export { createAdapterConfigRoutes } from "./adapter-config";
5
6
  export { createManageRoutes } from "./manage";
6
7
  export { createStoreRoutes } from "./store";
7
8
  export { createDataManagementRoutes } from "./data-management";
package/routes/manage.ts CHANGED
@@ -27,6 +27,9 @@ export function createManageRoutes() {
27
27
  app.get("/services", (c) =>
28
28
  c.json({ ok: true, data: listManagedPackages("service") }),
29
29
  );
30
+ app.get("/adapters", (c) =>
31
+ c.json({ ok: true, data: listManagedPackages("adapter") }),
32
+ );
30
33
  app.get("/plugins/overview", async (c) =>
31
34
  c.json({ ok: true, data: await listManagedPackagesWithUpdates("plugin") }),
32
35
  );
@@ -43,6 +46,14 @@ export function createManageRoutes() {
43
46
  const result = await getManagedPackageDetail(name, "service");
44
47
  return c.json(result);
45
48
  });
49
+ app.get("/adapters/overview", async (c) =>
50
+ c.json({ ok: true, data: await listManagedPackagesWithUpdates("adapter") }),
51
+ );
52
+ app.get("/adapters/:name", async (c) => {
53
+ const name = c.req.param("name");
54
+ const result = await getManagedPackageDetail(name, "adapter");
55
+ return c.json(result);
56
+ });
46
57
 
47
58
  app.post("/install", async (c) => {
48
59
  const body = (await c.req.json()) as InstallRequest;
@@ -53,7 +64,7 @@ export function createManageRoutes() {
53
64
  app.post("/check-update", async (c) => {
54
65
  const body = (await c.req.json()) as {
55
66
  name: string;
56
- target: "plugin" | "service";
67
+ target: "plugin" | "service" | "adapter";
57
68
  };
58
69
  const result = await checkUpdate(body.name, body.target);
59
70
  return c.json(result);
@@ -82,7 +93,7 @@ export function createManageRoutes() {
82
93
  const body = (await c.req.json()) as Partial<ChangeRepoRequest>;
83
94
  const result = await changeManagedPackageRepo(
84
95
  String(body.name || ""),
85
- (body.target || "plugin") as "plugin" | "service",
96
+ (body.target || "plugin") as "plugin" | "service" | "adapter",
86
97
  String(body.repoUrl || ""),
87
98
  );
88
99
  return c.json(result);
package/routes/store.ts CHANGED
@@ -19,6 +19,7 @@ interface OfficialServiceEntry {
19
19
  interface OfficialRegistry {
20
20
  plugins?: Record<string, OfficialPluginEntry>;
21
21
  services?: Record<string, OfficialServiceEntry>;
22
+ adapters?: Record<string, OfficialPluginEntry>;
22
23
  }
23
24
 
24
25
  let cachedOfficialRegistry: OfficialRegistry | null = null;
@@ -56,9 +57,11 @@ async function readOfficialRegistry(force = false): Promise<OfficialRegistry> {
56
57
  function buildOfficialMaps(registry: OfficialRegistry): {
57
58
  plugins: Map<string, OfficialPluginEntry>;
58
59
  services: Map<string, OfficialServiceEntry>;
60
+ adapters: Map<string, OfficialPluginEntry>;
59
61
  } {
60
62
  const plugins = new Map<string, OfficialPluginEntry>();
61
63
  const services = new Map<string, OfficialServiceEntry>();
64
+ const adapters = new Map<string, OfficialPluginEntry>();
62
65
 
63
66
  for (const [, entry] of Object.entries(registry.plugins || {})) {
64
67
  if (entry?.npm) {
@@ -72,21 +75,25 @@ function buildOfficialMaps(registry: OfficialRegistry): {
72
75
  }
73
76
  }
74
77
 
75
- return { plugins, services };
76
- }
78
+ for (const [, entry] of Object.entries(registry.adapters || {})) {
79
+ if (entry?.npm) {
80
+ adapters.set(entry.npm, entry);
81
+ }
82
+ }
77
83
 
78
- function isServicePackage(name: string): boolean {
79
- return name.startsWith("mioku-service-");
84
+ return { plugins, services, adapters };
80
85
  }
81
86
 
82
- function inferTypeFromPackageName(name: string): "plugin" | "service" | null {
87
+ function inferTypeFromPackageName(name: string): "plugin" | "service" | "adapter" | null {
83
88
  if (name.startsWith("mioku-plugin-")) return "plugin";
84
89
  if (name.startsWith("mioku-service-")) return "service";
90
+ if (name.startsWith("mioku-adapter-")) return "adapter";
85
91
  return null;
86
92
  }
87
93
 
88
- function inferDisplayName(packageName: string, type: "plugin" | "service"): string {
89
- const prefix = type === "plugin" ? "mioku-plugin-" : "mioku-service-";
94
+ function inferDisplayName(packageName: string, type: "plugin" | "service" | "adapter"): string {
95
+ const prefix =
96
+ type === "plugin" ? "mioku-plugin-" : type === "service" ? "mioku-service-" : "mioku-adapter-";
90
97
  return packageName.startsWith(prefix)
91
98
  ? packageName.slice(prefix.length)
92
99
  : packageName;
@@ -189,7 +196,9 @@ export function createStoreRoutes() {
189
196
  const officialEntry =
190
197
  type === "plugin"
191
198
  ? officialMaps.plugins.get(packageName)
192
- : officialMaps.services.get(packageName);
199
+ : type === "service"
200
+ ? officialMaps.services.get(packageName)
201
+ : officialMaps.adapters.get(packageName);
193
202
  const repositoryUrl = normalizeRepositoryUrl(latest?.repository);
194
203
 
195
204
  return c.json({
@@ -213,11 +222,13 @@ export function createStoreRoutes() {
213
222
  requiredServices: Array.isArray(latest?.mioku?.services)
214
223
  ? latest.mioku.services
215
224
  : [],
216
- installTarget: isServicePackage(packageName) ? "service" : "plugin",
225
+ installTarget: type,
217
226
  installPath:
218
227
  type === "plugin"
219
228
  ? `plugins/${inferDisplayName(packageName, "plugin")}`
220
- : getServiceInstallPath(latest, packageName),
229
+ : type === "service"
230
+ ? getServiceInstallPath(latest, packageName)
231
+ : `adapters/${inferDisplayName(packageName, "adapter")}`,
221
232
  },
222
233
  });
223
234
  } catch (error: any) {
@@ -1,5 +1,7 @@
1
1
  import { Hono } from "hono";
2
- import { connectedBots, logger } from "mioki";
2
+ import type { Bot } from "mioku";
3
+ import {connectedBots,
4
+ logger} from "mioku";
3
5
 
4
6
  interface StrangerInfoResponse {
5
7
  ok: boolean;
@@ -18,7 +20,7 @@ const FETCH_TIMEOUT_MS = 4000;
18
20
  const nicknameCache = new Map<string, CacheEntry>();
19
21
  const inflight = new Map<string, Promise<string | null>>();
20
22
 
21
- function getConnectedBots(): any[] {
23
+ function getConnectedBots(): Bot[] {
22
24
  return Array.from(connectedBots.values()).filter(Boolean);
23
25
  }
24
26
 
@@ -48,23 +50,20 @@ function setCached(userId: string, nickname: string | null): void {
48
50
  }
49
51
 
50
52
  async function fetchNicknameFromBot(
51
- bot: any,
53
+ bot: Bot,
52
54
  userId: string,
53
55
  ): Promise<string | null> {
54
- const controller = new AbortController();
55
- const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
56
56
  try {
57
- const res = await bot.api("get_stranger_info", { user_id: userId });
58
- const data = res?.data || res;
59
- const nickname = String(data?.nickname ?? "").trim();
57
+ const info = await bot.getFriendInfo(userId);
58
+ const nickname = String(info?.nickname ?? "").trim();
60
59
  return nickname || null;
61
- } catch (error: any) {
60
+ } catch (error) {
62
61
  logger.warn(
63
- `stranger-info: bot ${bot?.uin || bot?.bot_id || "?"} 查询 ${userId} 失败: ${error?.message || error}`,
62
+ `stranger-info: bot ${bot.bot_id} 查询 ${userId} 失败: ${
63
+ error instanceof Error ? error.message : String(error)
64
+ }`,
64
65
  );
65
66
  return null;
66
- } finally {
67
- clearTimeout(timer);
68
67
  }
69
68
  }
70
69
 
@@ -77,9 +76,7 @@ async function lookupNickname(userId: string): Promise<string | null> {
77
76
 
78
77
  const promise = (async () => {
79
78
  const bots = getConnectedBots();
80
- if (bots.length === 0) {
81
- return null;
82
- }
79
+ if (bots.length === 0) return null;
83
80
  const results = await Promise.all(
84
81
  bots.map((bot) => fetchNicknameFromBot(bot, userId)),
85
82
  );
@@ -105,7 +102,7 @@ export function createStrangerRoutes() {
105
102
  const app = new Hono();
106
103
 
107
104
  app.post("/info", async (c) => {
108
- const body = await c.req.json().catch(() => ({}));
105
+ const body = (await c.req.json().catch(() => ({}))) as { user_id?: unknown };
109
106
  const userId = normalizeUserId(body?.user_id);
110
107
  if (!isValidQQNumber(userId)) {
111
108
  return c.json(
@@ -129,8 +126,10 @@ export function createStrangerRoutes() {
129
126
  ok: true,
130
127
  data: { userId, nickname },
131
128
  } satisfies StrangerInfoResponse);
132
- } catch (error: any) {
133
- logger.error(`stranger-info 失败: ${error?.message || error}`);
129
+ } catch (error) {
130
+ logger.error(
131
+ `stranger-info 失败: ${error instanceof Error ? error.message : String(error)}`,
132
+ );
134
133
  return c.json(
135
134
  {
136
135
  ok: false,
package/system.ts CHANGED
@@ -3,7 +3,9 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import process from "node:process";
5
5
  import AdmZip from "adm-zip";
6
- import { connectedBots, logger, systemInfo } from "mioki";
6
+ import systemInfo from "systeminformation";
7
+ import {connectedBots,
8
+ logger} from "mioku";
7
9
  import type {
8
10
  InstallRequest,
9
11
  ManagedTarget,
@@ -87,81 +89,16 @@ interface MiokiRuntimeConfig {
87
89
  [key: string]: any;
88
90
  }
89
91
 
90
- const SYSTEM_PLUGIN_NAMES = new Set(["boot", "chat", "help"]);
92
+ const SYSTEM_PLUGIN_NAMES = new Set(["chat", "help"]);
91
93
  const SYSTEM_SERVICE_NAMES = new Set(["ai", "config", "help", "screenshot"]);
92
- const BOOT_CONFIG_PATH = path.join(
94
+ const CORE_CONFIG_PATH = path.join(
93
95
  process.cwd(),
94
96
  "config",
95
- "boot",
97
+ "core",
96
98
  "base.json",
97
99
  );
98
100
 
99
- export interface BootSystemCommandsConfig {
100
- update: {
101
- enabled: boolean;
102
- selectTimeoutMs: number;
103
- };
104
- install: {
105
- enabled: boolean;
106
- };
107
- market: {
108
- enabled: boolean;
109
- };
110
- restart: {
111
- enabled: boolean;
112
- };
113
- }
114
-
115
- const DEFAULT_BOOT_SYSTEM_COMMANDS_CONFIG: BootSystemCommandsConfig = {
116
- update: {
117
- enabled: true,
118
- selectTimeoutMs: 60000,
119
- },
120
- install: {
121
- enabled: true,
122
- },
123
- market: {
124
- enabled: true,
125
- },
126
- restart: {
127
- enabled: true,
128
- },
129
- };
130
-
131
- function normalizeBootSystemCommandsConfig(
132
- input: any,
133
- ): BootSystemCommandsConfig {
134
- const merged = deepMerge(
135
- cloneJson(DEFAULT_BOOT_SYSTEM_COMMANDS_CONFIG),
136
- input || {},
137
- );
138
- const rawTimeout = Number(merged?.update?.selectTimeoutMs);
139
- return {
140
- update: {
141
- enabled: Boolean(merged?.update?.enabled),
142
- selectTimeoutMs:
143
- Number.isFinite(rawTimeout) && rawTimeout >= 1000
144
- ? Math.floor(rawTimeout)
145
- : DEFAULT_BOOT_SYSTEM_COMMANDS_CONFIG.update.selectTimeoutMs,
146
- },
147
- install: {
148
- enabled: Boolean(merged?.install?.enabled),
149
- },
150
- market: {
151
- enabled: Boolean(merged?.market?.enabled),
152
- },
153
- restart: {
154
- enabled: Boolean(merged?.restart?.enabled),
155
- },
156
- };
157
- }
158
-
159
- interface AccessRuleConfig {
160
- whitelist: Array<string | number>;
161
- blacklist: Array<string | number>;
162
- }
163
-
164
- export interface BootSystemConfig {
101
+ export interface CoreSystemConfig {
165
102
  likeCommand: {
166
103
  enabled: boolean;
167
104
  keyword: string;
@@ -173,47 +110,31 @@ export interface BootSystemConfig {
173
110
  };
174
111
  group: {
175
112
  minMemberCount: number;
176
- welcome: {
177
- enabled: boolean;
178
- mode: "ai" | "text";
179
- text: string;
180
- aiPrompt: string;
181
- };
182
113
  };
183
- messageFilter: {
184
- user: AccessRuleConfig;
185
- group: AccessRuleConfig;
114
+ autoUpdate: {
115
+ enabled: boolean;
116
+ time: string;
117
+ frequency: "daily" | "weekly" | "monthly";
186
118
  };
187
119
  }
188
120
 
189
- const DEFAULT_BOOT_SYSTEM_CONFIG: BootSystemConfig = {
121
+ const DEFAULT_CORE_SYSTEM_CONFIG: CoreSystemConfig = {
190
122
  likeCommand: {
191
123
  enabled: true,
192
124
  keyword: "赞我",
193
125
  likeTimes: 10,
194
- reactionEmojiId: 66,
126
+ reactionEmojiId: 201,
195
127
  },
196
128
  friend: {
197
- autoApprove: false,
129
+ autoApprove: true,
198
130
  },
199
131
  group: {
200
132
  minMemberCount: 0,
201
- welcome: {
202
- enabled: true,
203
- mode: "ai",
204
- text: "欢迎 {user} 加入 {group}",
205
- aiPrompt: "",
206
- },
207
133
  },
208
- messageFilter: {
209
- user: {
210
- whitelist: [],
211
- blacklist: [],
212
- },
213
- group: {
214
- whitelist: [],
215
- blacklist: [],
216
- },
134
+ autoUpdate: {
135
+ enabled: true,
136
+ time: "03:00",
137
+ frequency: "daily",
217
138
  },
218
139
  };
219
140
 
@@ -238,7 +159,9 @@ function isSystemServiceName(name: string): boolean {
238
159
  }
239
160
 
240
161
  function getTargetRoot(target: ManagedTarget): string {
241
- return target === "plugin" ? PLUGINS_DIR : SERVICES_DIR;
162
+ if (target === "plugin") return PLUGINS_DIR;
163
+ if (target === "service") return SERVICES_DIR;
164
+ return NODE_MODULES_DIR;
242
165
  }
243
166
 
244
167
  async function getCurrentBranchName(dir: string): Promise<string> {
@@ -310,17 +233,11 @@ function cloneJson<T>(value: T): T {
310
233
  return JSON.parse(JSON.stringify(value)) as T;
311
234
  }
312
235
 
313
- function normalizeAccessRuleConfig(input: any): AccessRuleConfig {
314
- return {
315
- whitelist: Array.isArray(input?.whitelist) ? input.whitelist : [],
316
- blacklist: Array.isArray(input?.blacklist) ? input.blacklist : [],
317
- };
318
- }
319
-
320
- function normalizeBootSystemConfig(input: any): BootSystemConfig {
321
- const merged = deepMerge(cloneJson(DEFAULT_BOOT_SYSTEM_CONFIG), input || {});
322
- const legacyPrivateFilter = input?.messageFilter?.private;
323
- const userFilterSource = input?.messageFilter?.user ?? legacyPrivateFilter;
236
+ function normalizeCoreSystemConfig(input: any): CoreSystemConfig {
237
+ const merged = deepMerge(cloneJson(DEFAULT_CORE_SYSTEM_CONFIG), input || {});
238
+ const validFreq = ["daily", "weekly", "monthly"].includes(
239
+ merged?.autoUpdate?.frequency,
240
+ );
324
241
 
325
242
  return {
326
243
  likeCommand: {
@@ -329,17 +246,17 @@ function normalizeBootSystemConfig(input: any): BootSystemConfig {
329
246
  typeof merged?.likeCommand?.keyword === "string" &&
330
247
  merged.likeCommand.keyword.trim()
331
248
  ? merged.likeCommand.keyword.trim()
332
- : DEFAULT_BOOT_SYSTEM_CONFIG.likeCommand.keyword,
249
+ : DEFAULT_CORE_SYSTEM_CONFIG.likeCommand.keyword,
333
250
  likeTimes:
334
251
  typeof merged?.likeCommand?.likeTimes === "number" &&
335
252
  Number.isFinite(merged.likeCommand.likeTimes)
336
253
  ? merged.likeCommand.likeTimes
337
- : DEFAULT_BOOT_SYSTEM_CONFIG.likeCommand.likeTimes,
254
+ : DEFAULT_CORE_SYSTEM_CONFIG.likeCommand.likeTimes,
338
255
  reactionEmojiId:
339
256
  typeof merged?.likeCommand?.reactionEmojiId === "number" &&
340
257
  Number.isFinite(merged.likeCommand.reactionEmojiId)
341
258
  ? merged.likeCommand.reactionEmojiId
342
- : DEFAULT_BOOT_SYSTEM_CONFIG.likeCommand.reactionEmojiId,
259
+ : DEFAULT_CORE_SYSTEM_CONFIG.likeCommand.reactionEmojiId,
343
260
  },
344
261
  friend: {
345
262
  autoApprove: Boolean(merged?.friend?.autoApprove),
@@ -349,23 +266,18 @@ function normalizeBootSystemConfig(input: any): BootSystemConfig {
349
266
  typeof merged?.group?.minMemberCount === "number" &&
350
267
  Number.isFinite(merged.group.minMemberCount)
351
268
  ? merged.group.minMemberCount
352
- : DEFAULT_BOOT_SYSTEM_CONFIG.group.minMemberCount,
353
- welcome: {
354
- enabled: Boolean(merged?.group?.welcome?.enabled),
355
- mode: merged?.group?.welcome?.mode === "text" ? "text" : "ai",
356
- text:
357
- typeof merged?.group?.welcome?.text === "string"
358
- ? merged.group.welcome.text
359
- : DEFAULT_BOOT_SYSTEM_CONFIG.group.welcome.text,
360
- aiPrompt:
361
- typeof merged?.group?.welcome?.aiPrompt === "string"
362
- ? merged.group.welcome.aiPrompt
363
- : DEFAULT_BOOT_SYSTEM_CONFIG.group.welcome.aiPrompt,
364
- },
269
+ : DEFAULT_CORE_SYSTEM_CONFIG.group.minMemberCount,
365
270
  },
366
- messageFilter: {
367
- user: normalizeAccessRuleConfig(userFilterSource),
368
- group: normalizeAccessRuleConfig(merged?.messageFilter?.group),
271
+ autoUpdate: {
272
+ enabled: Boolean(merged?.autoUpdate?.enabled),
273
+ time:
274
+ typeof merged?.autoUpdate?.time === "string" &&
275
+ merged.autoUpdate.time.trim()
276
+ ? merged.autoUpdate.time.trim()
277
+ : DEFAULT_CORE_SYSTEM_CONFIG.autoUpdate.time,
278
+ frequency: validFreq
279
+ ? merged.autoUpdate.frequency
280
+ : DEFAULT_CORE_SYSTEM_CONFIG.autoUpdate.frequency,
369
281
  },
370
282
  };
371
283
  }
@@ -408,8 +320,8 @@ function readRootPackageJson(): any {
408
320
 
409
321
  function readMiokiPackageJson(): any | null {
410
322
  const candidates = [
411
- path.join(process.cwd(), "node_modules", "mioki", "package.json"),
412
- path.join(process.cwd(), "..", "node_modules", "mioki", "package.json"),
323
+ path.join(process.cwd(), "node_modules", "mioku", "package.json"),
324
+ path.join(process.cwd(), "..", "node_modules", "mioku", "package.json"),
413
325
  ];
414
326
  for (const candidate of candidates) {
415
327
  if (fs.existsSync(candidate)) {
@@ -484,20 +396,20 @@ export function updateWebUISettings(
484
396
  return next;
485
397
  }
486
398
 
487
- export function getBootSystemConfig(): BootSystemConfig {
488
- ensureDir(path.dirname(BOOT_CONFIG_PATH));
489
- const raw = readJsonFile<any>(BOOT_CONFIG_PATH, DEFAULT_BOOT_SYSTEM_CONFIG);
490
- const normalized = normalizeBootSystemConfig(raw);
491
- writeJsonFile(BOOT_CONFIG_PATH, normalized);
399
+ export function getCoreSystemConfig(): CoreSystemConfig {
400
+ ensureDir(path.dirname(CORE_CONFIG_PATH));
401
+ const raw = readJsonFile<any>(CORE_CONFIG_PATH, DEFAULT_CORE_SYSTEM_CONFIG);
402
+ const normalized = normalizeCoreSystemConfig(raw);
403
+ writeJsonFile(CORE_CONFIG_PATH, normalized);
492
404
  return normalized;
493
405
  }
494
406
 
495
- export function updateBootSystemConfig(
496
- input: Partial<BootSystemConfig>,
497
- ): BootSystemConfig {
498
- const current = getBootSystemConfig();
499
- const next = normalizeBootSystemConfig(deepMerge(current, input || {}));
500
- writeJsonFile(BOOT_CONFIG_PATH, next);
407
+ export function updateCoreSystemConfig(
408
+ input: Partial<CoreSystemConfig>,
409
+ ): CoreSystemConfig {
410
+ const current = getCoreSystemConfig();
411
+ const next = normalizeCoreSystemConfig(deepMerge(current, input || {}));
412
+ writeJsonFile(CORE_CONFIG_PATH, next);
501
413
  return next;
502
414
  }
503
415
 
@@ -523,17 +435,31 @@ function assertSafePackageName(name: string): string {
523
435
  return trimmed;
524
436
  }
525
437
 
438
+ function npmPrefixOf(target: ManagedTarget): string {
439
+ if (target === "plugin") return "mioku-plugin-";
440
+ if (target === "service") return "mioku-service-";
441
+ return "mioku-adapter-";
442
+ }
443
+
444
+ function fullPackageName(target: ManagedTarget, name: string): string {
445
+ const prefix = npmPrefixOf(target);
446
+ return name.startsWith(prefix) ? name : `${prefix}${name}`;
447
+ }
448
+
449
+ function isNpmManagedName(name: string): boolean {
450
+ return (
451
+ name.startsWith("mioku-plugin-") ||
452
+ name.startsWith("mioku-service-") ||
453
+ name.startsWith("mioku-adapter-")
454
+ );
455
+ }
456
+
526
457
  function resolveManagedDir(target: ManagedTarget, name: string): string {
527
458
  const safeName = assertSafePackageName(name);
528
459
  const root = path.resolve(getTargetRoot(target));
529
460
 
530
- // 对于 npm 包,前端传入的是不带前缀的名称如 "boot",需要还原完整包名
531
- let fullName = safeName;
532
- if (target === "plugin" && !safeName.startsWith("mioku-plugin-")) {
533
- fullName = `mioku-plugin-${safeName}`;
534
- } else if (target === "service" && !safeName.startsWith("mioku-service-")) {
535
- fullName = `mioku-service-${safeName}`;
536
- }
461
+ // 对于 npm 包,前端传入的是不带前缀的名称如 "chat",需要还原完整包名
462
+ const fullName = fullPackageName(target, safeName);
537
463
 
538
464
  const dir = path.resolve(root, fullName);
539
465
  if (!dir.startsWith(`${root}${path.sep}`)) {
@@ -608,7 +534,7 @@ function parseGitHubRepo(
608
534
 
609
535
  const patterns = [
610
536
  /^https?:\/\/github\.com\/([^/]+)\/([^/#?]+?)(?:\.git)?(?:[/?#].*)?$/i,
611
- /^git@github\.com:([^/]+)\/([^/#?]+?)(?:\.git)?$/i,
537
+ /^git@github\.com: [^/]+\/([^/#?]+?)(?:\.git)?$/i,
612
538
  /^ssh:\/\/git@github\.com\/([^/]+)\/([^/#?]+?)(?:\.git)?$/i,
613
539
  ];
614
540
 
@@ -927,6 +853,9 @@ export function listManagedPackages(
927
853
  if (target === "service") {
928
854
  return listServicesFromNodeModules();
929
855
  }
856
+ if (target === "adapter") {
857
+ return listAdaptersFromNodeModules();
858
+ }
930
859
  const root = getTargetRoot(target);
931
860
  ensureDir(root);
932
861
  const names = fs
@@ -1007,6 +936,34 @@ function listServicesFromNodeModules(): Array<Record<string, any>> {
1007
936
  return services.sort((a, b) => a.name.localeCompare(b.name));
1008
937
  }
1009
938
 
939
+ function listAdaptersFromNodeModules(): Array<Record<string, any>> {
940
+ const adapters: Array<Record<string, any>> = [];
941
+ const modulesPath = NODE_MODULES_DIR;
942
+ if (!fs.existsSync(modulesPath)) return adapters;
943
+
944
+ const entries = fs.readdirSync(modulesPath, { withFileTypes: true });
945
+ for (const entry of entries) {
946
+ if (!entry.name.startsWith("mioku-adapter-")) continue;
947
+ const fullPath = path.join(modulesPath, entry.name);
948
+ const stat = fs.lstatSync(fullPath);
949
+ if (!stat.isDirectory() && !stat.isSymbolicLink()) continue;
950
+ const name = entry.name.replace(/^mioku-adapter-/, "");
951
+ const pkg = readPackageJson(fullPath);
952
+ adapters.push({
953
+ name,
954
+ path: fullPath,
955
+ version: pkg?.version ?? "0.0.0",
956
+ description: pkg?.description ?? "",
957
+ hasGit: fs.existsSync(path.join(fullPath, ".git")),
958
+ isSystemPlugin: false,
959
+ isSystemService: false,
960
+ repository: getRepositoryFromPackage(pkg),
961
+ requiredServices: pkg?.mioku?.services ?? [],
962
+ });
963
+ }
964
+ return adapters.sort((a, b) => a.name.localeCompare(b.name));
965
+ }
966
+
1010
967
  export async function installManagedPackage(
1011
968
  input: InstallRequest,
1012
969
  ): Promise<Record<string, any>> {
@@ -1046,8 +1003,7 @@ export async function installManagedPackage(
1046
1003
  const installedName =
1047
1004
  afterDeps.find(
1048
1005
  (name) =>
1049
- !beforeDeps.has(name) &&
1050
- (name.startsWith("mioku-plugin-") || name.startsWith("mioku-service-")),
1006
+ !beforeDeps.has(name) && isNpmManagedName(name),
1051
1007
  ) || pkgName;
1052
1008
 
1053
1009
  const packageJsonPath = path.join(
@@ -1082,8 +1038,7 @@ export async function checkUpdate(
1082
1038
  name: string,
1083
1039
  target: ManagedTarget,
1084
1040
  ): Promise<Record<string, any>> {
1085
- const isNpmPackage =
1086
- name.startsWith("mioku-plugin-") || name.startsWith("mioku-service-");
1041
+ const isNpmPackage = isNpmManagedName(name);
1087
1042
 
1088
1043
  if (isNpmPackage) {
1089
1044
  // 使用 npm view 检查 npm 包更新
@@ -1138,9 +1093,7 @@ export async function updateManagedPackage(
1138
1093
  const installCmd = getInstallCommand();
1139
1094
 
1140
1095
  // 检查是 npm 包还是 git 包
1141
- const isNpmPackage =
1142
- input.name.startsWith("mioku-plugin-") ||
1143
- input.name.startsWith("mioku-service-");
1096
+ const isNpmPackage = isNpmManagedName(input.name);
1144
1097
  const npmPackagePath = path.join(
1145
1098
  process.cwd(),
1146
1099
  "node_modules",
@@ -1214,11 +1167,9 @@ export async function removeManagedPackage(
1214
1167
  }
1215
1168
 
1216
1169
  // construct full package name from short name
1217
- const fullName = input.name.startsWith("mioku-plugin-") || input.name.startsWith("mioku-service-")
1170
+ const fullName = isNpmManagedName(input.name)
1218
1171
  ? input.name
1219
- : input.target === "plugin"
1220
- ? `mioku-plugin-${input.name}`
1221
- : `mioku-service-${input.name}`;
1172
+ : fullPackageName(input.target, input.name);
1222
1173
 
1223
1174
  const installCmd = getInstallCommand();
1224
1175
  const removeArgs = installCmd.args.map((a) =>
@@ -1316,9 +1267,9 @@ export async function getManagedPackageDetail(
1316
1267
  ): Promise<Record<string, any>> {
1317
1268
  // 插件名称前端传入时没有前缀,服务有前缀
1318
1269
  const isNpmPackage =
1319
- name.startsWith("mioku-plugin-") ||
1320
- name.startsWith("mioku-service-") ||
1321
- (target === "plugin" && /^[a-zA-Z0-9_-]+$/.test(name));
1270
+ isNpmManagedName(name) ||
1271
+ (target === "plugin" && /^[a-zA-Z0-9_-]+$/.test(name)) ||
1272
+ (target === "adapter" && /^[a-zA-Z0-9_-]+$/.test(name));
1322
1273
 
1323
1274
  // NPM 包从 npm registry 获取信息
1324
1275
  if (isNpmPackage || target === "service") {
@@ -1327,6 +1278,8 @@ export async function getManagedPackageDetail(
1327
1278
  fullName = `mioku-plugin-${name}`;
1328
1279
  } else if (target === "service" && !name.startsWith("mioku-service-")) {
1329
1280
  fullName = `mioku-service-${name}`;
1281
+ } else if (target === "adapter" && !name.startsWith("mioku-adapter-")) {
1282
+ fullName = `mioku-adapter-${name}`;
1330
1283
  }
1331
1284
 
1332
1285
  // 从 npm registry 获取包信息
@@ -1790,12 +1743,11 @@ async function notifyOwnerWebUIDownloaded(version: string): Promise<void> {
1790
1743
  return;
1791
1744
  }
1792
1745
 
1793
- const bots = Array.from(connectedBots.values());
1746
+ const bots = Array.from(connectedBots.values()).filter(Boolean);
1794
1747
  if (bots.length === 0) {
1795
1748
  return;
1796
1749
  }
1797
1750
 
1798
- const bot = bots[0];
1799
1751
  const settings = getWebUISettings();
1800
1752
  const auth = readJsonFile<{ token?: string }>(AUTH_PATH, {});
1801
1753
  const url = `http://${settings.host}:${settings.port}`;
@@ -1807,13 +1759,26 @@ async function notifyOwnerWebUIDownloaded(version: string): Promise<void> {
1807
1759
  ].join("\n");
1808
1760
 
1809
1761
  for (const ownerId of owners) {
1810
- try {
1811
- await bot.api("send_private_msg", {
1812
- user_id: Number(ownerId),
1813
- message,
1814
- });
1815
- } catch (err: any) {
1816
- logger.warn(`[webui] 通知主人 ${ownerId} 失败: ${err?.message || err}`);
1762
+ let notified = false;
1763
+ let lastError: unknown;
1764
+ for (const bot of bots) {
1765
+ if (!bot || (typeof bot.online === "boolean" && !bot.online)) {
1766
+ continue;
1767
+ }
1768
+ try {
1769
+ await bot.sendMessage({ type: "private", user_id: ownerId }, message);
1770
+ logger.info(
1771
+ `[webui] 已通过 ${bot.adapter ?? "bot"}/${bot.bot_id ?? "?"} 通知主人 ${ownerId} WebUI 已就绪`,
1772
+ );
1773
+ notified = true;
1774
+ break;
1775
+ } catch (err) {
1776
+ lastError = err;
1777
+ }
1778
+ }
1779
+ if (!notified) {
1780
+ const msg = formatSendError(lastError);
1781
+ logger.warn(`[webui] 通知主人 ${ownerId} 失败: ${msg}`);
1817
1782
  }
1818
1783
  }
1819
1784
  }
@@ -1844,7 +1809,6 @@ export async function notifyOwnersAuthTokenRefreshed(
1844
1809
  return;
1845
1810
  }
1846
1811
 
1847
- const bot = bots[0];
1848
1812
  const settings = getWebUISettings();
1849
1813
  const url = `http://${settings.host}:${settings.port}`;
1850
1814
  const expireText = new Date(expiresAt).toLocaleString("zh-CN");
@@ -1856,19 +1820,49 @@ export async function notifyOwnersAuthTokenRefreshed(
1856
1820
  ].join("\n");
1857
1821
 
1858
1822
  for (const ownerId of owners) {
1859
- try {
1860
- await bot.api("send_private_msg", {
1861
- user_id: Number(ownerId),
1862
- message,
1863
- });
1864
- } catch (err: any) {
1823
+ let notified = false;
1824
+ let lastError: unknown;
1825
+ for (const bot of bots) {
1826
+ if (!bot || (typeof bot.online === "boolean" && !bot.online)) {
1827
+ continue;
1828
+ }
1829
+ try {
1830
+ await bot.sendMessage(
1831
+ { type: "private", user_id: ownerId },
1832
+ message,
1833
+ );
1834
+ logger.info(
1835
+ `[webui] 已通过 ${bot.adapter ?? "bot"}/${bot.bot_id ?? "?"} 通知主人 ${ownerId} 密钥更新`,
1836
+ );
1837
+ notified = true;
1838
+ break;
1839
+ } catch (err) {
1840
+ lastError = err;
1841
+ }
1842
+ }
1843
+ if (!notified) {
1844
+ const msg = formatSendError(lastError);
1865
1845
  logger.warn(
1866
- `[webui] 通知主人 ${ownerId} 密钥更新失败: ${err?.message || err}`,
1846
+ `[webui] 通知主人 ${ownerId} 密钥更新失败: ${msg}`,
1867
1847
  );
1868
1848
  }
1869
1849
  }
1870
1850
  }
1871
1851
 
1852
+ /** 格式化 sendMessage 抛出的错误,方便排查(icqq 的 ApiRejection 带 code/message)。 */
1853
+ function formatSendError(err: unknown): string {
1854
+ if (!err) return "未知错误";
1855
+ if (typeof err === "string") return err;
1856
+ if (err instanceof Error) {
1857
+ const code = (err as { code?: number }).code;
1858
+ if (typeof code === "number" && Number.isFinite(code)) {
1859
+ return `${err.message} (code=${code})`;
1860
+ }
1861
+ return err.message;
1862
+ }
1863
+ return String(err);
1864
+ }
1865
+
1872
1866
  export async function updateWebUIDistFromRelease(): Promise<
1873
1867
  Record<string, any>
1874
1868
  > {
@@ -2113,7 +2107,7 @@ async function getCpuUsagePercent(): Promise<number> {
2113
2107
  return Number((((totalDelta - idleDelta) / totalDelta) * 100).toFixed(1));
2114
2108
  }
2115
2109
 
2116
- function toHttpBaseUrl(bot: any): string {
2110
+ function toHttpBaseUrl(bot: { options?: { protocol?: string; host?: string; port?: number } }): string {
2117
2111
  const protocol = String(bot.options?.protocol || "ws");
2118
2112
  const httpProtocol = protocol === "wss" ? "https" : "http";
2119
2113
  const host = bot.options?.host || "127.0.0.1";
@@ -2148,15 +2142,23 @@ function normalizeVersionSpec(input: string): string {
2148
2142
  return matched?.[0] || cleaned;
2149
2143
  }
2150
2144
 
2151
- async function getBotDetails(bot: any): Promise<Record<string, any>> {
2145
+ async function getBotDetails(bot: import("mioku").Bot): Promise<Record<string, any>> {
2146
+ const botAny = bot as unknown as {
2147
+ uin?: number | string;
2148
+ user_id?: number | string;
2149
+ name?: string;
2150
+ options?: { protocol?: string; host?: string; port?: number };
2151
+ app_version?: string;
2152
+ };
2153
+ const qq = String(botAny.uin ?? botAny.user_id ?? bot.bot_id);
2152
2154
  const base = {
2153
- botId: bot?.bot_id || bot?.uin || 0,
2154
- qq: bot?.uin || bot?.user_id || 0,
2155
- nickname: bot?.nickname || bot?.name || "Unknown Bot",
2156
- avatar: `https://q1.qlogo.cn/g?b=qq&nk=${bot?.uin || bot?.user_id || 0}&s=160`,
2157
- online: true,
2158
- napcatVersion: bot?.app_version || "unknown",
2159
- napcatApiBase: toHttpBaseUrl(bot),
2155
+ botId: bot.bot_id,
2156
+ qq,
2157
+ nickname: bot.nickname || botAny.name || "Unknown Bot",
2158
+ avatar: `https://q1.qlogo.cn/g?b=qq&nk=${qq}&s=160`,
2159
+ online: bot.online,
2160
+ napcatVersion: botAny.app_version || "unknown",
2161
+ napcatApiBase: toHttpBaseUrl(botAny),
2160
2162
  groupCount: 0,
2161
2163
  friendCount: 0,
2162
2164
  onlineDurationMs: 0,
@@ -2165,8 +2167,8 @@ async function getBotDetails(bot: any): Promise<Record<string, any>> {
2165
2167
 
2166
2168
  try {
2167
2169
  const [status, versionInfo, groups, friends] = await Promise.all([
2168
- bot.api("get_status").catch(() => null),
2169
- bot.api("get_version_info").catch(() => null),
2170
+ bot.sendApi<{ stat?: { start_time?: number; online?: boolean }; online?: boolean }>("get_status").catch(() => null),
2171
+ bot.sendApi<{ app_version?: string }>("get_version_info").catch(() => null),
2170
2172
  bot.getGroupList().catch(() => []),
2171
2173
  bot.getFriendList().catch(() => []),
2172
2174
  ]);
@@ -2307,6 +2309,24 @@ export function updateChatConfig(fileName: string, data: any): any {
2307
2309
  return data;
2308
2310
  }
2309
2311
 
2312
+ export function getAdapterConfigs(adapterName: string): any {
2313
+ const rootPkg = readRootPackageJson();
2314
+ const adapters = rootPkg?.mioku?.adapters ?? {};
2315
+ const safeName = String(adapterName || "").trim().replace(/[^a-zA-Z0-9_-]/g, "");
2316
+ return adapters[safeName] ?? {};
2317
+ }
2318
+
2319
+ export function updateAdapterConfig(adapterName: string, data: any): any {
2320
+ const safeName = String(adapterName || "").trim().replace(/[^a-zA-Z0-9_-]/g, "");
2321
+ if (!safeName) throw new Error("适配器名称无效");
2322
+ const rootPkg = readRootPackageJson();
2323
+ rootPkg.mioku = rootPkg.mioku ?? {};
2324
+ rootPkg.mioku.adapters = rootPkg.mioku.adapters ?? {};
2325
+ rootPkg.mioku.adapters[safeName] = data ?? {};
2326
+ writeRootPackageJson(rootPkg);
2327
+ return data;
2328
+ }
2329
+
2310
2330
  export interface MiokuConfig {
2311
2331
  owners: number[];
2312
2332
  admins: number[];
@@ -2318,7 +2338,7 @@ export interface MiokuConfig {
2318
2338
  token: string;
2319
2339
  }>;
2320
2340
  plugins: string[];
2321
- boot: BootSystemConfig;
2341
+ core: CoreSystemConfig;
2322
2342
  }
2323
2343
 
2324
2344
  export function getMiokuConfig(): MiokuConfig {
@@ -2333,7 +2353,7 @@ export function getMiokuConfig(): MiokuConfig {
2333
2353
  admins: Array.isArray(miokiConfig.admins) ? miokiConfig.admins : [],
2334
2354
  napcat: Array.isArray(miokiConfig.napcat) ? miokiConfig.napcat : [],
2335
2355
  plugins: Array.isArray(miokiConfig.plugins) ? miokiConfig.plugins : [],
2336
- boot: getBootSystemConfig(),
2356
+ core: getCoreSystemConfig(),
2337
2357
  };
2338
2358
  }
2339
2359
 
@@ -2344,13 +2364,16 @@ export function updateMiokuConfig(config: Partial<MiokuConfig>): MiokuConfig {
2344
2364
  admins: Array.isArray(config.admins) ? config.admins : current.admins,
2345
2365
  napcat: Array.isArray(config.napcat) ? config.napcat : current.napcat,
2346
2366
  plugins: Array.isArray(config.plugins) ? config.plugins : current.plugins,
2347
- boot: config.boot ? updateBootSystemConfig(config.boot) : current.boot,
2367
+ core: config.core ? updateCoreSystemConfig(config.core) : current.core,
2348
2368
  };
2349
2369
 
2350
2370
  const localConfig = readJsonFile<any>(LOCAL_CONFIG_PATH, { mioki: {} });
2351
2371
  localConfig.mioki = {
2352
2372
  ...localConfig.mioki,
2353
- ...updated,
2373
+ owners: updated.owners,
2374
+ admins: updated.admins,
2375
+ napcat: updated.napcat,
2376
+ plugins: updated.plugins,
2354
2377
  };
2355
2378
  writeJsonFile(LOCAL_CONFIG_PATH, localConfig);
2356
2379
 
@@ -2421,9 +2444,6 @@ export function togglePlugin(name: string, enabled: boolean): PluginStatusItem {
2421
2444
  if (enabled) {
2422
2445
  plugins.add(safeName);
2423
2446
  } else {
2424
- if (safeName.toLowerCase() === "boot") {
2425
- throw new Error("boot 插件不可关闭");
2426
- }
2427
2447
  plugins.delete(safeName);
2428
2448
  }
2429
2449
 
package/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // 管理类型
2
- export type ManagedTarget = "plugin" | "service";
2
+ export type ManagedTarget = "plugin" | "service" | "adapter";
3
3
 
4
4
  // 包管理器
5
5
  export type PackageManager = "bun";
package/utils.ts CHANGED
@@ -133,9 +133,14 @@ export function isNpmPackageName(name: string): boolean {
133
133
 
134
134
  export function resolveNpmPackageName(
135
135
  name: string,
136
- target: "plugin" | "service",
136
+ target: "plugin" | "service" | "adapter",
137
137
  ): string {
138
- const prefix = target === "plugin" ? "mioku-plugin-" : "mioku-service-";
138
+ const prefix =
139
+ target === "plugin"
140
+ ? "mioku-plugin-"
141
+ : target === "service"
142
+ ? "mioku-service-"
143
+ : "mioku-adapter-";
139
144
  if (name.startsWith(prefix)) {
140
145
  return name;
141
146
  }