mioku-service-webui 4.7.0 → 4.8.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.
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());
@@ -239,8 +247,6 @@ const runtime = new WebUIRuntime();
239
247
 
240
248
  const webUIService: MiokuService = {
241
249
  name: "webui",
242
- version: "4.2.0",
243
- description: "Mioku WebUI 管理服务",
244
250
  api: {
245
251
  getSettings: () => getWebUISettings(),
246
252
  } as WebUIServiceAPI,
@@ -250,9 +256,13 @@ const webUIService: MiokuService = {
250
256
  ensureAuthConfig();
251
257
  await ensureWebUIDist();
252
258
  await runtime.initRoutes();
259
+ // 启动密钥定时巡检:即便长时间没有前端访问 / 登录请求,
260
+ // 也会在密钥接近过期时主动刷新并通知主人。
261
+ startAuthRefreshTimer();
253
262
  },
254
263
 
255
264
  async dispose() {
265
+ stopAuthRefreshTimer();
256
266
  runtime.stop();
257
267
  },
258
268
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-service-webui",
3
- "version": "4.7.0",
3
+ "version": "4.8.1",
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";
5
- import { readJsonFile, writeJsonFile, NODE_MODULES_DIR } from "../utils";
4
+ import { getActiveCommandManager, logger } from "mioku";
5
+ import { isPackageDir, 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 = {
@@ -38,24 +38,37 @@ interface AccessItem {
38
38
  desc?: string;
39
39
  match?: string;
40
40
  event?: string;
41
+ permission?: string;
42
+ priority?: number;
41
43
  fromHook: boolean;
42
44
  }
43
45
 
44
46
  function readAccessCatalog(): AccessItem[] {
47
+ const manager = getActiveCommandManager();
48
+ if (manager) {
49
+ return manager.catalog().map((item) => ({
50
+ kind: item.kind,
51
+ plugin: item.plugin,
52
+ id: item.id,
53
+ label: item.label,
54
+ desc: item.desc,
55
+ match: item.match,
56
+ event: item.event,
57
+ permission: item.permission,
58
+ priority: item.priority,
59
+ fromHook:
60
+ item.source === "manifest" && Boolean(item.match || item.event),
61
+ }));
62
+ }
45
63
  if (!fs.existsSync(NODE_MODULES_DIR)) return [];
46
64
  const entries = fs.readdirSync(NODE_MODULES_DIR, { withFileTypes: true });
47
65
  const items: AccessItem[] = [];
48
66
 
49
67
  for (const entry of entries) {
50
- if (!entry.name.startsWith("mioku-plugin-")) continue;
68
+ if (!entry.name.toLowerCase().startsWith("mioku-plugin-")) continue;
51
69
  const fullPath = path.join(NODE_MODULES_DIR, entry.name);
52
- let stat: fs.Stats;
53
- try {
54
- stat = fs.lstatSync(fullPath);
55
- } catch {
56
- continue;
57
- }
58
- if (!stat.isDirectory() && !stat.isSymbolicLink()) continue;
70
+ // Windows 上 pnpm/bun workspace 会用 NTFS junction,旧 lstat 检查会误判
71
+ if (!isPackageDir(fullPath)) continue;
59
72
 
60
73
  const pkgPath = path.join(fullPath, "package.json");
61
74
  if (!fs.existsSync(pkgPath)) continue;
@@ -66,7 +79,7 @@ function readAccessCatalog(): AccessItem[] {
66
79
  continue;
67
80
  }
68
81
 
69
- const name = entry.name.replace(/^mioku-plugin-/, "");
82
+ const name = entry.name.replace(/^mioku-plugin-/i, "");
70
83
  const mioku = pkg.mioku || {};
71
84
  const help = mioku.help || { title: name, description: "", commands: [] };
72
85
  const hooks: Array<{ id: string; match?: string; event?: string; description?: string }> =
@@ -0,0 +1,85 @@
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
+ import { isPackageDir } from "../utils";
7
+
8
+ function listAdaptersFromNodeModules(): string[] {
9
+ const modulesPath = path.join(process.cwd(), "node_modules");
10
+ const adapters: string[] = [];
11
+ if (!fs.existsSync(modulesPath)) return adapters;
12
+
13
+ let entries: import("fs").Dirent[];
14
+ try {
15
+ entries = fs.readdirSync(modulesPath, { withFileTypes: true });
16
+ } catch {
17
+ return adapters;
18
+ }
19
+
20
+ for (const entry of entries) {
21
+ if (!entry.name.toLowerCase().startsWith("mioku-adapter-")) continue;
22
+ const fullPath = path.join(modulesPath, entry.name);
23
+ if (!isPackageDir(fullPath)) continue;
24
+ adapters.push(entry.name.replace(/^mioku-adapter-/i, ""));
25
+ }
26
+ return adapters.sort((a, b) => a.localeCompare(b));
27
+ }
28
+
29
+ export function createAdapterConfigRoutes() {
30
+ const app = new Hono();
31
+
32
+ app.get("/overview", (c) => {
33
+ const adapters = listAdaptersFromNodeModules();
34
+ const overview = adapters
35
+ .map((name) => {
36
+ const manifest = loadAdapterConfigPage(name);
37
+ return {
38
+ name,
39
+ title: manifest?.title || name,
40
+ description: manifest?.description,
41
+ hasPage: manifest?.hasCustomPage || false,
42
+ };
43
+ })
44
+ .filter((a) => a.hasPage);
45
+
46
+ return c.json({ ok: true, data: overview });
47
+ });
48
+
49
+ app.get("/:name/page", (c) => {
50
+ const name = c.req.param("name");
51
+ const manifest = loadAdapterConfigPage(name);
52
+ if (!manifest) {
53
+ return c.json({ ok: true, data: null });
54
+ }
55
+
56
+ return c.json({
57
+ ok: true,
58
+ data: {
59
+ plugin: manifest.plugin,
60
+ title: manifest.title,
61
+ description: manifest.description,
62
+ markdown: manifest.markdown,
63
+ fields: manifest.fields,
64
+ hasCustomPage: manifest.hasCustomPage,
65
+ configs: {
66
+ [name]: getAdapterConfigs(name),
67
+ },
68
+ },
69
+ });
70
+ });
71
+
72
+ app.get("/:name", (c) => {
73
+ const name = c.req.param("name");
74
+ return c.json({ ok: true, data: getAdapterConfigs(name) });
75
+ });
76
+
77
+ app.put("/:name", async (c) => {
78
+ const name = c.req.param("name");
79
+ const body = await c.req.json();
80
+ const data = updateAdapterConfig(name, body);
81
+ return c.json({ ok: true, data });
82
+ });
83
+
84
+ return app;
85
+ }
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) => {
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,