koishi-plugin-chatluna-affinity 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/affinity-store.js +194 -0
- package/lib/cache.js +19 -0
- package/lib/history.js +94 -0
- package/lib/index.d.ts +35 -0
- package/lib/index.js +68 -0
- package/lib/logger.js +10 -0
- package/lib/middleware.js +127 -0
- package/lib/providers.js +42 -0
- package/lib/schema.js +66 -0
- package/lib/template.js +8 -0
- package/lib/tools.js +94 -0
- package/package.json +25 -0
- package/readme.md +67 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
const MODEL_NAME = 'chatluna_affinity_records';
|
|
2
|
+
|
|
3
|
+
function clamp(value, low, high) {
|
|
4
|
+
if (typeof value !== 'number' || Number.isNaN(value)) return low;
|
|
5
|
+
return Math.min(high, Math.max(low, Math.round(value)));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function normalizeRelationshipConfig(config) {
|
|
9
|
+
if (Array.isArray(config.relationships)) return [...config.relationships];
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function extendModel(ctx) {
|
|
14
|
+
ctx.model.extend(
|
|
15
|
+
MODEL_NAME,
|
|
16
|
+
{
|
|
17
|
+
id: 'string',
|
|
18
|
+
platform: 'string',
|
|
19
|
+
selfId: { type: 'string', nullable: true },
|
|
20
|
+
userId: 'string',
|
|
21
|
+
affinity: 'integer',
|
|
22
|
+
affinityInited: 'boolean',
|
|
23
|
+
relation: { type: 'string', nullable: true },
|
|
24
|
+
updatedAt: { type: 'timestamp', nullable: true },
|
|
25
|
+
relationUpdatedAt: { type: 'timestamp', nullable: true }
|
|
26
|
+
},
|
|
27
|
+
{ primary: 'id' }
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createAffinityStore(ctx, config, log) {
|
|
32
|
+
extendModel(ctx);
|
|
33
|
+
|
|
34
|
+
const makeId = (platform, userId) => `${platform || 'unknown'}:${userId || 'anonymous'}`;
|
|
35
|
+
|
|
36
|
+
const readLegacy = async (platform, userId) => {
|
|
37
|
+
if (!ctx.database?.getUser) return null;
|
|
38
|
+
try {
|
|
39
|
+
const legacy = await ctx.database.getUser(platform, userId, ['affinity', 'affinityInited']);
|
|
40
|
+
if (!legacy) return null;
|
|
41
|
+
if (typeof legacy.affinity !== 'number' && !legacy.affinityInited) return null;
|
|
42
|
+
const initial = typeof legacy.affinity === 'number' ? legacy.affinity : config.initial;
|
|
43
|
+
const now = new Date();
|
|
44
|
+
return {
|
|
45
|
+
id: makeId(platform, userId),
|
|
46
|
+
platform,
|
|
47
|
+
selfId: '',
|
|
48
|
+
userId,
|
|
49
|
+
affinity: initial,
|
|
50
|
+
affinityInited: Boolean(legacy.affinityInited),
|
|
51
|
+
relation: '',
|
|
52
|
+
updatedAt: now,
|
|
53
|
+
relationUpdatedAt: now
|
|
54
|
+
};
|
|
55
|
+
} catch (error) {
|
|
56
|
+
log('warn', '读取旧版好感度数据失败', error);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const load = async (platform, userId) => {
|
|
62
|
+
if (!platform || !userId) return null;
|
|
63
|
+
const id = makeId(platform, userId);
|
|
64
|
+
const record = await ctx.database.get(MODEL_NAME, { id });
|
|
65
|
+
if (record?.length) return record[0];
|
|
66
|
+
const legacy = await readLegacy(platform, userId);
|
|
67
|
+
if (!legacy) return null;
|
|
68
|
+
await ctx.database.upsert(MODEL_NAME, [legacy]);
|
|
69
|
+
return legacy;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const normalizeIdentity = (seed) => {
|
|
73
|
+
if (!seed) return { platform: '', userId: '', selfId: '' };
|
|
74
|
+
return {
|
|
75
|
+
platform: seed.platform ?? seed?.session?.platform ?? '',
|
|
76
|
+
userId: seed.userId ?? seed?.session?.userId ?? '',
|
|
77
|
+
selfId: seed.selfId ?? seed?.session?.selfId ?? ''
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const save = async (seed, value, inited = true, relation) => {
|
|
82
|
+
const identity = normalizeIdentity(seed);
|
|
83
|
+
if (!identity.platform || !identity.userId) return null;
|
|
84
|
+
|
|
85
|
+
const id = makeId(identity.platform, identity.userId);
|
|
86
|
+
const existing = await ctx.database.get(MODEL_NAME, { id });
|
|
87
|
+
const current = existing?.[0];
|
|
88
|
+
const now = new Date();
|
|
89
|
+
const relationText = relation !== undefined && relation !== null ? String(relation).trim() : undefined;
|
|
90
|
+
|
|
91
|
+
const payload = {
|
|
92
|
+
id,
|
|
93
|
+
platform: identity.platform,
|
|
94
|
+
selfId: identity.selfId || current?.selfId || '',
|
|
95
|
+
userId: identity.userId,
|
|
96
|
+
affinity: typeof value === 'number' ? value : current?.affinity ?? config.initial,
|
|
97
|
+
affinityInited: inited ?? current?.affinityInited ?? false,
|
|
98
|
+
relation: relationText !== undefined ? relationText : current?.relation ?? '',
|
|
99
|
+
updatedAt: now,
|
|
100
|
+
relationUpdatedAt: relationText !== undefined ? now : current?.relationUpdatedAt ?? now
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
await ctx.database.upsert(MODEL_NAME, [payload]);
|
|
104
|
+
return payload;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const resolveLevelByAffinity = (value) => {
|
|
108
|
+
if (typeof value !== 'number') return null;
|
|
109
|
+
return (config.relationshipAffinityLevels || []).find((level) => {
|
|
110
|
+
if (!level) return false;
|
|
111
|
+
const min = Number.isFinite(level.min) ? level.min : -Infinity;
|
|
112
|
+
const max = Number.isFinite(level.max) ? level.max : Infinity;
|
|
113
|
+
return value >= min && value <= max;
|
|
114
|
+
}) || null;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const resolveLevelByRelation = (relationName) => {
|
|
118
|
+
if (!relationName) return null;
|
|
119
|
+
return (config.relationshipAffinityLevels || []).find((level) => level?.relation === relationName) || null;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const findManualRelationship = (platform, userId) => {
|
|
123
|
+
const list = normalizeRelationshipConfig(config);
|
|
124
|
+
return list.find((item) => {
|
|
125
|
+
if (!item?.userId) return false;
|
|
126
|
+
return String(item.userId).trim() === String(userId).trim();
|
|
127
|
+
}) || null;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const applyConfigUpdate = () => {
|
|
131
|
+
ctx.scope?.update?.(config, true);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const updateRelationshipConfig = (userId, relationName, affinityValue) => {
|
|
135
|
+
const list = normalizeRelationshipConfig(config);
|
|
136
|
+
const normalizedId = String(userId).trim();
|
|
137
|
+
const index = list.findIndex((item) => item && String(item.userId ?? '').trim() === normalizedId);
|
|
138
|
+
const existing = index >= 0 ? list[index] : {};
|
|
139
|
+
const entry = {
|
|
140
|
+
userId: normalizedId,
|
|
141
|
+
relation: relationName ?? existing.relation ?? '',
|
|
142
|
+
note: existing.note ?? '',
|
|
143
|
+
initialAffinity: Number.isFinite(affinityValue) ? clamp(affinityValue, config.min, config.max) : existing.initialAffinity ?? null
|
|
144
|
+
};
|
|
145
|
+
list[index >= 0 ? index : list.length] = entry;
|
|
146
|
+
config.relationships = list;
|
|
147
|
+
applyConfigUpdate();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const ensure = async (session, clampFn, fallbackInitial) => {
|
|
151
|
+
const platform = session?.platform;
|
|
152
|
+
const userId = session?.userId;
|
|
153
|
+
if (!platform || !userId) {
|
|
154
|
+
const manual = fallbackInitial !== undefined ? null : (session?.userId ? findManualRelationship(session?.platform, session?.userId) : null);
|
|
155
|
+
const base = fallbackInitial !== undefined
|
|
156
|
+
? fallbackInitial
|
|
157
|
+
: manual?.initialAffinity ?? config.initial;
|
|
158
|
+
return { affinity: clampFn(base, config.min, config.max), isNew: false };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const record = await load(platform, userId);
|
|
162
|
+
if (!record || !record.affinityInited) {
|
|
163
|
+
const manual = findManualRelationship(platform, userId);
|
|
164
|
+
const base = fallbackInitial !== undefined
|
|
165
|
+
? fallbackInitial
|
|
166
|
+
: manual?.initialAffinity ?? config.initial;
|
|
167
|
+
const initial = clampFn(base, config.min, config.max);
|
|
168
|
+
const level = resolveLevelByAffinity(initial);
|
|
169
|
+
await save({ platform, userId, selfId: session?.selfId }, initial, true, level?.relation ?? '');
|
|
170
|
+
return { affinity: initial, isNew: true };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const normalized = clampFn(record.affinity ?? config.initial, config.min, config.max);
|
|
174
|
+
if (normalized !== record.affinity) {
|
|
175
|
+
const level = resolveLevelByAffinity(normalized);
|
|
176
|
+
await save({ platform, userId, selfId: session?.selfId }, normalized, record.affinityInited, level?.relation ?? record.relation ?? '');
|
|
177
|
+
}
|
|
178
|
+
return { affinity: normalized, isNew: false };
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
clamp: (value) => clamp(value, config.min, config.max),
|
|
183
|
+
save,
|
|
184
|
+
load,
|
|
185
|
+
ensure,
|
|
186
|
+
resolveLevelByAffinity,
|
|
187
|
+
resolveLevelByRelation,
|
|
188
|
+
findManualRelationship,
|
|
189
|
+
updateRelationshipConfig,
|
|
190
|
+
defaultInitial: config.initial
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = { createAffinityStore };
|
package/lib/cache.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function createAffinityCache() {
|
|
2
|
+
let entry = null;
|
|
3
|
+
|
|
4
|
+
const match = (platform, userId) => entry && entry.platform === platform && entry.userId === userId;
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
get(platform, userId) {
|
|
8
|
+
return match(platform, userId) ? entry.value : null;
|
|
9
|
+
},
|
|
10
|
+
set(platform, userId, value) {
|
|
11
|
+
entry = { platform, userId, value };
|
|
12
|
+
},
|
|
13
|
+
clear(platform, userId) {
|
|
14
|
+
if (match(platform, userId)) entry = null;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { createAffinityCache };
|
package/lib/history.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
function createHistoryManager(ctx, config, log) {
|
|
2
|
+
const cache = new Map();
|
|
3
|
+
const limit = Math.max((config.historyMessageCount || 0) * 6, 60);
|
|
4
|
+
|
|
5
|
+
function makeKey(session) {
|
|
6
|
+
if (!session) return 'unknown';
|
|
7
|
+
if (session.guildId) {
|
|
8
|
+
return `${session.platform || 'unknown'}:${session.selfId || 'self'}:${session.guildId}:${session.channelId || session.guildId}`;
|
|
9
|
+
}
|
|
10
|
+
return `${session.platform || 'unknown'}:${session.selfId || 'self'}:direct:${session.channelId || session.userId || 'unknown'}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeEntries(entries, size) {
|
|
14
|
+
if (!Array.isArray(entries) || !entries.length) return [];
|
|
15
|
+
return entries
|
|
16
|
+
.slice(-size)
|
|
17
|
+
.map((item) => ({
|
|
18
|
+
userId: item.userId,
|
|
19
|
+
username: item.username || item.user?.name || item.author?.name || item.sender?.name || item.userId || '未知用户',
|
|
20
|
+
content: typeof item.content === 'string' && item.content.trim() ? item.content.trim() : '[无文本内容]',
|
|
21
|
+
timestamp: new Date(item.timestamp ?? Date.now()).getTime()
|
|
22
|
+
}))
|
|
23
|
+
.sort((a, b) => a.timestamp - b.timestamp)
|
|
24
|
+
.map((item) => {
|
|
25
|
+
const name = item.username || item.userId || '未知用户';
|
|
26
|
+
const idText = item.userId ? `(${item.userId})` : '';
|
|
27
|
+
return `${name}${idText}: ${item.content}`;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function record(session) {
|
|
32
|
+
if (!session?.platform) return;
|
|
33
|
+
const key = makeKey(session);
|
|
34
|
+
const list = cache.get(key) || [];
|
|
35
|
+
list.push({
|
|
36
|
+
userId: session.userId,
|
|
37
|
+
username: session.username || session.author?.name || session.event?.user?.name || session.user?.name || session.userId,
|
|
38
|
+
content: session.content ?? '',
|
|
39
|
+
timestamp: new Date(session.timestamp ?? Date.now()).getTime()
|
|
40
|
+
});
|
|
41
|
+
if (list.length > limit) list.splice(0, list.length - limit);
|
|
42
|
+
cache.set(key, list);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function readFromService(session, count) {
|
|
46
|
+
const service = ctx.chatluna_group_analysis_message;
|
|
47
|
+
if (!service?.getHistoricalMessages) return [];
|
|
48
|
+
try {
|
|
49
|
+
const messages = await service.getHistoricalMessages({
|
|
50
|
+
guildId: session.guildId,
|
|
51
|
+
channelId: session.channelId,
|
|
52
|
+
limit: count,
|
|
53
|
+
selfId: session.selfId,
|
|
54
|
+
platform: session.platform,
|
|
55
|
+
endTime: new Date()
|
|
56
|
+
});
|
|
57
|
+
return normalizeEntries(messages, count);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
log('warn', '调用 chatluna_group_analysis_message 获取历史消息失败', error);
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function readFromCache(session, count) {
|
|
65
|
+
const cached = cache.get(makeKey(session));
|
|
66
|
+
if (cached?.length) return normalizeEntries(cached, count);
|
|
67
|
+
if (!ctx.database?.tables?.message || !ctx.database.get) return [];
|
|
68
|
+
try {
|
|
69
|
+
const rows = await ctx.database.get(
|
|
70
|
+
'message',
|
|
71
|
+
{ platform: session.platform, channelId: session.channelId },
|
|
72
|
+
{ limit: count, sort: { time: 'desc' } }
|
|
73
|
+
);
|
|
74
|
+
return normalizeEntries(rows, count);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
log('warn', '获取历史消息失败', error);
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function fetch(session) {
|
|
82
|
+
const count = config.historyMessageCount || 0;
|
|
83
|
+
if (count <= 0) return [];
|
|
84
|
+
const serviceEntries = await readFromService(session, count);
|
|
85
|
+
if (serviceEntries.length) return serviceEntries;
|
|
86
|
+
return readFromCache(session, count);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
ctx.on('message', record);
|
|
90
|
+
|
|
91
|
+
return { fetch };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { createHistoryManager };
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Context, Schema } from 'koishi';
|
|
2
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
3
|
+
export interface Config {
|
|
4
|
+
variableName: string;
|
|
5
|
+
initial: number;
|
|
6
|
+
min: number;
|
|
7
|
+
max: number;
|
|
8
|
+
model: string;
|
|
9
|
+
analysisPrompt: string;
|
|
10
|
+
maxDeltaPerMessage: number;
|
|
11
|
+
enableAnalysis: boolean;
|
|
12
|
+
debugLogging: boolean;
|
|
13
|
+
triggerNicknames: string[];
|
|
14
|
+
useLastAffinity: boolean;
|
|
15
|
+
historyMessageCount: number;
|
|
16
|
+
personaPrompt: string;
|
|
17
|
+
relationshipVariableName: string;
|
|
18
|
+
relationships: Array<{
|
|
19
|
+
initialAffinity: number | null;
|
|
20
|
+
userId: string;
|
|
21
|
+
relation: string;
|
|
22
|
+
note: string;
|
|
23
|
+
}>;
|
|
24
|
+
relationshipAffinityLevels: Array<{
|
|
25
|
+
min: number;
|
|
26
|
+
max: number;
|
|
27
|
+
relation: string;
|
|
28
|
+
note: string;
|
|
29
|
+
}>;
|
|
30
|
+
registerAffinityTool: boolean;
|
|
31
|
+
registerRelationshipTool: boolean;
|
|
32
|
+
}
|
|
33
|
+
export declare const Config: Schema<Config>;
|
|
34
|
+
export declare const inject: string[];
|
|
35
|
+
export declare const name = "chatluna-affinity";
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const { modelSchema } = require('koishi-plugin-chatluna/utils/schema');
|
|
2
|
+
const { ChatLunaPlugin } = require('koishi-plugin-chatluna/services/chat');
|
|
3
|
+
const { getMessageContent } = require('koishi-plugin-chatluna/utils/string');
|
|
4
|
+
|
|
5
|
+
const { Config, inject, name } = require('./schema');
|
|
6
|
+
const { createLogger } = require('./logger');
|
|
7
|
+
const { renderTemplate } = require('./template');
|
|
8
|
+
const { createAffinityStore } = require('./affinity-store');
|
|
9
|
+
const { createHistoryManager } = require('./history');
|
|
10
|
+
const { createAffinityCache } = require('./cache');
|
|
11
|
+
const { createAffinityProvider, createRelationshipProvider } = require('./providers');
|
|
12
|
+
const { createToolRegistry } = require('./tools');
|
|
13
|
+
const { createAnalysisMiddleware } = require('./middleware');
|
|
14
|
+
|
|
15
|
+
function apply(ctx, config) {
|
|
16
|
+
const plugin = new ChatLunaPlugin(ctx, config, 'affinity', false);
|
|
17
|
+
modelSchema(ctx);
|
|
18
|
+
|
|
19
|
+
const log = createLogger(ctx, config);
|
|
20
|
+
const cache = createAffinityCache();
|
|
21
|
+
const store = createAffinityStore(ctx, config, log);
|
|
22
|
+
const history = createHistoryManager(ctx, config, log);
|
|
23
|
+
|
|
24
|
+
let modelRef;
|
|
25
|
+
const getModel = () => modelRef?.value ?? modelRef ?? null;
|
|
26
|
+
|
|
27
|
+
const affinityProvider = createAffinityProvider({ config, cache, store });
|
|
28
|
+
const relationshipProvider = createRelationshipProvider({ config, store });
|
|
29
|
+
|
|
30
|
+
ctx.on('ready', async () => {
|
|
31
|
+
try {
|
|
32
|
+
modelRef = await ctx.chatluna.createChatModel(config.model || ctx.chatluna.config.defaultModel);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
log('warn', '模型初始化失败', error);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ctx.chatluna.promptRenderer.registerFunctionProvider(config.variableName, affinityProvider);
|
|
38
|
+
ctx.chatluna.promptRenderer.registerFunctionProvider(config.relationshipVariableName, relationshipProvider);
|
|
39
|
+
|
|
40
|
+
const registry = createToolRegistry(config, store, cache);
|
|
41
|
+
if (config.registerAffinityTool) {
|
|
42
|
+
plugin.registerTool('adjust_affinity', {
|
|
43
|
+
selector: registry.affinitySelector,
|
|
44
|
+
createTool: registry.createAffinityTool
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (config.registerRelationshipTool) {
|
|
48
|
+
plugin.registerTool('adjust_relationship', {
|
|
49
|
+
selector: registry.relationshipSelector,
|
|
50
|
+
createTool: registry.createRelationshipTool
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const middleware = createAnalysisMiddleware(ctx, config, {
|
|
56
|
+
store,
|
|
57
|
+
history,
|
|
58
|
+
cache,
|
|
59
|
+
renderTemplate,
|
|
60
|
+
getMessageContent,
|
|
61
|
+
getModel,
|
|
62
|
+
log
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
ctx.middleware(middleware);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { apply, Config, inject, name };
|
package/lib/logger.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
function createLogger(ctx, config) {
|
|
2
|
+
const base = ctx.logger ? ctx.logger('chatluna-affinity') : console;
|
|
3
|
+
return (level, message, detail) => {
|
|
4
|
+
if (!config.debugLogging) return;
|
|
5
|
+
const writer = typeof base?.[level] === 'function' ? base[level] : base?.info ?? base?.log ?? console.log;
|
|
6
|
+
detail === undefined ? writer.call(base, message) : writer.call(base, message, detail);
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
module.exports = { createLogger };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
function shouldAnalyzeSession(session, nicknames, log, debugEnabled) {
|
|
2
|
+
const text = String(session.content ?? '');
|
|
3
|
+
const lowerText = text.toLowerCase();
|
|
4
|
+
const nicknameHit = nicknames.some((name) => lowerText.includes(name.toLowerCase()));
|
|
5
|
+
if (debugEnabled && nicknameHit) log('debug', '昵称匹配命中', { text, nicknames });
|
|
6
|
+
|
|
7
|
+
const selfIdLower = String(session.selfId ?? '').toLowerCase();
|
|
8
|
+
const mentionHit = Boolean(
|
|
9
|
+
session.elements?.some((el) => {
|
|
10
|
+
if (el.type !== 'at') return false;
|
|
11
|
+
const attrs = el.attrs ?? {};
|
|
12
|
+
const candidates = [attrs.id, attrs.userId, attrs.cid, attrs.name, attrs.nickname]
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
.map((value) => String(value).toLowerCase());
|
|
15
|
+
return selfIdLower ? candidates.includes(selfIdLower) : candidates.length > 0;
|
|
16
|
+
})
|
|
17
|
+
);
|
|
18
|
+
if (debugEnabled && mentionHit) log('debug', '@ 匹配命中', { elements: session.elements });
|
|
19
|
+
|
|
20
|
+
const quote = session.quote;
|
|
21
|
+
const quoteHit = Boolean(
|
|
22
|
+
quote && (
|
|
23
|
+
quote.userId === session.selfId ||
|
|
24
|
+
quote?.author?.userId === session.selfId ||
|
|
25
|
+
quote?.author?.id === session.selfId ||
|
|
26
|
+
quote?.user?.id === session.selfId
|
|
27
|
+
)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return session.isDirect || session?.atMe || nicknameHit || mentionHit || quoteHit;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function resolveTriggerNicknames(ctx, config) {
|
|
34
|
+
const names = new Set();
|
|
35
|
+
for (const value of config.triggerNicknames || []) {
|
|
36
|
+
if (value) names.add(String(value).trim());
|
|
37
|
+
}
|
|
38
|
+
const chatlunaConfig = ctx.chatluna?.config ?? {};
|
|
39
|
+
const possibleNames = [chatlunaConfig.nicknames, chatlunaConfig.nickname, chatlunaConfig.names, chatlunaConfig.name, chatlunaConfig.botNames];
|
|
40
|
+
for (const item of possibleNames) {
|
|
41
|
+
if (Array.isArray(item)) item.forEach((name) => name && names.add(String(name).trim()));
|
|
42
|
+
else if (item) names.add(String(item).trim());
|
|
43
|
+
}
|
|
44
|
+
const rendererNames = ctx.chatluna?.promptRenderer?.nicknames;
|
|
45
|
+
if (rendererNames?.size) for (const name of rendererNames) names.add(String(name).trim());
|
|
46
|
+
const rendererGetter = ctx.chatluna?.promptRenderer?.getNicknames;
|
|
47
|
+
if (typeof rendererGetter === 'function') {
|
|
48
|
+
const extra = await rendererGetter();
|
|
49
|
+
if (Array.isArray(extra)) extra.forEach((name) => name && names.add(String(name).trim()));
|
|
50
|
+
}
|
|
51
|
+
return Array.from(names).map((name) => name.toLowerCase());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createAnalysisMiddleware(ctx, config, { store, history, cache, getModel, renderTemplate, getMessageContent, log }) {
|
|
55
|
+
const debugEnabled = config.debugLogging;
|
|
56
|
+
|
|
57
|
+
return async (session, next) => {
|
|
58
|
+
if (!config.enableAnalysis) return next();
|
|
59
|
+
const nicknames = await resolveTriggerNicknames(ctx, config);
|
|
60
|
+
if (!shouldAnalyzeSession(session, nicknames, log, debugEnabled)) return next();
|
|
61
|
+
if (!session?.platform || !session?.userId) return next();
|
|
62
|
+
|
|
63
|
+
const clampValue = (value, low, high) => Math.min(high, Math.max(low, value));
|
|
64
|
+
try {
|
|
65
|
+
const manual = store.findManualRelationship(session.platform, session.userId);
|
|
66
|
+
const fallback = manual && typeof manual.initialAffinity === 'number' ? manual.initialAffinity : undefined;
|
|
67
|
+
const result = await store.ensure(session, clampValue, fallback);
|
|
68
|
+
const oldAffinity = result.affinity;
|
|
69
|
+
if (debugEnabled) log('debug', '读取已有好感度', { userId: session.userId, platform: session.platform, affinity: oldAffinity });
|
|
70
|
+
|
|
71
|
+
const historyLines = await history.fetch(session);
|
|
72
|
+
const prompt = renderTemplate(config.analysisPrompt, {
|
|
73
|
+
currentAffinity: oldAffinity,
|
|
74
|
+
minAffinity: config.min,
|
|
75
|
+
maxAffinity: config.max,
|
|
76
|
+
historyCount: historyLines.length,
|
|
77
|
+
historyText: historyLines.join('\n'),
|
|
78
|
+
historyJson: JSON.stringify(historyLines, null, 2),
|
|
79
|
+
userMessage: session.content ?? '',
|
|
80
|
+
personaPrompt: config.personaPrompt ?? ''
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const model = getModel();
|
|
84
|
+
if (!model) {
|
|
85
|
+
if (config.useLastAffinity) cache.set(session.platform, session.userId, oldAffinity);
|
|
86
|
+
log('warn', '模型尚未就绪,跳过分析', { userId: session.userId, platform: session.platform });
|
|
87
|
+
return next();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const message = await model.invoke(prompt);
|
|
91
|
+
const text = getMessageContent(message?.content ?? message);
|
|
92
|
+
const jsonCandidate = typeof text === 'string' ? text : String(text ?? '');
|
|
93
|
+
const match = jsonCandidate.match(/\{[\s\S]*\}/);
|
|
94
|
+
let delta = 0;
|
|
95
|
+
if (match) {
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(match[0]);
|
|
98
|
+
const raw = typeof parsed.delta === 'number' ? parsed.delta : parseInt(parsed.delta, 10);
|
|
99
|
+
if (Number.isFinite(raw)) delta = Math.trunc(raw);
|
|
100
|
+
const action = typeof parsed.action === 'string' ? parsed.action.toLowerCase() : '';
|
|
101
|
+
if (action === 'increase' && delta <= 0) delta = Math.max(1, Math.abs(delta));
|
|
102
|
+
if (action === 'decrease' && delta >= 0) delta = -Math.max(1, Math.abs(delta));
|
|
103
|
+
if (action === 'hold') delta = 0;
|
|
104
|
+
if (debugEnabled) log('info', '模型返回', { raw: parsed, parsedDelta: delta, action, userId: session.userId, platform: session.platform });
|
|
105
|
+
} catch (error) {
|
|
106
|
+
log('warn', '解析模型响应失败', { text: jsonCandidate, error });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const limitedDelta = clampValue(delta, -Math.abs(config.maxDeltaPerMessage), Math.abs(config.maxDeltaPerMessage));
|
|
111
|
+
const nextAffinity = clampValue(oldAffinity + limitedDelta, config.min, config.max);
|
|
112
|
+
if (nextAffinity !== oldAffinity) {
|
|
113
|
+
const level = store.resolveLevelByAffinity(nextAffinity);
|
|
114
|
+
await store.save({ platform: session.platform, userId: session.userId, selfId: session?.selfId }, nextAffinity, true, level?.relation ?? '');
|
|
115
|
+
cache.set(session.platform, session.userId, nextAffinity);
|
|
116
|
+
log('info', '好感度已更新', { oldAffinity, delta: limitedDelta, nextAffinity, userId: session.userId, platform: session.platform });
|
|
117
|
+
} else if (config.useLastAffinity) {
|
|
118
|
+
cache.set(session.platform, session.userId, oldAffinity);
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
log('warn', '分析流程异常', error);
|
|
122
|
+
}
|
|
123
|
+
return next();
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { createAnalysisMiddleware };
|
package/lib/providers.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
function createAffinityProvider({ config, cache, store }) {
|
|
2
|
+
return async (_, __, configurable) => {
|
|
3
|
+
const session = configurable?.session;
|
|
4
|
+
if (!session?.platform || !session?.userId) {
|
|
5
|
+
return store.clamp(config.initial);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const cached = config.useLastAffinity ? cache.get(session.platform, session.userId) : null;
|
|
9
|
+
if (cached !== null) return store.clamp(cached);
|
|
10
|
+
|
|
11
|
+
const manual = store.findManualRelationship(session.platform, session.userId);
|
|
12
|
+
const override = manual && typeof manual.initialAffinity === 'number' ? manual.initialAffinity : undefined;
|
|
13
|
+
const { affinity } = await store.ensure(session, (value, lo, hi) => Math.min(hi, Math.max(lo, value)), override);
|
|
14
|
+
cache.set(session.platform, session.userId, affinity);
|
|
15
|
+
return affinity;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createRelationshipProvider({ config, store }) {
|
|
20
|
+
return async (args, _variables, configurable) => {
|
|
21
|
+
const session = configurable?.session;
|
|
22
|
+
const [userArg, platformArg] = args || [];
|
|
23
|
+
const userId = String(userArg || session?.userId || '').trim();
|
|
24
|
+
const platform = String(platformArg || session?.platform || '').trim();
|
|
25
|
+
if (!userId) return '';
|
|
26
|
+
|
|
27
|
+
const manual = store.findManualRelationship(platform, userId);
|
|
28
|
+
if (manual?.relation) return manual.note ? `${manual.relation}(${manual.note})` : manual.relation;
|
|
29
|
+
|
|
30
|
+
const record = await store.load(platform, userId);
|
|
31
|
+
const affinity = record?.affinity;
|
|
32
|
+
if (typeof affinity === 'number') {
|
|
33
|
+
const level = store.resolveLevelByAffinity(affinity);
|
|
34
|
+
if (!level) return '';
|
|
35
|
+
return level.note ? `${level.relation}(${level.note})` : level.relation;
|
|
36
|
+
}
|
|
37
|
+
const level = store.resolveLevelByAffinity(config.initial);
|
|
38
|
+
return level ? level.relation : '';
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { createAffinityProvider, createRelationshipProvider };
|
package/lib/schema.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const { Schema } = require('koishi');
|
|
2
|
+
|
|
3
|
+
const name = 'chatluna-affinity';
|
|
4
|
+
|
|
5
|
+
const inject = {
|
|
6
|
+
required: ['chatluna', 'database'],
|
|
7
|
+
optional: ['chatluna_group_analysis_message', 'chatluna_group_analysis']
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const AffinitySchema = Schema.object({
|
|
11
|
+
variableName: Schema.string().default('affinity').description('变量名称'),
|
|
12
|
+
initial: Schema.number().default(30).description('初始好感度'),
|
|
13
|
+
min: Schema.number().default(0).description('最小值'),
|
|
14
|
+
max: Schema.number().default(100).description('最大值'),
|
|
15
|
+
maxDeltaPerMessage: Schema.number().default(5).description('单次增减的最大幅度'),
|
|
16
|
+
model: Schema.dynamic('model').description('用于好感度分析的模型'),
|
|
17
|
+
enableAnalysis: Schema.boolean().default(true).description('是否启用好感度分析'),
|
|
18
|
+
useLastAffinity: Schema.boolean().default(false).description('允许直接返回上一次的好感度,不等待分析结果写入'),
|
|
19
|
+
historyMessageCount: Schema.number().default(10).min(0).description('用于分析的最近消息条数'),
|
|
20
|
+
triggerNicknames: Schema.array(Schema.string().description('昵称'))
|
|
21
|
+
.role('table')
|
|
22
|
+
.default([])
|
|
23
|
+
.description('触发分析的额外昵称列表'),
|
|
24
|
+
analysisPrompt: Schema.string()
|
|
25
|
+
.role('textarea')
|
|
26
|
+
.default(
|
|
27
|
+
'你是好感度管家,需要根据上下文评估是否调整好感度。\n- 关注最近若干条群聊消息,判断整体语气与语境;\n- 当用户友善、感谢、积极互动时,适度增加;\n- 当用户正常交流且无明显倾向时,保持不变;\n- 当用户冒犯、敷衍、重复打扰时,减少;\n- 每次调整幅度不超过 5 ,并保持在提供的范围内;\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
|
+
)
|
|
29
|
+
.description('好感度分析主提示词'),
|
|
30
|
+
personaPrompt: Schema.string()
|
|
31
|
+
.role('textarea')
|
|
32
|
+
.default('你是一位温暖可靠的伙伴,会根据好感度高低调整语气:好感度越高越亲近,越低越保持礼貌。')
|
|
33
|
+
.description('补充的人设提示词,会注入到分析提示词中'),
|
|
34
|
+
debugLogging: Schema.boolean().default(false).description('输出调试日志'),
|
|
35
|
+
registerAffinityTool: Schema.boolean().default(false).description('注册 ChatLuna 工具:调整好感度')
|
|
36
|
+
}).description('好感度设置');
|
|
37
|
+
|
|
38
|
+
const RelationshipSchema = Schema.object({
|
|
39
|
+
relationshipVariableName: Schema.string().default('relationship').description('关系变量名称'),
|
|
40
|
+
relationships: Schema.array(
|
|
41
|
+
Schema.object({
|
|
42
|
+
initialAffinity: Schema.number().default(null).description('针对该用户的初始好感度(留空则使用全局初始值)'),
|
|
43
|
+
userId: Schema.string().default('').description('用户 ID'),
|
|
44
|
+
relation: Schema.string().default('').description('关系'),
|
|
45
|
+
note: Schema.string().default('').description('备注')
|
|
46
|
+
})
|
|
47
|
+
).role('table').default([]).description('关系配置列表'),
|
|
48
|
+
relationshipAffinityLevels: Schema.array(
|
|
49
|
+
Schema.object({
|
|
50
|
+
min: Schema.number().default(0).description('好感度下限'),
|
|
51
|
+
max: Schema.number().default(100).description('好感度上限'),
|
|
52
|
+
relation: Schema.string().description('关系'),
|
|
53
|
+
note: Schema.string().default('').description('备注')
|
|
54
|
+
})
|
|
55
|
+
).role('table').default([
|
|
56
|
+
{ min: 0, max: 29, relation: '陌生人', note: '保持距离' },
|
|
57
|
+
{ min: 30, max: 59, relation: '友好', note: '一般关系' },
|
|
58
|
+
{ min: 60, max: 89, relation: '亲近', note: '值得信赖' },
|
|
59
|
+
{ min: 90, max: 100, relation: '挚友', note: '非常重要' }
|
|
60
|
+
]).description('好感度区间关系'),
|
|
61
|
+
registerRelationshipTool: Schema.boolean().default(false).description('注册 ChatLuna 工具:调整关系')
|
|
62
|
+
}).description('关系设置');
|
|
63
|
+
|
|
64
|
+
const Config = Schema.intersect([AffinitySchema, RelationshipSchema]);
|
|
65
|
+
|
|
66
|
+
module.exports = { name, inject, Config };
|
package/lib/template.js
ADDED
package/lib/tools.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const { StructuredTool } = require('@langchain/core/tools');
|
|
2
|
+
const { z } = require('zod');
|
|
3
|
+
|
|
4
|
+
function createAffinityTool(options) {
|
|
5
|
+
return new (class extends StructuredTool {
|
|
6
|
+
constructor() {
|
|
7
|
+
super({});
|
|
8
|
+
this.name = 'adjust_affinity';
|
|
9
|
+
this.description = 'Adjust affinity for a specific user and sync derived relationship.';
|
|
10
|
+
this.schema = z.object({
|
|
11
|
+
affinity: z.number().min(options.min).max(options.max).describe(`Target affinity (range ${options.min}-${options.max})`),
|
|
12
|
+
targetUserId: z.string().optional().describe('Target user ID; defaults to current session'),
|
|
13
|
+
platform: z.string().optional().describe('Target platform; defaults to current session')
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
async _call(input, _manager, runnable) {
|
|
17
|
+
const session = runnable?.configurable?.session;
|
|
18
|
+
const platform = input.platform || session?.platform;
|
|
19
|
+
const userId = input.targetUserId || session?.userId;
|
|
20
|
+
if (!platform || !userId) return 'Missing platform or user ID. Unable to adjust affinity.';
|
|
21
|
+
const value = options.clamp(input.affinity);
|
|
22
|
+
const level = options.resolveLevelByAffinity(value);
|
|
23
|
+
await options.save({ platform, userId, selfId: session?.selfId }, value, true, level?.relation ?? options.defaultRelation);
|
|
24
|
+
options.cache.set(platform, userId, value);
|
|
25
|
+
if (level?.relation) {
|
|
26
|
+
return `Affinity for ${platform}/${userId} set to ${value}. Relationship updated to ${level.relation}.`;
|
|
27
|
+
}
|
|
28
|
+
return `Affinity for ${platform}/${userId} set to ${value}.`;
|
|
29
|
+
}
|
|
30
|
+
})();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createRelationshipTool(options) {
|
|
34
|
+
return new (class extends StructuredTool {
|
|
35
|
+
constructor() {
|
|
36
|
+
super({});
|
|
37
|
+
this.name = 'adjust_relationship';
|
|
38
|
+
this.description = 'Set relationship for a user and align affinity to the relationship lower bound.';
|
|
39
|
+
const levels = Array.isArray(options.relationLevels)
|
|
40
|
+
? options.relationLevels.map((item) => item && item.relation ? { ...item, relation: String(item.relation).trim() } : null).filter(Boolean)
|
|
41
|
+
: [];
|
|
42
|
+
const summary = levels.map((item) => `${item.relation}: ${item.min}-${item.max}`).join(' | ');
|
|
43
|
+
this.schema = z.object({
|
|
44
|
+
relation: z.string().min(1, 'Relation cannot be empty').describe(summary ? `Target relation (configured: ${summary})` : 'Target relation name'),
|
|
45
|
+
targetUserId: z.string().optional().describe('Target user ID; defaults to current session'),
|
|
46
|
+
platform: z.string().optional().describe('Target platform; defaults to current session')
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async _call(input, _manager, runnable) {
|
|
50
|
+
const session = runnable?.configurable?.session;
|
|
51
|
+
const platform = input.platform || session?.platform;
|
|
52
|
+
const userId = input.targetUserId || session?.userId;
|
|
53
|
+
if (!platform || !userId) return 'Missing platform or user ID. Unable to adjust relationship.';
|
|
54
|
+
const relationName = input.relation.trim();
|
|
55
|
+
let level = options.resolveLevelByRelation(relationName);
|
|
56
|
+
if (!level && Array.isArray(options.relationLevels)) {
|
|
57
|
+
level = options.relationLevels.find((item) => item && item.relation === relationName) || null;
|
|
58
|
+
}
|
|
59
|
+
const base = level ? options.clamp(level.min) : options.clamp(options.defaultInitial);
|
|
60
|
+
await options.save({ platform, userId, selfId: session?.selfId }, base, true, relationName);
|
|
61
|
+
options.cache.set(platform, userId, base);
|
|
62
|
+
options.updateRelationshipConfig(userId, relationName, base);
|
|
63
|
+
if (level) {
|
|
64
|
+
return `Relationship for ${platform}/${userId} set to ${relationName}. Affinity updated to ${base}.`;
|
|
65
|
+
}
|
|
66
|
+
return `Relationship for ${platform}/${userId} set to ${relationName} (custom). Affinity updated to ${base}.`;
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createToolRegistry(config, store, cache) {
|
|
72
|
+
const options = {
|
|
73
|
+
clamp: store.clamp,
|
|
74
|
+
resolveLevelByAffinity: store.resolveLevelByAffinity,
|
|
75
|
+
resolveLevelByRelation: store.resolveLevelByRelation,
|
|
76
|
+
relationLevels: config.relationshipAffinityLevels || [],
|
|
77
|
+
defaultRelation: '',
|
|
78
|
+
defaultInitial: store.defaultInitial,
|
|
79
|
+
save: store.save,
|
|
80
|
+
cache,
|
|
81
|
+
updateRelationshipConfig: store.updateRelationshipConfig,
|
|
82
|
+
min: config.min,
|
|
83
|
+
max: config.max
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
affinitySelector: () => true,
|
|
88
|
+
relationshipSelector: () => true,
|
|
89
|
+
createAffinityTool: () => createAffinityTool(options),
|
|
90
|
+
createRelationshipTool: () => createRelationshipTool(options)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { createToolRegistry };
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-chatluna-affinity",
|
|
3
|
+
"description": "为 ChatLuna 提供{好感度}与{关系}变量并提供对应的工具调用",
|
|
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
|
+
"dependencies": {
|
|
21
|
+
"@langchain/core": "^0.3.62",
|
|
22
|
+
"koishi-plugin-chatluna": "^1.3.0-alpha.78",
|
|
23
|
+
"zod": "^3.23.8"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# koishi-plugin-chatluna-affinity
|
|
2
|
+
|
|
3
|
+
在 ChatLuna 中按用户维度维护“好感度”与“关系”状态,并提供可操作的工具与变量。
|
|
4
|
+
|
|
5
|
+
## 亮点一览
|
|
6
|
+
|
|
7
|
+
- `affinity()` 变量:随时取得用户与机器人的好感度。初次使用会自动初始化并写入数据库。
|
|
8
|
+
- `relationship()` 变量:返回友好度对应的称谓,支持自定义关系区间与备注。
|
|
9
|
+
- 自动分析:收到私聊、@、引用或命中昵称时,调用指定模型分析对话并调整好感度。
|
|
10
|
+
- 调整工具:
|
|
11
|
+
- `adjust_affinity` 将好感度设为指定数值,并同步关系区间。
|
|
12
|
+
- `adjust_relationship` 将关系切换为自定义称谓,同时把好感度调至对应区间的下限。
|
|
13
|
+
- 特殊关系:可为指定用户记录“初始好感度 + 自定义称谓”,工具操作会实时写回配置。
|
|
14
|
+
|
|
15
|
+
## 主要配置
|
|
16
|
+
|
|
17
|
+
| 字段 | 说明 |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| `variableName` | 好感度变量名称,默认 `affinity` |
|
|
20
|
+
| `initial` / `min` / `max` | 好感度初始值与上下限 |
|
|
21
|
+
| `maxDeltaPerMessage` | 单次分析允许的最大变动幅度 |
|
|
22
|
+
| `model` | 用于分析的 ChatLuna 模型 |
|
|
23
|
+
| `analysisPrompt` | 主提示词模板,支持 `{{currentAffinity}}` 等占位符 |
|
|
24
|
+
| `personaPrompt` | 机器人的人设补充说明 |
|
|
25
|
+
| `triggerNicknames` | 额外触发昵称列表(默认含 ChatLuna 配置的昵称/@ 名) |
|
|
26
|
+
| `useLastAffinity` | 开启后变量能立即返回上一次结果(无需等待分析完成) |
|
|
27
|
+
| `historyMessageCount` | 提供给模型的历史消息条数(从数据库或 group-analysis 插件取得) |
|
|
28
|
+
| `relationships` | 特殊关系配置:`userId`、`initialAffinity`、`relation`、`note` |
|
|
29
|
+
| `relationshipAffinityLevels` | 区间 → 称谓映射,默认提供“陌生人/友好/亲近/挚友” |
|
|
30
|
+
| `registerAffinityTool` / `registerRelationshipTool` | 是否注册对应 ChatLuna 工具 |
|
|
31
|
+
|
|
32
|
+
## 工具调用
|
|
33
|
+
|
|
34
|
+
启用 `registerAffinityTool` 或 `registerRelationshipTool` 后,ChatLuna 代理可直接调用。
|
|
35
|
+
|
|
36
|
+
若 `relation` 不在区间表中,会按全局初始值创建一条“自定义关系”条目,后续可在面板中继续编辑。
|
|
37
|
+
|
|
38
|
+
## 数据存储
|
|
39
|
+
|
|
40
|
+
插件会在数据库里创建表 `chatluna_affinity_records`,字段包含:
|
|
41
|
+
|
|
42
|
+
- `platform` / `userId` / `selfId`
|
|
43
|
+
- `affinity` 与 `affinityInited`
|
|
44
|
+
- `relation` 与更新时间
|
|
45
|
+
|
|
46
|
+
## 调试
|
|
47
|
+
|
|
48
|
+
开启 `debugLogging` 后,控制台会输出:
|
|
49
|
+
|
|
50
|
+
- 触发判定:昵称集合、@ 命中、引用命中
|
|
51
|
+
- 模型返回的 `delta` 与解析日志
|
|
52
|
+
- 好感度更新 / 未变化 / 异常提示
|
|
53
|
+
|
|
54
|
+
方便快速定位触发条件和模型配置问题。
|
|
55
|
+
|
|
56
|
+
## 常见问题
|
|
57
|
+
|
|
58
|
+
1. **没有触发分析?**
|
|
59
|
+
- 确认 `enableAnalysis` 为 `true`,且消息满足私聊 / @ / 引用 / 昵称命中任一条件。
|
|
60
|
+
2. **工具调用失败?**
|
|
61
|
+
- 确认配置里开启了对应工具开关;日志会提示是否缺失用户 ID 或关系名称。
|
|
62
|
+
3. **如何清理数据?**
|
|
63
|
+
- 直接删除数据库中 `chatluna_affinity_records` 数据,或指定用户重新初始化即可。
|
|
64
|
+
|
|
65
|
+
## 许可协议
|
|
66
|
+
|
|
67
|
+
MIT License © 2024 chatluna-affinity contributors
|