koishi-plugin-mai-bridge 0.1.0 → 0.1.1

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.
@@ -193,6 +193,7 @@ async function sessionToMaimMessage(session, route, apiKey, options = {}) {
193
193
  additional_config: {
194
194
  koishi_route_id: route.routeId,
195
195
  koishi_self_id: session.selfId,
196
+ platform_io_account_id: session.selfId,
196
197
  koishi_platform: session.platform,
197
198
  koishi_channel_id: session.channelId || '',
198
199
  koishi_guild_id: session.guildId,
@@ -20,6 +20,9 @@ export declare class MaibotDockerManager {
20
20
  private buildEnv;
21
21
  private volumeMap;
22
22
  private ensureVolumeDirs;
23
+ private resolveDockerVolumes;
24
+ private resolveDockerHostPath;
25
+ private resolveDockerNetwork;
23
26
  private containerExists;
24
27
  private removeContainer;
25
28
  private docker;
@@ -106,12 +106,14 @@ class MaibotDockerManager {
106
106
  throw new Error(`blocked: ${agreements.reason}`);
107
107
  }
108
108
  this.ensureVolumeDirs();
109
- const args = this.createRunArgs(agreements.env);
109
+ const dockerNetwork = await this.resolveDockerNetwork();
110
+ const dockerVolumes = await this.resolveDockerVolumes();
111
+ const args = this.createRunArgs(agreements.env, dockerNetwork, dockerVolumes);
110
112
  this.pushLog(`creating Docker container: ${this.config.dockerContainerName}`);
111
113
  const result = await this.docker(args);
112
114
  (0, spawn_1.assertSuccessful)(result, 'docker create');
113
115
  }
114
- createRunArgs(agreementEnv) {
116
+ createRunArgs(agreementEnv, dockerNetwork = this.config.dockerNetwork, dockerVolumes = this.volumeMap()) {
115
117
  const args = [
116
118
  'create',
117
119
  '--name',
@@ -119,8 +121,8 @@ class MaibotDockerManager {
119
121
  '--restart',
120
122
  'unless-stopped',
121
123
  ];
122
- if (this.config.dockerNetwork) {
123
- args.push('--network', this.config.dockerNetwork);
124
+ if (dockerNetwork) {
125
+ args.push('--network', dockerNetwork);
124
126
  }
125
127
  if (this.config.dockerPublishedWebuiPort > 0) {
126
128
  args.push('-p', `${this.config.dockerPublishedWebuiPort}:${this.config.webuiPort}`);
@@ -129,7 +131,7 @@ class MaibotDockerManager {
129
131
  for (const [key, value] of Object.entries(env)) {
130
132
  args.push('-e', `${key}=${value}`);
131
133
  }
132
- for (const [source, target] of this.volumeMap()) {
134
+ for (const [source, target] of dockerVolumes) {
133
135
  args.push('-v', `${source}:${target}`);
134
136
  }
135
137
  args.push(this.config.dockerImageName);
@@ -167,8 +169,74 @@ class MaibotDockerManager {
167
169
  (0, fs_1.mkdirSync)(source, { recursive: true });
168
170
  }
169
171
  }
172
+ async resolveDockerVolumes() {
173
+ const volumes = [];
174
+ for (const [source, target] of this.volumeMap()) {
175
+ volumes.push([await this.resolveDockerHostPath(source), target]);
176
+ }
177
+ return volumes;
178
+ }
179
+ async resolveDockerHostPath(containerPath) {
180
+ const hostname = process.env.HOSTNAME;
181
+ if (!hostname)
182
+ return containerPath;
183
+ const result = await this.run(this.config.dockerCommand, [
184
+ 'container',
185
+ 'inspect',
186
+ hostname,
187
+ '--format',
188
+ '{{json .Mounts}}',
189
+ ], false);
190
+ if (result.code !== 0)
191
+ return containerPath;
192
+ try {
193
+ const mounts = JSON.parse(result.stdout);
194
+ const normalizedPath = (0, path_1.resolve)(containerPath);
195
+ const candidates = mounts
196
+ .filter((mount) => mount.Source && mount.Destination)
197
+ .map((mount) => ({
198
+ source: mount.Source,
199
+ destination: (0, path_1.resolve)(mount.Destination),
200
+ }))
201
+ .sort((a, b) => b.destination.length - a.destination.length);
202
+ for (const mount of candidates) {
203
+ const rest = (0, path_1.relative)(mount.destination, normalizedPath);
204
+ if (rest === '' || (rest && !rest.startsWith('..') && !(0, path_1.isAbsolute)(rest))) {
205
+ return (0, path_1.join)(mount.source, rest);
206
+ }
207
+ }
208
+ }
209
+ catch (error) {
210
+ this.logger.debug(error);
211
+ }
212
+ return containerPath;
213
+ }
214
+ async resolveDockerNetwork() {
215
+ if (this.config.dockerNetwork)
216
+ return this.config.dockerNetwork;
217
+ const hostname = process.env.HOSTNAME;
218
+ if (!hostname)
219
+ return '';
220
+ const result = await this.run(this.config.dockerCommand, [
221
+ 'container',
222
+ 'inspect',
223
+ hostname,
224
+ '--format',
225
+ '{{range $name, $_ := .NetworkSettings.Networks}}{{println $name}}{{end}}',
226
+ ], false);
227
+ if (result.code !== 0)
228
+ return '';
229
+ const network = result.stdout
230
+ .split(/\r?\n/)
231
+ .map((line) => line.trim())
232
+ .find((line) => line && !['bridge', 'host', 'none'].includes(line));
233
+ if (!network)
234
+ return '';
235
+ this.pushLog(`using Docker network from Koishi container: ${network}`);
236
+ return network;
237
+ }
170
238
  async containerExists() {
171
- const result = await this.run(this.config.dockerCommand, ['inspect', this.config.dockerContainerName], false);
239
+ const result = await this.run(this.config.dockerCommand, ['container', 'inspect', this.config.dockerContainerName], false);
172
240
  return result.code === 0;
173
241
  }
174
242
  async removeContainer(force = false) {
package/out/config.js CHANGED
@@ -19,7 +19,7 @@ exports.Config = koishi_1.Schema.intersect([
19
19
  pythonCommand: koishi_1.Schema.string().default('python3').description('用于启动 mai.ko 的 Python 命令。'),
20
20
  entryScript: koishi_1.Schema.string().default('bot.py').description('相对于 mai.ko 根目录的启动脚本。'),
21
21
  autoStart: koishi_1.Schema.boolean().default(true).description('Koishi ready 后自动启动 mai.ko。'),
22
- acceptMaibotAgreements: koishi_1.Schema.boolean().default(false).description('是否由插件注入 EULA/Privacy 确认哈希。默认关闭。'),
22
+ acceptMaibotAgreements: koishi_1.Schema.boolean().default(false).description('确认同意 MaiBot 的 EULA/Privacy'),
23
23
  }).description('mai.ko 进程'),
24
24
  koishi_1.Schema.object({
25
25
  apiHost: koishi_1.Schema.string().default('maimai-ko').description('Koishi 连接 maim_message 新版 API Server 的地址。Docker 模式默认使用容器名。'),
package/out/console.js CHANGED
@@ -5,7 +5,7 @@ const path_1 = require("path");
5
5
  function registerConsole(ctx, config) {
6
6
  if (!config.enableConsole)
7
7
  return;
8
- ctx.inject(['console'], (ctx) => {
8
+ ctx.inject(['console', 'maimai'], (ctx) => {
9
9
  const options = { authority: config.commandAuthority };
10
10
  ctx.console.addListener('mai-ko/status', () => ctx.maimai.getStatus(), options);
11
11
  ctx.console.addListener('mai-ko/start', () => ctx.maimai.launch(), options);
package/out/service.js CHANGED
@@ -503,7 +503,10 @@ class MaibotService extends koishi_1.Service {
503
503
  if (host === '::')
504
504
  host = '::1';
505
505
  const normalizedHost = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
506
- return `http://${normalizedHost}:${this.pluginConfig.webuiPort}`;
506
+ const port = this.pluginConfig.processMode === 'docker' && this.pluginConfig.dockerPublishedWebuiPort > 0
507
+ ? this.pluginConfig.dockerPublishedWebuiPort
508
+ : this.pluginConfig.webuiPort;
509
+ return `http://${normalizedHost}:${port}`;
507
510
  }
508
511
  async handleMaimMessage(message) {
509
512
  this.bridge.maimReceived += 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-mai-bridge",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "将 maibot 作为 docker 托管的消息回复后端运行,使用 mai.ko 桥接消息收发。",
5
5
  "author": "Jelly10086 <wow40469600@gmail.com>",
6
6
  "contributors": [
@@ -551,3 +551,48 @@ index 23af394..41860ae 100644
551
551
  @staticmethod
552
552
  def _first_non_empty_string(*values: Any) -> str:
553
553
  """返回第一个非空字符串表示。"""
554
+ diff --git a/src/services/send_service.py b/src/services/send_service.py
555
+ index 6e8aea1..f63933a 100644
556
+ --- a/src/services/send_service.py
557
+ +++ b/src/services/send_service.py
558
+ @@ -288,6 +288,31 @@ def _inherit_platform_io_route_metadata(target_stream: BotChatSession) -> Dict[s
559
+ return inherited_metadata
560
+
561
+
562
+ +def _get_target_stream_bot_account(target_stream: BotChatSession) -> str:
563
+ + """获取目标会话对应的机器人账号。"""
564
+ +
565
+ + bot_account = get_bot_account(target_stream.platform)
566
+ + if bot_account:
567
+ + return bot_account
568
+ +
569
+ + context_message = target_stream.context.message if target_stream.context else None
570
+ + if context_message is None:
571
+ + return ""
572
+ +
573
+ + additional_config = context_message.message_info.additional_config
574
+ + if not isinstance(additional_config, dict):
575
+ + return ""
576
+ +
577
+ + for key in (*RouteKeyFactory.ACCOUNT_ID_KEYS, "koishi_self_id"):
578
+ + value = additional_config.get(key)
579
+ + if value is None:
580
+ + continue
581
+ + normalized_value = str(value).strip()
582
+ + if normalized_value:
583
+ + return normalized_value
584
+ + return ""
585
+ +
586
+ +
587
+ def _build_binary_component_from_base64(component_type: str, raw_data: str) -> StandardMessageComponents:
588
+ """根据 Base64 数据构造二进制消息组件。
589
+
590
+ @@ -538,7 +563,7 @@ def _build_outbound_session_message(
591
+ logger.error(f"[SendService] 未找到聊天流: {stream_id}")
592
+ return None
593
+
594
+ - bot_user_id = get_bot_account(target_stream.platform)
595
+ + bot_user_id = _get_target_stream_bot_account(target_stream)
596
+ if not bot_user_id:
597
+ logger.error(f"[SendService] 平台 {target_stream.platform} 未配置机器人账号,无法发送消息")
598
+ return None
@@ -216,6 +216,7 @@ export async function sessionToMaimMessage(
216
216
  additional_config: {
217
217
  koishi_route_id: route.routeId,
218
218
  koishi_self_id: session.selfId,
219
+ platform_io_account_id: session.selfId,
219
220
  koishi_platform: session.platform,
220
221
  koishi_channel_id: session.channelId || '',
221
222
  koishi_guild_id: session.guildId,
@@ -1,5 +1,5 @@
1
1
  import { existsSync, mkdirSync } from 'fs'
2
- import { join } from 'path'
2
+ import { isAbsolute, join, relative, resolve } from 'path'
3
3
  import { Logger } from 'koishi'
4
4
  import type { Config } from '../config'
5
5
  import type { DockerState, RuntimeStatus } from '../types'
@@ -8,6 +8,13 @@ import { assertSuccessful, runCommand, type CommandResult } from '../utils/spawn
8
8
  import { redactSecret } from '../utils/command'
9
9
  import { prepareAgreementEnv } from './agreements'
10
10
 
11
+ interface DockerMount {
12
+ Source?: string
13
+ Destination?: string
14
+ }
15
+
16
+ type DockerVolume = readonly [string, string]
17
+
11
18
  export class MaibotDockerManager {
12
19
  private logger = new Logger('mai.ko/docker')
13
20
  private state: DockerState = 'idle'
@@ -110,13 +117,19 @@ export class MaibotDockerManager {
110
117
  }
111
118
 
112
119
  this.ensureVolumeDirs()
113
- const args = this.createRunArgs(agreements.env)
120
+ const dockerNetwork = await this.resolveDockerNetwork()
121
+ const dockerVolumes = await this.resolveDockerVolumes()
122
+ const args = this.createRunArgs(agreements.env, dockerNetwork, dockerVolumes)
114
123
  this.pushLog(`creating Docker container: ${this.config.dockerContainerName}`)
115
124
  const result = await this.docker(args)
116
125
  assertSuccessful(result, 'docker create')
117
126
  }
118
127
 
119
- private createRunArgs(agreementEnv: Record<string, string>) {
128
+ private createRunArgs(
129
+ agreementEnv: Record<string, string>,
130
+ dockerNetwork = this.config.dockerNetwork,
131
+ dockerVolumes: readonly DockerVolume[] = this.volumeMap(),
132
+ ) {
120
133
  const args = [
121
134
  'create',
122
135
  '--name',
@@ -125,8 +138,8 @@ export class MaibotDockerManager {
125
138
  'unless-stopped',
126
139
  ]
127
140
 
128
- if (this.config.dockerNetwork) {
129
- args.push('--network', this.config.dockerNetwork)
141
+ if (dockerNetwork) {
142
+ args.push('--network', dockerNetwork)
130
143
  }
131
144
 
132
145
  if (this.config.dockerPublishedWebuiPort > 0) {
@@ -138,7 +151,7 @@ export class MaibotDockerManager {
138
151
  args.push('-e', `${key}=${value}`)
139
152
  }
140
153
 
141
- for (const [source, target] of this.volumeMap()) {
154
+ for (const [source, target] of dockerVolumes) {
142
155
  args.push('-v', `${source}:${target}`)
143
156
  }
144
157
 
@@ -181,8 +194,76 @@ export class MaibotDockerManager {
181
194
  }
182
195
  }
183
196
 
197
+ private async resolveDockerVolumes() {
198
+ const volumes: DockerVolume[] = []
199
+ for (const [source, target] of this.volumeMap()) {
200
+ volumes.push([await this.resolveDockerHostPath(source), target])
201
+ }
202
+ return volumes
203
+ }
204
+
205
+ private async resolveDockerHostPath(containerPath: string) {
206
+ const hostname = process.env.HOSTNAME
207
+ if (!hostname) return containerPath
208
+
209
+ const result = await this.run(this.config.dockerCommand, [
210
+ 'container',
211
+ 'inspect',
212
+ hostname,
213
+ '--format',
214
+ '{{json .Mounts}}',
215
+ ], false)
216
+ if (result.code !== 0) return containerPath
217
+
218
+ try {
219
+ const mounts = JSON.parse(result.stdout) as DockerMount[]
220
+ const normalizedPath = resolve(containerPath)
221
+ const candidates = mounts
222
+ .filter((mount) => mount.Source && mount.Destination)
223
+ .map((mount) => ({
224
+ source: mount.Source!,
225
+ destination: resolve(mount.Destination!),
226
+ }))
227
+ .sort((a, b) => b.destination.length - a.destination.length)
228
+
229
+ for (const mount of candidates) {
230
+ const rest = relative(mount.destination, normalizedPath)
231
+ if (rest === '' || (rest && !rest.startsWith('..') && !isAbsolute(rest))) {
232
+ return join(mount.source, rest)
233
+ }
234
+ }
235
+ } catch (error) {
236
+ this.logger.debug(error)
237
+ }
238
+
239
+ return containerPath
240
+ }
241
+
242
+ private async resolveDockerNetwork() {
243
+ if (this.config.dockerNetwork) return this.config.dockerNetwork
244
+ const hostname = process.env.HOSTNAME
245
+ if (!hostname) return ''
246
+
247
+ const result = await this.run(this.config.dockerCommand, [
248
+ 'container',
249
+ 'inspect',
250
+ hostname,
251
+ '--format',
252
+ '{{range $name, $_ := .NetworkSettings.Networks}}{{println $name}}{{end}}',
253
+ ], false)
254
+ if (result.code !== 0) return ''
255
+
256
+ const network = result.stdout
257
+ .split(/\r?\n/)
258
+ .map((line) => line.trim())
259
+ .find((line) => line && !['bridge', 'host', 'none'].includes(line))
260
+ if (!network) return ''
261
+ this.pushLog(`using Docker network from Koishi container: ${network}`)
262
+ return network
263
+ }
264
+
184
265
  private async containerExists() {
185
- const result = await this.run(this.config.dockerCommand, ['inspect', this.config.dockerContainerName], false)
266
+ const result = await this.run(this.config.dockerCommand, ['container', 'inspect', this.config.dockerContainerName], false)
186
267
  return result.code === 0
187
268
  }
188
269
 
package/src/config.ts CHANGED
@@ -68,7 +68,7 @@ export const Config: Schema<Config> = Schema.intersect([
68
68
  pythonCommand: Schema.string().default('python3').description('用于启动 mai.ko 的 Python 命令。'),
69
69
  entryScript: Schema.string().default('bot.py').description('相对于 mai.ko 根目录的启动脚本。'),
70
70
  autoStart: Schema.boolean().default(true).description('Koishi ready 后自动启动 mai.ko。'),
71
- acceptMaibotAgreements: Schema.boolean().default(false).description('是否由插件注入 EULA/Privacy 确认哈希。默认关闭。'),
71
+ acceptMaibotAgreements: Schema.boolean().default(false).description('确认同意 MaiBot 的 EULA/Privacy'),
72
72
  }).description('mai.ko 进程'),
73
73
  Schema.object({
74
74
  apiHost: Schema.string().default('maimai-ko').description('Koishi 连接 maim_message 新版 API Server 的地址。Docker 模式默认使用容器名。'),
package/src/console.ts CHANGED
@@ -20,7 +20,7 @@ declare module '@koishijs/plugin-console' {
20
20
 
21
21
  export function registerConsole(ctx: Context, config: Config) {
22
22
  if (!config.enableConsole) return
23
- ctx.inject(['console'], (ctx) => {
23
+ ctx.inject(['console', 'maimai'], (ctx) => {
24
24
  const options = { authority: config.commandAuthority }
25
25
  ctx.console.addListener('mai-ko/status', () => ctx.maimai.getStatus(), options)
26
26
  ctx.console.addListener('mai-ko/start', () => ctx.maimai.launch(), options)
package/src/service.ts CHANGED
@@ -511,7 +511,10 @@ export class MaibotService extends Service {
511
511
  if (host === '0.0.0.0') host = '127.0.0.1'
512
512
  if (host === '::') host = '::1'
513
513
  const normalizedHost = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host
514
- return `http://${normalizedHost}:${this.pluginConfig.webuiPort}`
514
+ const port = this.pluginConfig.processMode === 'docker' && this.pluginConfig.dockerPublishedWebuiPort > 0
515
+ ? this.pluginConfig.dockerPublishedWebuiPort
516
+ : this.pluginConfig.webuiPort
517
+ return `http://${normalizedHost}:${port}`
515
518
  }
516
519
 
517
520
  private async handleMaimMessage(message: MaimApiMessage) {