mioku-service-webui 4.8.1 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/system.ts +255 -142
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-service-webui",
3
- "version": "4.8.1",
3
+ "version": "4.9.0",
4
4
  "description": "Mioku WebUI 服务",
5
5
  "main": "index.ts",
6
6
  "type": "module",
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,6 +17,7 @@ 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,
@@ -641,6 +642,11 @@ interface ManagedPackageUpdateInfo {
641
642
  error?: string;
642
643
  }
643
644
 
645
+ interface NpmPackageUpdateInfo extends ManagedPackageUpdateInfo {
646
+ currentVersion: string;
647
+ latestVersion: string;
648
+ }
649
+
644
650
  interface ManagedPackageUpdateCacheEntry {
645
651
  checkedAt: number;
646
652
  info: ManagedPackageUpdateInfo;
@@ -728,6 +734,65 @@ function isManagedUpdateCacheFresh(
728
734
  return Date.now() - entry.checkedAt < MANAGED_UPDATE_CACHE_TTL_MS;
729
735
  }
730
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
+
731
796
  async function refreshManagedUpdatesInBackground(
732
797
  target: ManagedTarget,
733
798
  packages: Array<Record<string, any>>,
@@ -736,21 +801,9 @@ async function refreshManagedUpdatesInBackground(
736
801
  const name = String(item.name || "");
737
802
  if (!name) continue;
738
803
 
739
- if (!item.hasGit) {
740
- setCachedManagedUpdateInfo(target, name, {
741
- state: "no-git",
742
- hasUpdates: false,
743
- behind: 0,
744
- changelog: [],
745
- error: "NOT_GIT_REPO",
746
- });
747
- continue;
748
- }
749
-
804
+ const fullName = fullPackageName(target, name);
750
805
  try {
751
- const updateInfo = await getManagedPackageUpdateInfo(
752
- String(item.path || ""),
753
- );
806
+ const updateInfo = await checkNpmManagedUpdate(fullName);
754
807
  setCachedManagedUpdateInfo(target, name, updateInfo);
755
808
  } catch (error: any) {
756
809
  setCachedManagedUpdateInfo(target, name, {
@@ -770,8 +823,8 @@ function scheduleManagedUpdatesRefresh(
770
823
  ): void {
771
824
  if (managedOverviewRefreshInFlight.has(target)) return;
772
825
 
826
+ // npm 包与 git 包统一走 registry 更新检查(已不再使用 git 仓库形式)
773
827
  const shouldRefresh = packages.some((item) => {
774
- if (!item.hasGit) return false;
775
828
  const cached = getCachedManagedUpdateInfo(target, String(item.name || ""));
776
829
  return !isManagedUpdateCacheFresh(cached);
777
830
  });
@@ -1002,10 +1055,8 @@ export async function installManagedPackage(
1002
1055
  ...Object.keys(afterPkg?.devDependencies || {}),
1003
1056
  ];
1004
1057
  const installedName =
1005
- afterDeps.find(
1006
- (name) =>
1007
- !beforeDeps.has(name) && isNpmManagedName(name),
1008
- ) || pkgName;
1058
+ afterDeps.find((name) => !beforeDeps.has(name) && isNpmManagedName(name)) ||
1059
+ pkgName;
1009
1060
 
1010
1061
  const packageJsonPath = path.join(
1011
1062
  NODE_MODULES_DIR,
@@ -1039,34 +1090,25 @@ export async function checkUpdate(
1039
1090
  name: string,
1040
1091
  target: ManagedTarget,
1041
1092
  ): Promise<Record<string, any>> {
1042
- 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);
1043
1099
 
1044
1100
  if (isNpmPackage) {
1045
- // 使用 npm view 检查 npm 包更新
1046
- const currentVersion = await runCommand(
1047
- "bun",
1048
- ["npm", "info", name, "version"],
1049
- process.cwd(),
1050
- );
1051
- const latestVersion = await runCommand(
1052
- "bun",
1053
- ["npm", "info", name, "dist-tags.latest"],
1054
- process.cwd(),
1055
- );
1056
- const current = currentVersion.stdout?.trim() || "";
1057
- const latest = latestVersion.stdout?.trim() || "";
1058
- const hasUpdates = current !== latest && latest !== "";
1059
-
1101
+ const info = await checkNpmManagedUpdate(fullName);
1060
1102
  return {
1061
1103
  ok: true,
1062
- state: "npm",
1063
- hasUpdates,
1064
- behind: hasUpdates ? 1 : 0,
1065
- changelog: [],
1104
+ state: info.state,
1105
+ hasUpdates: info.hasUpdates,
1106
+ behind: info.behind,
1107
+ changelog: info.changelog,
1066
1108
  hasGit: false,
1067
- error: null,
1068
- currentVersion: current,
1069
- latestVersion: latest,
1109
+ error: info.error || null,
1110
+ currentVersion: info.currentVersion,
1111
+ latestVersion: info.latestVersion,
1070
1112
  };
1071
1113
  }
1072
1114
 
@@ -1093,28 +1135,30 @@ export async function updateManagedPackage(
1093
1135
  ): Promise<Record<string, any>> {
1094
1136
  const installCmd = getInstallCommand();
1095
1137
 
1096
- // 检查是 npm 包还是 git 包
1097
- const isNpmPackage = isNpmManagedName(input.name);
1138
+ // 前端传短名(如 chat),统一还原为完整 npm 包名
1139
+ const fullName = isNpmManagedName(input.name)
1140
+ ? input.name
1141
+ : fullPackageName(input.target, input.name);
1098
1142
  const npmPackagePath = path.join(
1099
1143
  process.cwd(),
1100
1144
  "node_modules",
1101
- input.name,
1145
+ fullName,
1102
1146
  "package.json",
1103
1147
  );
1104
1148
 
1105
- if (isNpmPackage && fs.existsSync(npmPackagePath)) {
1106
- // Bun 包更新
1107
- const updateArgs = installCmd.args.map((arg) =>
1108
- arg === "install" ? "update" : arg,
1109
- );
1149
+ if (fs.existsSync(npmPackagePath)) {
1150
+ // Bun 包更新:bun add 会拉取 npm 最新版本
1110
1151
  const update = await runCommand(
1111
1152
  installCmd.cmd,
1112
- [...updateArgs, input.name],
1153
+ [...installCmd.args, fullName],
1113
1154
  process.cwd(),
1114
1155
  );
1115
1156
  if (update.code !== 0) {
1116
- throw new Error(`bun update 失败: ${update.stderr || update.stdout}`);
1157
+ throw new Error(`bun add 更新失败: ${update.stderr || update.stdout}`);
1117
1158
  }
1159
+ managedPackageUpdateCache.delete(
1160
+ makeManagedUpdateCacheKey(input.target, fullName),
1161
+ );
1118
1162
  return {
1119
1163
  ok: true,
1120
1164
  restartRequired: true,
@@ -1173,9 +1217,7 @@ export async function removeManagedPackage(
1173
1217
  : fullPackageName(input.target, input.name);
1174
1218
 
1175
1219
  const installCmd = getInstallCommand();
1176
- const removeArgs = installCmd.args.map((a) =>
1177
- a === "add" ? "remove" : a,
1178
- );
1220
+ const removeArgs = installCmd.args.map((a) => (a === "add" ? "remove" : a));
1179
1221
 
1180
1222
  const result = await runCommand(
1181
1223
  installCmd.cmd,
@@ -1194,10 +1236,15 @@ export async function removeManagedPackage(
1194
1236
  try {
1195
1237
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
1196
1238
  const mioki = pkg.mioki ?? {};
1197
- const plugins: string[] = Array.isArray(mioki.plugins) ? mioki.plugins : [];
1239
+ const plugins: string[] = Array.isArray(mioki.plugins)
1240
+ ? mioki.plugins
1241
+ : [];
1198
1242
  const shortName = fullName.replace(/^mioku-plugin-/, "");
1199
1243
  if (plugins.includes(shortName)) {
1200
- pkg.mioki = { ...mioki, plugins: plugins.filter((p: string) => p !== shortName) };
1244
+ pkg.mioki = {
1245
+ ...mioki,
1246
+ plugins: plugins.filter((p: string) => p !== shortName),
1247
+ };
1201
1248
  fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
1202
1249
  }
1203
1250
  } catch {
@@ -1226,17 +1273,6 @@ export async function listManagedPackagesWithUpdates(
1226
1273
  return packages.map((item) => {
1227
1274
  const name = String(item.name || "");
1228
1275
 
1229
- if (!item.hasGit) {
1230
- return {
1231
- ...item,
1232
- updateState: "no-git",
1233
- hasUpdates: false,
1234
- behind: 0,
1235
- updateError: "NOT_GIT_REPO",
1236
- updateChecking: false,
1237
- };
1238
- }
1239
-
1240
1276
  const cached = getCachedManagedUpdateInfo(target, name);
1241
1277
  if (cached) {
1242
1278
  return {
@@ -1300,6 +1336,9 @@ export async function getManagedPackageDetail(
1300
1336
  } else {
1301
1337
  repositoryUrl = normalizeRepositoryUrl(latest?.repository);
1302
1338
  }
1339
+ // npm 包以包内 repository 字段为准,缺失时回退到 mioku-lab 仓库
1340
+ const packageRepository = normalizeRepositoryUrl(latest?.repository);
1341
+ const finalRepository = packageRepository || repositoryUrl;
1303
1342
 
1304
1343
  // 获取本地已安装版本
1305
1344
  const localPkg =
@@ -1325,7 +1364,7 @@ export async function getManagedPackageDetail(
1325
1364
  hasGit: false,
1326
1365
  isSystemPlugin,
1327
1366
  isSystemService,
1328
- repository: repositoryUrl,
1367
+ repository: finalRepository,
1329
1368
  originUrl: "",
1330
1369
  homepage: String(latest?.homepage || "").trim(),
1331
1370
  requiredServices: Array.isArray(latest?.mioku?.services)
@@ -1335,7 +1374,7 @@ export async function getManagedPackageDetail(
1335
1374
  help: latest?.mioku?.help || null,
1336
1375
  readme: String(data?.readme || "").trim(),
1337
1376
  readmeFile: "README.md",
1338
- updateState: "npm",
1377
+ updateState: hasUpdates ? "has-updates" : "up-to-date",
1339
1378
  hasUpdates,
1340
1379
  behind: hasUpdates ? 1 : 0,
1341
1380
  changelog: [],
@@ -1445,19 +1484,24 @@ export async function updateAllManagedPackages(input: {
1445
1484
  const results: Array<Record<string, any>> = [];
1446
1485
 
1447
1486
  for (const item of packages) {
1448
- const updateInfo = await getManagedPackageUpdateInfo(item.path);
1449
- if (updateInfo.state === "no-git") {
1450
- results.push({
1451
- name: item.name,
1452
- ok: false,
1453
- skipped: true,
1454
- reason: "NOT_GIT_REPO",
1455
- });
1456
- 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);
1457
1500
  }
1501
+
1458
1502
  if (!updateInfo.hasUpdates) {
1459
1503
  results.push({
1460
- name: item.name,
1504
+ name,
1461
1505
  ok: true,
1462
1506
  skipped: true,
1463
1507
  reason: updateInfo.state === "unknown" ? "CHECK_FAILED" : "UP_TO_DATE",
@@ -1468,19 +1512,19 @@ export async function updateAllManagedPackages(input: {
1468
1512
 
1469
1513
  try {
1470
1514
  const updated = await updateManagedPackage({
1471
- name: item.name,
1515
+ name,
1472
1516
  target: input.target,
1473
1517
  packageManager: input.packageManager,
1474
1518
  });
1475
1519
  results.push({
1476
- name: item.name,
1520
+ name,
1477
1521
  ok: true,
1478
1522
  skipped: false,
1479
1523
  ...updated,
1480
1524
  });
1481
1525
  } catch (error: any) {
1482
1526
  results.push({
1483
- name: item.name,
1527
+ name,
1484
1528
  ok: false,
1485
1529
  skipped: false,
1486
1530
  error: error?.message || "UPDATE_FAILED",
@@ -1828,10 +1872,7 @@ export async function notifyOwnersAuthTokenRefreshed(
1828
1872
  continue;
1829
1873
  }
1830
1874
  try {
1831
- await bot.sendMessage(
1832
- { type: "private", user_id: ownerId },
1833
- message,
1834
- );
1875
+ await bot.sendMessage({ type: "private", user_id: ownerId }, message);
1835
1876
  logger.info(
1836
1877
  `[webui] 已通过 ${bot.adapter ?? "bot"}/${bot.bot_id ?? "?"} 通知主人 ${ownerId} 密钥更新`,
1837
1878
  );
@@ -1843,9 +1884,7 @@ export async function notifyOwnersAuthTokenRefreshed(
1843
1884
  }
1844
1885
  if (!notified) {
1845
1886
  const msg = formatSendError(lastError);
1846
- logger.warn(
1847
- `[webui] 通知主人 ${ownerId} 密钥更新失败: ${msg}`,
1848
- );
1887
+ logger.warn(`[webui] 通知主人 ${ownerId} 密钥更新失败: ${msg}`);
1849
1888
  }
1850
1889
  }
1851
1890
  }
@@ -2108,14 +2147,6 @@ async function getCpuUsagePercent(): Promise<number> {
2108
2147
  return Number((((totalDelta - idleDelta) / totalDelta) * 100).toFixed(1));
2109
2148
  }
2110
2149
 
2111
- function toHttpBaseUrl(bot: { options?: { protocol?: string; host?: string; port?: number } }): string {
2112
- const protocol = String(bot.options?.protocol || "ws");
2113
- const httpProtocol = protocol === "wss" ? "https" : "http";
2114
- const host = bot.options?.host || "127.0.0.1";
2115
- const port = bot.options?.port || 3001;
2116
- return `${httpProtocol}://${host}:${port}`;
2117
- }
2118
-
2119
2150
  async function fetchYiyan(): Promise<{ text: string }> {
2120
2151
  const controller = new AbortController();
2121
2152
  const timer = setTimeout(() => controller.abort(), 4500);
@@ -2143,61 +2174,138 @@ function normalizeVersionSpec(input: string): string {
2143
2174
  return matched?.[0] || cleaned;
2144
2175
  }
2145
2176
 
2146
- 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>> {
2147
2197
  const botAny = bot as unknown as {
2148
2198
  uin?: number | string;
2149
2199
  user_id?: number | string;
2150
2200
  name?: string;
2151
- options?: { protocol?: string; host?: string; port?: number };
2152
- app_version?: string;
2153
2201
  };
2154
- 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";
2155
2204
  const base = {
2156
2205
  botId: bot.bot_id,
2157
- qq,
2206
+ accountId,
2207
+ qq: accountId,
2158
2208
  nickname: bot.nickname || botAny.name || "Unknown Bot",
2159
- 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`,
2160
2212
  online: bot.online,
2161
- napcatVersion: botAny.app_version || "unknown",
2162
- 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: "",
2163
2221
  groupCount: 0,
2164
2222
  friendCount: 0,
2223
+ send: 0,
2224
+ receive: 0,
2165
2225
  onlineDurationMs: 0,
2166
- statusText: "online",
2226
+ statusText: bot.online ? "online" : "offline",
2167
2227
  };
2168
2228
 
2169
2229
  try {
2170
- const [status, versionInfo, groups, friends] = await Promise.all([
2171
- bot.sendApi<{ stat?: { start_time?: number; online?: boolean }; online?: boolean }>("get_status").catch(() => null),
2172
- bot.sendApi<{ app_version?: string }>("get_version_info").catch(() => null),
2173
- bot.getGroupList().catch(() => []),
2174
- bot.getFriendList().catch(() => []),
2175
- ]);
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
+ }
2176
2239
 
2177
- const stat = status?.stat || null;
2178
- const startTs = Number(stat?.start_time || 0);
2179
- const onlineDurationMs =
2180
- startTs > 0 ? Math.max(0, Date.now() - startTs) : 0;
2181
- const onlineFromStatus =
2182
- 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
+ );
2183
2246
 
2184
- return {
2185
- ...base,
2186
- online: onlineFromStatus,
2187
- napcatVersion: versionInfo?.app_version || base.napcatVersion,
2188
- groupCount: Array.isArray(groups) ? groups.length : 0,
2189
- friendCount: Array.isArray(friends) ? friends.length : 0,
2190
- onlineDurationMs,
2191
- statusText: onlineFromStatus ? "online" : "offline",
2192
- };
2193
- } catch (error: any) {
2194
- return {
2195
- ...base,
2196
- online: false,
2197
- statusText: "error",
2198
- error: error?.message || "NAPCAT_API_ERROR",
2199
- };
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
+ }
2200
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
+ );
2201
2309
  }
2202
2310
 
2203
2311
  export async function getSystemOverview(): Promise<Record<string, any>> {
@@ -2221,9 +2329,10 @@ export async function getSystemOverview(): Promise<Record<string, any>> {
2221
2329
  : 0;
2222
2330
  const siSnapshot = await getSystemInformationSnapshot();
2223
2331
 
2224
- const botInstances = Array.from(connectedBots.values());
2225
- const bots = await Promise.all(botInstances.map((bot) => getBotDetails(bot)));
2226
- 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;
2227
2336
 
2228
2337
  return {
2229
2338
  uptimeSeconds: process.uptime(),
@@ -2313,12 +2422,16 @@ export function updateChatConfig(fileName: string, data: any): any {
2313
2422
  export function getAdapterConfigs(adapterName: string): any {
2314
2423
  const rootPkg = readRootPackageJson();
2315
2424
  const adapters = rootPkg?.mioku?.adapters ?? {};
2316
- 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, "");
2317
2428
  return adapters[safeName] ?? {};
2318
2429
  }
2319
2430
 
2320
2431
  export function updateAdapterConfig(adapterName: string, data: any): any {
2321
- 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, "");
2322
2435
  if (!safeName) throw new Error("适配器名称无效");
2323
2436
  const rootPkg = readRootPackageJson();
2324
2437
  rootPkg.mioku = rootPkg.mioku ?? {};