koishi-plugin-rolecard 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/config.d.ts CHANGED
@@ -2,9 +2,59 @@
2
2
  * 插件配置 Schema 定义。
3
3
  *
4
4
  * `types.ts` 中的 `Config` 接口是运行时配置的类型契约,
5
- * 本文件则提供对应的 Koishi Schema 声明(默认值、校验、UI 描述),
6
- * 二者共同构成配置层。后续提升配置复杂度与通用度时,集中在此维护。
5
+ * 本文件提供对应的 Koishi Schema 声明(默认值、校验、UI 描述、条件联动)。
6
+ *
7
+ * 条件联动通过 `Schema.intersect` 实现:
8
+ * - 全局层:`enableRandom` / `enableImage` 为真时才显示对应概率项
9
+ * - 群聊层:勾选 `belikov` 角色卡时才显示「别里科夫专属配置组」
7
10
  */
8
11
  import { Schema } from 'koishi';
9
- import type { Config as ConfigType } from './types';
10
- export declare const Config: Schema<ConfigType>;
12
+ export declare const Config: Schema<{
13
+ cooldownWhitelist?: string[] | null;
14
+ } & import("cosmokit").Dict & ({
15
+ channels?: (({
16
+ channelId?: string | null;
17
+ botId?: string | null;
18
+ rolecards?: "belikov"[] | null;
19
+ } & import("cosmokit").Dict & {
20
+ rolecards?: string[] | null;
21
+ }) | ({
22
+ channelId?: string | null;
23
+ botId?: string | null;
24
+ rolecards?: "belikov"[] | null;
25
+ } & import("cosmokit").Dict & {}))[] | null;
26
+ } & ((({
27
+ enableRandom?: boolean | null;
28
+ } & import("cosmokit").Dict & {
29
+ enableRandom?: true | null;
30
+ randomProbability?: number | null;
31
+ }) | ({
32
+ enableRandom?: boolean | null;
33
+ } & import("cosmokit").Dict & {})) & (({
34
+ enableImage?: boolean | null;
35
+ } & import("cosmokit").Dict & {
36
+ enableImage?: true | null;
37
+ imageProbability?: number | null;
38
+ }) | ({
39
+ enableImage?: boolean | null;
40
+ } & import("cosmokit").Dict & {})))), {
41
+ cooldownWhitelist: string[];
42
+ } & import("cosmokit").Dict & ({
43
+ channels: ({
44
+ channelId: string;
45
+ botId: string;
46
+ rolecards: "belikov"[];
47
+ } & import("cosmokit").Dict & (Schemastery.ObjectT<{
48
+ rolecards: Schema<string[], string[]>;
49
+ }> | Schemastery.ObjectT<{}>))[];
50
+ } & (({
51
+ enableRandom: boolean;
52
+ } & import("cosmokit").Dict & (Schemastery.ObjectT<{
53
+ enableRandom: Schema<true, true>;
54
+ randomProbability: Schema<number, number>;
55
+ }> | Schemastery.ObjectT<{}>)) & ({
56
+ enableImage: boolean;
57
+ } & import("cosmokit").Dict & (Schemastery.ObjectT<{
58
+ enableImage: Schema<true, true>;
59
+ imageProbability: Schema<number, number>;
60
+ }> | Schemastery.ObjectT<{}>))))>;
package/lib/config.js CHANGED
@@ -3,48 +3,136 @@
3
3
  * 插件配置 Schema 定义。
4
4
  *
5
5
  * `types.ts` 中的 `Config` 接口是运行时配置的类型契约,
6
- * 本文件则提供对应的 Koishi Schema 声明(默认值、校验、UI 描述),
7
- * 二者共同构成配置层。后续提升配置复杂度与通用度时,集中在此维护。
6
+ * 本文件提供对应的 Koishi Schema 声明(默认值、校验、UI 描述、条件联动)。
7
+ *
8
+ * 条件联动通过 `Schema.intersect` 实现:
9
+ * - 全局层:`enableRandom` / `enableImage` 为真时才显示对应概率项
10
+ * - 群聊层:勾选 `belikov` 角色卡时才显示「别里科夫专属配置组」
8
11
  */
9
12
  Object.defineProperty(exports, "__esModule", { value: true });
10
13
  exports.Config = void 0;
11
14
  const koishi_1 = require("koishi");
