koishi-plugin-minecraft-adapter 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.d.ts CHANGED
@@ -178,6 +178,12 @@ export declare class MinecraftBot<C extends Context = Context> extends Bot<C, Mi
178
178
  */
179
179
  executeCommand(command: string): Promise<string>;
180
180
  }
181
+ export interface ChatImageConfig {
182
+ /** 是否启用 ChatImage CICode 生成(出站方向),默认关闭 */
183
+ enabled?: boolean;
184
+ /** 图片在聊天栏中的默认显示名称 */
185
+ defaultImageName?: string;
186
+ }
181
187
  export interface MinecraftAdapterConfig {
182
188
  bots: MinecraftBotConfig[];
183
189
  debug?: boolean;
@@ -187,6 +193,8 @@ export interface MinecraftAdapterConfig {
187
193
  maxReconnectAttempts?: number;
188
194
  /** 是否在消息前添加默认前缀 [鹊桥],默认不添加(由服务端配置) */
189
195
  useMessagePrefix?: boolean;
196
+ /** ChatImage 集成配置 */
197
+ chatImage?: ChatImageConfig;
190
198
  }
191
199
  export declare class MinecraftAdapter<C extends Context = Context> extends Adapter<C, MinecraftBot<C>> {
192
200
  static reusable: boolean;
@@ -201,15 +209,25 @@ export declare class MinecraftAdapter<C extends Context = Context> extends Adapt
201
209
  private reconnectInterval;
202
210
  private maxReconnectAttempts;
203
211
  private useMessagePrefix;
212
+ private chatImageEnabled;
213
+ private chatImageDefaultName;
204
214
  constructor(ctx: C, config: MinecraftAdapterConfig);
205
215
  /**
206
216
  * 生成唯一的请求 ID
207
217
  */
208
218
  private generateEcho;
219
+ private toTextComponent;
220
+ private extractRawText;
209
221
  /**
210
- * 将消息转换为 Minecraft 文本组件格式
222
+ * 生成 ChatImage CICode: [[CICode,url=<url>,name=<name>]]
211
223
  */
212
- private toTextComponent;
224
+ private buildCICode;
225
+ /**
226
+ * 解析出站消息中的 Koishi 元素标签 (<img src="..."/>, <image url="..."/>)
227
+ */
228
+ private parseOutboundMessage;
229
+ private extractAttr;
230
+ private decodeHtmlEntities;
213
231
  /**
214
232
  * 发送 WebSocket API 请求并等待响应
215
233
  */
@@ -228,8 +246,11 @@ export declare class MinecraftAdapter<C extends Context = Context> extends Adapt
228
246
  private createSession;
229
247
  /**
230
248
  * 解析消息文本为 Koishi 元素数组
249
+ * 入站方向始终解析 CICode 和裸图片 URL(不受 chatImage.enabled 控制)
231
250
  */
232
251
  private parseMessageToElements;
252
+ private extractCICodeParam;
253
+ private addTextElements;
233
254
  /**
234
255
  * 发送私聊消息 (send_private_msg)
235
256
  */
package/lib/index.js CHANGED
@@ -69,6 +69,8 @@ class MinecraftAdapter extends koishi_1.Adapter {
69
69
  reconnectInterval;
70
70
  maxReconnectAttempts;
71
71
  useMessagePrefix;
72
+ chatImageEnabled;
73
+ chatImageDefaultName;
72
74
  constructor(ctx, config) {
73
75
  super(ctx);
74
76
  try {
@@ -78,6 +80,8 @@ class MinecraftAdapter extends koishi_1.Adapter {
78
80
  this.reconnectInterval = config.reconnectInterval ?? 5000;
79
81
  this.maxReconnectAttempts = config.maxReconnectAttempts ?? 10;
80
82
  this.useMessagePrefix = config.useMessagePrefix ?? false;
83
+ this.chatImageEnabled = config.chatImage?.enabled ?? false;
84
+ this.chatImageDefaultName = config.chatImage?.defaultImageName ?? '图片';
81
85
  if (this.debug) {
82
86
  logger.info(`[DEBUG] MinecraftAdapter initialized with config:`, {
83
87
  debug: this.debug,
@@ -157,53 +161,115 @@ class MinecraftAdapter extends koishi_1.Adapter {
157
161
  generateEcho() {
158
162
  return `koishi_${Date.now()}_${++this.requestCounter}`;
159
163
  }
160
- /**
161
- * 将消息转换为 Minecraft 文本组件格式
162
- */
163
164
  toTextComponent(message) {
164
- const extractText = (item) => {
165
- if (item == null)
166
- return '';
167
- if (typeof item === 'string')
168
- return item;
169
- if (typeof item === 'number' || typeof item === 'boolean')
170
- return String(item);
171
- if (Array.isArray(item))
172
- return item.map(extractText).join('');
173
- if (typeof item === 'object') {
174
- if (item.attrs && typeof item.attrs.content === 'string')
175
- return item.attrs.content;
176
- if (typeof item.content === 'string')
177
- return item.content;
178
- if (typeof item.text === 'string')
179
- return item.text;
180
- if (item.children)
181
- return extractText(item.children);
165
+ const raw = this.extractRawText(message);
166
+ if (!raw)
167
+ return [{ text: '' }];
168
+ const segments = this.parseOutboundMessage(raw);
169
+ if (segments.length === 0)
170
+ return [{ text: '' }];
171
+ const fullText = segments.map(seg => {
172
+ if (seg.type === 'image') {
173
+ if (this.chatImageEnabled) {
174
+ return this.buildCICode(seg.url, seg.name);
175
+ }
176
+ return seg.url;
177
+ }
178
+ return seg.text;
179
+ }).join('');
180
+ return [{ text: fullText }];
181
+ }
182
+ extractRawText(message) {
183
+ if (message == null)
184
+ return '';
185
+ if (typeof message === 'string')
186
+ return message;
187
+ if (typeof message === 'number' || typeof message === 'boolean')
188
+ return String(message);
189
+ if (Array.isArray(message))
190
+ return message.map(item => this.extractRawText(item)).join('');
191
+ if (typeof message === 'object') {
192
+ if (message.attrs && typeof message.attrs.content === 'string')
193
+ return message.attrs.content;
194
+ if (typeof message.content === 'string')
195
+ return message.content;
196
+ if (typeof message.text === 'string')
197
+ return message.text;
198
+ if (message.children)
199
+ return this.extractRawText(message.children);
200
+ try {
201
+ if (typeof message.toString === 'function' && message.toString !== Object.prototype.toString) {
202
+ const s = message.toString();
203
+ if (typeof s === 'string' && s !== '[object Object]')
204
+ return s;
205
+ }
206
+ }
207
+ catch (e) {
208
+ // ignore
209
+ }
210
+ let acc = '';
211
+ for (const key in message) {
182
212
  try {
183
- if (typeof item.toString === 'function' && item.toString !== Object.prototype.toString) {
184
- const s = item.toString();
185
- if (typeof s === 'string' && s !== '[object Object]')
186
- return s;
187
- }
213
+ acc += this.extractRawText(message[key]);
188
214
  }
189
215
  catch (e) {
190
216
  // ignore
191
217
  }
192
- let acc = '';
193
- for (const key in item) {
194
- try {
195
- acc += extractText(item[key]);
196
- }
197
- catch (e) {
198
- // ignore
199
- }
200
- }
201
- return acc;
202
218
  }
203
- return String(item);
204
- };
205
- const text = extractText(message);
206
- return [{ text }];
219
+ return acc;
220
+ }
221
+ return String(message);
222
+ }
223
+ /**
224
+ * 生成 ChatImage CICode: [[CICode,url=<url>,name=<name>]]
225
+ */
226
+ buildCICode(url, name) {
227
+ const displayName = name || this.chatImageDefaultName;
228
+ return `[[CICode,url=${url},name=${displayName}]]`;
229
+ }
230
+ /**
231
+ * 解析出站消息中的 Koishi 元素标签 (<img src="..."/>, <image url="..."/>)
232
+ */
233
+ parseOutboundMessage(content) {
234
+ const segments = [];
235
+ const imgTagRegex = /<(?:img|image)\s+([^>]*?)\/?>(?:<\/(?:img|image)>)?/gi;
236
+ let lastIndex = 0;
237
+ let match;
238
+ while ((match = imgTagRegex.exec(content)) !== null) {
239
+ if (match.index > lastIndex) {
240
+ segments.push({ type: 'text', text: content.slice(lastIndex, match.index) });
241
+ }
242
+ const attrs = match[1];
243
+ const rawUrl = this.extractAttr(attrs, 'src') || this.extractAttr(attrs, 'url');
244
+ if (rawUrl) {
245
+ const url = this.decodeHtmlEntities(rawUrl);
246
+ const name = this.extractAttr(attrs, 'alt') || this.extractAttr(attrs, 'name') || this.extractAttr(attrs, 'summary');
247
+ segments.push({ type: 'image', url, name: name || undefined });
248
+ }
249
+ lastIndex = match.index + match[0].length;
250
+ }
251
+ if (lastIndex < content.length) {
252
+ segments.push({ type: 'text', text: content.slice(lastIndex) });
253
+ }
254
+ return segments;
255
+ }
256
+ // 从 HTML 属性字符串中提取指定属性值: name="val" | name='val' | name=val
257
+ extractAttr(attrs, name) {
258
+ const regex = new RegExp(`${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|(\\S+))`, 'i');
259
+ const match = regex.exec(attrs);
260
+ if (!match)
261
+ return null;
262
+ return match[1] ?? match[2] ?? match[3] ?? null;
263
+ }
264
+ decodeHtmlEntities(str) {
265
+ return str
266
+ .replace(/&amp;/g, '&')
267
+ .replace(/&lt;/g, '<')
268
+ .replace(/&gt;/g, '>')
269
+ .replace(/&quot;/g, '"')
270
+ .replace(/&#39;/g, "'")
271
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
272
+ .replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(parseInt(dec, 10)));
207
273
  }
208
274
  /**
209
275
  * 发送 WebSocket API 请求并等待响应
@@ -624,20 +690,68 @@ class MinecraftAdapter extends koishi_1.Adapter {
624
690
  }
625
691
  /**
626
692
  * 解析消息文本为 Koishi 元素数组
693
+ * 入站方向始终解析 CICode 和裸图片 URL(不受 chatImage.enabled 控制)
627
694
  */
628
695
  parseMessageToElements(messageText) {
629
696
  if (!messageText)
630
697
  return [];
698
+ const elements = [];
699
+ // CICode: [[CICode,url=<url>(,name=<name>)(,nsfw=<bool>)(,pre=<p>)(,suf=<s>)]]
700
+ // 裸图片 URL: https?://....(png|jpg|jpeg|gif|bmp|ico|jfif|webp)
701
+ const ciCodePattern = /\[\[CICode,([^\]]*)\]\]/g;
702
+ const imageUrlPattern = /https?:\/\/\S+\.(?:png|jpe?g|gif|bmp|ico|jfif|webp)(?:\?[^\s]*)?/gi;
703
+ const combinedPattern = new RegExp(`(${ciCodePattern.source})|(${imageUrlPattern.source})`, 'gi');
704
+ let lastIndex = 0;
705
+ let match;
706
+ while ((match = combinedPattern.exec(messageText)) !== null) {
707
+ if (match.index > lastIndex) {
708
+ const textBefore = messageText.slice(lastIndex, match.index);
709
+ this.addTextElements(elements, textBefore);
710
+ }
711
+ if (match[1]) {
712
+ const params = match[2];
713
+ const url = this.extractCICodeParam(params, 'url');
714
+ if (url) {
715
+ const el = { type: 'img', attrs: { src: url } };
716
+ const name = this.extractCICodeParam(params, 'name');
717
+ if (name)
718
+ el.attrs.alt = name;
719
+ el.toString = function () { return `[${this.attrs.alt || '图片'}]`; };
720
+ elements.push(el);
721
+ }
722
+ }
723
+ else {
724
+ const url = match[0];
725
+ const el = { type: 'img', attrs: { src: url } };
726
+ el.toString = function () { return `[图片]`; };
727
+ elements.push(el);
728
+ }
729
+ lastIndex = match.index + match[0].length;
730
+ }
731
+ if (lastIndex < messageText.length) {
732
+ const textAfter = messageText.slice(lastIndex);
733
+ this.addTextElements(elements, textAfter);
734
+ }
735
+ if (elements.length === 0) {
736
+ this.addTextElements(elements, messageText);
737
+ }
738
+ return elements;
739
+ }
740
+ // 提取 CICode 参数: "url=xxx,name=yyy" => { url: "xxx", name: "yyy" }
741
+ extractCICodeParam(params, key) {
742
+ const regex = new RegExp(`(?:^|,)${key}=([^,]*)`, 'i');
743
+ const match = regex.exec(params);
744
+ return match ? match[1] : null;
745
+ }
746
+ addTextElements(elements, text) {
631
747
  const tokens = this.tokenizeMode === 'none'
632
- ? [messageText]
633
- : messageText.split(/(\s+)/).filter((s) => s.length > 0);
634
- return tokens.map((token) => {
748
+ ? [text]
749
+ : text.split(/(\s+)/).filter((s) => s.length > 0);
750
+ for (const token of tokens) {
635
751
  const el = { type: 'text', attrs: { content: token } };
636
- el.toString = function () {
637
- return this.attrs?.content ?? '';
638
- };
639
- return el;
640
- });
752
+ el.toString = function () { return this.attrs?.content ?? ''; };
753
+ elements.push(el);
754
+ }
641
755
  }
642
756
  /**
643
757
  * 发送私聊消息 (send_private_msg)
@@ -856,6 +970,10 @@ exports.MinecraftAdapter = MinecraftAdapter;
856
970
  reconnectInterval: koishi_1.Schema.number().description('重连间隔时间(ms)').default(5000),
857
971
  maxReconnectAttempts: koishi_1.Schema.number().description('最大重连尝试次数').default(10),
858
972
  useMessagePrefix: koishi_1.Schema.boolean().description('是否在消息前添加默认前缀(由服务端配置)').default(false),
973
+ chatImage: koishi_1.Schema.object({
974
+ enabled: koishi_1.Schema.boolean().description('启用 ChatImage CICode 图片发送(需客户端安装 ChatImage Mod)').default(false),
975
+ defaultImageName: koishi_1.Schema.string().description('图片在聊天栏中的默认显示名称').default('图片'),
976
+ }).description('ChatImage 图片显示集成配置'),
859
977
  bots: koishi_1.Schema.array(koishi_1.Schema.object({
860
978
  selfId: koishi_1.Schema.string().description('机器人 ID(唯一标识)').required(),
861
979
  serverName: koishi_1.Schema.string().description('服务器名称(需与鹊桥 config.yml 中的 server_name 一致)'),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-minecraft-adapter",
3
3
  "description": "Minecraft adapter for Koishi based on QueQiao V2 protocol",
4
- "version": "1.0.1",
4
+ "version": "1.0.2",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [