mioku-service-webui 4.5.0 → 4.6.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/database.ts +3 -1
- package/package.json +1 -1
- package/port-utils.ts +26 -1
- package/routes/config.ts +17 -0
- package/routes/db.ts +4 -1
- package/system.ts +113 -91
- package/utils.ts +2 -29
package/database.ts
CHANGED
|
@@ -300,7 +300,9 @@ export function listMemeTree(): Record<string, Record<string, string[]>> {
|
|
|
300
300
|
tree[character][emotion] = fs
|
|
301
301
|
.readdirSync(emotionDir)
|
|
302
302
|
.filter((name) => /\.(png|jpg|jpeg|gif|webp)$/i.test(name))
|
|
303
|
-
.map((name) =>
|
|
303
|
+
.map((name) =>
|
|
304
|
+
path.posix.join("data", "chat", "meme", character, emotion, name),
|
|
305
|
+
);
|
|
304
306
|
}
|
|
305
307
|
}
|
|
306
308
|
|
package/package.json
CHANGED
package/port-utils.ts
CHANGED
|
@@ -41,9 +41,33 @@ function parsePids(raw: string): number[] {
|
|
|
41
41
|
return Array.from(pids);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function parseNetstatPids(raw: string, port: number): number[] {
|
|
45
|
+
const pids = new Set<number>();
|
|
46
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
47
|
+
const cols = line.trim().split(/\s+/);
|
|
48
|
+
if (cols.length < 5 || cols[0].toUpperCase() !== "TCP") continue;
|
|
49
|
+
if (!/^LISTENING$/i.test(cols[3])) continue;
|
|
50
|
+
// Local address is host:port — IPv6 forms look like [::]:3339.
|
|
51
|
+
const localPort = cols[1].slice(cols[1].lastIndexOf(":") + 1);
|
|
52
|
+
if (Number(localPort) !== port) continue;
|
|
53
|
+
const pid = Number(cols[4]);
|
|
54
|
+
if (Number.isInteger(pid) && pid > 0) pids.add(pid);
|
|
55
|
+
}
|
|
56
|
+
return Array.from(pids);
|
|
57
|
+
}
|
|
58
|
+
|
|
44
59
|
async function listListeningPids(
|
|
45
60
|
port: number,
|
|
46
61
|
): Promise<{ pids: number[]; lsofAvailable: boolean }> {
|
|
62
|
+
if (process.platform === "win32") {
|
|
63
|
+
try {
|
|
64
|
+
const { stdout } = await execFileAsync("netstat", ["-ano", "-p", "TCP"]);
|
|
65
|
+
return { pids: parseNetstatPids(stdout, port), lsofAvailable: true };
|
|
66
|
+
} catch (err: any) {
|
|
67
|
+
return { pids: [], lsofAvailable: err?.code !== "ENOENT" };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
47
71
|
try {
|
|
48
72
|
const { stdout } = await execFileAsync("lsof", [
|
|
49
73
|
"-nP",
|
|
@@ -84,8 +108,9 @@ export async function freePort(
|
|
|
84
108
|
const initial = await listListeningPids(port);
|
|
85
109
|
|
|
86
110
|
if (!initial.lsofAvailable) {
|
|
111
|
+
const missing = process.platform === "win32" ? "netstat" : "lsof";
|
|
87
112
|
log.warn(
|
|
88
|
-
`未检测到
|
|
113
|
+
`未检测到 ${missing} 命令,无法定位端口 ${port} 的占用进程,建议安装 ${missing}`,
|
|
89
114
|
);
|
|
90
115
|
return {
|
|
91
116
|
port,
|
package/routes/config.ts
CHANGED
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
getMiokuConfig,
|
|
5
5
|
updateMiokuConfig,
|
|
6
6
|
getAvailablePlugins,
|
|
7
|
+
getPluginStatusList,
|
|
8
|
+
togglePlugin,
|
|
7
9
|
checkMiokuReleaseUpdate,
|
|
8
10
|
updateMiokuFromMain,
|
|
9
11
|
} from "../system";
|
|
@@ -48,5 +50,20 @@ export function createConfigRoutes() {
|
|
|
48
50
|
return c.json({ ok: true, data });
|
|
49
51
|
});
|
|
50
52
|
|
|
53
|
+
app.get("/plugins", (c) => {
|
|
54
|
+
const data = getPluginStatusList();
|
|
55
|
+
return c.json({ ok: true, data });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
app.post("/plugins/toggle", async (c) => {
|
|
59
|
+
const body = (await c.req.json()) as { name: string; enabled: boolean };
|
|
60
|
+
const data = togglePlugin(body.name, body.enabled);
|
|
61
|
+
logger.info(`[webui-action] config.plugins.toggle`, {
|
|
62
|
+
name: body.name,
|
|
63
|
+
enabled: body.enabled,
|
|
64
|
+
});
|
|
65
|
+
return c.json({ ok: true, data });
|
|
66
|
+
});
|
|
67
|
+
|
|
51
68
|
return app;
|
|
52
69
|
}
|
package/routes/db.ts
CHANGED
|
@@ -231,7 +231,10 @@ export function createMemeRoutes() {
|
|
|
231
231
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
232
232
|
fs.writeFileSync(filePath, buffer);
|
|
233
233
|
|
|
234
|
-
return c.json({
|
|
234
|
+
return c.json({
|
|
235
|
+
ok: true,
|
|
236
|
+
path: path.relative(process.cwd(), filePath).split(path.sep).join("/"),
|
|
237
|
+
});
|
|
235
238
|
});
|
|
236
239
|
|
|
237
240
|
app.delete("/", async (c) => {
|
package/system.ts
CHANGED
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
WEBUI_DIST,
|
|
34
34
|
writeJsonFile,
|
|
35
35
|
} from "./utils";
|
|
36
|
-
import
|
|
36
|
+
import * as fsp from "node:fs/promises";
|
|
37
37
|
|
|
38
38
|
async function fetchJson(url: string): Promise<any> {
|
|
39
39
|
const res = await fetch(url, {
|
|
@@ -553,44 +553,6 @@ function getRepositoryFromPackage(pkg: any): string {
|
|
|
553
553
|
return "";
|
|
554
554
|
}
|
|
555
555
|
|
|
556
|
-
async function commandExists(cmd: string): Promise<boolean> {
|
|
557
|
-
const result = await runCommand("which", [cmd], process.cwd());
|
|
558
|
-
return result.code === 0;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
async function ensureUnzipCommand(): Promise<void> {
|
|
562
|
-
if (await commandExists("unzip")) {
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const platform = os.platform();
|
|
567
|
-
if (platform === "darwin") {
|
|
568
|
-
const installRes = await runCommand(
|
|
569
|
-
"brew",
|
|
570
|
-
["install", "unzip"],
|
|
571
|
-
process.cwd(),
|
|
572
|
-
);
|
|
573
|
-
if (installRes.code !== 0) {
|
|
574
|
-
throw new Error(
|
|
575
|
-
`未找到 unzip 且自动安装失败: ${installRes.stderr || installRes.stdout}。\n` +
|
|
576
|
-
`请手动运行: brew install unzip`,
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
} else {
|
|
580
|
-
const installRes = await runCommand(
|
|
581
|
-
"sudo",
|
|
582
|
-
["apt", "install", "-y", "unzip"],
|
|
583
|
-
process.cwd(),
|
|
584
|
-
);
|
|
585
|
-
if (installRes.code !== 0) {
|
|
586
|
-
throw new Error(
|
|
587
|
-
`未找到 unzip 且自动安装失败: ${installRes.stderr || installRes.stdout}。\n` +
|
|
588
|
-
`请手动运行: sudo apt install unzip`,
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
556
|
async function getGitOriginUrl(dir: string): Promise<string> {
|
|
595
557
|
const res = await runCommand("git", ["remote", "get-url", "origin"], dir);
|
|
596
558
|
if (res.code !== 0) return "";
|
|
@@ -1251,9 +1213,46 @@ export async function removeManagedPackage(
|
|
|
1251
1213
|
throw new Error("系统服务不可卸载");
|
|
1252
1214
|
}
|
|
1253
1215
|
|
|
1254
|
-
|
|
1216
|
+
// construct full package name from short name
|
|
1217
|
+
const fullName = input.name.startsWith("mioku-plugin-") || input.name.startsWith("mioku-service-")
|
|
1218
|
+
? input.name
|
|
1219
|
+
: input.target === "plugin"
|
|
1220
|
+
? `mioku-plugin-${input.name}`
|
|
1221
|
+
: `mioku-service-${input.name}`;
|
|
1222
|
+
|
|
1223
|
+
const installCmd = getInstallCommand();
|
|
1224
|
+
const removeArgs = installCmd.args.map((a) =>
|
|
1225
|
+
a === "add" ? "remove" : a,
|
|
1226
|
+
);
|
|
1227
|
+
|
|
1228
|
+
const result = await runCommand(
|
|
1229
|
+
installCmd.cmd,
|
|
1230
|
+
[...removeArgs, fullName],
|
|
1231
|
+
process.cwd(),
|
|
1232
|
+
);
|
|
1255
1233
|
|
|
1256
|
-
|
|
1234
|
+
if (result.code !== 0) {
|
|
1235
|
+
throw new Error(`bun remove 失败: ${result.stderr || result.stdout}`);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// clean up mioki.plugins list for plugins
|
|
1239
|
+
if (input.target === "plugin") {
|
|
1240
|
+
const pkgPath = ROOT_PACKAGE_PATH;
|
|
1241
|
+
if (fs.existsSync(pkgPath)) {
|
|
1242
|
+
try {
|
|
1243
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
1244
|
+
const mioki = pkg.mioki ?? {};
|
|
1245
|
+
const plugins: string[] = Array.isArray(mioki.plugins) ? mioki.plugins : [];
|
|
1246
|
+
const shortName = fullName.replace(/^mioku-plugin-/, "");
|
|
1247
|
+
if (plugins.includes(shortName)) {
|
|
1248
|
+
pkg.mioki = { ...mioki, plugins: plugins.filter((p: string) => p !== shortName) };
|
|
1249
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
1250
|
+
}
|
|
1251
|
+
} catch {
|
|
1252
|
+
// silently ignore package.json parse errors
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1257
1256
|
|
|
1258
1257
|
managedPackageUpdateCache.delete(
|
|
1259
1258
|
makeManagedUpdateCacheKey(input.target, input.name),
|
|
@@ -1912,58 +1911,31 @@ export async function updateWebUIDistFromRelease(): Promise<
|
|
|
1912
1911
|
try {
|
|
1913
1912
|
logger.info(`[webui] 正在下载 WebUI dist...`);
|
|
1914
1913
|
|
|
1915
|
-
await
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
"-A",
|
|
1923
|
-
"mioku-webui-updater/1.0",
|
|
1924
|
-
"--silent",
|
|
1925
|
-
"--show-error",
|
|
1926
|
-
downloadUrl,
|
|
1927
|
-
],
|
|
1928
|
-
{
|
|
1929
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1930
|
-
},
|
|
1931
|
-
);
|
|
1932
|
-
|
|
1933
|
-
let stderr = "";
|
|
1934
|
-
curl.stderr.on("data", (chunk) => {
|
|
1935
|
-
stderr += String(chunk);
|
|
1936
|
-
});
|
|
1914
|
+
const res = await fetch(downloadUrl, {
|
|
1915
|
+
headers: { "User-Agent": "mioku-webui-updater/1.0" },
|
|
1916
|
+
redirect: "follow",
|
|
1917
|
+
});
|
|
1918
|
+
if (!res.ok || !res.body) {
|
|
1919
|
+
throw new Error(`下载失败: HTTP ${res.status}`);
|
|
1920
|
+
}
|
|
1937
1921
|
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
// Periodically log progress by checking file size
|
|
1951
|
-
const progressTimer = setInterval(() => {
|
|
1952
|
-
if (fs.existsSync(zipPath)) {
|
|
1953
|
-
const stat = fs.statSync(zipPath);
|
|
1954
|
-
const now = Date.now();
|
|
1955
|
-
if (now - lastLogTime >= LOG_INTERVAL_MS) {
|
|
1956
|
-
logger.info(
|
|
1957
|
-
`[webui] 正在下载 WebUI dist... ${(stat.size / 1024 / 1024).toFixed(1)} MB 已下载`,
|
|
1958
|
-
);
|
|
1959
|
-
lastLogTime = now;
|
|
1960
|
-
}
|
|
1922
|
+
let downloaded = 0;
|
|
1923
|
+
const handle = await fsp.open(zipPath, "w");
|
|
1924
|
+
try {
|
|
1925
|
+
for await (const chunk of res.body as unknown as AsyncIterable<Uint8Array>) {
|
|
1926
|
+
await handle.write(chunk);
|
|
1927
|
+
downloaded += chunk.byteLength;
|
|
1928
|
+
const now = Date.now();
|
|
1929
|
+
if (now - lastLogTime >= LOG_INTERVAL_MS) {
|
|
1930
|
+
logger.info(
|
|
1931
|
+
`[webui] 正在下载 WebUI dist... ${(downloaded / 1024 / 1024).toFixed(1)} MB 已下载`,
|
|
1932
|
+
);
|
|
1933
|
+
lastLogTime = now;
|
|
1961
1934
|
}
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
});
|
|
1935
|
+
}
|
|
1936
|
+
} finally {
|
|
1937
|
+
await handle.close();
|
|
1938
|
+
}
|
|
1967
1939
|
|
|
1968
1940
|
const zipStat = fs.statSync(zipPath);
|
|
1969
1941
|
logger.info(
|
|
@@ -2414,3 +2386,53 @@ export function getAvailablePlugins(): string[] {
|
|
|
2414
2386
|
}
|
|
2415
2387
|
return plugins.sort((a, b) => a.localeCompare(b));
|
|
2416
2388
|
}
|
|
2389
|
+
|
|
2390
|
+
export interface PluginStatusItem {
|
|
2391
|
+
name: string;
|
|
2392
|
+
enabled: boolean;
|
|
2393
|
+
system: boolean;
|
|
2394
|
+
description: string;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
export function getPluginStatusList(): PluginStatusItem[] {
|
|
2398
|
+
const allPlugins = getAvailablePlugins();
|
|
2399
|
+
const miokuConfig = getMiokuConfig();
|
|
2400
|
+
const enabledPlugins = new Set(miokuConfig.plugins || []);
|
|
2401
|
+
|
|
2402
|
+
return allPlugins.map((name) => {
|
|
2403
|
+
const fullPath = path.join(NODE_MODULES_DIR, `mioku-plugin-${name}`);
|
|
2404
|
+
const pkg = readPackageJson(fullPath);
|
|
2405
|
+
return {
|
|
2406
|
+
name,
|
|
2407
|
+
enabled: enabledPlugins.has(name),
|
|
2408
|
+
system: SYSTEM_PLUGIN_NAMES.has(name.toLowerCase()),
|
|
2409
|
+
description: pkg?.description ?? "",
|
|
2410
|
+
};
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
export function togglePlugin(name: string, enabled: boolean): PluginStatusItem {
|
|
2415
|
+
const safeName = String(name || "").trim();
|
|
2416
|
+
if (!safeName) throw new Error("插件名称为空");
|
|
2417
|
+
|
|
2418
|
+
const config = getMiokuConfig();
|
|
2419
|
+
const plugins = new Set(config.plugins || []);
|
|
2420
|
+
|
|
2421
|
+
if (enabled) {
|
|
2422
|
+
plugins.add(safeName);
|
|
2423
|
+
} else {
|
|
2424
|
+
if (safeName.toLowerCase() === "boot") {
|
|
2425
|
+
throw new Error("boot 插件不可关闭");
|
|
2426
|
+
}
|
|
2427
|
+
plugins.delete(safeName);
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
updateMiokuConfig({ ...config, plugins: Array.from(plugins) });
|
|
2431
|
+
|
|
2432
|
+
return {
|
|
2433
|
+
name: safeName,
|
|
2434
|
+
enabled: plugins.has(safeName),
|
|
2435
|
+
system: SYSTEM_PLUGIN_NAMES.has(safeName.toLowerCase()),
|
|
2436
|
+
description: "",
|
|
2437
|
+
};
|
|
2438
|
+
}
|
package/utils.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
4
|
import type { PackageManager, WebUISettings } from "./types";
|
|
6
5
|
import {
|
|
7
6
|
getServiceConfigDir,
|
|
8
7
|
getServiceDataDir,
|
|
9
8
|
getConfigDir,
|
|
10
9
|
getDataDir,
|
|
10
|
+
runCommand as runMiokuCommand,
|
|
11
11
|
} from "mioku";
|
|
12
12
|
|
|
13
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -86,34 +86,7 @@ export async function runCommand(
|
|
|
86
86
|
args: string[],
|
|
87
87
|
cwd: string,
|
|
88
88
|
): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
89
|
-
return
|
|
90
|
-
const child = spawn(command, args, {
|
|
91
|
-
cwd,
|
|
92
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
93
|
-
});
|
|
94
|
-
let stdout = "";
|
|
95
|
-
let stderr = "";
|
|
96
|
-
|
|
97
|
-
child.stdout.on("data", (chunk) => {
|
|
98
|
-
stdout += String(chunk);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
child.stderr.on("data", (chunk) => {
|
|
102
|
-
stderr += String(chunk);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
child.on("close", (code) => {
|
|
106
|
-
resolve({ stdout, stderr, code: code ?? 1 });
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
child.on("error", (error) => {
|
|
110
|
-
resolve({
|
|
111
|
-
stdout,
|
|
112
|
-
stderr: `${stderr}\n${error.message}`.trim(),
|
|
113
|
-
code: 1,
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
});
|
|
89
|
+
return runMiokuCommand(command, args, { cwd });
|
|
117
90
|
}
|
|
118
91
|
|
|
119
92
|
export function getInstallCommand(): { cmd: string; args: string[] } {
|