mioku 1.0.8 → 1.0.9

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/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- const require_exec = require('./exec-BzdYBQLQ.cjs');
1
+ const require_exec = require('./exec-BUUBQy_V.cjs');
2
2
  const node_fs = require_exec.__toESM(require("node:fs"));
3
3
  const node_path = require_exec.__toESM(require("node:path"));
4
4
  const node_url = require_exec.__toESM(require("node:url"));
@@ -856,6 +856,7 @@ var AdapterContextImpl = class {
856
856
  #logger;
857
857
  #emit;
858
858
  #correlator;
859
+ #commands;
859
860
  #pendingStarts = new Set();
860
861
  constructor(options) {
861
862
  this.#state = options.state;
@@ -866,6 +867,7 @@ var AdapterContextImpl = class {
866
867
  this.#logger = options.logger;
867
868
  this.#emit = options.emit;
868
869
  this.#correlator = options.correlator;
870
+ this.#commands = options.commands;
869
871
  }
870
872
  registerBot(bot) {
871
873
  return this.#bots.register(bot);
@@ -940,6 +942,12 @@ var AdapterContextImpl = class {
940
942
  }
941
943
  }
942
944
  }
945
+ if (event.kind === "message") {
946
+ const duplicate = this.#correlator?.observationOf(event)?.duplicate ?? false;
947
+ if (!duplicate) {
948
+ if (await this.#commands.dispatch(event)) return;
949
+ }
950
+ }
943
951
  await this.#bus.dispatch(event);
944
952
  }
945
953
  listen(route, handler) {
@@ -1055,9 +1063,13 @@ var EventBus = class {
1055
1063
  #nextOrder = 0;
1056
1064
  #dispatched = 0;
1057
1065
  #logger = null;
1066
+ #filter = null;
1058
1067
  setLogger(logger) {
1059
1068
  this.#logger = logger;
1060
1069
  }
1070
+ setFilter(filter$1) {
1071
+ this.#filter = filter$1;
1072
+ }
1061
1073
  /** 注册事件监听,返回取消注册函数 */
1062
1074
  register(route, handler, options = {}) {
1063
1075
  if (!route) throw new Error("EventBus.register: route must be a non-empty string");
@@ -1091,6 +1103,11 @@ var EventBus = class {
1091
1103
  const matched = [];
1092
1104
  for (const reg of this.#registrations) {
1093
1105
  if (reg.disposed) continue;
1106
+ if (this.#filter) try {
1107
+ if (!this.#filter(reg, event)) continue;
1108
+ } catch (err) {
1109
+ this.#logger?.("error", `Event registration filter failed for "${reg.source}"`, err);
1110
+ }
1094
1111
  if (isWildcardMatch(reg.route, event.type)) {
1095
1112
  matched.push(reg);
1096
1113
  continue;
@@ -1129,6 +1146,57 @@ var EventBus = class {
1129
1146
  }
1130
1147
  };
1131
1148
 
1149
+ //#endregion
1150
+ //#region src/runtime/permissions.ts
1151
+ const isObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1152
+ /** 从事件对象中提取发送者 id(支持裸 id / user_id / sender.user_id) */
1153
+ const toUserId = (value) => {
1154
+ if (typeof value === "number" || typeof value === "string" || typeof value === "bigint") return String(value);
1155
+ if (isObject$1(value)) {
1156
+ if ("user_id" in value) return toUserId(value.user_id);
1157
+ if ("sender" in value) {
1158
+ const sender = value.sender;
1159
+ if (isObject$1(sender) && "user_id" in sender) return toUserId(sender.user_id);
1160
+ }
1161
+ }
1162
+ return void 0;
1163
+ };
1164
+ /** 是否群消息 */
1165
+ const isGroupMessage = (event) => isObject$1(event) && event.kind === "message" && event.message_type === "group";
1166
+ /** 当前群群主(仅限所在群) */
1167
+ const isEventGroupOwner = (event) => {
1168
+ if (!isGroupMessage(event)) return false;
1169
+ return event.sender?.role === "owner";
1170
+ };
1171
+ /** 当前群群主或群管理员(仅限所在群) */
1172
+ const isEventGroupAdmin = (event) => {
1173
+ if (!isGroupMessage(event)) return false;
1174
+ const role = event.sender?.role;
1175
+ return role === "owner" || role === "admin";
1176
+ };
1177
+ /** 仅看 owners 名单:bot 主人(无群身份加成) */
1178
+ const isEventMaster$1 = (event) => {
1179
+ const id = toUserId(event);
1180
+ if (!id) return false;
1181
+ return isOwner(id);
1182
+ };
1183
+ /** 仅看 admins 名单:配置管理员 */
1184
+ const isEventAdminConfigOnly = (event) => {
1185
+ const id = toUserId(event);
1186
+ if (!id) return false;
1187
+ return isAdmin(id);
1188
+ };
1189
+ /**
1190
+ * master 或当前群群主
1191
+ */
1192
+ const isEventOwner$1 = (event) => isEventMaster$1(event) || isEventGroupOwner(event);
1193
+ /**
1194
+ * master、配置管理员、当前群群主或群管理员
1195
+ */
1196
+ const isEventAdmin$1 = (event) => isEventOwner$1(event) || isEventAdminConfigOnly(event) || isEventGroupAdmin(event);
1197
+ const isEventOwnerOrAdmin$1 = (event) => isEventAdmin$1(event);
1198
+ const hasEventRight$1 = (event) => isEventOwnerOrAdmin$1(event);
1199
+
1132
1200
  //#endregion
1133
1201
  //#region src/utils.ts
1134
1202
  const ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0.0.0";
@@ -1896,31 +1964,18 @@ const Services = {
1896
1964
 
1897
1965
  //#endregion
1898
1966
  //#region src/runtime/mioku-context.ts
1899
- const isObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1900
- /** 从事件对象中提取发送者 id(支持裸 id / user_id / sender.user_id) */
1901
- const toUserId = (value) => {
1902
- if (typeof value === "number" || typeof value === "string" || typeof value === "bigint") return String(value);
1903
- if (isObject$1(value)) {
1904
- if ("user_id" in value) return toUserId(value.user_id);
1905
- if ("sender" in value) {
1906
- const sender = value.sender;
1907
- if (isObject$1(sender) && "user_id" in sender) return toUserId(sender.user_id);
1908
- }
1909
- }
1910
- return void 0;
1911
- };
1912
- const isEventOwner = (event) => {
1913
- const id = toUserId(event);
1914
- if (!id) return false;
1915
- return isOwner(id);
1916
- };
1917
- const isEventAdmin = (event) => {
1918
- const id = toUserId(event);
1919
- if (!id) return false;
1920
- return isAdmin(id);
1921
- };
1922
- const isEventOwnerOrAdmin = (event) => isEventOwner(event) || isEventAdmin(event);
1923
- const hasEventRight = (event) => isEventOwnerOrAdmin(event);
1967
+ /** 仅看 owners 名单:bot 主人(无群身份加成) */
1968
+ const isEventMaster = (event) => isEventMaster$1(event);
1969
+ /**
1970
+ * master 或当前群群主。等价于 `ctx.command({ permission: "owner" })` 的判断。
1971
+ */
1972
+ const isEventOwner = (event) => isEventOwner$1(event);
1973
+ /**
1974
+ * master、配置管理员、当前群群主或群管理员。等价于 `ctx.command({ permission: "admin" })` 的判断。
1975
+ */
1976
+ const isEventAdmin = (event) => isEventAdmin$1(event);
1977
+ const isEventOwnerOrAdmin = (event) => isEventOwnerOrAdmin$1(event);
1978
+ const hasEventRight = (event) => hasEventRight$1(event);
1924
1979
  const CTX_UTILS = {
1925
1980
  localeDate,
1926
1981
  localeTime,
@@ -1975,6 +2030,9 @@ var MiokuContext = class {
1975
2030
  get pluginName() {
1976
2031
  return this.#options.pluginName;
1977
2032
  }
2033
+ get pluginId() {
2034
+ return this.#options.pluginId;
2035
+ }
1978
2036
  /** 第一个已连接的 bot(多数单 bot 场景直接用这个) */
1979
2037
  get bot() {
1980
2038
  return this.#options.bots.all()[0];
@@ -2074,7 +2132,7 @@ var MiokuContext = class {
2074
2132
  const inputRoutes = Array.isArray(route) ? route : [route];
2075
2133
  const bypassDedup = inputRoutes.some((item) => item.startsWith("!"));
2076
2134
  const routes = inputRoutes.map((item) => item.startsWith("!") ? item.slice(1) : item);
2077
- const source = `plugin:${this.#options.pluginName}`;
2135
+ const source = `plugin:${this.#options.pluginId}`;
2078
2136
  const handledEvents = new WeakSet();
2079
2137
  const correlator = bypassDedup ? void 0 : this.#options.correlator;
2080
2138
  const wrappedHandler = async (event) => {
@@ -2096,6 +2154,17 @@ var MiokuContext = class {
2096
2154
  this.#addCleanup(dispose);
2097
2155
  return dispose;
2098
2156
  }
2157
+ command(definitionOrMatcher, handler, options = {}) {
2158
+ const dispose = typeof definitionOrMatcher === "object" && definitionOrMatcher !== null && "name" in definitionOrMatcher ? this.#options.commands.register(this.#options.pluginId, {
2159
+ ...definitionOrMatcher,
2160
+ priority: definitionOrMatcher.priority ?? this.#options.priority
2161
+ }, this) : this.#options.commands.register(this.#options.pluginId, definitionOrMatcher, handler, {
2162
+ ...options,
2163
+ priority: options.priority ?? this.#options.priority
2164
+ }, this);
2165
+ this.#addCleanup(dispose);
2166
+ return dispose;
2167
+ }
2099
2168
  /** 注册 cron 定时任务,插件卸载时自动停止 */
2100
2169
  cron(expression, handler) {
2101
2170
  const task = node_cron.default.schedule(expression, async (taskContext) => {
@@ -2152,6 +2221,10 @@ var MiokuContext = class {
2152
2221
  get buses() {
2153
2222
  return this.#options.bus;
2154
2223
  }
2224
+ /** 当前运行时的命令管理器 */
2225
+ get commands() {
2226
+ return this.#options.commands;
2227
+ }
2155
2228
  get segment() {
2156
2229
  return segment;
2157
2230
  }
@@ -2192,24 +2265,30 @@ var MiokuContext = class {
2192
2265
  return noticeAdmins(this.#options.bots.all(), this.#options.config.admins, message, options);
2193
2266
  }
2194
2267
  isGroupMsg(event) {
2195
- return isObject$1(event) && event.kind === "message" && event.message_type === "group";
2268
+ return isObject(event) && event.kind === "message" && event.message_type === "group";
2196
2269
  }
2197
2270
  isPrivateMsg(event) {
2198
- return isObject$1(event) && event.kind === "message" && event.message_type === "private";
2271
+ return isObject(event) && event.kind === "message" && event.message_type === "private";
2199
2272
  }
2200
- /** 事件发送者是否为主人 */
2273
+ /** 事件发送者是否为 bot 主人(仅看 owners 名单,不识别群身份) */
2274
+ isMaster(event) {
2275
+ return isEventMaster(event);
2276
+ }
2277
+ /** 事件发送者是否为 bot 主人或当前群群主;等价于 `permission: "owner"` */
2201
2278
  isOwner(event) {
2202
2279
  return isEventOwner(event);
2203
2280
  }
2204
- /** 事件发送者是否为管理员 */
2281
+ /** 事件发送者是否为 bot 主人、配置管理员、当前群群主或群管理员;等价于 `permission: "admin"` */
2205
2282
  isAdmin(event) {
2206
2283
  return isEventAdmin(event);
2207
2284
  }
2285
+ /** 同 `isAdmin`,保留旧名称 */
2208
2286
  isOwnerOrAdmin(event) {
2209
- return isEventOwnerOrAdmin(event);
2287
+ return isEventAdmin(event);
2210
2288
  }
2289
+ /** 同 `isAdmin`,别名 */
2211
2290
  hasRight(event) {
2212
- return hasEventRight(event);
2291
+ return isEventAdmin(event);
2213
2292
  }
2214
2293
  async dispose() {
2215
2294
  const all = Array.from(this.#cleanup);
@@ -2500,19 +2579,387 @@ var EventCorrelator = class {
2500
2579
  }
2501
2580
  };
2502
2581
 
2582
+ //#endregion
2583
+ //#region src/runtime/commands.ts
2584
+ const normalizeRole = (role) => {
2585
+ const value = String(role ?? "member").trim().toLowerCase();
2586
+ if (value === "master" || value === "owner" || value === "admin") return value;
2587
+ return "member";
2588
+ };
2589
+ const userIdOf = (event) => toUserId(event);
2590
+ const textHookMatches = (text$1, raw) => {
2591
+ const source = String(raw ?? "").trim();
2592
+ if (!source) return false;
2593
+ if (source.startsWith("/") && source.length > 1) {
2594
+ const end = source.lastIndexOf("/");
2595
+ if (end > 0) try {
2596
+ const flags = source.slice(end + 1);
2597
+ const regex = new RegExp(source.slice(1, end), flags);
2598
+ return regex.test(text$1);
2599
+ } catch {
2600
+ return false;
2601
+ }
2602
+ }
2603
+ return text$1 === source || text$1.startsWith(source);
2604
+ };
2605
+ const routeMatches = (event, route) => {
2606
+ if (!route) return false;
2607
+ return event.type === route || event.routes.some((item) => item === route);
2608
+ };
2609
+ const parsePrefixes = (prefixes, defaultPrefix) => {
2610
+ if (prefixes === false) return ["", defaultPrefix];
2611
+ const values = Array.isArray(prefixes) ? prefixes : [prefixes];
2612
+ const normalized = values.map((value) => String(value)).filter((value, index, all) => all.indexOf(value) === index);
2613
+ return normalized.length > 0 ? normalized : [defaultPrefix];
2614
+ };
2615
+ const commandBody = (text$1, prefixes) => {
2616
+ const prefix = [...prefixes].sort((a, b) => b.length - a.length).find((item) => item === "" || text$1.startsWith(item));
2617
+ if (prefix === void 0) return void 0;
2618
+ return text$1.slice(prefix.length).trimStart();
2619
+ };
2620
+ const stringMatch = (body, matcher) => {
2621
+ const value = matcher.trim();
2622
+ if (!value) return void 0;
2623
+ if (body !== value && !(body.startsWith(value) && /\s/.test(body[value.length] ?? ""))) return void 0;
2624
+ const rest = body.slice(value.length).trim();
2625
+ return { args: rest ? rest.split(/\s+/) : [] };
2626
+ };
2627
+ const commandKey$1 = (value) => String(value).trim().replace(/^[#/.]+\s*/, "").split(/\s+/)[0].toLowerCase();
2628
+ const displayCommand = (command, defaultPrefix) => {
2629
+ if (command.prefixes === false) return command.name;
2630
+ const prefixes = command.prefixes === void 0 ? [defaultPrefix] : Array.isArray(command.prefixes) ? command.prefixes : [command.prefixes];
2631
+ return `${prefixes.find((prefix) => prefix.length > 0) ?? ""}${command.name}`;
2632
+ };
2633
+ var CommandManager = class {
2634
+ #getDefaultPrefix;
2635
+ #getOwners;
2636
+ #getAdmins;
2637
+ #logger;
2638
+ #commands = [];
2639
+ #legacy = new Map();
2640
+ #access;
2641
+ #nextOrder = 0;
2642
+ constructor(options) {
2643
+ this.#getDefaultPrefix = options.getDefaultPrefix;
2644
+ this.#getOwners = options.getOwners;
2645
+ this.#getAdmins = options.getAdmins;
2646
+ this.#logger = options.logger;
2647
+ }
2648
+ register(plugin, definitionOrMatcher, handlerOrContext, optionsOrContext = {}, context) {
2649
+ const isDefinition = typeof definitionOrMatcher === "object" && definitionOrMatcher !== null && "name" in definitionOrMatcher;
2650
+ const shortcutOptions = isDefinition ? {} : optionsOrContext;
2651
+ const resolvedContext = isDefinition ? handlerOrContext : context;
2652
+ const handler = isDefinition ? void 0 : handlerOrContext;
2653
+ const definition = isDefinition ? definitionOrMatcher : {
2654
+ ...shortcutOptions,
2655
+ name: shortcutOptions.name ?? String(definitionOrMatcher),
2656
+ match: definitionOrMatcher,
2657
+ handler
2658
+ };
2659
+ if (typeof definition.handler !== "function") throw new Error("CommandManager.register: handler must be a function");
2660
+ if (!resolvedContext) throw new Error("CommandManager.register: context is required");
2661
+ const name = String(definition.name ?? "").trim();
2662
+ if (!name) throw new Error("CommandManager.register: name is required");
2663
+ const matcher = definition.match ?? name;
2664
+ const command = {
2665
+ plugin,
2666
+ id: String(definition.id ?? name).trim() || name,
2667
+ name,
2668
+ aliases: (definition.aliases ?? []).map((item) => String(item).trim()).filter(Boolean),
2669
+ matcher,
2670
+ description: String(definition.description ?? "").trim(),
2671
+ usage: definition.usage,
2672
+ permission: normalizeRole(definition.permission),
2673
+ priority: typeof definition.priority === "number" && !Number.isNaN(definition.priority) ? definition.priority : 0,
2674
+ prefixes: definition.prefixes,
2675
+ handler: definition.handler,
2676
+ order: this.#nextOrder++,
2677
+ context: resolvedContext
2678
+ };
2679
+ this.#commands.push(command);
2680
+ return () => {
2681
+ const index = this.#commands.indexOf(command);
2682
+ if (index >= 0) this.#commands.splice(index, 1);
2683
+ };
2684
+ }
2685
+ setAccessControl(config) {
2686
+ this.#access = config;
2687
+ }
2688
+ syncMetadata(metadata) {
2689
+ this.#legacy.set(metadata.name, { metadata });
2690
+ }
2691
+ removePlugin(plugin) {
2692
+ this.#legacy.delete(plugin);
2693
+ for (let i = this.#commands.length - 1; i >= 0; i--) if (this.#commands[i].plugin === plugin) this.#commands.splice(i, 1);
2694
+ }
2695
+ clear() {
2696
+ this.#commands.length = 0;
2697
+ this.#legacy.clear();
2698
+ this.#access = void 0;
2699
+ }
2700
+ list() {
2701
+ return this.#orderedCommands();
2702
+ }
2703
+ #orderedCommands() {
2704
+ return [...this.#commands].sort((a, b) => a.priority - b.priority || a.order - b.order);
2705
+ }
2706
+ getPluginHelp(plugin) {
2707
+ const metadata = this.#legacy.get(plugin)?.metadata;
2708
+ const runtime = this.#commands.filter((item) => item.plugin === plugin);
2709
+ if (!metadata?.config.help && runtime.length === 0) return void 0;
2710
+ const commands = [];
2711
+ const seen = new Set();
2712
+ const runtimeByKey = new Map(runtime.map((command) => [commandKey$1(command.name), command]));
2713
+ for (const command of metadata?.config.help?.commands ?? []) {
2714
+ const key = commandKey$1(command.cmd);
2715
+ const runtimeCommand = runtimeByKey.get(key);
2716
+ if (runtimeCommand) {
2717
+ const cmd = displayCommand(runtimeCommand, this.#getDefaultPrefix());
2718
+ seen.add(key);
2719
+ commands.push({
2720
+ cmd,
2721
+ desc: runtimeCommand.description || command.desc || cmd,
2722
+ usage: runtimeCommand.usage ?? command.usage,
2723
+ role: runtimeCommand.permission
2724
+ });
2725
+ } else if (!seen.has(key)) {
2726
+ seen.add(key);
2727
+ commands.push(command);
2728
+ }
2729
+ }
2730
+ for (const command of runtime) {
2731
+ const cmd = displayCommand(command, this.#getDefaultPrefix());
2732
+ if (seen.has(commandKey$1(cmd))) continue;
2733
+ seen.add(commandKey$1(cmd));
2734
+ commands.push({
2735
+ cmd,
2736
+ desc: command.description || cmd,
2737
+ usage: command.usage,
2738
+ role: command.permission
2739
+ });
2740
+ }
2741
+ const help = metadata?.config.help;
2742
+ return {
2743
+ title: help?.title || plugin,
2744
+ description: help?.description || metadata?.description || "",
2745
+ commands
2746
+ };
2747
+ }
2748
+ catalog() {
2749
+ const items = [];
2750
+ const plugins = new Set([...this.#legacy.keys(), ...this.#commands.map((item) => item.plugin)]);
2751
+ for (const plugin of plugins) {
2752
+ const metadata = this.#legacy.get(plugin)?.metadata;
2753
+ const help = metadata?.config.help;
2754
+ items.push({
2755
+ kind: "plugin",
2756
+ plugin,
2757
+ id: plugin,
2758
+ label: help?.title || plugin,
2759
+ desc: help?.description || metadata?.description,
2760
+ source: metadata ? "manifest" : "runtime"
2761
+ });
2762
+ const seen = new Set();
2763
+ for (const command of this.#commands.filter((item) => item.plugin === plugin)) {
2764
+ seen.add(commandKey$1(command.id));
2765
+ items.push({
2766
+ kind: "command",
2767
+ plugin,
2768
+ id: command.id,
2769
+ label: command.name,
2770
+ desc: command.description,
2771
+ usage: command.usage,
2772
+ permission: command.permission,
2773
+ priority: command.priority,
2774
+ match: command.matcher instanceof RegExp ? command.matcher.toString() : command.matcher,
2775
+ source: "runtime"
2776
+ });
2777
+ }
2778
+ for (const hook of metadata?.config.accessHooks ?? []) {
2779
+ if (seen.has(commandKey$1(hook.id))) continue;
2780
+ seen.add(commandKey$1(hook.id));
2781
+ items.push({
2782
+ kind: "command",
2783
+ plugin,
2784
+ id: hook.id,
2785
+ label: hook.id,
2786
+ desc: hook.description,
2787
+ match: hook.match,
2788
+ event: hook.event,
2789
+ source: "manifest"
2790
+ });
2791
+ }
2792
+ for (const command of help?.commands ?? []) {
2793
+ if (seen.has(commandKey$1(command.cmd))) continue;
2794
+ seen.add(commandKey$1(command.cmd));
2795
+ items.push({
2796
+ kind: "command",
2797
+ plugin,
2798
+ id: command.cmd,
2799
+ label: command.cmd,
2800
+ desc: command.desc,
2801
+ usage: command.usage,
2802
+ permission: command.role,
2803
+ source: "manifest"
2804
+ });
2805
+ }
2806
+ }
2807
+ return items.sort((a, b) => a.kind === b.kind ? a.plugin.localeCompare(b.plugin) || a.id.localeCompare(b.id) : a.kind === "plugin" ? -1 : 1);
2808
+ }
2809
+ async dispatch(event) {
2810
+ const text$1 = String(event.message?.text?.() ?? "").trim();
2811
+ if (!text$1) return false;
2812
+ const match$1 = this.#find(text$1);
2813
+ if (!match$1) return false;
2814
+ if (!this.#canUse(match$1.command, event)) return true;
2815
+ try {
2816
+ await match$1.command.handler({
2817
+ ctx: match$1.command.context,
2818
+ event,
2819
+ command: match$1.command,
2820
+ text: text$1,
2821
+ body: match$1.body,
2822
+ args: match$1.args,
2823
+ match: match$1.match
2824
+ });
2825
+ } catch (error) {
2826
+ this.#logger?.error(`Command "${match$1.command.plugin}:${match$1.command.id}" failed`, error);
2827
+ }
2828
+ return true;
2829
+ }
2830
+ shouldDispatch(source, event) {
2831
+ if (!source.startsWith("plugin:")) return true;
2832
+ const plugin = source.slice(7);
2833
+ const metadata = this.#legacy.get(plugin)?.metadata;
2834
+ if (!metadata) return this.#pluginAllowed(plugin, event);
2835
+ if (!this.#pluginAllowed(plugin, event)) return false;
2836
+ const text$1 = event.kind === "message" ? String(event.message.text()).trim() : "";
2837
+ const matches = (metadata.config.accessHooks ?? []).filter((hook) => event.kind === "message" && textHookMatches(text$1, hook.match) || routeMatches(event, hook.event));
2838
+ return matches.every((hook) => this.#commandAllowed(plugin, hook.id, event));
2839
+ }
2840
+ #find(text$1) {
2841
+ for (const command of this.#orderedCommands()) {
2842
+ const prefixes = parsePrefixes(command.prefixes ?? this.#getDefaultPrefix(), this.#getDefaultPrefix());
2843
+ const body = commandBody(text$1, prefixes);
2844
+ if (body === void 0) continue;
2845
+ if (typeof command.matcher === "string") {
2846
+ const candidates = [
2847
+ command.matcher,
2848
+ command.name,
2849
+ ...command.aliases
2850
+ ];
2851
+ for (const candidate of candidates) {
2852
+ const result = stringMatch(body, candidate);
2853
+ if (result) return {
2854
+ command,
2855
+ body,
2856
+ args: result.args
2857
+ };
2858
+ }
2859
+ continue;
2860
+ }
2861
+ command.matcher.lastIndex = 0;
2862
+ const matched = body.match(command.matcher);
2863
+ if (!matched) continue;
2864
+ const args = matched.length > 1 ? matched.slice(1).filter((item) => item != null) : body.slice(matched[0].length).trim().split(/\s+/).filter(Boolean);
2865
+ return {
2866
+ command,
2867
+ body,
2868
+ args,
2869
+ match: matched
2870
+ };
2871
+ }
2872
+ return void 0;
2873
+ }
2874
+ #canUse(command, event) {
2875
+ const userId = userIdOf(event);
2876
+ const master = Boolean(userId && this.#isMaster(userId));
2877
+ const owner = master || this.#isGroupOwner(event);
2878
+ const admin = owner || Boolean(userId && this.#isAdmin(userId)) || this.#isGroupAdmin(event);
2879
+ if (command.permission === "master") {
2880
+ if (!master) return false;
2881
+ } else if (command.permission === "owner") {
2882
+ if (!owner) return false;
2883
+ } else if (command.permission === "admin" && !admin) return false;
2884
+ return this.#commandAllowed(command.plugin, command.id, event);
2885
+ }
2886
+ #pluginAllowed(plugin, event) {
2887
+ const userId = event.kind === "message" ? userIdOf(event) : "user_id" in event ? event.user_id : void 0;
2888
+ if (userId && (this.#isMaster(userId) || this.#isAdmin(userId)) || this.#isGroupOwner(event) || this.#isGroupAdmin(event)) return true;
2889
+ return this.#resolveAction(plugin, void 0, event) !== "block";
2890
+ }
2891
+ #commandAllowed(plugin, command, event) {
2892
+ const userId = event.kind === "message" ? userIdOf(event) : "user_id" in event ? event.user_id : void 0;
2893
+ if (userId && (this.#isMaster(userId) || this.#isAdmin(userId)) || this.#isGroupOwner(event) || this.#isGroupAdmin(event)) return true;
2894
+ return this.#resolveAction(plugin, command, event) !== "block";
2895
+ }
2896
+ #resolveAction(plugin, command, event) {
2897
+ const access = this.#access;
2898
+ if (!access) return void 0;
2899
+ const scopes = [
2900
+ (() => {
2901
+ const userId = event.kind === "message" ? userIdOf(event) : "user_id" in event && event.user_id ? String(event.user_id) : void 0;
2902
+ return userId ? access.users[userId] : void 0;
2903
+ })(),
2904
+ (() => {
2905
+ const groupId = "group_id" in event && event.group_id ? String(event.group_id) : void 0;
2906
+ return groupId ? access.groups[groupId] : void 0;
2907
+ })(),
2908
+ access.global
2909
+ ];
2910
+ for (const scope of scopes) {
2911
+ if (!scope) continue;
2912
+ const commandRule = command ? scope.commands?.[plugin]?.[command]?.action : void 0;
2913
+ if (commandRule) return commandRule;
2914
+ const pluginRule = scope.plugins?.[plugin]?.action;
2915
+ if (pluginRule) return pluginRule;
2916
+ }
2917
+ return void 0;
2918
+ }
2919
+ #isMaster(userId) {
2920
+ return this.#owners().includes(userId);
2921
+ }
2922
+ #isAdmin(userId) {
2923
+ return this.#isMaster(userId) || this.#admins().includes(userId);
2924
+ }
2925
+ #isGroupOwner(event) {
2926
+ return isEventGroupOwner(event);
2927
+ }
2928
+ #isGroupAdmin(event) {
2929
+ return isEventGroupAdmin(event);
2930
+ }
2931
+ #owners() {
2932
+ return this.#configIds("owners");
2933
+ }
2934
+ #admins() {
2935
+ return this.#configIds("admins");
2936
+ }
2937
+ #configIds(key) {
2938
+ return [...key === "owners" ? this.#getOwners() : this.#getAdmins()].map(String);
2939
+ }
2940
+ };
2941
+ let activeCommandManager;
2942
+ const setActiveCommandManager = (manager) => {
2943
+ activeCommandManager = manager;
2944
+ };
2945
+ const getActiveCommandManager = () => activeCommandManager;
2946
+
2503
2947
  //#endregion
2504
2948
  //#region src/runtime/plugin-metadata.ts
2505
2949
  const store$1 = getOrCreate("plugin-metadata", () => new Map());
2506
2950
  const setPluginMetadata = (meta) => {
2507
2951
  store$1.set(meta.name, meta);
2952
+ getActiveCommandManager()?.syncMetadata(meta);
2508
2953
  };
2509
2954
  const removePluginMetadata = (name) => {
2510
2955
  store$1.delete(name);
2956
+ getActiveCommandManager()?.removePlugin(name);
2511
2957
  };
2512
2958
  const getPluginMetadata = (name) => store$1.get(name);
2513
2959
  const getPluginMetadataList = () => [...store$1.values()];
2514
2960
  const resetPluginMetadata = () => {
2515
2961
  store$1.clear();
2962
+ getActiveCommandManager()?.clear();
2516
2963
  };
2517
2964
 
2518
2965
  //#endregion
@@ -3172,12 +3619,6 @@ async function sendTextOrForward(options) {
3172
3619
  }
3173
3620
  }
3174
3621
 
3175
- //#endregion
3176
- //#region src/builtin/core/commands/prefix.ts
3177
- function getCommandPrefix() {
3178
- return botConfig.prefix ?? ".";
3179
- }
3180
-
3181
3622
  //#endregion
3182
3623
  //#region src/builtin/core/system/package-manager.ts
3183
3624
  const NPM_REGISTRY = "https://registry.npmjs.org";
@@ -3769,102 +4210,102 @@ async function performUpdateAndReport(ctx, event, names) {
3769
4210
  triggerRestart(marker);
3770
4211
  }
3771
4212
  function registerUpdateCommands(ctx) {
3772
- const dispose = ctx.handle("message", async (event) => {
3773
- const text$1 = ctx.text(event)?.trim();
3774
- if (!text$1 || event?.user_id === event?.self_id) return;
3775
- const prefix = getCommandPrefix();
3776
- if (!text$1.startsWith(`${prefix}update`)) return;
3777
- if (!isEventOwner(event)) {
3778
- ctx.logger.warn("[core] update 指令仅主人可用");
3779
- return;
3780
- }
3781
- const arg = text$1.slice(prefix.length + 6).trim();
3782
- const selfId = String(event?.self_id || "");
3783
- const bot = event.bot;
3784
- if (!bot) return;
3785
- if (arg === "all") {
3786
- const managed = await updateAllManaged();
3787
- if (managed.names.length === 0) {
3788
- await replyText(event, "未找到可更新的 mioku 包");
3789
- return;
3790
- }
3791
- await performUpdateAndReport(ctx, event, managed.names);
3792
- return;
3793
- }
3794
- if (arg === "mioku" || arg === "self") {
3795
- await performUpdateAndReport(ctx, event, ["mioku"]);
3796
- return;
3797
- }
3798
- if (arg.startsWith("plugin ") || arg.startsWith("service ")) {
3799
- const [typeRaw, ...rest] = arg.split(/\s+/);
3800
- const name = rest.join(" ").trim();
3801
- const type = typeRaw === "plugin" ? "plugin" : "service";
3802
- if (!name) {
3803
- await replyText(event, `用法:${prefix}update ${type} <名称>`);
4213
+ const dispose = ctx.command({
4214
+ name: "update",
4215
+ aliases: ["更新"],
4216
+ permission: "master",
4217
+ priority: -1e3,
4218
+ description: "检查并选择插件/服务更新",
4219
+ handler: async ({ event, args }) => {
4220
+ const prefix = String(ctx.config.prefix ?? ".");
4221
+ const arg = args.join(" ").trim();
4222
+ const selfId = String(event?.self_id || "");
4223
+ const bot = event.bot;
4224
+ if (!bot) return;
4225
+ if (arg === "all") {
4226
+ const managed = await updateAllManaged();
4227
+ if (managed.names.length === 0) {
4228
+ await replyText(event, "未找到可更新的 mioku 包");
4229
+ return;
4230
+ }
4231
+ await performUpdateAndReport(ctx, event, managed.names);
3804
4232
  return;
3805
4233
  }
3806
- const pkgPrefix = type === "plugin" ? "mioku-plugin-" : "mioku-service-";
3807
- const fullName = name.startsWith(pkgPrefix) ? name : `${pkgPrefix}${name}`;
3808
- await performUpdateAndReport(ctx, event, [fullName]);
3809
- return;
3810
- }
3811
- if (arg === "check" || arg === "检查") {
3812
- const pending = getPendingMap();
3813
- const key = conversationKey(event);
3814
- const existing = pending.get(key);
3815
- if (existing) {
3816
- existing.disposer();
3817
- clearTimeout(existing.timer);
3818
- pending.delete(key);
3819
- }
3820
- let items;
3821
- try {
3822
- items = await checkUpdates();
3823
- } catch (error) {
3824
- ctx.logger.error(`[core] 检查更新失败: ${error}`);
3825
- await replyText(event, `检查更新失败:${String(error)}`);
4234
+ if (arg === "mioku" || arg === "self") {
4235
+ await performUpdateAndReport(ctx, event, ["mioku"]);
3826
4236
  return;
3827
4237
  }
3828
- if (items.length === 0) {
3829
- await replyText(event, "所有插件与服务均已是最新版本");
4238
+ if (arg.startsWith("plugin ") || arg.startsWith("service ")) {
4239
+ const [typeRaw, ...rest] = arg.split(/\s+/);
4240
+ const name = rest.join(" ").trim();
4241
+ const type = typeRaw === "plugin" ? "plugin" : "service";
4242
+ if (!name) {
4243
+ await replyText(event, `用法:${prefix}update ${type} <名称>`);
4244
+ return;
4245
+ }
4246
+ const pkgPrefix = type === "plugin" ? "mioku-plugin-" : "mioku-service-";
4247
+ const fullName = name.startsWith(pkgPrefix) ? name : `${pkgPrefix}${name}`;
4248
+ await performUpdateAndReport(ctx, event, [fullName]);
3830
4249
  return;
3831
4250
  }
3832
- await replyText(event, renderUpdateList(items));
3833
- const timeoutMs = SELECT_TIMEOUT_MS;
3834
- const listenerDispose = ctx.handle("message", async (ev) => {
3835
- if (String(ev?.self_id || "") !== selfId) return;
3836
- if (conversationKey(ev) !== key) return;
3837
- if (!isEventOwner(ev)) return;
3838
- const evText = ctx.text(ev)?.trim() || "";
3839
- if (evText.startsWith(`${prefix}update`)) return;
3840
- const sel = pending.get(key);
3841
- if (!sel) return;
3842
- clearTimeout(sel.timer);
3843
- sel.disposer();
3844
- pending.delete(key);
3845
- const parsed = parseSelection(evText, sel.items.length);
3846
- const chosen = parsed.all ? sel.items : parsed.indices.map((i) => sel.items[i]).filter(Boolean);
3847
- if (chosen.length === 0) {
3848
- await replyText(event, "未选择任何有效项,已取消");
4251
+ if (arg === "check" || arg === "检查") {
4252
+ const pending = getPendingMap();
4253
+ const key = conversationKey(event);
4254
+ const existing = pending.get(key);
4255
+ if (existing) {
4256
+ existing.disposer();
4257
+ clearTimeout(existing.timer);
4258
+ pending.delete(key);
4259
+ }
4260
+ let items;
4261
+ try {
4262
+ items = await checkUpdates();
4263
+ } catch (error) {
4264
+ ctx.logger.error(`[core] 检查更新失败: ${error}`);
4265
+ await replyText(event, `检查更新失败:${String(error)}`);
3849
4266
  return;
3850
4267
  }
3851
- await performUpdateAndReport(ctx, event, chosen.map((i) => i.name));
3852
- });
3853
- const timer = setTimeout(async () => {
3854
- const sel = pending.get(key);
3855
- if (!sel) return;
3856
- sel.disposer();
3857
- pending.delete(key);
3858
- await replyText(event, "操作超时,已取消更新");
3859
- }, timeoutMs);
3860
- pending.set(key, {
3861
- disposer: listenerDispose,
3862
- timer,
3863
- items
3864
- });
3865
- return;
4268
+ if (items.length === 0) {
4269
+ await replyText(event, "所有插件与服务均已是最新版本");
4270
+ return;
4271
+ }
4272
+ await replyText(event, renderUpdateList(items));
4273
+ const timeoutMs = SELECT_TIMEOUT_MS;
4274
+ const listenerDispose = ctx.handle("message", async (ev) => {
4275
+ if (String(ev?.self_id || "") !== selfId) return;
4276
+ if (conversationKey(ev) !== key) return;
4277
+ if (!isEventMaster(ev)) return;
4278
+ const evText = ctx.text(ev)?.trim() || "";
4279
+ if (evText.startsWith(`${prefix}update`)) return;
4280
+ const sel = pending.get(key);
4281
+ if (!sel) return;
4282
+ clearTimeout(sel.timer);
4283
+ sel.disposer();
4284
+ pending.delete(key);
4285
+ const parsed = parseSelection(evText, sel.items.length);
4286
+ const chosen = parsed.all ? sel.items : parsed.indices.map((i) => sel.items[i]).filter(Boolean);
4287
+ if (chosen.length === 0) {
4288
+ await replyText(event, "未选择任何有效项,已取消");
4289
+ return;
4290
+ }
4291
+ await performUpdateAndReport(ctx, event, chosen.map((i) => i.name));
4292
+ });
4293
+ const timer = setTimeout(async () => {
4294
+ const sel = pending.get(key);
4295
+ if (!sel) return;
4296
+ sel.disposer();
4297
+ pending.delete(key);
4298
+ await replyText(event, "操作超时,已取消更新");
4299
+ }, timeoutMs);
4300
+ pending.set(key, {
4301
+ disposer: listenerDispose,
4302
+ timer,
4303
+ items
4304
+ });
4305
+ return;
4306
+ }
4307
+ await replyText(event, `用法:\n${prefix}update check 检查并选择更新\n${prefix}update all 更新全部\n${prefix}update mioku 更新框架\n${prefix}update plugin <名称>\n${prefix}update service <名称>`);
3866
4308
  }
3867
- await replyText(event, `用法:\n${prefix}update check 检查并选择更新\n${prefix}update all 更新全部\n${prefix}update mioku 更新框架\n${prefix}update plugin <名称>\n${prefix}update service <名称>`);
3868
4309
  });
3869
4310
  return () => {
3870
4311
  dispose();
@@ -3879,15 +4320,13 @@ function registerUpdateCommands(ctx) {
3879
4320
 
3880
4321
  //#endregion
3881
4322
  //#region src/builtin/core/commands/install.ts
3882
- function parseTargetArgs(text$1, head) {
3883
- if (!text$1.startsWith(head)) return null;
3884
- const rest = text$1.slice(head.length).trim();
3885
- const matched = rest.match(/^(plugin|service)\s+([\s\S]+)$/);
3886
- if (!matched) return null;
3887
- const name = matched[2].trim();
4323
+ function parseTargetArgs(args) {
4324
+ const type = args[0];
4325
+ if (type !== "plugin" && type !== "service") return null;
4326
+ const name = args.slice(1).join(" ").trim();
3888
4327
  if (!name) return null;
3889
4328
  return {
3890
- type: matched[1],
4329
+ type,
3891
4330
  name
3892
4331
  };
3893
4332
  }
@@ -3895,68 +4334,70 @@ function typeLabel(type) {
3895
4334
  return type === "plugin" ? "插件" : "服务";
3896
4335
  }
3897
4336
  function registerInstallCommands(ctx) {
3898
- const dispose = ctx.handle("message", async (event) => {
3899
- const text$1 = ctx.text(event)?.trim();
3900
- if (!text$1 || event?.user_id === event?.self_id) return;
3901
- const prefix = getCommandPrefix();
3902
- const installHead = `${prefix}install`;
3903
- const uninstallHead = `${prefix}uninstall`;
3904
- if (!text$1.startsWith(installHead) && !text$1.startsWith(uninstallHead)) return;
3905
- if (!isEventOwner(event)) {
3906
- ctx.logger.warn("[core] install/uninstall 指令仅主人可用");
3907
- return;
3908
- }
3909
- if (text$1.startsWith(installHead)) {
3910
- const parsed$1 = parseTargetArgs(text$1, installHead);
3911
- if (!parsed$1) {
3912
- await replyText(event, `用法:${prefix}install plugin <名称> 或 ${prefix}install service <名称>`);
4337
+ const register = (name) => ctx.command({
4338
+ name,
4339
+ aliases: name === "install" ? ["安装"] : ["卸载"],
4340
+ permission: "master",
4341
+ priority: -1e3,
4342
+ description: name === "install" ? "从 npm 安装插件/服务" : "卸载插件/服务",
4343
+ handler: async ({ event, args }) => {
4344
+ if (name === "install") {
4345
+ const parsed$1 = parseTargetArgs(args);
4346
+ if (!parsed$1) {
4347
+ await replyText(event, `用法:${ctx.config.prefix ?? "."}install plugin <名称> 或 ${ctx.config.prefix ?? "."}install service <名称>`);
4348
+ return;
4349
+ }
4350
+ await replyText(event, `正在从 npm 安装${typeLabel(parsed$1.type)} ${parsed$1.name}...`);
4351
+ try {
4352
+ const result = await installPackage(parsed$1.type, parsed$1.name);
4353
+ if (!result.ok) {
4354
+ ctx.logger.error(`[core] 安装失败: ${result.error || result.output}`);
4355
+ await replyText(event, `安装失败:${result.error || result.output}`);
4356
+ return;
4357
+ }
4358
+ const lines = [
4359
+ `已安装 ${result.packageName}`,
4360
+ result.enabled ? "已在 plugins 中启用" : "",
4361
+ "重启后生效"
4362
+ ].filter(Boolean);
4363
+ await replyText(event, lines.join("\n"));
4364
+ } catch (error) {
4365
+ ctx.logger.error(`[core] 安装失败: ${error}`);
4366
+ await replyText(event, `安装失败:${String(error)}`);
4367
+ }
4368
+ return;
4369
+ }
4370
+ const parsed = parseTargetArgs(args);
4371
+ if (!parsed) {
4372
+ await replyText(event, `用法:${ctx.config.prefix ?? "."}uninstall plugin <名称> 或 ${ctx.config.prefix ?? "."}uninstall service <名称>`);
3913
4373
  return;
3914
4374
  }
3915
- await replyText(event, `正在从 npm 安装${typeLabel(parsed$1.type)} ${parsed$1.name}...`);
4375
+ await replyText(event, `正在卸载${typeLabel(parsed.type)} ${parsed.name}...`);
3916
4376
  try {
3917
- const result = await installPackage(parsed$1.type, parsed$1.name);
4377
+ const result = await uninstallPackage(parsed.type, parsed.name);
3918
4378
  if (!result.ok) {
3919
- ctx.logger.error(`[core] 安装失败: ${result.error || result.output}`);
3920
- await replyText(event, `安装失败:${result.error || result.output}`);
4379
+ ctx.logger.error(`[core] 卸载失败: ${result.error || result.output}`);
4380
+ await replyText(event, `卸载失败:${result.error || result.output}`);
3921
4381
  return;
3922
4382
  }
3923
4383
  const lines = [
3924
- `已安装 ${result.packageName}`,
3925
- result.enabled ? "已在 plugins 中启用" : "",
4384
+ `已卸载 ${result.packageName}`,
4385
+ result.removedFromConfig ? "已从 plugins 中移除" : "",
3926
4386
  "重启后生效"
3927
4387
  ].filter(Boolean);
3928
4388
  await replyText(event, lines.join("\n"));
3929
4389
  } catch (error) {
3930
- ctx.logger.error(`[core] 安装失败: ${error}`);
3931
- await replyText(event, `安装失败:${String(error)}`);
4390
+ ctx.logger.error(`[core] 卸载失败: ${error}`);
4391
+ await replyText(event, `卸载失败:${String(error)}`);
3932
4392
  }
3933
- return;
3934
- }
3935
- const parsed = parseTargetArgs(text$1, uninstallHead);
3936
- if (!parsed) {
3937
- await replyText(event, `用法:${prefix}uninstall plugin <名称> 或 ${prefix}uninstall service <名称>`);
3938
- return;
3939
- }
3940
- await replyText(event, `正在卸载${typeLabel(parsed.type)} ${parsed.name}...`);
3941
- try {
3942
- const result = await uninstallPackage(parsed.type, parsed.name);
3943
- if (!result.ok) {
3944
- ctx.logger.error(`[core] 卸载失败: ${result.error || result.output}`);
3945
- await replyText(event, `卸载失败:${result.error || result.output}`);
3946
- return;
3947
- }
3948
- const lines = [
3949
- `已卸载 ${result.packageName}`,
3950
- result.removedFromConfig ? "已从 plugins 中移除" : "",
3951
- "重启后生效"
3952
- ].filter(Boolean);
3953
- await replyText(event, lines.join("\n"));
3954
- } catch (error) {
3955
- ctx.logger.error(`[core] 卸载失败: ${error}`);
3956
- await replyText(event, `卸载失败:${String(error)}`);
3957
4393
  }
3958
4394
  });
3959
- return dispose;
4395
+ const install = register("install");
4396
+ const uninstall = register("uninstall");
4397
+ return () => {
4398
+ install();
4399
+ uninstall();
4400
+ };
3960
4401
  }
3961
4402
 
3962
4403
  //#endregion
@@ -3970,64 +4411,76 @@ function renderMarketText(kind, items) {
3970
4411
  return [`Mioku ${label}市场(共 ${items.length} 个)`, ...lines].join("\n");
3971
4412
  }
3972
4413
  function registerMarketCommands(ctx) {
3973
- const dispose = ctx.handle("message", async (event) => {
3974
- const text$1 = ctx.text(event)?.trim();
3975
- if (!text$1 || event?.user_id === event?.self_id) return;
3976
- const prefix = getCommandPrefix();
3977
- const kind = text$1 === `${prefix}plugin-market` || text$1 === `${prefix}插件市场` ? "plugin" : text$1 === `${prefix}service-market` || text$1 === `${prefix}服务市场` ? "service" : null;
3978
- if (!kind) return;
3979
- if (!isEventOwner(event)) {
3980
- ctx.logger.warn("[core] market 指令仅主人可用");
3981
- return;
3982
- }
3983
- let items;
3984
- try {
3985
- items = await getMarketItems(kind);
3986
- } catch (error) {
3987
- ctx.logger.error(`[core] 获取市场信息失败: ${error}`);
3988
- await replyText(event, `获取市场失败:${String(error)}`);
3989
- return;
3990
- }
3991
- await sendTextOrForward({
3992
- ctx,
3993
- event,
3994
- text: renderMarketText(kind, items),
3995
- source: `Mioku ${kind === "plugin" ? "插件" : "服务"}市场`,
3996
- summary: `共 ${items.length} 个`
3997
- });
4414
+ const pluginMarket = ctx.command({
4415
+ id: "market",
4416
+ name: "plugin-market",
4417
+ aliases: ["插件市场"],
4418
+ permission: "master",
4419
+ priority: -1e3,
4420
+ description: "查看插件市场",
4421
+ handler: (input) => handleMarket(ctx, input.event, "plugin")
4422
+ });
4423
+ const serviceMarket = ctx.command({
4424
+ id: "market",
4425
+ name: "service-market",
4426
+ aliases: ["服务市场"],
4427
+ permission: "master",
4428
+ priority: -1e3,
4429
+ description: "查看服务市场",
4430
+ handler: (input) => handleMarket(ctx, input.event, "service")
4431
+ });
4432
+ return () => {
4433
+ pluginMarket();
4434
+ serviceMarket();
4435
+ };
4436
+ }
4437
+ async function handleMarket(ctx, event, kind) {
4438
+ let items;
4439
+ try {
4440
+ items = await getMarketItems(kind);
4441
+ } catch (error) {
4442
+ ctx.logger.error(`[core] 获取市场信息失败: ${error}`);
4443
+ await replyText(event, `获取市场失败:${String(error)}`);
4444
+ return;
4445
+ }
4446
+ await sendTextOrForward({
4447
+ ctx,
4448
+ event,
4449
+ text: renderMarketText(kind, items),
4450
+ source: `Mioku ${kind === "plugin" ? "插件" : "服务"}市场`,
4451
+ summary: `共 ${items.length} 个`
3998
4452
  });
3999
- return dispose;
4000
4453
  }
4001
4454
 
4002
4455
  //#endregion
4003
4456
  //#region src/builtin/core/commands/restart.ts
4004
4457
  function registerRestartCommand(ctx) {
4005
- const dispose = ctx.handle("message", async (event) => {
4006
- const text$1 = ctx.text(event)?.trim();
4007
- if (!text$1 || event?.user_id === event?.self_id) return;
4008
- const prefix = getCommandPrefix();
4009
- if (text$1 !== `${prefix}restart` && text$1 !== `${prefix}重启`) return;
4010
- if (!isEventOwner(event)) {
4011
- ctx.logger.warn("[core] restart 指令仅主人可用");
4012
- return;
4013
- }
4014
- const uptimeMs = process.uptime() * 1e3;
4015
- const selfId = Number(event?.self_id || 0);
4016
- const groupId = event?.message_type === "group" && event?.group_id ? Number(event.group_id) : null;
4017
- const userId = Number(event?.user_id || 0);
4018
- const marker = {
4019
- initiatedAt: Date.now(),
4020
- selfId,
4021
- groupId,
4022
- userId
4023
- };
4024
- try {
4025
- await replyText(event, `Bot已运行${formatUptime(uptimeMs)},正在重启...`);
4026
- } catch (error) {
4027
- ctx.logger.warn(`[core] 发送重启提示失败: ${error}`);
4458
+ const dispose = ctx.command({
4459
+ name: "restart",
4460
+ aliases: ["重启"],
4461
+ permission: "master",
4462
+ priority: -1e3,
4463
+ description: "重启机器人进程",
4464
+ handler: async ({ event }) => {
4465
+ if (event?.user_id === event?.self_id) return;
4466
+ const uptimeMs = process.uptime() * 1e3;
4467
+ const selfId = Number(event?.self_id || 0);
4468
+ const groupId = event?.message_type === "group" && event?.group_id ? Number(event.group_id) : null;
4469
+ const userId = Number(event?.user_id || 0);
4470
+ const marker = {
4471
+ initiatedAt: Date.now(),
4472
+ selfId,
4473
+ groupId,
4474
+ userId
4475
+ };
4476
+ try {
4477
+ await replyText(event, `Bot已运行${formatUptime(uptimeMs)},正在重启...`);
4478
+ } catch (error) {
4479
+ ctx.logger.warn(`[core] 发送重启提示失败: ${error}`);
4480
+ }
4481
+ ctx.logger.info(`[core] 正在执行重启命令 ${formatUptime(uptimeMs)}`);
4482
+ triggerRestart(marker);
4028
4483
  }
4029
- ctx.logger.info(`[core] 正在执行重启命令 ${formatUptime(uptimeMs)}`);
4030
- triggerRestart(marker);
4031
4484
  });
4032
4485
  return dispose;
4033
4486
  }
@@ -4065,63 +4518,63 @@ function chunk(arr, size) {
4065
4518
  return result;
4066
4519
  }
4067
4520
  function registerLogCommand(ctx) {
4068
- const dispose = ctx.handle("message", async (event) => {
4069
- const text$1 = ctx.text(event)?.trim();
4070
- if (!text$1 || event?.user_id === event?.self_id) return;
4071
- const prefix = getCommandPrefix();
4072
- if (text$1 !== `${prefix}log` && text$1 !== `${prefix}日志`) return;
4073
- if (!isEventOwner(event)) {
4074
- ctx.logger.warn("[core] 日志指令仅主人可用");
4075
- return;
4076
- }
4077
- const selfId = String(event?.self_id || "");
4078
- const bot = event.bot;
4079
- if (!bot) return;
4080
- const logFile = getActiveLogFile();
4081
- if (!logFile) {
4082
- await replyText(event, "未找到日志文件。");
4083
- return;
4084
- }
4085
- let lines;
4086
- try {
4087
- lines = readLastLines(logFile, LOG_LINE_COUNT);
4088
- } catch (error) {
4089
- ctx.logger.error(`[core] 读取日志失败: ${error}`);
4090
- await replyText(event, `读取日志失败:${String(error)}`);
4091
- return;
4092
- }
4093
- if (lines.length === 0) {
4094
- await replyText(event, "日志为空。");
4095
- return;
4096
- }
4097
- let infoCount = 0;
4098
- let warnCount = 0;
4099
- let errorCount = 0;
4100
- for (const line of lines) if (/\bERROR\b|\berror\b/i.test(line)) errorCount++;
4101
- else if (/\bWARN\b|\bwarn\b/i.test(line)) warnCount++;
4102
- else if (/\bINFO\b|\binfo\b/i.test(line)) infoCount++;
4103
- const summary = `info ${infoCount} warn ${warnCount} error ${errorCount}`;
4104
- const chunks = chunk(lines, LINES_PER_NODE);
4105
- const nodes = chunks.map((chunkLines, idx) => ({
4106
- user_id: selfId,
4107
- nickname: `第${idx + 1}条日志`,
4108
- content: [ctx.segment.text(chunkLines.join("\n"))]
4109
- }));
4110
- try {
4111
- const target = event?.message_type === "group" && event?.group_id ? {
4112
- type: "group",
4113
- group_id: event.group_id
4114
- } : event?.user_id ? {
4115
- type: "private",
4116
- user_id: event.user_id
4117
- } : void 0;
4118
- if (target) await bot.sendForward(target, nodes, {
4119
- source: "最近100条运行日志",
4120
- summary
4121
- });
4122
- } catch (error) {
4123
- ctx.logger.error(`[core] 发送日志转发失败: ${error}`);
4124
- await replyText(event, `发送日志失败:${String(error)}`);
4521
+ const dispose = ctx.command({
4522
+ name: "log",
4523
+ aliases: ["日志"],
4524
+ permission: "master",
4525
+ priority: -1e3,
4526
+ description: "查看最近100条日志",
4527
+ handler: async ({ event }) => {
4528
+ if (event?.user_id === event?.self_id) return;
4529
+ const selfId = String(event?.self_id || "");
4530
+ const bot = event.bot;
4531
+ if (!bot) return;
4532
+ const logFile = getActiveLogFile();
4533
+ if (!logFile) {
4534
+ await replyText(event, "未找到日志文件。");
4535
+ return;
4536
+ }
4537
+ let lines;
4538
+ try {
4539
+ lines = readLastLines(logFile, LOG_LINE_COUNT);
4540
+ } catch (error) {
4541
+ ctx.logger.error(`[core] 读取日志失败: ${error}`);
4542
+ await replyText(event, `读取日志失败:${String(error)}`);
4543
+ return;
4544
+ }
4545
+ if (lines.length === 0) {
4546
+ await replyText(event, "日志为空。");
4547
+ return;
4548
+ }
4549
+ let infoCount = 0;
4550
+ let warnCount = 0;
4551
+ let errorCount = 0;
4552
+ for (const line of lines) if (/\bERROR\b|\berror\b/i.test(line)) errorCount++;
4553
+ else if (/\bWARN\b|\bwarn\b/i.test(line)) warnCount++;
4554
+ else if (/\bINFO\b|\binfo\b/i.test(line)) infoCount++;
4555
+ const summary = `info ${infoCount} warn ${warnCount} error ${errorCount}`;
4556
+ const chunks = chunk(lines, LINES_PER_NODE);
4557
+ const nodes = chunks.map((chunkLines, idx) => ({
4558
+ user_id: selfId,
4559
+ nickname: `第${idx + 1}条日志`,
4560
+ content: [ctx.segment.text(chunkLines.join("\n"))]
4561
+ }));
4562
+ try {
4563
+ const target = event?.message_type === "group" && event?.group_id ? {
4564
+ type: "group",
4565
+ group_id: event.group_id
4566
+ } : event?.user_id ? {
4567
+ type: "private",
4568
+ user_id: event.user_id
4569
+ } : void 0;
4570
+ if (target) await bot.sendForward(target, nodes, {
4571
+ source: "最近100条运行日志",
4572
+ summary
4573
+ });
4574
+ } catch (error) {
4575
+ ctx.logger.error(`[core] 发送日志转发失败: ${error}`);
4576
+ await replyText(event, `发送日志失败:${String(error)}`);
4577
+ }
4125
4578
  }
4126
4579
  });
4127
4580
  return dispose;
@@ -4348,7 +4801,6 @@ const core = definePlugin({
4348
4801
  description: "mioku 内置核心插件",
4349
4802
  async setup(ctx) {
4350
4803
  const rawPrefix = ctx.config.prefix ?? ".";
4351
- const cmdPrefix = new RegExp(`^${escapeRegExp(rawPrefix)}`);
4352
4804
  const displayPrefix = rawPrefix;
4353
4805
  const statusAdminOnly = ctx.config.status_permission === "admin-only";
4354
4806
  rootLogger.info("========================================");
@@ -4366,12 +4818,17 @@ const core = definePlugin({
4366
4818
  }));
4367
4819
  } else ctx.logger.warn("config-service 未加载,core 插件将使用默认配置");
4368
4820
  let accessRules = ensureAccessControlConfig();
4821
+ ctx.commands.setAccessControl(accessRules);
4369
4822
  if (configService) {
4370
4823
  await configService.registerConfig("core", "access-control", accessRules);
4371
4824
  const persisted = await configService.getConfig("core", "access-control");
4372
- if (persisted) accessRules = normalizeAccessConfig(persisted);
4825
+ if (persisted) {
4826
+ accessRules = normalizeAccessConfig(persisted);
4827
+ ctx.commands.setAccessControl(accessRules);
4828
+ }
4373
4829
  disposers.push(configService.onConfigChange("core", "access-control", (next) => {
4374
4830
  accessRules = normalizeAccessConfig(next);
4831
+ ctx.commands.setAccessControl(accessRules);
4375
4832
  }));
4376
4833
  }
4377
4834
  rootLogger.info(`访问控制已挂载: ${ctx.bots.length} 个 bot`);
@@ -4433,17 +4890,49 @@ const core = definePlugin({
4433
4890
  bots: collectBots(),
4434
4891
  adapters: collectAdapters()
4435
4892
  }));
4893
+ ctx.command({
4894
+ name: "help",
4895
+ aliases: ["帮助"],
4896
+ permission: "master",
4897
+ priority: -1e3,
4898
+ description: "显示帮助信息",
4899
+ handler: async ({ event }) => {
4900
+ await event.reply(buildCoreHelpText(displayPrefix));
4901
+ }
4902
+ });
4903
+ ctx.command({
4904
+ name: "status",
4905
+ aliases: ["状态"],
4906
+ priority: -1e3,
4907
+ description: "显示框架状态",
4908
+ handler: async ({ event }) => {
4909
+ if (statusAdminOnly && !isEventOwnerOrAdmin(event)) return;
4910
+ await event.reply(await formatMiokuStatus(await getStatus()));
4911
+ }
4912
+ });
4913
+ ctx.command({
4914
+ name: "adapter",
4915
+ aliases: ["适配器"],
4916
+ priority: -1e3,
4917
+ description: "显示适配器与连接实例",
4918
+ handler: async ({ event }) => {
4919
+ if (statusAdminOnly && !isEventOwnerOrAdmin(event)) return;
4920
+ await event.reply(await getAdapterText());
4921
+ }
4922
+ });
4436
4923
  const atTarget = (event) => {
4437
4924
  const at = event.message.find((seg) => seg.type === "at");
4438
4925
  const qq = at?.data?.qq ?? at?.data?.target;
4439
4926
  return qq != null ? String(qq) : void 0;
4440
4927
  };
4441
- disposers.push(ctx.handle("message", async (event) => {
4928
+ const handleCoreMessage = async (event) => {
4442
4929
  const ev = event;
4443
4930
  const text$1 = ev.message.text();
4444
- if (!cmdPrefix.test(text$1)) return;
4931
+ const currentPrefix = String(ctx.config.prefix ?? ".");
4932
+ const currentCmdPrefix = new RegExp(`^${escapeRegExp(currentPrefix)}`);
4933
+ if (!currentCmdPrefix.test(text$1)) return;
4445
4934
  if (statusAdminOnly && !isEventOwnerOrAdmin(ev)) return;
4446
- const bare = text$1.replace(cmdPrefix, "").trim();
4935
+ const bare = text$1.replace(currentCmdPrefix, "").trim();
4447
4936
  if (bare === "状态" || bare === "status") {
4448
4937
  await ev.reply(await formatMiokuStatus(await getStatus()));
4449
4938
  return;
@@ -4452,10 +4941,10 @@ const core = definePlugin({
4452
4941
  await ev.reply(await getAdapterText());
4453
4942
  return;
4454
4943
  }
4455
- if (!isEventOwner(ev)) return;
4944
+ if (!isEventMaster(ev)) return;
4456
4945
  const { cmd, params } = createCmd(text$1);
4457
4946
  if (!cmd) return;
4458
- const subCmd = cmd.replace(cmdPrefix, "").replace(/\s+/g, "");
4947
+ const subCmd = cmd.replace(currentCmdPrefix, "").replace(/\s+/g, "");
4459
4948
  const target = params[0];
4460
4949
  switch (subCmd) {
4461
4950
  case "help":
@@ -4677,7 +5166,29 @@ const core = definePlugin({
4677
5166
  process.exit(0);
4678
5167
  }
4679
5168
  }
4680
- }));
5169
+ };
5170
+ for (const command of [
5171
+ {
5172
+ name: "plugin",
5173
+ aliases: ["插件"],
5174
+ description: "插件管理"
5175
+ },
5176
+ {
5177
+ name: "settings",
5178
+ aliases: ["设置"],
5179
+ description: "框架设置管理"
5180
+ },
5181
+ {
5182
+ name: "exit",
5183
+ aliases: ["退出"],
5184
+ description: "退出框架进程"
5185
+ }
5186
+ ]) ctx.command({
5187
+ ...command,
5188
+ permission: "master",
5189
+ priority: -1e3,
5190
+ handler: ({ event }) => handleCoreMessage(event)
5191
+ });
4681
5192
  rootLogger.info("========================================");
4682
5193
  rootLogger.info(" Mioku 服务初始化完成");
4683
5194
  rootLogger.info("========================================");
@@ -5085,6 +5596,7 @@ var MiokuRuntime = class {
5085
5596
  #driverFactory;
5086
5597
  #builtinPlugins;
5087
5598
  #correlator;
5599
+ #commands;
5088
5600
  #started = false;
5089
5601
  #stopped = false;
5090
5602
  constructor(options) {
@@ -5102,6 +5614,14 @@ var MiokuRuntime = class {
5102
5614
  });
5103
5615
  this.#bots = new BotRegistry();
5104
5616
  this.#capabilities = new CapabilityRegistry();
5617
+ this.#commands = new CommandManager({
5618
+ getDefaultPrefix: () => String(botConfig.prefix ?? "."),
5619
+ getOwners: () => botConfig.owners,
5620
+ getAdmins: () => botConfig.admins,
5621
+ logger: this.#logger
5622
+ });
5623
+ this.#bus.setFilter((registration, event) => this.#commands.shouldDispatch(registration.source, event));
5624
+ setActiveCommandManager(this.#commands);
5105
5625
  }
5106
5626
  get cwd() {
5107
5627
  return this.#cwd;
@@ -5115,6 +5635,9 @@ var MiokuRuntime = class {
5115
5635
  get bus() {
5116
5636
  return this.#bus;
5117
5637
  }
5638
+ get commands() {
5639
+ return this.#commands;
5640
+ }
5118
5641
  /** 事件关联器(跨适配器去重);为 undefined 时表示已禁用 */
5119
5642
  get correlator() {
5120
5643
  return this.#correlator;
@@ -5133,14 +5656,16 @@ var MiokuRuntime = class {
5133
5656
  return this.#bots.pick(bot_id);
5134
5657
  }
5135
5658
  listPlugins() {
5136
- return Array.from(this.#enabledPlugins.entries()).map(([key, entry]) => {
5659
+ return Array.from(this.#enabledPlugins.entries()).map(([key]) => {
5137
5660
  const colon = key.indexOf(":");
5138
5661
  const type = key.slice(0, colon);
5139
5662
  const name = key.slice(colon + 1);
5663
+ const isExternal = type !== "builtin";
5664
+ const version$2 = isExternal ? getPluginMetadata(name)?.version : void 0;
5140
5665
  return {
5141
5666
  name,
5142
5667
  type: type === "builtin" ? "builtin" : "external",
5143
- version: entry.plugin.version
5668
+ version: version$2
5144
5669
  };
5145
5670
  });
5146
5671
  }
@@ -5227,13 +5752,16 @@ var MiokuRuntime = class {
5227
5752
  capabilities: this.#capabilities,
5228
5753
  logger: this.#logger.child({ adapter: state.definition.name }),
5229
5754
  emit: (event) => this.#emitLifecycle(event),
5230
- correlator: this.#correlator
5755
+ correlator: this.#correlator,
5756
+ commands: this.#commands
5231
5757
  });
5232
5758
  }
5233
5759
  async #loadPlugin(plugin, type) {
5234
5760
  const cleanupTasks = [];
5235
5761
  const ctx = new MiokuContext({
5236
5762
  pluginName: `${type}:${plugin.name}`,
5763
+ pluginId: plugin.name === "mioku-core" ? "core" : plugin.name,
5764
+ commands: this.#commands,
5237
5765
  bus: this.#bus,
5238
5766
  bots: this.#bots,
5239
5767
  driver: this.#driver,
@@ -5531,6 +6059,8 @@ var MiokuRuntime = class {
5531
6059
  this.#capabilities.clear();
5532
6060
  this.#bots.clear();
5533
6061
  this.#correlator?.clear();
6062
+ this.#commands.clear();
6063
+ setActiveCommandManager(void 0);
5534
6064
  resetPluginMetadata();
5535
6065
  try {
5536
6066
  await this.#driver.shutdown();
@@ -5547,22 +6077,38 @@ const createRuntime = (options) => {
5547
6077
 
5548
6078
  //#endregion
5549
6079
  //#region src/runtime/plugin-artifacts.ts
6080
+ const commandKey = (value) => String(value).trim().replace(/^[#/.]+\s*/, "").split(/\s+/)[0].toLowerCase();
5550
6081
  async function registerPluginArtifacts(ctx) {
5551
6082
  const helpService = ctx ? getService(ctx, Services.Help) : servicesRegistry["help"];
5552
6083
  const plugins = getPluginMetadataList();
6084
+ const commandManager = getActiveCommandManager();
6085
+ const metadataByName = new Map(plugins.map((meta) => [meta.name, meta]));
6086
+ const pluginNames = new Set([...metadataByName.keys(), ...(commandManager?.catalog() ?? []).filter((item) => item.kind === "plugin").map((item) => item.plugin)]);
5553
6087
  let helpCount = 0;
5554
- for (const meta of plugins) {
5555
- if (meta.config?.help) if (helpService) {
5556
- helpService.registerHelp(meta.name, meta.config.help);
6088
+ for (const pluginName of pluginNames) {
6089
+ const meta = metadataByName.get(pluginName);
6090
+ const generated = commandManager?.getPluginHelp(pluginName) ?? meta?.config?.help;
6091
+ const existing = helpService?.getHelp(pluginName);
6092
+ const help = generated ? {
6093
+ ...generated,
6094
+ title: existing?.title || generated.title,
6095
+ description: existing?.description || generated.description,
6096
+ commands: [...existing?.commands ?? [], ...generated.commands.filter((command) => {
6097
+ const key = commandKey(command.cmd);
6098
+ return !(existing?.commands ?? []).some((item) => commandKey(item.cmd) === key);
6099
+ })]
6100
+ } : existing;
6101
+ if (help) if (helpService) {
6102
+ helpService.registerHelp(pluginName, help);
5557
6103
  helpCount++;
5558
- } else rootLogger.warn(`[plugin-artifacts] 帮助服务未加载,跳过插件 "${meta.name}" 的帮助注册`);
5559
- const required = meta.config?.services;
6104
+ } else rootLogger.warn(`[plugin-artifacts] 帮助服务未加载,跳过插件 "${pluginName}" 的帮助注册`);
6105
+ const required = meta?.config?.services;
5560
6106
  if (required && required.length > 0) {
5561
6107
  const missing = required.filter((name) => !servicesRegistry[name]);
5562
- if (missing.length > 0) rootLogger.warn(`插件 "${meta.name}" 声明依赖服务 [${missing.join(", ")}],但它们未加载`);
6108
+ if (missing.length > 0) rootLogger.warn(`插件 "${pluginName}" 声明依赖服务 [${missing.join(", ")}],但它们未加载`);
5563
6109
  }
5564
6110
  }
5565
- rootLogger.info(`[plugin-artifacts] 已处理 ${plugins.length} 个插件的元数据,注册帮助 ${helpCount} 个`);
6111
+ rootLogger.info(`[plugin-artifacts] 已处理 ${pluginNames.size} 个插件,注册帮助 ${helpCount} 个`);
5566
6112
  }
5567
6113
 
5568
6114
  //#endregion
@@ -5728,6 +6274,7 @@ exports.BotRegistry = BotRegistry;
5728
6274
  exports.CORE_PLUGINS = CORE_PLUGINS;
5729
6275
  exports.CapabilityRegistry = CapabilityRegistry;
5730
6276
  exports.ChromeUA = ChromeUA;
6277
+ exports.CommandManager = CommandManager;
5731
6278
  exports.DriverShutdownError = DriverShutdownError;
5732
6279
  exports.EventBus = EventBus;
5733
6280
  exports.EventCorrelator = EventCorrelator;
@@ -5840,6 +6387,7 @@ Object.defineProperty(exports, 'fs', {
5840
6387
  return node_fs.default;
5841
6388
  }
5842
6389
  });
6390
+ exports.getActiveCommandManager = getActiveCommandManager;
5843
6391
  exports.getAllPluginRuntimeStates = getAllPluginRuntimeStates;
5844
6392
  exports.getBuiltinPlugins = getBuiltinPlugins;
5845
6393
  exports.getConfigDir = getConfigDir;
@@ -5873,6 +6421,10 @@ exports.isBoolean = isBoolean;
5873
6421
  exports.isBotEvent = isBotEvent;
5874
6422
  exports.isDefined = isDefined;
5875
6423
  exports.isEventAdmin = isEventAdmin;
6424
+ exports.isEventAdminConfigOnly = isEventAdminConfigOnly;
6425
+ exports.isEventGroupAdmin = isEventGroupAdmin;
6426
+ exports.isEventGroupOwner = isEventGroupOwner;
6427
+ exports.isEventMaster = isEventMaster;
5876
6428
  exports.isEventOwner = isEventOwner;
5877
6429
  exports.isEventOwnerOrAdmin = isEventOwnerOrAdmin;
5878
6430
  exports.isFunction = isFunction;
@@ -5959,6 +6511,7 @@ exports.segment = segment;
5959
6511
  exports.sendMessage = sendMessage;
5960
6512
  exports.serviceManager = manager_default;
5961
6513
  exports.servicesRegistry = servicesRegistry;
6514
+ exports.setActiveCommandManager = setActiveCommandManager;
5962
6515
  exports.setBotCwd = setBotCwd;
5963
6516
  exports.setBuiltinPlugins = setBuiltinPlugins;
5964
6517
  exports.setPluginMetadata = setPluginMetadata;