koishi-plugin-chatluna-affinity 0.0.4 → 0.0.6

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.
@@ -318,4 +318,4 @@ function createAffinityStore(ctx, config, log) {
318
318
  };
319
319
  }
320
320
 
321
- module.exports = { createAffinityStore };
321
+ module.exports = { createAffinityStore, MODEL_NAME };
package/lib/history.js CHANGED
@@ -42,25 +42,6 @@ function createHistoryManager(ctx, config, log) {
42
42
  cache.set(key, list);
43
43
  }
44
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
45
  async function readFromCache(session, count) {
65
46
  const cached = cache.get(makeKey(session));
66
47
  if (cached?.length) return normalizeEntries(cached, count);
@@ -81,8 +62,6 @@ function createHistoryManager(ctx, config, log) {
81
62
  async function fetch(session) {
82
63
  const count = config.historyMessageCount || 0;
83
64
  if (count <= 0) return [];
84
- const serviceEntries = await readFromService(session, count);
85
- if (serviceEntries.length) return serviceEntries;
86
65
  return readFromCache(session, count);
87
66
  }
88
67
 
package/lib/index.d.ts CHANGED
@@ -38,7 +38,10 @@ export interface Config {
38
38
  note: string;
39
39
  }>;
40
40
  registerAffinityTool: boolean;
41
+ registerBlacklistTool: boolean;
41
42
  registerRelationshipTool: boolean;
43
+ rankDefaultLimit: number;
44
+ rankRenderAsImage: boolean;
42
45
  }
43
46
  export declare const Config: Schema<Config>;
44
47
  export declare const inject: string[];
package/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ const { h } = require('koishi');
1
2
  const { modelSchema } = require('koishi-plugin-chatluna/utils/schema');
2
3
  const { ChatLunaPlugin } = require('koishi-plugin-chatluna/services/chat');
3
4
  const { getMessageContent } = require('koishi-plugin-chatluna/utils/string');
@@ -5,7 +6,7 @@ const { getMessageContent } = require('koishi-plugin-chatluna/utils/string');
5
6
  const { Config, inject, name } = require('./schema');
6
7
  const { createLogger } = require('./logger');
7
8
  const { renderTemplate } = require('./template');
8
- const { createAffinityStore } = require('./affinity-store');
9
+ const { createAffinityStore, MODEL_NAME } = require('./affinity-store');
9
10
  const { createHistoryManager } = require('./history');
10
11
  const { createAffinityCache } = require('./cache');
11
12
  const { createAffinityProvider, createRelationshipProvider } = require('./providers');
@@ -63,6 +64,12 @@ function apply(ctx, config) {
63
64
  createTool: registry.createRelationshipTool
64
65
  });
65
66
  }
67
+ if (config.registerBlacklistTool) {
68
+ plugin.registerTool('adjust_blacklist', {
69
+ selector: registry.blacklistSelector,
70
+ createTool: registry.createBlacklistTool
71
+ });
72
+ }
66
73
  });
67
74
 
