mioku-plugin-meme 2.1.2 → 3.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/index.ts CHANGED
@@ -1,10 +1,14 @@
1
- import type { AIService, ConfigService } from "mioku";
2
- import { definePlugin, type MiokiContext } from "mioki";
1
+ import {
2
+ definePlugin,
3
+ type MiokuContext,
4
+ getService,
5
+ Services,
6
+ } from "mioku";
3
7
  import { MEME_BASE_CONFIG } from "./configs/base";
4
8
  import { MEME_FILTER_CONFIG } from "./configs/filters";
5
9
  import { MemePluginRuntime, replyWithParts } from "./shared";
6
- import { resetMemeRuntimeState, setMemeRuntimeState } from "./runtime";
7
10
  import type { MemeBaseConfig, MemeFilterConfig } from "./types";
11
+ import { createMemeSkills } from "./skills/meme";
8
12
 
9
13
  function cloneConfig<T>(value: T): T {
10
14
  return JSON.parse(JSON.stringify(value)) as T;
@@ -41,9 +45,9 @@ const memePlugin = definePlugin({
41
45
  version: "1.0.0",
42
46
  description: "基于 meme-generator API 的表情包制作插件",
43
47
 
44
- async setup(ctx: MiokiContext) {
45
- const configService = ctx.services?.config as ConfigService | undefined;
46
- const aiService = ctx.services?.ai as AIService | undefined;
48
+ async setup(ctx: MiokuContext) {
49
+ const configService = getService(ctx, Services.Config);
50
+ const aiService = getService(ctx, Services.AI);
47
51
 
48
52
  let baseConfig = cloneConfig(MEME_BASE_CONFIG);
49
53
  let filterConfig = cloneConfig(MEME_FILTER_CONFIG);
@@ -123,7 +127,9 @@ const memePlugin = definePlugin({
123
127
  );
124
128
  }
125
129
 
126
- setMemeRuntimeState({ runtime });
130
+ if (aiService) {
131
+ for (const skill of createMemeSkills(runtime)) aiService.registerSkill(skill);
132
+ }
127
133
 
128
134
  const disposers: Array<() => void> = [];
129
135
  if (configService) {
@@ -141,7 +147,7 @@ const memePlugin = definePlugin({
141
147
  );
142
148
  }
143
149
 
144
- ctx.handle("message", async (event: any) => {
150
+ ctx.handle("message", async (event) => {
145
151
  const rawText = ctx.text(event)?.trim();
146
152
  if (!rawText) {
147
153
  return;
@@ -366,10 +372,8 @@ const memePlugin = definePlugin({
366
372
  });
367
373
 
368
374
  return () => {
369
- for (const dispose of disposers) {
370
- dispose();
371
- }
372
- resetMemeRuntimeState();
375
+ for (const dispose of disposers) dispose();
376
+ if (aiService) aiService.removeSkill("meme");
373
377
  ctx.logger.info("meme 插件已卸载");
374
378
  };
375
379
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-meme",
3
- "version": "2.1.2",
3
+ "version": "3.0.0",
4
4
  "description": "基于 meme-generator API 的表情包制作插件",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -82,11 +82,9 @@
82
82
  }
83
83
  },
84
84
  "peerDependencies": {
85
- "mioku": "^0.8.0",
86
- "mioki": "^0.16.0"
85
+ "mioku": "^1.0.0"
87
86
  },
88
87
  "devDependencies": {
89
- "mioku": "workspace:*",
90
- "mioki": "^0.16.0"
88
+ "mioku": "workspace:*"
91
89
  }
92
90
  }
package/shared.ts CHANGED
@@ -187,10 +187,10 @@ export async function replyWithParts(options: {
187
187
  parts: any[];
188
188
  quoteReply?: boolean;
189
189
  }): Promise<void> {
190
- const { event, parts, quoteReply = false } = options;
190
+ const { ctx, event, parts, quoteReply = false } = options;
191
191
  const payload = [...parts];
192
192
  if (quoteReply && event?.message_id != null) {
193
- payload.unshift({ type: "reply", id: String(event.message_id) });
193
+ payload.unshift(ctx.segment.reply(event.message_id));
194
194
  }
195
195
  await event.reply(payload.length === 1 ? payload[0] : payload);
196
196
  }
@@ -208,9 +208,7 @@ export async function replyWithImage(options: {
208
208
  parts.push(caption);
209
209
  }
210
210
 
211
- const imageSegment = ctx?.segment?.image
212
- ? ctx.segment.image(image)
213
- : { type: "image", file: image };
211
+ const imageSegment = ctx.segment.image(image);
214
212
  parts.push(imageSegment);
215
213
 
216
214
  try {
@@ -228,11 +226,7 @@ export async function replyWithImage(options: {
228
226
  if (caption) {
229
227
  fallbackParts.push(caption);
230
228
  }
231
- fallbackParts.push(
232
- ctx?.segment?.image
233
- ? ctx.segment.image(base64Image)
234
- : { type: "image", file: base64Image },
235
- );
229
+ fallbackParts.push(ctx.segment.image(base64Image));
236
230
  await replyWithParts({ ctx, event, parts: fallbackParts, quoteReply });
237
231
  }
238
232
 
@@ -244,11 +238,7 @@ export async function sendSkillImage(options: {
244
238
  quoteReply?: boolean;
245
239
  }): Promise<void> {
246
240
  const { ctx, event, image, caption, quoteReply = false } = options;
247
- const selfId = event?.self_id != null ? Number(event.self_id) : undefined;
248
- const bot =
249
- selfId != null && typeof ctx?.pickBot === "function"
250
- ? ctx.pickBot(selfId)
251
- : undefined;
241
+ const bot = event?.bot;
252
242
 
253
243
  if (!bot) {
254
244
  throw new Error("当前上下文不支持发送图片");
@@ -257,14 +247,12 @@ export async function sendSkillImage(options: {
257
247
  const buildPayload = (file: string) => {
258
248
  const payload: any[] = [];
259
249
  if (quoteReply && event?.message_id != null) {
260
- payload.push({ type: "reply", id: String(event.message_id) });
250
+ payload.push(ctx.segment.reply(event.message_id));
261
251
  }
262
252
  if (caption) {
263
253
  payload.push(caption);
264
254
  }
265
- payload.push(
266
- ctx?.segment?.image ? ctx.segment.image(file) : { type: "image", file },
267
- );
255
+ payload.push(ctx.segment.image(file));
268
256
  return payload;
269
257
  };
270
258
 
@@ -622,11 +610,7 @@ export class MemePluginRuntime {
622
610
  );
623
611
  const previewBuffer = Buffer.from(await previewResponse.arrayBuffer());
624
612
  const previewBase64 = await imageBufferToBase64(previewBuffer);
625
- parts.push(
626
- ctx?.segment?.image
627
- ? ctx.segment.image(previewBase64)
628
- : { type: "image", file: previewBase64 },
629
- );
613
+ parts.push(ctx.segment.image(previewBase64));
630
614
  } catch (error) {
631
615
  this.logger.warn(`[meme] 获取预览失败: ${error}`);
632
616
  }
@@ -978,11 +962,11 @@ export class MemePluginRuntime {
978
962
  messageId: number,
979
963
  ): Promise<string | null> {
980
964
  try {
981
- const selfId = event?.self_id != null ? Number(event.self_id) : undefined;
982
- if (selfId == null || !ctx?.pickBot) {
965
+ const bot = event?.bot;
966
+ if (!bot) {
983
967
  return null;
984
968
  }
985
- const msg = await ctx.pickBot(selfId).getMsg(messageId);
969
+ const msg = await bot.getMessage(messageId);
986
970
  if (!msg?.message || !Array.isArray(msg.message)) {
987
971
  return null;
988
972
  }
@@ -1013,11 +997,8 @@ export class MemePluginRuntime {
1013
997
  ];
1014
998
  }
1015
999
 
1016
- const bot =
1017
- event?.self_id != null && typeof ctx?.pickBot === "function"
1018
- ? ctx.pickBot(event.self_id)
1019
- : undefined;
1020
- if (!bot?.getGroupMemberInfo) {
1000
+ const bot = event?.bot;
1001
+ if (!bot) {
1021
1002
  return atUserIds.map((userId) => ({
1022
1003
  name: String(userId),
1023
1004
  gender: "unknown",
@@ -1027,7 +1008,7 @@ export class MemePluginRuntime {
1027
1008
  const infos = await Promise.all(
1028
1009
  atUserIds.map(async (userId) => {
1029
1010
  try {
1030
- const member = await bot.getGroupMemberInfo(event.group_id, userId);
1011
+ const member = await bot.getMemberInfo(event.group_id, userId);
1031
1012
  return {
1032
1013
  name: member?.card || member?.nickname || String(userId),
1033
1014
  gender: String(member?.sex || "unknown"),
package/skills/meme.ts ADDED
@@ -0,0 +1,149 @@
1
+ import type { AISkill, AITool } from "mioku";
2
+ import type { MemePluginRuntime } from "../shared";
3
+ import type { MemeImageSourceOptions } from "../types";
4
+
5
+ function parseImageSource(args: any): {
6
+ ok: boolean;
7
+ source?: MemeImageSourceOptions;
8
+ message?: string;
9
+ } {
10
+ const sourceType = String(args?.source_type || "").trim();
11
+ if (!sourceType) {
12
+ return {
13
+ ok: false,
14
+ message: "缺少 source_type。可选值: user_avatar 或 message_image。",
15
+ };
16
+ }
17
+
18
+ if (sourceType === "user_avatar") {
19
+ const qq = Number(args?.qq);
20
+ if (!Number.isFinite(qq) || qq <= 0) {
21
+ return {
22
+ ok: false,
23
+ message: "source_type=user_avatar 时必须提供有效的 qq。",
24
+ };
25
+ }
26
+ return {
27
+ ok: true,
28
+ source: {
29
+ type: "user_avatar",
30
+ qq,
31
+ },
32
+ };
33
+ }
34
+
35
+ if (sourceType === "message_image") {
36
+ const messageId = Number(args?.message_id);
37
+ if (!Number.isFinite(messageId) || messageId <= 0) {
38
+ return {
39
+ ok: false,
40
+ message: "source_type=message_image 时必须提供有效的 message_id。",
41
+ };
42
+ }
43
+ return {
44
+ ok: true,
45
+ source: {
46
+ type: "message_image",
47
+ messageId,
48
+ },
49
+ };
50
+ }
51
+
52
+ return {
53
+ ok: false,
54
+ message: "source_type 无效。只能是 user_avatar 或 message_image。",
55
+ };
56
+ }
57
+
58
+ export function createMemeSkills(runtime: MemePluginRuntime): AISkill[] {
59
+ return [
60
+ {
61
+ name: "meme",
62
+ description:
63
+ "搜索,制作和发送各种各样的整蛊群友的表情包,可以使用聊天中的图片或用户头像制作",
64
+ permission: "member",
65
+ tools: [
66
+ {
67
+ name: "search_memes",
68
+ description: "按关键词搜索可用的 meme 表情关键词",
69
+ parameters: {
70
+ type: "object",
71
+ properties: {
72
+ query: {
73
+ type: "string",
74
+ description: "搜索关键词,可以是中文或英文片段",
75
+ },
76
+ },
77
+ required: ["query"],
78
+ },
79
+ handler: async (args: any) => {
80
+ await runtime.ensureCache();
81
+ const hits = runtime.searchKeywords(String(args?.query || ""));
82
+ return hits.length > 0
83
+ ? hits.join("、")
84
+ : "没有找到匹配的 meme 关键词";
85
+ },
86
+ } as AITool,
87
+ {
88
+ name: "send_meme",
89
+ description:
90
+ "发送 meme。必须指定图片来源:用户头像(qq)或聊天图片(message_id)。",
91
+ parameters: {
92
+ type: "object",
93
+ properties: {
94
+ keyword: {
95
+ type: "string",
96
+ description: "要生成的 meme 关键词",
97
+ },
98
+ text: {
99
+ type: "string",
100
+ description: "文本内容,多段文本用 / 分隔",
101
+ },
102
+ args: {
103
+ type: "string",
104
+ description: "额外参数,例如 圆、左、循环 等",
105
+ },
106
+ source_type: {
107
+ type: "string",
108
+ description:
109
+ "图片来源类型,只能是 user_avatar 或 message_image",
110
+ enum: ["user_avatar", "message_image"],
111
+ },
112
+ qq: {
113
+ type: "number",
114
+ description: "source_type=user_avatar 时必填,头像对应 QQ 号",
115
+ },
116
+ message_id: {
117
+ type: "number",
118
+ description:
119
+ "source_type=message_image 时必填,包含图片的消息 message_id",
120
+ },
121
+ },
122
+ required: ["keyword", "source_type"],
123
+ },
124
+ handler: async (args: any, runtimeCtx?: any) => {
125
+ const ctx = runtimeCtx?.ctx;
126
+ const event = runtimeCtx?.event || runtimeCtx?.rawEvent;
127
+ if (!ctx || !event) {
128
+ return "当前上下文不支持发送 meme";
129
+ }
130
+
131
+ const source = parseImageSource(args);
132
+ if (!source.ok) {
133
+ return source.message || "图片来源参数不合法";
134
+ }
135
+
136
+ const result = await runtime.generateByKeyword(ctx, event, {
137
+ keyword: String(args?.keyword || ""),
138
+ text: args?.text ? String(args.text) : "",
139
+ args: args?.args ? String(args.args) : "",
140
+ imageSource: source.source,
141
+ send: true,
142
+ });
143
+ return result.message;
144
+ },
145
+ } as AITool,
146
+ ],
147
+ },
148
+ ];
149
+ }
package/runtime.ts DELETED
@@ -1,26 +0,0 @@
1
- import type { MemePluginRuntime } from "./shared";
2
- import {
3
- getPluginRuntimeState,
4
- resetPluginRuntimeState,
5
- setPluginRuntimeState,
6
- } from "mioku";
7
-
8
- export interface MemeRuntimeState {
9
- runtime?: MemePluginRuntime;
10
- }
11
-
12
- const MEME_PLUGIN_NAME = "meme";
13
-
14
- export function setMemeRuntimeState(
15
- nextState: MemeRuntimeState,
16
- ): MemeRuntimeState {
17
- return setPluginRuntimeState(MEME_PLUGIN_NAME, nextState) as MemeRuntimeState;
18
- }
19
-
20
- export function getMemeRuntimeState(): MemeRuntimeState {
21
- return getPluginRuntimeState(MEME_PLUGIN_NAME) as MemeRuntimeState;
22
- }
23
-
24
- export function resetMemeRuntimeState(): void {
25
- resetPluginRuntimeState(MEME_PLUGIN_NAME);
26
- }
package/skills.ts DELETED
@@ -1,157 +0,0 @@
1
- import type { AISkill, AITool } from "mioku";
2
- import type { MemeImageSourceOptions } from "./types";
3
- import { getMemeRuntimeState } from "./runtime";
4
-
5
- function parseImageSource(args: any): {
6
- ok: boolean;
7
- source?: MemeImageSourceOptions;
8
- message?: string;
9
- } {
10
- const sourceType = String(args?.source_type || "").trim();
11
- if (!sourceType) {
12
- return {
13
- ok: false,
14
- message: "缺少 source_type。可选值: user_avatar 或 message_image。",
15
- };
16
- }
17
-
18
- if (sourceType === "user_avatar") {
19
- const qq = Number(args?.qq);
20
- if (!Number.isFinite(qq) || qq <= 0) {
21
- return {
22
- ok: false,
23
- message: "source_type=user_avatar 时必须提供有效的 qq。",
24
- };
25
- }
26
- return {
27
- ok: true,
28
- source: {
29
- type: "user_avatar",
30
- qq,
31
- },
32
- };
33
- }
34
-
35
- if (sourceType === "message_image") {
36
- const messageId = Number(args?.message_id);
37
- if (!Number.isFinite(messageId) || messageId <= 0) {
38
- return {
39
- ok: false,
40
- message: "source_type=message_image 时必须提供有效的 message_id。",
41
- };
42
- }
43
- return {
44
- ok: true,
45
- source: {
46
- type: "message_image",
47
- messageId,
48
- },
49
- };
50
- }
51
-
52
- return {
53
- ok: false,
54
- message: "source_type 无效。只能是 user_avatar 或 message_image。",
55
- };
56
- }
57
-
58
- const memeSkills: AISkill[] = [
59
- {
60
- name: "meme",
61
- description:
62
- "搜索,制作和发送各种各样的整蛊群友的表情包,可以使用聊天中的图片或用户头像制作",
63
- permission: "member",
64
- tools: [
65
- {
66
- name: "search_memes",
67
- description: "按关键词搜索可用的 meme 表情关键词",
68
- parameters: {
69
- type: "object",
70
- properties: {
71
- query: {
72
- type: "string",
73
- description: "搜索关键词,可以是中文或英文片段",
74
- },
75
- },
76
- required: ["query"],
77
- },
78
- handler: async (args: any) => {
79
- const runtime = getMemeRuntimeState().runtime;
80
- if (!runtime) {
81
- return "meme 插件尚未初始化";
82
- }
83
-
84
- await runtime.ensureCache();
85
- const hits = runtime.searchKeywords(String(args?.query || ""));
86
- return hits.length > 0
87
- ? hits.join("、")
88
- : "没有找到匹配的 meme 关键词";
89
- },
90
- } as AITool,
91
- {
92
- name: "send_meme",
93
- description:
94
- "发送 meme。必须指定图片来源:用户头像(qq)或聊天图片(message_id)。",
95
- parameters: {
96
- type: "object",
97
- properties: {
98
- keyword: {
99
- type: "string",
100
- description: "要生成的 meme 关键词",
101
- },
102
- text: {
103
- type: "string",
104
- description: "文本内容,多段文本用 / 分隔",
105
- },
106
- args: {
107
- type: "string",
108
- description: "额外参数,例如 圆、左、循环 等",
109
- },
110
- source_type: {
111
- type: "string",
112
- description: "图片来源类型,只能是 user_avatar 或 message_image",
113
- enum: ["user_avatar", "message_image"],
114
- },
115
- qq: {
116
- type: "number",
117
- description: "source_type=user_avatar 时必填,头像对应 QQ 号",
118
- },
119
- message_id: {
120
- type: "number",
121
- description:
122
- "source_type=message_image 时必填,包含图片的消息 message_id",
123
- },
124
- },
125
- required: ["keyword", "source_type"],
126
- },
127
- handler: async (args: any, runtimeCtx?: any) => {
128
- const runtime = getMemeRuntimeState().runtime;
129
- const ctx = runtimeCtx?.ctx;
130
- const event = runtimeCtx?.event || runtimeCtx?.rawEvent;
131
- if (!runtime) {
132
- return "meme 插件尚未初始化";
133
- }
134
- if (!ctx || !event) {
135
- return "当前上下文不支持发送 meme";
136
- }
137
-
138
- const source = parseImageSource(args);
139
- if (!source.ok) {
140
- return source.message || "图片来源参数不合法";
141
- }
142
-
143
- const result = await runtime.generateByKeyword(ctx, event, {
144
- keyword: String(args?.keyword || ""),
145
- text: args?.text ? String(args.text) : "",
146
- args: args?.args ? String(args.args) : "",
147
- imageSource: source.source,
148
- send: true,
149
- });
150
- return result.message;
151
- },
152
- } as AITool,
153
- ],
154
- },
155
- ];
156
-
157
- export default memeSkills;