mioku-service-webui 4.8.1 → 4.10.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
@@ -26,6 +26,7 @@ import {
26
26
  createConfigRoutes,
27
27
  createWebUISettingsRoutes,
28
28
  createAIRoutes,
29
+ createAgentRoutes,
29
30
  createDBRoutes,
30
31
  createPluginConfigRoutes,
31
32
  createServiceConfigRoutes,
@@ -120,6 +121,7 @@ class WebUIRuntime {
120
121
  this.app.route("/api/config", createConfigRoutes());
121
122
  this.app.route("/api/settings", createWebUISettingsRoutes());
122
123
  this.app.route("/api/ai", createAIRoutes());
124
+ this.app.route("/api/agent", createAgentRoutes());
123
125
  this.app.route("/api/manage", createManageRoutes());
124
126
  this.app.route("/api/store", createStoreRoutes());
125
127
  this.app.route("/api/db", createDBRoutes());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-service-webui",
3
- "version": "4.8.1",
3
+ "version": "4.10.0",
4
4
  "description": "Mioku WebUI 服务",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -0,0 +1,132 @@
1
+ import { Hono } from "hono";
2
+ import { logger } from "mioku";
3
+ import { getAgentConfig, updateAgentConfig } from "../system";
4
+ import aiService from "mioku-service-ai";
5
+
6
+ const PERMISSION_LEVELS = new Set([
7
+ "read-only",
8
+ "workspace-write",
9
+ "auto",
10
+ "full",
11
+ "yolo",
12
+ ]);
13
+
14
+ function normalizeIdList(input: unknown): number[] {
15
+ if (!Array.isArray(input)) return [];
16
+ return Array.from(
17
+ new Set(
18
+ input
19
+ .map((item) => Math.floor(Number(item)))
20
+ .filter((id: number) => Number.isFinite(id) && id > 0),
21
+ ),
22
+ );
23
+ }
24
+
25
+ function sanitizeAccess(input: any): any {
26
+ const access = input && typeof input === "object" ? input : {};
27
+ return {
28
+ allowAdmins: Boolean(access.allowAdmins),
29
+ users: normalizeIdList(access.users),
30
+ };
31
+ }
32
+
33
+ function sanitizeBaseForSave(body: any): any {
34
+ if (!body || typeof body !== "object") return body;
35
+ const next = { ...body };
36
+ delete next.persona;
37
+ delete next.roleBindings;
38
+ next.access = sanitizeAccess(next.access);
39
+ next.workspaceDir = String(next.workspaceDir ?? "").trim();
40
+ next.permissionLevel = PERMISSION_LEVELS.has(next.permissionLevel)
41
+ ? next.permissionLevel
42
+ : "workspace-write";
43
+ next.model = String(next.model ?? "").trim();
44
+ return next;
45
+ }
46
+
47
+ function sanitizeSettingsForSave(body: any): any {
48
+ if (!body || typeof body !== "object") return body;
49
+ const next = { ...body };
50
+ const positive = (value: unknown, fallback: number) => {
51
+ const num = Math.floor(Number(value));
52
+ return Number.isFinite(num) && num > 0 ? num : fallback;
53
+ };
54
+ delete next.temperature;
55
+ delete next.enableMediaRecognition;
56
+ next.maxIterations = positive(next.maxIterations, 500);
57
+ next.maxContextTokens = positive(next.maxContextTokens, 524288);
58
+ next.stream = next.stream !== false;
59
+ next.enableMarkdownScreenshot = next.enableMarkdownScreenshot !== false;
60
+ next.compaction = {
61
+ enabled: next.compaction?.enabled !== false,
62
+ keepRecentMessages: positive(next.compaction?.keepRecentMessages, 20),
63
+ };
64
+ next.webSearch = {
65
+ enabled: next.webSearch?.enabled !== false,
66
+ baseUrl: String(next.webSearch?.baseUrl ?? "").trim(),
67
+ timeoutMs: positive(next.webSearch?.timeoutMs, 8000),
68
+ defaultLimit: positive(next.webSearch?.defaultLimit, 5),
69
+ maxLimit: positive(next.webSearch?.maxLimit, 8),
70
+ maxSearchCount: positive(next.webSearch?.maxSearchCount, 50),
71
+ };
72
+ next.webFetch = {
73
+ enabled: next.webFetch?.enabled !== false,
74
+ timeoutMs: positive(next.webFetch?.timeoutMs, 15000),
75
+ maxChars: positive(next.webFetch?.maxChars, 12000),
76
+ };
77
+ next.bash = {
78
+ enabled: next.bash?.enabled !== false,
79
+ timeoutMs: positive(next.bash?.timeoutMs, 120000),
80
+ approvalTimeoutMs: positive(next.bash?.approvalTimeoutMs, 300000),
81
+ };
82
+ next.dataCollection = { enabled: next.dataCollection?.enabled !== false };
83
+ next.debug = Boolean(next.debug);
84
+ return next;
85
+ }
86
+
87
+ function getAIApi() {
88
+ return aiService?.api as any;
89
+ }
90
+
91
+ export function createAgentRoutes() {
92
+ const app = new Hono();
93
+
94
+ app.get("/base", (c) =>
95
+ c.json({ ok: true, data: getAgentConfig("base.json") }),
96
+ );
97
+
98
+ app.put("/base", async (c) => {
99
+ const body = sanitizeBaseForSave(await c.req.json());
100
+ logger.info(`[webui-action] agent.base.update`, {
101
+ permissionLevel: body?.permissionLevel,
102
+ });
103
+ return c.json({ ok: true, data: updateAgentConfig("base.json", body) });
104
+ });
105
+
106
+ app.get("/settings", (c) =>
107
+ c.json({ ok: true, data: getAgentConfig("settings.json") }),
108
+ );
109
+
110
+ app.put("/settings", async (c) => {
111
+ const body = sanitizeSettingsForSave(await c.req.json());
112
+ logger.info(`[webui-action] agent.settings.update`);
113
+ return c.json({ ok: true, data: updateAgentConfig("settings.json", body) });
114
+ });
115
+
116
+ app.get("/status", (c) => {
117
+ const api = getAIApi();
118
+ const bindings = api?.getRoleBindings?.() ?? {};
119
+ const base = getAgentConfig("base.json");
120
+ return c.json({
121
+ ok: true,
122
+ data: {
123
+ aiConfigured: Boolean(bindings.main),
124
+ mainModel: bindings.main ?? "",
125
+ permissionLevel: base?.permissionLevel ?? "workspace-write",
126
+ workspaceDir: base?.workspaceDir ?? "",
127
+ },
128
+ });
129
+ });
130
+
131
+ return app;
132
+ }
package/routes/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { createConfigRoutes } from "./config";
2
2
  export { createWebUISettingsRoutes } from "./webui";
3
3
  export { createAIRoutes } from "./ai";
4
+ export { createAgentRoutes } from "./agent";
4
5
  export { createDBRoutes, createPluginConfigRoutes, createMemeRoutes, createServiceConfigRoutes } from "./db";
5
6
  export { createAdapterConfigRoutes } from "./adapter-config";
6
7
  export { createManageRoutes } from "./manage";
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,
@@ -16,7 +16,9 @@ import type {
16
16
  } from "./types";
17
17
  import {
18
18
  AUTH_PATH,
19
+ AGENT_CONFIG_DIR,
19
20
  CHAT_CONFIG_DIR,
21
+ CONFIG_DIR,
20
22
  defaultWebUISettings,
21
23
  ensureDir,
22
24
  getInstallCommand,
@@ -641,6 +643,11 @@ interface ManagedPackageUpdateInfo {
641
643
  error?: string;
642
644
  }
643
645
 
646
+ interface NpmPackageUpdateInfo extends ManagedPackageUpdateInfo {
647
+ currentVersion: string;
648
+ latestVersion: string;
649
+ }
650
+
644
651
  interface ManagedPackageUpdateCacheEntry {
645
652
  checkedAt: number;
646
653
  info: ManagedPackageUpdateInfo;
@@ -728,6 +735,65 @@ function isManagedUpdateCacheFresh(
728
735
  return Date.now() - entry.checkedAt < MANAGED_UPDATE_CACHE_TTL_MS;
729
736
  }
730
737
 
738
+ /** 本地已安装版本:node_modules/<fullName>/package.json */
739
+ function readLocalPackageVersion(fullName: string): string {
740
+ const pkg = readPackageJson(path.join(NODE_MODULES_DIR, fullName));
741
+ return pkg?.version || "";
742
+ }
743
+
744
+ async function getNpmLatestVersion(fullName: string): Promise<string> {
745
+ try {
746
+ const data = await fetchJson(
747
+ `https://registry.npmjs.org/${encodeURIComponent(fullName)}`,
748
+ );
749
+ return String(data?.["dist-tags"]?.latest || "").trim();
750
+ } catch {
751
+ return "";
752
+ }
753
+ }
754
+
755
+ function npmManagedUpdateError(
756
+ fullName: string,
757
+ error: string,
758
+ ): NpmPackageUpdateInfo {
759
+ return {
760
+ state: "unknown",
761
+ hasUpdates: false,
762
+ behind: 0,
763
+ changelog: [],
764
+ error,
765
+ currentVersion: readLocalPackageVersion(fullName),
766
+ latestVersion: "",
767
+ };
768
+ }
769
+
770
+ async function checkNpmManagedUpdate(
771
+ fullName: string,
772
+ ): Promise<NpmPackageUpdateInfo> {
773
+ const current = readLocalPackageVersion(fullName);
774
+ let latest = "";
775
+ try {
776
+ latest = await getNpmLatestVersion(fullName);
777
+ } catch (error: any) {
778
+ return npmManagedUpdateError(
779
+ fullName,
780
+ error?.message || "UPDATE_CHECK_FAILED",
781
+ );
782
+ }
783
+ if (!latest) {
784
+ return npmManagedUpdateError(fullName, "无法获取 npm 最新版本");
785
+ }
786
+ const hasUpdates = Boolean(current) && current !== latest;
787
+ return {
788
+ state: hasUpdates ? "has-updates" : "up-to-date",
789
+ hasUpdates,
790
+ behind: hasUpdates ? 1 : 0,
791
+ changelog: [],
792
+ currentVersion: current || "0.0.0",
793
+ latestVersion: latest,
794
+ };
795
+ }
796
+
731
797
  async function refreshManagedUpdatesInBackground(
732
798
  target: ManagedTarget,
733
799
  packages: Array<Record<string, any>>,
@@ -736,21 +802,9 @@ async function refreshManagedUpdatesInBackground(
736
802
  const name = String(item.name || "");
737
803
  if (!name) continue;
738
804
 
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
-
805
+ const fullName = fullPackageName(target, name);
750
806
  try {
751
- const updateInfo = await getManagedPackageUpdateInfo(
752
- String(item.path || ""),
753
- );
807
+ const updateInfo = await checkNpmManagedUpdate(fullName);
754
808
  setCachedManagedUpdateInfo(target, name, updateInfo);
755
809
  } catch (error: any) {
756
810
  setCachedManagedUpdateInfo(target, name, {
@@ -770,8 +824,8 @@ function scheduleManagedUpdatesRefresh(
770
824
  ): void {
771
825
  if (managedOverviewRefreshInFlight.has(target)) return;
772
826
 
827
+ // npm 包与 git 包统一走 registry 更新检查(已不再使用 git 仓库形式)
773
828
  const shouldRefresh = packages.some((item) => {
774
- if (!item.hasGit) return false;
775
829
  const cached = getCachedManagedUpdateInfo(target, String(item.name || ""));
776
830
  return !isManagedUpdateCacheFresh(cached);
777
831
  });
@@ -1002,10 +1056,8 @@ export async function installManagedPackage(
1002
1056
  ...Object.keys(afterPkg?.devDependencies || {}),
1003
1057
  ];
1004
1058
  const installedName =
1005
- afterDeps.find(
1006
- (name) =>
1007
- !beforeDeps.has(name) && isNpmManagedName(name),
1008
- ) || pkgName;
1059
+ afterDeps.find((name) => !beforeDeps.has(name) && isNpmManagedName(name)) ||
1060
+ pkgName;
1009
1061
 
1010
1062
  const packageJsonPath = path.join(
1011
1063
  NODE_MODULES_DIR,
@@ -1039,34 +1091,25 @@ export async function checkUpdate(
1039
1091
  name: string,
1040
1092
  target: ManagedTarget,
1041
1093
  ): Promise<Record<string, any>> {
1042
- const isNpmPackage = isNpmManagedName(name);
1094
+ // 现在插件/服务/适配器全部以 npm 包形式安装,统一走 npm 更新检查
1095
+ const fullName = isNpmManagedName(name)
1096
+ ? name
1097
+ : fullPackageName(target, name);
1098
+ const localPath = path.join(NODE_MODULES_DIR, fullName);
1099
+ const isNpmPackage = fs.existsSync(localPath) || isNpmManagedName(name);
1043
1100
 
1044
1101
  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
-
1102
+ const info = await checkNpmManagedUpdate(fullName);
1060
1103
  return {
1061
1104
  ok: true,
1062
- state: "npm",
1063
- hasUpdates,
1064
- behind: hasUpdates ? 1 : 0,
1065
- changelog: [],
1105
+ state: info.state,
1106
+ hasUpdates: info.hasUpdates,
1107
+ behind: info.behind,
1108
+ changelog: info.changelog,
1066
1109
  hasGit: false,
1067
- error: null,
1068
- currentVersion: current,
1069
- latestVersion: latest,
1110
+ error: info.error || null,
1111
+ currentVersion: info.currentVersion,
1112
+ latestVersion: info.latestVersion,
1070
1113
  };
1071
1114
  }
1072
1115
 
@@ -1093,28 +1136,30 @@ export async function updateManagedPackage(
1093
1136
  ): Promise<Record<string, any>> {
1094
1137
  const installCmd = getInstallCommand();
1095
1138
 
1096
- // 检查是 npm 包还是 git 包
1097
- const isNpmPackage = isNpmManagedName(input.name);
1139
+ // 前端传短名(如 chat),统一还原为完整 npm 包名
1140
+ const fullName = isNpmManagedName(input.name)
1141
+ ? input.name
1142
+ : fullPackageName(input.target, input.name);
1098
1143
  const npmPackagePath = path.join(
1099
1144
  process.cwd(),
1100
1145
  "node_modules",
1101
- input.name,
1146
+ fullName,
1102
1147
  "package.json",
1103
1148
  );
1104
1149
 
1105
- if (isNpmPackage && fs.existsSync(npmPackagePath)) {
1106
- // Bun 包更新
1107
- const updateArgs = installCmd.args.map((arg) =>
1108
- arg === "install" ? "update" : arg,
1109
- );
1150
+ if (fs.existsSync(npmPackagePath)) {
1151
+ // Bun 包更新:bun add 会拉取 npm 最新版本
1110
1152
  const update = await runCommand(
1111
1153
  installCmd.cmd,
1112
- [...updateArgs, input.name],
1154
+ [...installCmd.args, fullName],
1113
1155
  process.cwd(),
1114
1156
  );
1115
1157
  if (update.code !== 0) {
1116
- throw new Error(`bun update 失败: ${update.stderr || update.stdout}`);
1158
+ throw new Error(`bun add 更新失败: ${update.stderr || update.stdout}`);
1117
1159
  }
1160
+ managedPackageUpdateCache.delete(
1161
+ makeManagedUpdateCacheKey(input.target, fullName),
1162
+ );
1118
1163
  return {
1119
1164
  ok: true,
1120
1165
  restartRequired: true,
@@ -1173,9 +1218,7 @@ export async function removeManagedPackage(
1173
1218
  : fullPackageName(input.target, input.name);
1174
1219
 
1175
1220
  const installCmd = getInstallCommand();
1176
- const removeArgs = installCmd.args.map((a) =>
1177
- a === "add" ? "remove" : a,
1178
- );
1221
+ const removeArgs = installCmd.args.map((a) => (a === "add" ? "remove" : a));
1179
1222
 
1180
1223
  const result = await runCommand(
1181
1224
  installCmd.cmd,
@@ -1194,10 +1237,15 @@ export async function removeManagedPackage(
1194
1237
  try {
1195
1238
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
1196
1239
  const mioki = pkg.mioki ?? {};
1197
- const plugins: string[] = Array.isArray(mioki.plugins) ? mioki.plugins : [];
1240
+ const plugins: string[] = Array.isArray(mioki.plugins)
1241
+ ? mioki.plugins
1242
+ : [];
1198
1243
  const shortName = fullName.replace(/^mioku-plugin-/, "");
1199
1244
  if (plugins.includes(shortName)) {
1200
- pkg.mioki = { ...mioki, plugins: plugins.filter((p: string) => p !== shortName) };
1245
+ pkg.mioki = {
1246
+ ...mioki,
1247
+ plugins: plugins.filter((p: string) => p !== shortName),
1248
+ };
1201
1249
  fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
1202
1250
  }
1203
1251
  } catch {
@@ -1226,17 +1274,6 @@ export async function listManagedPackagesWithUpdates(
1226
1274
  return packages.map((item) => {
1227
1275
  const name = String(item.name || "");
1228
1276
 
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
1277
  const cached = getCachedManagedUpdateInfo(target, name);
1241
1278
  if (cached) {
1242
1279
  return {
@@ -1300,6 +1337,9 @@ export async function getManagedPackageDetail(
1300
1337
  } else {
1301
1338
  repositoryUrl = normalizeRepositoryUrl(latest?.repository);
1302
1339
  }
1340
+ // npm 包以包内 repository 字段为准,缺失时回退到 mioku-lab 仓库
1341
+ const packageRepository = normalizeRepositoryUrl(latest?.repository);
1342
+ const finalRepository = packageRepository || repositoryUrl;
1303
1343
 
1304
1344
  // 获取本地已安装版本
1305
1345
  const localPkg =
@@ -1325,7 +1365,7 @@ export async function getManagedPackageDetail(
1325
1365
  hasGit: false,
1326
1366
  isSystemPlugin,
1327
1367
  isSystemService,
1328
- repository: repositoryUrl,
1368
+ repository: finalRepository,
1329
1369
  originUrl: "",
1330
1370
  homepage: String(latest?.homepage || "").trim(),
1331
1371
  requiredServices: Array.isArray(latest?.mioku?.services)
@@ -1335,7 +1375,7 @@ export async function getManagedPackageDetail(
1335
1375
  help: latest?.mioku?.help || null,
1336
1376
  readme: String(data?.readme || "").trim(),
1337
1377
  readmeFile: "README.md",
1338
- updateState: "npm",
1378
+ updateState: hasUpdates ? "has-updates" : "up-to-date",
1339
1379
  hasUpdates,
1340
1380
  behind: hasUpdates ? 1 : 0,
1341
1381
  changelog: [],
@@ -1445,19 +1485,24 @@ export async function updateAllManagedPackages(input: {
1445
1485
  const results: Array<Record<string, any>> = [];
1446
1486
 
1447
1487
  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;
1488
+ const name = String(item.name || "");
1489
+ if (!name) continue;
1490
+
1491
+ // 统一走 npm 更新检查(已不再使用 git 仓库形式)
1492
+ const cached = getCachedManagedUpdateInfo(input.target, name);
1493
+ let updateInfo: ManagedPackageUpdateInfo;
1494
+ if (cached && isManagedUpdateCacheFresh(cached)) {
1495
+ updateInfo = cached.info;
1496
+ } else {
1497
+ updateInfo = await checkNpmManagedUpdate(
1498
+ fullPackageName(input.target, name),
1499
+ );
1500
+ setCachedManagedUpdateInfo(input.target, name, updateInfo);
1457
1501
  }
1502
+
1458
1503
  if (!updateInfo.hasUpdates) {
1459
1504
  results.push({
1460
- name: item.name,
1505
+ name,
1461
1506
  ok: true,
1462
1507
  skipped: true,
1463
1508
  reason: updateInfo.state === "unknown" ? "CHECK_FAILED" : "UP_TO_DATE",
@@ -1468,19 +1513,19 @@ export async function updateAllManagedPackages(input: {
1468
1513
 
1469
1514
  try {
1470
1515
  const updated = await updateManagedPackage({
1471
- name: item.name,
1516
+ name,
1472
1517
  target: input.target,
1473
1518
  packageManager: input.packageManager,
1474
1519
  });
1475
1520
  results.push({
1476
- name: item.name,
1521
+ name,
1477
1522
  ok: true,
1478
1523
  skipped: false,
1479
1524
  ...updated,
1480
1525
  });
1481
1526
  } catch (error: any) {
1482
1527
  results.push({
1483
- name: item.name,
1528
+ name,
1484
1529
  ok: false,
1485
1530
  skipped: false,
1486
1531
  error: error?.message || "UPDATE_FAILED",
@@ -1828,10 +1873,7 @@ export async function notifyOwnersAuthTokenRefreshed(
1828
1873
  continue;
1829
1874
  }
1830
1875
  try {
1831
- await bot.sendMessage(
1832
- { type: "private", user_id: ownerId },
1833
- message,
1834
- );
1876
+ await bot.sendMessage({ type: "private", user_id: ownerId }, message);
1835
1877
  logger.info(
1836
1878
  `[webui] 已通过 ${bot.adapter ?? "bot"}/${bot.bot_id ?? "?"} 通知主人 ${ownerId} 密钥更新`,
1837
1879
  );
@@ -1843,9 +1885,7 @@ export async function notifyOwnersAuthTokenRefreshed(
1843
1885
  }
1844
1886
  if (!notified) {
1845
1887
  const msg = formatSendError(lastError);
1846
- logger.warn(
1847
- `[webui] 通知主人 ${ownerId} 密钥更新失败: ${msg}`,
1848
- );
1888
+ logger.warn(`[webui] 通知主人 ${ownerId} 密钥更新失败: ${msg}`);
1849
1889
  }
1850
1890
  }
1851
1891
  }
@@ -2108,14 +2148,6 @@ async function getCpuUsagePercent(): Promise<number> {
2108
2148
  return Number((((totalDelta - idleDelta) / totalDelta) * 100).toFixed(1));
2109
2149
  }
2110
2150
 
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
2151
  async function fetchYiyan(): Promise<{ text: string }> {
2120
2152
  const controller = new AbortController();
2121
2153
  const timer = setTimeout(() => controller.abort(), 4500);
@@ -2143,61 +2175,138 @@ function normalizeVersionSpec(input: string): string {
2143
2175
  return matched?.[0] || cleaned;
2144
2176
  }
2145
2177
 
2146
- async function getBotDetails(bot: import("mioku").Bot): Promise<Record<string, any>> {
2178
+ /** 读取已安装适配器包的版本(与 help 状态图里 ctx.getAdapter().version 同源) */
2179
+ function readAdapterPackageVersion(name: string): string {
2180
+ const pkg = readPackageJson(
2181
+ path.join(NODE_MODULES_DIR, `mioku-adapter-${name}`),
2182
+ );
2183
+ return pkg?.version || "";
2184
+ }
2185
+
2186
+ /** 与 help 状态图一致:stdin 使用 help/status 配置的随机头像,未配置时用默认随机图接口 */
2187
+ function readStdinAvatar(): string {
2188
+ const statusConfig = readJsonFile<any>(
2189
+ path.join(CONFIG_DIR, "help", "status.json"),
2190
+ null,
2191
+ );
2192
+ const configured = String(statusConfig?.stdinAvatar || "").trim();
2193
+ if (configured) return configured;
2194
+ return "https://uapis.cn/api/v1/random/image?category=bq&type=eciyuan";
2195
+ }
2196
+
2197
+ async function getBotDetails(bot: Bot): Promise<Record<string, any>> {
2147
2198
  const botAny = bot as unknown as {
2148
2199
  uin?: number | string;
2149
2200
  user_id?: number | string;
2150
2201
  name?: string;
2151
- options?: { protocol?: string; host?: string; port?: number };
2152
- app_version?: string;
2153
2202
  };
2154
- const qq = String(botAny.uin ?? botAny.user_id ?? bot.bot_id);
2203
+ const accountId = String(botAny.uin ?? botAny.user_id ?? bot.bot_id);
2204
+ const isStdin = bot.adapter === "stdin";
2155
2205
  const base = {
2156
2206
  botId: bot.bot_id,
2157
- qq,
2207
+ accountId,
2208
+ qq: accountId,
2158
2209
  nickname: bot.nickname || botAny.name || "Unknown Bot",
2159
- avatar: `https://q1.qlogo.cn/g?b=qq&nk=${qq}&s=160`,
2210
+ avatar: isStdin
2211
+ ? readStdinAvatar()
2212
+ : `https://q1.qlogo.cn/g?b=qq&nk=${accountId}&s=160`,
2160
2213
  online: bot.online,
2161
- napcatVersion: botAny.app_version || "unknown",
2162
- napcatApiBase: toHttpBaseUrl(botAny),
2214
+ adapter: String(bot.adapter),
2215
+ adapterVersion: readAdapterPackageVersion(String(bot.adapter)),
2216
+ implLabel: "",
2217
+ framework: isStdin ? "stdin" : String(bot.adapter),
2218
+ appVersion: "",
2219
+ protocolVersion: "",
2220
+ platform: "",
2221
+ platformVersion: "",
2163
2222
  groupCount: 0,
2164
2223
  friendCount: 0,
2224
+ send: 0,
2225
+ receive: 0,
2165
2226
  onlineDurationMs: 0,
2166
- statusText: "online",
2227
+ statusText: bot.online ? "online" : "offline",
2167
2228
  };
2168
2229
 
2169
2230
  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
- ]);
2231
+ if (typeof bot.getAvatar === "function" && !isStdin) {
2232
+ const avatar = await bot.getAvatar().catch(() => null);
2233
+ if (avatar) base.avatar = avatar;
2234
+ }
2235
+ } catch {
2236
+ // keep qlogo fallback
2237
+ }
2238
+ return base;
2239
+ }
2176
2240
 
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;
2241
+ async function getBotOverviewList(): Promise<Record<string, any>[]> {
2242
+ const bots = Array.from(connectedBots.values());
2243
+ if (bots.length === 0) return [];
2244
+ const sorted = [...bots].sort(
2245
+ (a, b) => Number(b.adapter === "stdin") - Number(a.adapter === "stdin"),
2246
+ );
2183
2247
 
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
- };
2248
+ const adapters = sorted.map((bot) => ({
2249
+ name: String(bot.adapter),
2250
+ version: readAdapterPackageVersion(String(bot.adapter)),
2251
+ }));
2252
+ const report = await buildAdapterReport({ bots: sorted, adapters }).catch(
2253
+ () => null,
2254
+ );
2255
+ const instances = new Map<string, AdapterInstanceStatus>();
2256
+ const implOf = new Map<
2257
+ string,
2258
+ { name: string; version?: string } | undefined
2259
+ >();
2260
+ for (const entry of report?.adapters ?? []) {
2261
+ implOf.set(entry.name, entry.impl);
2262
+ for (const instance of entry.instances) {
2263
+ instances.set(`${entry.name}:${instance.bot_id}`, instance);
2264
+ }
2200
2265
  }
2266
+
2267
+ return Promise.all(
2268
+ sorted.map(async (bot) => {
2269
+ const detail = await getBotDetails(bot);
2270
+ const uin = String(detail.accountId);
2271
+ const adapterName = String(detail.adapter);
2272
+ const isStdin = adapterName === "stdin";
2273
+ const instance = instances.get(`${adapterName}:${uin}`);
2274
+ const lib = implOf.get(adapterName);
2275
+ const appVersion = instance?.version || lib?.version || "";
2276
+ const implLabel = isStdin
2277
+ ? "stdin"
2278
+ : instance?.impl
2279
+ ? instance.version
2280
+ ? `${instance.impl}/${instance.version}`
2281
+ : String(instance.impl)
2282
+ : lib?.name
2283
+ ? lib.version
2284
+ ? `${lib.name} v${lib.version}`
2285
+ : lib.name
2286
+ : adapterName;
2287
+
2288
+ return {
2289
+ ...detail,
2290
+ implLabel,
2291
+ framework: isStdin ? "stdin" : adapterName,
2292
+ appVersion: isStdin
2293
+ ? String(readAdapterPackageVersion(adapterName))
2294
+ : appVersion,
2295
+ protocolVersion: instance?.protocol ?? "",
2296
+ platform: instance?.platform ?? "",
2297
+ platformVersion: instance?.platformVersion ?? "",
2298
+ online: instance?.online ?? bot.online,
2299
+ groupCount: instance?.stats.groups ?? 0,
2300
+ friendCount: instance?.stats.friends ?? 0,
2301
+ send: instance?.stats.sent ?? 0,
2302
+ receive: instance?.stats.received ?? 0,
2303
+ onlineDurationMs:
2304
+ bot.connected_at != null && bot.online
2305
+ ? Math.max(0, Date.now() - bot.connected_at)
2306
+ : 0,
2307
+ };
2308
+ }),
2309
+ );
2201
2310
  }
2202
2311
 
2203
2312
  export async function getSystemOverview(): Promise<Record<string, any>> {
@@ -2221,9 +2330,10 @@ export async function getSystemOverview(): Promise<Record<string, any>> {
2221
2330
  : 0;
2222
2331
  const siSnapshot = await getSystemInformationSnapshot();
2223
2332
 
2224
- const botInstances = Array.from(connectedBots.values());
2225
- const bots = await Promise.all(botInstances.map((bot) => getBotDetails(bot)));
2226
- const selectedBot = bots[0] || null;
2333
+ const bots = await getBotOverviewList();
2334
+ // 默认展示第一个非 stdin 的 bot(stdin 是终端通道,不是真实账号)
2335
+ const selectedBot =
2336
+ bots.find((bot) => bot.adapter !== "stdin") || bots[0] || null;
2227
2337
 
2228
2338
  return {
2229
2339
  uptimeSeconds: process.uptime(),
@@ -2310,15 +2420,30 @@ export function updateChatConfig(fileName: string, data: any): any {
2310
2420
  return data;
2311
2421
  }
2312
2422
 
2423
+ export function getAgentConfig(fileName: string): any {
2424
+ const filePath = path.join(AGENT_CONFIG_DIR, fileName);
2425
+ return readJsonFile(filePath, {});
2426
+ }
2427
+
2428
+ export function updateAgentConfig(fileName: string, data: any): any {
2429
+ const filePath = path.join(AGENT_CONFIG_DIR, fileName);
2430
+ writeJsonFile(filePath, data);
2431
+ return data;
2432
+ }
2433
+
2313
2434
  export function getAdapterConfigs(adapterName: string): any {
2314
2435
  const rootPkg = readRootPackageJson();
2315
2436
  const adapters = rootPkg?.mioku?.adapters ?? {};
2316
- const safeName = String(adapterName || "").trim().replace(/[^a-zA-Z0-9_-]/g, "");
2437
+ const safeName = String(adapterName || "")
2438
+ .trim()
2439
+ .replace(/[^a-zA-Z0-9_-]/g, "");
2317
2440
  return adapters[safeName] ?? {};
2318
2441
  }
2319
2442
 
2320
2443
  export function updateAdapterConfig(adapterName: string, data: any): any {
2321
- const safeName = String(adapterName || "").trim().replace(/[^a-zA-Z0-9_-]/g, "");
2444
+ const safeName = String(adapterName || "")
2445
+ .trim()
2446
+ .replace(/[^a-zA-Z0-9_-]/g, "");
2322
2447
  if (!safeName) throw new Error("适配器名称无效");
2323
2448
  const rootPkg = readRootPackageJson();
2324
2449
  rootPkg.mioku = rootPkg.mioku ?? {};
package/utils.ts CHANGED
@@ -28,6 +28,7 @@ export const DATA_DIR = getDataDir();
28
28
  export const PLUGINS_DIR = path.join(process.cwd(), "plugins");
29
29
  export const SERVICES_DIR = path.join(process.cwd(), "node_modules");
30
30
  export const CHAT_CONFIG_DIR = path.join(getConfigDir(), "chat");
31
+ export const AGENT_CONFIG_DIR = path.join(getConfigDir(), "agent");
31
32
  export const CHAT_DATA_DIR = path.join(getDataDir(), "chat");
32
33
  export const LOGS_DIR = path.join(process.cwd(), "logs");
33
34
  export const NODE_MODULES_DIR = path.join(process.cwd(), "node_modules");