12
- exports.Config = koishi_1.Schema.object({
13
- rolecard: koishi_1.Schema.string()
14
- .default('')
15
- .description('要加载的角色卡 ID(留空则自动加载第一个找到的角色卡)。例如:belikov'),
16
- cooldown: koishi_1.Schema.number()
17
- .default(60)
18
- .min(0)
19
- .description('冷却时间(秒),同一频道内两次触发的最小间隔,防止刷屏'),
20
- cooldownWhitelist: koishi_1.Schema.array(koishi_1.Schema.string())
21
- .default([])
22
- .description('冷却白名单:填入用户 ID,这些用户的消息不受冷却限制'),
23
- disabledTags: koishi_1.Schema.array(koishi_1.Schema.string())
24
- .default([])
25
- .description('禁用的触发标签(留空表示全部启用)。例如填入 emotional 可关闭「情绪」类触发'),
26
- enableRandom: koishi_1.Schema.boolean()
27
- .default(true)
28
- .description('启用全部消息概率随机触发(神预言效果)'),
29
- randomProbability: koishi_1.Schema.number()
30
- .default(3)
31
- .min(0)
32
- .max(100)
33
- .description('随机触发概率(0-100,3 表示 3%)'),
34
- enableImage: koishi_1.Schema.boolean().default(true).description('启用角色卡插图'),
35
- imageWithMessage: koishi_1.Schema.boolean()
36
- .default(true)
37
- .description('将图片与文字一起发送(关闭则图片作为单独消息发送)'),
38
- imageProbability: koishi_1.Schema.number()
39
- .default(100)
40
- .min(0)
41
- .max(100)
42
- .description('附带图片的概率(0-100,100 表示每次触发都发图)'),
43
- respondIn: koishi_1.Schema.union([
44
- koishi_1.Schema.const('group').description('仅群聊'),
45
- koishi_1.Schema.const('private').description('仅私聊'),
46
- koishi_1.Schema.const('both').description('群聊与私聊'),
47
- ])
48
- .default('group')
49
- .description('响应范围'),
15
+ // ──────────────────────────────────────────────────────────────
16
+ // 别里科夫专属配置
17
+ // ──────────────────────────────────────────────────────────────
18
+ /** 别里科夫的标签选项(与 trigger-words.json 中的 groups 对应)。 */
19
+ const belikovTagOptions = [
20
+ { tag: 'proposal', name: '提议与搞事' },
21
+ { tag: 'emotional', name: '情绪波动与违规边缘' },
22
+ { tag: 'rules', name: '规章制度与日常通知' },
23
+ { tag: 'distancing', name: '撇清关系与甩锅' },
24
+ ];
25
+ /** 别里科夫专属配置组 Schema。 */
26
+ const BelikovConfig = koishi_1.Schema.object({
27
+ belikov: koishi_1.Schema.object({
28
+ cooldown: koishi_1.Schema.number()
29
+ .default(60)
30
+ .min(0)
31
+ .description('别里科夫冷却时间(秒),该群聊内两次触发的最小间隔'),
32
+ tags: koishi_1.Schema.array(koishi_1.Schema.object({
33
+ tag: koishi_1.Schema.string().required().description('标签名'),
34
+ name: koishi_1.Schema.string().required().description('标签别称'),
35
+ enabled: koishi_1.Schema.boolean()
36
+ .default(true)
37
+ .description('是否启用该标签触发'),
38
+ }))
39
+ .role('table')
40
+ .default(belikovTagOptions.map((t) => ({ ...t, enabled: true })))
41
+ .description('启用的触发标签:逐项控制每个标签是否在该群聊生效'),
42
+ })
43
+ .description('别里科夫专属配置'),
50
44
  });
45
+ // ──────────────────────────────────────────────────────────────
46
+ // 群聊配置项
47
+ // ──────────────────────────────────────────────────────────────
48
+ /** 可选角色卡列表(后续扩展新角色卡时在此追加)。 */
49
+ const rolecardChoices = [
50
+ koishi_1.Schema.const('belikov').description('别里科夫 · 套中人'),
51
+ // Schema.const('newcharacter').description('新角色 · ...'),
52
+ ];
53
+ /**
54
+ * 单个群聊配置项。
55
+ *
56
+ * 使用 Schema.intersect 实现条件联动:
57
+ * 基础字段始终显示;当 rolecards 含 'belikov' 时,别里科夫专属配置组才显示。
58
+ */
59
+ const ChannelItem = koishi_1.Schema.intersect([
60
+ koishi_1.Schema.object({
61
+ channelId: koishi_1.Schema.string()
62
+ .required()
63
+ .description('群号或频道号'),
64
+ botId: koishi_1.Schema.string()
65
+ .default('')
66
+ .description('启用的机器人 selfId(留空表示不限定)'),
67
+ rolecards: koishi_1.Schema.array(koishi_1.Schema.union(rolecardChoices))
68
+ .role('checkbox')
69
+ .default(['belikov'])
70
+ .description('启用的角色卡(可多选)'),
71
+ }),
72
+ koishi_1.Schema.union([
73
+ koishi_1.Schema.object({
74
+ rolecards: koishi_1.Schema.array(koishi_1.Schema.string()).required(),
75
+ ...BelikovConfig.dict,
76
+ }),
77
+ koishi_1.Schema.object({}),
78
+ ]),
79
+ ]).description('群聊配置');
80
+ // ──────────────────────────────────────────────────────────────
81
+ // 全局配置
82
+ // ──────────────────────────────────────────────────────────────
83
+ exports.Config = koishi_1.Schema.intersect([
84
+ // ── 基础配置 ──
85
+ koishi_1.Schema.object({
86
+ cooldownWhitelist: koishi_1.Schema.array(koishi_1.Schema.string())
87
+ .default([])
88
+ .role('table')
89
+ .description('冷却白名单:填入用户 ID,这些用户的消息不受冷却限制(用于 Debug)'),
90
+ }),
91
+ // ── 多群聊配置 ──
92
+ koishi_1.Schema.object({
93
+ channels: koishi_1.Schema.array(ChannelItem)
94
+ .role('table')
95
+ .default([])
96
+ .description('各群聊/频道的独立配置。未列出的群聊将使用全局默认行为'),
97
+ }),
98
+ // ── 全局触发概率配置(条件联动) ──
99
+ koishi_1.Schema.intersect([
100
+ koishi_1.Schema.object({
101
+ enableRandom: koishi_1.Schema.boolean()
102
+ .default(false)
103
+ .description('启用全部消息概率随机触发(神预言效果)'),
104
+ }),
105
+ koishi_1.Schema.union([
106
+ koishi_1.Schema.object({
107
+ enableRandom: koishi_1.Schema.const(true).required(),
108
+ randomProbability: koishi_1.Schema.number()
109
+ .default(0.03)
110
+ .min(0)
111
+ .max(1)
112
+ .step(0.01)
113
+ .description('随机触发概率(0-1,如 0.03 表示 3%)'),
114
+ }),
115
+ koishi_1.Schema.object({}),
116
+ ]),
117
+ ]),
118
+ // ── 插图配置(条件联动) ──
119
+ koishi_1.Schema.intersect([
120
+ koishi_1.Schema.object({
121
+ enableImage: koishi_1.Schema.boolean()
122
+ .default(false)
123
+ .description('启用角色卡插图'),
124
+ }),
125
+ koishi_1.Schema.union([
126
+ koishi_1.Schema.object({
127
+ enableImage: koishi_1.Schema.const(true).required(),
128
+ imageProbability: koishi_1.Schema.number()
129
+ .default(1)
130
+ .min(0)
131
+ .max(1)
132
+ .step(0.01)
133
+ .description('附带图片的概率(0-1,如 1 表示每次触发都发图)'),
134
+ }),
135
+ koishi_1.Schema.object({}),
136
+ ]),
137
+ ]),
138
+ ]);
package/lib/core.d.ts CHANGED
@@ -1,15 +1,14 @@
1
1
  /**
2
2
  * 角色卡核心引擎。
3
3
  *
4
- * `RolecardEngine` 是与具体角色完全解耦的通用台词引擎。它接收一个已加载的
5
- * `Rolecard`(纯数据)和 `Config`(运行时参数),负责:
4
+ * `RolecardEngine` 是与具体角色完全解耦的通用台词引擎。每个引擎实例绑定
5
+ * 一个角色卡 + 一个群聊配置,负责:
6
6
  *
7
7
  * 1. 将台词按标签索引,供关键词命中后随机取用
8
8
  * 2. 按优先级匹配消息中的触发词
9
9
  * 3. 未命中关键词时按概率随机触发
10
10
  * 4. 分频道冷却控制(支持白名单豁免)
11
- * 5. 按配置决定是否附带插图、插图发送方式
12
- * 6. 按配置限定响应范围(群聊 / 私聊 / 两者)
11
+ * 5. 按配置决定是否附带插图
13
12
  *
14
13
  * 任何新角色卡只要提供符合 `types.ts` 契约的数据文件即可被引擎驱动,
15
14
  * 无需修改本模块任何代码。
@@ -17,6 +16,7 @@
17
16
  import type { Context } from 'koishi';
18
17
  import type { Config, Rolecard } from './types';
19
18
  export declare class RolecardEngine {
19
+ private readonly rolecard;
20
20
  private readonly config;
21
21
  private readonly logger;
22
22
  private readonly quotesByTag;
@@ -26,4 +26,19 @@ export declare class RolecardEngine {
26
26
  private readonly lastTrigger;
27
27
  constructor(ctx: Context, rolecard: Rolecard, config: Config);
28
28
  private handle;
29
+ /**
30
+ * 获取该群聊的冷却时间。
31
+ * 若角色卡有专属配置(如别里科夫)则取其 cooldown,否则返回 0(不冷却)。
32
+ */
33
+ private getChannelCooldown;
34
+ /**
35
+ * 获取该群聊禁用的触发标签。
36
+ * 若角色卡有专属配置(如别里科夫)则从其 tags 表中取 enabled=false 的标签。
37
+ */
38
+ private getDisabledTags;
39
+ /**
40
+ * 获取别里科夫专属配置(仅当当前引擎的角色卡是别里科夫时)。
41
+ * 调用方需确保 config.channels 中存在对应 channelId 的配置。
42
+ */
43
+ private getBelikovConfig;
29
44
  }
package/lib/core.js CHANGED
@@ -2,15 +2,14 @@
2
2
  /**
3
3
  * 角色卡核心引擎。
4
4
  *
5
- * `RolecardEngine` 是与具体角色完全解耦的通用台词引擎。它接收一个已加载的
6
- * `Rolecard`(纯数据)和 `Config`(运行时参数),负责:
5
+ * `RolecardEngine` 是与具体角色完全解耦的通用台词引擎。每个引擎实例绑定
6
+ * 一个角色卡 + 一个群聊配置,负责:
7
7
  *
8
8
  * 1. 将台词按标签索引,供关键词命中后随机取用
9
9
  * 2. 按优先级匹配消息中的触发词
10
10
  * 3. 未命中关键词时按概率随机触发
11
11
  * 4. 分频道冷却控制(支持白名单豁免)
12
- * 5. 按配置决定是否附带插图、插图发送方式
13
- * 6. 按配置限定响应范围(群聊 / 私聊 / 两者)
12
+ * 5. 按配置决定是否附带插图
14
13
  *
15
14
  * 任何新角色卡只要提供符合 `types.ts` 契约的数据文件即可被引擎驱动,
16
15
  * 无需修改本模块任何代码。
@@ -24,6 +23,7 @@ function pick(arr) {
24
23
  return arr[Math.floor(Math.random() * arr.length)];
25
24
  }
26
25
  class RolecardEngine {
26
+ rolecard;
27
27
  config;
28
28
  logger;
29
29
  quotesByTag = new Map();
@@ -32,6 +32,7 @@ class RolecardEngine {
32
32
  imageBuffer = null;
33
33
  lastTrigger = new Map();
34
34
  constructor(ctx, rolecard, config) {
35
+ this.rolecard = rolecard;
35
36
  this.config = config;
36
37
  this.logger = ctx.logger(`rolecard:${rolecard.manifest.id}`);
37
38
  // 按标签建立台词索引
@@ -64,12 +65,6 @@ class RolecardEngine {
64
65
  this.logger.info(`角色卡已加载:${rolecard.manifest.name}`);
65
66
  }
66
67
  async handle(session) {
67
- // 响应范围控制
68
- const isGroup = !!session.guildId;
69
- if (isGroup && this.config.respondIn === 'private')
70
- return;
71
- if (!isGroup && this.config.respondIn === 'group')
72
- return;
73
68
  // 忽略自身与空用户
74
69
  if (!session.userId || session.userId === session.selfId)
75
70
  return;
@@ -80,16 +75,18 @@ class RolecardEngine {
80
75
  return;
81
76
  const channelId = session.channelId ?? session.guildId ?? session.userId;
82
77
  const now = Date.now();
83
- // 冷却控制(白名单豁免)
78
+ // 冷却控制(全局白名单豁免)
84
79
  if (!this.config.cooldownWhitelist.includes(session.userId)) {
85
80
  const last = this.lastTrigger.get(channelId) ?? 0;
86
- if (now - last < this.config.cooldown * 1000)
81
+ const cooldown = this.getChannelCooldown();
82
+ if (cooldown > 0 && now - last < cooldown * 1000)
87
83
  return;
88
84
  }
89
85
  // 1. 按优先级匹配关键词
90
86
  let matchedTag = null;
87
+ const disabledTags = this.getDisabledTags();
91
88
  for (const g of this.groups) {
92
- if (this.config.disabledTags.includes(g.tag))
89
+ if (disabledTags.includes(g.tag))
93
90
  continue;
94
91
  if (g.matchMode === 'include' && g.keywords.some((k) => content.includes(k))) {
95
92
  matchedTag = g.tag;
@@ -102,7 +99,7 @@ class RolecardEngine {
102
99
  quote = pick(this.quotesByTag.get(matchedTag) ?? this.allQuotes);
103
100
  }
104
101
  else if (this.config.enableRandom &&
105
- Math.random() * 100 < this.config.randomProbability) {
102
+ Math.random() < this.config.randomProbability) {
106
103
  quote = pick(this.allQuotes);
107
104
  }
108
105
  if (!quote)
@@ -113,18 +110,12 @@ class RolecardEngine {
113
110
  try {
114
111
  const wantImage = this.config.enableImage &&
115
112
  !!this.imageBuffer &&
116
- Math.random() * 100 < this.config.imageProbability;
113
+ Math.random() < this.config.imageProbability;
117
114
  if (wantImage && this.imageBuffer) {
118
- if (this.config.imageWithMessage) {
119
- await session.send([
120
- koishi_1.h.text(quote.text),
121
- koishi_1.h.image(this.imageBuffer, 'image/png'),
122
- ]);
123
- }
124
- else {
125
- await session.send(quote.text);
126
- await session.send(koishi_1.h.image(this.imageBuffer, 'image/png'));
127
- }
115
+ await session.send([
116
+ koishi_1.h.text(quote.text),
117
+ koishi_1.h.image(this.imageBuffer, 'image/png'),
118
+ ]);
128
119
  }
129
120
  else {
130
121
  await session.send(quote.text);
@@ -134,5 +125,36 @@ class RolecardEngine {
134
125
  this.logger.warn('发送消息失败', e);
135
126
  }
136
127
  }
128
+ /**
129
+ * 获取该群聊的冷却时间。
130
+ * 若角色卡有专属配置(如别里科夫)则取其 cooldown,否则返回 0(不冷却)。
131
+ */
132
+ getChannelCooldown() {
133
+ const belikovCfg = this.getBelikovConfig();
134
+ if (belikovCfg)
135
+ return belikovCfg.cooldown;
136
+ return 0;
137
+ }
138
+ /**
139
+ * 获取该群聊禁用的触发标签。
140
+ * 若角色卡有专属配置(如别里科夫)则从其 tags 表中取 enabled=false 的标签。
141
+ */
142
+ getDisabledTags() {
143
+ const belikovCfg = this.getBelikovConfig();
144
+ if (!belikovCfg)
145
+ return [];
146
+ return belikovCfg.tags.filter((t) => !t.enabled).map((t) => t.tag);
147
+ }
148
+ /**
149
+ * 获取别里科夫专属配置(仅当当前引擎的角色卡是别里科夫时)。
150
+ * 调用方需确保 config.channels 中存在对应 channelId 的配置。
151
+ */
152
+ getBelikovConfig() {
153
+ if (this.rolecard.manifest.id !== 'belikov')
154
+ return null;
155
+ // 由 index.ts 注入的 ChannelConfig 通过闭包传递
156
+ // 这里简化处理:belikov 配置直接挂在 config 上(见 index.ts 的引擎实例化逻辑)
157
+ return this.config.__channelBelikov ?? null;
158
+ }
137
159
  }
