koishi-plugin-chat-patch 1.2.0 → 2.0.0

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.js CHANGED
@@ -39,12 +39,17 @@ __export(src_exports, {
39
39
  usage: () => usage
40
40
  });
41
41
  module.exports = __toCommonJS(src_exports);
42
- var import_node_path2 = __toESM(require("node:path"));
42
+ var import_node_path3 = __toESM(require("node:path"));
43
43
 
44
44
  // src/utils.ts
45
+ var import_node_fs = require("node:fs");
46
+ var import_node_path = require("node:path");
47
+ var import_node_crypto = require("node:crypto");
48
+ var import_node_url = require("node:url");
45
49
  var Utils = class {
46
- constructor(config) {
50
+ constructor(config, ctx) {
47
51
  this.config = config;
52
+ this.ctx = ctx;
48
53
  }
49
54
  static {
50
55
  __name(this, "Utils");
@@ -95,14 +100,41 @@ var Utils = class {
95
100
  }
96
101
  return false;
97
102
  }
98
- // 清理对象中的base64内容
103
+ // 持久化 base64 图片并返回本地文件 URL
104
+ persistBase64Image(base64Data) {
105
+ if (!this.ctx || !base64Data.startsWith("data:image/")) return base64Data;
106
+ try {
107
+ const dir = (0, import_node_path.join)(this.ctx.baseDir, "data", "chat-patch", "persist-images");
108
+ if (!(0, import_node_fs.existsSync)(dir)) (0, import_node_fs.mkdirSync)(dir, { recursive: true });
109
+ const hash = (0, import_node_crypto.createHash)("md5").update(base64Data).digest("hex");
110
+ const ext = base64Data.split(";")[0].split("/")[1] || "png";
111
+ const filename = `${Date.now()}_${hash}.${ext}`;
112
+ const filePath = (0, import_node_path.join)(dir, filename);
113
+ const base64Content = base64Data.split(",")[1];
114
+ (0, import_node_fs.writeFileSync)(filePath, Buffer.from(base64Content, "base64"));
115
+ this.cleanupPersistImages(dir);
116
+ return (0, import_node_url.pathToFileURL)(filePath).href;
117
+ } catch (e) {
118
+ return base64Data;
119
+ }
120
+ }
121
+ cleanupPersistImages(dir) {
122
+ try {
123
+ const files = (0, import_node_fs.readdirSync)(dir).map((name2) => ({ name: name2, path: (0, import_node_path.join)(dir, name2), mtime: (0, import_node_fs.statSync)((0, import_node_path.join)(dir, name2)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
124
+ if (files.length > this.config.maxPersistImages) {
125
+ files.slice(this.config.maxPersistImages).forEach((f) => (0, import_node_fs.unlinkSync)(f.path));
126
+ }
127
+ } catch (e) {
128
+ }
129
+ }
130
+ // 清理对象中的base64内容,改为持久化存储
99
131
  cleanBase64Content(obj) {
100
132
  if (obj === null || obj === void 0) {
101
133
  return obj;
102
134
  }
103
135
  if (typeof obj === "string") {
104
136
  if (this.isBase64(obj)) {
105
- return "暂不支持记录base64内容";
137
+ return this.persistBase64Image(obj);
106
138
  }
107
139
  return obj;
108
140
  }
@@ -113,7 +145,7 @@ var Utils = class {
113
145
  const cleaned = {};
114
146
  for (const [key, value] of Object.entries(obj)) {
115
147
  if (typeof value === "string" && (key === "src" || key === "url" || key === "file" || key === "data" || key === "content") && this.isBase64(value)) {
116
- cleaned[key] = "暂不支持记录base64内容";
148
+ cleaned[key] = this.persistBase64Image(value);
117
149
  } else {
118
150
  cleaned[key] = this.cleanBase64Content(value);
119
151
  }
@@ -166,14 +198,31 @@ var MessageHandler = class {
166
198
  // 更新频道信息到JSON文件
167
199
  async updateChannelInfoToFile(session) {
168
200
  let guildName = session.channelId;
201
+ let directUserName = session.username || session.userId || "未知用户";
169
202
  const data = this.fileManager.readChatDataFromFile();
170
203
  try {
171
204
  if (session.guildId && session.bot.getGuild && typeof session.bot.getGuild === "function") {
172
205
  const guild = await session.bot.getGuild(session.guildId);
173
206
  guildName = guild?.name || session.channelId;
174
207
  }
175
- } catch (guildError) {
176
- this.logInfo("获取群组信息失败,使用频道ID作为备用:", guildError);
208
+ if (session.userId && session.isDirect && session.bot.getUser && typeof session.bot.getUser === "function") {
209
+ try {
210
+ const user = await session.bot.getUser(session.userId);
211
+ directUserName = user?.name || session.username || session.userId || "未知用户";
212
+ } catch (userError) {
213
+ this.logInfo("获取用户信息失败,使用备用名称:", userError);
214
+ }
215
+ }
216
+ if (session.guildId && !session.bot.getGuild && session.bot.getChannel && typeof session.bot.getChannel === "function") {
217
+ try {
218
+ const channel = await session.bot.getChannel(session.guildId);
219
+ guildName = channel?.name || session.channelId;
220
+ } catch (channelError) {
221
+ this.logInfo("获取频道信息失败,使用频道ID作为备用:", channelError);
222
+ }
223
+ }
224
+ } catch (error) {
225
+ this.logInfo("获取频道信息失败,使用频道ID作为备用:", error);
177
226
  guildName = session.channelId;
178
227
  }
179
228
  if (!data.channels[session.selfId]) {
@@ -181,7 +230,7 @@ var MessageHandler = class {
181
230
  }
182
231
  const channelInfo = {
183
232
  id: session.channelId,
184
- name: session.isDirect ? `私信 ${session.channelId}` : `${guildName} (${session.channelId})`,
233
+ name: session.isDirect ? `私聊(${session.username || session.userId || directUserName})` : `${guildName} (${session.channelId})`,
185
234
  type: session.type || 0,
186
235
  channelId: session.channelId,
187
236
  guildName,
@@ -286,8 +335,8 @@ var MessageHandler = class {
286
335
  await this.updateBotInfoToFile(session);
287
336
  const guildName = await this.updateChannelInfoToFile(session);
288
337
  const timestamp = Date.now();
289
- let content = "";
290
- if (session.event?.message?.elements) {
338
+ let content = session.content || "";
339
+ if (!content && session.event?.message?.elements) {
291
340
  content = this.utils.extractTextContent(session.event.message.elements).trim();
292
341
  }
293
342
  const messageInfo = {
@@ -353,13 +402,13 @@ var MessageHandler = class {
353
402
  };
354
403
 
355
404
  // src/file-manager.ts
356
- var import_node_path = __toESM(require("node:path"));
357
- var import_node_fs = __toESM(require("node:fs"));
405
+ var import_node_path2 = __toESM(require("node:path"));
406
+ var import_node_fs2 = __toESM(require("node:fs"));
358
407
  var FileManager = class {
359
408
  constructor(ctx, config) {
360
409
  this.ctx = ctx;
361
410
  this.config = config;
362
- this.dataFilePath = import_node_path.default.resolve(ctx.baseDir, "data", "chat-patch", "chat-data.json");
411
+ this.dataFilePath = import_node_path2.default.resolve(ctx.baseDir, "data", "chat-patch", "chat-data.json");
363
412
  this.logger = ctx.logger("chat-patch");
364
413
  this.utils = new Utils(config);
365
414
  }
@@ -372,16 +421,16 @@ var FileManager = class {
372
421
  utils;
373
422
  // 确保目录存在
374
423
  ensureDataDir() {
375
- const dir = import_node_path.default.dirname(this.dataFilePath);
376
- if (!import_node_fs.default.existsSync(dir)) {
377
- import_node_fs.default.mkdirSync(dir, { recursive: true });
424
+ const dir = import_node_path2.default.dirname(this.dataFilePath);
425
+ if (!import_node_fs2.default.existsSync(dir)) {
426
+ import_node_fs2.default.mkdirSync(dir, { recursive: true });
378
427
  }
379
428
  }
380
429
  // 从JSON文件读取数据
381
430
  readChatDataFromFile() {
382
431
  try {
383
- if (import_node_fs.default.existsSync(this.dataFilePath)) {
384
- const jsonData = import_node_fs.default.readFileSync(this.dataFilePath, "utf8");
432
+ if (import_node_fs2.default.existsSync(this.dataFilePath)) {
433
+ const jsonData = import_node_fs2.default.readFileSync(this.dataFilePath, "utf8");
385
434
  const data = JSON.parse(jsonData);
386
435
  return {
387
436
  bots: data.bots || {},
@@ -409,7 +458,7 @@ var FileManager = class {
409
458
  this.ensureDataDir();
410
459
  data.lastSaveTime = Date.now();
411
460
  const jsonData = JSON.stringify(data, null, 2);
412
- import_node_fs.default.writeFileSync(this.dataFilePath, jsonData, "utf8");
461
+ import_node_fs2.default.writeFileSync(this.dataFilePath, jsonData, "utf8");
413
462
  } catch (error) {
414
463
  this.logger.error("写入聊天数据失败:", error);
415
464
  }
@@ -497,7 +546,9 @@ var FileManager = class {
497
546
 
498
547
  // src/api-handlers.ts
499
548
  var import_koishi = require("koishi");
500
- var import_node_url = require("node:url");
549
+ var import_node_url2 = require("node:url");
550
+ var import_node_fs3 = require("node:fs");
551
+ var mime = __toESM(require("mime-types"));
501
552
  var ApiHandlers = class {
502
553
  constructor(ctx, config, fileManager, messageHandler) {
503
554
  this.ctx = ctx;
@@ -689,8 +740,8 @@ var ApiHandlers = class {
689
740
  messageId,
690
741
  content: messageContent,
691
742
  userId: data.selfId,
692
- botUsername: bot2?.user?.name || `Bot-${data.selfId}`,
693
- botAvatar: bot2?.user?.avatar,
743
+ username: bot2?.user?.name || `Bot-${data.selfId}`,
744
+ avatar: bot2?.user?.avatar,
694
745
  timestamp: Date.now(),
695
746
  guildName: "",
696
747
  // 这个信息在前端会补充
@@ -864,7 +915,6 @@ var ApiHandlers = class {
864
915
  keepTempImages: this.config.keepTempImages,
865
916
  loggerinfo: this.config.loggerinfo,
866
917
  blockedPlatforms: this.config.blockedPlatforms || [],
867
- chatContainerHeight: this.config.chatContainerHeight,
868
918
  clearIndexedDBOnStart: this.config.clearIndexedDBOnStart
869
919
  }
870
920
  };
@@ -889,7 +939,7 @@ var ApiHandlers = class {
889
939
  // 检查是否为文件 URL
890
940
  isFileUrl(url) {
891
941
  try {
892
- const parsedUrl = new import_node_url.URL(url);
942
+ const parsedUrl = new import_node_url2.URL(url);
893
943
  return parsedUrl.protocol === "file:";
894
944
  } catch {
895
945
  return false;
@@ -898,7 +948,7 @@ var ApiHandlers = class {
898
948
  // 创建文件 URL
899
949
  createFileUrl(filePath) {
900
950
  try {
901
- return (0, import_node_url.pathToFileURL)(filePath).href;
951
+ return (0, import_node_url2.pathToFileURL)(filePath).href;
902
952
  } catch (error) {
903
953
  this.logger.error("创建文件URL失败:", { filePath, error });
904
954
  return `file://${filePath}`;
@@ -907,15 +957,16 @@ var ApiHandlers = class {
907
957
  // 处理本地文件请求
908
958
  async handleLocalFileRequest(fileUrl) {
909
959
  try {
910
- const fileresponse = await this.ctx.http.file(fileUrl);
911
- const fileresponsebase64 = Buffer.from(fileresponse.data).toString("base64");
912
- let contentType = fileresponse.type;
913
- this.logInfo("成功读取本地文件:", { fileUrl, contentType });
960
+ const filePath = (0, import_node_url2.fileURLToPath)(fileUrl);
961
+ const buffer = (0, import_node_fs3.readFileSync)(filePath);
962
+ const base64 = buffer.toString("base64");
963
+ const contentType = mime.lookup(filePath) || "application/octet-stream";
964
+ this.logInfo("成功读取本地文件:", { fileUrl, filePath, contentType });
914
965
  return {
915
966
  success: true,
916
- base64: fileresponsebase64,
967
+ base64,
917
968
  contentType,
918
- dataUrl: `data:${contentType};base64,${fileresponsebase64}`
969
+ dataUrl: `data:${contentType};base64,${base64}`
919
970
  };
920
971
  } catch (error) {
921
972
  this.logger.error("读取本地文件失败:", { fileUrl, error: error.message });
@@ -998,6 +1049,7 @@ var Config = import_koishi2.Schema.intersect([
998
1049
  maxMessagesPerChannel: import_koishi2.Schema.number().default(500).description("每个群组最大保存消息数量").min(50).max(1500),
999
1050
  keepMessagesOnClear: import_koishi2.Schema.number().default(50).description("手动清理历史记录时保留的消息数量").min(0).max(1e3),
1000
1051
  keepTempImages: import_koishi2.Schema.number().default(50).description("发送消息保留的临时图片数量(最新的N张)").min(10).max(200),
1052
+ maxPersistImages: import_koishi2.Schema.number().default(100).description("持久化存储的机器人发送图片数量").min(10).max(500),
1001
1053
  blockedPlatforms: import_koishi2.Schema.array(import_koishi2.Schema.object({
1002
1054
  platformName: import_koishi2.Schema.string().description("平台名称或关键词"),
1003
1055
  exactMatch: import_koishi2.Schema.boolean().default(false).description("完全匹配?如果关闭,包含关键词即屏蔽").default(true)
@@ -1018,9 +1070,6 @@ var Config = import_koishi2.Schema.intersect([
1018
1070
  ]
1019
1071
  )
1020
1072
  }).description("基础设置"),
1021
- import_koishi2.Schema.object({
1022
- chatContainerHeight: import_koishi2.Schema.number().default(80).description("手机端使用的视口高度(防止文本输入框被挡住)").min(50).max(100)
1023
- }).description("进阶设置"),
1024
1073
  import_koishi2.Schema.object({
1025
1074
  clearIndexedDBOnStart: import_koishi2.Schema.boolean().default(true).description("启动时强制清空IndexedDB缓存(适用于紧急情况,防止浏览器卡死)"),
1026
1075
  loggerinfo: import_koishi2.Schema.boolean().default(false).description("日志调试模式").experimental()
@@ -1049,7 +1098,7 @@ async function apply(ctx, config) {
1049
1098
  const fileManager = new FileManager(ctx, config);
1050
1099
  const messageHandler = new MessageHandler(ctx, config, fileManager);
1051
1100
  const apiHandlers = new ApiHandlers(ctx, config, fileManager, messageHandler);
1052
- const utils = new Utils(config);
1101
+ const utils = new Utils(config, ctx);
1053
1102
  const initialData = fileManager.readChatDataFromFile();
1054
1103
  const cleanedData = fileManager.cleanExcessMessages(initialData);
1055
1104
  const originalCount = Object.values(initialData.messages).reduce((total, msgs) => total + msgs.length, 0);
@@ -1098,8 +1147,8 @@ async function apply(ctx, config) {
1098
1147
  });
1099
1148
  apiHandlers.registerApiHandlers();
1100
1149
  ctx.console.addEntry({
1101
- dev: import_node_path2.default.resolve(__dirname, "../client/index.ts"),
1102
- prod: import_node_path2.default.resolve(__dirname, "../dist")
1150
+ dev: import_node_path3.default.resolve(__dirname, "../client/index.ts"),
1151
+ prod: import_node_path3.default.resolve(__dirname, "../dist")
1103
1152
  });
1104
1153
  }
1105
1154
  __name(apply, "apply");
package/package.json CHANGED
@@ -1,40 +1,49 @@
1
- {
2
- "name": "koishi-plugin-chat-patch",
3
- "description": "[<ruby>chat-patch<rp>(</rp><rt>点我预览效果</rt><rp>)</rp></ruby>](https://i0.hdslb.com/bfs/openplatform/71074dfc9e5256fc3333d8bd8478bec1af874046.png) 视奸小插件((bushi( (低性能警告)。手机端适配。灵感来自 chat 插件。",
4
- "version": "1.2.0",
5
- "main": "lib/index.js",
6
- "typings": "lib/index.d.ts",
7
- "files": [
8
- "client",
9
- "dist",
10
- "lib",
11
- "src"
12
- ],
13
- "license": "MIT",
14
- "homepage": "https://github.com/shangxueink/koishi-shangxue-apps/",
15
- "bugs": {
16
- "url": "https://github.com/shangxueink/koishi-shangxue-apps/issues"
17
- },
18
- "keywords": [
19
- "koishi",
20
- "plugin",
21
- "chat",
22
- "聊天室",
23
- "console"
24
- ],
25
- "cordis": {
26
- "service": {
27
- "required": [
28
- "console"
29
- ]
30
- }
31
- },
32
- "peerDependencies": {
33
- "@koishijs/plugin-console": "^5.11.0",
34
- "koishi": "^4.16.0"
35
- },
36
- "devDependencies": {
37
- "@koishijs/client": "^5.11.0",
38
- "koishi": "^4.16.0"
39
- }
40
- }
1
+ {
2
+ "name": "koishi-plugin-chat-patch",
3
+ "description": "[<ruby>chat-patch<rp>(</rp><rt>点我预览效果</rt><rp>)</rp></ruby>](https://i0.hdslb.com/bfs/openplatform/71074dfc9e5256fc3333d8bd8478bec1af874046.png) 视奸小插件((bushi( (低性能警告)。手机端适配。灵感来自 chat 插件。",
4
+ "version": "2.0.0",
5
+ "main": "lib/index.js",
6
+ "typings": "lib/index.d.ts",
7
+ "files": [
8
+ "client",
9
+ "dist",
10
+ "lib",
11
+ "src"
12
+ ],
13
+ "license": "MIT",
14
+ "homepage": "https://github.com/koishi-shangxue-plugins/koishi-shangxue-apps/",
15
+ "bugs": {
16
+ "url": "https://github.com/koishi-shangxue-plugins/koishi-shangxue-apps/issues"
17
+ },
18
+ "keywords": [
19
+ "koishi",
20
+ "plugin",
21
+ "chat",
22
+ "聊天室",
23
+ "console"
24
+ ],
25
+ "koishi": {
26
+ "service": {
27
+ "required": [
28
+ "console"
29
+ ]
30
+ }
31
+ },
32
+ "peerDependencies": {
33
+ "@koishijs/plugin-console": "^5.11.0",
34
+ "koishi": "^4.16.0"
35
+ },
36
+ "devDependencies": {
37
+ "@koishijs/client": "^5.11.0",
38
+ "@types/mime-types": "^3.0.1",
39
+ "autoprefixer": "^10.4.23",
40
+ "koishi": "^4.16.0",
41
+ "postcss": "^8.5.6",
42
+ "tailwindcss": "^3.4.17"
43
+ },
44
+ "dependencies": {
45
+ "@element-plus/icons-vue": "^2.3.2",
46
+ "element-plus": "^2.13.0",
47
+ "mime-types": "^3.0.2"
48
+ }
49
+ }
package/readme.md CHANGED
@@ -1,25 +1,25 @@
1
- # koishi-plugin-chat-patch
2
-
3
- [![npm](https://img.shields.io/npm/v/koishi-plugin-chat-patch?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-chat-patch) [![npm downloads](https://img.shields.io/npm/dm/koishi-plugin-chat-patch)](https://www.npmjs.com/package/koishi-plugin-chat-patch)
4
-
5
- Koishi 控制台聊天插件,允许用户直接在控制台中管理机器人、频道和消息。
6
-
7
- 支持消息的收发、图片缓存、频道置顶等。
8
-
9
- ## ✨ 功能特性
10
-
11
- - **聊天界面**:在 Koishi 控制台内直接查看机器人的聊天记录。
12
- - **消息收发**:支持接收文本、图片等多种类型的消息。
13
- - **图片缓存**:通过 IndexedDB 缓存图片,优化加载速度并减少网络请求。
14
- - **手机端优化**:针对手机端进行适配,提供滑动返回等手势操作,并可配置聊天容器高度以防止输入框被遮挡。
15
- - **平台屏蔽**:可配置屏蔽特定平台的消息,避免不必要的干扰。
16
-
17
- ## 🛠️ 开发与贡献
18
-
19
- 欢迎 PR ~
20
-
21
- PR 方式请参考 -> https://github.com/shangxueink/koishi-shangxue-apps/tree/main?tab=readme-ov-file#%E5%A6%82%E4%BD%95%E5%9C%A8%E9%A1%B9%E7%9B%AE%E6%A8%A1%E6%9D%BF%E4%B8%AD%E5%BC%80%E5%8F%91%E6%AD%A4%E4%BB%93%E5%BA%93
22
-
23
- ## 许可证
24
-
25
- 本项目采用 [MIT 许可证](LICENSE) 开源。
1
+ # koishi-plugin-chat-patch
2
+
3
+ [![npm](https://img.shields.io/npm/v/koishi-plugin-chat-patch?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-chat-patch) [![npm downloads](https://img.shields.io/npm/dm/koishi-plugin-chat-patch)](https://www.npmjs.com/package/koishi-plugin-chat-patch)
4
+
5
+ Koishi 控制台聊天插件,允许用户直接在控制台中管理机器人、频道和消息。
6
+
7
+ 支持消息的收发、图片缓存、频道置顶等。
8
+
9
+ ## ✨ 功能特性
10
+
11
+ - **聊天界面**:在 Koishi 控制台内直接查看机器人的聊天记录。
12
+ - **消息收发**:支持接收文本、图片等多种类型的消息。
13
+ - **图片缓存**:通过 IndexedDB 缓存图片,优化加载速度并减少网络请求。
14
+ - **手机端优化**:针对手机端进行适配,提供滑动返回等手势操作,并可配置聊天容器高度以防止输入框被遮挡。
15
+ - **平台屏蔽**:可配置屏蔽特定平台的消息,避免不必要的干扰。
16
+
17
+ ## 🛠️ 开发与贡献
18
+
19
+ 欢迎 PR ~
20
+
21
+ PR 方式请参考 -> <https://github.com/koishi-shangxue-plugins/koishi-shangxue-apps/tree/main?tab=readme-ov-file#%E5%A6%82%E4%BD%95%E5%9C%A8%E9%A1%B9%E7%9B%AE%E6%A8%A1%E6%9D%BF%E4%B8%AD%E5%BC%80%E5%8F%91%E6%AD%A4%E4%BB%93%E5%BA%93>
22
+
23
+ ## 许可证
24
+
25
+ 本项目采用 [MIT 许可证](LICENSE) 开源。