mioku-service-webui 4.0.9 → 4.3.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/index.ts +11 -2
- package/package.json +1 -1
- package/routes/access-control.ts +162 -0
- package/routes/index.ts +2 -0
- package/routes/stranger.ts +145 -0
- package/system.ts +26 -0
package/index.ts
CHANGED
|
@@ -8,7 +8,12 @@ import { createNodeWebSocket } from "@hono/node-ws";
|
|
|
8
8
|
import type { MiokuService } from "mioku";
|
|
9
9
|
import type { WebUISettings } from "./types";
|
|
10
10
|
import { ensureAuthConfig, loginWithToken, requireAuth } from "./auth";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
ensureWebUIDist,
|
|
13
|
+
getWebUISettings,
|
|
14
|
+
getSystemOverview,
|
|
15
|
+
getSaying,
|
|
16
|
+
} from "./system";
|
|
12
17
|
import { CHAT_CONFIG_DIR, CHAT_DATA_DIR, LOGS_DIR, WEBUI_DIST } from "./utils";
|
|
13
18
|
import { initBuiltinDatasources } from "./datasources";
|
|
14
19
|
import {
|
|
@@ -21,6 +26,8 @@ import {
|
|
|
21
26
|
createManageRoutes,
|
|
22
27
|
createStoreRoutes,
|
|
23
28
|
createDataManagementRoutes,
|
|
29
|
+
createStrangerRoutes,
|
|
30
|
+
createAccessControlRoutes,
|
|
24
31
|
} from "./routes";
|
|
25
32
|
|
|
26
33
|
export interface WebUIServiceAPI {
|
|
@@ -100,6 +107,8 @@ class WebUIRuntime {
|
|
|
100
107
|
this.app.route("/api/plugin-config", createPluginConfigRoutes());
|
|
101
108
|
this.app.route("/api/meme", createMemeRoutes());
|
|
102
109
|
this.app.route("/api/data-management", createDataManagementRoutes());
|
|
110
|
+
this.app.route("/api/stranger", createStrangerRoutes());
|
|
111
|
+
this.app.route("/api/access-control", createAccessControlRoutes());
|
|
103
112
|
|
|
104
113
|
this.app.get(
|
|
105
114
|
"/api/ws/logs",
|
|
@@ -211,7 +220,7 @@ const runtime = new WebUIRuntime();
|
|
|
211
220
|
|
|
212
221
|
const webUIService: MiokuService = {
|
|
213
222
|
name: "webui",
|
|
214
|
-
version: "2.0
|
|
223
|
+
version: "4.2.0",
|
|
215
224
|
description: "Mioku WebUI 管理服务",
|
|
216
225
|
api: {
|
|
217
226
|
getSettings: () => getWebUISettings(),
|
package/package.json
CHANGED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { logger } from "mioki";
|
|
5
|
+
import { readJsonFile, writeJsonFile, NODE_MODULES_DIR } from "../utils";
|
|
6
|
+
|
|
7
|
+
const ACCESS_CONFIG_PATH = path.resolve(
|
|
8
|
+
process.cwd(),
|
|
9
|
+
"config/boot/access-control.json",
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const DEFAULT_CONFIG = {
|
|
13
|
+
version: 1,
|
|
14
|
+
global: { plugins: {}, commands: {} },
|
|
15
|
+
groups: {},
|
|
16
|
+
users: {},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function normalizeAccessConfig(input: any) {
|
|
20
|
+
return {
|
|
21
|
+
version: 1,
|
|
22
|
+
global: {
|
|
23
|
+
plugins: input?.global?.plugins ?? {},
|
|
24
|
+
commands: input?.global?.commands ?? {},
|
|
25
|
+
},
|
|
26
|
+
groups: input?.groups ?? {},
|
|
27
|
+
users: input?.users ?? {},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type AccessItemKind = "plugin" | "command";
|
|
32
|
+
|
|
33
|
+
interface AccessItem {
|
|
34
|
+
kind: AccessItemKind;
|
|
35
|
+
plugin: string;
|
|
36
|
+
id: string;
|
|
37
|
+
label: string;
|
|
38
|
+
desc?: string;
|
|
39
|
+
match?: string;
|
|
40
|
+
event?: string;
|
|
41
|
+
fromHook: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readAccessCatalog(): AccessItem[] {
|
|
45
|
+
if (!fs.existsSync(NODE_MODULES_DIR)) return [];
|
|
46
|
+
const entries = fs.readdirSync(NODE_MODULES_DIR, { withFileTypes: true });
|
|
47
|
+
const items: AccessItem[] = [];
|
|
48
|
+
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (!entry.name.startsWith("mioku-plugin-")) continue;
|
|
51
|
+
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;
|
|
59
|
+
|
|
60
|
+
const pkgPath = path.join(fullPath, "package.json");
|
|
61
|
+
if (!fs.existsSync(pkgPath)) continue;
|
|
62
|
+
let pkg: any;
|
|
63
|
+
try {
|
|
64
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
65
|
+
} catch {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const name = entry.name.replace(/^mioku-plugin-/, "");
|
|
70
|
+
const mioku = pkg.mioku || {};
|
|
71
|
+
const help = mioku.help || { title: name, description: "", commands: [] };
|
|
72
|
+
const hooks: Array<{ id: string; match?: string; event?: string; description?: string }> =
|
|
73
|
+
mioku.accessHooks || [];
|
|
74
|
+
const helpCommands: Array<any> = help.commands || [];
|
|
75
|
+
|
|
76
|
+
items.push({
|
|
77
|
+
kind: "plugin",
|
|
78
|
+
plugin: name,
|
|
79
|
+
id: name,
|
|
80
|
+
label: help.title || name,
|
|
81
|
+
desc: help.description,
|
|
82
|
+
fromHook: false,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const seen = new Set<string>();
|
|
86
|
+
for (const hook of hooks) {
|
|
87
|
+
seen.add(hook.id);
|
|
88
|
+
items.push({
|
|
89
|
+
kind: "command",
|
|
90
|
+
plugin: name,
|
|
91
|
+
id: hook.id,
|
|
92
|
+
label: hook.id,
|
|
93
|
+
desc: hook.description,
|
|
94
|
+
match: hook.match,
|
|
95
|
+
event: hook.event,
|
|
96
|
+
fromHook: true,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
for (const c of helpCommands) {
|
|
100
|
+
const id = String(c.cmd || "").trim();
|
|
101
|
+
if (!id || seen.has(id)) continue;
|
|
102
|
+
items.push({
|
|
103
|
+
kind: "command",
|
|
104
|
+
plugin: name,
|
|
105
|
+
id,
|
|
106
|
+
label: id,
|
|
107
|
+
desc: c.desc,
|
|
108
|
+
fromHook: false,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return items.sort((a, b) => {
|
|
114
|
+
if (a.kind !== b.kind) return a.kind === "plugin" ? -1 : 1;
|
|
115
|
+
if (a.plugin !== b.plugin) return a.plugin.localeCompare(b.plugin);
|
|
116
|
+
return a.id.localeCompare(b.id);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function filterCatalog(items: AccessItem[], query?: string): AccessItem[] {
|
|
121
|
+
if (!query) return items;
|
|
122
|
+
const q = query.trim().toLowerCase();
|
|
123
|
+
if (!q) return items;
|
|
124
|
+
return items.filter((it) => {
|
|
125
|
+
return (
|
|
126
|
+
it.id.toLowerCase().includes(q) ||
|
|
127
|
+
it.label.toLowerCase().includes(q) ||
|
|
128
|
+
it.plugin.toLowerCase().includes(q) ||
|
|
129
|
+
(it.desc || "").toLowerCase().includes(q)
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function createAccessControlRoutes() {
|
|
135
|
+
const app = new Hono();
|
|
136
|
+
|
|
137
|
+
app.get("/", (c) => {
|
|
138
|
+
const data = readJsonFile(ACCESS_CONFIG_PATH, DEFAULT_CONFIG);
|
|
139
|
+
return c.json({ ok: true, data: normalizeAccessConfig(data) });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
app.put("/", async (c) => {
|
|
143
|
+
const body = await c.req.json().catch(() => null);
|
|
144
|
+
if (!body) {
|
|
145
|
+
return c.json({ ok: false, error: "INVALID_BODY" }, 400);
|
|
146
|
+
}
|
|
147
|
+
const data = normalizeAccessConfig(body);
|
|
148
|
+
writeJsonFile(ACCESS_CONFIG_PATH, data);
|
|
149
|
+
logger.info(
|
|
150
|
+
`[webui-action] access-control.update | groups=${Object.keys(data.groups).length} users=${Object.keys(data.users).length}`,
|
|
151
|
+
);
|
|
152
|
+
return c.json({ ok: true, data });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
app.get("/catalog", (c) => {
|
|
156
|
+
const q = c.req.query("q");
|
|
157
|
+
const data = filterCatalog(readAccessCatalog(), q);
|
|
158
|
+
return c.json({ ok: true, data });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return app;
|
|
162
|
+
}
|
package/routes/index.ts
CHANGED
|
@@ -5,3 +5,5 @@ export { createDBRoutes, createPluginConfigRoutes, createMemeRoutes } from "./db
|
|
|
5
5
|
export { createManageRoutes } from "./manage";
|
|
6
6
|
export { createStoreRoutes } from "./store";
|
|
7
7
|
export { createDataManagementRoutes } from "./data-management";
|
|
8
|
+
export { createStrangerRoutes } from "./stranger";
|
|
9
|
+
export { createAccessControlRoutes } from "./access-control";
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { connectedBots, logger } from "mioki";
|
|
3
|
+
|
|
4
|
+
interface StrangerInfoResponse {
|
|
5
|
+
ok: boolean;
|
|
6
|
+
data?: { userId: string; nickname: string };
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface CacheEntry {
|
|
11
|
+
nickname: string | null;
|
|
12
|
+
expiresAt: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
16
|
+
const FETCH_TIMEOUT_MS = 4000;
|
|
17
|
+
|
|
18
|
+
const nicknameCache = new Map<string, CacheEntry>();
|
|
19
|
+
const inflight = new Map<string, Promise<string | null>>();
|
|
20
|
+
|
|
21
|
+
function getConnectedBots(): any[] {
|
|
22
|
+
return Array.from(connectedBots.values()).filter(Boolean);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeUserId(input: unknown): string {
|
|
26
|
+
return String(input ?? "").trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isValidQQNumber(id: string): boolean {
|
|
30
|
+
return /^\d{5,12}$/.test(id);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getCached(userId: string): string | null | undefined {
|
|
34
|
+
const entry = nicknameCache.get(userId);
|
|
35
|
+
if (!entry) return undefined;
|
|
36
|
+
if (entry.expiresAt < Date.now()) {
|
|
37
|
+
nicknameCache.delete(userId);
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
return entry.nickname;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function setCached(userId: string, nickname: string | null): void {
|
|
44
|
+
nicknameCache.set(userId, {
|
|
45
|
+
nickname,
|
|
46
|
+
expiresAt: Date.now() + CACHE_TTL_MS,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function fetchNicknameFromBot(
|
|
51
|
+
bot: any,
|
|
52
|
+
userId: string,
|
|
53
|
+
): Promise<string | null> {
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
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();
|
|
60
|
+
return nickname || null;
|
|
61
|
+
} catch (error: any) {
|
|
62
|
+
logger.warn(
|
|
63
|
+
`stranger-info: bot ${bot?.uin || bot?.bot_id || "?"} 查询 ${userId} 失败: ${error?.message || error}`,
|
|
64
|
+
);
|
|
65
|
+
return null;
|
|
66
|
+
} finally {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function lookupNickname(userId: string): Promise<string | null> {
|
|
72
|
+
const cached = getCached(userId);
|
|
73
|
+
if (cached !== undefined) return cached;
|
|
74
|
+
|
|
75
|
+
const pending = inflight.get(userId);
|
|
76
|
+
if (pending) return pending;
|
|
77
|
+
|
|
78
|
+
const promise = (async () => {
|
|
79
|
+
const bots = getConnectedBots();
|
|
80
|
+
if (bots.length === 0) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const results = await Promise.all(
|
|
84
|
+
bots.map((bot) => fetchNicknameFromBot(bot, userId)),
|
|
85
|
+
);
|
|
86
|
+
for (const nickname of results) {
|
|
87
|
+
if (nickname) {
|
|
88
|
+
setCached(userId, nickname);
|
|
89
|
+
return nickname;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
setCached(userId, null);
|
|
93
|
+
return null;
|
|
94
|
+
})();
|
|
95
|
+
|
|
96
|
+
inflight.set(userId, promise);
|
|
97
|
+
try {
|
|
98
|
+
return await promise;
|
|
99
|
+
} finally {
|
|
100
|
+
inflight.delete(userId);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function createStrangerRoutes() {
|
|
105
|
+
const app = new Hono();
|
|
106
|
+
|
|
107
|
+
app.post("/info", async (c) => {
|
|
108
|
+
const body = await c.req.json().catch(() => ({}));
|
|
109
|
+
const userId = normalizeUserId(body?.user_id);
|
|
110
|
+
if (!isValidQQNumber(userId)) {
|
|
111
|
+
return c.json(
|
|
112
|
+
{ ok: false, error: "INVALID_USER_ID" } satisfies StrangerInfoResponse,
|
|
113
|
+
400,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const nickname = await lookupNickname(userId);
|
|
119
|
+
if (!nickname) {
|
|
120
|
+
return c.json(
|
|
121
|
+
{
|
|
122
|
+
ok: false,
|
|
123
|
+
error: "NICKNAME_NOT_FOUND",
|
|
124
|
+
} satisfies StrangerInfoResponse,
|
|
125
|
+
404,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return c.json({
|
|
129
|
+
ok: true,
|
|
130
|
+
data: { userId, nickname },
|
|
131
|
+
} satisfies StrangerInfoResponse);
|
|
132
|
+
} catch (error: any) {
|
|
133
|
+
logger.error(`stranger-info 失败: ${error?.message || error}`);
|
|
134
|
+
return c.json(
|
|
135
|
+
{
|
|
136
|
+
ok: false,
|
|
137
|
+
error: "INTERNAL_ERROR",
|
|
138
|
+
} satisfies StrangerInfoResponse,
|
|
139
|
+
500,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return app;
|
|
145
|
+
}
|
package/system.ts
CHANGED
|
@@ -322,6 +322,26 @@ function readPackageJson(dir: string): any {
|
|
|
322
322
|
}
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
const WEBUI_PLUGIN_PREFIX = "mioku-plugin-";
|
|
326
|
+
|
|
327
|
+
function appendToMiokiPlugins(cwd: string, pkgName: string): boolean {
|
|
328
|
+
if (!pkgName.startsWith(WEBUI_PLUGIN_PREFIX)) return false;
|
|
329
|
+
const shortName = pkgName.slice(WEBUI_PLUGIN_PREFIX.length);
|
|
330
|
+
|
|
331
|
+
const packageJsonPath = path.join(cwd, "package.json");
|
|
332
|
+
if (!fs.existsSync(packageJsonPath)) return false;
|
|
333
|
+
|
|
334
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
335
|
+
const mioki = pkg.mioki ?? {};
|
|
336
|
+
const plugins = Array.isArray(mioki.plugins) ? [...mioki.plugins] : [];
|
|
337
|
+
if (plugins.includes(shortName)) return false;
|
|
338
|
+
|
|
339
|
+
plugins.push(shortName);
|
|
340
|
+
pkg.mioki = { ...mioki, plugins };
|
|
341
|
+
fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
|
|
325
345
|
function readRootPackageJson(): any {
|
|
326
346
|
return JSON.parse(fs.readFileSync(ROOT_PACKAGE_PATH, "utf-8"));
|
|
327
347
|
}
|
|
@@ -1020,12 +1040,18 @@ export async function installManagedPackage(
|
|
|
1020
1040
|
makeManagedUpdateCacheKey(input.target, installedName),
|
|
1021
1041
|
);
|
|
1022
1042
|
|
|
1043
|
+
let enabled = false;
|
|
1044
|
+
if (input.target === "plugin") {
|
|
1045
|
+
enabled = appendToMiokiPlugins(installDir, installedName);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1023
1048
|
return {
|
|
1024
1049
|
ok: true,
|
|
1025
1050
|
name: installedName,
|
|
1026
1051
|
missingServices,
|
|
1027
1052
|
packageManager: "bun",
|
|
1028
1053
|
restartRequired: true,
|
|
1054
|
+
enabled,
|
|
1029
1055
|
installOutput: install.stdout || install.stderr,
|
|
1030
1056
|
};
|
|
1031
1057
|
}
|