68
75
  const middleware = createAnalysisMiddleware(ctx, config, {
@@ -76,6 +83,138 @@ function apply(ctx, config) {
76
83
  });
77
84
 
78
85
  ctx.middleware(middleware);
86
+
87
+ const renderRankImage = async (lines) => {
88
+ const puppeteer = ctx.puppeteer;
89
+ if (!puppeteer?.page) return null;
90
+ const html = `<!DOCTYPE html>
91
+ <html lang="zh-CN">
92
+ <head>
93
+ <meta charset="utf-8" />
94
+ <style>
95
+ body {
96
+ margin: 0;
97
+ font-family: "Segoe UI", "Helvetica Neue", PingFangSC, "Microsoft Yahei", sans-serif;
98
+ background: #ffffff;
99
+ color: #111111;
100
+ }
101
+ .container {
102
+ padding: 20px 24px;
103
+ }
104
+ h1 {
105
+ font-size: 18px;
106
+ margin: 0 0 16px;
107
+ font-weight: 600;
108
+ }
109
+ table {
110
+ border-collapse: collapse;
111
+ width: 100%;
112
+ min-width: 360px;
113
+ font-size: 14px;
114
+ }
115
+ th, td {
116
+ padding: 8px 12px;
117
+ border-bottom: 1px solid #e5e5e5;
118
+ text-align: left;
119
+ white-space: nowrap;
120
+ }
121
+ th {
122
+ background: #f5f7fa;
123
+ font-weight: 600;
124
+ }
125
+ tr:nth-child(odd) td {
126
+ background: #fbfcfe;
127
+ }
128
+ </style>
129
+ </head>
130
+ <body>
131
+ <div class="container" id="rank-root">
132
+ <h1>好感度排行</h1>
133
+ <table>
134
+ <thead>
135
+ <tr><th>排名</th><th>用户名</th><th>关系</th><th>好感度</th></tr>
136
+ </thead>
137
+ <tbody>
138
+ ${lines.map((line, index) => {
139
+ const [name, relation, affinity] = line;
140
+ return `<tr><td>${index + 1}</td><td>${name}</td><td>${relation}</td><td>${affinity}</td></tr>`;
141
+ }).join('')}
142
+ </tbody>
143
+ </table>
144
+ </div>
145
+ </body>
146
+ </html>`;
147
+
148
+ let page;
149
+ try {
150
+ page = await puppeteer.page();
151
+ await page.setViewport({ width: 600, height: 200 + lines.length * 32 });
152
+ await page.setContent(html, { waitUntil: 'networkidle0' });
153
+ const element = await page.$('#rank-root');
154
+ if (!element) return null;
155
+ const buffer = await element.screenshot({ omitBackground: false });
156
+ return buffer;
157
+ } catch (error) {
158
+ ctx.logger?.warn?.('排行榜图片渲染失败', error);
159
+ return null;
160
+ } finally {
161
+ try {
162
+ await page?.close();
163
+ } catch (_) {
164
+ // ignore
165
+ }
166
+ }
167
+ };
168
+
169
+ ctx.command('affinity.rank [limit:number] [platform:string] [image]', '查看当前好感度排行', { authority: 1 })
170
+ .alias('好感度排行')
171
+ .usage('affinity.rank [人数] [平台] [image|text]')
172
+ .action(async ({ session }, limitArg, platformArg, imageArg) => {
173
+ const parsedLimit = Number(limitArg);
174
+ const limit = Math.max(1, Math.min(Number.isFinite(parsedLimit) ? parsedLimit : config.rankDefaultLimit, 50));
175
+
176
+ const shouldRenderImage = (() => {
177
+ if (imageArg === undefined) return !!config.rankRenderAsImage;
178
+ const lower = String(imageArg).toLowerCase();
179
+ if (['0', 'false', 'text', 'no', 'n'].includes(lower)) return false;
180
+ if (['1', 'true', 'image', 'img', 'yes', 'y'].includes(lower)) return true;
181
+ return !!config.rankRenderAsImage;
182
+ })();
183
+
184
+ if (shouldRenderImage && (!ctx.puppeteer || typeof ctx.puppeteer.page !== 'function')) {
185
+ return '当前环境未启用 puppeteer,已改为文本模式(可安装 koishi-plugin-puppeteer 或传入 text)。';
186
+ }
187
+
188
+ const conditions = {};
189
+ const platform = platformArg || session?.platform;
190
+ if (platform) conditions.platform = platform;
191
+ if (session?.selfId) conditions.selfId = session.selfId;
192
+
193
+ const query = ctx.database
194
+ .select(MODEL_NAME)
195
+ .project(['platform', 'selfId', 'userId', 'nickname', 'affinity', 'relation'])
196
+ .orderBy('affinity', 'desc')
197
+ .limit(limit);
198
+ if (Object.keys(conditions).length) query.where(conditions);
199
+
200
+ const rows = await query.execute();
201
+ if (!rows.length) return '当前暂无好感度记录。';
202
+
203
+ const lines = rows.map((row) => {
204
+ const name = row.nickname || row.userId;
205
+ const relation = row.relation || '——';
206
+ return [name, relation, row.affinity];
207
+ });
208
+ const textLines = ['用户名 关系 好感度', ...lines.map((item, index) => `${index + 1}. ${item[0]} ${item[1]} ${item[2]}`)];
209
+
210
+ if (shouldRenderImage) {
211
+ const buffer = await renderRankImage(lines);
212
+ if (buffer) return h.image(buffer, 'image/png');
213
+ ctx.logger?.warn?.('排行榜图片渲染失败或服务缺失,已改为文本输出');
214
+ return textLines.join('\n');
215
+ }
216
+ return textLines.join('\n');
217
+ });
79
218
  }
80
219
 
81
220
  module.exports = { apply, Config, inject, name };
package/lib/schema.js CHANGED
@@ -4,7 +4,7 @@ const name = 'chatluna-affinity';
4
4
 
5
5
  const inject = {
6
6
  required: ['chatluna', 'database'],
7
- optional: ['chatluna_group_analysis_message', 'chatluna_group_analysis']
7
+ optional: ['puppeteer']
8
8
  };
9
9
 