138
160
  exports.RolecardEngine = RolecardEngine;
package/lib/index.d.ts CHANGED
@@ -4,18 +4,18 @@
4
4
  * 架构概览:
5
5
  *
6
6
  * - `types.ts` 共享类型契约(角色卡内容 + 运行时配置接口)
7
- * - `config.ts` 配置 Schema 声明(默认值、校验、UI 描述)
7
+ * - `config.ts` 配置 Schema 声明(默认值、校验、UI 描述、条件联动)
8
8
  * - `loader.ts` 角色卡加载器(扫描并解析 assets/ 目录)
9
9
  * - `core.ts` 核心引擎(通用台词引擎,与具体角色解耦)
10
10
  * - `index.ts` 本文件,组装以上模块并对接 Koishi 生命周期
11
11
  *
12
- * 角色卡是纯数据资源,存放在 `assets/<id>/` 下。用户通过 Config.rolecard
13
- * 选择要激活的角色卡。新增角色卡只需添加数据目录,无需改动任何源码。
12
+ * 角色卡是纯数据资源,存放在 `assets/<id>/` 下。用户通过各群聊配置中的
13
+ * `rolecards` 字段选择该群聊启用哪些角色卡。新增角色卡只需添加数据目录。
14
14
  */
15
15
  import type { Context } from 'koishi';
