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 +68 -1
- package/config-page-loader.ts +9 -1
- package/datasources.ts +1 -1
- package/index.ts +14 -4
- package/package.json +4 -5
- package/port-utils.ts +1 -1
- package/routes/access-control.ts +25 -12
- package/routes/adapter-config.ts +85 -0
- package/routes/ai.ts +27 -3
- package/routes/config.ts +2 -2
- package/routes/data-management.ts +1 -1
- package/routes/index.ts +1 -0
- package/routes/manage.ts +13 -2
- package/routes/store.ts +21 -10
- package/routes/stranger.ts +17 -18
- package/system.ts +231 -210
- package/types.ts +1 -1
- package/utils.ts +12 -2
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();
|
package/config-page-loader.ts
CHANGED
|
@@ -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 "
|
|
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
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 "
|
|
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 {
|
|
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.
|
|
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.
|
|
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
package/routes/access-control.ts
CHANGED
|
@@ -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 "
|
|
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/
|
|
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
|
-
|
|
53
|
-
|
|
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 "
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
:
|
|
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:
|
|
225
|
+
installTarget: type,
|
|
217
226
|
installPath:
|
|
218
227
|
type === "plugin"
|
|
219
228
|
? `plugins/${inferDisplayName(packageName, "plugin")}`
|
|
220
|
-
:
|
|
229
|
+
: type === "service"
|
|
230
|
+
? getServiceInstallPath(latest, packageName)
|
|
231
|
+
: `adapters/${inferDisplayName(packageName, "adapter")}`,
|
|
221
232
|
},
|
|
222
233
|
});
|
|
223
234
|
} catch (error: any) {
|
package/routes/stranger.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import {
|
|
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():
|
|
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:
|
|
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
|
|
58
|
-
const
|
|
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
|
|
60
|
+
} catch (error) {
|
|
62
61
|
logger.warn(
|
|
63
|
-
`stranger-info: bot ${bot
|
|
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
|
|
133
|
-
logger.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,
|