koishi-plugin-mai-bridge 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -58,6 +58,7 @@ webuiEnabled: true
58
58
  webuiHost: 0.0.0.0
59
59
  webuiPort: 8002
60
60
  messageMode: coexist
61
+ groupMessageTriggerCount: 1
61
62
  ```
62
63
 
63
64
  说明:
@@ -67,6 +68,7 @@ messageMode: coexist
67
68
  - `apiKey` 可以留空。插件会生成并复用运行期密钥。
68
69
  - `dockerNetwork` 按你的 Koishi Docker 网络填写。Koishi 需要能通过 `apiHost` 访问 `maimai-ko`。
69
70
  - `webuiPublicUrl` 可填写反代或宿主机映射后的 WebUI 地址,用于 Koishi 控制台显示入口。
71
+ - `groupMessageTriggerCount` 控制群聊累计多少条消息后才转发给 maibot。设为 `3` 时,同一群前两条只记录上下文,第 3 条才拉起思考;私聊不受影响。
70
72
 
71
73
  ## 消息模式
72
74
 
@@ -0,0 +1,16 @@
1
+ import type { Session } from 'koishi';
2
+ import type { KoishiRoute } from '../types';
3
+ export interface GroupTriggerResult {
4
+ shouldForward: boolean;
5
+ count: number;
6
+ threshold: number;
7
+ key?: string;
8
+ }
9
+ export declare class GroupMessageTrigger {
10
+ private threshold;
11
+ private counters;
12
+ constructor(threshold: number);
13
+ test(session: Session, route: KoishiRoute): GroupTriggerResult;
14
+ clear(): void;
15
+ private createKey;
16
+ }
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GroupMessageTrigger = void 0;
4
+ class GroupMessageTrigger {
5
+ threshold;
6
+ counters = new Map();
7
+ constructor(threshold) {
8
+ this.threshold = threshold;
9
+ }
10
+ test(session, route) {
11
+ const threshold = Math.max(1, Math.floor(this.threshold || 1));
12
+ if (threshold <= 1 || session.isDirect || !route.channelId) {
13
+ return { shouldForward: true, count: 1, threshold };
14
+ }
15
+ const key = this.createKey(route);
16
+ const count = (this.counters.get(key) || 0) + 1;
17
+ if (count >= threshold) {
18
+ this.counters.delete(key);
19
+ return { shouldForward: true, count, threshold, key };
20
+ }
21
+ this.counters.set(key, count);
22
+ return { shouldForward: false, count, threshold, key };
23
+ }
24
+ clear() {
25
+ this.counters.clear();
26
+ }
27
+ createKey(route) {
28
+ return [
29
+ route.platform,
30
+ route.botSelfId,
31
+ route.guildId || '',
32
+ route.channelId,
33
+ ].join(':');
34
+ }
35
+ }
36
+ exports.GroupMessageTrigger = GroupMessageTrigger;
@@ -32,7 +32,12 @@ export declare class MaibotPrepareManager {
32
32
  private isValidGitRepository;
33
33
  private isRecoverablePartialDirectory;
34
34
  private ensurePatch;
35
+ private applyPatch;
35
36
  private checkPatchState;
37
+ private refreshManagedPatch;
38
+ private readMarker;
39
+ private listPatchPaths;
40
+ private listDirtyTrackedFiles;
36
41
  private readCommit;
37
42
  private writeMarker;
38
43
  private resolvePatchPath;
@@ -133,6 +133,13 @@ class MaibotPrepareManager {
133
133
  throw new Error(`blocked: bundled patch not found: ${patchPath}`);
134
134
  }
135
135
  if (patchState === 'conflict') {
136
+ const refreshed = await this.refreshManagedPatch(patchPath);
137
+ if (refreshed === 'applied')
138
+ return;
139
+ if (refreshed === 'pending') {
140
+ await this.applyPatch(patchPath);
141
+ return;
142
+ }
136
143
  throw new Error('blocked: bundled patch cannot be applied cleanly; check MaiBot ref or update maimai-koishi.patch');
137
144
  }
138
145
  if (patchState === 'applied') {
@@ -140,6 +147,9 @@ class MaibotPrepareManager {
140
147
  this.pushLog('bundled patch already applied');
141
148
  return;
142
149
  }
150
+ await this.applyPatch(patchPath);
151
+ }
152
+ async applyPatch(patchPath) {
143
153
  this.pushLog(`applying bundled patch: ${patchPath}`);
144
154
  await this.git(['apply', patchPath], this.root);
145
155
  this.patchApplied = true;
@@ -155,6 +165,71 @@ class MaibotPrepareManager {
155
165
  return 'applied';
156
166
  return 'conflict';
157
167
  }
168
+ async refreshManagedPatch(patchPath) {
169
+ const marker = this.readMarker();
170
+ if (!marker?.patchApplied)
171
+ return;
172
+ if (marker.gitUrl !== this.config.maibotGitUrl || marker.gitRef !== this.config.maibotGitRef)
173
+ return;
174
+ if (!marker.patchChecksum || marker.patchChecksum === this.patchChecksum)
175
+ return;
176
+ const patchPaths = new Set(this.listPatchPaths(patchPath));
177
+ const dirtyFiles = await this.listDirtyTrackedFiles();
178
+ if (!dirtyFiles.length)
179
+ return;
180
+ const foreignFiles = dirtyFiles.filter((file) => !patchPaths.has(file));
181
+ if (foreignFiles.length) {
182
+ throw new Error(`blocked: MaiBot has local changes outside bundled patch: ${foreignFiles.slice(0, 5).join(', ')}`);
183
+ }
184
+ this.pushLog('refreshing managed bundled patch');
185
+ await this.git(['restore', '--source', 'HEAD', '--', ...dirtyFiles], this.root);
186
+ const patchState = await this.checkPatchState(patchPath);
187
+ if (patchState === 'pending')
188
+ return 'pending';
189
+ if (patchState === 'applied') {
190
+ this.patchApplied = true;
191
+ this.pushLog('bundled patch already applied');
192
+ return 'applied';
193
+ }
194
+ throw new Error('blocked: bundled patch cannot be refreshed cleanly; check MaiBot ref or update maimai-koishi.patch');
195
+ }
196
+ readMarker() {
197
+ try {
198
+ const marker = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(this.root, '.mai-ko-prepare.json'), 'utf8'));
199
+ if (!marker || typeof marker !== 'object')
200
+ return;
201
+ return marker;
202
+ }
203
+ catch {
204
+ return;
205
+ }
206
+ }
207
+ listPatchPaths(patchPath) {
208
+ const paths = new Set();
209
+ const patch = (0, fs_1.readFileSync)(patchPath, 'utf8');
210
+ for (const line of patch.split(/\r?\n/)) {
211
+ const match = /^diff --git a\/(.+) b\/(.+)$/.exec(line);
212
+ if (!match)
213
+ continue;
214
+ if (match[2] !== '/dev/null')
215
+ paths.add(match[2]);
216
+ }
217
+ return [...paths];
218
+ }
219
+ async listDirtyTrackedFiles() {
220
+ const result = await this.git(['status', '--porcelain', '--untracked-files=no'], this.root, true, false);
221
+ if (result.code !== 0)
222
+ return [];
223
+ return result.stdout
224
+ .split(/\r?\n/)
225
+ .map((line) => {
226
+ if (!line.trim())
227
+ return '';
228
+ const path = line.slice(3);
229
+ return path.includes(' -> ') ? path.split(' -> ').pop() : path;
230
+ })
231
+ .filter(Boolean);
232
+ }
158
233
  async readCommit() {
159
234
  const result = await this.git(['rev-parse', 'HEAD'], this.root, true, false);
160
235
  return result.code === 0 ? result.stdout.trim() : undefined;
package/out/config.d.ts CHANGED
@@ -23,6 +23,7 @@ export interface Config {
23
23
  showWebuiToken: boolean;
24
24
  messageMode: MessageMode;
25
25
  commandPrefix: string;
26
+ groupMessageTriggerCount: number;
26
27
  imageDownloadEnabled: boolean;
27
28
  imageDownloadTimeout: number;
28
29
  imageDownloadMaxBytes: number;
package/out/config.js CHANGED
@@ -43,6 +43,7 @@ exports.Config = koishi_1.Schema.intersect([
43
43
  koishi_1.Schema.const('command').description('命令:仅命中指令前缀时转发。'),
44
44
  ]).default('coexist').description('消息转发模式。'),
45
45
  commandPrefix: koishi_1.Schema.string().default('mai.ko').description('command 模式下触发 mai.ko 的文本前缀。'),
46
+ groupMessageTriggerCount: koishi_1.Schema.number().min(1).default(1).description('同一群聊累计达到多少条消息后才转发给 mai.ko 触发思考;1 表示每条群消息都转发。私聊不受影响。'),
46
47
  imageDownloadEnabled: koishi_1.Schema.boolean().default(true).description('转发图片消息前由 Koishi 下载图片并转为 mai.ko 可识别的 base64 图片段。'),
47
48
  imageDownloadTimeout: koishi_1.Schema.number().min(1000).default(10000).description('下载单张入站图片的超时时间,单位毫秒。'),
48
49
  imageDownloadMaxBytes: koishi_1.Schema.number().min(1024).default(10 * 1024 * 1024).description('允许转发给 mai.ko 的单张图片最大字节数。'),
package/out/service.d.ts CHANGED
@@ -10,6 +10,7 @@ export declare class MaibotService extends Service {
10
10
  private docker;
11
11
  private process;
12
12
  private externalLogs;
13
+ private groupTrigger;
13
14
  private history;
14
15
  private routes;
15
16
  private transport;
package/out/service.js CHANGED
@@ -5,6 +5,7 @@ const koishi_1 = require("koishi");
5
5
  const convert_1 = require("./bridge/convert");
6
6
  const docker_1 = require("./bridge/docker");
7
7
  const external_logs_1 = require("./bridge/external-logs");
8
+ const group_trigger_1 = require("./bridge/group-trigger");
8
9
  const history_1 = require("./bridge/history");
9
10
  const prepare_1 = require("./bridge/prepare");
10
11
  const process_1 = require("./bridge/process");
@@ -21,6 +22,7 @@ class MaibotService extends koishi_1.Service {
21
22
  docker;
22
23
  process;
23
24
  externalLogs;
25
+ groupTrigger;
24
26
  history;
25
27
  routes;
26
28
  transport;
@@ -43,6 +45,7 @@ class MaibotService extends koishi_1.Service {
43
45
  koishiSent: 0,
44
46
  routeMissed: 0,
45
47
  sendFailed: 0,
48
+ groupTriggerSkipped: 0,
46
49
  };
47
50
  constructor(ctx, pluginConfig) {
48
51
  super(ctx, 'maimai');
@@ -53,6 +56,7 @@ class MaibotService extends koishi_1.Service {
53
56
  this.docker = new docker_1.MaibotDockerManager(pluginConfig, this.apiKey);
54
57
  this.process = new process_1.MaibotProcessManager(ctx, pluginConfig, this.apiKey);
55
58
  this.externalLogs = new external_logs_1.ExternalLogForwarder(ctx, pluginConfig, [this.apiKey, pluginConfig.apiKey]);
59
+ this.groupTrigger = new group_trigger_1.GroupMessageTrigger(pluginConfig.groupMessageTriggerCount);
56
60
  this.history = new history_1.MessageHistory(pluginConfig.routeTtl);
57
61
  this.routes = new routes_1.RouteRegistry(pluginConfig.routeTtl);
58
62
  this.transport = new transport_1.MaimTransport(ctx, pluginConfig, this.apiKey, {
@@ -221,6 +225,19 @@ class MaibotService extends koishi_1.Service {
221
225
  this.bridge.lastMessageId = String(session.messageId || session.id || '');
222
226
  this.logMessageDetail(`koishi message received: ${(0, logging_1.describeSession)(session)}`);
223
227
  try {
228
+ const route = this.routes.remember(session);
229
+ const trigger = this.groupTrigger.test(session, route);
230
+ if (!trigger.shouldForward) {
231
+ this.history.rememberSession(session, route);
232
+ this.bridge.groupTriggerSkipped += 1;
233
+ this.bridge.lastGroupTriggerCount = trigger.count;
234
+ this.bridge.lastGroupTriggerThreshold = trigger.threshold;
235
+ this.bridge.lastRouteId = route.routeId;
236
+ this.logMessageDetail(`koishi -> maimai gated: route=${route.routeId} count=${trigger.count}/${trigger.threshold} ${(0, logging_1.describeSession)(session)}`);
237
+ if (this.pluginConfig.messageMode === 'exclusive')
238
+ return '';
239
+ return next();
240
+ }
224
241
  if (this.transportState !== 'connected') {
225
242
  const error = `bridge is not connected: state=${this.transportState}`;
226
243
  this.bridge.sendFailed += 1;
@@ -232,7 +249,6 @@ class MaibotService extends koishi_1.Service {
232
249
  return '';
233
250
  return next();
234
251
  }
235
- const route = this.routes.remember(session);
236
252
  const replyContext = this.history.resolveReplyContext(session, route);
237
253
  const message = await (0, convert_1.sessionToMaimMessage)(session, route, this.apiKey, {
238
254
  resolveImage: (source) => this.resolveImageToBase64(source),
@@ -260,6 +276,7 @@ class MaibotService extends koishi_1.Service {
260
276
  async dispose() {
261
277
  await this.shutdown();
262
278
  this.routes.clear();
279
+ this.groupTrigger.clear();
263
280
  this.history.clear();
264
281
  }
265
282
  logMessageDetail(message) {
package/out/types.d.ts CHANGED
@@ -51,12 +51,15 @@ export interface RuntimeStatus {
51
51
  koishiSent: number;
52
52
  routeMissed: number;
53
53
  sendFailed: number;
54
+ groupTriggerSkipped: number;
54
55
  lastKoishiMessageAt?: number;
55
56
  lastMaimSendAt?: number;
56
57
  lastMaimMessageAt?: number;
57
58
  lastKoishiSendAt?: number;
58
59
  lastRouteId?: string;
59
60
  lastMessageId?: string;
61
+ lastGroupTriggerCount?: number;
62
+ lastGroupTriggerThreshold?: number;
60
63
  lastError?: string;
61
64
  };
62
65
  webui: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-mai-bridge",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "将 maibot 作为 docker 托管的消息回复后端运行,使用 mai.ko 桥接消息收发。",
5
5
  "author": "Jelly10086 <wow40469600@gmail.com>",
6
6
  "contributors": [
@@ -0,0 +1,45 @@
1
+ import type { Session } from 'koishi'
2
+ import type { KoishiRoute } from '../types'
3
+
4
+ export interface GroupTriggerResult {
5
+ shouldForward: boolean
6
+ count: number
7
+ threshold: number
8
+ key?: string
9
+ }
10
+
11
+ export class GroupMessageTrigger {
12
+ private counters = new Map<string, number>()
13
+
14
+ constructor(private threshold: number) {}
15
+
16
+ test(session: Session, route: KoishiRoute): GroupTriggerResult {
17
+ const threshold = Math.max(1, Math.floor(this.threshold || 1))
18
+ if (threshold <= 1 || session.isDirect || !route.channelId) {
19
+ return { shouldForward: true, count: 1, threshold }
20
+ }
21
+
22
+ const key = this.createKey(route)
23
+ const count = (this.counters.get(key) || 0) + 1
24
+ if (count >= threshold) {
25
+ this.counters.delete(key)
26
+ return { shouldForward: true, count, threshold, key }
27
+ }
28
+
29
+ this.counters.set(key, count)
30
+ return { shouldForward: false, count, threshold, key }
31
+ }
32
+
33
+ clear() {
34
+ this.counters.clear()
35
+ }
36
+
37
+ private createKey(route: KoishiRoute) {
38
+ return [
39
+ route.platform,
40
+ route.botSelfId,
41
+ route.guildId || '',
42
+ route.channelId,
43
+ ].join(':')
44
+ }
45
+ }
@@ -158,6 +158,12 @@ export class MaibotPrepareManager {
158
158
  throw new Error(`blocked: bundled patch not found: ${patchPath}`)
159
159
  }
160
160
  if (patchState === 'conflict') {
161
+ const refreshed = await this.refreshManagedPatch(patchPath)
162
+ if (refreshed === 'applied') return
163
+ if (refreshed === 'pending') {
164
+ await this.applyPatch(patchPath)
165
+ return
166
+ }
161
167
  throw new Error('blocked: bundled patch cannot be applied cleanly; check MaiBot ref or update maimai-koishi.patch')
162
168
  }
163
169
  if (patchState === 'applied') {
@@ -166,6 +172,10 @@ export class MaibotPrepareManager {
166
172
  return
167
173
  }
168
174
 
175
+ await this.applyPatch(patchPath)
176
+ }
177
+
178
+ private async applyPatch(patchPath: string) {
169
179
  this.pushLog(`applying bundled patch: ${patchPath}`)
170
180
  await this.git(['apply', patchPath], this.root)
171
181
  this.patchApplied = true
@@ -180,6 +190,68 @@ export class MaibotPrepareManager {
180
190
  return 'conflict'
181
191
  }
182
192
 
193
+ private async refreshManagedPatch(patchPath: string): Promise<'pending' | 'applied' | undefined> {
194
+ const marker = this.readMarker()
195
+ if (!marker?.patchApplied) return
196
+ if (marker.gitUrl !== this.config.maibotGitUrl || marker.gitRef !== this.config.maibotGitRef) return
197
+ if (!marker.patchChecksum || marker.patchChecksum === this.patchChecksum) return
198
+
199
+ const patchPaths = new Set(this.listPatchPaths(patchPath))
200
+ const dirtyFiles = await this.listDirtyTrackedFiles()
201
+ if (!dirtyFiles.length) return
202
+
203
+ const foreignFiles = dirtyFiles.filter((file) => !patchPaths.has(file))
204
+ if (foreignFiles.length) {
205
+ throw new Error(`blocked: MaiBot has local changes outside bundled patch: ${foreignFiles.slice(0, 5).join(', ')}`)
206
+ }
207
+
208
+ this.pushLog('refreshing managed bundled patch')
209
+ await this.git(['restore', '--source', 'HEAD', '--', ...dirtyFiles], this.root)
210
+
211
+ const patchState = await this.checkPatchState(patchPath)
212
+ if (patchState === 'pending') return 'pending'
213
+ if (patchState === 'applied') {
214
+ this.patchApplied = true
215
+ this.pushLog('bundled patch already applied')
216
+ return 'applied'
217
+ }
218
+ throw new Error('blocked: bundled patch cannot be refreshed cleanly; check MaiBot ref or update maimai-koishi.patch')
219
+ }
220
+
221
+ private readMarker(): PrepareMarker | undefined {
222
+ try {
223
+ const marker = JSON.parse(readFileSync(join(this.root, '.mai-ko-prepare.json'), 'utf8')) as PrepareMarker
224
+ if (!marker || typeof marker !== 'object') return
225
+ return marker
226
+ } catch {
227
+ return
228
+ }
229
+ }
230
+
231
+ private listPatchPaths(patchPath: string) {
232
+ const paths = new Set<string>()
233
+ const patch = readFileSync(patchPath, 'utf8')
234
+ for (const line of patch.split(/\r?\n/)) {
235
+ const match = /^diff --git a\/(.+) b\/(.+)$/.exec(line)
236
+ if (!match) continue
237
+ if (match[2] !== '/dev/null') paths.add(match[2])
238
+ }
239
+ return [...paths]
240
+ }
241
+
242
+ private async listDirtyTrackedFiles() {
243
+ const result = await this.git(['status', '--porcelain', '--untracked-files=no'], this.root, true, false)
244
+ if (result.code !== 0) return []
245
+ return result.stdout
246
+ .split(/\r?\n/)
247
+ .map((line) => {
248
+ if (!line.trim()) return ''
249
+ const path = line.slice(3)
250
+ return path.includes(' -> ') ? path.split(' -> ').pop()! : path
251
+ })
252
+ .filter(Boolean)
253
+ }
254
+
183
255
  private async readCommit() {
184
256
  const result = await this.git(['rev-parse', 'HEAD'], this.root, true, false)
185
257
  return result.code === 0 ? result.stdout.trim() : undefined
package/src/config.ts CHANGED
@@ -24,6 +24,7 @@ export interface Config {
24
24
  showWebuiToken: boolean
25
25
  messageMode: MessageMode
26
26
  commandPrefix: string
27
+ groupMessageTriggerCount: number
27
28
  imageDownloadEnabled: boolean
28
29
  imageDownloadTimeout: number
29
30
  imageDownloadMaxBytes: number
@@ -92,6 +93,7 @@ export const Config: Schema<Config> = Schema.intersect([
92
93
  Schema.const('command').description('命令:仅命中指令前缀时转发。'),
93
94
  ]).default('coexist').description('消息转发模式。'),
94
95
  commandPrefix: Schema.string().default('mai.ko').description('command 模式下触发 mai.ko 的文本前缀。'),
96
+ groupMessageTriggerCount: Schema.number().min(1).default(1).description('同一群聊累计达到多少条消息后才转发给 mai.ko 触发思考;1 表示每条群消息都转发。私聊不受影响。'),
95
97
  imageDownloadEnabled: Schema.boolean().default(true).description('转发图片消息前由 Koishi 下载图片并转为 mai.ko 可识别的 base64 图片段。'),
96
98
  imageDownloadTimeout: Schema.number().min(1000).default(10000).description('下载单张入站图片的超时时间,单位毫秒。'),
97
99
  imageDownloadMaxBytes: Schema.number().min(1024).default(10 * 1024 * 1024).description('允许转发给 mai.ko 的单张图片最大字节数。'),
package/src/service.ts CHANGED
@@ -3,6 +3,7 @@ import type { Config } from './config'
3
3
  import { getFallbackRouteHints, getRouteIdFromMaim, maimMessageToFragment, sessionToMaimMessage, shouldForwardSession } from './bridge/convert'
4
4
  import { MaibotDockerManager } from './bridge/docker'
5
5
  import { ExternalLogForwarder } from './bridge/external-logs'
6
+ import { GroupMessageTrigger } from './bridge/group-trigger'
6
7
  import { MessageHistory } from './bridge/history'
7
8
  import { MaibotPrepareManager } from './bridge/prepare'
8
9
  import { MaibotProcessManager, createApiKey } from './bridge/process'
@@ -19,6 +20,7 @@ export class MaibotService extends Service {
19
20
  private docker: MaibotDockerManager
20
21
  private process: MaibotProcessManager
21
22
  private externalLogs: ExternalLogForwarder
23
+ private groupTrigger: GroupMessageTrigger
22
24
  private history: MessageHistory
23
25
  private routes: RouteRegistry
24
26
  private transport: MaimTransport
@@ -41,6 +43,7 @@ export class MaibotService extends Service {
41
43
  koishiSent: 0,
42
44
  routeMissed: 0,
43
45
  sendFailed: 0,
46
+ groupTriggerSkipped: 0,
44
47
  }
45
48
 
46
49
  constructor(public ctx: Context, private pluginConfig: Config) {
@@ -50,6 +53,7 @@ export class MaibotService extends Service {
50
53
  this.docker = new MaibotDockerManager(pluginConfig, this.apiKey)
51
54
  this.process = new MaibotProcessManager(ctx, pluginConfig, this.apiKey)
52
55
  this.externalLogs = new ExternalLogForwarder(ctx, pluginConfig, [this.apiKey, pluginConfig.apiKey])
56
+ this.groupTrigger = new GroupMessageTrigger(pluginConfig.groupMessageTriggerCount)
53
57
  this.history = new MessageHistory(pluginConfig.routeTtl)
54
58
  this.routes = new RouteRegistry(pluginConfig.routeTtl)
55
59
  this.transport = new MaimTransport(ctx, pluginConfig, this.apiKey, {
@@ -227,6 +231,19 @@ export class MaibotService extends Service {
227
231
  this.logMessageDetail(`koishi message received: ${describeSession(session)}`)
228
232
 
229
233
  try {
234
+ const route = this.routes.remember(session)
235
+ const trigger = this.groupTrigger.test(session, route)
236
+ if (!trigger.shouldForward) {
237
+ this.history.rememberSession(session, route)
238
+ this.bridge.groupTriggerSkipped += 1
239
+ this.bridge.lastGroupTriggerCount = trigger.count
240
+ this.bridge.lastGroupTriggerThreshold = trigger.threshold
241
+ this.bridge.lastRouteId = route.routeId
242
+ this.logMessageDetail(`koishi -> maimai gated: route=${route.routeId} count=${trigger.count}/${trigger.threshold} ${describeSession(session)}`)
243
+ if (this.pluginConfig.messageMode === 'exclusive') return ''
244
+ return next()
245
+ }
246
+
230
247
  if (this.transportState !== 'connected') {
231
248
  const error = `bridge is not connected: state=${this.transportState}`
232
249
  this.bridge.sendFailed += 1
@@ -237,7 +254,6 @@ export class MaibotService extends Service {
237
254
  return next()
238
255
  }
239
256
 
240
- const route = this.routes.remember(session)
241
257
  const replyContext = this.history.resolveReplyContext(session, route)
242
258
  const message = await sessionToMaimMessage(session, route, this.apiKey, {
243
259
  resolveImage: (source) => this.resolveImageToBase64(source),
@@ -265,6 +281,7 @@ export class MaibotService extends Service {
265
281
  async dispose() {
266
282
  await this.shutdown()
267
283
  this.routes.clear()
284
+ this.groupTrigger.clear()
268
285
  this.history.clear()
269
286
  }
270
287
 
package/src/types.ts CHANGED
@@ -53,12 +53,15 @@ export interface RuntimeStatus {
53
53
  koishiSent: number
54
54
  routeMissed: number
55
55
  sendFailed: number
56
+ groupTriggerSkipped: number
56
57
  lastKoishiMessageAt?: number
57
58
  lastMaimSendAt?: number
58
59
  lastMaimMessageAt?: number
59
60
  lastKoishiSendAt?: number
60
61
  lastRouteId?: string
61
62
  lastMessageId?: string
63
+ lastGroupTriggerCount?: number
64
+ lastGroupTriggerThreshold?: number
62
65
  lastError?: string
63
66
  }
64
67
  webui: {