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 +23 -2
- package/lib/index.js +166 -48
- package/package.json +1 -1
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
|
-
*
|
|
222
|
+
* 生成 ChatImage CICode: [[CICode,url=<url>,name=<name>]]
|
|
211
223
|
*/
|
|
212
|
-
private
|
|
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
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
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(/&/g, '&')
|
|
267
|
+
.replace(/</g, '<')
|
|
268
|
+
.replace(/>/g, '>')
|
|
269
|
+
.replace(/"/g, '"')
|
|
270
|
+
.replace(/'/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
|
-
? [
|
|
633
|
-
:
|
|
634
|
-
|
|
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
|
-
|
|
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