koishi-plugin-smmcat-yeyechat 0.0.1
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/config.d.ts +18 -0
- package/lib/emoji.d.ts +2 -0
- package/lib/index.d.ts +51 -0
- package/lib/index.js +572 -0
- package/lib/message.d.ts +4 -0
- package/lib/prompt.d.ts +11 -0
- package/lib/service.d.ts +15 -0
- package/lib/storage.d.ts +10 -0
- package/lib/trigger.d.ts +9 -0
- package/lib/types.d.ts +30 -0
- package/package.json +20 -0
- package/readme.md +5 -0
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Schema } from 'koishi';
|
|
2
|
+
import { EmotionMap } from './types';
|
|
3
|
+
export interface Config {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
token: string;
|
|
6
|
+
model: string;
|
|
7
|
+
persona: string;
|
|
8
|
+
shortMemoryLimit: number;
|
|
9
|
+
longMemoryLimit: number;
|
|
10
|
+
enableByDefault: boolean;
|
|
11
|
+
groupManagerAuthority: number;
|
|
12
|
+
timeZone: string;
|
|
13
|
+
randomReplyRate: number;
|
|
14
|
+
passiveReplyCooldown: number;
|
|
15
|
+
triggerKeywords: string[];
|
|
16
|
+
emojiMap: EmotionMap;
|
|
17
|
+
}
|
|
18
|
+
export declare const Config: Schema<Config>;
|
package/lib/emoji.d.ts
ADDED
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Context, Schema } from 'koishi';
|
|
2
|
+
export declare const name = "smmcat-yeyechat";
|
|
3
|
+
export declare const inject: string[];
|
|
4
|
+
export type ChatRole = 'system' | 'user' | 'assistant';
|
|
5
|
+
export interface ChatInfo {
|
|
6
|
+
role: ChatRole;
|
|
7
|
+
content: string;
|
|
8
|
+
}
|
|
9
|
+
export type EmotionMap = Record<string, string[]>;
|
|
10
|
+
export interface UserDetailInfo {
|
|
11
|
+
userId?: string;
|
|
12
|
+
nickname: string;
|
|
13
|
+
likes: string[];
|
|
14
|
+
favorability: number;
|
|
15
|
+
localChat: ChatInfo[];
|
|
16
|
+
longMemory: string[];
|
|
17
|
+
isBan: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface GroupChatConfig {
|
|
20
|
+
id: string;
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface IncomingMessage {
|
|
24
|
+
text: string;
|
|
25
|
+
images: string[];
|
|
26
|
+
atMe: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface Config {
|
|
29
|
+
baseUrl: string;
|
|
30
|
+
token: string;
|
|
31
|
+
model: string;
|
|
32
|
+
persona: string;
|
|
33
|
+
shortMemoryLimit: number;
|
|
34
|
+
longMemoryLimit: number;
|
|
35
|
+
enableByDefault: boolean;
|
|
36
|
+
groupManagerAuthority: number;
|
|
37
|
+
timeZone: string;
|
|
38
|
+
randomReplyRate: number;
|
|
39
|
+
passiveReplyCooldown: number;
|
|
40
|
+
triggerKeywords: string[];
|
|
41
|
+
emojiMap: EmotionMap;
|
|
42
|
+
collectWindow: number;
|
|
43
|
+
queueLimit: number;
|
|
44
|
+
queueSweepInterval: number;
|
|
45
|
+
queueExpireAfter: number;
|
|
46
|
+
replyLeadDelay: number;
|
|
47
|
+
replyCharDelay: number;
|
|
48
|
+
replyMaxSegmentLength: number;
|
|
49
|
+
}
|
|
50
|
+
export declare const Config: Schema<Config>;
|
|
51
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name2 in all)
|
|
8
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
Config: () => Config,
|
|
24
|
+
apply: () => apply,
|
|
25
|
+
inject: () => inject,
|
|
26
|
+
name: () => name
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(src_exports);
|
|
29
|
+
var import_koishi = require("koishi");
|
|
30
|
+
var name = "smmcat-yeyechat";
|
|
31
|
+
var inject = ["database"];
|
|
32
|
+
var Config = import_koishi.Schema.object({
|
|
33
|
+
baseUrl: import_koishi.Schema.string().default("").description("接口基础地址"),
|
|
34
|
+
token: import_koishi.Schema.string().default("").description("接口令牌"),
|
|
35
|
+
model: import_koishi.Schema.string().default("").description("模型名称"),
|
|
36
|
+
persona: import_koishi.Schema.string().default("你叫夜夜酱,是一个活跃在群聊里的拟人化聊天角色。你说话自然、真诚、带一点点情绪起伏,但不要夸张。你会认真接住别人的话,并且像真人一样分成几行回复。不要一次写成很长的段落。").description("人物设定"),
|
|
37
|
+
shortMemoryLimit: import_koishi.Schema.natural().default(15).description("短期记忆条数"),
|
|
38
|
+
longMemoryLimit: import_koishi.Schema.natural().default(30).description("长期记忆条数"),
|
|
39
|
+
enableByDefault: import_koishi.Schema.boolean().default(false).description("新群是否默认开启"),
|
|
40
|
+
groupManagerAuthority: import_koishi.Schema.natural().default(3).description("开关群聊对话权限"),
|
|
41
|
+
timeZone: import_koishi.Schema.string().default("").description("本地时区"),
|
|
42
|
+
randomReplyRate: import_koishi.Schema.number().default(0.02).min(0).max(1).description("无触发时的随机发言概率"),
|
|
43
|
+
passiveReplyCooldown: import_koishi.Schema.natural().default(300).description("随机发言冷却秒数"),
|
|
44
|
+
triggerKeywords: import_koishi.Schema.array(String).default(["夜夜酱", "yeye", "在吗", "帮我"]).description("触发关键词"),
|
|
45
|
+
emojiMap: import_koishi.Schema.dict(import_koishi.Schema.array(String)).default({ 开心: [], 难过: [] }).description("表情映射"),
|
|
46
|
+
collectWindow: import_koishi.Schema.natural().default(6e3).description("收集窗口毫秒数"),
|
|
47
|
+
queueLimit: import_koishi.Schema.natural().default(24).description("单群最大排队数"),
|
|
48
|
+
queueSweepInterval: import_koishi.Schema.natural().default(1200).description("排队扫描间隔毫秒数"),
|
|
49
|
+
queueExpireAfter: import_koishi.Schema.natural().default(15 * 60 * 1e3).description("队列过期毫秒数"),
|
|
50
|
+
replyLeadDelay: import_koishi.Schema.natural().default(500).description("首段发送前延迟毫秒数"),
|
|
51
|
+
replyCharDelay: import_koishi.Schema.natural().default(45).description("按字数增加的发送间隔"),
|
|
52
|
+
replyMaxSegmentLength: import_koishi.Schema.natural().default(24).description("单段最大字数")
|
|
53
|
+
});
|
|
54
|
+
var favorabilityDict = {
|
|
55
|
+
0: "陌生",
|
|
56
|
+
20: "认识",
|
|
57
|
+
30: "熟悉",
|
|
58
|
+
50: "朋友",
|
|
59
|
+
60: "好感",
|
|
60
|
+
100: "亲近",
|
|
61
|
+
130: "挚友"
|
|
62
|
+
};
|
|
63
|
+
function normalizeText(text) {
|
|
64
|
+
return text.replace(/\s+/g, " ").trim();
|
|
65
|
+
}
|
|
66
|
+
__name(normalizeText, "normalizeText");
|
|
67
|
+
function collectKeywords(text, keywords) {
|
|
68
|
+
const normalized = text.toLowerCase();
|
|
69
|
+
return keywords.some((keyword) => normalized.includes(keyword.toLowerCase()));
|
|
70
|
+
}
|
|
71
|
+
__name(collectKeywords, "collectKeywords");
|
|
72
|
+
function extractMessage(session) {
|
|
73
|
+
const text = import_koishi.h.select(session.elements, "text").map((node) => node.attrs.content).join("").trim();
|
|
74
|
+
const images = import_koishi.h.select(session.elements, "img").map((node) => node.attrs.src).filter(Boolean);
|
|
75
|
+
const atNodes = import_koishi.h.select(session.elements, "at");
|
|
76
|
+
const selfId = String(session.selfId ?? "");
|
|
77
|
+
const atMe = atNodes.some((node) => {
|
|
78
|
+
const attrs = node.attrs || {};
|
|
79
|
+
const target = String(attrs.id ?? attrs.qq ?? attrs.userId ?? attrs.qqId ?? "");
|
|
80
|
+
return target === selfId;
|
|
81
|
+
});
|
|
82
|
+
return { text, images, atMe };
|
|
83
|
+
}
|
|
84
|
+
__name(extractMessage, "extractMessage");
|
|
85
|
+
function buildUserContent(message) {
|
|
86
|
+
const text = normalizeText(message.text);
|
|
87
|
+
const imageText = message.images.length ? message.images.map((url) => `[图片] ${url}`).join("\n") : "";
|
|
88
|
+
return [text, imageText].filter(Boolean).join("\n").trim();
|
|
89
|
+
}
|
|
90
|
+
__name(buildUserContent, "buildUserContent");
|
|
91
|
+
function renderEmojis(text, emojiMap) {
|
|
92
|
+
const children = [];
|
|
93
|
+
const pattern = /%([^%\s]{1,20})%/g;
|
|
94
|
+
let lastIndex = 0;
|
|
95
|
+
let hasImage = false;
|
|
96
|
+
text.replace(pattern, (match, key, index) => {
|
|
97
|
+
if (index > lastIndex) children.push(text.slice(lastIndex, index));
|
|
98
|
+
const candidates = emojiMap[key];
|
|
99
|
+
if (candidates?.length) {
|
|
100
|
+
hasImage = true;
|
|
101
|
+
const url = candidates[Math.floor(Math.random() * candidates.length)];
|
|
102
|
+
children.push(import_koishi.h.image(url));
|
|
103
|
+
}
|
|
104
|
+
lastIndex = index + match.length;
|
|
105
|
+
return match;
|
|
106
|
+
});
|
|
107
|
+
if (lastIndex < text.length) children.push(text.slice(lastIndex));
|
|
108
|
+
if (!hasImage) return children.join("");
|
|
109
|
+
return children;
|
|
110
|
+
}
|
|
111
|
+
__name(renderEmojis, "renderEmojis");
|
|
112
|
+
function splitReplySegments(text, maxLength) {
|
|
113
|
+
const normalized = text.replace(/\r/g, "").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
114
|
+
if (!normalized) return [];
|
|
115
|
+
const paragraphs = normalized.split(/\n+/).map((line) => line.trim()).filter(Boolean);
|
|
116
|
+
const segments = [];
|
|
117
|
+
const sentencePattern = /([^。!?!?;;…]+[。!?!?;;…]?)/g;
|
|
118
|
+
for (const paragraph of paragraphs) {
|
|
119
|
+
const pieces = paragraph.match(sentencePattern) || [paragraph];
|
|
120
|
+
let buffer = "";
|
|
121
|
+
for (const rawPiece of pieces) {
|
|
122
|
+
const piece = rawPiece.trim();
|
|
123
|
+
if (!piece) continue;
|
|
124
|
+
if (!buffer) {
|
|
125
|
+
buffer = piece;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (buffer.length + piece.length <= maxLength) {
|
|
129
|
+
buffer += piece;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
segments.push(buffer);
|
|
133
|
+
buffer = piece;
|
|
134
|
+
}
|
|
135
|
+
if (buffer) segments.push(buffer);
|
|
136
|
+
}
|
|
137
|
+
if (!segments.length) return [normalized];
|
|
138
|
+
const output = [];
|
|
139
|
+
for (const segment of segments) {
|
|
140
|
+
if (segment.length <= maxLength) {
|
|
141
|
+
output.push(segment);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
for (let index = 0; index < segment.length; index += maxLength) {
|
|
145
|
+
output.push(segment.slice(index, index + maxLength));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return output.filter(Boolean);
|
|
149
|
+
}
|
|
150
|
+
__name(splitReplySegments, "splitReplySegments");
|
|
151
|
+
function parseAttributeBlock(text) {
|
|
152
|
+
const result = {};
|
|
153
|
+
const regex = /%\[([^\]]+)\]:([^%\n]+)%?/g;
|
|
154
|
+
let match;
|
|
155
|
+
let cleanedText = text;
|
|
156
|
+
while ((match = regex.exec(text)) !== null) {
|
|
157
|
+
const key = match[1].trim();
|
|
158
|
+
let value = match[2].trim();
|
|
159
|
+
if (value.endsWith("%")) value = value.slice(0, -1);
|
|
160
|
+
try {
|
|
161
|
+
if (value.startsWith('"') || value === "true" || value === "false" || value === "null" || !Number.isNaN(Number(value)) || value.startsWith("{") || value.startsWith("[")) {
|
|
162
|
+
result[key] = JSON.parse(value);
|
|
163
|
+
} else {
|
|
164
|
+
result[key] = value;
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
result[key] = value;
|
|
168
|
+
}
|
|
169
|
+
cleanedText = cleanedText.replace(match[0], "");
|
|
170
|
+
}
|
|
171
|
+
return { attributes: result, cleanedText: cleanedText.trim() };
|
|
172
|
+
}
|
|
173
|
+
__name(parseAttributeBlock, "parseAttributeBlock");
|
|
174
|
+
function applyAttributeBlock(data, attributes) {
|
|
175
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
176
|
+
if (key === "nickname" && typeof value === "string") {
|
|
177
|
+
data.nickname = value.slice(0, 30);
|
|
178
|
+
}
|
|
179
|
+
if (key === "favorability") {
|
|
180
|
+
const next = Number(value);
|
|
181
|
+
if (Number.isFinite(next)) data.favorability = Math.max(0, Math.min(150, Math.round(next)));
|
|
182
|
+
}
|
|
183
|
+
if (key === "likes") {
|
|
184
|
+
const values = Array.isArray(value) ? value : String(value).split(/[、,,\s]+/);
|
|
185
|
+
data.likes = Array.from(/* @__PURE__ */ new Set([
|
|
186
|
+
...data.likes,
|
|
187
|
+
...values.map((item) => String(item).trim()).filter(Boolean)
|
|
188
|
+
])).slice(0, 30);
|
|
189
|
+
}
|
|
190
|
+
if (key === "longMemory") {
|
|
191
|
+
const values = Array.isArray(value) ? value : [value];
|
|
192
|
+
data.longMemory = Array.from(/* @__PURE__ */ new Set([
|
|
193
|
+
...data.longMemory,
|
|
194
|
+
...values.map((item) => String(item).trim()).filter(Boolean)
|
|
195
|
+
])).slice(0, 30);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
__name(applyAttributeBlock, "applyAttributeBlock");
|
|
200
|
+
function buildSystemPrompt(config) {
|
|
201
|
+
const emojiKeys = Object.keys(config.emojiMap || {}).map((key) => `%${key}%`).join("、") || "无";
|
|
202
|
+
return [
|
|
203
|
+
{ role: "system", content: config.persona },
|
|
204
|
+
{ role: "system", content: "回复要像真人群友,不要过长,不要一次性写成一整大段。尽量按思路自然分行,每行短一点,口语化一点。" },
|
|
205
|
+
{ role: "system", content: '如果需要记录用户状态,可以只在正文末尾加属性块,例如 %[favorability]:23% 或 %[likes]:["奶茶"]%。属性块不会直接发给用户。' },
|
|
206
|
+
{ role: "system", content: "优先根据上下文、长期记忆、短期记忆、好感度和本地时间来回复,不要编造当前日期。" },
|
|
207
|
+
{ role: "system", content: `可用的表情关键字:${emojiKeys}。` }
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
__name(buildSystemPrompt, "buildSystemPrompt");
|
|
211
|
+
function buildLocalTime(config) {
|
|
212
|
+
const options = {
|
|
213
|
+
year: "numeric",
|
|
214
|
+
month: "2-digit",
|
|
215
|
+
day: "2-digit",
|
|
216
|
+
hour: "2-digit",
|
|
217
|
+
minute: "2-digit",
|
|
218
|
+
second: "2-digit",
|
|
219
|
+
hour12: false,
|
|
220
|
+
weekday: "long"
|
|
221
|
+
};
|
|
222
|
+
if (config.timeZone) options.timeZone = config.timeZone;
|
|
223
|
+
return {
|
|
224
|
+
role: "system",
|
|
225
|
+
content: `【本地时间】${new Intl.DateTimeFormat("zh-CN", options).format(/* @__PURE__ */ new Date())}`
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
__name(buildLocalTime, "buildLocalTime");
|
|
229
|
+
function buildUserDetail(data) {
|
|
230
|
+
const favorabilityLabel = Object.keys(favorabilityDict).reverse().find((value) => data.favorability >= Number(value)) || "0";
|
|
231
|
+
return {
|
|
232
|
+
role: "system",
|
|
233
|
+
content: [
|
|
234
|
+
"【用户信息】",
|
|
235
|
+
`与你的关系:${data.favorability}(${favorabilityDict[Number(favorabilityLabel)]})`,
|
|
236
|
+
`对方称呼:${data.nickname || "你"}`,
|
|
237
|
+
`对方偏好:${data.likes.length ? data.likes.join("、") : "无"}`,
|
|
238
|
+
`长期记忆:${data.longMemory.length ? data.longMemory.join(";") : "无"}`
|
|
239
|
+
].join("\n")
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
__name(buildUserDetail, "buildUserDetail");
|
|
243
|
+
function buildRequestMessages(config, data) {
|
|
244
|
+
return [
|
|
245
|
+
...buildSystemPrompt(config),
|
|
246
|
+
buildLocalTime(config),
|
|
247
|
+
buildUserDetail(data),
|
|
248
|
+
...data.localChat
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
__name(buildRequestMessages, "buildRequestMessages");
|
|
252
|
+
function createStorage(ctx, config) {
|
|
253
|
+
const shortMemoryLimit = Math.max(1, config.shortMemoryLimit || 15);
|
|
254
|
+
const longMemoryLimit = Math.max(1, config.longMemoryLimit || 30);
|
|
255
|
+
ctx.database.extend("smm_ai_chat_data", {
|
|
256
|
+
userId: "string",
|
|
257
|
+
nickname: "string",
|
|
258
|
+
likes: "array",
|
|
259
|
+
favorability: "integer",
|
|
260
|
+
localChat: "json",
|
|
261
|
+
longMemory: "json",
|
|
262
|
+
isBan: "boolean"
|
|
263
|
+
}, {
|
|
264
|
+
primary: "userId",
|
|
265
|
+
autoInc: false
|
|
266
|
+
});
|
|
267
|
+
ctx.database.extend("smm_ai_chat_group_config", {
|
|
268
|
+
id: "string",
|
|
269
|
+
enabled: "boolean"
|
|
270
|
+
}, {
|
|
271
|
+
primary: "id",
|
|
272
|
+
autoInc: false
|
|
273
|
+
});
|
|
274
|
+
const clampMemory = /* @__PURE__ */ __name((data) => {
|
|
275
|
+
while (data.localChat.length > shortMemoryLimit) data.localChat.shift();
|
|
276
|
+
while (data.longMemory.length > longMemoryLimit) data.longMemory.shift();
|
|
277
|
+
}, "clampMemory");
|
|
278
|
+
return {
|
|
279
|
+
clampMemory,
|
|
280
|
+
async getUser(userId) {
|
|
281
|
+
const [data] = await ctx.database.get("smm_ai_chat_data", { userId });
|
|
282
|
+
if (!data) {
|
|
283
|
+
const init = {
|
|
284
|
+
userId,
|
|
285
|
+
nickname: "",
|
|
286
|
+
likes: [],
|
|
287
|
+
favorability: 20,
|
|
288
|
+
localChat: [],
|
|
289
|
+
longMemory: [],
|
|
290
|
+
isBan: false
|
|
291
|
+
};
|
|
292
|
+
await ctx.database.create("smm_ai_chat_data", init);
|
|
293
|
+
return init;
|
|
294
|
+
}
|
|
295
|
+
data.likes ||= [];
|
|
296
|
+
data.localChat ||= [];
|
|
297
|
+
data.longMemory ||= [];
|
|
298
|
+
clampMemory(data);
|
|
299
|
+
return data;
|
|
300
|
+
},
|
|
301
|
+
async saveUser(data) {
|
|
302
|
+
const { userId } = data;
|
|
303
|
+
const next = { ...data };
|
|
304
|
+
delete next.userId;
|
|
305
|
+
await ctx.database.set("smm_ai_chat_data", { userId }, next);
|
|
306
|
+
},
|
|
307
|
+
async getGroup(groupId) {
|
|
308
|
+
const [data] = await ctx.database.get("smm_ai_chat_group_config", { id: groupId });
|
|
309
|
+
return data;
|
|
310
|
+
},
|
|
311
|
+
async setGroupEnabled(groupId, enabled) {
|
|
312
|
+
const [data] = await ctx.database.get("smm_ai_chat_group_config", { id: groupId });
|
|
313
|
+
if (data) await ctx.database.set("smm_ai_chat_group_config", { id: groupId }, { enabled });
|
|
314
|
+
else await ctx.database.create("smm_ai_chat_group_config", { id: groupId, enabled });
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
__name(createStorage, "createStorage");
|
|
319
|
+
function createTriggerController(config) {
|
|
320
|
+
const state = /* @__PURE__ */ new Map();
|
|
321
|
+
const key = /* @__PURE__ */ __name((session) => {
|
|
322
|
+
const targetId = session.guildId || session.channelId;
|
|
323
|
+
return targetId ? `${session.platform}:${targetId}` : "";
|
|
324
|
+
}, "key");
|
|
325
|
+
const getState = /* @__PURE__ */ __name((session) => {
|
|
326
|
+
const id = key(session);
|
|
327
|
+
if (!id) return null;
|
|
328
|
+
if (!state.has(id)) state.set(id, { lastPassiveReplyAt: 0 });
|
|
329
|
+
return state.get(id) || null;
|
|
330
|
+
}, "getState");
|
|
331
|
+
const shouldPassivelyReply = /* @__PURE__ */ __name((session) => {
|
|
332
|
+
const current = getState(session);
|
|
333
|
+
if (!current) return false;
|
|
334
|
+
const now = Date.now();
|
|
335
|
+
if (now - current.lastPassiveReplyAt < config.passiveReplyCooldown * 1e3) return false;
|
|
336
|
+
if (Math.random() > config.randomReplyRate) return false;
|
|
337
|
+
current.lastPassiveReplyAt = now;
|
|
338
|
+
return true;
|
|
339
|
+
}, "shouldPassivelyReply");
|
|
340
|
+
const shouldOpenConversation = /* @__PURE__ */ __name((session, text, atMe) => {
|
|
341
|
+
if (!session.guildId && !session.channelId) return true;
|
|
342
|
+
if (atMe) return true;
|
|
343
|
+
if (collectKeywords(text, config.triggerKeywords || [])) return true;
|
|
344
|
+
return shouldPassivelyReply(session);
|
|
345
|
+
}, "shouldOpenConversation");
|
|
346
|
+
return { key, shouldOpenConversation };
|
|
347
|
+
}
|
|
348
|
+
__name(createTriggerController, "createTriggerController");
|
|
349
|
+
function createReplyEngine(ctx, config, storage) {
|
|
350
|
+
const authHeader = config.token.startsWith("Bearer ") ? config.token : `Bearer ${config.token}`;
|
|
351
|
+
const composeReply = /* @__PURE__ */ __name(async (item, data, text) => {
|
|
352
|
+
data.localChat.push({ role: "user", content: text });
|
|
353
|
+
storage.clampMemory(data);
|
|
354
|
+
try {
|
|
355
|
+
const result = await ctx.http.post(config.baseUrl + "/v1/chat/completions", {
|
|
356
|
+
model: config.model,
|
|
357
|
+
messages: buildRequestMessages(config, data),
|
|
358
|
+
max_tokens: 2048,
|
|
359
|
+
stream: false
|
|
360
|
+
}, {
|
|
361
|
+
headers: { Authorization: authHeader }
|
|
362
|
+
});
|
|
363
|
+
const content = result?.choices?.[0]?.message?.content || result?.choices?.[0]?.text || "";
|
|
364
|
+
if (!content) return null;
|
|
365
|
+
const parsed = parseAttributeBlock(String(content));
|
|
366
|
+
applyAttributeBlock(data, parsed.attributes);
|
|
367
|
+
data.localChat.push({ role: "assistant", content: parsed.cleanedText });
|
|
368
|
+
storage.clampMemory(data);
|
|
369
|
+
return {
|
|
370
|
+
text: parsed.cleanedText,
|
|
371
|
+
segments: splitReplySegments(parsed.cleanedText, config.replyMaxSegmentLength)
|
|
372
|
+
};
|
|
373
|
+
} catch (error) {
|
|
374
|
+
console.log(error);
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
}, "composeReply");
|
|
378
|
+
const sendReply = /* @__PURE__ */ __name(async (session, plan) => {
|
|
379
|
+
const segments = plan.segments.length ? plan.segments : [plan.text];
|
|
380
|
+
if (!segments.length) return;
|
|
381
|
+
const firstDelay = Math.max(config.replyLeadDelay, Math.min(2e3, 350 + segments[0].length * config.replyCharDelay));
|
|
382
|
+
await session.cancelQueued(firstDelay);
|
|
383
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
384
|
+
const segment = segments[index];
|
|
385
|
+
const delay = index < segments.length - 1 ? Math.max(160, Math.min(1400, 180 + segment.length * config.replyCharDelay)) : 0;
|
|
386
|
+
await session.sendQueued(renderEmojis(segment, config.emojiMap), delay);
|
|
387
|
+
}
|
|
388
|
+
}, "sendReply");
|
|
389
|
+
return { composeReply, sendReply };
|
|
390
|
+
}
|
|
391
|
+
__name(createReplyEngine, "createReplyEngine");
|
|
392
|
+
function createConversationQueue(ctx, config, storage, trigger, replyEngine) {
|
|
393
|
+
const groups = /* @__PURE__ */ new Map();
|
|
394
|
+
const groupKey = /* @__PURE__ */ __name((session) => trigger.key(session), "groupKey");
|
|
395
|
+
const itemKey = /* @__PURE__ */ __name((session) => `${groupKey(session)}:${session.userId}`, "itemKey");
|
|
396
|
+
const ensureGroup = /* @__PURE__ */ __name((key) => {
|
|
397
|
+
if (!groups.has(key)) groups.set(key, { active: false, items: /* @__PURE__ */ new Map() });
|
|
398
|
+
return groups.get(key);
|
|
399
|
+
}, "ensureGroup");
|
|
400
|
+
const scoreItem = /* @__PURE__ */ __name((item, now = Date.now()) => {
|
|
401
|
+
const ageScore = Math.min(90, Math.floor((now - item.updatedAt) / 1e3));
|
|
402
|
+
const waitScore = Math.min(60, Math.floor((now - item.createdAt) / 2e3));
|
|
403
|
+
return item.priority + ageScore + waitScore;
|
|
404
|
+
}, "scoreItem");
|
|
405
|
+
const pruneGroup = /* @__PURE__ */ __name((state) => {
|
|
406
|
+
const limit = Math.max(1, config.queueLimit || 24);
|
|
407
|
+
while (state.items.size > limit) {
|
|
408
|
+
const victim = [...state.items.values()].filter((item) => !item.lockedAt).sort((a, b) => scoreItem(a) - scoreItem(b) || a.updatedAt - b.updatedAt)[0];
|
|
409
|
+
if (!victim) break;
|
|
410
|
+
state.items.delete(victim.id);
|
|
411
|
+
}
|
|
412
|
+
}, "pruneGroup");
|
|
413
|
+
const queueMessage = /* @__PURE__ */ __name(async (session, message, force = false) => {
|
|
414
|
+
const gKey = groupKey(session);
|
|
415
|
+
if (!gKey) return false;
|
|
416
|
+
const group = ensureGroup(gKey);
|
|
417
|
+
const key = itemKey(session);
|
|
418
|
+
const now = Date.now();
|
|
419
|
+
const content = buildUserContent(message);
|
|
420
|
+
const existing = group.items.get(key);
|
|
421
|
+
const user = await storage.getUser(session.userId);
|
|
422
|
+
if (user.isBan) return false;
|
|
423
|
+
if (existing) {
|
|
424
|
+
existing.updatedAt = now;
|
|
425
|
+
existing.dirty = true;
|
|
426
|
+
existing.turns += 1;
|
|
427
|
+
if (content) existing.textParts.push(content);
|
|
428
|
+
if (message.images.length) existing.imageParts.push(...message.images);
|
|
429
|
+
existing.priority = Math.max(existing.priority, buildPriority(session, message, existing.turns, user.favorability));
|
|
430
|
+
pruneGroup(group);
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
if (!force && !trigger.shouldOpenConversation(session, message.text, message.atMe)) return false;
|
|
434
|
+
const created = {
|
|
435
|
+
id: key,
|
|
436
|
+
groupKey: gKey,
|
|
437
|
+
userId: session.userId,
|
|
438
|
+
session,
|
|
439
|
+
createdAt: now,
|
|
440
|
+
updatedAt: now,
|
|
441
|
+
lockedAt: 0,
|
|
442
|
+
priority: buildPriority(session, message, 1, user.favorability),
|
|
443
|
+
dirty: false,
|
|
444
|
+
turns: 1,
|
|
445
|
+
textParts: content ? [content] : [],
|
|
446
|
+
imageParts: [...message.images]
|
|
447
|
+
};
|
|
448
|
+
group.items.set(key, created);
|
|
449
|
+
pruneGroup(group);
|
|
450
|
+
return true;
|
|
451
|
+
}, "queueMessage");
|
|
452
|
+
const buildPriority = /* @__PURE__ */ __name((session, message, turns, favorability = 20) => {
|
|
453
|
+
const base = message.atMe ? 120 : 0;
|
|
454
|
+
const keyword = collectKeywords(message.text, config.triggerKeywords || []) ? 80 : 0;
|
|
455
|
+
const favorabilityBonus = Math.min(260, Math.max(0, favorability) * 2);
|
|
456
|
+
const turnBonus = Math.min(25, turns * 3);
|
|
457
|
+
const randomBias = Math.floor(Math.random() * 5);
|
|
458
|
+
return base + keyword + favorabilityBonus + turnBonus + randomBias;
|
|
459
|
+
}, "buildPriority");
|
|
460
|
+
const takeNext = /* @__PURE__ */ __name((state) => {
|
|
461
|
+
const now = Date.now();
|
|
462
|
+
const readyItems = [...state.items.values()].filter((item) => !item.lockedAt).filter((item) => now - item.updatedAt >= config.collectWindow).sort((a, b) => scoreItem(b, now) - scoreItem(a, now) || a.updatedAt - b.updatedAt);
|
|
463
|
+
return readyItems[0] || null;
|
|
464
|
+
}, "takeNext");
|
|
465
|
+
const finishItem = /* @__PURE__ */ __name((state, item, snapshot) => {
|
|
466
|
+
if (item.updatedAt > item.lockedAt) {
|
|
467
|
+
item.textParts = item.textParts.slice(snapshot.textCount);
|
|
468
|
+
item.imageParts = item.imageParts.slice(snapshot.imageCount);
|
|
469
|
+
item.lockedAt = 0;
|
|
470
|
+
item.dirty = false;
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
state.items.delete(item.id);
|
|
474
|
+
}, "finishItem");
|
|
475
|
+
const turn = /* @__PURE__ */ __name(async (state, item) => {
|
|
476
|
+
state.active = true;
|
|
477
|
+
item.lockedAt = Date.now();
|
|
478
|
+
const snapshot = { textCount: item.textParts.length, imageCount: item.imageParts.length };
|
|
479
|
+
try {
|
|
480
|
+
const data = await storage.getUser(item.userId);
|
|
481
|
+
if (!data || data.isBan) {
|
|
482
|
+
state.items.delete(item.id);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const userText = item.textParts.join("\n").trim();
|
|
486
|
+
if (!userText) {
|
|
487
|
+
state.items.delete(item.id);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const plan = await replyEngine.composeReply(item, data, userText);
|
|
491
|
+
if (!plan?.text) {
|
|
492
|
+
state.items.delete(item.id);
|
|
493
|
+
await storage.saveUser(data);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
await replyEngine.sendReply(item.session, plan);
|
|
497
|
+
await storage.saveUser(data);
|
|
498
|
+
finishItem(state, item, snapshot);
|
|
499
|
+
} catch (error) {
|
|
500
|
+
console.log(error);
|
|
501
|
+
item.lockedAt = 0;
|
|
502
|
+
} finally {
|
|
503
|
+
state.active = false;
|
|
504
|
+
}
|
|
505
|
+
}, "turn");
|
|
506
|
+
const tick = /* @__PURE__ */ __name(async () => {
|
|
507
|
+
for (const state of groups.values()) {
|
|
508
|
+
if (state.active) continue;
|
|
509
|
+
for (const item of [...state.items.values()]) {
|
|
510
|
+
if (!item.lockedAt && Date.now() - item.updatedAt > config.queueExpireAfter) {
|
|
511
|
+
state.items.delete(item.id);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const next = takeNext(state);
|
|
515
|
+
if (next) {
|
|
516
|
+
void turn(state, next).catch((error) => {
|
|
517
|
+
console.log(error);
|
|
518
|
+
state.active = false;
|
|
519
|
+
next.lockedAt = 0;
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}, "tick");
|
|
524
|
+
const timer = setInterval(() => {
|
|
525
|
+
void tick();
|
|
526
|
+
}, Math.max(500, config.queueSweepInterval || 1200));
|
|
527
|
+
ctx.on("dispose", () => clearInterval(timer));
|
|
528
|
+
return {
|
|
529
|
+
queueMessage,
|
|
530
|
+
groupKey,
|
|
531
|
+
isEnabled: /* @__PURE__ */ __name(async (session) => {
|
|
532
|
+
const key = groupKey(session);
|
|
533
|
+
if (!key) return true;
|
|
534
|
+
const data = await storage.getGroup(key);
|
|
535
|
+
return data?.enabled ?? config.enableByDefault;
|
|
536
|
+
}, "isEnabled")
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
__name(createConversationQueue, "createConversationQueue");
|
|
540
|
+
function apply(ctx, config) {
|
|
541
|
+
const storage = createStorage(ctx, config);
|
|
542
|
+
const trigger = createTriggerController(config);
|
|
543
|
+
const replyEngine = createReplyEngine(ctx, config, storage);
|
|
544
|
+
const queue = createConversationQueue(ctx, config, storage, trigger, replyEngine);
|
|
545
|
+
const setGroupSpeaking = /* @__PURE__ */ __name(async (session, enabled) => {
|
|
546
|
+
if (!session) return;
|
|
547
|
+
const id = queue.groupKey(session);
|
|
548
|
+
if (!id) return "这里只能在群聊或频道里设置对话开关。";
|
|
549
|
+
await storage.setGroupEnabled(id, enabled);
|
|
550
|
+
return enabled ? "好,我开始在本群说话。" : "好,我先闭嘴啦。";
|
|
551
|
+
}, "setGroupSpeaking");
|
|
552
|
+
ctx.command("夜夜说话", "开启夜夜酱在本群的对话", { authority: config.groupManagerAuthority }).action(async ({ session }) => setGroupSpeaking(session, true));
|
|
553
|
+
ctx.command("夜夜闭嘴", "关闭夜夜酱在本群的对话", { authority: config.groupManagerAuthority }).action(async ({ session }) => setGroupSpeaking(session, false));
|
|
554
|
+
ctx.middleware(async (session, next) => {
|
|
555
|
+
if (session.userId === session.selfId) return next();
|
|
556
|
+
const message = extractMessage(session);
|
|
557
|
+
if (!message.text && !message.images.length && !message.atMe) return next();
|
|
558
|
+
const enabled = await queue.isEnabled(session);
|
|
559
|
+
if (!enabled) return next();
|
|
560
|
+
const accepted = await queue.queueMessage(session, message);
|
|
561
|
+
if (!accepted) return next();
|
|
562
|
+
return next();
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
__name(apply, "apply");
|
|
566
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
567
|
+
0 && (module.exports = {
|
|
568
|
+
Config,
|
|
569
|
+
apply,
|
|
570
|
+
inject,
|
|
571
|
+
name
|
|
572
|
+
});
|
package/lib/message.d.ts
ADDED
package/lib/prompt.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Config } from './config';
|
|
2
|
+
import { ChatInfo, UserDetailInfo } from './types';
|
|
3
|
+
export declare function buildSystemPrompt(config: Config): ChatInfo[];
|
|
4
|
+
export declare function buildLocalTime(config: Config): ChatInfo;
|
|
5
|
+
export declare function buildUserDetail(data: UserDetailInfo): ChatInfo;
|
|
6
|
+
export declare function buildRequestMessages(config: Config, data: UserDetailInfo): ChatInfo[];
|
|
7
|
+
export declare function parseAttributeBlock(text: string): {
|
|
8
|
+
attributes: Record<string, any>;
|
|
9
|
+
cleanedText: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function applyAttributeBlock(data: UserDetailInfo, attributes: Record<string, any>): void;
|
package/lib/service.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Context, Session } from 'koishi';
|
|
2
|
+
import { Config } from './config';
|
|
3
|
+
import { UserDetailInfo } from './types';
|
|
4
|
+
export declare function createService(ctx: Context, config: Config): {
|
|
5
|
+
storage: {
|
|
6
|
+
clampMemory: (data: UserDetailInfo) => void;
|
|
7
|
+
getUser(userId: string): Promise<UserDetailInfo>;
|
|
8
|
+
saveUser(data: UserDetailInfo): Promise<void>;
|
|
9
|
+
getGroup(groupId: string): Promise<import("./types").GroupChatConfig>;
|
|
10
|
+
setGroupEnabled(groupId: string, enabled: boolean): Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
getGroupKey: (session: Session) => string;
|
|
13
|
+
isGroupAllowed: (session: Session) => Promise<boolean>;
|
|
14
|
+
getWebMsgApi: (session: Session, fn?: (err: string) => Promise<void> | void) => Promise<string | any[]>;
|
|
15
|
+
};
|
package/lib/storage.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { Config } from './config';
|
|
3
|
+
import { GroupChatConfig, UserDetailInfo } from './types';
|
|
4
|
+
export declare function createStorage(ctx: Context, config: Config): {
|
|
5
|
+
clampMemory: (data: UserDetailInfo) => void;
|
|
6
|
+
getUser(userId: string): Promise<UserDetailInfo>;
|
|
7
|
+
saveUser(data: UserDetailInfo): Promise<void>;
|
|
8
|
+
getGroup(groupId: string): Promise<GroupChatConfig>;
|
|
9
|
+
setGroupEnabled(groupId: string, enabled: boolean): Promise<void>;
|
|
10
|
+
};
|
package/lib/trigger.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Session } from 'koishi';
|
|
2
|
+
import { Config } from './config';
|
|
3
|
+
export type GroupTriggerState = {
|
|
4
|
+
lastPassiveReplyAt: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function createTriggerController(config: Config): {
|
|
7
|
+
shouldReply: (session: Session, text: string, atMe: boolean) => boolean;
|
|
8
|
+
key: (session: Session) => string;
|
|
9
|
+
};
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type ChatRole = 'system' | 'user' | 'assistant';
|
|
2
|
+
export type ChatInfo = {
|
|
3
|
+
role: ChatRole;
|
|
4
|
+
content: string;
|
|
5
|
+
};
|
|
6
|
+
export type EmotionMap = Record<string, string[]>;
|
|
7
|
+
export type UserDetailInfo = {
|
|
8
|
+
userId?: string;
|
|
9
|
+
nickname: string;
|
|
10
|
+
likes: string[];
|
|
11
|
+
favorability: number;
|
|
12
|
+
localChat: ChatInfo[];
|
|
13
|
+
longMemory: string[];
|
|
14
|
+
isBan: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type GroupChatConfig = {
|
|
17
|
+
id: string;
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type IncomingMessage = {
|
|
21
|
+
text: string;
|
|
22
|
+
images: string[];
|
|
23
|
+
atMe: boolean;
|
|
24
|
+
};
|
|
25
|
+
declare module 'koishi' {
|
|
26
|
+
interface Tables {
|
|
27
|
+
smm_ai_chat_data: UserDetailInfo;
|
|
28
|
+
smm_ai_chat_group_config: GroupChatConfig;
|
|
29
|
+
}
|
|
30
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-smmcat-yeyechat",
|
|
3
|
+
"description": "名为【夜夜酱】的聊天系统",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"typings": "lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib",
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"chatbot",
|
|
14
|
+
"koishi",
|
|
15
|
+
"plugin"
|
|
16
|
+
],
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"koishi": "^4.18.7"
|
|
19
|
+
}
|
|
20
|
+
}
|