mioku-service-webui 4.8.0 → 4.9.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 +0 -2
- package/package.json +1 -1
- package/routes/access-control.ts +24 -11
- package/routes/adapter-config.ts +13 -11
- package/system.ts +272 -158
- package/utils.ts +5 -0
package/index.ts
CHANGED
package/package.json
CHANGED
package/routes/access-control.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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 "mioku";
|
|
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(),
|
|
@@ -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 }> =
|
package/routes/adapter-config.ts
CHANGED
|
@@ -3,23 +3,25 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { loadAdapterConfigPage } from "../config-page-loader";
|
|
5
5
|
import { getAdapterConfigs, updateAdapterConfig } from "../system";
|
|
6
|
+
import { isPackageDir } from "../utils";
|
|
6
7
|
|
|
7
8
|
function listAdaptersFromNodeModules(): string[] {
|
|
8
9
|
const modulesPath = path.join(process.cwd(), "node_modules");
|
|
9
|
-
if (!fs.existsSync(modulesPath)) return [];
|
|
10
10
|
const adapters: string[] = [];
|
|
11
|
-
|
|
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
|
+
|
|
12
20
|
for (const entry of entries) {
|
|
13
|
-
if (!entry.name.startsWith("mioku-adapter-")) continue;
|
|
21
|
+
if (!entry.name.toLowerCase().startsWith("mioku-adapter-")) continue;
|
|
14
22
|
const fullPath = path.join(modulesPath, entry.name);
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
if (!isPackageDir(fullPath)) continue;
|
|
24
|
+
adapters.push(entry.name.replace(/^mioku-adapter-/i, ""));
|
|
23
25
|
}
|
|
24
26
|
return adapters.sort((a, b) => a.localeCompare(b));
|
|
25
27
|
}
|
package/system.ts
CHANGED
|
@@ -4,8 +4,8 @@ import * as path from "node:path";
|
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import AdmZip from "adm-zip";
|
|
6
6
|
import systemInfo from "systeminformation";
|
|
7
|
-
import {connectedBots,
|
|
8
|
-
|
|
7
|
+
import { buildAdapterReport, connectedBots, logger } from "mioku";
|
|
8
|
+
import type { AdapterInstanceStatus, Bot } from "mioku";
|
|
9
9
|
import type {
|
|
10
10
|
InstallRequest,
|
|
11
11
|
ManagedTarget,
|
|
@@ -17,10 +17,12 @@ import type {
|
|
|
17
17
|
import {
|
|
18
18
|
AUTH_PATH,
|
|
19
19
|
CHAT_CONFIG_DIR,
|
|
20
|
+
CONFIG_DIR,
|
|
20
21
|
defaultWebUISettings,
|
|
21
22
|
ensureDir,
|
|
22
23
|
getInstallCommand,
|
|
23
24
|
isNpmPackageName,
|
|
25
|
+
isPackageDir,
|
|
24
26
|
isValidRepoUrl,
|
|
25
27
|
LOCAL_CONFIG_PATH,
|
|
26
28
|
NODE_MODULES_DIR,
|
|
@@ -640,6 +642,11 @@ interface ManagedPackageUpdateInfo {
|
|
|
640
642
|
error?: string;
|
|
641
643
|
}
|
|
642
644
|
|
|
645
|
+
interface NpmPackageUpdateInfo extends ManagedPackageUpdateInfo {
|
|
646
|
+
currentVersion: string;
|
|
647
|
+
latestVersion: string;
|
|
648
|
+
}
|
|
649
|
+
|
|
643
650
|
interface ManagedPackageUpdateCacheEntry {
|
|
644
651
|
checkedAt: number;
|
|
645
652
|
info: ManagedPackageUpdateInfo;
|
|
@@ -727,6 +734,65 @@ function isManagedUpdateCacheFresh(
|
|
|
727
734
|
return Date.now() - entry.checkedAt < MANAGED_UPDATE_CACHE_TTL_MS;
|
|
728
735
|
}
|
|
729
736
|
|
|
737
|
+
/** 本地已安装版本:node_modules/<fullName>/package.json */
|
|
738
|
+
function readLocalPackageVersion(fullName: string): string {
|
|
739
|
+
const pkg = readPackageJson(path.join(NODE_MODULES_DIR, fullName));
|
|
740
|
+
return pkg?.version || "";
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async function getNpmLatestVersion(fullName: string): Promise<string> {
|
|
744
|
+
try {
|
|
745
|
+
const data = await fetchJson(
|
|
746
|
+
`https://registry.npmjs.org/${encodeURIComponent(fullName)}`,
|
|
747
|
+
);
|
|
748
|
+
return String(data?.["dist-tags"]?.latest || "").trim();
|
|
749
|
+
} catch {
|
|
750
|
+
return "";
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function npmManagedUpdateError(
|
|
755
|
+
fullName: string,
|
|
756
|
+
error: string,
|
|
757
|
+
): NpmPackageUpdateInfo {
|
|
758
|
+
return {
|
|
759
|
+
state: "unknown",
|
|
760
|
+
hasUpdates: false,
|
|
761
|
+
behind: 0,
|
|
762
|
+
changelog: [],
|
|
763
|
+
error,
|
|
764
|
+
currentVersion: readLocalPackageVersion(fullName),
|
|
765
|
+
latestVersion: "",
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async function checkNpmManagedUpdate(
|
|
770
|
+
fullName: string,
|
|
771
|
+
): Promise<NpmPackageUpdateInfo> {
|
|
772
|
+
const current = readLocalPackageVersion(fullName);
|
|
773
|
+
let latest = "";
|
|
774
|
+
try {
|
|
775
|
+
latest = await getNpmLatestVersion(fullName);
|
|
776
|
+
} catch (error: any) {
|
|
777
|
+
return npmManagedUpdateError(
|
|
778
|
+
fullName,
|
|
779
|
+
error?.message || "UPDATE_CHECK_FAILED",
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
if (!latest) {
|
|
783
|
+
return npmManagedUpdateError(fullName, "无法获取 npm 最新版本");
|
|
784
|
+
}
|
|
785
|
+
const hasUpdates = Boolean(current) && current !== latest;
|
|
786
|
+
return {
|
|
787
|
+
state: hasUpdates ? "has-updates" : "up-to-date",
|
|
788
|
+
hasUpdates,
|
|
789
|
+
behind: hasUpdates ? 1 : 0,
|
|
790
|
+
changelog: [],
|
|
791
|
+
currentVersion: current || "0.0.0",
|
|
792
|
+
latestVersion: latest,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
730
796
|
async function refreshManagedUpdatesInBackground(
|
|
731
797
|
target: ManagedTarget,
|
|
732
798
|
packages: Array<Record<string, any>>,
|
|
@@ -735,21 +801,9 @@ async function refreshManagedUpdatesInBackground(
|
|
|
735
801
|
const name = String(item.name || "");
|
|
736
802
|
if (!name) continue;
|
|
737
803
|
|
|
738
|
-
|
|
739
|
-
setCachedManagedUpdateInfo(target, name, {
|
|
740
|
-
state: "no-git",
|
|
741
|
-
hasUpdates: false,
|
|
742
|
-
behind: 0,
|
|
743
|
-
changelog: [],
|
|
744
|
-
error: "NOT_GIT_REPO",
|
|
745
|
-
});
|
|
746
|
-
continue;
|
|
747
|
-
}
|
|
748
|
-
|
|
804
|
+
const fullName = fullPackageName(target, name);
|
|
749
805
|
try {
|
|
750
|
-
const updateInfo = await
|
|
751
|
-
String(item.path || ""),
|
|
752
|
-
);
|
|
806
|
+
const updateInfo = await checkNpmManagedUpdate(fullName);
|
|
753
807
|
setCachedManagedUpdateInfo(target, name, updateInfo);
|
|
754
808
|
} catch (error: any) {
|
|
755
809
|
setCachedManagedUpdateInfo(target, name, {
|
|
@@ -769,8 +823,8 @@ function scheduleManagedUpdatesRefresh(
|
|
|
769
823
|
): void {
|
|
770
824
|
if (managedOverviewRefreshInFlight.has(target)) return;
|
|
771
825
|
|
|
826
|
+
// npm 包与 git 包统一走 registry 更新检查(已不再使用 git 仓库形式)
|
|
772
827
|
const shouldRefresh = packages.some((item) => {
|
|
773
|
-
if (!item.hasGit) return false;
|
|
774
828
|
const cached = getCachedManagedUpdateInfo(target, String(item.name || ""));
|
|
775
829
|
return !isManagedUpdateCacheFresh(cached);
|
|
776
830
|
});
|
|
@@ -887,11 +941,11 @@ function listPluginsFromNodeModules(): Array<Record<string, any>> {
|
|
|
887
941
|
|
|
888
942
|
const entries = fs.readdirSync(modulesPath, { withFileTypes: true });
|
|
889
943
|
for (const entry of entries) {
|
|
890
|
-
if (!entry.name.startsWith("mioku-plugin-")) continue;
|
|
944
|
+
if (!entry.name.toLowerCase().startsWith("mioku-plugin-")) continue;
|
|
891
945
|
const fullPath = path.join(modulesPath, entry.name);
|
|
892
|
-
|
|
893
|
-
if (!
|
|
894
|
-
const name = entry.name.replace(/^mioku-plugin
|
|
946
|
+
// Windows 上 pnpm/bun workspace 会用 NTFS junction,旧 lstat 检查会误判
|
|
947
|
+
if (!isPackageDir(fullPath)) continue;
|
|
948
|
+
const name = entry.name.replace(/^mioku-plugin-/i, "");
|
|
895
949
|
const pkg = readPackageJson(fullPath);
|
|
896
950
|
plugins.push({
|
|
897
951
|
name,
|
|
@@ -915,11 +969,11 @@ function listServicesFromNodeModules(): Array<Record<string, any>> {
|
|
|
915
969
|
|
|
916
970
|
const entries = fs.readdirSync(modulesPath, { withFileTypes: true });
|
|
917
971
|
for (const entry of entries) {
|
|
918
|
-
if (!entry.name.startsWith("mioku-service-")) continue;
|
|
972
|
+
if (!entry.name.toLowerCase().startsWith("mioku-service-")) continue;
|
|
919
973
|
const fullPath = path.join(modulesPath, entry.name);
|
|
920
|
-
|
|
921
|
-
if (!
|
|
922
|
-
const name = entry.name.replace(/^mioku-service
|
|
974
|
+
// Windows 上 pnpm/bun workspace 会用 NTFS junction,旧 lstat 检查会误判
|
|
975
|
+
if (!isPackageDir(fullPath)) continue;
|
|
976
|
+
const name = entry.name.replace(/^mioku-service-/i, "");
|
|
923
977
|
const pkg = readPackageJson(fullPath);
|
|
924
978
|
services.push({
|
|
925
979
|
name,
|
|
@@ -943,11 +997,11 @@ function listAdaptersFromNodeModules(): Array<Record<string, any>> {
|
|
|
943
997
|
|
|
944
998
|
const entries = fs.readdirSync(modulesPath, { withFileTypes: true });
|
|
945
999
|
for (const entry of entries) {
|
|
946
|
-
if (!entry.name.startsWith("mioku-adapter-")) continue;
|
|
1000
|
+
if (!entry.name.toLowerCase().startsWith("mioku-adapter-")) continue;
|
|
947
1001
|
const fullPath = path.join(modulesPath, entry.name);
|
|
948
|
-
|
|
949
|
-
if (!
|
|
950
|
-
const name = entry.name.replace(/^mioku-adapter
|
|
1002
|
+
// Windows 上 pnpm/bun workspace 会用 NTFS junction,旧 lstat 检查会误判
|
|
1003
|
+
if (!isPackageDir(fullPath)) continue;
|
|
1004
|
+
const name = entry.name.replace(/^mioku-adapter-/i, "");
|
|
951
1005
|
const pkg = readPackageJson(fullPath);
|
|
952
1006
|
adapters.push({
|
|
953
1007
|
name,
|
|
@@ -1001,10 +1055,8 @@ export async function installManagedPackage(
|
|
|
1001
1055
|
...Object.keys(afterPkg?.devDependencies || {}),
|
|
1002
1056
|
];
|
|
1003
1057
|
const installedName =
|
|
1004
|
-
afterDeps.find(
|
|
1005
|
-
|
|
1006
|
-
!beforeDeps.has(name) && isNpmManagedName(name),
|
|
1007
|
-
) || pkgName;
|
|
1058
|
+
afterDeps.find((name) => !beforeDeps.has(name) && isNpmManagedName(name)) ||
|
|
1059
|
+
pkgName;
|
|
1008
1060
|
|
|
1009
1061
|
const packageJsonPath = path.join(
|
|
1010
1062
|
NODE_MODULES_DIR,
|
|
@@ -1038,34 +1090,25 @@ export async function checkUpdate(
|
|
|
1038
1090
|
name: string,
|
|
1039
1091
|
target: ManagedTarget,
|
|
1040
1092
|
): Promise<Record<string, any>> {
|
|
1041
|
-
|
|
1093
|
+
// 现在插件/服务/适配器全部以 npm 包形式安装,统一走 npm 更新检查
|
|
1094
|
+
const fullName = isNpmManagedName(name)
|
|
1095
|
+
? name
|
|
1096
|
+
: fullPackageName(target, name);
|
|
1097
|
+
const localPath = path.join(NODE_MODULES_DIR, fullName);
|
|
1098
|
+
const isNpmPackage = fs.existsSync(localPath) || isNpmManagedName(name);
|
|
1042
1099
|
|
|
1043
1100
|
if (isNpmPackage) {
|
|
1044
|
-
|
|
1045
|
-
const currentVersion = await runCommand(
|
|
1046
|
-
"bun",
|
|
1047
|
-
["npm", "info", name, "version"],
|
|
1048
|
-
process.cwd(),
|
|
1049
|
-
);
|
|
1050
|
-
const latestVersion = await runCommand(
|
|
1051
|
-
"bun",
|
|
1052
|
-
["npm", "info", name, "dist-tags.latest"],
|
|
1053
|
-
process.cwd(),
|
|
1054
|
-
);
|
|
1055
|
-
const current = currentVersion.stdout?.trim() || "";
|
|
1056
|
-
const latest = latestVersion.stdout?.trim() || "";
|
|
1057
|
-
const hasUpdates = current !== latest && latest !== "";
|
|
1058
|
-
|
|
1101
|
+
const info = await checkNpmManagedUpdate(fullName);
|
|
1059
1102
|
return {
|
|
1060
1103
|
ok: true,
|
|
1061
|
-
state:
|
|
1062
|
-
hasUpdates,
|
|
1063
|
-
behind:
|
|
1064
|
-
changelog:
|
|
1104
|
+
state: info.state,
|
|
1105
|
+
hasUpdates: info.hasUpdates,
|
|
1106
|
+
behind: info.behind,
|
|
1107
|
+
changelog: info.changelog,
|
|
1065
1108
|
hasGit: false,
|
|
1066
|
-
error: null,
|
|
1067
|
-
currentVersion:
|
|
1068
|
-
latestVersion:
|
|
1109
|
+
error: info.error || null,
|
|
1110
|
+
currentVersion: info.currentVersion,
|
|
1111
|
+
latestVersion: info.latestVersion,
|
|
1069
1112
|
};
|
|
1070
1113
|
}
|
|
1071
1114
|
|
|
@@ -1092,28 +1135,30 @@ export async function updateManagedPackage(
|
|
|
1092
1135
|
): Promise<Record<string, any>> {
|
|
1093
1136
|
const installCmd = getInstallCommand();
|
|
1094
1137
|
|
|
1095
|
-
//
|
|
1096
|
-
const
|
|
1138
|
+
// 前端传短名(如 chat),统一还原为完整 npm 包名
|
|
1139
|
+
const fullName = isNpmManagedName(input.name)
|
|
1140
|
+
? input.name
|
|
1141
|
+
: fullPackageName(input.target, input.name);
|
|
1097
1142
|
const npmPackagePath = path.join(
|
|
1098
1143
|
process.cwd(),
|
|
1099
1144
|
"node_modules",
|
|
1100
|
-
|
|
1145
|
+
fullName,
|
|
1101
1146
|
"package.json",
|
|
1102
1147
|
);
|
|
1103
1148
|
|
|
1104
|
-
if (
|
|
1105
|
-
// Bun
|
|
1106
|
-
const updateArgs = installCmd.args.map((arg) =>
|
|
1107
|
-
arg === "install" ? "update" : arg,
|
|
1108
|
-
);
|
|
1149
|
+
if (fs.existsSync(npmPackagePath)) {
|
|
1150
|
+
// Bun 包更新:bun add 会拉取 npm 最新版本
|
|
1109
1151
|
const update = await runCommand(
|
|
1110
1152
|
installCmd.cmd,
|
|
1111
|
-
[...
|
|
1153
|
+
[...installCmd.args, fullName],
|
|
1112
1154
|
process.cwd(),
|
|
1113
1155
|
);
|
|
1114
1156
|
if (update.code !== 0) {
|
|
1115
|
-
throw new Error(`bun
|
|
1157
|
+
throw new Error(`bun add 更新失败: ${update.stderr || update.stdout}`);
|
|
1116
1158
|
}
|
|
1159
|
+
managedPackageUpdateCache.delete(
|
|
1160
|
+
makeManagedUpdateCacheKey(input.target, fullName),
|
|
1161
|
+
);
|
|
1117
1162
|
return {
|
|
1118
1163
|
ok: true,
|
|
1119
1164
|
restartRequired: true,
|
|
@@ -1172,9 +1217,7 @@ export async function removeManagedPackage(
|
|
|
1172
1217
|
: fullPackageName(input.target, input.name);
|
|
1173
1218
|
|
|
1174
1219
|
const installCmd = getInstallCommand();
|
|
1175
|
-
const removeArgs = installCmd.args.map((a) =>
|
|
1176
|
-
a === "add" ? "remove" : a,
|
|
1177
|
-
);
|
|
1220
|
+
const removeArgs = installCmd.args.map((a) => (a === "add" ? "remove" : a));
|
|
1178
1221
|
|
|
1179
1222
|
const result = await runCommand(
|
|
1180
1223
|
installCmd.cmd,
|
|
@@ -1193,10 +1236,15 @@ export async function removeManagedPackage(
|
|
|
1193
1236
|
try {
|
|
1194
1237
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
1195
1238
|
const mioki = pkg.mioki ?? {};
|
|
1196
|
-
const plugins: string[] = Array.isArray(mioki.plugins)
|
|
1239
|
+
const plugins: string[] = Array.isArray(mioki.plugins)
|
|
1240
|
+
? mioki.plugins
|
|
1241
|
+
: [];
|
|
1197
1242
|
const shortName = fullName.replace(/^mioku-plugin-/, "");
|
|
1198
1243
|
if (plugins.includes(shortName)) {
|
|
1199
|
-
pkg.mioki = {
|
|
1244
|
+
pkg.mioki = {
|
|
1245
|
+
...mioki,
|
|
1246
|
+
plugins: plugins.filter((p: string) => p !== shortName),
|
|
1247
|
+
};
|
|
1200
1248
|
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
1201
1249
|
}
|
|
1202
1250
|
} catch {
|
|
@@ -1225,17 +1273,6 @@ export async function listManagedPackagesWithUpdates(
|
|
|
1225
1273
|
return packages.map((item) => {
|
|
1226
1274
|
const name = String(item.name || "");
|
|
1227
1275
|
|
|
1228
|
-
if (!item.hasGit) {
|
|
1229
|
-
return {
|
|
1230
|
-
...item,
|
|
1231
|
-
updateState: "no-git",
|
|
1232
|
-
hasUpdates: false,
|
|
1233
|
-
behind: 0,
|
|
1234
|
-
updateError: "NOT_GIT_REPO",
|
|
1235
|
-
updateChecking: false,
|
|
1236
|
-
};
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
1276
|
const cached = getCachedManagedUpdateInfo(target, name);
|
|
1240
1277
|
if (cached) {
|
|
1241
1278
|
return {
|
|
@@ -1299,6 +1336,9 @@ export async function getManagedPackageDetail(
|
|
|
1299
1336
|
} else {
|
|
1300
1337
|
repositoryUrl = normalizeRepositoryUrl(latest?.repository);
|
|
1301
1338
|
}
|
|
1339
|
+
// npm 包以包内 repository 字段为准,缺失时回退到 mioku-lab 仓库
|
|
1340
|
+
const packageRepository = normalizeRepositoryUrl(latest?.repository);
|
|
1341
|
+
const finalRepository = packageRepository || repositoryUrl;
|
|
1302
1342
|
|
|
1303
1343
|
// 获取本地已安装版本
|
|
1304
1344
|
const localPkg =
|
|
@@ -1324,7 +1364,7 @@ export async function getManagedPackageDetail(
|
|
|
1324
1364
|
hasGit: false,
|
|
1325
1365
|
isSystemPlugin,
|
|
1326
1366
|
isSystemService,
|
|
1327
|
-
repository:
|
|
1367
|
+
repository: finalRepository,
|
|
1328
1368
|
originUrl: "",
|
|
1329
1369
|
homepage: String(latest?.homepage || "").trim(),
|
|
1330
1370
|
requiredServices: Array.isArray(latest?.mioku?.services)
|
|
@@ -1334,7 +1374,7 @@ export async function getManagedPackageDetail(
|
|
|
1334
1374
|
help: latest?.mioku?.help || null,
|
|
1335
1375
|
readme: String(data?.readme || "").trim(),
|
|
1336
1376
|
readmeFile: "README.md",
|
|
1337
|
-
updateState: "
|
|
1377
|
+
updateState: hasUpdates ? "has-updates" : "up-to-date",
|
|
1338
1378
|
hasUpdates,
|
|
1339
1379
|
behind: hasUpdates ? 1 : 0,
|
|
1340
1380
|
changelog: [],
|
|
@@ -1444,19 +1484,24 @@ export async function updateAllManagedPackages(input: {
|
|
|
1444
1484
|
const results: Array<Record<string, any>> = [];
|
|
1445
1485
|
|
|
1446
1486
|
for (const item of packages) {
|
|
1447
|
-
const
|
|
1448
|
-
if (
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1487
|
+
const name = String(item.name || "");
|
|
1488
|
+
if (!name) continue;
|
|
1489
|
+
|
|
1490
|
+
// 统一走 npm 更新检查(已不再使用 git 仓库形式)
|
|
1491
|
+
const cached = getCachedManagedUpdateInfo(input.target, name);
|
|
1492
|
+
let updateInfo: ManagedPackageUpdateInfo;
|
|
1493
|
+
if (cached && isManagedUpdateCacheFresh(cached)) {
|
|
1494
|
+
updateInfo = cached.info;
|
|
1495
|
+
} else {
|
|
1496
|
+
updateInfo = await checkNpmManagedUpdate(
|
|
1497
|
+
fullPackageName(input.target, name),
|
|
1498
|
+
);
|
|
1499
|
+
setCachedManagedUpdateInfo(input.target, name, updateInfo);
|
|
1456
1500
|
}
|
|
1501
|
+
|
|
1457
1502
|
if (!updateInfo.hasUpdates) {
|
|
1458
1503
|
results.push({
|
|
1459
|
-
name
|
|
1504
|
+
name,
|
|
1460
1505
|
ok: true,
|
|
1461
1506
|
skipped: true,
|
|
1462
1507
|
reason: updateInfo.state === "unknown" ? "CHECK_FAILED" : "UP_TO_DATE",
|
|
@@ -1467,19 +1512,19 @@ export async function updateAllManagedPackages(input: {
|
|
|
1467
1512
|
|
|
1468
1513
|
try {
|
|
1469
1514
|
const updated = await updateManagedPackage({
|
|
1470
|
-
name
|
|
1515
|
+
name,
|
|
1471
1516
|
target: input.target,
|
|
1472
1517
|
packageManager: input.packageManager,
|
|
1473
1518
|
});
|
|
1474
1519
|
results.push({
|
|
1475
|
-
name
|
|
1520
|
+
name,
|
|
1476
1521
|
ok: true,
|
|
1477
1522
|
skipped: false,
|
|
1478
1523
|
...updated,
|
|
1479
1524
|
});
|
|
1480
1525
|
} catch (error: any) {
|
|
1481
1526
|
results.push({
|
|
1482
|
-
name
|
|
1527
|
+
name,
|
|
1483
1528
|
ok: false,
|
|
1484
1529
|
skipped: false,
|
|
1485
1530
|
error: error?.message || "UPDATE_FAILED",
|
|
@@ -1827,10 +1872,7 @@ export async function notifyOwnersAuthTokenRefreshed(
|
|
|
1827
1872
|
continue;
|
|
1828
1873
|
}
|
|
1829
1874
|
try {
|
|
1830
|
-
await bot.sendMessage(
|
|
1831
|
-
{ type: "private", user_id: ownerId },
|
|
1832
|
-
message,
|
|
1833
|
-
);
|
|
1875
|
+
await bot.sendMessage({ type: "private", user_id: ownerId }, message);
|
|
1834
1876
|
logger.info(
|
|
1835
1877
|
`[webui] 已通过 ${bot.adapter ?? "bot"}/${bot.bot_id ?? "?"} 通知主人 ${ownerId} 密钥更新`,
|
|
1836
1878
|
);
|
|
@@ -1842,9 +1884,7 @@ export async function notifyOwnersAuthTokenRefreshed(
|
|
|
1842
1884
|
}
|
|
1843
1885
|
if (!notified) {
|
|
1844
1886
|
const msg = formatSendError(lastError);
|
|
1845
|
-
logger.warn(
|
|
1846
|
-
`[webui] 通知主人 ${ownerId} 密钥更新失败: ${msg}`,
|
|
1847
|
-
);
|
|
1887
|
+
logger.warn(`[webui] 通知主人 ${ownerId} 密钥更新失败: ${msg}`);
|
|
1848
1888
|
}
|
|
1849
1889
|
}
|
|
1850
1890
|
}
|
|
@@ -2107,14 +2147,6 @@ async function getCpuUsagePercent(): Promise<number> {
|
|
|
2107
2147
|
return Number((((totalDelta - idleDelta) / totalDelta) * 100).toFixed(1));
|
|
2108
2148
|
}
|
|
2109
2149
|
|
|
2110
|
-
function toHttpBaseUrl(bot: { options?: { protocol?: string; host?: string; port?: number } }): string {
|
|
2111
|
-
const protocol = String(bot.options?.protocol || "ws");
|
|
2112
|
-
const httpProtocol = protocol === "wss" ? "https" : "http";
|
|
2113
|
-
const host = bot.options?.host || "127.0.0.1";
|
|
2114
|
-
const port = bot.options?.port || 3001;
|
|
2115
|
-
return `${httpProtocol}://${host}:${port}`;
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
2150
|
async function fetchYiyan(): Promise<{ text: string }> {
|
|
2119
2151
|
const controller = new AbortController();
|
|
2120
2152
|
const timer = setTimeout(() => controller.abort(), 4500);
|
|
@@ -2142,61 +2174,138 @@ function normalizeVersionSpec(input: string): string {
|
|
|
2142
2174
|
return matched?.[0] || cleaned;
|
|
2143
2175
|
}
|
|
2144
2176
|
|
|
2145
|
-
|
|
2177
|
+
/** 读取已安装适配器包的版本(与 help 状态图里 ctx.getAdapter().version 同源) */
|
|
2178
|
+
function readAdapterPackageVersion(name: string): string {
|
|
2179
|
+
const pkg = readPackageJson(
|
|
2180
|
+
path.join(NODE_MODULES_DIR, `mioku-adapter-${name}`),
|
|
2181
|
+
);
|
|
2182
|
+
return pkg?.version || "";
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
/** 与 help 状态图一致:stdin 使用 help/status 配置的随机头像,未配置时用默认随机图接口 */
|
|
2186
|
+
function readStdinAvatar(): string {
|
|
2187
|
+
const statusConfig = readJsonFile<any>(
|
|
2188
|
+
path.join(CONFIG_DIR, "help", "status.json"),
|
|
2189
|
+
null,
|
|
2190
|
+
);
|
|
2191
|
+
const configured = String(statusConfig?.stdinAvatar || "").trim();
|
|
2192
|
+
if (configured) return configured;
|
|
2193
|
+
return "https://uapis.cn/api/v1/random/image?category=bq&type=eciyuan";
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
async function getBotDetails(bot: Bot): Promise<Record<string, any>> {
|
|
2146
2197
|
const botAny = bot as unknown as {
|
|
2147
2198
|
uin?: number | string;
|
|
2148
2199
|
user_id?: number | string;
|
|
2149
2200
|
name?: string;
|
|
2150
|
-
options?: { protocol?: string; host?: string; port?: number };
|
|
2151
|
-
app_version?: string;
|
|
2152
2201
|
};
|
|
2153
|
-
const
|
|
2202
|
+
const accountId = String(botAny.uin ?? botAny.user_id ?? bot.bot_id);
|
|
2203
|
+
const isStdin = bot.adapter === "stdin";
|
|
2154
2204
|
const base = {
|
|
2155
2205
|
botId: bot.bot_id,
|
|
2156
|
-
|
|
2206
|
+
accountId,
|
|
2207
|
+
qq: accountId,
|
|
2157
2208
|
nickname: bot.nickname || botAny.name || "Unknown Bot",
|
|
2158
|
-
avatar:
|
|
2209
|
+
avatar: isStdin
|
|
2210
|
+
? readStdinAvatar()
|
|
2211
|
+
: `https://q1.qlogo.cn/g?b=qq&nk=${accountId}&s=160`,
|
|
2159
2212
|
online: bot.online,
|
|
2160
|
-
|
|
2161
|
-
|
|
2213
|
+
adapter: String(bot.adapter),
|
|
2214
|
+
adapterVersion: readAdapterPackageVersion(String(bot.adapter)),
|
|
2215
|
+
implLabel: "",
|
|
2216
|
+
framework: isStdin ? "stdin" : String(bot.adapter),
|
|
2217
|
+
appVersion: "",
|
|
2218
|
+
protocolVersion: "",
|
|
2219
|
+
platform: "",
|
|
2220
|
+
platformVersion: "",
|
|
2162
2221
|
groupCount: 0,
|
|
2163
2222
|
friendCount: 0,
|
|
2223
|
+
send: 0,
|
|
2224
|
+
receive: 0,
|
|
2164
2225
|
onlineDurationMs: 0,
|
|
2165
|
-
statusText: "online",
|
|
2226
|
+
statusText: bot.online ? "online" : "offline",
|
|
2166
2227
|
};
|
|
2167
2228
|
|
|
2168
2229
|
try {
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2230
|
+
if (typeof bot.getAvatar === "function" && !isStdin) {
|
|
2231
|
+
const avatar = await bot.getAvatar().catch(() => null);
|
|
2232
|
+
if (avatar) base.avatar = avatar;
|
|
2233
|
+
}
|
|
2234
|
+
} catch {
|
|
2235
|
+
// keep qlogo fallback
|
|
2236
|
+
}
|
|
2237
|
+
return base;
|
|
2238
|
+
}
|
|
2175
2239
|
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2240
|
+
async function getBotOverviewList(): Promise<Record<string, any>[]> {
|
|
2241
|
+
const bots = Array.from(connectedBots.values());
|
|
2242
|
+
if (bots.length === 0) return [];
|
|
2243
|
+
const sorted = [...bots].sort(
|
|
2244
|
+
(a, b) => Number(b.adapter === "stdin") - Number(a.adapter === "stdin"),
|
|
2245
|
+
);
|
|
2182
2246
|
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2247
|
+
const adapters = sorted.map((bot) => ({
|
|
2248
|
+
name: String(bot.adapter),
|
|
2249
|
+
version: readAdapterPackageVersion(String(bot.adapter)),
|
|
2250
|
+
}));
|
|
2251
|
+
const report = await buildAdapterReport({ bots: sorted, adapters }).catch(
|
|
2252
|
+
() => null,
|
|
2253
|
+
);
|
|
2254
|
+
const instances = new Map<string, AdapterInstanceStatus>();
|
|
2255
|
+
const implOf = new Map<
|
|
2256
|
+
string,
|
|
2257
|
+
{ name: string; version?: string } | undefined
|
|
2258
|
+
>();
|
|
2259
|
+
for (const entry of report?.adapters ?? []) {
|
|
2260
|
+
implOf.set(entry.name, entry.impl);
|
|
2261
|
+
for (const instance of entry.instances) {
|
|
2262
|
+
instances.set(`${entry.name}:${instance.bot_id}`, instance);
|
|
2263
|
+
}
|
|
2199
2264
|
}
|
|
2265
|
+
|
|
2266
|
+
return Promise.all(
|
|
2267
|
+
sorted.map(async (bot) => {
|
|
2268
|
+
const detail = await getBotDetails(bot);
|
|
2269
|
+
const uin = String(detail.accountId);
|
|
2270
|
+
const adapterName = String(detail.adapter);
|
|
2271
|
+
const isStdin = adapterName === "stdin";
|
|
2272
|
+
const instance = instances.get(`${adapterName}:${uin}`);
|
|
2273
|
+
const lib = implOf.get(adapterName);
|
|
2274
|
+
const appVersion = instance?.version || lib?.version || "";
|
|
2275
|
+
const implLabel = isStdin
|
|
2276
|
+
? "stdin"
|
|
2277
|
+
: instance?.impl
|
|
2278
|
+
? instance.version
|
|
2279
|
+
? `${instance.impl}/${instance.version}`
|
|
2280
|
+
: String(instance.impl)
|
|
2281
|
+
: lib?.name
|
|
2282
|
+
? lib.version
|
|
2283
|
+
? `${lib.name} v${lib.version}`
|
|
2284
|
+
: lib.name
|
|
2285
|
+
: adapterName;
|
|
2286
|
+
|
|
2287
|
+
return {
|
|
2288
|
+
...detail,
|
|
2289
|
+
implLabel,
|
|
2290
|
+
framework: isStdin ? "stdin" : adapterName,
|
|
2291
|
+
appVersion: isStdin
|
|
2292
|
+
? String(readAdapterPackageVersion(adapterName))
|
|
2293
|
+
: appVersion,
|
|
2294
|
+
protocolVersion: instance?.protocol ?? "",
|
|
2295
|
+
platform: instance?.platform ?? "",
|
|
2296
|
+
platformVersion: instance?.platformVersion ?? "",
|
|
2297
|
+
online: instance?.online ?? bot.online,
|
|
2298
|
+
groupCount: instance?.stats.groups ?? 0,
|
|
2299
|
+
friendCount: instance?.stats.friends ?? 0,
|
|
2300
|
+
send: instance?.stats.sent ?? 0,
|
|
2301
|
+
receive: instance?.stats.received ?? 0,
|
|
2302
|
+
onlineDurationMs:
|
|
2303
|
+
bot.connected_at != null && bot.online
|
|
2304
|
+
? Math.max(0, Date.now() - bot.connected_at)
|
|
2305
|
+
: 0,
|
|
2306
|
+
};
|
|
2307
|
+
}),
|
|
2308
|
+
);
|
|
2200
2309
|
}
|
|
2201
2310
|
|
|
2202
2311
|
export async function getSystemOverview(): Promise<Record<string, any>> {
|
|
@@ -2220,9 +2329,10 @@ export async function getSystemOverview(): Promise<Record<string, any>> {
|
|
|
2220
2329
|
: 0;
|
|
2221
2330
|
const siSnapshot = await getSystemInformationSnapshot();
|
|
2222
2331
|
|
|
2223
|
-
const
|
|
2224
|
-
|
|
2225
|
-
const selectedBot =
|
|
2332
|
+
const bots = await getBotOverviewList();
|
|
2333
|
+
// 默认展示第一个非 stdin 的 bot(stdin 是终端通道,不是真实账号)
|
|
2334
|
+
const selectedBot =
|
|
2335
|
+
bots.find((bot) => bot.adapter !== "stdin") || bots[0] || null;
|
|
2226
2336
|
|
|
2227
2337
|
return {
|
|
2228
2338
|
uptimeSeconds: process.uptime(),
|
|
@@ -2312,12 +2422,16 @@ export function updateChatConfig(fileName: string, data: any): any {
|
|
|
2312
2422
|
export function getAdapterConfigs(adapterName: string): any {
|
|
2313
2423
|
const rootPkg = readRootPackageJson();
|
|
2314
2424
|
const adapters = rootPkg?.mioku?.adapters ?? {};
|
|
2315
|
-
const safeName = String(adapterName || "")
|
|
2425
|
+
const safeName = String(adapterName || "")
|
|
2426
|
+
.trim()
|
|
2427
|
+
.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
2316
2428
|
return adapters[safeName] ?? {};
|
|
2317
2429
|
}
|
|
2318
2430
|
|
|
2319
2431
|
export function updateAdapterConfig(adapterName: string, data: any): any {
|
|
2320
|
-
const safeName = String(adapterName || "")
|
|
2432
|
+
const safeName = String(adapterName || "")
|
|
2433
|
+
.trim()
|
|
2434
|
+
.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
2321
2435
|
if (!safeName) throw new Error("适配器名称无效");
|
|
2322
2436
|
const rootPkg = readRootPackageJson();
|
|
2323
2437
|
rootPkg.mioku = rootPkg.mioku ?? {};
|
|
@@ -2401,11 +2515,11 @@ export function getAvailablePlugins(): string[] {
|
|
|
2401
2515
|
const plugins: string[] = [];
|
|
2402
2516
|
const entries = fs.readdirSync(modulesPath, { withFileTypes: true });
|
|
2403
2517
|
for (const entry of entries) {
|
|
2404
|
-
if (!entry.name.startsWith("mioku-plugin-")) continue;
|
|
2518
|
+
if (!entry.name.toLowerCase().startsWith("mioku-plugin-")) continue;
|
|
2405
2519
|
const fullPath = path.join(modulesPath, entry.name);
|
|
2406
|
-
|
|
2407
|
-
if (!
|
|
2408
|
-
plugins.push(entry.name.replace(/^mioku-plugin
|
|
2520
|
+
// Windows 上 pnpm/bun workspace 会用 NTFS junction,旧 lstat 检查会误判
|
|
2521
|
+
if (!isPackageDir(fullPath)) continue;
|
|
2522
|
+
plugins.push(entry.name.replace(/^mioku-plugin-/i, ""));
|
|
2409
2523
|
}
|
|
2410
2524
|
return plugins.sort((a, b) => a.localeCompare(b));
|
|
2411
2525
|
}
|
package/utils.ts
CHANGED
|
@@ -97,6 +97,11 @@ export function normalizePackageManager(): PackageManager {
|
|
|
97
97
|
return "bun";
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
export function isPackageDir(fullPath: string | null | undefined): boolean {
|
|
101
|
+
if (!fullPath) return false;
|
|
102
|
+
return fs.existsSync(path.join(fullPath, "package.json"));
|
|
103
|
+
}
|
|
104
|
+
|
|
100
105
|
export function isValidRepoUrl(url: string): boolean {
|
|
101
106
|
const trimmed = url.trim();
|
|
102
107
|
// 拒绝空白字符:堵 `ssh://host -o ProxyCommand=...` 之类参数注入
|