koishi-plugin-mai-bridge 0.1.3 → 0.1.4

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
@@ -59,6 +59,7 @@ webuiHost: 0.0.0.0
59
59
  webuiPort: 8002
60
60
  messageMode: coexist
61
61
  groupMessageTriggerCount: 1
62
+ directMessageTriggerCount: 1
62
63
  ```
63
64
 
64
65
  说明:
@@ -68,7 +69,8 @@ groupMessageTriggerCount: 1
68
69
  - `apiKey` 可以留空。插件会生成并复用运行期密钥。
69
70
  - `dockerNetwork` 按你的 Koishi Docker 网络填写。Koishi 需要能通过 `apiHost` 访问 `maimai-ko`。
70
71
  - `webuiPublicUrl` 可填写反代或宿主机映射后的 WebUI 地址,用于 Koishi 控制台显示入口。
71
- - `groupMessageTriggerCount` 控制群聊累计多少条消息后批量转发并强制触发 maibot 思考。设为 `3` 时,同一群前两条先缓存,第 3 条会连同前两条一起转发;私聊不受影响。
72
+ - `groupMessageTriggerCount` 控制群聊累计多少条消息后批量转发并强制触发 maibot 思考。设为 `3` 时,同一群前两条先缓存,第 3 条会连同前两条一起转发。
73
+ - `directMessageTriggerCount` 控制私聊累计多少条消息后批量转发并强制触发 maibot 思考。默认 `1`,表示每条私信都会触发;设为 `3` 时,同一私聊前两条只缓存,第 3 条统一转发并触发回复。
72
74
 
73
75
  ## 消息模式
74
76
 
package/client/page.vue CHANGED
@@ -10,6 +10,7 @@
10
10
  <p>Bridge:{{ status?.transport.state || 'unknown' }}</p>
11
11
  <p v-if="status?.bridge">中转:Koishi 收 {{ status.bridge.koishiReceived }} / 发往 mai.ko {{ status.bridge.maimSent }} / 收到 mai.ko {{ status.bridge.maimReceived }} / 回发 Koishi {{ status.bridge.koishiSent }}</p>
12
12
  <p v-if="status?.bridge">中转异常:路由失败 {{ status.bridge.routeMissed }} / 发送失败 {{ status.bridge.sendFailed }}</p>
13
+ <p v-if="status?.bridge">触发限制:群聊跳过 {{ status.bridge.groupTriggerSkipped }} / 私信跳过 {{ status.bridge.directTriggerSkipped }}</p>
13
14
  <p>WebUI:{{ webuiText }}</p>
14
15
  <p v-if="status?.webui.token?.value">WebUI Token:<code>{{ status.webui.token.value }}</code></p>
15
16
  <p v-else-if="status?.webui.token?.lastError">WebUI Token:{{ status.webui.token.lastError }}</p>
@@ -76,6 +77,8 @@ interface RuntimeStatus {
76
77
  koishiSent: number
77
78
  routeMissed: number
78
79
  sendFailed: number
80
+ groupTriggerSkipped: number
81
+ directTriggerSkipped: number
79
82
  lastError?: string
80
83
  }
81
84
  webui: {
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import{defineComponent as Z,ref as ee,computed as te,onMounted as oe,resolveComponent as p,openBlock as a,createBlock as F,withCtx as r,createVNode as s,createElementVNode as d,toDisplayString as o,createElementBlock as n,createCommentVNode as u,createTextVNode as l}from"vue";import{send as k}from"@koishijs/client";const ae={key:0},re={key:1},ne={key:2},ue={key:3},le={key:4},ie={key:5},se={key:6},ke={key:7},de={key:8},ve={key:9},pe={key:11},ce={key:12},fe=Z({__name:"page",setup(c){const e=ee(),P=te(()=>e.value?e.value.webui.enabled?e.value.webui.url||`${e.value.webui.host}:${e.value.webui.port}`:"disabled":"unknown");async function f(){e.value=await k("mai-ko/status")}async function h(){e.value=await k("mai-ko/start")}async function q(){e.value=await k("mai-ko/stop")}async function z(){e.value=await k("mai-ko/restart")}async function A(){e.value=await k("mai-ko/reconnect")}async function G(){e.value=await k("mai-ko/prepare")}async function J(){e.value=await k("mai-ko/docker-start")}async function L(){e.value=await k("mai-ko/docker-stop")}async function O(){e.value=await k("mai-ko/docker-restart")}function Q(){var v;(v=e.value)!=null&&v.webui.url&&window.open(e.value.webui.url,"_blank","noopener,noreferrer")}return oe(f),(v,t)=>{const i=p("k-button"),X=p("k-card"),Y=p("k-layout");return a(),F(Y,null,{default:r(()=>[s(X,null,{header:r(()=>[...t[0]||(t[0]=[d("span",null,"mai.ko 托管状态",-1)])]),default:r(()=>{var b,m,w,_,y,g,C,E,D,R,x,B,I,N,S,W,T,U,V,H,K,M,$,j;return[d("p",null,"进程:"+o(((b=e.value)==null?void 0:b.process.state)||"unknown"),1),d("p",null,"准备:"+o(((m=e.value)==null?void 0:m.prepare.state)||"unknown")+" / "+o(((w=e.value)==null?void 0:w.prepare.root)||"-"),1),d("p",null,"Docker:"+o(((_=e.value)==null?void 0:_.docker.state)||"unknown")+" / "+o(((y=e.value)==null?void 0:y.docker.containerName)||"-"),1),d("p",null,"Bridge:"+o(((g=e.value)==null?void 0:g.transport.state)||"unknown"),1),(C=e.value)!=null&&C.bridge?(a(),n("p",ae,"中转:Koishi 收 "+o(e.value.bridge.koishiReceived)+" / 发往 mai.ko "+o(e.value.bridge.maimSent)+" / 收到 mai.ko "+o(e.value.bridge.maimReceived)+" / 回发 Koishi "+o(e.value.bridge.koishiSent),1)):u("v-if",true),(E=e.value)!=null&&E.bridge?(a(),n("p",re,"中转异常:路由失败 "+o(e.value.bridge.routeMissed)+" / 发送失败 "+o(e.value.bridge.sendFailed),1)):u("v-if",true),d("p",null,"WebUI:"+o(P.value),1),(R=(D=e.value)==null?void 0:D.webui.token)!=null&&R.value?(a(),n("p",ne,[t[1]||(t[1]=l("WebUI Token:",-1)),d("code",null,o(e.value.webui.token.value),1)])):(B=(x=e.value)==null?void 0:x.webui.token)!=null&&B.lastError?(a(),n("p",ue,"WebUI Token:"+o(e.value.webui.token.lastError),1)):u("v-if",true),d("p",null,"PID:"+o(((I=e.value)==null?void 0:I.process.pid)||"-"),1),(N=e.value)!=null&&N.process.blockedReason?(a(),n("p",le,"阻塞原因:"+o(e.value.process.blockedReason),1)):u("v-if",true),(S=e.value)!=null&&S.prepare.blockedReason?(a(),n("p",ie,"准备阻塞:"+o(e.value.prepare.blockedReason),1)):u("v-if",true),(W=e.value)!=null&&W.prepare.lastError?(a(),n("p",se,"准备错误:"+o(e.value.prepare.lastError),1)):u("v-if",true),(T=e.value)!=null&&T.docker.lastError?(a(),n("p",ke,"Docker 错误:"+o(e.value.docker.lastError),1)):u("v-if",true),(U=e.value)!=null&&U.transport.lastError?(a(),n("p",de,"Bridge 错误:"+o(e.value.transport.lastError),1)):u("v-if",true),(H=(V=e.value)==null?void 0:V.bridge)!=null&&H.lastError?(a(),n("p",ve,"中转错误:"+o(e.value.bridge.lastError),1)):u("v-if",true),s(i,{onClick:f},{default:r(()=>[...t[2]||(t[2]=[l("刷新",-1)])]),_:1}),s(i,{onClick:h},{default:r(()=>[...t[3]||(t[3]=[l("启动",-1)])]),_:1}),s(i,{onClick:q},{default:r(()=>[...t[4]||(t[4]=[l("停止",-1)])]),_:1}),s(i,{onClick:z},{default:r(()=>[...t[5]||(t[5]=[l("重启",-1)])]),_:1}),s(i,{onClick:A},{default:r(()=>[...t[6]||(t[6]=[l("重连",-1)])]),_:1}),s(i,{onClick:G},{default:r(()=>[...t[7]||(t[7]=[l("准备",-1)])]),_:1}),s(i,{onClick:J},{default:r(()=>[...t[8]||(t[8]=[l("Docker 启动",-1)])]),_:1}),s(i,{onClick:L},{default:r(()=>[...t[9]||(t[9]=[l("Docker 停止",-1)])]),_:1}),s(i,{onClick:O},{default:r(()=>[...t[10]||(t[10]=[l("Docker 重启",-1)])]),_:1}),(K=e.value)!=null&&K.webui.enabled&&((M=e.value)!=null&&M.webui.url)?(a(),F(i,{key:10,onClick:Q},{default:r(()=>[...t[11]||(t[11]=[l("打开 WebUI",-1)])]),_:1})):u("v-if",true),($=e.value)!=null&&$.logsHint?(a(),n("p",pe,o(e.value.logsHint),1)):u("v-if",true),(j=e.value)!=null&&j.logs.length?(a(),n("pre",ce,o(e.value.logs.slice(-20).join(`
2
- `)),1)):u("v-if",true)]}),_:1})]),_:1})}}}),we=c=>{c.page({name:"mai.ko",path:"/mai-ko",authority:3,component:fe})};export{we as default};
1
+ import{defineComponent as ee,ref as te,computed as oe,onMounted as re,resolveComponent as v,openBlock as r,createBlock as F,withCtx as a,createVNode as s,createElementVNode as k,toDisplayString as t,createElementBlock as n,createCommentVNode as u,createTextVNode as l}from"vue";import{send as d}from"@koishijs/client";const ae={key:0},ne={key:1},ue={key:2},le={key:3},ie={key:4},se={key:5},de={key:6},ke={key:7},pe={key:8},ve={key:9},ce={key:10},fe={key:12},be={key:13},me=ee({__name:"page",setup(c){const e=te(),P=oe(()=>e.value?e.value.webui.enabled?e.value.webui.url||`${e.value.webui.host}:${e.value.webui.port}`:"disabled":"unknown");async function f(){e.value=await d("mai-ko/status")}async function q(){e.value=await d("mai-ko/start")}async function z(){e.value=await d("mai-ko/stop")}async function A(){e.value=await d("mai-ko/restart")}async function G(){e.value=await d("mai-ko/reconnect")}async function J(){e.value=await d("mai-ko/prepare")}async function L(){e.value=await d("mai-ko/docker-start")}async function O(){e.value=await d("mai-ko/docker-stop")}async function Q(){e.value=await d("mai-ko/docker-restart")}function X(){var p;(p=e.value)!=null&&p.webui.url&&window.open(e.value.webui.url,"_blank","noopener,noreferrer")}return re(f),(p,o)=>{const i=v("k-button"),Y=v("k-card"),Z=v("k-layout");return r(),F(Z,null,{default:a(()=>[s(Y,null,{header:a(()=>[...o[0]||(o[0]=[k("span",null,"mai.ko 托管状态",-1)])]),default:a(()=>{var b,m,_,w,g,y,C,E,D,R,S,x,T,B,I,N,W,U,V,H,K,M,$,h,j;return[k("p",null,"进程:"+t(((b=e.value)==null?void 0:b.process.state)||"unknown"),1),k("p",null,"准备:"+t(((m=e.value)==null?void 0:m.prepare.state)||"unknown")+" / "+t(((_=e.value)==null?void 0:_.prepare.root)||"-"),1),k("p",null,"Docker:"+t(((w=e.value)==null?void 0:w.docker.state)||"unknown")+" / "+t(((g=e.value)==null?void 0:g.docker.containerName)||"-"),1),k("p",null,"Bridge:"+t(((y=e.value)==null?void 0:y.transport.state)||"unknown"),1),(C=e.value)!=null&&C.bridge?(r(),n("p",ae,"中转:Koishi 收 "+t(e.value.bridge.koishiReceived)+" / 发往 mai.ko "+t(e.value.bridge.maimSent)+" / 收到 mai.ko "+t(e.value.bridge.maimReceived)+" / 回发 Koishi "+t(e.value.bridge.koishiSent),1)):u("v-if",true),(E=e.value)!=null&&E.bridge?(r(),n("p",ne,"中转异常:路由失败 "+t(e.value.bridge.routeMissed)+" / 发送失败 "+t(e.value.bridge.sendFailed),1)):u("v-if",true),(D=e.value)!=null&&D.bridge?(r(),n("p",ue,"触发限制:群聊跳过 "+t(e.value.bridge.groupTriggerSkipped)+" / 私信跳过 "+t(e.value.bridge.directTriggerSkipped),1)):u("v-if",true),k("p",null,"WebUI:"+t(P.value),1),(S=(R=e.value)==null?void 0:R.webui.token)!=null&&S.value?(r(),n("p",le,[o[1]||(o[1]=l("WebUI Token:",-1)),k("code",null,t(e.value.webui.token.value),1)])):(T=(x=e.value)==null?void 0:x.webui.token)!=null&&T.lastError?(r(),n("p",ie,"WebUI Token:"+t(e.value.webui.token.lastError),1)):u("v-if",true),k("p",null,"PID:"+t(((B=e.value)==null?void 0:B.process.pid)||"-"),1),(I=e.value)!=null&&I.process.blockedReason?(r(),n("p",se,"阻塞原因:"+t(e.value.process.blockedReason),1)):u("v-if",true),(N=e.value)!=null&&N.prepare.blockedReason?(r(),n("p",de,"准备阻塞:"+t(e.value.prepare.blockedReason),1)):u("v-if",true),(W=e.value)!=null&&W.prepare.lastError?(r(),n("p",ke,"准备错误:"+t(e.value.prepare.lastError),1)):u("v-if",true),(U=e.value)!=null&&U.docker.lastError?(r(),n("p",pe,"Docker 错误:"+t(e.value.docker.lastError),1)):u("v-if",true),(V=e.value)!=null&&V.transport.lastError?(r(),n("p",ve,"Bridge 错误:"+t(e.value.transport.lastError),1)):u("v-if",true),(K=(H=e.value)==null?void 0:H.bridge)!=null&&K.lastError?(r(),n("p",ce,"中转错误:"+t(e.value.bridge.lastError),1)):u("v-if",true),s(i,{onClick:f},{default:a(()=>[...o[2]||(o[2]=[l("刷新",-1)])]),_:1}),s(i,{onClick:q},{default:a(()=>[...o[3]||(o[3]=[l("启动",-1)])]),_:1}),s(i,{onClick:z},{default:a(()=>[...o[4]||(o[4]=[l("停止",-1)])]),_:1}),s(i,{onClick:A},{default:a(()=>[...o[5]||(o[5]=[l("重启",-1)])]),_:1}),s(i,{onClick:G},{default:a(()=>[...o[6]||(o[6]=[l("重连",-1)])]),_:1}),s(i,{onClick:J},{default:a(()=>[...o[7]||(o[7]=[l("准备",-1)])]),_:1}),s(i,{onClick:L},{default:a(()=>[...o[8]||(o[8]=[l("Docker 启动",-1)])]),_:1}),s(i,{onClick:O},{default:a(()=>[...o[9]||(o[9]=[l("Docker 停止",-1)])]),_:1}),s(i,{onClick:Q},{default:a(()=>[...o[10]||(o[10]=[l("Docker 重启",-1)])]),_:1}),(M=e.value)!=null&&M.webui.enabled&&(($=e.value)!=null&&$.webui.url)?(r(),F(i,{key:11,onClick:X},{default:a(()=>[...o[11]||(o[11]=[l("打开 WebUI",-1)])]),_:1})):u("v-if",true),(h=e.value)!=null&&h.logsHint?(r(),n("p",fe,t(e.value.logsHint),1)):u("v-if",true),(j=e.value)!=null&&j.logs.length?(r(),n("pre",be,t(e.value.logs.slice(-20).join(`
2
+ `)),1)):u("v-if",true)]}),_:1})]),_:1})}}}),ge=c=>{c.page({name:"mai.ko",path:"/mai-ko",authority:3,component:me})};export{ge as default};
@@ -204,7 +204,8 @@ async function sessionToMaimMessage(session, route, apiKey, options = {}) {
204
204
  koishi_reply_to_message_id: replyContext?.targetMessageId,
205
205
  koishi_reply_context_count: replyContext?.contextCount,
206
206
  koishi_is_direct: !!session.isDirect,
207
- koishi_group_trigger_forced: forceMention,
207
+ koishi_group_trigger_forced: !session.isDirect && forceMention,
208
+ koishi_direct_trigger_forced: !!session.isDirect && forceMention,
208
209
  at_bot: mentioned,
209
210
  is_mentioned: mentioned,
210
211
  },
@@ -20,3 +20,11 @@ export declare class GroupMessageTrigger {
20
20
  clear(): void;
21
21
  private createKey;
22
22
  }
23
+ export declare class DirectMessageTrigger {
24
+ private threshold;
25
+ private pending;
26
+ constructor(threshold: number);
27
+ test(session: Session, route: KoishiRoute): GroupTriggerResult;
28
+ clear(): void;
29
+ private createKey;
30
+ }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GroupMessageTrigger = void 0;
3
+ exports.DirectMessageTrigger = exports.GroupMessageTrigger = void 0;
4
4
  class GroupMessageTrigger {
5
5
  threshold;
6
6
  pending = new Map();
@@ -35,3 +35,39 @@ class GroupMessageTrigger {
35
35
  }
36
36
  }
37
37
  exports.GroupMessageTrigger = GroupMessageTrigger;
38
+ class DirectMessageTrigger {
39
+ threshold;
40
+ pending = new Map();
41
+ constructor(threshold) {
42
+ this.threshold = threshold;
43
+ }
44
+ test(session, route) {
45
+ const threshold = Math.max(1, Math.floor(this.threshold || 1));
46
+ const entry = { session, route };
47
+ if (!session.isDirect || !route.userId) {
48
+ return { shouldForward: true, count: 1, threshold, entries: [entry] };
49
+ }
50
+ if (threshold <= 1) {
51
+ return { shouldForward: true, count: 1, threshold, entries: [entry], forceMention: true };
52
+ }
53
+ const key = this.createKey(route);
54
+ const entries = [...this.pending.get(key) || [], entry];
55
+ if (entries.length >= threshold) {
56
+ this.pending.delete(key);
57
+ return { shouldForward: true, count: entries.length, threshold, key, entries, forceMention: true };
58
+ }
59
+ this.pending.set(key, entries);
60
+ return { shouldForward: false, count: entries.length, threshold, key, entries: [] };
61
+ }
62
+ clear() {
63
+ this.pending.clear();
64
+ }
65
+ createKey(route) {
66
+ return [
67
+ route.platform,
68
+ route.botSelfId,
69
+ route.userId,
70
+ ].join(':');
71
+ }
72
+ }
73
+ exports.DirectMessageTrigger = DirectMessageTrigger;
@@ -16,6 +16,7 @@ function renderStatus(status) {
16
16
  `重连次数: ${status.transport.reconnectAttempts}`,
17
17
  `中转: Koishi收=${status.bridge.koishiReceived} / 发往mai.ko=${status.bridge.maimSent} / 收到mai.ko=${status.bridge.maimReceived} / 回发Koishi=${status.bridge.koishiSent}`,
18
18
  `中转异常: 路由失败=${status.bridge.routeMissed} / 发送失败=${status.bridge.sendFailed}`,
19
+ `触发限制: 群聊跳过=${status.bridge.groupTriggerSkipped} / 私信跳过=${status.bridge.directTriggerSkipped}`,
19
20
  `启动时间: ${formatTime(status.process.startedAt)}`,
20
21
  ];
21
22
  if (status.process.blockedReason)
package/out/config.d.ts CHANGED
@@ -24,6 +24,7 @@ export interface Config {
24
24
  messageMode: MessageMode;
25
25
  commandPrefix: string;
26
26
  groupMessageTriggerCount: number;
27
+ directMessageTriggerCount: number;
27
28
  imageDownloadEnabled: boolean;
28
29
  imageDownloadTimeout: number;
29
30
  imageDownloadMaxBytes: number;
package/out/config.js CHANGED
@@ -43,7 +43,8 @@ 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
+ directMessageTriggerCount: koishi_1.Schema.number().min(1).default(1).description('同一私聊累计达到多少条消息后批量转发并强制触发 mai.ko 思考;1 表示每条私信都转发。'),
47
48
  imageDownloadEnabled: koishi_1.Schema.boolean().default(true).description('转发图片消息前由 Koishi 下载图片并转为 mai.ko 可识别的 base64 图片段。'),
48
49
  imageDownloadTimeout: koishi_1.Schema.number().min(1000).default(10000).description('下载单张入站图片的超时时间,单位毫秒。'),
49
50
  imageDownloadMaxBytes: koishi_1.Schema.number().min(1024).default(10 * 1024 * 1024).description('允许转发给 mai.ko 的单张图片最大字节数。'),
package/out/service.d.ts CHANGED
@@ -11,6 +11,7 @@ export declare class MaibotService extends Service {
11
11
  private process;
12
12
  private externalLogs;
13
13
  private groupTrigger;
14
+ private directTrigger;
14
15
  private history;
15
16
  private routes;
16
17
  private transport;
package/out/service.js CHANGED
@@ -23,6 +23,7 @@ class MaibotService extends koishi_1.Service {
23
23
  process;
24
24
  externalLogs;
25
25
  groupTrigger;
26
+ directTrigger;
26
27
  history;
27
28
  routes;
28
29
  transport;
@@ -46,6 +47,7 @@ class MaibotService extends koishi_1.Service {
46
47
  routeMissed: 0,
47
48
  sendFailed: 0,
48
49
  groupTriggerSkipped: 0,
50
+ directTriggerSkipped: 0,
49
51
  };
50
52
  constructor(ctx, pluginConfig) {
51
53
  super(ctx, 'maimai');
@@ -57,6 +59,7 @@ class MaibotService extends koishi_1.Service {
57
59
  this.process = new process_1.MaibotProcessManager(ctx, pluginConfig, this.apiKey);
58
60
  this.externalLogs = new external_logs_1.ExternalLogForwarder(ctx, pluginConfig, [this.apiKey, pluginConfig.apiKey]);
59
61
  this.groupTrigger = new group_trigger_1.GroupMessageTrigger(pluginConfig.groupMessageTriggerCount);
62
+ this.directTrigger = new group_trigger_1.DirectMessageTrigger(pluginConfig.directMessageTriggerCount);
60
63
  this.history = new history_1.MessageHistory(pluginConfig.routeTtl);
61
64
  this.routes = new routes_1.RouteRegistry(pluginConfig.routeTtl);
62
65
  this.transport = new transport_1.MaimTransport(ctx, pluginConfig, this.apiKey, {
@@ -226,14 +229,24 @@ class MaibotService extends koishi_1.Service {
226
229
  this.logMessageDetail(`koishi message received: ${(0, logging_1.describeSession)(session)}`);
227
230
  try {
228
231
  const route = this.routes.remember(session);
229
- const trigger = this.groupTrigger.test(session, route);
232
+ const triggerKind = session.isDirect ? 'direct' : 'group';
233
+ const trigger = session.isDirect
234
+ ? this.directTrigger.test(session, route)
235
+ : this.groupTrigger.test(session, route);
230
236
  if (!trigger.shouldForward) {
231
237
  this.history.rememberSession(session, route);
232
- this.bridge.groupTriggerSkipped += 1;
233
- this.bridge.lastGroupTriggerCount = trigger.count;
234
- this.bridge.lastGroupTriggerThreshold = trigger.threshold;
238
+ if (triggerKind === 'direct') {
239
+ this.bridge.directTriggerSkipped += 1;
240
+ this.bridge.lastDirectTriggerCount = trigger.count;
241
+ this.bridge.lastDirectTriggerThreshold = trigger.threshold;
242
+ }
243
+ else {
244
+ this.bridge.groupTriggerSkipped += 1;
245
+ this.bridge.lastGroupTriggerCount = trigger.count;
246
+ this.bridge.lastGroupTriggerThreshold = trigger.threshold;
247
+ }
235
248
  this.bridge.lastRouteId = route.routeId;
236
- this.logMessageDetail(`koishi -> maimai gated: route=${route.routeId} count=${trigger.count}/${trigger.threshold} ${(0, logging_1.describeSession)(session)}`);
249
+ this.logMessageDetail(`koishi -> maimai gated: kind=${triggerKind} route=${route.routeId} count=${trigger.count}/${trigger.threshold} ${(0, logging_1.describeSession)(session)}`);
237
250
  if (this.pluginConfig.messageMode === 'exclusive')
238
251
  return '';
239
252
  return next();
@@ -251,7 +264,7 @@ class MaibotService extends koishi_1.Service {
251
264
  }
252
265
  const entries = trigger.entries.length ? trigger.entries : [{ session, route }];
253
266
  if (trigger.forceMention) {
254
- this.logMessageSummary(`group trigger reached: route=${route.routeId} count=${trigger.count}/${trigger.threshold} flushing=${entries.length}`);
267
+ this.logMessageSummary(`${triggerKind} trigger reached: route=${route.routeId} count=${trigger.count}/${trigger.threshold} flushing=${entries.length}`);
255
268
  }
256
269
  for (let index = 0; index < entries.length; index++) {
257
270
  const entry = entries[index];
@@ -283,13 +296,14 @@ class MaibotService extends koishi_1.Service {
283
296
  this.bridge.lastRouteId = route.routeId;
284
297
  this.bridge.lastMessageId = message.message_info.message_id;
285
298
  const replyLog = replyContext ? ` reply=${replyContext.targetMessageId} context=${replyContext.contextCount || 0}` : '';
286
- const forceLog = forceMention ? ' forced=group-trigger' : '';
299
+ const forceLog = forceMention ? ' forced=message-trigger' : '';
287
300
  this.logMessageDetail(`koishi -> maimai forwarded: route=${route.routeId}${replyLog}${forceLog} ${(0, logging_1.describeSegment)(message.message_segment)}`);
288
301
  }
289
302
  async dispose() {
290
303
  await this.shutdown();
291
304
  this.routes.clear();
292
305
  this.groupTrigger.clear();
306
+ this.directTrigger.clear();
293
307
  this.history.clear();
294
308
  }
295
309
  logMessageDetail(message) {
package/out/types.d.ts CHANGED
@@ -52,6 +52,7 @@ export interface RuntimeStatus {
52
52
  routeMissed: number;
53
53
  sendFailed: number;
54
54
  groupTriggerSkipped: number;
55
+ directTriggerSkipped: number;
55
56
  lastKoishiMessageAt?: number;
56
57
  lastMaimSendAt?: number;
57
58
  lastMaimMessageAt?: number;
@@ -60,6 +61,8 @@ export interface RuntimeStatus {
60
61
  lastMessageId?: string;
61
62
  lastGroupTriggerCount?: number;
62
63
  lastGroupTriggerThreshold?: number;
64
+ lastDirectTriggerCount?: number;
65
+ lastDirectTriggerThreshold?: number;
63
66
  lastError?: string;
64
67
  };
65
68
  webui: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-mai-bridge",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "将 maibot 作为 docker 托管的消息回复后端运行,使用 mai.ko 桥接消息收发。",
5
5
  "author": "Jelly10086 <wow40469600@gmail.com>",
6
6
  "contributors": [
@@ -228,7 +228,8 @@ export async function sessionToMaimMessage(
228
228
  koishi_reply_to_message_id: replyContext?.targetMessageId,
229
229
  koishi_reply_context_count: replyContext?.contextCount,
230
230
  koishi_is_direct: !!session.isDirect,
231
- koishi_group_trigger_forced: forceMention,
231
+ koishi_group_trigger_forced: !session.isDirect && forceMention,
232
+ koishi_direct_trigger_forced: !!session.isDirect && forceMention,
232
233
  at_bot: mentioned,
233
234
  is_mentioned: mentioned,
234
235
  },
@@ -51,3 +51,42 @@ export class GroupMessageTrigger {
51
51
  ].join(':')
52
52
  }
53
53
  }
54
+
55
+ export class DirectMessageTrigger {
56
+ private pending = new Map<string, GroupTriggerEntry[]>()
57
+
58
+ constructor(private threshold: number) {}
59
+
60
+ test(session: Session, route: KoishiRoute): GroupTriggerResult {
61
+ const threshold = Math.max(1, Math.floor(this.threshold || 1))
62
+ const entry = { session, route }
63
+ if (!session.isDirect || !route.userId) {
64
+ return { shouldForward: true, count: 1, threshold, entries: [entry] }
65
+ }
66
+ if (threshold <= 1) {
67
+ return { shouldForward: true, count: 1, threshold, entries: [entry], forceMention: true }
68
+ }
69
+
70
+ const key = this.createKey(route)
71
+ const entries = [...this.pending.get(key) || [], entry]
72
+ if (entries.length >= threshold) {
73
+ this.pending.delete(key)
74
+ return { shouldForward: true, count: entries.length, threshold, key, entries, forceMention: true }
75
+ }
76
+
77
+ this.pending.set(key, entries)
78
+ return { shouldForward: false, count: entries.length, threshold, key, entries: [] }
79
+ }
80
+
81
+ clear() {
82
+ this.pending.clear()
83
+ }
84
+
85
+ private createKey(route: KoishiRoute) {
86
+ return [
87
+ route.platform,
88
+ route.botSelfId,
89
+ route.userId,
90
+ ].join(':')
91
+ }
92
+ }
@@ -17,6 +17,7 @@ export function renderStatus(status: RuntimeStatus) {
17
17
  `重连次数: ${status.transport.reconnectAttempts}`,
18
18
  `中转: Koishi收=${status.bridge.koishiReceived} / 发往mai.ko=${status.bridge.maimSent} / 收到mai.ko=${status.bridge.maimReceived} / 回发Koishi=${status.bridge.koishiSent}`,
19
19
  `中转异常: 路由失败=${status.bridge.routeMissed} / 发送失败=${status.bridge.sendFailed}`,
20
+ `触发限制: 群聊跳过=${status.bridge.groupTriggerSkipped} / 私信跳过=${status.bridge.directTriggerSkipped}`,
20
21
  `启动时间: ${formatTime(status.process.startedAt)}`,
21
22
  ]
22
23
  if (status.process.blockedReason) lines.push(`阻塞原因: ${status.process.blockedReason}`)
package/src/config.ts CHANGED
@@ -25,6 +25,7 @@ export interface Config {
25
25
  messageMode: MessageMode
26
26
  commandPrefix: string
27
27
  groupMessageTriggerCount: number
28
+ directMessageTriggerCount: number
28
29
  imageDownloadEnabled: boolean
29
30
  imageDownloadTimeout: number
30
31
  imageDownloadMaxBytes: number
@@ -93,7 +94,8 @@ export const Config: Schema<Config> = Schema.intersect([
93
94
  Schema.const('command').description('命令:仅命中指令前缀时转发。'),
94
95
  ]).default('coexist').description('消息转发模式。'),
95
96
  commandPrefix: Schema.string().default('mai.ko').description('command 模式下触发 mai.ko 的文本前缀。'),
96
- groupMessageTriggerCount: Schema.number().min(1).default(1).description('同一群聊累计达到多少条消息后批量转发并强制触发 mai.ko 思考;1 表示每条群消息都转发。私聊不受影响。'),
97
+ groupMessageTriggerCount: Schema.number().min(1).default(1).description('同一群聊累计达到多少条消息后批量转发并强制触发 mai.ko 思考;1 表示每条群消息都转发。'),
98
+ directMessageTriggerCount: Schema.number().min(1).default(1).description('同一私聊累计达到多少条消息后批量转发并强制触发 mai.ko 思考;1 表示每条私信都转发。'),
97
99
  imageDownloadEnabled: Schema.boolean().default(true).description('转发图片消息前由 Koishi 下载图片并转为 mai.ko 可识别的 base64 图片段。'),
98
100
  imageDownloadTimeout: Schema.number().min(1000).default(10000).description('下载单张入站图片的超时时间,单位毫秒。'),
99
101
  imageDownloadMaxBytes: Schema.number().min(1024).default(10 * 1024 * 1024).description('允许转发给 mai.ko 的单张图片最大字节数。'),
package/src/service.ts CHANGED
@@ -3,7 +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
+ import { DirectMessageTrigger, GroupMessageTrigger } from './bridge/group-trigger'
7
7
  import { MessageHistory } from './bridge/history'
8
8
  import { MaibotPrepareManager } from './bridge/prepare'
9
9
  import { MaibotProcessManager, createApiKey } from './bridge/process'
@@ -21,6 +21,7 @@ export class MaibotService extends Service {
21
21
  private process: MaibotProcessManager
22
22
  private externalLogs: ExternalLogForwarder
23
23
  private groupTrigger: GroupMessageTrigger
24
+ private directTrigger: DirectMessageTrigger
24
25
  private history: MessageHistory
25
26
  private routes: RouteRegistry
26
27
  private transport: MaimTransport
@@ -44,6 +45,7 @@ export class MaibotService extends Service {
44
45
  routeMissed: 0,
45
46
  sendFailed: 0,
46
47
  groupTriggerSkipped: 0,
48
+ directTriggerSkipped: 0,
47
49
  }
48
50
 
49
51
  constructor(public ctx: Context, private pluginConfig: Config) {
@@ -54,6 +56,7 @@ export class MaibotService extends Service {
54
56
  this.process = new MaibotProcessManager(ctx, pluginConfig, this.apiKey)
55
57
  this.externalLogs = new ExternalLogForwarder(ctx, pluginConfig, [this.apiKey, pluginConfig.apiKey])
56
58
  this.groupTrigger = new GroupMessageTrigger(pluginConfig.groupMessageTriggerCount)
59
+ this.directTrigger = new DirectMessageTrigger(pluginConfig.directMessageTriggerCount)
57
60
  this.history = new MessageHistory(pluginConfig.routeTtl)
58
61
  this.routes = new RouteRegistry(pluginConfig.routeTtl)
59
62
  this.transport = new MaimTransport(ctx, pluginConfig, this.apiKey, {
@@ -232,14 +235,23 @@ export class MaibotService extends Service {
232
235
 
233
236
  try {
234
237
  const route = this.routes.remember(session)
235
- const trigger = this.groupTrigger.test(session, route)
238
+ const triggerKind = session.isDirect ? 'direct' : 'group'
239
+ const trigger = session.isDirect
240
+ ? this.directTrigger.test(session, route)
241
+ : this.groupTrigger.test(session, route)
236
242
  if (!trigger.shouldForward) {
237
243
  this.history.rememberSession(session, route)
238
- this.bridge.groupTriggerSkipped += 1
239
- this.bridge.lastGroupTriggerCount = trigger.count
240
- this.bridge.lastGroupTriggerThreshold = trigger.threshold
244
+ if (triggerKind === 'direct') {
245
+ this.bridge.directTriggerSkipped += 1
246
+ this.bridge.lastDirectTriggerCount = trigger.count
247
+ this.bridge.lastDirectTriggerThreshold = trigger.threshold
248
+ } else {
249
+ this.bridge.groupTriggerSkipped += 1
250
+ this.bridge.lastGroupTriggerCount = trigger.count
251
+ this.bridge.lastGroupTriggerThreshold = trigger.threshold
252
+ }
241
253
  this.bridge.lastRouteId = route.routeId
242
- this.logMessageDetail(`koishi -> maimai gated: route=${route.routeId} count=${trigger.count}/${trigger.threshold} ${describeSession(session)}`)
254
+ this.logMessageDetail(`koishi -> maimai gated: kind=${triggerKind} route=${route.routeId} count=${trigger.count}/${trigger.threshold} ${describeSession(session)}`)
243
255
  if (this.pluginConfig.messageMode === 'exclusive') return ''
244
256
  return next()
245
257
  }
@@ -256,7 +268,7 @@ export class MaibotService extends Service {
256
268
 
257
269
  const entries = trigger.entries.length ? trigger.entries : [{ session, route }]
258
270
  if (trigger.forceMention) {
259
- this.logMessageSummary(`group trigger reached: route=${route.routeId} count=${trigger.count}/${trigger.threshold} flushing=${entries.length}`)
271
+ this.logMessageSummary(`${triggerKind} trigger reached: route=${route.routeId} count=${trigger.count}/${trigger.threshold} flushing=${entries.length}`)
260
272
  }
261
273
 
262
274
  for (let index = 0; index < entries.length; index++) {
@@ -290,7 +302,7 @@ export class MaibotService extends Service {
290
302
  this.bridge.lastRouteId = route.routeId
291
303
  this.bridge.lastMessageId = message.message_info.message_id
292
304
  const replyLog = replyContext ? ` reply=${replyContext.targetMessageId} context=${replyContext.contextCount || 0}` : ''
293
- const forceLog = forceMention ? ' forced=group-trigger' : ''
305
+ const forceLog = forceMention ? ' forced=message-trigger' : ''
294
306
  this.logMessageDetail(`koishi -> maimai forwarded: route=${route.routeId}${replyLog}${forceLog} ${describeSegment(message.message_segment)}`)
295
307
  }
296
308
 
@@ -298,6 +310,7 @@ export class MaibotService extends Service {
298
310
  await this.shutdown()
299
311
  this.routes.clear()
300
312
  this.groupTrigger.clear()
313
+ this.directTrigger.clear()
301
314
  this.history.clear()
302
315
  }
303
316
 
package/src/types.ts CHANGED
@@ -54,6 +54,7 @@ export interface RuntimeStatus {
54
54
  routeMissed: number
55
55
  sendFailed: number
56
56
  groupTriggerSkipped: number
57
+ directTriggerSkipped: number
57
58
  lastKoishiMessageAt?: number
58
59
  lastMaimSendAt?: number
59
60
  lastMaimMessageAt?: number
@@ -62,6 +63,8 @@ export interface RuntimeStatus {
62
63
  lastMessageId?: string
63
64
  lastGroupTriggerCount?: number
64
65
  lastGroupTriggerThreshold?: number
66
+ lastDirectTriggerCount?: number
67
+ lastDirectTriggerThreshold?: number
65
68
  lastError?: string
66
69
  }
67
70
  webui: {