koishi-plugin-chatluna-affinity 0.0.1 → 0.0.3
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/affinity-store.js +116 -6
- package/lib/index.d.ts +12 -2
- package/lib/index.js +13 -0
- package/lib/middleware.js +63 -8
- package/lib/providers.js +3 -2
- package/lib/schema.js +18 -6
- package/lib/tools.js +2 -1
- package/package.json +2 -2
- package/readme.md +12 -4
package/lib/affinity-store.js
CHANGED
|
@@ -5,6 +5,34 @@ function clamp(value, low, high) {
|
|
|
5
5
|
return Math.min(high, Math.max(low, Math.round(value)));
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
function formatBeijingTimestamp() {
|
|
9
|
+
try {
|
|
10
|
+
const formatter = new Intl.DateTimeFormat('en-CA', {
|
|
11
|
+
timeZone: 'Asia/Shanghai',
|
|
12
|
+
hour12: false,
|
|
13
|
+
year: 'numeric',
|
|
14
|
+
month: '2-digit',
|
|
15
|
+
day: '2-digit',
|
|
16
|
+
hour: '2-digit',
|
|
17
|
+
minute: '2-digit',
|
|
18
|
+
second: '2-digit'
|
|
19
|
+
});
|
|
20
|
+
const parts = formatter.formatToParts(new Date()).reduce((acc, part) => {
|
|
21
|
+
if (part.type !== 'literal') acc[part.type] = part.value;
|
|
22
|
+
return acc;
|
|
23
|
+
}, {});
|
|
24
|
+
const { year, month, day, hour, minute, second } = parts;
|
|
25
|
+
if (year && month && day && hour && minute && second) {
|
|
26
|
+
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
|
27
|
+
}
|
|
28
|
+
} catch (_) {
|
|
29
|
+
// ignore and fallback
|
|
30
|
+
}
|
|
31
|
+
const now = new Date();
|
|
32
|
+
const pad = (value) => value.toString().padStart(2, '0');
|
|
33
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
8
36
|
function normalizeRelationshipConfig(config) {
|
|
9
37
|
if (Array.isArray(config.relationships)) return [...config.relationships];
|
|
10
38
|
return [];
|
|
@@ -33,13 +61,41 @@ function createAffinityStore(ctx, config, log) {
|
|
|
33
61
|
|
|
34
62
|
const makeId = (platform, userId) => `${platform || 'unknown'}:${userId || 'anonymous'}`;
|
|
35
63
|
|
|
64
|
+
const resolveInitialRange = () => {
|
|
65
|
+
const rawMin = Number.isFinite(config.min) ? config.min : 0;
|
|
66
|
+
const rawMax = Number.isFinite(config.max) ? config.max : 100;
|
|
67
|
+
const boundaryMin = Math.min(rawMin, rawMax);
|
|
68
|
+
const boundaryMax = Math.max(rawMin, rawMax);
|
|
69
|
+
const fallbackCenter = clamp(Math.round((boundaryMin + boundaryMax) / 2), boundaryMin, boundaryMax);
|
|
70
|
+
let lower = Number.isFinite(config.initialRandomMin) ? config.initialRandomMin : fallbackCenter;
|
|
71
|
+
let upper = Number.isFinite(config.initialRandomMax) ? config.initialRandomMax : fallbackCenter;
|
|
72
|
+
lower = clamp(lower, boundaryMin, boundaryMax);
|
|
73
|
+
upper = clamp(upper, boundaryMin, boundaryMax);
|
|
74
|
+
if (lower > upper) [lower, upper] = [upper, lower];
|
|
75
|
+
return { low: lower, high: upper, min: boundaryMin, max: boundaryMax };
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const rollInitial = () => {
|
|
79
|
+
const range = resolveInitialRange();
|
|
80
|
+
if (range.low === range.high) return range.low;
|
|
81
|
+
const span = Math.max(0, range.high - range.low);
|
|
82
|
+
if (span <= 0) return range.low;
|
|
83
|
+
return range.low + Math.round(Math.random() * span);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const defaultInitialValue = () => {
|
|
87
|
+
const range = resolveInitialRange();
|
|
88
|
+
if (range.low === range.high) return range.low;
|
|
89
|
+
return clamp((range.low + range.high) / 2, range.min, range.max);
|
|
90
|
+
};
|
|
91
|
+
|
|
36
92
|
const readLegacy = async (platform, userId) => {
|
|
37
93
|
if (!ctx.database?.getUser) return null;
|
|
38
94
|
try {
|
|
39
95
|
const legacy = await ctx.database.getUser(platform, userId, ['affinity', 'affinityInited']);
|
|
40
96
|
if (!legacy) return null;
|
|
41
97
|
if (typeof legacy.affinity !== 'number' && !legacy.affinityInited) return null;
|
|
42
|
-
const initial = typeof legacy.affinity === 'number' ? legacy.affinity :
|
|
98
|
+
const initial = typeof legacy.affinity === 'number' ? legacy.affinity : rollInitial();
|
|
43
99
|
const now = new Date();
|
|
44
100
|
return {
|
|
45
101
|
id: makeId(platform, userId),
|
|
@@ -93,7 +149,7 @@ function createAffinityStore(ctx, config, log) {
|
|
|
93
149
|
platform: identity.platform,
|
|
94
150
|
selfId: identity.selfId || current?.selfId || '',
|
|
95
151
|
userId: identity.userId,
|
|
96
|
-
affinity: typeof value === 'number' ? value : current?.affinity ??
|
|
152
|
+
affinity: typeof value === 'number' ? value : current?.affinity ?? defaultInitialValue(),
|
|
97
153
|
affinityInited: inited ?? current?.affinityInited ?? false,
|
|
98
154
|
relation: relationText !== undefined ? relationText : current?.relation ?? '',
|
|
99
155
|
updatedAt: now,
|
|
@@ -147,6 +203,55 @@ function createAffinityStore(ctx, config, log) {
|
|
|
147
203
|
applyConfigUpdate();
|
|
148
204
|
};
|
|
149
205
|
|
|
206
|
+
const recordBlacklist = (platform, userId, note = '') => {
|
|
207
|
+
const normalizedPlatform = String(platform ?? '').trim();
|
|
208
|
+
const normalizedUser = String(userId ?? '').trim();
|
|
209
|
+
if (!normalizedPlatform || !normalizedUser) return null;
|
|
210
|
+
const list = Array.isArray(config.autoBlacklist) ? [...config.autoBlacklist] : [];
|
|
211
|
+
const index = list.findIndex((item) => item && String(item.platform ?? '').trim() === normalizedPlatform && String(item.userId ?? '').trim() === normalizedUser);
|
|
212
|
+
const entry = {
|
|
213
|
+
platform: normalizedPlatform,
|
|
214
|
+
userId: normalizedUser,
|
|
215
|
+
blockedAt: formatBeijingTimestamp(),
|
|
216
|
+
note: note != null ? String(note) : ''
|
|
217
|
+
};
|
|
218
|
+
list[index >= 0 ? index : list.length] = entry;
|
|
219
|
+
config.autoBlacklist = list;
|
|
220
|
+
applyConfigUpdate();
|
|
221
|
+
return entry;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const removeBlacklist = (platform, userId) => {
|
|
225
|
+
const normalizedPlatform = String(platform ?? '').trim();
|
|
226
|
+
const normalizedUser = String(userId ?? '').trim();
|
|
227
|
+
if (!normalizedPlatform || !normalizedUser) return false;
|
|
228
|
+
const list = Array.isArray(config.autoBlacklist) ? [...config.autoBlacklist] : [];
|
|
229
|
+
const nextList = list.filter((item) => {
|
|
230
|
+
if (!item) return false;
|
|
231
|
+
const p = String(item.platform ?? '').trim();
|
|
232
|
+
const u = String(item.userId ?? '').trim();
|
|
233
|
+
return !(p === normalizedPlatform && u === normalizedUser);
|
|
234
|
+
});
|
|
235
|
+
const changed = nextList.length !== list.length;
|
|
236
|
+
if (changed) {
|
|
237
|
+
config.autoBlacklist = nextList;
|
|
238
|
+
applyConfigUpdate();
|
|
239
|
+
}
|
|
240
|
+
return changed;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const isBlacklisted = (platform, userId) => {
|
|
244
|
+
const normalizedPlatform = String(platform ?? '').trim();
|
|
245
|
+
const normalizedUser = String(userId ?? '').trim();
|
|
246
|
+
if (!normalizedPlatform || !normalizedUser) return false;
|
|
247
|
+
return (config.autoBlacklist || []).some((item) => {
|
|
248
|
+
if (!item) return false;
|
|
249
|
+
const p = String(item.platform ?? '').trim();
|
|
250
|
+
const u = String(item.userId ?? '').trim();
|
|
251
|
+
return p === normalizedPlatform && u === normalizedUser;
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
|
|
150
255
|
const ensure = async (session, clampFn, fallbackInitial) => {
|
|
151
256
|
const platform = session?.platform;
|
|
152
257
|
const userId = session?.userId;
|
|
@@ -154,7 +259,7 @@ function createAffinityStore(ctx, config, log) {
|
|
|
154
259
|
const manual = fallbackInitial !== undefined ? null : (session?.userId ? findManualRelationship(session?.platform, session?.userId) : null);
|
|
155
260
|
const base = fallbackInitial !== undefined
|
|
156
261
|
? fallbackInitial
|
|
157
|
-
: manual?.initialAffinity ??
|
|
262
|
+
: manual?.initialAffinity ?? defaultInitialValue();
|
|
158
263
|
return { affinity: clampFn(base, config.min, config.max), isNew: false };
|
|
159
264
|
}
|
|
160
265
|
|
|
@@ -163,14 +268,14 @@ function createAffinityStore(ctx, config, log) {
|
|
|
163
268
|
const manual = findManualRelationship(platform, userId);
|
|
164
269
|
const base = fallbackInitial !== undefined
|
|
165
270
|
? fallbackInitial
|
|
166
|
-
: manual?.initialAffinity ??
|
|
271
|
+
: manual?.initialAffinity ?? rollInitial();
|
|
167
272
|
const initial = clampFn(base, config.min, config.max);
|
|
168
273
|
const level = resolveLevelByAffinity(initial);
|
|
169
274
|
await save({ platform, userId, selfId: session?.selfId }, initial, true, level?.relation ?? '');
|
|
170
275
|
return { affinity: initial, isNew: true };
|
|
171
276
|
}
|
|
172
277
|
|
|
173
|
-
const normalized = clampFn(record.affinity ??
|
|
278
|
+
const normalized = clampFn(record.affinity ?? defaultInitialValue(), config.min, config.max);
|
|
174
279
|
if (normalized !== record.affinity) {
|
|
175
280
|
const level = resolveLevelByAffinity(normalized);
|
|
176
281
|
await save({ platform, userId, selfId: session?.selfId }, normalized, record.affinityInited, level?.relation ?? record.relation ?? '');
|
|
@@ -187,7 +292,12 @@ function createAffinityStore(ctx, config, log) {
|
|
|
187
292
|
resolveLevelByRelation,
|
|
188
293
|
findManualRelationship,
|
|
189
294
|
updateRelationshipConfig,
|
|
190
|
-
|
|
295
|
+
recordBlacklist,
|
|
296
|
+
removeBlacklist,
|
|
297
|
+
isBlacklisted,
|
|
298
|
+
defaultInitial: defaultInitialValue,
|
|
299
|
+
randomInitial: rollInitial,
|
|
300
|
+
initialRange: resolveInitialRange
|
|
191
301
|
};
|
|
192
302
|
}
|
|
193
303
|
|
package/lib/index.d.ts
CHANGED
|
@@ -2,13 +2,23 @@ import { Context, Schema } from 'koishi';
|
|
|
2
2
|
export declare function apply(ctx: Context, config: Config): void;
|
|
3
3
|
export interface Config {
|
|
4
4
|
variableName: string;
|
|
5
|
-
|
|
5
|
+
initialRandomMin: number;
|
|
6
|
+
initialRandomMax: number;
|
|
6
7
|
min: number;
|
|
7
8
|
max: number;
|
|
8
9
|
model: string;
|
|
9
10
|
analysisPrompt: string;
|
|
10
|
-
|
|
11
|
+
maxIncreasePerMessage: number;
|
|
12
|
+
maxDecreasePerMessage: number;
|
|
11
13
|
enableAnalysis: boolean;
|
|
14
|
+
enableAutoBlacklist: boolean;
|
|
15
|
+
blacklistThreshold: number;
|
|
16
|
+
autoBlacklist: Array<{
|
|
17
|
+
platform: string;
|
|
18
|
+
userId: string;
|
|
19
|
+
blockedAt: string;
|
|
20
|
+
note: string;
|
|
21
|
+
}>;
|
|
12
22
|
debugLogging: boolean;
|
|
13
23
|
triggerNicknames: string[];
|
|
14
24
|
useLastAffinity: boolean;
|
package/lib/index.js
CHANGED
|
@@ -27,6 +27,19 @@ function apply(ctx, config) {
|
|
|
27
27
|
const affinityProvider = createAffinityProvider({ config, cache, store });
|
|
28
28
|
const relationshipProvider = createRelationshipProvider({ config, store });
|
|
29
29
|
|
|
30
|
+
const globalGuard = async (session, next) => {
|
|
31
|
+
if (!config.enableAutoBlacklist) return next();
|
|
32
|
+
const platform = session?.platform;
|
|
33
|
+
const userId = session?.userId;
|
|
34
|
+
if (!platform || !userId) return next();
|
|
35
|
+
if (!store.isBlacklisted(platform, userId)) return next();
|
|
36
|
+
cache.clear(platform, userId);
|
|
37
|
+
log('info', '消息已因自动拉黑被拦截', { platform, userId });
|
|
38
|
+
return;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
ctx.middleware(globalGuard, true);
|
|
42
|
+
|
|
30
43
|
ctx.on('ready', async () => {
|
|
31
44
|
try {
|
|
32
45
|
modelRef = await ctx.chatluna.createChatModel(config.model || ctx.chatluna.config.defaultModel);
|
package/lib/middleware.js
CHANGED
|
@@ -51,28 +51,65 @@ async function resolveTriggerNicknames(ctx, config) {
|
|
|
51
51
|
return Array.from(names).map((name) => name.toLowerCase());
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
const tryBlacklistUser = async (ctx, session, store, cache, log) => {
|
|
55
|
+
const platform = session?.platform;
|
|
56
|
+
const userId = session?.userId;
|
|
57
|
+
if (!platform || !userId) return { skipped: true };
|
|
58
|
+
if (store.isBlacklisted(platform, userId)) {
|
|
59
|
+
log('debug', '用户已在自动拉黑列表,跳过重复处理', { platform, userId });
|
|
60
|
+
return { skipped: true };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const recorded = store.recordBlacklist(platform, userId, 'local guard');
|
|
64
|
+
if (recorded) log('info', '已记录自动拉黑用户', { platform, userId, note: 'local guard' });
|
|
65
|
+
cache?.clear?.(platform, userId);
|
|
66
|
+
|
|
67
|
+
return { recorded: Boolean(recorded) };
|
|
68
|
+
};
|
|
69
|
+
|
|
54
70
|
function createAnalysisMiddleware(ctx, config, { store, history, cache, getModel, renderTemplate, getMessageContent, log }) {
|
|
55
71
|
const debugEnabled = config.debugLogging;
|
|
56
72
|
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
const clampValue = (value, low, high) => Math.min(high, Math.max(low, value));
|
|
74
|
+
|
|
75
|
+
const resolveIncreaseLimit = () => {
|
|
76
|
+
if (Number.isFinite(config.maxIncreasePerMessage)) return Math.abs(config.maxIncreasePerMessage);
|
|
77
|
+
return 5;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const resolveDecreaseLimit = () => {
|
|
81
|
+
if (Number.isFinite(config.maxDecreasePerMessage)) return Math.abs(config.maxDecreasePerMessage);
|
|
82
|
+
return 5;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const executeAnalysis = async (session) => {
|
|
86
|
+
if (config.enableAutoBlacklist && store.isBlacklisted(session?.platform, session?.userId)) {
|
|
87
|
+
cache.clear(session?.platform, session?.userId);
|
|
88
|
+
if (debugEnabled) log('debug', '用户处于自动拉黑名单,跳过分析', { platform: session?.platform, userId: session?.userId });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
59
92
|
const nicknames = await resolveTriggerNicknames(ctx, config);
|
|
60
|
-
if (!shouldAnalyzeSession(session, nicknames, log, debugEnabled)) return
|
|
61
|
-
if (!session?.platform || !session?.userId) return
|
|
93
|
+
if (!shouldAnalyzeSession(session, nicknames, log, debugEnabled)) return;
|
|
94
|
+
if (!session?.platform || !session?.userId) return;
|
|
62
95
|
|
|
63
|
-
const clampValue = (value, low, high) => Math.min(high, Math.max(low, value));
|
|
64
96
|
try {
|
|
65
97
|
const manual = store.findManualRelationship(session.platform, session.userId);
|
|
66
98
|
const fallback = manual && typeof manual.initialAffinity === 'number' ? manual.initialAffinity : undefined;
|
|
67
99
|
const result = await store.ensure(session, clampValue, fallback);
|
|
68
100
|
const oldAffinity = result.affinity;
|
|
69
101
|
if (debugEnabled) log('debug', '读取已有好感度', { userId: session.userId, platform: session.platform, affinity: oldAffinity });
|
|
102
|
+
if (config.useLastAffinity) cache.set(session.platform, session.userId, oldAffinity);
|
|
70
103
|
|
|
71
104
|
const historyLines = await history.fetch(session);
|
|
105
|
+
const maxIncreaseLimit = resolveIncreaseLimit();
|
|
106
|
+
const maxDecreaseLimit = resolveDecreaseLimit();
|
|
72
107
|
const prompt = renderTemplate(config.analysisPrompt, {
|
|
73
108
|
currentAffinity: oldAffinity,
|
|
74
109
|
minAffinity: config.min,
|
|
75
110
|
maxAffinity: config.max,
|
|
111
|
+
maxIncreasePerMessage: maxIncreaseLimit,
|
|
112
|
+
maxDecreasePerMessage: maxDecreaseLimit,
|
|
76
113
|
historyCount: historyLines.length,
|
|
77
114
|
historyText: historyLines.join('\n'),
|
|
78
115
|
historyJson: JSON.stringify(historyLines, null, 2),
|
|
@@ -82,9 +119,8 @@ function createAnalysisMiddleware(ctx, config, { store, history, cache, getModel
|
|
|
82
119
|
|
|
83
120
|
const model = getModel();
|
|
84
121
|
if (!model) {
|
|
85
|
-
if (config.useLastAffinity) cache.set(session.platform, session.userId, oldAffinity);
|
|
86
122
|
log('warn', '模型尚未就绪,跳过分析', { userId: session.userId, platform: session.platform });
|
|
87
|
-
return
|
|
123
|
+
return;
|
|
88
124
|
}
|
|
89
125
|
|
|
90
126
|
const message = await model.invoke(prompt);
|
|
@@ -107,7 +143,11 @@ function createAnalysisMiddleware(ctx, config, { store, history, cache, getModel
|
|
|
107
143
|
}
|
|
108
144
|
}
|
|
109
145
|
|
|
110
|
-
const
|
|
146
|
+
const positiveLimit = resolveIncreaseLimit();
|
|
147
|
+
const negativeLimit = resolveDecreaseLimit();
|
|
148
|
+
const limitedDelta = delta >= 0
|
|
149
|
+
? Math.min(delta, positiveLimit)
|
|
150
|
+
: Math.max(delta, -negativeLimit);
|
|
111
151
|
const nextAffinity = clampValue(oldAffinity + limitedDelta, config.min, config.max);
|
|
112
152
|
if (nextAffinity !== oldAffinity) {
|
|
113
153
|
const level = store.resolveLevelByAffinity(nextAffinity);
|
|
@@ -117,9 +157,24 @@ function createAnalysisMiddleware(ctx, config, { store, history, cache, getModel
|
|
|
117
157
|
} else if (config.useLastAffinity) {
|
|
118
158
|
cache.set(session.platform, session.userId, oldAffinity);
|
|
119
159
|
}
|
|
160
|
+
|
|
161
|
+
if (config.enableAutoBlacklist && nextAffinity < config.blacklistThreshold) {
|
|
162
|
+
await tryBlacklistUser(ctx, session, store, cache, log);
|
|
163
|
+
}
|
|
120
164
|
} catch (error) {
|
|
121
165
|
log('warn', '分析流程异常', error);
|
|
122
166
|
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return async (session, next) => {
|
|
170
|
+
if (!config.enableAnalysis) return next();
|
|
171
|
+
|
|
172
|
+
if (config.useLastAffinity) {
|
|
173
|
+
executeAnalysis(session).catch((error) => log('warn', '异步分析流程异常', error));
|
|
174
|
+
return next();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
await executeAnalysis(session);
|
|
123
178
|
return next();
|
|
124
179
|
};
|
|
125
180
|
}
|
package/lib/providers.js
CHANGED
|
@@ -2,7 +2,7 @@ function createAffinityProvider({ config, cache, store }) {
|
|
|
2
2
|
return async (_, __, configurable) => {
|
|
3
3
|
const session = configurable?.session;
|
|
4
4
|
if (!session?.platform || !session?.userId) {
|
|
5
|
-
return store.clamp(
|
|
5
|
+
return store.clamp(store.defaultInitial());
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
const cached = config.useLastAffinity ? cache.get(session.platform, session.userId) : null;
|
|
@@ -34,7 +34,8 @@ function createRelationshipProvider({ config, store }) {
|
|
|
34
34
|
if (!level) return '';
|
|
35
35
|
return level.note ? `${level.relation}(${level.note})` : level.relation;
|
|
36
36
|
}
|
|
37
|
-
const
|
|
37
|
+
const fallback = store.defaultInitial();
|
|
38
|
+
const level = store.resolveLevelByAffinity(fallback);
|
|
38
39
|
return level ? level.relation : '';
|
|
39
40
|
};
|
|
40
41
|
}
|
package/lib/schema.js
CHANGED
|
@@ -9,22 +9,34 @@ const inject = {
|
|
|
9
9
|
|
|
10
10
|
const AffinitySchema = Schema.object({
|
|
11
11
|
variableName: Schema.string().default('affinity').description('变量名称'),
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
initialRandomMin: Schema.number().default(20).description('初始好感度随机范围下限'),
|
|
13
|
+
initialRandomMax: Schema.number().default(40).description('初始好感度随机范围上限'),
|
|
14
|
+
min: Schema.number().default(0).description('好感度最小值'),
|
|
15
|
+
max: Schema.number().default(100).description('好感度最大值'),
|
|
16
|
+
maxIncreasePerMessage: Schema.number().default(5).description('单次增加的最大幅度'),
|
|
17
|
+
maxDecreasePerMessage: Schema.number().default(5).description('单次减少的最大幅度'),
|
|
16
18
|
model: Schema.dynamic('model').description('用于好感度分析的模型'),
|
|
17
19
|
enableAnalysis: Schema.boolean().default(true).description('是否启用好感度分析'),
|
|
18
20
|
useLastAffinity: Schema.boolean().default(false).description('允许直接返回上一次的好感度,不等待分析结果写入'),
|
|
19
21
|
historyMessageCount: Schema.number().default(10).min(0).description('用于分析的最近消息条数'),
|
|
22
|
+
enableAutoBlacklist: Schema.boolean().default(false).description('当好感度低于阈值时自动拉黑用户'),
|
|
23
|
+
blacklistThreshold: Schema.number().default(0).description('好感度低于该值时触发自动拉黑'),
|
|
24
|
+
autoBlacklist: Schema.array(
|
|
25
|
+
Schema.object({
|
|
26
|
+
platform: Schema.string().default('').description('平台'),
|
|
27
|
+
userId: Schema.string().default('').description('用户 ID'),
|
|
28
|
+
blockedAt: Schema.string().default('').description('拉黑时间'),
|
|
29
|
+
note: Schema.string().default('').description('备注')
|
|
30
|
+
})
|
|
31
|
+
).role('table').default([]).description('自动拉黑记录'),
|
|
20
32
|
triggerNicknames: Schema.array(Schema.string().description('昵称'))
|
|
21
33
|
.role('table')
|
|
22
34
|
.default([])
|
|
23
|
-
.description('
|
|
35
|
+
.description('触发分析的 bot 昵称列表'),
|
|
24
36
|
analysisPrompt: Schema.string()
|
|
25
37
|
.role('textarea')
|
|
26
38
|
.default(
|
|
27
|
-
'你是好感度管家,需要根据上下文评估是否调整好感度。\n- 关注最近若干条群聊消息,判断整体语气与语境;\n- 当用户友善、感谢、积极互动时,适度增加;\n- 当用户正常交流且无明显倾向时,保持不变;\n- 当用户冒犯、敷衍、重复打扰时,减少;\n-
|
|
39
|
+
'你是好感度管家,需要根据上下文评估是否调整好感度。\n- 关注最近若干条群聊消息,判断整体语气与语境;\n- 当用户友善、感谢、积极互动时,适度增加;\n- 当用户正常交流且无明显倾向时,保持不变;\n- 当用户冒犯、敷衍、重复打扰时,减少;\n- 增加幅度不超过 {{maxIncreasePerMessage}} ,减少幅度不超过 {{maxDecreasePerMessage}} ,并保持在提供的范围内;\n- 使用 action 表示行为:increase 增加、decrease 减少、hold 保持。\n\n角色设定:{{personaPrompt}}\n\n当前好感度:{{currentAffinity}} (范围 {{minAffinity}} ~ {{maxAffinity}})\n最近 {{historyCount}} 条消息(旧 -> 新):\n{{historyText}}\n\n本次用户消息:\n{{userMessage}}\n\n请仅输出 JSON:{"delta": 整数, "action": "increase|decrease|hold", "reason": "简短中文原因"}。'
|
|
28
40
|
)
|
|
29
41
|
.description('好感度分析主提示词'),
|
|
30
42
|
personaPrompt: Schema.string()
|
package/lib/tools.js
CHANGED
|
@@ -56,7 +56,8 @@ function createRelationshipTool(options) {
|
|
|
56
56
|
if (!level && Array.isArray(options.relationLevels)) {
|
|
57
57
|
level = options.relationLevels.find((item) => item && item.relation === relationName) || null;
|
|
58
58
|
}
|
|
59
|
-
const
|
|
59
|
+
const baseValue = level ? level.min : options.defaultInitial();
|
|
60
|
+
const base = options.clamp(baseValue);
|
|
60
61
|
await options.save({ platform, userId, selfId: session?.selfId }, base, true, relationName);
|
|
61
62
|
options.cache.set(platform, userId, base);
|
|
62
63
|
options.updateRelationshipConfig(userId, relationName, base);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-chatluna-affinity",
|
|
3
|
-
"description": "为 ChatLuna 提供{好感度}与{关系}
|
|
4
|
-
"version": "0.0.
|
|
3
|
+
"description": "为 ChatLuna 提供{好感度}与{关系}变量并提供对应的工具调用和低好感自动拉黑功能。",
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
package/readme.md
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
- `affinity()` 变量:随时取得用户与机器人的好感度。初次使用会自动初始化并写入数据库。
|
|
8
8
|
- `relationship()` 变量:返回友好度对应的称谓,支持自定义关系区间与备注。
|
|
9
|
-
-
|
|
9
|
+
- 初始好感度:可在自定义范围内随机生成,避免所有用户同值起步。
|
|
10
|
+
- 安全守卫:当好感度低于自定义阈值时,记录到插件黑名单并全局拦截后续消息。
|
|
11
|
+
- 控制台名单:自动拉黑后会同步到配置表,方便复核与手动维护。
|
|
12
|
+
- 自动分析:收到私聊、@、引用或命中 bot 昵称时,调用指定模型分析对话并调整好感度。
|
|
10
13
|
- 调整工具:
|
|
11
14
|
- `adjust_affinity` 将好感度设为指定数值,并同步关系区间。
|
|
12
15
|
- `adjust_relationship` 将关系切换为自定义称谓,同时把好感度调至对应区间的下限。
|
|
@@ -17,14 +20,19 @@
|
|
|
17
20
|
| 字段 | 说明 |
|
|
18
21
|
| --- | --- |
|
|
19
22
|
| `variableName` | 好感度变量名称,默认 `affinity` |
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
23
|
+
| `min` / `max` | 好感度最小值 / 最大值 |
|
|
24
|
+
| `initialRandomMin` / `initialRandomMax` | 初始好感度随机区间(含边界,默认 20~40,取整数) |
|
|
25
|
+
| `maxIncreasePerMessage` | 单次允许增加的最大幅度(提示词变量 `{{maxIncreasePerMessage}}`) |
|
|
26
|
+
| `maxDecreasePerMessage` | 单次允许减少的最大幅度(提示词变量 `{{maxDecreasePerMessage}}`) |
|
|
22
27
|
| `model` | 用于分析的 ChatLuna 模型 |
|
|
23
28
|
| `analysisPrompt` | 主提示词模板,支持 `{{currentAffinity}}` 等占位符 |
|
|
24
29
|
| `personaPrompt` | 机器人的人设补充说明 |
|
|
25
|
-
| `triggerNicknames` |
|
|
30
|
+
| `triggerNicknames` | 触发分析的 bot 昵称列表(默认含 ChatLuna 配置的昵称/@ 名) |
|
|
26
31
|
| `useLastAffinity` | 开启后变量能立即返回上一次结果(无需等待分析完成) |
|
|
27
32
|
| `historyMessageCount` | 提供给模型的历史消息条数(从数据库或 group-analysis 插件取得) |
|
|
33
|
+
| `enableAutoBlacklist` | 启用后,好感度低于阈值会写入黑名单并阻断消息 |
|
|
34
|
+
| `blacklistThreshold` | 自动拉黑触发值(小于该值即触发) |
|
|
35
|
+
| `autoBlacklist` | 自动拉黑记录列表(自动维护,可在控制台查看与编辑) |
|
|
28
36
|
| `relationships` | 特殊关系配置:`userId`、`initialAffinity`、`relation`、`note` |
|
|
29
37
|
| `relationshipAffinityLevels` | 区间 → 称谓映射,默认提供“陌生人/友好/亲近/挚友” |
|
|
30
38
|
| `registerAffinityTool` / `registerRelationshipTool` | 是否注册对应 ChatLuna 工具 |
|