koishi-plugin-mai-bridge 0.1.2 → 0.1.3

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
@@ -68,7 +68,7 @@ groupMessageTriggerCount: 1
68
68
  - `apiKey` 可以留空。插件会生成并复用运行期密钥。
69
69
  - `dockerNetwork` 按你的 Koishi Docker 网络填写。Koishi 需要能通过 `apiHost` 访问 `maimai-ko`。
70
70
  - `webuiPublicUrl` 可填写反代或宿主机映射后的 WebUI 地址,用于 Koishi 控制台显示入口。
71
- - `groupMessageTriggerCount` 控制群聊累计多少条消息后才转发给 maibot。设为 `3` 时,同一群前两条只记录上下文,第 3 条才拉起思考;私聊不受影响。
71
+ - `groupMessageTriggerCount` 控制群聊累计多少条消息后批量转发并强制触发 maibot 思考。设为 `3` 时,同一群前两条先缓存,第 3 条会连同前两条一起转发;私聊不受影响。
72
72
 
73
73
  ## 消息模式
74
74
 
@@ -4,6 +4,7 @@ import type { KoishiRoute, MaimApiMessage } from '../types';
4
4
  export interface SessionToMaimOptions {
5
5
  resolveImage?: (source: string) => Awaitable<string | undefined>;
6
6
  replyContext?: ReplyContext;
7
+ forceMention?: boolean;
7
8
  }
