mioku-plugin-meme 1.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/LICENSE +21 -0
- package/README.md +29 -0
- package/config.md +103 -0
- package/configs/base.ts +25 -0
- package/configs/filters.ts +26 -0
- package/index.ts +374 -0
- package/package.json +61 -0
- package/runtime.ts +26 -0
- package/shared.ts +1160 -0
- package/skills.ts +157 -0
- package/types.ts +93 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jerry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Meme Plugin
|
|
2
|
+
|
|
3
|
+
Mioku 的 `meme` 插件,用来接入 `meme-generator` API,在聊天中搜索、查看和生成表情包。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
- 支持 `meme` 菜单、搜索、详情、更新
|
|
8
|
+
- 支持直接用表情关键词生成表情
|
|
9
|
+
- 支持 AI 调用 `search_memes` 和 `send_meme`
|
|
10
|
+
- 支持 WebUI 配置页
|
|
11
|
+
- 支持黑名单关键词拦截
|
|
12
|
+
- 支持用户头像或指定消息图片作为生成来源
|
|
13
|
+
|
|
14
|
+
## 使用
|
|
15
|
+
|
|
16
|
+
这个插件依赖一个单独运行的 `meme-generator` API 服务
|
|
17
|
+
|
|
18
|
+
默认 API 地址:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
http://127.0.0.1:2233
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
你可以在 WebUI 或配置文件里修改
|
|
25
|
+
|
|
26
|
+
## 搭建 Meme API
|
|
27
|
+
|
|
28
|
+
- GitHub: https://github.com/MeetWq/meme-generator
|
|
29
|
+
- Docker Hub: https://hub.docker.com/r/meetwq/meme-generator
|
package/config.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Meme 插件配置
|
|
3
|
+
description: 在这里调整表情包插件的配置
|
|
4
|
+
fields:
|
|
5
|
+
- key: base.api.baseUrl
|
|
6
|
+
label: Meme API 地址
|
|
7
|
+
type: text
|
|
8
|
+
description: meme-generator 服务地址,可自行搭建
|
|
9
|
+
placeholder: http://127.0.0.1:2233
|
|
10
|
+
|
|
11
|
+
- key: base.api.timeoutMs
|
|
12
|
+
label: 请求超时毫秒
|
|
13
|
+
type: number
|
|
14
|
+
description: 访问 meme API 的超时时间
|
|
15
|
+
placeholder: 15000
|
|
16
|
+
|
|
17
|
+
- key: base.trigger.requirePrefix
|
|
18
|
+
label: 命令是否要求前缀
|
|
19
|
+
type: switch
|
|
20
|
+
description: 开启后,菜单/搜索/更新等命令必须带前缀
|
|
21
|
+
|
|
22
|
+
- key: base.trigger.prefixes
|
|
23
|
+
label: 命令前缀列表
|
|
24
|
+
type: json
|
|
25
|
+
description: 命令识别前缀数组。默认是 [\"/\", \"#\"]。
|
|
26
|
+
|
|
27
|
+
- key: base.trigger.directKeywordNeedPrefix
|
|
28
|
+
label: 关键词是否要前缀
|
|
29
|
+
type: switch
|
|
30
|
+
description: 控制 `<表情关键词> [文本][#参数]` 这种是否必须带前缀
|
|
31
|
+
|
|
32
|
+
- key: base.behavior.quoteReply
|
|
33
|
+
label: 生成结果是否引用
|
|
34
|
+
type: switch
|
|
35
|
+
description: 发送生成图片时是否附带引用回复段
|
|
36
|
+
|
|
37
|
+
- key: base.behavior.useSenderAvatarFallback
|
|
38
|
+
label: 使用发送着头像
|
|
39
|
+
type: switch
|
|
40
|
+
description: 在自动取图模式下,如果消息和引用里都没有图,是否自动使用发送者头像进行制作
|
|
41
|
+
|
|
42
|
+
- key: base.behavior.includePreviewInDetail
|
|
43
|
+
label: 详情页附带预览图
|
|
44
|
+
type: switch
|
|
45
|
+
description: 执行“meme 详情 关键词”时是否再拉取一张预览图
|
|
46
|
+
|
|
47
|
+
- key: base.behavior.maxSearchResults
|
|
48
|
+
label: 搜索结果上限
|
|
49
|
+
type: number
|
|
50
|
+
description: 单次搜索返回的关键词最大条数
|
|
51
|
+
placeholder: 20
|
|
52
|
+
|
|
53
|
+
- key: base.cache.refreshOnStartup
|
|
54
|
+
label: 启动自动刷新缓存
|
|
55
|
+
type: switch
|
|
56
|
+
description: 开启后插件每次启动都会重新拉取关键词和菜单图,关闭则优先使用本地缓存
|
|
57
|
+
|
|
58
|
+
- key: base.permissions.ownerOnlyUpdate
|
|
59
|
+
label: 更新是否仅主人可用
|
|
60
|
+
type: switch
|
|
61
|
+
description: 控制“meme 更新”命令的权限。开启后只有 owner 能手动刷新缓存
|
|
62
|
+
|
|
63
|
+
- key: filters.enabled
|
|
64
|
+
label: 是否启用黑名单
|
|
65
|
+
type: switch
|
|
66
|
+
description: 开启后命中黑名单关键词会阻止生成
|
|
67
|
+
|
|
68
|
+
- key: filters.blockedKeywords
|
|
69
|
+
label: 黑名单关键词列表
|
|
70
|
+
type: json
|
|
71
|
+
description: 关键词数组,命中任意一项即视为黑名单触发
|
|
72
|
+
|
|
73
|
+
- key: filters.replyOnBlocked
|
|
74
|
+
label: 黑名单命中时是否回复
|
|
75
|
+
type: switch
|
|
76
|
+
description: 关闭则静默拦截不回消息
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
```mioku-fields
|
|
80
|
+
keys:
|
|
81
|
+
- base.api.baseU
|
|
82
|
+
- base.api.timeoutMs
|
|
83
|
+
- base.trigger.requirePrefix
|
|
84
|
+
- base.trigger.prefixes
|
|
85
|
+
- base.trigger.directKeywordNeedPrefix
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```mioku-fields
|
|
89
|
+
keys:
|
|
90
|
+
- base.behavior.quoteReply
|
|
91
|
+
- base.behavior.useSenderAvatarFallback
|
|
92
|
+
- base.behavior.includePreviewInDetail
|
|
93
|
+
- base.behavior.maxSearchResults
|
|
94
|
+
- base.cache.refreshOnStartup
|
|
95
|
+
- base.permissions.ownerOnlyUpdate
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```mioku-fields
|
|
99
|
+
keys:
|
|
100
|
+
- filters.enabled
|
|
101
|
+
- filters.blockedKeywords
|
|
102
|
+
- filters.replyOnBlocked
|
|
103
|
+
```
|
package/configs/base.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MemeBaseConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
export const MEME_BASE_CONFIG: MemeBaseConfig = {
|
|
4
|
+
api: {
|
|
5
|
+
baseUrl: "http://127.0.0.1:2233",
|
|
6
|
+
timeoutMs: 15000,
|
|
7
|
+
},
|
|
8
|
+
trigger: {
|
|
9
|
+
requirePrefix: true,
|
|
10
|
+
prefixes: ["/", "#"],
|
|
11
|
+
directKeywordNeedPrefix: false,
|
|
12
|
+
},
|
|
13
|
+
behavior: {
|
|
14
|
+
quoteReply: false,
|
|
15
|
+
useSenderAvatarFallback: true,
|
|
16
|
+
includePreviewInDetail: true,
|
|
17
|
+
maxSearchResults: 20,
|
|
18
|
+
},
|
|
19
|
+
cache: {
|
|
20
|
+
refreshOnStartup: false,
|
|
21
|
+
},
|
|
22
|
+
permissions: {
|
|
23
|
+
ownerOnlyUpdate: true,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MemeFilterConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
export const MEME_FILTER_CONFIG: MemeFilterConfig = {
|
|
4
|
+
enabled: true,
|
|
5
|
+
blockedKeywords: [
|
|
6
|
+
"撅",
|
|
7
|
+
"砍",
|
|
8
|
+
"射",
|
|
9
|
+
"🐍",
|
|
10
|
+
"看看你的",
|
|
11
|
+
"换位思考",
|
|
12
|
+
"打胶",
|
|
13
|
+
"飞机杯",
|
|
14
|
+
"鞭",
|
|
15
|
+
"导",
|
|
16
|
+
"打飞机",
|
|
17
|
+
"口",
|
|
18
|
+
"火柴撸",
|
|
19
|
+
"求",
|
|
20
|
+
"啃",
|
|
21
|
+
"对称",
|
|
22
|
+
"sm",
|
|
23
|
+
"斩",
|
|
24
|
+
],
|
|
25
|
+
replyOnBlocked: true,
|
|
26
|
+
};
|
package/index.ts
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import type { AIService } from "../../src/services/ai/types";
|
|
2
|
+
import type { ConfigService } from "../../src/services/config/tpyes";
|
|
3
|
+
import { definePlugin, type MiokiContext } from "mioki";
|
|
4
|
+
import { MEME_BASE_CONFIG } from "./configs/base";
|
|
5
|
+
import { MEME_FILTER_CONFIG } from "./configs/filters";
|
|
6
|
+
import { MemePluginRuntime, replyWithParts } from "./shared";
|
|
7
|
+
import { resetMemeRuntimeState, setMemeRuntimeState } from "./runtime";
|
|
8
|
+
import type { MemeBaseConfig, MemeFilterConfig } from "./types";
|
|
9
|
+
|
|
10
|
+
function cloneConfig<T>(value: T): T {
|
|
11
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeErrorMessage(error: unknown): string {
|
|
15
|
+
if (error instanceof Error && error.message) return error.message;
|
|
16
|
+
if (typeof error === "string") return error;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.stringify(error);
|
|
19
|
+
} catch {
|
|
20
|
+
return String(error);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function stripCommandPrefix(
|
|
25
|
+
text: string,
|
|
26
|
+
prefixes: string[],
|
|
27
|
+
): { value: string; hasPrefix: boolean } {
|
|
28
|
+
const trimmed = String(text || "").trim();
|
|
29
|
+
for (const prefix of prefixes) {
|
|
30
|
+
if (trimmed.startsWith(prefix)) {
|
|
31
|
+
return {
|
|
32
|
+
value: trimmed.slice(prefix.length).trim(),
|
|
33
|
+
hasPrefix: true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { value: trimmed, hasPrefix: false };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const memePlugin = definePlugin({
|
|
41
|
+
name: "meme",
|
|
42
|
+
version: "1.0.0",
|
|
43
|
+
description: "基于 meme-generator API 的表情包制作插件",
|
|
44
|
+
|
|
45
|
+
async setup(ctx: MiokiContext) {
|
|
46
|
+
const configService = ctx.services?.config as ConfigService | undefined;
|
|
47
|
+
const aiService = ctx.services?.ai as AIService | undefined;
|
|
48
|
+
|
|
49
|
+
let baseConfig = cloneConfig(MEME_BASE_CONFIG);
|
|
50
|
+
let filterConfig = cloneConfig(MEME_FILTER_CONFIG);
|
|
51
|
+
|
|
52
|
+
if (configService) {
|
|
53
|
+
await configService.registerConfig("meme", "base", baseConfig);
|
|
54
|
+
await configService.registerConfig("meme", "filters", filterConfig);
|
|
55
|
+
|
|
56
|
+
const nextBase = await configService.getConfig("meme", "base");
|
|
57
|
+
const nextFilters = await configService.getConfig("meme", "filters");
|
|
58
|
+
if (nextBase) {
|
|
59
|
+
baseConfig = nextBase as MemeBaseConfig;
|
|
60
|
+
}
|
|
61
|
+
if (nextFilters) {
|
|
62
|
+
filterConfig = nextFilters as MemeFilterConfig;
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
ctx.logger.warn("config-service 未加载,meme 插件将使用内置默认配置");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const notifyByAIRuntime = async (
|
|
69
|
+
event: any,
|
|
70
|
+
instruction: string,
|
|
71
|
+
fallbackMessage?: string,
|
|
72
|
+
error?: unknown,
|
|
73
|
+
) => {
|
|
74
|
+
if (error != null) {
|
|
75
|
+
ctx.logger.error(
|
|
76
|
+
`[meme] ${instruction}\n执行错误: ${normalizeErrorMessage(error)}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
const chatRuntime = aiService?.getChatRuntime();
|
|
80
|
+
if (chatRuntime) {
|
|
81
|
+
try {
|
|
82
|
+
await chatRuntime.generateNotice({
|
|
83
|
+
event,
|
|
84
|
+
instruction,
|
|
85
|
+
send: true,
|
|
86
|
+
promptInjections: [
|
|
87
|
+
{
|
|
88
|
+
title: "Meme Plugin Notice",
|
|
89
|
+
content:
|
|
90
|
+
"A meme-related action was triggered. Judge whether the user likely intended this action or triggered it accidentally. If it looks accidental or like a casual mention, weave a natural reply into the conversation without mentioning the plugin, tools, or commands. If the user seems to want this feature, respond helpfully. Keep it brief and natural.",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
} catch (noticeError) {
|
|
96
|
+
ctx.logger.error(`meme notice 发送失败 ${noticeError}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await replyWithParts({
|
|
101
|
+
ctx,
|
|
102
|
+
event,
|
|
103
|
+
parts: [fallbackMessage || "请求处理失败,请稍后重试"],
|
|
104
|
+
quoteReply: baseConfig.behavior.quoteReply,
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const runtime = new MemePluginRuntime({
|
|
109
|
+
logger: ctx.logger,
|
|
110
|
+
baseConfig,
|
|
111
|
+
filterConfig,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await runtime.initialize();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
ctx.logger.warn(
|
|
118
|
+
`meme 插件初始化缓存失败,首次使用时会再尝试拉取: ${error}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
setMemeRuntimeState({ runtime });
|
|
123
|
+
|
|
124
|
+
const disposers: Array<() => void> = [];
|
|
125
|
+
if (configService) {
|
|
126
|
+
disposers.push(
|
|
127
|
+
configService.onConfigChange("meme", "base", (next) => {
|
|
128
|
+
baseConfig = next as MemeBaseConfig;
|
|
129
|
+
runtime.updateBaseConfig(baseConfig);
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
disposers.push(
|
|
133
|
+
configService.onConfigChange("meme", "filters", (next) => {
|
|
134
|
+
filterConfig = next as MemeFilterConfig;
|
|
135
|
+
runtime.updateFilterConfig(filterConfig);
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
ctx.handle("message", async (event: any) => {
|
|
141
|
+
const rawText = ctx.text(event)?.trim();
|
|
142
|
+
if (!rawText) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const stripResult = stripCommandPrefix(
|
|
147
|
+
rawText,
|
|
148
|
+
baseConfig.trigger.prefixes,
|
|
149
|
+
);
|
|
150
|
+
const prefixedText = stripResult.value;
|
|
151
|
+
const commandText =
|
|
152
|
+
stripResult.hasPrefix || !baseConfig.trigger.requirePrefix
|
|
153
|
+
? prefixedText
|
|
154
|
+
: "";
|
|
155
|
+
const directText = stripResult.hasPrefix
|
|
156
|
+
? prefixedText
|
|
157
|
+
: baseConfig.trigger.directKeywordNeedPrefix
|
|
158
|
+
? ""
|
|
159
|
+
: rawText;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
if (
|
|
163
|
+
commandText &&
|
|
164
|
+
(/^(?:表情|meme)包?(?:菜单|展示|制作(?:列表)?)$/i.test(commandText) ||
|
|
165
|
+
/^meme\s+(?:菜单|列表|展示)$/i.test(commandText))
|
|
166
|
+
) {
|
|
167
|
+
await runtime.sendMenu(ctx, event);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (commandText && /^随机(?:表情|meme)(?:包)?$/i.test(commandText)) {
|
|
172
|
+
await runtime.sendRandom(ctx, event);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (
|
|
177
|
+
commandText &&
|
|
178
|
+
(/^(?:(?:头像|文字)?(?:表情|meme)包?(?:制作(?:菜单|教程)?|帮助|说明|指令|使用说明)|(?:头像|文字)(?:表情|meme)包?)$/i.test(
|
|
179
|
+
commandText,
|
|
180
|
+
) ||
|
|
181
|
+
/^meme\s+帮助$/i.test(commandText))
|
|
182
|
+
) {
|
|
183
|
+
await runtime.sendHelp(ctx, event);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const searchMatch = commandText.match(
|
|
188
|
+
/^(?:表情|meme)包?(?:搜索|检索)\s*(.*)$/i,
|
|
189
|
+
);
|
|
190
|
+
if (searchMatch) {
|
|
191
|
+
const query = (searchMatch[1] || "").trim();
|
|
192
|
+
if (!query) {
|
|
193
|
+
await notifyByAIRuntime(
|
|
194
|
+
event,
|
|
195
|
+
"用户发起了 meme 搜索但没有给关键词。请自然提醒他补一个搜索词",
|
|
196
|
+
"你想搜什么?",
|
|
197
|
+
);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
await runtime.sendSearch(ctx, event, query);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const detailCommandMatch =
|
|
205
|
+
commandText.match(/^meme包?\s+详情\s+(.+)$/i);
|
|
206
|
+
if (detailCommandMatch) {
|
|
207
|
+
const keyword = detailCommandMatch[1].trim();
|
|
208
|
+
if (!runtime.getDetail(keyword)) {
|
|
209
|
+
await notifyByAIRuntime(
|
|
210
|
+
event,
|
|
211
|
+
`用户查询了不存在的表情详情关键词 "${keyword}"。请自然提醒先搜索可用关键词`,
|
|
212
|
+
`未找到表情关键词:${keyword}`,
|
|
213
|
+
);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
await runtime.sendDetail(ctx, event, keyword);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (
|
|
221
|
+
commandText &&
|
|
222
|
+
(/^(?:表情|meme)包?更新$/i.test(commandText) ||
|
|
223
|
+
/^meme\s+更新$/i.test(commandText))
|
|
224
|
+
) {
|
|
225
|
+
if (baseConfig.permissions.ownerOnlyUpdate && !ctx.isOwner(event)) {
|
|
226
|
+
await notifyByAIRuntime(
|
|
227
|
+
event,
|
|
228
|
+
"用户尝试刷新 meme 缓存,但权限不足。请自然提醒该操作仅主人可用",
|
|
229
|
+
"不支持小男娘使用喵",
|
|
230
|
+
);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
await replyWithParts({
|
|
235
|
+
ctx,
|
|
236
|
+
event,
|
|
237
|
+
parts: ["开始刷新 meme 缓存,请稍等..."],
|
|
238
|
+
quoteReply: baseConfig.behavior.quoteReply,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
await runtime.refreshCache();
|
|
243
|
+
await runtime.sendMenu(ctx, event);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
await notifyByAIRuntime(
|
|
246
|
+
event,
|
|
247
|
+
`meme 缓存刷新失败,错误信息:${error}。请自然告知用户稍后重试或让管理员检查 meme API 服务状态。`,
|
|
248
|
+
`刷新 meme 缓存失败:${error}`,
|
|
249
|
+
error,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const genericGenerateMatch = commandText.match(
|
|
256
|
+
/^meme包?\s+(?:生成|制作)\s+(.+)$/i,
|
|
257
|
+
);
|
|
258
|
+
if (genericGenerateMatch) {
|
|
259
|
+
const result = await runtime.generateFromInput(
|
|
260
|
+
ctx,
|
|
261
|
+
event,
|
|
262
|
+
genericGenerateMatch[1],
|
|
263
|
+
{
|
|
264
|
+
send: true,
|
|
265
|
+
},
|
|
266
|
+
);
|
|
267
|
+
if (!result.ok && result.shouldNotice) {
|
|
268
|
+
await notifyByAIRuntime(
|
|
269
|
+
event,
|
|
270
|
+
result.noticeInstruction || `meme 生成失败:${result.message}`,
|
|
271
|
+
result.message,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const genericPassThroughMatch = commandText.match(/^meme包?\s+(.+)$/i);
|
|
278
|
+
if (genericPassThroughMatch) {
|
|
279
|
+
const passThrough = genericPassThroughMatch[1].trim();
|
|
280
|
+
if (!passThrough) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (/(详情|帮助|成分)$/.test(passThrough)) {
|
|
285
|
+
const keyword = passThrough.replace(/(详情|帮助|成分)$/, "").trim();
|
|
286
|
+
if (!runtime.getDetail(keyword)) {
|
|
287
|
+
await notifyByAIRuntime(
|
|
288
|
+
event,
|
|
289
|
+
`用户查询了不存在的表情详情关键词 "${keyword}"。请自然提醒先搜索可用关键词`,
|
|
290
|
+
`未找到表情关键词:${keyword}`,
|
|
291
|
+
);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
await runtime.sendDetail(ctx, event, keyword);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const result = await runtime.generateFromInput(
|
|
299
|
+
ctx,
|
|
300
|
+
event,
|
|
301
|
+
passThrough,
|
|
302
|
+
{
|
|
303
|
+
send: true,
|
|
304
|
+
},
|
|
305
|
+
);
|
|
306
|
+
if (!result.ok && result.shouldNotice) {
|
|
307
|
+
await notifyByAIRuntime(
|
|
308
|
+
event,
|
|
309
|
+
result.noticeInstruction || `meme 生成失败:${result.message}`,
|
|
310
|
+
result.message,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (!directText) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const directMatch = runtime.findMatch(directText);
|
|
321
|
+
if (!directMatch) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (/(详情|帮助|成分)$/.test(directMatch.rest)) {
|
|
326
|
+
if (!runtime.getDetail(directMatch.keyword)) {
|
|
327
|
+
await notifyByAIRuntime(
|
|
328
|
+
event,
|
|
329
|
+
`用户查询了不存在的表情详情关键词 "${directMatch.keyword}"。请自然提醒先搜索可用关键词`,
|
|
330
|
+
`未找到表情关键词:${directMatch.keyword}`,
|
|
331
|
+
);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
await runtime.sendDetail(ctx, event, directMatch.keyword);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const directResult = await runtime.generateFromInput(
|
|
339
|
+
ctx,
|
|
340
|
+
event,
|
|
341
|
+
directText,
|
|
342
|
+
{
|
|
343
|
+
send: true,
|
|
344
|
+
},
|
|
345
|
+
);
|
|
346
|
+
if (!directResult.ok && directResult.shouldNotice) {
|
|
347
|
+
await notifyByAIRuntime(
|
|
348
|
+
event,
|
|
349
|
+
directResult.noticeInstruction ||
|
|
350
|
+
`meme 生成失败:${directResult.message}`,
|
|
351
|
+
directResult.message,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
} catch (error) {
|
|
355
|
+
await notifyByAIRuntime(
|
|
356
|
+
event,
|
|
357
|
+
`meme 插件执行失败,错误信息:${error}。请自然告诉用户当前处理失败并建议稍后重试`,
|
|
358
|
+
`meme 插件执行失败:${error}`,
|
|
359
|
+
error,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
return () => {
|
|
365
|
+
for (const dispose of disposers) {
|
|
366
|
+
dispose();
|
|
367
|
+
}
|
|
368
|
+
resetMemeRuntimeState();
|
|
369
|
+
ctx.logger.info("meme 插件已卸载");
|
|
370
|
+
};
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
export default memePlugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mioku-plugin-meme",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "基于 meme-generator API 的表情包制作插件",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"mioku"
|
|
8
|
+
],
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/mioku-lab/mioku-plugin-meme.git"
|
|
12
|
+
},
|
|
13
|
+
"mioku": {
|
|
14
|
+
"services": [
|
|
15
|
+
"config",
|
|
16
|
+
"ai"
|
|
17
|
+
],
|
|
18
|
+
"help": {
|
|
19
|
+
"title": "Meme 表情包",
|
|
20
|
+
"description": "接入 meme-generator API 的表情包制作插件",
|
|
21
|
+
"commands": [
|
|
22
|
+
{
|
|
23
|
+
"cmd": "/meme 菜单",
|
|
24
|
+
"desc": "查看已同步的表情菜单",
|
|
25
|
+
"usage": "/meme 菜单",
|
|
26
|
+
"role": "member"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"cmd": "/meme 搜索 <关键词>",
|
|
30
|
+
"desc": "搜索相关表情关键词",
|
|
31
|
+
"usage": "/meme 搜索 摸",
|
|
32
|
+
"role": "member"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"cmd": "/meme 详情 <关键词>",
|
|
36
|
+
"desc": "查看表情参数和预览",
|
|
37
|
+
"usage": "/meme 详情 摸头",
|
|
38
|
+
"role": "member"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"cmd": "/随机表情",
|
|
42
|
+
"desc": "随机生成一个头像类表情",
|
|
43
|
+
"usage": "/随机表情",
|
|
44
|
+
"role": "member"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"cmd": "<表情关键词> [文本1/文本2][#参数]",
|
|
48
|
+
"desc": "直接按关键词生成表情",
|
|
49
|
+
"usage": "摸头 小明#圆",
|
|
50
|
+
"role": "member"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"cmd": "/meme 更新",
|
|
54
|
+
"desc": "刷新远端表情缓存",
|
|
55
|
+
"usage": "/meme 更新",
|
|
56
|
+
"role": "owner"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
package/runtime.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MemePluginRuntime } from "./shared";
|
|
2
|
+
import {
|
|
3
|
+
getPluginRuntimeState,
|
|
4
|
+
resetPluginRuntimeState,
|
|
5
|
+
setPluginRuntimeState,
|
|
6
|
+
} from "../../src";
|
|
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<MemeRuntimeState>(MEME_PLUGIN_NAME, nextState);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getMemeRuntimeState(): MemeRuntimeState {
|
|
21
|
+
return getPluginRuntimeState<MemeRuntimeState>(MEME_PLUGIN_NAME);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resetMemeRuntimeState(): void {
|
|
25
|
+
resetPluginRuntimeState(MEME_PLUGIN_NAME);
|
|
26
|
+
}
|