10
10
  const AffinitySchema = Schema.object({
@@ -44,7 +44,10 @@ const AffinitySchema = Schema.object({
44
44
  .default('你是一位温暖可靠的伙伴,会根据好感度高低调整语气:好感度越高越亲近,越低越保持礼貌。')
45
45
  .description('补充的人设提示词,会注入到分析提示词中'),
46
46
  debugLogging: Schema.boolean().default(false).description('输出调试日志'),
47
- registerAffinityTool: Schema.boolean().default(false).description('注册 ChatLuna 工具:调整好感度')
47
+ registerAffinityTool: Schema.boolean().default(false).description('注册 ChatLuna 工具:调整好感度'),
48
+ registerBlacklistTool: Schema.boolean().default(false).description('注册 ChatLuna 工具:管理黑名单'),
49
+ rankDefaultLimit: Schema.number().default(10).min(1).max(50).description('排行榜默认展示人数'),
50
+ rankRenderAsImage: Schema.boolean().default(false).description('排行榜默认渲染为图片')
48
51
  }).description('好感度设置');
49
52
 
50
53
  const RelationshipSchema = Schema.object({
package/lib/tools.js CHANGED
@@ -69,6 +69,38 @@ function createRelationshipTool(options) {
69
69
  })();
70
70
  }
71
71
 
72
+ function createBlacklistTool(options) {
73
+ return new (class extends StructuredTool {
74
+ constructor() {
75
+ super({});
76
+ this.name = 'adjust_blacklist';
77
+ this.description = 'Add or remove a user from the affinity blacklist.';
78
+ this.schema = z.object({
79
+ action: z.enum(['add', 'remove']).describe('Add or remove the user from blacklist'),
80
+ targetUserId: z.string().describe('Target user ID'),
81
+ platform: z.string().optional().describe('Target platform; defaults to current session'),
82
+ note: z.string().optional().describe('Optional note when adding to blacklist')
83
+ });
84
+ }
85
+ async _call(input, _manager, runnable) {
86
+ const session = runnable?.configurable?.session;
87
+ const platform = input.platform || session?.platform;
88
+ const userId = input.targetUserId;
89
+ if (!platform || !userId) return 'Missing platform or user ID. Unable to adjust blacklist.';
90
+ if (input.action === 'add') {
91
+ options.store.recordBlacklist(platform, userId, input.note ?? 'tool');
92
+ options.cache.clear(platform, userId);
93
+ return `User ${platform}/${userId} added to blacklist.`;
94
+ }
95
+ const removed = options.store.removeBlacklist(platform, userId);
96
+ options.cache.clear(platform, userId);
97
+ return removed
98
+ ? `User ${platform}/${userId} removed from blacklist.`
99
+ : `User ${platform}/${userId} not found in blacklist.`;
100
+ }
101
+ })();
102
+ }
103
+
72
104
  function createToolRegistry(config, store, cache) {
73
105
  const options = {
74
106
  clamp: store.clamp,
@@ -80,6 +112,7 @@ function createToolRegistry(config, store, cache) {
80
112
  save: store.save,
81
113
  cache,
82
114
  updateRelationshipConfig: store.updateRelationshipConfig,
115
+ store,
83
116
  min: config.min,
84
117
  max: config.max
85
118
  };
@@ -87,8 +120,10 @@ function createToolRegistry(config, store, cache) {
87
120
  return {
88
121
  affinitySelector: () => true,
89
122
  relationshipSelector: () => true,
123
+ blacklistSelector: () => true,
90
124
  createAffinityTool: () => createAffinityTool(options),
91
- createRelationshipTool: () => createRelationshipTool(options)
125
+ createRelationshipTool: () => createRelationshipTool(options),
126
+ createBlacklistTool: () => createBlacklistTool(options)
92
127
  };
93
128
  }
94
129
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-chatluna-affinity",
3
3
  "description": "为 ChatLuna 提供{好感度}与{关系}变量并提供对应的工具调用和低好感自动拉黑功能。仅测试过伪装插件。",
4
- "version": "0.0.4",
4
+ "version": "0.0.6",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
package/readme.md CHANGED
@@ -13,6 +13,7 @@
13
13
  - 调整工具:
14
14
  - `adjust_affinity` 将好感度设为指定数值,并同步关系区间。
15
15
  - `adjust_relationship` 将关系切换为自定义称谓,同时把好感度调至对应区间的下限。
16
+ - `adjust_blacklist` 管理自动拉黑名单,可快速拉黑或解除指定用户。
16
17
  - 特殊关系:可为指定用户记录“初始好感度 + 自定义称谓”,工具操作会实时写回配置。
17
18
 
18
19
  ## 主要配置
@@ -35,7 +36,13 @@
35
36
  | `autoBlacklist` | 自动拉黑记录列表(自动维护,可在控制台查看与编辑) |
36
37
  | `relationships` | 特殊关系配置:`userId`、`initialAffinity`、`relation`、`note` |
37
38
  | `relationshipAffinityLevels` | 区间 → 称谓映射,默认提供“陌生人/友好/亲近/挚友” |
38
- | `registerAffinityTool` / `registerRelationshipTool` | 是否注册对应 ChatLuna 工具 |
39
+ | `registerAffinityTool` / `registerRelationshipTool` / `registerBlacklistTool` | 是否注册对应 ChatLuna 工具 |
40
+ | `rankDefaultLimit` | 指令默认展示的排行榜人数 |
41
+ | `rankRenderAsImage` | 指令默认是否渲染排行榜为图片(需 `koishi-plugin-puppeteer`) |
42
+
43
+ ## 指令
44
+
45
+ - `affinity.rank [limit] [platform] [image|text]`:查看好感度排行,可指定人数、平台,并选择文本或图片输出(图片模式需 `koishi-plugin-puppeteer`)。
39
46
 
40
47
  ## 工具调用
41
48