16
16
  import { Config } from './config';
17
17
  import type { Config as ConfigType } from './types';
18
18
  export declare const name = "rolecard";
19
- export declare const usage = "\n<div style=\"border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);\">\n <h2 style=\"margin-top: 0; color: #4a6ee0;\">\uD83C\uDFAD \u89D2\u8272\u5361\u63D2\u4EF6 \u00B7 Rolecard</h2>\n <p>\u6570\u636E\u9A71\u52A8\u7684\u89D2\u8272\u53F0\u8BCD\u5F15\u64CE\u3002\u6838\u5FC3\u903B\u8F91\u4E0E\u89D2\u8272\u5361\u5185\u5BB9\u5B8C\u5168\u89E3\u8026\u2014\u2014\u89D2\u8272\u5361\u662F\u7EAF\u6570\u636E\u8D44\u6E90\uFF08\u53F0\u8BCD\u5E93 + \u89E6\u53D1\u8BCD + \u63D2\u56FE\uFF09\uFF0C\u5F15\u64CE\u6839\u636E\u6570\u636E\u81EA\u52A8\u9A71\u52A8\u5BF9\u8BDD\u3002</p>\n <ul>\n <li>\u5728 <code>assets/</code> \u76EE\u5F55\u4E0B\u653E\u7F6E\u89D2\u8272\u5361\uFF0C\u6BCF\u4E2A\u89D2\u8272\u5361\u4E00\u4E2A\u5B50\u76EE\u5F55</li>\n <li>\u901A\u8FC7\u914D\u7F6E\u9879 <code>rolecard</code> \u9009\u62E9\u8981\u52A0\u8F7D\u7684\u89D2\u8272\u5361 ID</li>\n <li>\u652F\u6301\u5173\u952E\u8BCD\u89E6\u53D1\uFF08\u6309\u4F18\u5148\u7EA7\uFF09\u4E0E\u6982\u7387\u968F\u673A\u89E6\u53D1</li>\n <li>\u51B7\u5374\u9632\u5237\u5C4F\u3001\u63D2\u56FE\u53D1\u9001\u3001\u54CD\u5E94\u8303\u56F4\u5747\u53EF\u914D\u7F6E</li>\n </ul>\n <p>\uD83E\uDD1D \u60F3\u8D21\u732E\u65B0\u7684\u89D2\u8272\u5361\uFF1F\u6B22\u8FCE\u63D0\u4EA4 PR\uFF0C\u524D\u5F80 <a href=\"https://github.com/Oppenheymu/koishi-plugin-rolecard\" style=\"color:#4a6ee0;text-decoration:none;\">\u4ED3\u5E93</a> \u53C2\u4E0E\u5171\u5EFA</p>\n</div>\n\n<div style=\"border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);\">\n <h2 style=\"margin-top: 0; color: #4a6ee0;\">\uD83D\uDCAC \u4EA4\u6D41\u4E0E\u53CD\u9988</h2>\n <p>\uD83C\uDF1F \u559C\u6B22\u8FD9\u4E2A\u63D2\u4EF6\uFF1F\u6B22\u8FCE\u52A0\u5165 QQ \u7FA4 <a href=\"https://qm.qq.com/q/WngX4RQoca\" style=\"color:#4a6ee0;text-decoration:none;\"><strong>1071284605</strong></a>\u3010\u6653\u57FA\u5730\u63D2\u4EF6\u5DE5\u574A\u3011\u8FDB\u884C\u4EA4\u6D41</p>\n <p>\uD83D\uDC1B \u9047\u5230\u95EE\u9898\uFF1F\u6B22\u8FCE\u5728\u7FA4\u5185\u53CD\u9988\uFF0C\u6216\u70B9\u51FB <a href=\"https://qm.qq.com/q/WngX4RQoca\" style=\"color:#4a6ee0;text-decoration:none;\">\u6B64\u94FE\u63A5</a> \u52A0\u5165\u7FA4\u804A</p>\n</div>\n";
19
+ export declare const usage = "\n<div style=\"border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);\">\n <h2 style=\"margin-top: 0; color: #4a6ee0;\">\uD83C\uDFAD \u89D2\u8272\u5361\u63D2\u4EF6 \u00B7 Rolecard</h2>\n <p>\u6570\u636E\u9A71\u52A8\u7684\u89D2\u8272\u53F0\u8BCD\u5F15\u64CE\u3002\u6838\u5FC3\u903B\u8F91\u4E0E\u89D2\u8272\u5361\u5185\u5BB9\u5B8C\u5168\u89E3\u8026\u2014\u2014\u89D2\u8272\u5361\u662F\u7EAF\u6570\u636E\u8D44\u6E90\uFF08\u53F0\u8BCD\u5E93 + \u89E6\u53D1\u8BCD + \u63D2\u56FE\uFF09\uFF0C\u5F15\u64CE\u6839\u636E\u6570\u636E\u81EA\u52A8\u9A71\u52A8\u5BF9\u8BDD\u3002</p>\n <ul>\n <li>\u5728 <code>assets/</code> \u76EE\u5F55\u4E0B\u653E\u7F6E\u89D2\u8272\u5361\uFF0C\u6BCF\u4E2A\u89D2\u8272\u5361\u4E00\u4E2A\u5B50\u76EE\u5F55</li>\n <li>\u5728\u300C\u591A\u7FA4\u804A\u914D\u7F6E\u300D\u4E2D\u4E3A\u6BCF\u4E2A\u7FA4\u804A\u52FE\u9009\u8981\u542F\u7528\u7684\u89D2\u8272\u5361</li>\n <li>\u652F\u6301\u5173\u952E\u8BCD\u89E6\u53D1\uFF08\u6309\u4F18\u5148\u7EA7\uFF09\u4E0E\u6982\u7387\u968F\u673A\u89E6\u53D1</li>\n <li>\u51B7\u5374\u9632\u5237\u5C4F\u3001\u89E6\u53D1\u6807\u7B7E\u5F00\u5173\u3001\u63D2\u56FE\u53D1\u9001\u5747\u53EF\u6309\u7FA4\u804A\u72EC\u7ACB\u914D\u7F6E</li>\n </ul>\n <p>\uD83E\uDD1D \u60F3\u8D21\u732E\u65B0\u7684\u89D2\u8272\u5361\uFF1F\u6B22\u8FCE\u63D0\u4EA4 PR\uFF0C\u524D\u5F80 <a href=\"https://github.com/Oppenheymu/koishi-plugin-rolecard\" style=\"color:#4a6ee0;text-decoration:none;\">\u4ED3\u5E93</a> \u53C2\u4E0E\u5171\u5EFA</p>\n</div>\n\n<div style=\"border-radius: 10px; border: 1px solid #ddd; padding: 16px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);\">\n <h2 style=\"margin-top: 0; color: #4a6ee0;\">\uD83D\uDCAC \u4EA4\u6D41\u4E0E\u53CD\u9988</h2>\n <p>\uD83C\uDF1F \u559C\u6B22\u8FD9\u4E2A\u63D2\u4EF6\uFF1F\u6B22\u8FCE\u52A0\u5165 QQ \u7FA4 <a href=\"https://qm.qq.com/q/WngX4RQoca\" style=\"color:#4a6ee0;text-decoration:none;\"><strong>1071284605</strong></a>\u3010\u6653\u57FA\u5730\u63D2\u4EF6\u5DE5\u574A\u3011\u8FDB\u884C\u4EA4\u6D41</p>\n <p>\uD83D\uDC1B \u9047\u5230\u95EE\u9898\uFF1F\u6B22\u8FCE\u5728\u7FA4\u5185\u53CD\u9988\uFF0C\u6216\u70B9\u51FB <a href=\"https://qm.qq.com/q/WngX4RQoca\" style=\"color:#4a6ee0;text-decoration:none;\">\u6B64\u94FE\u63A5</a> \u52A0\u5165\u7FA4\u804A</p>\n</div>\n";
20
20
  export { Config };
