koishi-plugin-mai-bridge 0.1.3 → 0.1.5

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,8 @@ webuiHost: 0.0.0.0
59
59
  webuiPort: 8002
60
60
  messageMode: coexist
61
61
  groupMessageTriggerCount: 1
62
+ directMessageTriggerCount: 1
63
+ commandResultMode: source
62
64
  ```
63
65
 
64
66
  说明:
@@ -68,7 +70,10 @@ groupMessageTriggerCount: 1
68
70
  - `apiKey` 可以留空。插件会生成并复用运行期密钥。
69
71
  - `dockerNetwork` 按你的 Koishi Docker 网络填写。Koishi 需要能通过 `apiHost` 访问 `maimai-ko`。
70
72
  - `webuiPublicUrl` 可填写反代或宿主机映射后的 WebUI 地址,用于 Koishi 控制台显示入口。
71
- - `groupMessageTriggerCount` 控制群聊累计多少条消息后批量转发并强制触发 maibot 思考。设为 `3` 时,同一群前两条先缓存,第 3 条会连同前两条一起转发;私聊不受影响。
73
+ - `groupMessageTriggerCount` 控制群聊累计多少条消息后批量转发并强制触发 maibot 思考。设为 `3` 时,同一群前两条先缓存,第 3 条会连同前两条一起转发。
74
+ - `directMessageTriggerCount` 控制私聊累计多少条消息后批量转发并强制触发 maibot 思考。默认 `1`,表示每条私信都会触发;设为 `3` 时,同一私聊前两条只缓存,第 3 条统一转发并触发回复。
75
+ - `commandResultMode` 控制聊天中执行 `mai.ko.*` 管理指令后的结果发送方式。`source` 发回原群聊/私聊,`admin` 发到管理员私聊,`silent` 不发送结果。
76
+ - `commandResultAdminUserId` 仅在 `commandResultMode: admin` 时使用。填写管理员 QQ 号;留空时发给指令调用者。
72
77
 
73
78
  ## 消息模式
74
79
 
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;
@@ -1,5 +1,6 @@
1
- import type { Context } from 'koishi';
1
+ import { type Context, type Fragment, type Session } from 'koishi';
2
2
  import type { Config } from '../config';
3
3
  import type { RuntimeStatus } from '../types';
4
4
  export declare function renderStatus(status: RuntimeStatus): string;
5
+ export declare function sendCommandResult(session: Session, config: Config, content: string): Promise<Fragment | void>;
5
6
  export declare function registerCommands(ctx: Context, config: Config): void;
@@ -1,7 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderStatus = renderStatus;
4
+ exports.sendCommandResult = sendCommandResult;
4
5
  exports.registerCommands = registerCommands;
6
+ const koishi_1 = require("koishi");
7
+ const logger = new koishi_1.Logger('mai.ko/commands');
5
8
  function formatTime(value) {
6
9
  return value ? new Date(value).toLocaleString() : '-';
7
10
  }
@@ -16,6 +19,7 @@ function renderStatus(status) {
16
19
  `重连次数: ${status.transport.reconnectAttempts}`,
17
20
  `中转: Koishi收=${status.bridge.koishiReceived} / 发往mai.ko=${status.bridge.maimSent} / 收到mai.ko=${status.bridge.maimReceived} / 回发Koishi=${status.bridge.koishiSent}`,
18
21
  `中转异常: 路由失败=${status.bridge.routeMissed} / 发送失败=${status.bridge.sendFailed}`,
22
+ `触发限制: 群聊跳过=${status.bridge.groupTriggerSkipped} / 私信跳过=${status.bridge.directTriggerSkipped}`,
19
23
  `启动时间: ${formatTime(status.process.startedAt)}`,
20
24
  ];
21
25
  if (status.process.blockedReason)
@@ -38,27 +42,53 @@ function renderStatus(status) {
38
42
  }
39
43
  return lines.join('\n');
40
44
  }
45
+ async function sendCommandResult(session, config, content) {
46
+ if (config.commandResultMode === 'silent')
47
+ return;
48
+ if (config.commandResultMode !== 'admin')
49
+ return content;
50
+ const userId = config.commandResultAdminUserId.trim() || session.userId;
51
+ if (!userId)
52
+ return 'mai.ko 指令结果发送失败:没有可用的管理员用户 ID。';
53
+ if (!session.bot?.sendPrivateMessage)
54
+ return 'mai.ko 指令结果发送失败:当前平台不支持私聊发送。';
55
+ try {
56
+ const guildId = config.commandResultAdminGuildId.trim() || undefined;
57
+ await session.bot.sendPrivateMessage(userId, content, guildId, { session });
58
+ if (config.commandResultNotifySource)
59
+ return 'mai.ko 指令结果已发送给管理员。';
60
+ }
61
+ catch (error) {
62
+ const message = error instanceof Error ? error.message : String(error);
63
+ logger.warn(`send command result failed: user=${userId} error=${message}`);
64
+ return `mai.ko 指令结果发送失败:${message}`;
65
+ }
66
+ }
67
+ async function handleCommandResult(session, config, status) {
68
+ const content = renderStatus(await status);
69
+ return session ? sendCommandResult(session, config, content) : content;
70
+ }
41
71
  function registerCommands(ctx, config) {
42
72
  const options = { authority: config.commandAuthority };
43
73
  ctx.command('mai.ko', '查看 mai.ko 托管状态', options)
44
74
  .alias('maibot')
45
- .action(() => renderStatus(ctx.maimai.getStatus()));
75
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.getStatus()));
46
76
  ctx.command('mai.ko.status', '查看 mai.ko 托管状态', options)
47
- .action(() => renderStatus(ctx.maimai.getStatus()));
77
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.getStatus()));
48
78
  ctx.command('mai.ko.start', '启动 mai.ko', options)
49
- .action(async () => renderStatus(await ctx.maimai.launch()));
79
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.launch()));
50
80
  ctx.command('mai.ko.stop', '停止 mai.ko', options)
51
- .action(async () => renderStatus(await ctx.maimai.shutdown()));
81
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.shutdown()));
52
82
  ctx.command('mai.ko.restart', '重启 mai.ko', options)
53
- .action(async () => renderStatus(await ctx.maimai.restart()));
83
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.restart()));
54
84
  ctx.command('mai.ko.reconnect', '重连 mai.ko Bridge', options)
55
- .action(async () => renderStatus(await ctx.maimai.reconnect()));
85
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.reconnect()));
56
86
  ctx.command('mai.ko.prepare', '准备 MaiBot 源码、补丁与 Docker 镜像', options)
57
- .action(async () => renderStatus(await ctx.maimai.prepare()));
87
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.prepare()));
58
88
  ctx.command('mai.ko.docker.start', '启动 maimai-ko Docker 容器', options)
59
- .action(async () => renderStatus(await ctx.maimai.dockerStart()));
89
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.dockerStart()));
60
90
  ctx.command('mai.ko.docker.stop', '停止 maimai-ko Docker 容器', options)
61
- .action(async () => renderStatus(await ctx.maimai.dockerStop()));
91
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.dockerStop()));
62
92
  ctx.command('mai.ko.docker.restart', '重建并重启 maimai-ko Docker 容器', options)
63
- .action(async () => renderStatus(await ctx.maimai.dockerRestart()));
93
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.dockerRestart()));
64
94
  }
package/out/config.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Schema } from 'koishi';
2
- import type { MessageLogLevel, MessageMode, ProcessMode } from './types';
2
+ import type { CommandResultMode, MessageLogLevel, MessageMode, ProcessMode } from './types';
3
3
  export interface Config {
4
4
  processMode: ProcessMode;
5
5
  maibotRoot: string;
@@ -24,11 +24,16 @@ 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;
30
31
  messageLogLevel: MessageLogLevel;
31
32
  commandAuthority: number;
33
+ commandResultMode: CommandResultMode;
34
+ commandResultAdminUserId: string;
35
+ commandResultAdminGuildId: string;
36
+ commandResultNotifySource: boolean;
32
37
  acceptMaibotAgreements: boolean;
33
38
  startupTimeout: number;
34
39
  shutdownTimeout: 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 的单张图片最大字节数。'),
@@ -53,6 +54,14 @@ exports.Config = koishi_1.Schema.intersect([
53
54
  koishi_1.Schema.const('detail').description('详细:输出每一步消息中转细节。'),
54
55
  ]).default('summary').description('mai.ko 消息中转日志详细程度。'),
55
56
  commandAuthority: koishi_1.Schema.number().min(0).max(5).default(3).description('管理指令需要的权限等级。'),
57
+ commandResultMode: koishi_1.Schema.union([
58
+ koishi_1.Schema.const('source').description('原会话:指令结果发送到触发指令的群聊或私聊。'),
59
+ koishi_1.Schema.const('admin').description('管理员私聊:指令结果发送到指定管理员;未填写管理员时发送给指令调用者。'),
60
+ koishi_1.Schema.const('silent').description('静默:执行指令但不发送结果。'),
61
+ ]).default('source').description('聊天中执行 mai.ko 管理指令后的结果发送方式。'),
62
+ commandResultAdminUserId: koishi_1.Schema.string().default('').description('管理员用户 ID。仅 commandResultMode=admin 时使用;留空时发送给指令调用者。'),
63
+ commandResultAdminGuildId: koishi_1.Schema.string().default('').description('发送管理员私聊时附带的群组/频道 ID。通常可留空。'),
64
+ commandResultNotifySource: koishi_1.Schema.boolean().default(false).description('结果发送给管理员后,是否在原会话发送一条简短提示。'),
56
65
  }).description('消息路由'),
57
66
  koishi_1.Schema.object({
58
67
  startupTimeout: koishi_1.Schema.number().min(1000).default(60000).description('等待 mai.ko API 可连接的超时时间,单位毫秒。'),
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
@@ -1,5 +1,6 @@
1
1
  import type { Awaitable, Context, Fragment, Session } from 'koishi';
2
2
  export type MessageMode = 'coexist' | 'exclusive' | 'command';
3
+ export type CommandResultMode = 'source' | 'admin' | 'silent';
3
4
  export type ProcessMode = 'docker' | 'managed' | 'external';
4
5
  export type ProcessState = 'idle' | 'starting' | 'running' | 'stopping' | 'stopped' | 'blocked' | 'error';
5
6
  export type TransportState = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error';
@@ -52,6 +53,7 @@ export interface RuntimeStatus {
52
53
  routeMissed: number;
53
54
  sendFailed: number;
54
55
  groupTriggerSkipped: number;
56
+ directTriggerSkipped: number;
55
57
  lastKoishiMessageAt?: number;
56
58
  lastMaimSendAt?: number;
57
59
  lastMaimMessageAt?: number;
@@ -60,6 +62,8 @@ export interface RuntimeStatus {
60
62
  lastMessageId?: string;
61
63
  lastGroupTriggerCount?: number;
62
64
  lastGroupTriggerThreshold?: number;
65
+ lastDirectTriggerCount?: number;
66
+ lastDirectTriggerThreshold?: number;
63
67
  lastError?: string;
64
68
  };
65
69
  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.5",
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
+ }
@@ -1,7 +1,9 @@
1
- import type { Context } from 'koishi'
1
+ import { Logger, type Awaitable, type Context, type Fragment, type Session } from 'koishi'
2
2
  import type { Config } from '../config'
3
3
  import type { RuntimeStatus } from '../types'
4
4
 
5
+ const logger = new Logger('mai.ko/commands')
6
+
5
7
  function formatTime(value?: number) {
6
8
  return value ? new Date(value).toLocaleString() : '-'
7
9
  }
@@ -17,6 +19,7 @@ export function renderStatus(status: RuntimeStatus) {
17
19
  `重连次数: ${status.transport.reconnectAttempts}`,
18
20
  `中转: Koishi收=${status.bridge.koishiReceived} / 发往mai.ko=${status.bridge.maimSent} / 收到mai.ko=${status.bridge.maimReceived} / 回发Koishi=${status.bridge.koishiSent}`,
19
21
  `中转异常: 路由失败=${status.bridge.routeMissed} / 发送失败=${status.bridge.sendFailed}`,
22
+ `触发限制: 群聊跳过=${status.bridge.groupTriggerSkipped} / 私信跳过=${status.bridge.directTriggerSkipped}`,
20
23
  `启动时间: ${formatTime(status.process.startedAt)}`,
21
24
  ]
22
25
  if (status.process.blockedReason) lines.push(`阻塞原因: ${status.process.blockedReason}`)
@@ -33,37 +36,61 @@ export function renderStatus(status: RuntimeStatus) {
33
36
  return lines.join('\n')
34
37
  }
35
38
 
39
+ export async function sendCommandResult(session: Session, config: Config, content: string): Promise<Fragment | void> {
40
+ if (config.commandResultMode === 'silent') return
41
+ if (config.commandResultMode !== 'admin') return content
42
+
43
+ const userId = config.commandResultAdminUserId.trim() || session.userId
44
+ if (!userId) return 'mai.ko 指令结果发送失败:没有可用的管理员用户 ID。'
45
+ if (!session.bot?.sendPrivateMessage) return 'mai.ko 指令结果发送失败:当前平台不支持私聊发送。'
46
+
47
+ try {
48
+ const guildId = config.commandResultAdminGuildId.trim() || undefined
49
+ await session.bot.sendPrivateMessage(userId, content, guildId, { session })
50
+ if (config.commandResultNotifySource) return 'mai.ko 指令结果已发送给管理员。'
51
+ } catch (error) {
52
+ const message = error instanceof Error ? error.message : String(error)
53
+ logger.warn(`send command result failed: user=${userId} error=${message}`)
54
+ return `mai.ko 指令结果发送失败:${message}`
55
+ }
56
+ }
57
+
58
+ async function handleCommandResult(session: Session | undefined, config: Config, status: Awaitable<RuntimeStatus>) {
59
+ const content = renderStatus(await status)
60
+ return session ? sendCommandResult(session, config, content) : content
61
+ }
62
+
36
63
  export function registerCommands(ctx: Context, config: Config) {
37
64
  const options = { authority: config.commandAuthority }
38
65
 
39
66
  ctx.command('mai.ko', '查看 mai.ko 托管状态', options)
40
67
  .alias('maibot')
41
- .action(() => renderStatus(ctx.maimai.getStatus()))
68
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.getStatus()))
42
69
 
43
70
  ctx.command('mai.ko.status', '查看 mai.ko 托管状态', options)
44
- .action(() => renderStatus(ctx.maimai.getStatus()))
71
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.getStatus()))
45
72
 
46
73
  ctx.command('mai.ko.start', '启动 mai.ko', options)
47
- .action(async () => renderStatus(await ctx.maimai.launch()))
74
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.launch()))
48
75
 
49
76
  ctx.command('mai.ko.stop', '停止 mai.ko', options)
50
- .action(async () => renderStatus(await ctx.maimai.shutdown()))
77
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.shutdown()))
51
78
 
52
79
  ctx.command('mai.ko.restart', '重启 mai.ko', options)
53
- .action(async () => renderStatus(await ctx.maimai.restart()))
80
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.restart()))
54
81
 
55
82
  ctx.command('mai.ko.reconnect', '重连 mai.ko Bridge', options)
56
- .action(async () => renderStatus(await ctx.maimai.reconnect()))
83
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.reconnect()))
57
84
 
58
85
  ctx.command('mai.ko.prepare', '准备 MaiBot 源码、补丁与 Docker 镜像', options)
59
- .action(async () => renderStatus(await ctx.maimai.prepare()))
86
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.prepare()))
60
87
 
61
88
  ctx.command('mai.ko.docker.start', '启动 maimai-ko Docker 容器', options)
62
- .action(async () => renderStatus(await ctx.maimai.dockerStart()))
89
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.dockerStart()))
63
90
 
64
91
  ctx.command('mai.ko.docker.stop', '停止 maimai-ko Docker 容器', options)
65
- .action(async () => renderStatus(await ctx.maimai.dockerStop()))
92
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.dockerStop()))
66
93
 
67
94
  ctx.command('mai.ko.docker.restart', '重建并重启 maimai-ko Docker 容器', options)
68
- .action(async () => renderStatus(await ctx.maimai.dockerRestart()))
95
+ .action((argv) => handleCommandResult(argv.session, config, ctx.maimai.dockerRestart()))
69
96
  }
package/src/config.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Schema } from 'koishi'
2
- import type { MessageLogLevel, MessageMode, ProcessMode } from './types'
2
+ import type { CommandResultMode, MessageLogLevel, MessageMode, ProcessMode } from './types'
3
3
 
4
4
  export interface Config {
5
5
  processMode: ProcessMode
@@ -25,11 +25,16 @@ 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
31
32
  messageLogLevel: MessageLogLevel
32
33
  commandAuthority: number
34
+ commandResultMode: CommandResultMode
35
+ commandResultAdminUserId: string
36
+ commandResultAdminGuildId: string
37
+ commandResultNotifySource: boolean
33
38
  acceptMaibotAgreements: boolean
34
39
  startupTimeout: number
35
40
  shutdownTimeout: number
@@ -93,7 +98,8 @@ export const Config: Schema<Config> = Schema.intersect([
93
98
  Schema.const('command').description('命令:仅命中指令前缀时转发。'),
94
99
  ]).default('coexist').description('消息转发模式。'),
95
100
  commandPrefix: Schema.string().default('mai.ko').description('command 模式下触发 mai.ko 的文本前缀。'),
96
- groupMessageTriggerCount: Schema.number().min(1).default(1).description('同一群聊累计达到多少条消息后批量转发并强制触发 mai.ko 思考;1 表示每条群消息都转发。私聊不受影响。'),
101
+ groupMessageTriggerCount: Schema.number().min(1).default(1).description('同一群聊累计达到多少条消息后批量转发并强制触发 mai.ko 思考;1 表示每条群消息都转发。'),
102
+ directMessageTriggerCount: Schema.number().min(1).default(1).description('同一私聊累计达到多少条消息后批量转发并强制触发 mai.ko 思考;1 表示每条私信都转发。'),
97
103
  imageDownloadEnabled: Schema.boolean().default(true).description('转发图片消息前由 Koishi 下载图片并转为 mai.ko 可识别的 base64 图片段。'),
98
104
  imageDownloadTimeout: Schema.number().min(1000).default(10000).description('下载单张入站图片的超时时间,单位毫秒。'),
99
105
  imageDownloadMaxBytes: Schema.number().min(1024).default(10 * 1024 * 1024).description('允许转发给 mai.ko 的单张图片最大字节数。'),
@@ -103,6 +109,14 @@ export const Config: Schema<Config> = Schema.intersect([
103
109
  Schema.const('detail').description('详细:输出每一步消息中转细节。'),
104
110
  ]).default('summary').description('mai.ko 消息中转日志详细程度。'),
105
111
  commandAuthority: Schema.number().min(0).max(5).default(3).description('管理指令需要的权限等级。'),
112
+ commandResultMode: Schema.union([
113
+ Schema.const('source').description('原会话:指令结果发送到触发指令的群聊或私聊。'),
114
+ Schema.const('admin').description('管理员私聊:指令结果发送到指定管理员;未填写管理员时发送给指令调用者。'),
115
+ Schema.const('silent').description('静默:执行指令但不发送结果。'),
116
+ ]).default('source').description('聊天中执行 mai.ko 管理指令后的结果发送方式。'),
117
+ commandResultAdminUserId: Schema.string().default('').description('管理员用户 ID。仅 commandResultMode=admin 时使用;留空时发送给指令调用者。'),
118
+ commandResultAdminGuildId: Schema.string().default('').description('发送管理员私聊时附带的群组/频道 ID。通常可留空。'),
119
+ commandResultNotifySource: Schema.boolean().default(false).description('结果发送给管理员后,是否在原会话发送一条简短提示。'),
106
120
  }).description('消息路由'),
107
121
  Schema.object({
108
122
  startupTimeout: Schema.number().min(1000).default(60000).description('等待 mai.ko API 可连接的超时时间,单位毫秒。'),
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
@@ -1,6 +1,7 @@
1
1
  import type { Awaitable, Context, Fragment, Session } from 'koishi'
2
2
 
3
3
  export type MessageMode = 'coexist' | 'exclusive' | 'command'
4
+ export type CommandResultMode = 'source' | 'admin' | 'silent'
4
5
  export type ProcessMode = 'docker' | 'managed' | 'external'
5
6
  export type ProcessState = 'idle' | 'starting' | 'running' | 'stopping' | 'stopped' | 'blocked' | 'error'
6
7
  export type TransportState = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error'
@@ -54,6 +55,7 @@ export interface RuntimeStatus {
54
55
  routeMissed: number
55
56
  sendFailed: number
56
57
  groupTriggerSkipped: number
58
+ directTriggerSkipped: number
57
59
  lastKoishiMessageAt?: number
58
60
  lastMaimSendAt?: number
59
61
  lastMaimMessageAt?: number
@@ -62,6 +64,8 @@ export interface RuntimeStatus {
62
64
  lastMessageId?: string
63
65
  lastGroupTriggerCount?: number
64
66
  lastGroupTriggerThreshold?: number
67
+ lastDirectTriggerCount?: number
68
+ lastDirectTriggerThreshold?: number
65
69
  lastError?: string
66
70
  }
67
71
  webui: {