koishi-plugin-smmcat-gptimage2 0.1.21 → 0.1.23

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/chatSend.d.ts CHANGED
@@ -1,17 +1,67 @@
1
+ /**
2
+ * 内容发送模块(Markdown 优先 + 纯文本降级)
3
+ *
4
+ * 功能定位:
5
+ * - 统一封装向 QQ 平台发送消息:优先走 QQ 官方 Markdown 接口(useMd=true),
6
+ * 失败或非 QQ 平台时降级为纯文本 fallbackText。
7
+ *
8
+ * 核心算法(msg_seq 递增):
9
+ * - QQ 平台限制同一 msg_id 的被动回复次数;本模块以 msg_id 为键维护计数,
10
+ * 连续发送时递增 msg_seq;超过 5 次后尝试切换到 middleware 捕获的最新
11
+ * messageId,仍不可用则直接降级发纯文本。
12
+ *
13
+ * 注意事项:
14
+ * - 私聊场景会先移除 <qqbot-at-user> 标签(私聊不支持 @)。
15
+ * - chache/messageIdTemp 为有界内存缓存(分别 40/150 条,FIFO 淘汰)。
16
+ * - 调用方应同时提供 Markdown 内容与等价纯文本,保证降级后信息不丢失。
17
+ */
1
18
  import { Context, Session } from "koishi";
2
19
  import { Config } from ".";
20
+ import { QQKeyboard } from "./keyboards";
21
+ export interface ChatSendResult {
22
+ sent: true;
23
+ mode: 'markdown' | 'text';
24
+ }
25
+ type PassiveSource = {
26
+ type: 'msg_id';
27
+ id: string;
28
+ payload: {
29
+ msg_id: string;
30
+ };
31
+ } | {
32
+ type: 'event_id';
33
+ id: string;
34
+ payload: {
35
+ event_id: string;
36
+ };
37
+ };
3
38
  /** 内容发送模块 */
4
39
  export declare const Chat: {
5
40
  config: Config;
6
41
  ctx: Context;
7
- chache: {};
8
- messageIdTemp: {};
42
+ chache: Record<string, number>;
43
+ messageIdTemp: Record<string, string>;
44
+ buttonEventIdTemp: Record<string, string>;
9
45
  init(config: Config, ctx: Context): Promise<void>;
10
- /** 发送消息 */
11
- send(session: Session, content: string): Promise<void>;
46
+ /**
47
+ * 发送消息(Markdown 优先,失败自动降级)。
48
+ */
49
+ send(session: Session, content: string, fallbackText?: string, keyboard?: QQKeyboard): Promise<ChatSendResult>;
12
50
  isUseMd(session: Session): boolean;
13
- /** 清除一条 msgid 记录 */
51
+ /** 优先取当前 session 的 msg_id;如果是按钮事件,则取 event_id */
52
+ getCurrentPassiveSource(session: Session): PassiveSource | null;
53
+ /**
54
+ * 超过被动 MD 上限时:
55
+ * 1. 优先找 message 事件中缓存的新 msg_id
56
+ * 2. 没有新 msg_id,再找按钮事件缓存的 event_id
57
+ */
58
+ getNextPassiveSource(userId: string, current: PassiveSource): PassiveSource | null;
59
+ getCacheKey(source: PassiveSource): string;
60
+ /** 清除一条 msg_id/event_id 计数记录 */
14
61
  clearChache(): void;
15
62
  /** 清除一条 messageId 缓存 */
16
63
  clearMessageIdTemp(): void;
64
+ /** 清除一条按钮 event_id 缓存 */
65
+ clearButtonEventIdTemp(): void;
17
66
  };
