koishi-plugin-rolecard 1.0.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/core.d.ts +29 -0
- package/lib/core.js +138 -0
- package/lib/index.d.ts +20 -0
- package/lib/index.js +96 -0
- package/lib/loader.d.ts +26 -0
- package/lib/loader.js +77 -0
- package/lib/types.d.ts +101 -0
- package/lib/types.js +8 -0
- package/package.json +49 -0
- package/readme.md +122 -0
- package/rolecards/belikov/image.png +0 -0
- package/rolecards/belikov/rolecard.json +6 -0
- package/rolecards/belikov/trigger-words.json +60 -0
- package/rolecards/belikov/words.json +52 -0
package/lib/core.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 角色卡核心引擎。
|
|
3
|
+
*
|
|
4
|
+
* `RolecardEngine` 是与具体角色完全解耦的通用台词引擎。它接收一个已加载的
|
|
5
|
+
* `Rolecard`(纯数据)和 `Config`(运行时参数),负责:
|
|
6
|
+
*
|
|
7
|
+
* 1. 将台词按标签索引,供关键词命中后随机取用
|
|
8
|
+
* 2. 按优先级匹配消息中的触发词
|
|
9
|
+
* 3. 未命中关键词时按概率随机触发
|
|
10
|
+
* 4. 分频道冷却控制(支持白名单豁免)
|
|
11
|
+
* 5. 按配置决定是否附带插图、插图发送方式
|
|
12
|
+
* 6. 按配置限定响应范围(群聊 / 私聊 / 两者)
|
|
13
|
+
*
|
|
14
|
+
* 任何新角色卡只要提供符合 `types.ts` 契约的数据文件即可被引擎驱动,
|
|
15
|
+
* 无需修改本模块任何代码。
|
|
16
|
+
*/
|
|
17
|
+
import type { Context } from 'koishi';
|
|
18
|
+
import type { Config, Rolecard } from './types';
|
|
19
|
+
export declare class RolecardEngine {
|
|
20
|
+
private readonly config;
|
|
21
|
+
private readonly logger;
|
|
22
|
+
private readonly quotesByTag;
|
|
23
|
+
private readonly allQuotes;
|
|
24
|
+
private readonly groups;
|
|
25
|
+
private imageBuffer;
|
|
26
|
+
private readonly lastTrigger;
|
|
27
|
+
constructor(ctx: Context, rolecard: Rolecard, config: Config);
|
|
28
|
+
private handle;
|
|
29
|
+
}
|
package/lib/core.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 角色卡核心引擎。
|
|
4
|
+
*
|
|
5
|
+
* `RolecardEngine` 是与具体角色完全解耦的通用台词引擎。它接收一个已加载的
|
|
6
|
+
* `Rolecard`(纯数据)和 `Config`(运行时参数),负责:
|
|
7
|
+
*
|
|
8
|
+
* 1. 将台词按标签索引,供关键词命中后随机取用
|
|
9
|
+
* 2. 按优先级匹配消息中的触发词
|
|
10
|
+
* 3. 未命中关键词时按概率随机触发
|
|
11
|
+
* 4. 分频道冷却控制(支持白名单豁免)
|
|
12
|
+
* 5. 按配置决定是否附带插图、插图发送方式
|
|
13
|
+
* 6. 按配置限定响应范围(群聊 / 私聊 / 两者)
|
|
14
|
+
*
|
|
15
|
+
* 任何新角色卡只要提供符合 `types.ts` 契约的数据文件即可被引擎驱动,
|
|
16
|
+
* 无需修改本模块任何代码。
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.RolecardEngine = void 0;
|
|
20
|
+
const node_fs_1 = require("node:fs");
|
|
21
|
+
const koishi_1 = require("koishi");
|
|
22
|
+
/** 从数组中随机取一个元素。 */
|
|
23
|
+
function pick(arr) {
|
|
24
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
25
|
+
}
|
|
26
|
+
class RolecardEngine {
|
|
27
|
+
config;
|
|
28
|
+
logger;
|
|
29
|
+
quotesByTag = new Map();
|
|
30
|
+
allQuotes;
|
|
31
|
+
groups;
|
|
32
|
+
imageBuffer = null;
|
|
33
|
+
lastTrigger = new Map();
|
|
34
|
+
constructor(ctx, rolecard, config) {
|
|
35
|
+
this.config = config;
|
|
36
|
+
this.logger = ctx.logger(`rolecard:${rolecard.manifest.id}`);
|
|
37
|
+
// 按标签建立台词索引
|
|
38
|
+
for (const q of rolecard.words.quotes) {
|
|
39
|
+
for (const tag of q.tags) {
|
|
40
|
+
let list = this.quotesByTag.get(tag);
|
|
41
|
+
if (!list) {
|
|
42
|
+
list = [];
|
|
43
|
+
this.quotesByTag.set(tag, list);
|
|
44
|
+
}
|
|
45
|
+
list.push(q);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
this.allQuotes = rolecard.words.quotes;
|
|
49
|
+
// 按优先级排序触发词分组(数字越小越优先)
|
|
50
|
+
this.groups = [...rolecard.triggers.groups].sort((a, b) => a.priority - b.priority);
|
|
51
|
+
// 预加载插图到内存
|
|
52
|
+
if (rolecard.imagePath) {
|
|
53
|
+
try {
|
|
54
|
+
this.imageBuffer = (0, node_fs_1.readFileSync)(rolecard.imagePath);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
this.imageBuffer = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
this.logger.warn('未找到角色卡插图,将不发送图片');
|
|
62
|
+
}
|
|
63
|
+
ctx.on('message', (session) => this.handle(session));
|
|
64
|
+
this.logger.info(`角色卡已加载:${rolecard.manifest.name}`);
|
|
65
|
+
}
|
|
66
|
+
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
|
+
// 忽略自身与空用户
|
|
74
|
+
if (!session.userId || session.userId === session.selfId)
|
|
75
|
+
return;
|
|
76
|
+
// 纯文本:剥离 koishi 元素标签(如 <at id="..."/>)
|
|
77
|
+
const raw = session.content ?? '';
|
|
78
|
+
const content = raw.replace(/<[^>]+>/g, '');
|
|
79
|
+
if (!content)
|
|
80
|
+
return;
|
|
81
|
+
const channelId = session.channelId ?? session.guildId ?? session.userId;
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
// 冷却控制(白名单豁免)
|
|
84
|
+
if (!this.config.cooldownWhitelist.includes(session.userId)) {
|
|
85
|
+
const last = this.lastTrigger.get(channelId) ?? 0;
|
|
86
|
+
if (now - last < this.config.cooldown * 1000)
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// 1. 按优先级匹配关键词
|
|
90
|
+
let matchedTag = null;
|
|
91
|
+
for (const g of this.groups) {
|
|
92
|
+
if (this.config.disabledTags.includes(g.tag))
|
|
93
|
+
continue;
|
|
94
|
+
if (g.matchMode === 'include' && g.keywords.some((k) => content.includes(k))) {
|
|
95
|
+
matchedTag = g.tag;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// 2. 未命中关键词时按概率随机触发
|
|
100
|
+
let quote = null;
|
|
101
|
+
if (matchedTag) {
|
|
102
|
+
quote = pick(this.quotesByTag.get(matchedTag) ?? this.allQuotes);
|
|
103
|
+
}
|
|
104
|
+
else if (this.config.enableRandom &&
|
|
105
|
+
Math.random() * 100 < this.config.randomProbability) {
|
|
106
|
+
quote = pick(this.allQuotes);
|
|
107
|
+
}
|
|
108
|
+
if (!quote)
|
|
109
|
+
return;
|
|
110
|
+
// 标记触发时间
|
|
111
|
+
this.lastTrigger.set(channelId, now);
|
|
112
|
+
// 构建并发送消息
|
|
113
|
+
try {
|
|
114
|
+
const wantImage = this.config.enableImage &&
|
|
115
|
+
!!this.imageBuffer &&
|
|
116
|
+
Math.random() * 100 < this.config.imageProbability;
|
|
117
|
+
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
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
await session.send(quote.text);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
this.logger.warn('发送消息失败', e);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
exports.RolecardEngine = RolecardEngine;
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* koishi-plugin-rolecard 插件入口。
|
|
3
|
+
*
|
|
4
|
+
* 架构概览:
|
|
5
|
+
*
|
|
6
|
+
* - `types.ts` 共享类型契约(角色卡内容 + 运行时配置)
|
|
7
|
+
* - `loader.ts` 角色卡加载器(扫描并解析 rolecards/ 目录)
|
|
8
|
+
* - `core.ts` 核心引擎(通用台词引擎,与具体角色解耦)
|
|
9
|
+
* - `index.ts` 本文件,组装三者并对接 Koishi 生命周期
|
|
10
|
+
*
|
|
11
|
+
* 角色卡是纯数据资源,存放在 `rolecards/<id>/` 下。用户通过 Config.rolecard
|
|
12
|
+
* 选择要激活的角色卡。新增角色卡只需添加数据目录,无需改动任何源码。
|
|
13
|
+
*/
|
|
14
|
+
import type { Context } from 'koishi';
|
|
15
|
+
import { Schema } from 'koishi';
|
|
16
|
+
import type { Config as ConfigType } from './types';
|
|
17
|
+
export declare const name = "rolecard";
|
|
18
|
+
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>rolecards/</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</div>\n";
|
|
19
|
+
export declare const Config: Schema<ConfigType>;
|
|
20
|
+
export declare function apply(ctx: Context, config: ConfigType): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* koishi-plugin-rolecard 插件入口。
|
|
4
|
+
*
|
|
5
|
+
* 架构概览:
|
|
6
|
+
*
|
|
7
|
+
* - `types.ts` 共享类型契约(角色卡内容 + 运行时配置)
|
|
8
|
+
* - `loader.ts` 角色卡加载器(扫描并解析 rolecards/ 目录)
|
|
9
|
+
* - `core.ts` 核心引擎(通用台词引擎,与具体角色解耦)
|
|
10
|
+
* - `index.ts` 本文件,组装三者并对接 Koishi 生命周期
|
|
11
|
+
*
|
|
12
|
+
* 角色卡是纯数据资源,存放在 `rolecards/<id>/` 下。用户通过 Config.rolecard
|
|
13
|
+
* 选择要激活的角色卡。新增角色卡只需添加数据目录,无需改动任何源码。
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.Config = exports.usage = exports.name = void 0;
|
|
17
|
+
exports.apply = apply;
|
|
18
|
+
const node_path_1 = require("node:path");
|
|
19
|
+
const koishi_1 = require("koishi");
|
|
20
|
+
const core_1 = require("./core");
|
|
21
|
+
const loader_1 = require("./loader");
|
|
22
|
+
exports.name = 'rolecard';
|
|
23
|
+
exports.usage = `
|
|
24
|
+
<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);">
|
|
25
|
+
<h2 style="margin-top: 0; color: #4a6ee0;">🎭 角色卡插件 · Rolecard</h2>
|
|
26
|
+
<p>数据驱动的角色台词引擎。核心逻辑与角色卡内容完全解耦——角色卡是纯数据资源(台词库 + 触发词 + 插图),引擎根据数据自动驱动对话。</p>
|
|
27
|
+
<ul>
|
|
28
|
+
<li>在 <code>rolecards/</code> 目录下放置角色卡,每个角色卡一个子目录</li>
|
|
29
|
+
<li>通过配置项 <code>rolecard</code> 选择要加载的角色卡 ID</li>
|
|
30
|
+
<li>支持关键词触发(按优先级)与概率随机触发</li>
|
|
31
|
+
<li>冷却防刷屏、插图发送、响应范围均可配置</li>
|
|
32
|
+
</ul>
|
|
33
|
+
</div>
|
|
34
|
+
`;
|
|
35
|
+
exports.Config = koishi_1.Schema.object({
|
|
36
|
+
rolecard: koishi_1.Schema.string()
|
|
37
|
+
.default('')
|
|
38
|
+
.description('要加载的角色卡 ID(留空则自动加载第一个找到的角色卡)。例如:belikov'),
|
|
39
|
+
cooldown: koishi_1.Schema.number()
|
|
40
|
+
.default(60)
|
|
41
|
+
.min(0)
|
|
42
|
+
.description('冷却时间(秒),同一频道内两次触发的最小间隔,防止刷屏'),
|
|
43
|
+
cooldownWhitelist: koishi_1.Schema.array(koishi_1.Schema.string())
|
|
44
|
+
.default([])
|
|
45
|
+
.description('冷却白名单:填入用户 ID,这些用户的消息不受冷却限制'),
|
|
46
|
+
disabledTags: koishi_1.Schema.array(koishi_1.Schema.string())
|
|
47
|
+
.default([])
|
|
48
|
+
.description('禁用的触发标签(留空表示全部启用)。例如填入 emotional 可关闭「情绪」类触发'),
|
|
49
|
+
enableRandom: koishi_1.Schema.boolean()
|
|
50
|
+
.default(true)
|
|
51
|
+
.description('启用全部消息概率随机触发(神预言效果)'),
|
|
52
|
+
randomProbability: koishi_1.Schema.number()
|
|
53
|
+
.default(3)
|
|
54
|
+
.min(0)
|
|
55
|
+
.max(100)
|
|
56
|
+
.description('随机触发概率(0-100,3 表示 3%)'),
|
|
57
|
+
enableImage: koishi_1.Schema.boolean().default(true).description('启用角色卡插图'),
|
|
58
|
+
imageWithMessage: koishi_1.Schema.boolean()
|
|
59
|
+
.default(true)
|
|
60
|
+
.description('将图片与文字一起发送(关闭则图片作为单独消息发送)'),
|
|
61
|
+
imageProbability: koishi_1.Schema.number()
|
|
62
|
+
.default(100)
|
|
63
|
+
.min(0)
|
|
64
|
+
.max(100)
|
|
65
|
+
.description('附带图片的概率(0-100,100 表示每次触发都发图)'),
|
|
66
|
+
respondIn: koishi_1.Schema.union([
|
|
67
|
+
koishi_1.Schema.const('group').description('仅群聊'),
|
|
68
|
+
koishi_1.Schema.const('private').description('仅私聊'),
|
|
69
|
+
koishi_1.Schema.const('both').description('群聊与私聊'),
|
|
70
|
+
])
|
|
71
|
+
.default('group')
|
|
72
|
+
.description('响应范围'),
|
|
73
|
+
});
|
|
74
|
+
function apply(ctx, config) {
|
|
75
|
+
const logger = ctx.logger('rolecard');
|
|
76
|
+
// 角色卡目录位于插件包根目录下的 rolecards/
|
|
77
|
+
const rolecardsDir = (0, node_path_1.resolve)(__dirname, '..', 'rolecards');
|
|
78
|
+
const rolecards = (0, loader_1.loadRolecards)(rolecardsDir);
|
|
79
|
+
if (rolecards.length === 0) {
|
|
80
|
+
logger.warn('未找到任何角色卡,请检查 rolecards 目录');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const availableIds = rolecards.map((r) => r.manifest.id).join(', ');
|
|
84
|
+
logger.info(`发现角色卡:${availableIds}`);
|
|
85
|
+
// 按配置选择角色卡;留空则取第一个
|
|
86
|
+
const selected = config.rolecard
|
|
87
|
+
? rolecards.filter((r) => r.manifest.id === config.rolecard)
|
|
88
|
+
: [rolecards[0]];
|
|
89
|
+
if (selected.length === 0) {
|
|
90
|
+
logger.warn(`未找到角色卡 "${config.rolecard}",可用:${availableIds}`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const rolecard of selected) {
|
|
94
|
+
new core_1.RolecardEngine(ctx, rolecard, config);
|
|
95
|
+
}
|
|
96
|
+
}
|
package/lib/loader.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 角色卡加载器。
|
|
3
|
+
*
|
|
4
|
+
* 负责从磁盘扫描并解析角色卡目录。每个角色卡是一个子目录,包含:
|
|
5
|
+
*
|
|
6
|
+
* - `rolecard.json` 清单(必填)
|
|
7
|
+
* - `words.json` 台词库(必填,文件名可由清单指定)
|
|
8
|
+
* - `trigger-words.json` 触发词配置(必填,文件名可由清单指定)
|
|
9
|
+
* - `image.png` 插图(可选,文件名可由清单指定)
|
|
10
|
+
*
|
|
11
|
+
* 加载器只做「读取与校验」,不做任何消息处理逻辑,
|
|
12
|
+
* 因此核心引擎可以专注于运行时行为,二者完全解耦。
|
|
13
|
+
*/
|
|
14
|
+
import type { Rolecard } from './types';
|
|
15
|
+
/**
|
|
16
|
+
* 加载单个角色卡目录。
|
|
17
|
+
*
|
|
18
|
+
* @returns 解析成功返回完整 `Rolecard`;清单缺失/数据无效返回 `null`。
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadRolecard(dir: string): Rolecard | null;
|
|
21
|
+
/**
|
|
22
|
+
* 扫描目录下的所有角色卡子目录并加载。
|
|
23
|
+
*
|
|
24
|
+
* 非目录项与无效的角色卡会被静默跳过。
|
|
25
|
+
*/
|
|
26
|
+
export declare function loadRolecards(baseDir: string): Rolecard[];
|
package/lib/loader.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 角色卡加载器。
|
|
4
|
+
*
|
|
5
|
+
* 负责从磁盘扫描并解析角色卡目录。每个角色卡是一个子目录,包含:
|
|
6
|
+
*
|
|
7
|
+
* - `rolecard.json` 清单(必填)
|
|
8
|
+
* - `words.json` 台词库(必填,文件名可由清单指定)
|
|
9
|
+
* - `trigger-words.json` 触发词配置(必填,文件名可由清单指定)
|
|
10
|
+
* - `image.png` 插图(可选,文件名可由清单指定)
|
|
11
|
+
*
|
|
12
|
+
* 加载器只做「读取与校验」,不做任何消息处理逻辑,
|
|
13
|
+
* 因此核心引擎可以专注于运行时行为,二者完全解耦。
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.loadRolecard = loadRolecard;
|
|
17
|
+
exports.loadRolecards = loadRolecards;
|
|
18
|
+
const node_fs_1 = require("node:fs");
|
|
19
|
+
const node_path_1 = require("node:path");
|
|
20
|
+
/** 清单中路径字段的默认值。 */
|
|
21
|
+
const DEFAULT_WORDS_FILE = 'words.json';
|
|
22
|
+
const DEFAULT_TRIGGER_FILE = 'trigger-words.json';
|
|
23
|
+
const DEFAULT_IMAGE_FILE = 'image.png';
|
|
24
|
+
function readJson(path) {
|
|
25
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
26
|
+
return null;
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 加载单个角色卡目录。
|
|
36
|
+
*
|
|
37
|
+
* @returns 解析成功返回完整 `Rolecard`;清单缺失/数据无效返回 `null`。
|
|
38
|
+
*/
|
|
39
|
+
function loadRolecard(dir) {
|
|
40
|
+
const manifest = readJson((0, node_path_1.resolve)(dir, 'rolecard.json'));
|
|
41
|
+
if (!manifest?.id || !manifest?.name)
|
|
42
|
+
return null;
|
|
43
|
+
const wordsFile = manifest.wordsFile ?? DEFAULT_WORDS_FILE;
|
|
44
|
+
const triggerFile = manifest.triggerFile ?? DEFAULT_TRIGGER_FILE;
|
|
45
|
+
const imageFile = manifest.imageFile ?? DEFAULT_IMAGE_FILE;
|
|
46
|
+
const words = readJson((0, node_path_1.resolve)(dir, wordsFile));
|
|
47
|
+
const triggers = readJson((0, node_path_1.resolve)(dir, triggerFile));
|
|
48
|
+
if (!words || !triggers || !Array.isArray(triggers.groups))
|
|
49
|
+
return null;
|
|
50
|
+
const imagePath = (0, node_path_1.resolve)(dir, imageFile);
|
|
51
|
+
return {
|
|
52
|
+
manifest,
|
|
53
|
+
words,
|
|
54
|
+
triggers,
|
|
55
|
+
imagePath: (0, node_fs_1.existsSync)(imagePath) ? imagePath : null,
|
|
56
|
+
dir,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 扫描目录下的所有角色卡子目录并加载。
|
|
61
|
+
*
|
|
62
|
+
* 非目录项与无效的角色卡会被静默跳过。
|
|
63
|
+
*/
|
|
64
|
+
function loadRolecards(baseDir) {
|
|
65
|
+
if (!(0, node_fs_1.existsSync)(baseDir))
|
|
66
|
+
return [];
|
|
67
|
+
const result = [];
|
|
68
|
+
for (const entry of (0, node_fs_1.readdirSync)(baseDir)) {
|
|
69
|
+
const dir = (0, node_path_1.resolve)(baseDir, entry);
|
|
70
|
+
if (!(0, node_fs_1.statSync)(dir).isDirectory())
|
|
71
|
+
continue;
|
|
72
|
+
const card = loadRolecard(dir);
|
|
73
|
+
if (card)
|
|
74
|
+
result.push(card);
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 角色卡插件共享类型定义。
|
|
3
|
+
*
|
|
4
|
+
* 这些类型同时被核心引擎(core.ts)、加载器(loader.ts)和插件入口(index.ts)使用,
|
|
5
|
+
* 是「角色卡内容」与「运行时行为」之间唯一的契约。
|
|
6
|
+
*/
|
|
7
|
+
/** 单条台词。一条台词可属于多个标签,按标签过滤随机取用。 */
|
|
8
|
+
export interface Quote {
|
|
9
|
+
id: string;
|
|
10
|
+
text: string;
|
|
11
|
+
tags: string[];
|
|
12
|
+
}
|
|
13
|
+
/** 台词库文件(words.json)的结构。 */
|
|
14
|
+
export interface WordsData {
|
|
15
|
+
/** 角色名,如「别里科夫」。 */
|
|
16
|
+
character: string;
|
|
17
|
+
/** 角色来源,如「契诃夫《套中人》」。 */
|
|
18
|
+
source: string;
|
|
19
|
+
desc?: string;
|
|
20
|
+
/** 标签说明,key 为标签名,value 为该标签的场景描述。 */
|
|
21
|
+
tags?: Record<string, string>;
|
|
22
|
+
quotes: Quote[];
|
|
23
|
+
}
|
|
24
|
+
/** 触发词分组。一个分组对应一个标签,包含关键词集合与优先级。 */
|
|
25
|
+
export interface TriggerGroup {
|
|
26
|
+
/** 标签名,与 WordsData.quotes[].tags 中的值对应。 */
|
|
27
|
+
tag: string;
|
|
28
|
+
name: string;
|
|
29
|
+
desc?: string;
|
|
30
|
+
matchMode: 'include';
|
|
31
|
+
/** 优先级,数字越小越优先匹配。 */
|
|
32
|
+
priority: number;
|
|
33
|
+
keywords: string[];
|
|
34
|
+
}
|
|
35
|
+
/** 触发词文件(trigger-words.json)的结构。 */
|
|
36
|
+
export interface TriggerData {
|
|
37
|
+
desc?: string;
|
|
38
|
+
groups: TriggerGroup[];
|
|
39
|
+
/** 随机触发配置(仅作角色卡默认值参考,运行时以 Config 为准)。 */
|
|
40
|
+
random?: {
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
probability: number;
|
|
43
|
+
pool: string;
|
|
44
|
+
desc?: string;
|
|
45
|
+
};
|
|
46
|
+
/** 冷却秒数(仅作角色卡默认值参考,运行时以 Config 为准)。 */
|
|
47
|
+
cooldown?: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 角色卡清单(rolecard.json)。
|
|
51
|
+
*
|
|
52
|
+
* 每个角色卡目录下必须存在此文件,插件启动时由加载器读取。
|
|
53
|
+
* 路径字段缺省时使用默认文件名。
|
|
54
|
+
*/
|
|
55
|
+
export interface RolecardManifest {
|
|
56
|
+
/** 角色卡唯一标识,如 `belikov`。用于 Config.rolecard 选中。 */
|
|
57
|
+
id: string;
|
|
58
|
+
/** 角色显示名,如「别里科夫」。 */
|
|
59
|
+
name: string;
|
|
60
|
+
description: string;
|
|
61
|
+
source?: string;
|
|
62
|
+
/** 台词库文件名,默认 `words.json`。 */
|
|
63
|
+
wordsFile?: string;
|
|
64
|
+
/** 触发词文件名,默认 `trigger-words.json`。 */
|
|
65
|
+
triggerFile?: string;
|
|
66
|
+
/** 插图文件名,默认 `image.png`。 */
|
|
67
|
+
imageFile?: string;
|
|
68
|
+
}
|
|
69
|
+
/** 已加载的完整角色卡,包含清单与解析后的数据。 */
|
|
70
|
+
export interface Rolecard {
|
|
71
|
+
manifest: RolecardManifest;
|
|
72
|
+
words: WordsData;
|
|
73
|
+
triggers: TriggerData;
|
|
74
|
+
/** 插图绝对路径,无插图时为 null。 */
|
|
75
|
+
imagePath: string | null;
|
|
76
|
+
/** 角色卡目录绝对路径。 */
|
|
77
|
+
dir: string;
|
|
78
|
+
}
|
|
79
|
+
/** 插件运行时配置。所有行为参数统一由此处控制,与角色卡内容解耦。 */
|
|
80
|
+
export interface Config {
|
|
81
|
+
/** 要激活的角色卡 ID,留空则自动加载第一个找到的角色卡。 */
|
|
82
|
+
rolecard: string;
|
|
83
|
+
/** 冷却时间(秒),同一频道内两次触发的最小间隔。 */
|
|
84
|
+
cooldown: number;
|
|
85
|
+
/** 冷却白名单用户 ID,这些用户的消息不受冷却限制。 */
|
|
86
|
+
cooldownWhitelist: string[];
|
|
87
|
+
/** 禁用的触发标签,留空表示全部启用。 */
|
|
88
|
+
disabledTags: string[];
|
|
89
|
+
/** 是否启用全部消息概率随机触发。 */
|
|
90
|
+
enableRandom: boolean;
|
|
91
|
+
/** 随机触发概率(0-100)。 */
|
|
92
|
+
randomProbability: number;
|
|
93
|
+
/** 是否启用角色卡插图。 */
|
|
94
|
+
enableImage: boolean;
|
|
95
|
+
/** 图片与文字是否作为同一条消息发送。 */
|
|
96
|
+
imageWithMessage: boolean;
|
|
97
|
+
/** 附带图片的概率(0-100)。 */
|
|
98
|
+
imageProbability: number;
|
|
99
|
+
/** 响应范围:群聊 / 私聊 / 两者。 */
|
|
100
|
+
respondIn: 'group' | 'private' | 'both';
|
|
101
|
+
}
|
package/lib/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-rolecard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"contributors": [
|
|
5
|
+
"Oppenheymu <oppenheymu@gmail.com>"
|
|
6
|
+
],
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"types": "lib/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./lib/index.d.ts",
|
|
12
|
+
"development": "./src/index.ts",
|
|
13
|
+
"default": "./lib/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"lib",
|
|
19
|
+
"rolecards"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/Oppenheymu/koishi-plugin-rolecard",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/Oppenheymu/koishi-plugin-rolecard.git"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"chatbot",
|
|
32
|
+
"koishi",
|
|
33
|
+
"plugin",
|
|
34
|
+
"rolecard"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -b"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"koishi": "^4.17.4"
|
|
41
|
+
},
|
|
42
|
+
"koishi": {
|
|
43
|
+
"description": {
|
|
44
|
+
"en": "Rolecard plugin — data-driven character quote engine",
|
|
45
|
+
"zh": "身份卡插件 · 数据驱动的角色台词引擎,可选加载不同角色卡"
|
|
46
|
+
},
|
|
47
|
+
"service": {}
|
|
48
|
+
}
|
|
49
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# koishi-plugin-rolecard
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/koishi-plugin-rolecard)
|
|
4
|
+
|
|
5
|
+
数据驱动的角色台词引擎 · 角色卡插件。
|
|
6
|
+
|
|
7
|
+
核心逻辑与角色卡内容**完全解耦**:引擎是通用的,角色卡是纯数据资源。新增角色只需在 `rolecards/` 下添加一个数据目录,无需改动任何源码。
|
|
8
|
+
|
|
9
|
+
## 架构
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
src/
|
|
13
|
+
types.ts 共享类型契约(角色卡内容 + 运行时配置)
|
|
14
|
+
loader.ts 角色卡加载器(扫描并解析 rolecards/ 目录)
|
|
15
|
+
core.ts 核心引擎(通用台词引擎,与具体角色解耦)
|
|
16
|
+
index.ts 插件入口(组装三者,对接 Koishi 生命周期)
|
|
17
|
+
rolecards/
|
|
18
|
+
belikov/ 别里科夫角色卡(数据资源)
|
|
19
|
+
rolecard.json 清单
|
|
20
|
+
words.json 台词库
|
|
21
|
+
trigger-words.json 触发词配置
|
|
22
|
+
image.png 插图
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 分层职责
|
|
26
|
+
|
|
27
|
+
| 模块 | 职责 | 依赖 |
|
|
28
|
+
|------|------|------|
|
|
29
|
+
| `types.ts` | 定义角色卡数据结构与运行时配置的契约 | 无 |
|
|
30
|
+
| `loader.ts` | 从磁盘扫描、解析、校验角色卡目录 | `types.ts` |
|
|
31
|
+
| `core.ts` | 通用台词引擎:关键词匹配、概率触发、冷却、插图 | `types.ts` |
|
|
32
|
+
| `index.ts` | 加载角色卡、按配置实例化引擎 | 以上三者 |
|
|
33
|
+
|
|
34
|
+
角色卡(`rolecards/<id>/`)只包含数据,不含任何可执行逻辑,因此可以自由替换、增删,引擎自动适配。
|
|
35
|
+
|
|
36
|
+
## 内置角色卡
|
|
37
|
+
|
|
38
|
+
### 别里科夫 · 套中人
|
|
39
|
+
|
|
40
|
+
灵感源自契诃夫《套中人》中那位谨小慎微、墨守成规、害怕任何改变的别里科夫。机器人会监听群聊,在合适的时机抛出他那忧心忡忡、生怕"闹出什么乱子"的台词,制造一本正经的喜剧效果。
|
|
41
|
+
|
|
42
|
+
四类触发场景:
|
|
43
|
+
|
|
44
|
+
- **proposal** — 提议与搞事(要不、计划、面基……)
|
|
45
|
+
- **emotional** — 情绪波动与违规边缘(笑死、卧槽、节奏……)
|
|
46
|
+
- **rules** — 规章制度与日常通知(通知、公告、群规……)
|
|
47
|
+
- **distancing** — 撇清关系与甩锅(都怪、甩锅、谁的锅……)
|
|
48
|
+
|
|
49
|
+
## 配置项
|
|
50
|
+
|
|
51
|
+
| 配置项 | 类型 | 默认 | 说明 |
|
|
52
|
+
|--------|------|------|------|
|
|
53
|
+
| `rolecard` | string | `''` | 要加载的角色卡 ID(留空则自动加载第一个找到的角色卡) |
|
|
54
|
+
| `cooldown` | number | `60` | 冷却时间(秒),同一频道内两次触发的最小间隔 |
|
|
55
|
+
| `cooldownWhitelist` | string[] | `[]` | 冷却白名单,填入用户 ID 后其消息不受冷却限制 |
|
|
56
|
+
| `disabledTags` | string[] | `[]` | 禁用的触发标签(留空表示全部启用) |
|
|
57
|
+
| `enableRandom` | boolean | `true` | 启用全部消息概率随机触发 |
|
|
58
|
+
| `randomProbability` | number | `3` | 随机触发概率(0-100,3 表示 3%) |
|
|
59
|
+
| `enableImage` | boolean | `true` | 启用角色卡插图 |
|
|
60
|
+
| `imageWithMessage` | boolean | `true` | 图片与文字一起发送(关闭则图片作为单独消息发送) |
|
|
61
|
+
| `imageProbability` | number | `100` | 附带图片的概率(0-100) |
|
|
62
|
+
| `respondIn` | string | `group` | 响应范围:`group` / `private` / `both` |
|
|
63
|
+
|
|
64
|
+
## 添加新角色卡
|
|
65
|
+
|
|
66
|
+
1. 在 `rolecards/` 下新建目录,如 `rolecards/mycharacter/`
|
|
67
|
+
2. 创建 `rolecard.json` 清单:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"id": "mycharacter",
|
|
72
|
+
"name": "我的角色",
|
|
73
|
+
"description": "角色简介",
|
|
74
|
+
"source": "角色来源"
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
3. 创建 `words.json` 台词库:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"character": "我的角色",
|
|
83
|
+
"source": "来源",
|
|
84
|
+
"quotes": [
|
|
85
|
+
{ "id": "q1", "text": "台词内容", "tags": ["tag1"] }
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
4. 创建 `trigger-words.json` 触发词配置:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"groups": [
|
|
95
|
+
{
|
|
96
|
+
"tag": "tag1",
|
|
97
|
+
"name": "分组名",
|
|
98
|
+
"matchMode": "include",
|
|
99
|
+
"priority": 1,
|
|
100
|
+
"keywords": ["关键词1", "关键词2"]
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
5. 可选:放入 `image.png` 插图
|
|
107
|
+
|
|
108
|
+
6. 在插件配置中将 `rolecard` 设为 `mycharacter` 即可加载
|
|
109
|
+
|
|
110
|
+
`tags` 字段是台词与触发词之间的桥梁——触发词分组的 `tag` 对应台词的 `tags`,引擎据此筛选候选台词。
|
|
111
|
+
|
|
112
|
+
## 依赖
|
|
113
|
+
|
|
114
|
+
- [koishi](https://koishi.chat/) ^4.17.4
|
|
115
|
+
|
|
116
|
+
## 交流与反馈
|
|
117
|
+
|
|
118
|
+
遇到问题或有建议?欢迎加入 QQ 群 **[1071284605【晓基地插件工坊】](https://qm.qq.com/q/WngX4RQoca)** 进行交流。
|
|
119
|
+
|
|
120
|
+
## 许可证
|
|
121
|
+
|
|
122
|
+
MIT
|
|
Binary file
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"desc": "别里科夫触发词配置。matchMode=include 表示消息包含任一关键词即命中;按 group 优先级(priority 越小越优先)匹配,命中后从 words.json 中对应 tag 的语录里随机取一条。random 为概率随机触发,cooldown 为冷却秒数,防止刷屏。注:运行时行为以插件 Config 为准,此处的 random/cooldown 仅作角色卡默认值参考。",
|
|
3
|
+
"groups": [
|
|
4
|
+
{
|
|
5
|
+
"tag": "proposal",
|
|
6
|
+
"name": "提议与搞事",
|
|
7
|
+
"desc": "群友提议做新事情时泼冷水,效果最佳",
|
|
8
|
+
"matchMode": "include",
|
|
9
|
+
"priority": 1,
|
|
10
|
+
"keywords": [
|
|
11
|
+
"要不", "提议", "整一个", "搞个", "计划", "打算", "准备",
|
|
12
|
+
"去不去", "面基", "一起去", "约", "组织", "活动", "报名",
|
|
13
|
+
"试试", "能不能", "可以不", "行不行", "凑个", "开个"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"tag": "emotional",
|
|
18
|
+
"name": "情绪波动与违规边缘",
|
|
19
|
+
"desc": "群友情绪激昂或聊敏感话题,别里科夫胆小受惊",
|
|
20
|
+
"matchMode": "include",
|
|
21
|
+
"priority": 2,
|
|
22
|
+
"keywords": [
|
|
23
|
+
"笑死", "哈哈", "卧槽", "牛逼", "狂", "冲", "节奏", "举报",
|
|
24
|
+
"离谱", "绝了", "破防", "无语", "炸裂", "刺激", "敢说",
|
|
25
|
+
"牛批", "666", "牛啊", "牛逼啊", "疯了", "逆天", "没绷住", "难绷"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"tag": "rules",
|
|
30
|
+
"name": "规章制度与日常通知",
|
|
31
|
+
"desc": "群主发公告或讨论规则时,搬出教条",
|
|
32
|
+
"matchMode": "include",
|
|
33
|
+
"priority": 3,
|
|
34
|
+
"keywords": [
|
|
35
|
+
"通知", "公告", "群规", "放假", "开会", "新规定", "规定",
|
|
36
|
+
"规则", "禁", "禁止", "处罚", "踢", "禁言", "管理", "群主",
|
|
37
|
+
"解散", "迁移", "审核"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"tag": "distancing",
|
|
42
|
+
"name": "撇清关系与甩锅",
|
|
43
|
+
"desc": "群里起纷争、互相甩锅或被牵连时,急于划清界限",
|
|
44
|
+
"matchMode": "include",
|
|
45
|
+
"priority": 4,
|
|
46
|
+
"keywords": [
|
|
47
|
+
"别里科夫", "怪你", "赖你", "都怪", "谁干的", "你弄的",
|
|
48
|
+
"怪我", "不怪我", "跟我没关系", "你的锅", "谁的锅",
|
|
49
|
+
"甩锅", "背锅", "怪谁", "赖谁"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"random": {
|
|
54
|
+
"enabled": true,
|
|
55
|
+
"probability": 0.03,
|
|
56
|
+
"pool": "all",
|
|
57
|
+
"desc": "任意消息 3% 概率随机触发,pool=all 表示从全部语录中随机取一条"
|
|
58
|
+
},
|
|
59
|
+
"cooldown": 60
|
|
60
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"character": "别里科夫",
|
|
3
|
+
"source": "契诃夫《套中人》",
|
|
4
|
+
"desc": "谨小慎微、墨守成规、害怕任何改变。每条语录带 tags,可与 trigger-words.json 中的 group.tag 对应,按标签过滤随机取用。",
|
|
5
|
+
"tags": {
|
|
6
|
+
"proposal": "提议与搞事:群友提议新事情时,勉强附和又忧心忡忡",
|
|
7
|
+
"emotional": "情绪波动与违规边缘:群友情绪高昂或聊敏感话题时,惊恐劝阻",
|
|
8
|
+
"rules": "规章制度与日常通知:涉及规则/公告/管理时,搬出教条",
|
|
9
|
+
"distancing": "撇清关系与甩锅:群里起纷争或被牵连时,急于划清界限"
|
|
10
|
+
},
|
|
11
|
+
"quotes": [
|
|
12
|
+
{ "id": "q01", "text": "当然,行是行的", "tags": ["proposal"] },
|
|
13
|
+
{ "id": "q02", "text": "当然,行是行的,这固然很好,可是千万别闹出什么乱子", "tags": ["proposal"] },
|
|
14
|
+
{ "id": "q03", "text": "这固然很好,可是千万别闹出什么乱子", "tags": ["proposal", "rules"] },
|
|
15
|
+
{ "id": "q04", "text": "千万别闹出什么乱子", "tags": ["proposal"] },
|
|
16
|
+
{ "id": "q05", "text": "这个嘛,当然也对,这都很好,但愿不要惹出什么事端!", "tags": ["proposal"] },
|
|
17
|
+
{ "id": "q06", "text": "这都很好,但愿不要惹出什么事端!", "tags": ["proposal", "rules"] },
|
|
18
|
+
{ "id": "q07", "text": "唉,千万别传到当局那里", "tags": ["emotional", "rules"] },
|
|
19
|
+
{ "id": "q08", "text": "唉,千万别传到当局的耳朵里", "tags": ["emotional", "rules"] },
|
|
20
|
+
{ "id": "q09", "text": "哎呀,千万不要惹出什么事端!", "tags": ["emotional", "proposal"] },
|
|
21
|
+
{ "id": "q10", "text": "这一切,你知道吗,来得有点突然", "tags": ["emotional"] },
|
|
22
|
+
{ "id": "q11", "text": "这需要考虑考虑", "tags": ["proposal"] },
|
|
23
|
+
{ "id": "q12", "text": "我们首先应当掂量一下轻重,免得日后惹出什么麻烦", "tags": ["proposal"] },
|
|
24
|
+
{ "id": "q13", "text": "希望日后不惹出什么麻烦", "tags": ["proposal"] },
|
|
25
|
+
{ "id": "q14", "text": "群里竟有这样的人!", "tags": ["emotional"] },
|
|
26
|
+
{ "id": "q15", "text": "这成何体统?", "tags": ["emotional", "rules"] },
|
|
27
|
+
{ "id": "q16", "text": "群友都能这样,这成何体统?", "tags": ["emotional", "rules"] },
|
|
28
|
+
{ "id": "q17", "text": "那怎么行呢?", "tags": ["proposal", "emotional"] },
|
|
29
|
+
{ "id": "q18", "text": "这怎么可以??", "tags": ["proposal", "emotional"] },
|
|
30
|
+
{ "id": "q19", "text": "这怎么行呢?", "tags": ["proposal", "emotional"] },
|
|
31
|
+
{ "id": "q20", "text": "您这是什么话?!", "tags": ["emotional"] },
|
|
32
|
+
{ "id": "q21", "text": "哎呀,您这是什么话!", "tags": ["emotional"] },
|
|
33
|
+
{ "id": "q22", "text": "现在我的心情非常非常沉重", "tags": ["emotional"] },
|
|
34
|
+
{ "id": "q23", "text": "我认为有责任向您保证,这事与我毫不相干", "tags": ["distancing"] },
|
|
35
|
+
{ "id": "q24", "text": "这事与我毫不相干", "tags": ["distancing"] },
|
|
36
|
+
{ "id": "q25", "text": "等下,这事与我毫不相干!", "tags": ["distancing"] },
|
|
37
|
+
{ "id": "q26", "text": "我认为群友有责任向他提出忠告", "tags": ["rules"] },
|
|
38
|
+
{ "id": "q27", "text": "我并没有给人任何口实,可以招致这种事情", "tags": ["distancing"] },
|
|
39
|
+
{ "id": "q28", "text": "这是有伤大雅的!", "tags": ["emotional", "rules"] },
|
|
40
|
+
{ "id": "q29", "text": "这难道还须要解释吗?", "tags": ["rules"] },
|
|
41
|
+
{ "id": "q30", "text": "您还年轻,前程远大,所以您的举止行为要非常非常小心谨慎", "tags": ["emotional", "rules"] },
|
|
42
|
+
{ "id": "q31", "text": "那会有什么好结果?", "tags": ["proposal"] },
|
|
43
|
+
{ "id": "q32", "text": "这会有什么好结果??", "tags": ["proposal"] },
|
|
44
|
+
{ "id": "q33", "text": "难道这有什么好结果吗?", "tags": ["proposal"] },
|
|
45
|
+
{ "id": "q34", "text": "对当局您应当尊敬才是", "tags": ["rules"] },
|
|
46
|
+
{ "id": "q35", "text": "对当局您应当尊敬才是。哦!我是指群管理!", "tags": ["rules"] },
|
|
47
|
+
{ "id": "q36", "text": "您尽可以随便说去", "tags": ["distancing"] },
|
|
48
|
+
{ "id": "q37", "text": "我们得避免别人歪曲谈话的内容,惹出什么事端", "tags": ["emotional", "rules"] },
|
|
49
|
+
{ "id": "q38", "text": "我必须这么做", "tags": ["rules"] },
|
|
50
|
+
{ "id": "q39", "text": "我有责任这样做", "tags": ["rules"] }
|
|
51
|
+
]
|
|
52
|
+
}
|