21
21
  export declare function apply(ctx: Context, config: ConfigType): void;
package/lib/index.js CHANGED
@@ -5,13 +5,13 @@
5
5
  * 架构概览:
6
6
  *
7
7
  * - `types.ts` 共享类型契约(角色卡内容 + 运行时配置接口)
8
- * - `config.ts` 配置 Schema 声明(默认值、校验、UI 描述)
8
+ * - `config.ts` 配置 Schema 声明(默认值、校验、UI 描述、条件联动)
9
9
  * - `loader.ts` 角色卡加载器(扫描并解析 assets/ 目录)
10
10
  * - `core.ts` 核心引擎(通用台词引擎,与具体角色解耦)
11
11
  * - `index.ts` 本文件,组装以上模块并对接 Koishi 生命周期
12
12
  *
13
- * 角色卡是纯数据资源,存放在 `assets/<id>/` 下。用户通过 Config.rolecard
14
- * 选择要激活的角色卡。新增角色卡只需添加数据目录,无需改动任何源码。
13
+ * 角色卡是纯数据资源,存放在 `assets/<id>/` 下。用户通过各群聊配置中的
14
+ * `rolecards` 字段选择该群聊启用哪些角色卡。新增角色卡只需添加数据目录。
15
15
  */
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.Config = exports.usage = exports.name = void 0;
@@ -28,9 +28,9 @@ exports.usage = `
28
28
  <p>数据驱动的角色台词引擎。核心逻辑与角色卡内容完全解耦——角色卡是纯数据资源(台词库 + 触发词 + 插图),引擎根据数据自动驱动对话。</p>
29
29
  <ul>
30
30
  <li>在 <code>assets/</code> 目录下放置角色卡,每个角色卡一个子目录</li>
31
- <li>通过配置项 <code>rolecard</code> 选择要加载的角色卡 ID</li>
31
+ <li>在「多群聊配置」中为每个群聊勾选要启用的角色卡</li>
32
32
  <li>支持关键词触发(按优先级)与概率随机触发</li>
33
- <li>冷却防刷屏、插图发送、响应范围均可配置</li>
33
+ <li>冷却防刷屏、触发标签开关、插图发送均可按群聊独立配置</li>
34
34
  </ul>
35
35
  <p>🤝 想贡献新的角色卡?欢迎提交 PR,前往 <a href="https://github.com/Oppenheymu/koishi-plugin-rolecard" style="color:#4a6ee0;text-decoration:none;">仓库</a> 参与共建</p>
36
36
  </div>
@@ -50,17 +50,35 @@ function apply(ctx, config) {
50
50
  logger.warn('未找到任何角色卡,请检查 assets 目录');
51
51
  return;
52
52
  }
53
- const availableIds = rolecards.map((r) => r.manifest.id).join(', ');
54
- logger.info(`发现角色卡:${availableIds}`);
55
- // 按配置选择角色卡;留空则取第一个
56
- const selected = config.rolecard
57
- ? rolecards.filter((r) => r.manifest.id === config.rolecard)
58
- : [rolecards[0]];
59
- if (selected.length === 0) {
60
- logger.warn(`未找到角色卡 "${config.rolecard}",可用:${availableIds}`);
61
- return;
53
+ const rolecardMap = new Map(rolecards.map((r) => [r.manifest.id, r]));
54
+ logger.info(`发现角色卡:${[...rolecardMap.keys()].join(', ')}`);
55
+ // 按 channelId 索引群聊配置
56
+ const channelMap = new Map();
57
+ for (const ch of config.channels) {
58
+ if (ch.channelId)
59
+ channelMap.set(ch.channelId, ch);
62
60
  }
63
- for (const rolecard of selected) {
64
- new core_1.RolecardEngine(ctx, rolecard, config);
61
+ // 为每个群聊配置中启用的角色卡创建引擎实例
62
+ for (const ch of config.channels) {
63
+ if (!ch.channelId)
64
+ continue;
65
+ for (const cardId of ch.rolecards) {
66
+ const rolecard = rolecardMap.get(cardId);
67
+ if (!rolecard) {
68
+ logger.warn(`群聊 ${ch.channelId}:未找到角色卡 "${cardId}",跳过`);
69
+ continue;
70
+ }
71
+ // 将群聊级专属配置注入到引擎可访问的位置
72
+ // 别里科夫专属配置通过 __channelBelikov 传递
73
+ const engineConfig = {
74
+ ...config,
75
+ __channelBelikov: ch.belikov,
76
+ };
77
+ // 用子作用域隔离各引擎,便于按 channelId / botId 过滤消息
78
+ let subCtx = ctx.guild(ch.channelId);
79
+ if (ch.botId)
80
+ subCtx = subCtx.self(ch.botId);
81
+ new core_1.RolecardEngine(subCtx, rolecard, engineConfig);
82
+ }
65
83
  }
66
84
  }
package/lib/types.d.ts CHANGED
@@ -76,26 +76,45 @@ export interface Rolecard {
76
76
  /** 角色卡目录绝对路径。 */
77
77
  dir: string;
78
78
  }
79
+ /** 别里科夫专属的触发标签启用项。 */
80
+ export interface BelikovTagConfig {
81
+ /** 标签名,如 `proposal`、`emotional`。 */
82
+ tag: string;
83
+ /** 标签显示别称,如「提议与搞事」。 */
84
+ name: string;
85
+ /** 是否启用该标签触发。 */
86
+ enabled: boolean;
87
+ }
88
+ /** 别里科夫角色卡专属配置(仅在群聊勾选别里科夫时生效)。 */
89
+ export interface BelikovChannelConfig {
90
+ /** 冷却时间(秒),该群聊内两次触发的最小间隔。 */
91
+ cooldown: number;
92
+ /** 触发标签启用表,逐项控制每个标签是否在该群聊生效。 */
93
+ tags: BelikovTagConfig[];
94
+ }
95
+ /** 单个群聊/频道的配置项。 */
96
+ export interface ChannelConfig {
97
+ /** 群号或频道号。 */
98
+ channelId: string;
99
+ /** 启用的机器人 selfId,留空表示不限定。 */
100
+ botId: string;
101
+ /** 启用的角色卡 ID 列表,如 ['belikov']。 */
102
+ rolecards: string[];
103
+ /** 别里科夫专属配置(仅当 rolecards 含 'belikov' 时有效)。 */
104
+ belikov?: BelikovChannelConfig;
105
+ }
79
106
  /** 插件运行时配置。所有行为参数统一由此处控制,与角色卡内容解耦。 */
80
107
  export interface Config {
81
- /** 要激活的角色卡 ID,留空则自动加载第一个找到的角色卡。 */
82
- rolecard: string;
83
- /** 冷却时间(秒),同一频道内两次触发的最小间隔。 */
84
- cooldown: number;
85
- /** 冷却白名单用户 ID,这些用户的消息不受冷却限制。 */
108
+ /** 冷却白名单用户 ID,这些用户的消息不受任何冷却限制(用于 Debug)。 */
86
109
  cooldownWhitelist: string[];
87
- /** 禁用的触发标签,留空表示全部启用。 */
88
- disabledTags: string[];
110
+ /** 各群聊/频道的独立配置列表。 */
111
+ channels: ChannelConfig[];
89
112
  /** 是否启用全部消息概率随机触发。 */
90
113
  enableRandom: boolean;
91
- /** 随机触发概率(0-100)。 */
114
+ /** 随机触发概率(0-1)。仅在 enableRandom 为真时生效。 */
92
115
  randomProbability: number;
93
116
  /** 是否启用角色卡插图。 */
94
117
  enableImage: boolean;
95
- /** 图片与文字是否作为同一条消息发送。 */
96
- imageWithMessage: boolean;
97
- /** 附带图片的概率(0-100)。 */
118
+ /** 附带图片的概率(0-1)。仅在 enableImage 为真时生效。 */
98
119
  imageProbability: number;
99
- /** 响应范围:群聊 / 私聊 / 两者。 */
100
- respondIn: 'group' | 'private' | 'both';
101
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-rolecard",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "contributors": [
5
5
  "Oppenheymu <oppenheymu@gmail.com>"
6
6
  ],