8
9
  export interface ReplyContext {
9
10
  targetMessageId: string;
@@ -181,6 +181,8 @@ async function sessionToMaimMessage(session, route, apiKey, options = {}) {
181
181
  const messageId = String(session.messageId || session.id || `koishi-${Date.now()}`);
182
182
  const senderInfo = buildSenderInfo(session);
183
183
  const atBot = isAtBot(session);
184
+ const forceMention = !!options.forceMention;
185
+ const mentioned = atBot || forceMention;
184
186
  const replyContext = options.replyContext;
185
187
  return {
186
188
  message_info: {
@@ -202,8 +204,9 @@ async function sessionToMaimMessage(session, route, apiKey, options = {}) {
202
204
  koishi_reply_to_message_id: replyContext?.targetMessageId,
203
205
  koishi_reply_context_count: replyContext?.contextCount,
204
206
  koishi_is_direct: !!session.isDirect,
205
- at_bot: atBot,
206
- is_mentioned: atBot,
207
+ koishi_group_trigger_forced: forceMention,
208
+ at_bot: mentioned,
209
+ is_mentioned: mentioned,
207
210
  },
208
211
  format_info: {
209
212
  content_format: ['text', 'image'],
@@ -5,10 +5,16 @@ export interface GroupTriggerResult {
5
5
  count: number;
6
6
  threshold: number;
7
7
  key?: string;
8
+ entries: GroupTriggerEntry[];
9
+ forceMention?: boolean;
10
+ }
11
+ export interface GroupTriggerEntry {
12
+ session: Session;
13
+ route: KoishiRoute;
8
14
  }
9
15
  export declare class GroupMessageTrigger {
10
16
  private threshold;
11
- private counters;
17
+ private pending;
12
18
  constructor(threshold: number);
13
19
  test(session: Session, route: KoishiRoute): GroupTriggerResult;
14
20
  clear(): void;
@@ -3,26 +3,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GroupMessageTrigger = void 0;
4
4
  class GroupMessageTrigger {
5
5
  threshold;
6
- counters = new Map();
6
+ pending = new Map();
7
7
  constructor(threshold) {
8
8
  this.threshold = threshold;
9
9
  }
10
10
  test(session, route) {
11
11
  const threshold = Math.max(1, Math.floor(this.threshold || 1));
12
+ const entry = { session, route };
12
13
  if (threshold <= 1 || session.isDirect || !route.channelId) {
13
- return { shouldForward: true, count: 1, threshold };
14
+ return { shouldForward: true, count: 1, threshold, entries: [entry] };
14
15
  }
15
16
  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 };
17
+ const entries = [...this.pending.get(key) || [], entry];
18
+ if (entries.length >= threshold) {
19
+ this.pending.delete(key);
20
+ return { shouldForward: true, count: entries.length, threshold, key, entries, forceMention: true };
20
21
  }
21
- this.counters.set(key, count);
22
- return { shouldForward: false, count, threshold, key };
22
+ this.pending.set(key, entries);
23
+ return { shouldForward: false, count: entries.length, threshold, key, entries: [] };
23
24
  }
24
25
  clear() {
25
- this.counters.clear();
26
+ this.pending.clear();
26
27
  }
27
28
  createKey(route) {
28
29
  return [
package/out/config.js CHANGED
@@ -43,7 +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
+ groupMessageTriggerCount: koishi_1.Schema.number().min(1).default(1).description('同一群聊累计达到多少条消息后批量转发并强制触发 mai.ko 思考;1 表示每条群消息都转发。私聊不受影响。'),
47
47
  imageDownloadEnabled: koishi_1.Schema.boolean().default(true).description('转发图片消息前由 Koishi 下载图片并转为 mai.ko 可识别的 base64 图片段。'),
48
48
  imageDownloadTimeout: koishi_1.Schema.number().min(1000).default(10000).description('下载单张入站图片的超时时间,单位毫秒。'),
49
49
  imageDownloadMaxBytes: koishi_1.Schema.number().min(1024).default(10 * 1024 * 1024).description('允许转发给 mai.ko 的单张图片最大字节数。'),
package/out/service.d.ts CHANGED
@@ -38,6 +38,7 @@ export declare class MaibotService extends Service {
38
38
  dockerRestart(): Promise<RuntimeStatus>;
39
39
  getStatus(): RuntimeStatus;
40
40
  handleSession(session: Session, next: () => Awaitable<void | Fragment>): Promise<void | Fragment>;
41
+ private forwardSessionToMaim;
41
42
  dispose(): Promise<void>;
42
43
  private logMessageDetail;
43
44
  private logMessageSummary;
package/out/service.js CHANGED
@@ -249,19 +249,15 @@ class MaibotService extends koishi_1.Service {
249
249
  return '';
250
250
  return next();
251
251
  }
252
- const replyContext = this.history.resolveReplyContext(session, route);
253
- const message = await (0, convert_1.sessionToMaimMessage)(session, route, this.apiKey, {
254
- resolveImage: (source) => this.resolveImageToBase64(source),
255
- replyContext,
256
- });
257
- this.transport.sendMessage(message);
258
- this.history.rememberSession(session, route);
259
- this.bridge.maimSent += 1;
260
- this.bridge.lastMaimSendAt = Date.now();
261
- this.bridge.lastRouteId = route.routeId;
262
- this.bridge.lastMessageId = message.message_info.message_id;
263
- const replyLog = replyContext ? ` reply=${replyContext.targetMessageId} context=${replyContext.contextCount || 0}` : '';
264
- this.logMessageDetail(`koishi -> maimai forwarded: route=${route.routeId}${replyLog} ${(0, logging_1.describeSegment)(message.message_segment)}`);
252
+ const entries = trigger.entries.length ? trigger.entries : [{ session, route }];
253
+ if (trigger.forceMention) {
254
+ this.logMessageSummary(`group trigger reached: route=${route.routeId} count=${trigger.count}/${trigger.threshold} flushing=${entries.length}`);
255
+ }
256
+ for (let index = 0; index < entries.length; index++) {
257
+ const entry = entries[index];
258
+ const forceMention = !!trigger.forceMention && index === entries.length - 1;
259
+ await this.forwardSessionToMaim(entry.session, entry.route, forceMention);
260
+ }
265
261
  if (this.pluginConfig.messageMode === 'exclusive')
266
262
  return '';
267
263
  }
@@ -273,6 +269,23 @@ class MaibotService extends koishi_1.Service {
273
269
  }
274
270
  return next();
275
271
  }
272
+ async forwardSessionToMaim(session, route, forceMention = false) {
273
+ const replyContext = this.history.resolveReplyContext(session, route);
274
+ const message = await (0, convert_1.sessionToMaimMessage)(session, route, this.apiKey, {
275
+ resolveImage: (source) => this.resolveImageToBase64(source),
276
+ replyContext,
277
+ forceMention,
278
+ });
279
+ this.transport.sendMessage(message);
280
+ this.history.rememberSession(session, route);
281
+ this.bridge.maimSent += 1;
282
+ this.bridge.lastMaimSendAt = Date.now();
283
+ this.bridge.lastRouteId = route.routeId;
284
+ this.bridge.lastMessageId = message.message_info.message_id;
285
+ const replyLog = replyContext ? ` reply=${replyContext.targetMessageId} context=${replyContext.contextCount || 0}` : '';
286
+ const forceLog = forceMention ? ' forced=group-trigger' : '';
287
+ this.logMessageDetail(`koishi -> maimai forwarded: route=${route.routeId}${replyLog}${forceLog} ${(0, logging_1.describeSegment)(message.message_segment)}`);
288
+ }
276
289
  async dispose() {
277
290
  await this.shutdown();
278
291
  this.routes.clear();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-mai-bridge",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "将 maibot 作为 docker 托管的消息回复后端运行,使用 mai.ko 桥接消息收发。",
5
5
  "author": "Jelly10086 <wow40469600@gmail.com>",
6
6
  "contributors": [
@@ -7,6 +7,7 @@ const PLATFORM = 'koishi'
7
7
  export interface SessionToMaimOptions {
8
8
  resolveImage?: (source: string) => Awaitable<string | undefined>
9
9
  replyContext?: ReplyContext
10
+ forceMention?: boolean
10
11
  }
11
12
 
12
13
  export interface ReplyContext {
@@ -204,6 +205,8 @@ export async function sessionToMaimMessage(
204
205
  const messageId = String(session.messageId || session.id || `koishi-${Date.now()}`)
205
206
  const senderInfo = buildSenderInfo(session)
206
207
  const atBot = isAtBot(session)
208
+ const forceMention = !!options.forceMention
209
+ const mentioned = atBot || forceMention
207
210
  const replyContext = options.replyContext
208
211
  return {
209
212
  message_info: {
@@ -225,8 +228,9 @@ export async function sessionToMaimMessage(
225
228
  koishi_reply_to_message_id: replyContext?.targetMessageId,
226
229
  koishi_reply_context_count: replyContext?.contextCount,
227
230
  koishi_is_direct: !!session.isDirect,
228
- at_bot: atBot,
229
- is_mentioned: atBot,
231
+ koishi_group_trigger_forced: forceMention,
232
+ at_bot: mentioned,
233
+ is_mentioned: mentioned,
230
234
  },
231
235
  format_info: {
232
236
  content_format: ['text', 'image'],
@@ -6,32 +6,40 @@ export interface GroupTriggerResult {
6
6
  count: number
7
7
  threshold: number
8
8
  key?: string
9
+ entries: GroupTriggerEntry[]
10
+ forceMention?: boolean
11
+ }
12
+
13
+ export interface GroupTriggerEntry {
14
+ session: Session
15
+ route: KoishiRoute
9
16
  }
10
17
 
11
18
  export class GroupMessageTrigger {
12
- private counters = new Map<string, number>()
19
+ private pending = new Map<string, GroupTriggerEntry[]>()
13
20
 
14
21
  constructor(private threshold: number) {}
15
22
 
16
23
  test(session: Session, route: KoishiRoute): GroupTriggerResult {
17
24
  const threshold = Math.max(1, Math.floor(this.threshold || 1))
25
+ const entry = { session, route }
18
26
  if (threshold <= 1 || session.isDirect || !route.channelId) {
19
- return { shouldForward: true, count: 1, threshold }
27
+ return { shouldForward: true, count: 1, threshold, entries: [entry] }
20
28
  }
21
29
 
22
30
  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 }
31
+ const entries = [...this.pending.get(key) || [], entry]
32
+ if (entries.length >= threshold) {
33
+ this.pending.delete(key)
34
+ return { shouldForward: true, count: entries.length, threshold, key, entries, forceMention: true }
27
35
  }
28
36
 
29
- this.counters.set(key, count)
30
- return { shouldForward: false, count, threshold, key }
37
+ this.pending.set(key, entries)
38
+ return { shouldForward: false, count: entries.length, threshold, key, entries: [] }
31
39
  }
32
40
 
33
41
  clear() {
34
- this.counters.clear()
42
+ this.pending.clear()
35
43
  }
36
44
 
37
45
  private createKey(route: KoishiRoute) {
package/src/config.ts CHANGED
@@ -93,7 +93,7 @@ export const Config: Schema<Config> = Schema.intersect([
93
93
  Schema.const('command').description('命令:仅命中指令前缀时转发。'),
94
94
  ]).default('coexist').description('消息转发模式。'),
95
95
  commandPrefix: Schema.string().default('mai.ko').description('command 模式下触发 mai.ko 的文本前缀。'),
96
- groupMessageTriggerCount: Schema.number().min(1).default(1).description('同一群聊累计达到多少条消息后才转发给 mai.ko 触发思考;1 表示每条群消息都转发。私聊不受影响。'),
96
+ groupMessageTriggerCount: Schema.number().min(1).default(1).description('同一群聊累计达到多少条消息后批量转发并强制触发 mai.ko 思考;1 表示每条群消息都转发。私聊不受影响。'),
97
97
  imageDownloadEnabled: Schema.boolean().default(true).description('转发图片消息前由 Koishi 下载图片并转为 mai.ko 可识别的 base64 图片段。'),
98
98
  imageDownloadTimeout: Schema.number().min(1000).default(10000).description('下载单张入站图片的超时时间,单位毫秒。'),
99
99
  imageDownloadMaxBytes: Schema.number().min(1024).default(10 * 1024 * 1024).description('允许转发给 mai.ko 的单张图片最大字节数。'),
package/src/service.ts CHANGED
@@ -254,19 +254,17 @@ export class MaibotService extends Service {
254
254
  return next()
255
255
  }
256
256
 
257
- const replyContext = this.history.resolveReplyContext(session, route)
258
- const message = await sessionToMaimMessage(session, route, this.apiKey, {
259
- resolveImage: (source) => this.resolveImageToBase64(source),
260
- replyContext,
261
- })
262
- this.transport.sendMessage(message)
263
- this.history.rememberSession(session, route)
264
- this.bridge.maimSent += 1
265
- this.bridge.lastMaimSendAt = Date.now()
266
- this.bridge.lastRouteId = route.routeId
267
- this.bridge.lastMessageId = message.message_info.message_id
268
- const replyLog = replyContext ? ` reply=${replyContext.targetMessageId} context=${replyContext.contextCount || 0}` : ''
269
- this.logMessageDetail(`koishi -> maimai forwarded: route=${route.routeId}${replyLog} ${describeSegment(message.message_segment)}`)
257
+ const entries = trigger.entries.length ? trigger.entries : [{ session, route }]
258
+ if (trigger.forceMention) {
259
+ this.logMessageSummary(`group trigger reached: route=${route.routeId} count=${trigger.count}/${trigger.threshold} flushing=${entries.length}`)
260
+ }
261
+
262
+ for (let index = 0; index < entries.length; index++) {
263
+ const entry = entries[index]
264
+ const forceMention = !!trigger.forceMention && index === entries.length - 1
265
+ await this.forwardSessionToMaim(entry.session, entry.route, forceMention)
266
+ }
267
+
270
268
  if (this.pluginConfig.messageMode === 'exclusive') return ''
271
269
  } catch (error) {
272
270
  const message = error instanceof Error ? error.message : String(error)
@@ -278,6 +276,24 @@ export class MaibotService extends Service {
278
276
  return next()
279
277
  }
280
278
 
279
+ private async forwardSessionToMaim(session: Session, route: ReturnType<RouteRegistry['remember']>, forceMention = false) {
280
+ const replyContext = this.history.resolveReplyContext(session, route)
281
+ const message = await sessionToMaimMessage(session, route, this.apiKey, {
282
+ resolveImage: (source) => this.resolveImageToBase64(source),
283
+ replyContext,
284
+ forceMention,
285
+ })
286
+ this.transport.sendMessage(message)
287
+ this.history.rememberSession(session, route)
288
+ this.bridge.maimSent += 1
289
+ this.bridge.lastMaimSendAt = Date.now()
290
+ this.bridge.lastRouteId = route.routeId
291
+ this.bridge.lastMessageId = message.message_info.message_id
292
+ const replyLog = replyContext ? ` reply=${replyContext.targetMessageId} context=${replyContext.contextCount || 0}` : ''
293
+ const forceLog = forceMention ? ' forced=group-trigger' : ''
294
+ this.logMessageDetail(`koishi -> maimai forwarded: route=${route.routeId}${replyLog}${forceLog} ${describeSegment(message.message_segment)}`)
295
+ }
296
+
281
297
  async dispose() {
282
298
  await this.shutdown()
283
299
  this.routes.clear()