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 CHANGED
@@ -247,8 +247,6 @@ const runtime = new WebUIRuntime();
247
247
 
248
248
  const webUIService: MiokuService = {
249
249
  name: "webui",
250
- version: "4.2.0",
251
- description: "Mioku WebUI 管理服务",
252
250
  api: {
253
251
  getSettings: () => getWebUISettings(),
254
252
  } as WebUIServiceAPI,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-service-webui",
3
- "version": "4.8.0",
3
+ "version": "4.9.0",
4
4
  "description": "Mioku WebUI 服务",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -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
- 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 }> =
@@ -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
- const entries = fs.readdirSync(modulesPath, { withFileTypes: true });
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
- let stat: fs.Stats;
16
- try {
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
- logger} from "mioku";
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
- if (!item.hasGit) {
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 getManagedPackageUpdateInfo(
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
- const stat = fs.lstatSync(fullPath);
893
- if (!stat.isDirectory() && !stat.isSymbolicLink()) continue;
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
- const stat = fs.lstatSync(fullPath);
921
- if (!stat.isDirectory() && !stat.isSymbolicLink()) continue;
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
- const stat = fs.lstatSync(fullPath);
949
- if (!stat.isDirectory() && !stat.isSymbolicLink()) continue;
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
- (name) =>
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
- const isNpmPackage = isNpmManagedName(name);
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
- // 使用 npm view 检查 npm 包更新
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: "npm",
1062
- hasUpdates,
1063
- behind: hasUpdates ? 1 : 0,
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: current,
1068
- latestVersion: latest,
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
- // 检查是 npm 包还是 git 包
1096
- const isNpmPackage = isNpmManagedName(input.name);
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
- input.name,
1145
+ fullName,
1101
1146
  "package.json",
1102
1147
  );
1103
1148
 
1104
- if (isNpmPackage && fs.existsSync(npmPackagePath)) {
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
- [...updateArgs, input.name],
1153
+ [...installCmd.args, fullName],
1112
1154
  process.cwd(),
1113
1155
  );
1114
1156
  if (update.code !== 0) {
1115
- throw new Error(`bun update 失败: ${update.stderr || update.stdout}`);
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) ? 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 = { ...mioki, plugins: plugins.filter((p: string) => p !== shortName) };
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: repositoryUrl,
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: "npm",
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 updateInfo = await getManagedPackageUpdateInfo(item.path);
1448
- if (updateInfo.state === "no-git") {
1449
- results.push({
1450
- name: item.name,
1451
- ok: false,
1452
- skipped: true,
1453
- reason: "NOT_GIT_REPO",
1454
- });
1455
- continue;
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: item.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: item.name,
1515
+ name,
1471
1516
  target: input.target,
1472
1517
  packageManager: input.packageManager,
1473
1518
  });
1474
1519
  results.push({
1475
- name: item.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: item.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
- async function getBotDetails(bot: import("mioku").Bot): Promise<Record<string, any>> {
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 qq = String(botAny.uin ?? botAny.user_id ?? bot.bot_id);
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
- qq,
2206
+ accountId,
2207
+ qq: accountId,
2157
2208
  nickname: bot.nickname || botAny.name || "Unknown Bot",
2158
- avatar: `https://q1.qlogo.cn/g?b=qq&nk=${qq}&s=160`,
2209
+ avatar: isStdin
2210
+ ? readStdinAvatar()
2211
+ : `https://q1.qlogo.cn/g?b=qq&nk=${accountId}&s=160`,
2159
2212
  online: bot.online,
2160
- napcatVersion: botAny.app_version || "unknown",
2161
- napcatApiBase: toHttpBaseUrl(botAny),
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
- const [status, versionInfo, groups, friends] = await Promise.all([
2170
- bot.sendApi<{ stat?: { start_time?: number; online?: boolean }; online?: boolean }>("get_status").catch(() => null),
2171
- bot.sendApi<{ app_version?: string }>("get_version_info").catch(() => null),
2172
- bot.getGroupList().catch(() => []),
2173
- bot.getFriendList().catch(() => []),
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
- const stat = status?.stat || null;
2177
- const startTs = Number(stat?.start_time || 0);
2178
- const onlineDurationMs =
2179
- startTs > 0 ? Math.max(0, Date.now() - startTs) : 0;
2180
- const onlineFromStatus =
2181
- typeof status?.online === "boolean" ? status.online : true;
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
- return {
2184
- ...base,
2185
- online: onlineFromStatus,
2186
- napcatVersion: versionInfo?.app_version || base.napcatVersion,
2187
- groupCount: Array.isArray(groups) ? groups.length : 0,
2188
- friendCount: Array.isArray(friends) ? friends.length : 0,
2189
- onlineDurationMs,
2190
- statusText: onlineFromStatus ? "online" : "offline",
2191
- };
2192
- } catch (error: any) {
2193
- return {
2194
- ...base,
2195
- online: false,
2196
- statusText: "error",
2197
- error: error?.message || "NAPCAT_API_ERROR",
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 botInstances = Array.from(connectedBots.values());
2224
- const bots = await Promise.all(botInstances.map((bot) => getBotDetails(bot)));
2225
- const selectedBot = bots[0] || null;
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 || "").trim().replace(/[^a-zA-Z0-9_-]/g, "");
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 || "").trim().replace(/[^a-zA-Z0-9_-]/g, "");
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
- const stat = fs.lstatSync(fullPath);
2407
- if (!stat.isDirectory() && !stat.isSymbolicLink()) continue;
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=...` 之类参数注入