67
+ export {};
package/lib/index.js CHANGED
@@ -47,70 +47,169 @@ var Chat = {
47
47
  config: {},
48
48
  ctx: {},
49
49
  chache: {},
50
+ // message 事件缓存:userId -> msg_id
50
51
  messageIdTemp: {},
52
+ // 按钮事件缓存:userId -> event_id
53
+ buttonEventIdTemp: {},
51
54
  async init(config, ctx) {
52
55
  Chat.config = config;
53
56
  Chat.ctx = ctx;
57
+ Chat.config.deBug && console.log(Chat.chache);
54
58
  ctx.on("message", (session) => {
55
- if (!Chat.messageIdTemp[session.userId]) {
56
- Chat.messageIdTemp[session.userId] = {};
57
- }
59
+ if (!session.userId || !session.messageId) return;
58
60
  Chat.messageIdTemp[session.userId] = session.messageId;
59
61
  Chat.clearMessageIdTemp();
60
62
  }, true);
63
+ ctx.on("interaction/button", (session) => {
64
+ const eventId = session.event?._data?.id;
65
+ if (!session.userId || !eventId) {
66
+ Chat.config.deBug && console.log("[Chat] button interaction has no event_id:", session.event?._data);
67
+ return;
68
+ }
69
+ Chat.buttonEventIdTemp[session.userId] = eventId;
70
+ Chat.clearButtonEventIdTemp();
71
+ Chat.config.deBug && console.log("[Chat] cached button event_id:", eventId);
72
+ });
61
73
  },
62
- /** 发送消息 */
63
- async send(session, content) {
74
+ /**
75
+ * 发送消息(Markdown 优先,失败自动降级)。
76
+ */
77
+ async send(session, content, fallbackText = content, keyboard2) {
64
78
  if (session.platform !== "qq") {
65
- await session.send(content);
66
- return;
79
+ await session.send(fallbackText);
80
+ return { sent: true, mode: "text" };
67
81
  }
68
- if (Chat.config.useMd) {
69
- const messageId = session.messageId;
70
- const userId = session.userId;
71
- if (Chat.chache[messageId] == void 0) {
72
- Chat.chache[messageId] = 0;
73
- }
74
- const mdContent = {
75
- msg_id: session.messageId,
76
- msg_type: 2,
77
- msg_seq: ++Chat.chache[messageId] == 1 ? 0 : Chat.chache[messageId],
78
- markdown: {
79
- content
80
- }
81
- };
82
- if (Chat.chache[messageId] > 1) {
83
- Chat.config.deBug && console.log(`检测到MD连续发送,尝试递增 msg_seq 再进行再次发送;第${Chat.chache[messageId] + 1}次。`);
84
- if (Chat.chache[messageId] > 5) {
85
- if (Chat.messageIdTemp[userId] && mdContent.msg_id != Chat.messageIdTemp[userId]) {
86
- Chat.config.deBug && console.log("middleware 存在新 mse_id 数据,使用更新数据");
87
- mdContent.msg_id = Chat.messageIdTemp[userId];
88
- mdContent.msg_seq = 0;
89
- }
82
+ if (!Chat.config.useMd) {
83
+ await session.send(fallbackText);
84
+ return { sent: true, mode: "text" };
85
+ }
86
+ const userId = session.userId;
87
+ let source = Chat.getCurrentPassiveSource(session);
88
+ if (!source) {
89
+ Chat.config.deBug && console.log("[Chat.send] no msg_id/event_id, fallback to text");
90
+ await session.send(fallbackText);
91
+ return { sent: true, mode: "text" };
92
+ }
93
+ const cacheKey = Chat.getCacheKey(source);
94
+ if (Chat.chache[cacheKey] == void 0) {
95
+ Chat.chache[cacheKey] = 0;
96
+ }
97
+ let seq = ++Chat.chache[cacheKey];
98
+ let msgSeq = seq === 1 ? 0 : seq;
99
+ if (seq > 1) {
100
+ Chat.config.deBug && console.log(`检测到 MD 连续发送,尝试递增 msg_seq;当前第 ${seq} 次。`);
101
+ }
102
+ if (seq > 5) {
103
+ const nextSource = Chat.getNextPassiveSource(userId, source);
104
+ if (nextSource) {
105
+ source = nextSource;
106
+ const nextCacheKey = Chat.getCacheKey(source);
107
+ if (Chat.chache[nextCacheKey] == void 0) {
108
+ Chat.chache[nextCacheKey] = 0;
90
109
  }
110
+ const nextSeq = ++Chat.chache[nextCacheKey];
111
+ msgSeq = nextSeq === 1 ? 0 : nextSeq;
112
+ Chat.config.deBug && console.log(`[Chat.send] 被动 MD 超过上限,切换到 ${source.type}: ${source.id}`);
113
+ } else {
114
+ await session.send(fallbackText);
115
+ Chat.clearChache();
116
+ return { sent: true, mode: "text" };
91
117
  }
92
- try {
93
- if (session.guildId) {
94
- await session.bot.internal.sendMessage(session.channelId, mdContent);
95
- } else {
96
- mdContent.markdown.content = mdContent.markdown.content.replace(/<qqbot-at-user[^>]*\/>/g, "");
97
- await session.bot.internal.sendPrivateMessage(session.userId, mdContent);
98
- }
99
- } catch (error) {
100
- console.log(error);
118
+ }
119
+ const mdContent = {
120
+ ...source.payload,
121
+ msg_type: 2,
122
+ msg_seq: msgSeq,
123
+ markdown: {
124
+ content
125
+ },
126
+ ...keyboard2 ? { keyboard: keyboard2 } : {}
127
+ };
128
+ try {
129
+ if (session.guildId) {
130
+ await session.bot.internal.sendMessage(session.channelId, mdContent);
131
+ } else {
132
+ mdContent.markdown.content = mdContent.markdown.content.replace(/<qqbot-at-user[^>]*\/>/g, "");
133
+ await session.bot.internal.sendPrivateMessage(session.userId, mdContent);
101
134
  }
102
135
  Chat.clearChache();
103
- } else {
104
- await session.send(content);
136
+ return { sent: true, mode: "markdown" };
137
+ } catch (error) {
138
+ Chat.config.deBug && console.log("[Chat.send] Markdown 发送失败,回退普通文本", error);
139
+ try {
140
+ await session.send(fallbackText);
141
+ Chat.clearChache();
142
+ return { sent: true, mode: "text" };
143
+ } catch {
144
+ Chat.clearChache();
145
+ throw new Error("消息发送失败");
146
+ }
105
147
  }
106
148
  },
107
- // 当前 session 是否可以使用MD
149
+ // 当前 session 是否可以使用 MD
108
150
  isUseMd(session) {
109
151
  if (session.platform !== "qq") return false;
110
- if (Chat.chache[session.messageId] == void 0) return true;
111
- return Chat.chache[session.messageId] <= 5;
152
+ const source = Chat.getCurrentPassiveSource(session);
153
+ if (!source) return false;
154
+ const cacheKey = Chat.getCacheKey(source);
155
+ if (Chat.chache[cacheKey] == void 0) return true;
156
+ return Chat.chache[cacheKey] <= 5;
112
157
  },
113
- /** 清除一条 msgid 记录 */
158
+ /** 优先取当前 session 的 msg_id;如果是按钮事件,则取 event_id */
159
+ getCurrentPassiveSource(session) {
160
+ if (session.messageId) {
161
+ return {
162
+ type: "msg_id",
163
+ id: session.messageId,
164
+ payload: {
165
+ msg_id: session.messageId
166
+ }
167
+ };
168
+ }
169
+ const eventId = session.event?._data?.id;
170
+ if (eventId) {
171
+ return {
172
+ type: "event_id",
173
+ id: eventId,
174
+ payload: {
175
+ event_id: eventId
176
+ }
177
+ };
178
+ }
179
+ return null;
180
+ },
181
+ /**
182
+ * 超过被动 MD 上限时:
183
+ * 1. 优先找 message 事件中缓存的新 msg_id
184
+ * 2. 没有新 msg_id,再找按钮事件缓存的 event_id
185
+ */
186
+ getNextPassiveSource(userId, current) {
187
+ const cachedMessageId = Chat.messageIdTemp[userId];
188
+ if (cachedMessageId && !(current.type === "msg_id" && current.id === cachedMessageId)) {
189
+ return {
190
+ type: "msg_id",
191
+ id: cachedMessageId,
192
+ payload: {
193
+ msg_id: cachedMessageId
194
+ }
195
+ };
196
+ }
197
+ const cachedButtonEventId = Chat.buttonEventIdTemp[userId];
198
+ if (cachedButtonEventId && !(current.type === "event_id" && current.id === cachedButtonEventId)) {
199
+ return {
200
+ type: "event_id",
201
+ id: cachedButtonEventId,
202
+ payload: {
203
+ event_id: cachedButtonEventId
204
+ }
205
+ };
206
+ }
207
+ return null;
208
+ },
209
+ getCacheKey(source) {
210
+ return `${source.type}:${source.id}`;
211
+ },
212
+ /** 清除一条 msg_id/event_id 计数记录 */
114
213
  clearChache() {
115
214
  const chache = Object.keys(Chat.chache);
116
215
  if (chache.length >= 40) {
@@ -123,9 +222,45 @@ var Chat = {
123
222
  if (messageIdTempList.length > 150) {
124
223
  delete Chat.messageIdTemp[messageIdTempList[0]];
125
224
  }
225
+ },
226
+ /** 清除一条按钮 event_id 缓存 */
227
+ clearButtonEventIdTemp() {
228
+ const buttonEventIdTempList = Object.keys(Chat.buttonEventIdTemp);
229
+ if (buttonEventIdTempList.length > 150) {
230
+ delete Chat.buttonEventIdTemp[buttonEventIdTempList[0]];
231
+ }
126
232
  }
127
233
  };
128
234
 
235
+ // src/keyboards.ts
236
+ var UNSUPPORT_TIPS = "请升级QQ版本";
237
+ function cmdButton(label, data, style = 0, permission = { type: 2 }, visitedLabel = label, enter = true) {
238
+ return {
239
+ render_data: { label, visited_label: visitedLabel, style },
240
+ action: { type: 2, permission, data, enter, unsupport_tips: UNSUPPORT_TIPS }
241
+ };
242
+ }
243
+ __name(cmdButton, "cmdButton");
244
+ function callbackButton(label, data, style = 0, permission = { type: 2 }, visitedLabel = label, id) {
245
+ return {
246
+ id,
247
+ render_data: { label, visited_label: visitedLabel, style },
248
+ action: { type: 1, permission, data, unsupport_tips: UNSUPPORT_TIPS }
249
+ };
250
+ }
251
+ __name(callbackButton, "callbackButton");
252
+ function linkButton(label, url, style = 0) {
253
+ return {
254
+ render_data: { label, visited_label: label, style },
255
+ action: { type: 0, permission: { type: 2 }, data: url, unsupport_tips: UNSUPPORT_TIPS }
256
+ };
257
+ }
258
+ __name(linkButton, "linkButton");
259
+ function keyboard(...rows) {
260
+ return { content: { rows: rows.map((buttons) => ({ buttons })) } };
261
+ }
262
+ __name(keyboard, "keyboard");
263
+
129
264
  // src/index.ts
130
265
  var name = "smmcat-gptimage2";
131
266
  var inject = ["canvas", "database"];
@@ -380,7 +515,7 @@ function apply(ctx, config) {
380
515
  };
381
516
  config.deBug && console.log(body);
382
517
  const response = await ctx.http.post(body.images ? modelConfig.use.url : modelConfig.use.no_img_url, body, {
383
- headers: { "Authorization": config.token }
518
+ headers: { "Authorization": config.token, "Content-Type": "application/json" }
384
519
  });
385
520
  config.deBug && console.log(response);
386
521
  const data = response.data;
@@ -544,9 +679,18 @@ function apply(ctx, config) {
544
679
  if (!newData) {
545
680
  await Chat.send(session, `获取失败,稍后再试!`);
546
681
  }
547
- await Chat.send(session, VideoGpt.formatMsg(newData));
682
+ await Chat.send(session, VideoGpt.formatMsg(newData), VideoGpt.formatMsg(newData), VideoGpt.formatButton(newData, session));
548
683
  }
549
684
  },
685
+ formatButton(data, session) {
686
+ return keyboard(
687
+ [
688
+ session.guildId ? callbackButton("更新进度", data.task_id, 0, { type: 0, specify_user_ids: [session.userId] }, "查询完成", "check_video_task") : callbackButton("更新进度", data.task_id, 0, { type: 2 }, "查询完成", "check_video_task")
689
+ ],
690
+ [cmdButton("查询 teak_id", "/查视频进度 " + data.task_id, 0)],
691
+ data.webUrl ? [linkButton("查看视频", data.webUrl, 0)] : []
692
+ );
693
+ },
550
694
  formatMsg(data) {
551
695
  console.log(data);
552
696
  const video = data?.url ? import_koishi.h.video((0, import_node_url.pathToFileURL)(import_path.default.join(ctx.baseDir, config.download_videoPath, data.url)).href) : "";
@@ -555,10 +699,9 @@ function apply(ctx, config) {
555
699
  const status = VideoGpt.statusDict[data.status] || "未知";
556
700
  const model = data.model;
557
701
  const progress = data.progress;
558
- console.log(data.url);
559
702
  if (config.useMd) {
560
703
  return `
561
- **teak_id** <qqbot-cmd-input text="/查视频进度 ${data.task_id}" show="点击复制"/>
704
+ ### 视频生成进度查询
562
705
 
563
706
  \`\`\` 内容
564
707
  ${data.prompt}
@@ -568,8 +711,7 @@ function apply(ctx, config) {
568
711
  **进度** ${progress}
569
712
  **使用模型** ${model}
570
713
  **创建时间** ${createdTime}
571
-
572
- ${data?.webUrl ? `[查看信息](${data.webUrl})` : ""}`;
714
+ `;
573
715
  } else {
574
716
  return `teak_id:${data.task_id}
575
717
  内容:${prompt}
@@ -764,6 +906,26 @@ tip:您现在使用的是 ${videoConfig.use.type} 进行处理`);
764
906
  await VideoGpt.getVideoResult(session, task_id);
765
907
  });
766
908
  }
909
+ const cmdDict = {
910
+ "check_video_task": /* @__PURE__ */ __name((btnData) => {
911
+ return `查视频进度 ${btnData.data}`;
912
+ }, "check_video_task")
913
+ };
914
+ ctx.on("interaction/button", async (session) => {
915
+ if (session.platform !== "qq") return;
916
+ const button = session.event?.button;
917
+ const buttonKey = button?.id;
918
+ console.log(buttonKey);
919
+ if (!buttonKey) return;
920
+ const interactionId = session.event?._data?.id;
921
+ if (interactionId && session.bot.internal?.acknowledgeInteraction) {
922
+ void Promise.resolve(session.bot.internal.acknowledgeInteraction(interactionId, { code: 0 })).catch(() => {
923
+ });
924
+ }
925
+ if (buttonKey && cmdDict[buttonKey]) {
926
+ await session.execute(cmdDict[buttonKey](button));
927
+ }
928
+ });
767
929
  }
768
930
  __name(apply, "apply");
769
931
  // Annotate the CommonJS export names for ESM import in node:
@@ -0,0 +1,46 @@
1
+ /**
2
+ * QQ Markdown 下挂按钮(keyboard)组件
3
+ *
4
+ * 结构、样式与交互规范来自 robloxmdbutton.md / QQ_MARKDOWN_BUTTON_GUIDE.md:
5
+ * - action.type:0 链接 / 1 回调 / 2 指令
6
+ * - render_data.style:0 灰线框 / 1 蓝线框 / 2 蓝底 / 3 红色警示 / 4 主操作
7
+ * - permission.type:0 指定用户 / 1 管理者 / 2 所有人
8
+ * - 每行最多 4 个按钮,最多 5 行
9
+ */
10
+ export interface QQButtonRenderData {
11
+ label: string;
12
+ visited_label?: string;
13
+ style: 0 | 1 | 2 | 3 | 4;
14
+ }
15
+ export interface QQButtonPermission {
16
+ type: 0 | 1 | 2;
17
+ specify_user_ids?: string[];
18
+ }
19
+ export interface QQButtonAction {
20
+ type: 0 | 1 | 2;
21
+ permission: QQButtonPermission;
22
+ data: string;
23
+ enter?: boolean;
24
+ reply?: boolean;
25
+ unsupport_tips?: string;
26
+ }
27
+ export interface QQButton {
28
+ id?: string;
29
+ render_data: QQButtonRenderData;
30
+ action: QQButtonAction;
31
+ }
32
+ export interface QQKeyboardRow {
33
+ buttons: QQButton[];
34
+ }
35
+ export interface QQKeyboard {
36
+ content: {
37
+ rows: QQKeyboardRow[];
38
+ };
39
+ }
40
+ /** 指令按钮(action.type=2):以点击者身份发送指令 */
41
+ export declare function cmdButton(label: string, data: string, style?: QQButtonRenderData['style'], permission?: QQButtonPermission, visitedLabel?: string, enter?: boolean): QQButton;
42
+ /** 回调按钮(action.type=1):触发 interaction/button 事件 */
43
+ export declare function callbackButton(label: string, data: string, style?: QQButtonRenderData['style'], permission?: QQButtonPermission, visitedLabel?: string, id?: string): QQButton;
44
+ /** 链接按钮(action.type=0) */
45
+ export declare function linkButton(label: string, url: string, style?: QQButtonRenderData['style']): QQButton;
46
+ export declare function keyboard(...rows: QQButton[][]): QQKeyboard;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-smmcat-gptimage2",
3
3
  "description": "简单去使用云接口 GPT-Image2",
4
- "version": "0.1.21",
4
+ "version": "0.1.23",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [