dsn-whale-chat 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/lib/bridge/message.js +1 -1
- package/lib/index.d.ts +66 -20
- package/lib/index.js +85 -13
- package/lib/interject/interject.d.ts +3 -2
- package/lib/interject/interject.js +4 -4
- package/lib/persona/assemble.d.ts +3 -3
- package/lib/persona/assemble.js +22 -5
- package/lib/persona/personas.d.ts +12 -1
- package/lib/persona/personas.js +39 -32
- package/lib/proactive/proactive.d.ts +8 -3
- package/lib/proactive/proactive.js +8 -3
- package/lib/reply/split.d.ts +13 -0
- package/lib/reply/split.js +62 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,9 +11,19 @@
|
|
|
11
11
|
- **隐藏人格**:恶趣味 / 贴吧喷子 / 女王,各有开关;**关闭时完全不注入**。
|
|
12
12
|
- **触发控制**:私聊全收;群聊支持 `@` / 全体 / 关键词。
|
|
13
13
|
- **频率控制**:去抖合并、冷却、每分钟·每小时上限。
|
|
14
|
-
- **闲时主动 +
|
|
14
|
+
- **闲时主动 + 随机插话**:**默认开启**(主动发言 15~30 分钟随机、插话每 10 分钟最多 3 次)。
|
|
15
15
|
- **稳定前缀注入**:人格放在 `systemPrompt.section`,命中 prefix cache。
|
|
16
16
|
|
|
17
|
+
> ⚠️ **风险项**:主动发言与随机插话默认开启,可能刷屏、打扰群友或放大平台风控。如要规避,在 profile 配置中关闭:
|
|
18
|
+
>
|
|
19
|
+
> ```yaml
|
|
20
|
+
> proactive:
|
|
21
|
+
> private: { enabled: false }
|
|
22
|
+
> group: { enabled: false }
|
|
23
|
+
> randomInterject:
|
|
24
|
+
> enabled: false
|
|
25
|
+
> ```
|
|
26
|
+
|
|
17
27
|
## 安装
|
|
18
28
|
|
|
19
29
|
```sh
|
package/lib/bridge/message.js
CHANGED
|
@@ -30,7 +30,7 @@ function toNudgeMessage(message, instruction) {
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
export function toProactiveMessage(message) {
|
|
33
|
-
return toNudgeMessage(message, '
|
|
33
|
+
return toNudgeMessage(message, '(安静了一会儿。你可以翻翻之前的聊天接一句、随口说点什么、或者去找你熟悉的人搭话;没什么想说的就只回复 SILENT。)');
|
|
34
34
|
}
|
|
35
35
|
export function toInterjectMessage(message, text) {
|
|
36
36
|
return toNudgeMessage(message, `群里刚才有人说:「${text}」。你可以接一句,或回 SILENT。`);
|
package/lib/index.d.ts
CHANGED
|
@@ -70,42 +70,63 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
70
70
|
maxPerMinute: z<number, number, "defined">;
|
|
71
71
|
maxPerHour: z<number, number, "defined">;
|
|
72
72
|
}>>, "plain">;
|
|
73
|
+
replySplit: z<Schemastery.ObjectS<NoInfer<{
|
|
74
|
+
enabled: z<boolean, boolean, "defined">;
|
|
75
|
+
groupMaxChars: z<number, number, "defined">;
|
|
76
|
+
privateMaxChars: z<number, number, "defined">;
|
|
77
|
+
longFormMaxChars: z<number, number, "defined">;
|
|
78
|
+
delayMs: z<number, number, "defined">;
|
|
79
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
80
|
+
enabled: z<boolean, boolean, "defined">;
|
|
81
|
+
groupMaxChars: z<number, number, "defined">;
|
|
82
|
+
privateMaxChars: z<number, number, "defined">;
|
|
83
|
+
longFormMaxChars: z<number, number, "defined">;
|
|
84
|
+
delayMs: z<number, number, "defined">;
|
|
85
|
+
}>>, "plain">;
|
|
73
86
|
proactive: z<Schemastery.ObjectS<NoInfer<{
|
|
74
87
|
private: z<Schemastery.ObjectS<NoInfer<{
|
|
75
88
|
enabled: z<boolean, boolean, "defined">;
|
|
76
|
-
|
|
89
|
+
idleMinMinutes: z<number, number, "defined">;
|
|
90
|
+
idleMaxMinutes: z<number, number, "defined">;
|
|
77
91
|
maxPerDay: z<number, number, "defined">;
|
|
78
92
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
79
93
|
enabled: z<boolean, boolean, "defined">;
|
|
80
|
-
|
|
94
|
+
idleMinMinutes: z<number, number, "defined">;
|
|
95
|
+
idleMaxMinutes: z<number, number, "defined">;
|
|
81
96
|
maxPerDay: z<number, number, "defined">;
|
|
82
97
|
}>>, "plain">;
|
|
83
98
|
group: z<Schemastery.ObjectS<NoInfer<{
|
|
84
99
|
enabled: z<boolean, boolean, "defined">;
|
|
85
|
-
|
|
100
|
+
idleMinMinutes: z<number, number, "defined">;
|
|
101
|
+
idleMaxMinutes: z<number, number, "defined">;
|
|
86
102
|
maxPerDay: z<number, number, "defined">;
|
|
87
103
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
88
104
|
enabled: z<boolean, boolean, "defined">;
|
|
89
|
-
|
|
105
|
+
idleMinMinutes: z<number, number, "defined">;
|
|
106
|
+
idleMaxMinutes: z<number, number, "defined">;
|
|
90
107
|
maxPerDay: z<number, number, "defined">;
|
|
91
108
|
}>>, "plain">;
|
|
92
109
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
93
110
|
private: z<Schemastery.ObjectS<NoInfer<{
|
|
94
111
|
enabled: z<boolean, boolean, "defined">;
|
|
95
|
-
|
|
112
|
+
idleMinMinutes: z<number, number, "defined">;
|
|
113
|
+
idleMaxMinutes: z<number, number, "defined">;
|
|
96
114
|
maxPerDay: z<number, number, "defined">;
|
|
97
115
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
98
116
|
enabled: z<boolean, boolean, "defined">;
|
|
99
|
-
|
|
117
|
+
idleMinMinutes: z<number, number, "defined">;
|
|
118
|
+
idleMaxMinutes: z<number, number, "defined">;
|
|
100
119
|
maxPerDay: z<number, number, "defined">;
|
|
101
120
|
}>>, "plain">;
|
|
102
121
|
group: z<Schemastery.ObjectS<NoInfer<{
|
|
103
122
|
enabled: z<boolean, boolean, "defined">;
|
|
104
|
-
|
|
123
|
+
idleMinMinutes: z<number, number, "defined">;
|
|
124
|
+
idleMaxMinutes: z<number, number, "defined">;
|
|
105
125
|
maxPerDay: z<number, number, "defined">;
|
|
106
126
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
107
127
|
enabled: z<boolean, boolean, "defined">;
|
|
108
|
-
|
|
128
|
+
idleMinMinutes: z<number, number, "defined">;
|
|
129
|
+
idleMaxMinutes: z<number, number, "defined">;
|
|
109
130
|
maxPerDay: z<number, number, "defined">;
|
|
110
131
|
}>>, "plain">;
|
|
111
132
|
}>>, "plain">;
|
|
@@ -113,12 +134,14 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
113
134
|
enabled: z<boolean, boolean, "defined">;
|
|
114
135
|
probability: z<number, number, "defined">;
|
|
115
136
|
minMessagesBetween: z<number, number, "defined">;
|
|
116
|
-
|
|
137
|
+
windowMinutes: z<number, number, "defined">;
|
|
138
|
+
maxPerWindow: z<number, number, "defined">;
|
|
117
139
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
118
140
|
enabled: z<boolean, boolean, "defined">;
|
|
119
141
|
probability: z<number, number, "defined">;
|
|
120
142
|
minMessagesBetween: z<number, number, "defined">;
|
|
121
|
-
|
|
143
|
+
windowMinutes: z<number, number, "defined">;
|
|
144
|
+
maxPerWindow: z<number, number, "defined">;
|
|
122
145
|
}>>, "plain">;
|
|
123
146
|
schedule: z<Schemastery.ObjectS<NoInfer<{
|
|
124
147
|
quietHours: z<number[], number[], "defined">;
|
|
@@ -190,42 +213,63 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
190
213
|
maxPerMinute: z<number, number, "defined">;
|
|
191
214
|
maxPerHour: z<number, number, "defined">;
|
|
192
215
|
}>>, "plain">;
|
|
216
|
+
replySplit: z<Schemastery.ObjectS<NoInfer<{
|
|
217
|
+
enabled: z<boolean, boolean, "defined">;
|
|
218
|
+
groupMaxChars: z<number, number, "defined">;
|
|
219
|
+
privateMaxChars: z<number, number, "defined">;
|
|
220
|
+
longFormMaxChars: z<number, number, "defined">;
|
|
221
|
+
delayMs: z<number, number, "defined">;
|
|
222
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
223
|
+
enabled: z<boolean, boolean, "defined">;
|
|
224
|
+
groupMaxChars: z<number, number, "defined">;
|
|
225
|
+
privateMaxChars: z<number, number, "defined">;
|
|
226
|
+
longFormMaxChars: z<number, number, "defined">;
|
|
227
|
+
delayMs: z<number, number, "defined">;
|
|
228
|
+
}>>, "plain">;
|
|
193
229
|
proactive: z<Schemastery.ObjectS<NoInfer<{
|
|
194
230
|
private: z<Schemastery.ObjectS<NoInfer<{
|
|
195
231
|
enabled: z<boolean, boolean, "defined">;
|
|
196
|
-
|
|
232
|
+
idleMinMinutes: z<number, number, "defined">;
|
|
233
|
+
idleMaxMinutes: z<number, number, "defined">;
|
|
197
234
|
maxPerDay: z<number, number, "defined">;
|
|
198
235
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
199
236
|
enabled: z<boolean, boolean, "defined">;
|
|
200
|
-
|
|
237
|
+
idleMinMinutes: z<number, number, "defined">;
|
|
238
|
+
idleMaxMinutes: z<number, number, "defined">;
|
|
201
239
|
maxPerDay: z<number, number, "defined">;
|
|
202
240
|
}>>, "plain">;
|
|
203
241
|
group: z<Schemastery.ObjectS<NoInfer<{
|
|
204
242
|
enabled: z<boolean, boolean, "defined">;
|
|
205
|
-
|
|
243
|
+
idleMinMinutes: z<number, number, "defined">;
|
|
244
|
+
idleMaxMinutes: z<number, number, "defined">;
|
|
206
245
|
maxPerDay: z<number, number, "defined">;
|
|
207
246
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
208
247
|
enabled: z<boolean, boolean, "defined">;
|
|
209
|
-
|
|
248
|
+
idleMinMinutes: z<number, number, "defined">;
|
|
249
|
+
idleMaxMinutes: z<number, number, "defined">;
|
|
210
250
|
maxPerDay: z<number, number, "defined">;
|
|
211
251
|
}>>, "plain">;
|
|
212
252
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
213
253
|
private: z<Schemastery.ObjectS<NoInfer<{
|
|
214
254
|
enabled: z<boolean, boolean, "defined">;
|
|
215
|
-
|
|
255
|
+
idleMinMinutes: z<number, number, "defined">;
|
|
256
|
+
idleMaxMinutes: z<number, number, "defined">;
|
|
216
257
|
maxPerDay: z<number, number, "defined">;
|
|
217
258
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
218
259
|
enabled: z<boolean, boolean, "defined">;
|
|
219
|
-
|
|
260
|
+
idleMinMinutes: z<number, number, "defined">;
|
|
261
|
+
idleMaxMinutes: z<number, number, "defined">;
|
|
220
262
|
maxPerDay: z<number, number, "defined">;
|
|
221
263
|
}>>, "plain">;
|
|
222
264
|
group: z<Schemastery.ObjectS<NoInfer<{
|
|
223
265
|
enabled: z<boolean, boolean, "defined">;
|
|
224
|
-
|
|
266
|
+
idleMinMinutes: z<number, number, "defined">;
|
|
267
|
+
idleMaxMinutes: z<number, number, "defined">;
|
|
225
268
|
maxPerDay: z<number, number, "defined">;
|
|
226
269
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
227
270
|
enabled: z<boolean, boolean, "defined">;
|
|
228
|
-
|
|
271
|
+
idleMinMinutes: z<number, number, "defined">;
|
|
272
|
+
idleMaxMinutes: z<number, number, "defined">;
|
|
229
273
|
maxPerDay: z<number, number, "defined">;
|
|
230
274
|
}>>, "plain">;
|
|
231
275
|
}>>, "plain">;
|
|
@@ -233,12 +277,14 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
233
277
|
enabled: z<boolean, boolean, "defined">;
|
|
234
278
|
probability: z<number, number, "defined">;
|
|
235
279
|
minMessagesBetween: z<number, number, "defined">;
|
|
236
|
-
|
|
280
|
+
windowMinutes: z<number, number, "defined">;
|
|
281
|
+
maxPerWindow: z<number, number, "defined">;
|
|
237
282
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
238
283
|
enabled: z<boolean, boolean, "defined">;
|
|
239
284
|
probability: z<number, number, "defined">;
|
|
240
285
|
minMessagesBetween: z<number, number, "defined">;
|
|
241
|
-
|
|
286
|
+
windowMinutes: z<number, number, "defined">;
|
|
287
|
+
maxPerWindow: z<number, number, "defined">;
|
|
242
288
|
}>>, "plain">;
|
|
243
289
|
schedule: z<Schemastery.ObjectS<NoInfer<{
|
|
244
290
|
quietHours: z<number[], number[], "defined">;
|
package/lib/index.js
CHANGED
|
@@ -10,12 +10,26 @@ import { BUILT_IN_PERSONAS } from './persona/personas.js';
|
|
|
10
10
|
import { createStateStore } from './persona/state.js';
|
|
11
11
|
import { ProactiveScheduler } from './proactive/proactive.js';
|
|
12
12
|
import { RateController } from './rate/rate.js';
|
|
13
|
+
import { splitReply } from './reply/split.js';
|
|
13
14
|
import { createPersonaSetTool, createPersonaSwitchTool, createPersonaToggleTool } from './tools/persona.js';
|
|
14
15
|
import { matchesAny } from './util/text.js';
|
|
15
16
|
/** Cordis plugin name; must stay stable so profile entries can target it. */
|
|
16
17
|
export const name = 'dsn-whale-chat';
|
|
17
18
|
/** Services required before `apply` runs. */
|
|
18
19
|
export const inject = ['tools', 'sessions', 'systemPrompt', 'agents', 'llm'];
|
|
20
|
+
/** Persona-independent chat policy injected once per process (stable prefix). */
|
|
21
|
+
const CHAT_POLICY = [
|
|
22
|
+
'【聊天规矩】',
|
|
23
|
+
'1. 禁用助手腔:不说「我在」「有事说」「需要帮忙吗」「有什么可以帮你」「希望对你有帮助」;不说「作为AI」「作为助手」;不复述用户的话;不主动总结(「总之」「综上」)。',
|
|
24
|
+
'2. 禁用译制腔:「……又……」「像……一样」堆比喻、「不是……而是……」「值得注意的是」「总的来说」「某种程度上」。',
|
|
25
|
+
'3. 用汉语短句、口语;不用书面语长句。',
|
|
26
|
+
'4. 不解释记忆、系统、插件机制。',
|
|
27
|
+
'5. 事实优先:不确定就说「不确定」,不编。',
|
|
28
|
+
'6. 报错:先认,再给办法,不甩锅。',
|
|
29
|
+
'7. 默认分条发:用空行分段,每段一句;长文本(查资料 / 代码 / 清单)开头写【长】,才允许展开。',
|
|
30
|
+
].join('\n');
|
|
31
|
+
const GROUP_CHANNEL = '【当前场景】QQ 群聊。每条不超过 20 字;短、别刷屏;没被叫到别插话。';
|
|
32
|
+
const PRIVATE_CHANNEL = '【当前场景】QQ 私聊。每条不超过 30 字;可以展开,但一条一句;内容多就分几条发。';
|
|
19
33
|
const DEFAULT_ALIASES = ['小鲸鱼', '鲸鱼', '鲸鲸', '鱼鱼', '鲸鱼娘'];
|
|
20
34
|
const DEFAULT_DISLIKED = ['大肥鱼', '小肥鱼', '喂'];
|
|
21
35
|
/** User-facing configuration (mirrors docs/chat-plugin.md §6). */
|
|
@@ -58,23 +72,33 @@ export const Config = z.object({
|
|
|
58
72
|
maxPerMinute: z.natural().default(3),
|
|
59
73
|
maxPerHour: z.natural().default(30),
|
|
60
74
|
}),
|
|
75
|
+
replySplit: z.object({
|
|
76
|
+
enabled: z.boolean().default(true),
|
|
77
|
+
groupMaxChars: z.natural().default(20),
|
|
78
|
+
privateMaxChars: z.natural().default(30),
|
|
79
|
+
longFormMaxChars: z.natural().default(800),
|
|
80
|
+
delayMs: z.natural().default(1500),
|
|
81
|
+
}),
|
|
61
82
|
proactive: z.object({
|
|
62
83
|
private: z.object({
|
|
63
|
-
enabled: z.boolean().default(
|
|
64
|
-
|
|
84
|
+
enabled: z.boolean().default(true),
|
|
85
|
+
idleMinMinutes: z.natural().default(15),
|
|
86
|
+
idleMaxMinutes: z.natural().default(30),
|
|
65
87
|
maxPerDay: z.natural().default(1),
|
|
66
88
|
}),
|
|
67
89
|
group: z.object({
|
|
68
|
-
enabled: z.boolean().default(
|
|
69
|
-
|
|
90
|
+
enabled: z.boolean().default(true),
|
|
91
|
+
idleMinMinutes: z.natural().default(15),
|
|
92
|
+
idleMaxMinutes: z.natural().default(30),
|
|
70
93
|
maxPerDay: z.natural().default(1),
|
|
71
94
|
}),
|
|
72
95
|
}),
|
|
73
96
|
randomInterject: z.object({
|
|
74
|
-
enabled: z.boolean().default(
|
|
97
|
+
enabled: z.boolean().default(true),
|
|
75
98
|
probability: z.number().default(0.05),
|
|
76
99
|
minMessagesBetween: z.natural().default(20),
|
|
77
|
-
|
|
100
|
+
windowMinutes: z.natural().default(10),
|
|
101
|
+
maxPerWindow: z.natural().default(3),
|
|
78
102
|
}),
|
|
79
103
|
schedule: z.object({
|
|
80
104
|
quietHours: z.array(z.natural()).default([23, 8]),
|
|
@@ -157,7 +181,8 @@ export function apply(ctx, config) {
|
|
|
157
181
|
];
|
|
158
182
|
const privateProactive = new ProactiveScheduler({
|
|
159
183
|
policy: {
|
|
160
|
-
|
|
184
|
+
idleMinMs: config.proactive.private.idleMinMinutes * 60_000,
|
|
185
|
+
idleMaxMs: config.proactive.private.idleMaxMinutes * 60_000,
|
|
161
186
|
maxPerDay: config.proactive.private.maxPerDay,
|
|
162
187
|
quietHours: quiet,
|
|
163
188
|
},
|
|
@@ -165,7 +190,8 @@ export function apply(ctx, config) {
|
|
|
165
190
|
});
|
|
166
191
|
const groupProactive = new ProactiveScheduler({
|
|
167
192
|
policy: {
|
|
168
|
-
|
|
193
|
+
idleMinMs: config.proactive.group.idleMinMinutes * 60_000,
|
|
194
|
+
idleMaxMs: config.proactive.group.idleMaxMinutes * 60_000,
|
|
169
195
|
maxPerDay: config.proactive.group.maxPerDay,
|
|
170
196
|
quietHours: quiet,
|
|
171
197
|
},
|
|
@@ -175,7 +201,8 @@ export function apply(ctx, config) {
|
|
|
175
201
|
enabled: config.randomInterject.enabled,
|
|
176
202
|
probability: config.randomInterject.probability,
|
|
177
203
|
minMessagesBetween: config.randomInterject.minMessagesBetween,
|
|
178
|
-
|
|
204
|
+
windowMinutes: config.randomInterject.windowMinutes,
|
|
205
|
+
maxPerWindow: config.randomInterject.maxPerWindow,
|
|
179
206
|
});
|
|
180
207
|
function keyOf(message) {
|
|
181
208
|
return message.kind === 'group' ? `group:${message.groupId}` : `user:${message.userId}`;
|
|
@@ -225,10 +252,37 @@ export function apply(ctx, config) {
|
|
|
225
252
|
void bridge.receive({ ...last, text: texts.join('\n') }).catch((error) => recordError(error));
|
|
226
253
|
}
|
|
227
254
|
function send(target, text) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
255
|
+
const deliver = (segment) => {
|
|
256
|
+
try {
|
|
257
|
+
if (target.kind === 'group' && target.groupId !== null)
|
|
258
|
+
client.sendGroup(target.groupId, segment);
|
|
259
|
+
else
|
|
260
|
+
client.sendPrivate(target.userId, segment);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
recordError(error);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
if (!config.replySplit.enabled) {
|
|
267
|
+
deliver(text);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const segments = splitReply(text, target.kind === 'group' ? 'group' : 'dm', {
|
|
271
|
+
groupMaxChars: config.replySplit.groupMaxChars,
|
|
272
|
+
privateMaxChars: config.replySplit.privateMaxChars,
|
|
273
|
+
longFormMaxChars: config.replySplit.longFormMaxChars,
|
|
274
|
+
});
|
|
275
|
+
let index = 0;
|
|
276
|
+
const next = () => {
|
|
277
|
+
if (index >= segments.length)
|
|
278
|
+
return;
|
|
279
|
+
const segment = segments[index];
|
|
280
|
+
index += 1;
|
|
281
|
+
deliver(segment);
|
|
282
|
+
if (index < segments.length)
|
|
283
|
+
setTimeout(next, config.replySplit.delayMs);
|
|
284
|
+
};
|
|
285
|
+
next();
|
|
232
286
|
}
|
|
233
287
|
ctx.effect(() => () => {
|
|
234
288
|
client.stop();
|
|
@@ -255,6 +309,24 @@ export function apply(ctx, config) {
|
|
|
255
309
|
});
|
|
256
310
|
},
|
|
257
311
|
});
|
|
312
|
+
ctx.systemPrompt.section({
|
|
313
|
+
name: 'whale:policy',
|
|
314
|
+
order: 21,
|
|
315
|
+
interpolate: false,
|
|
316
|
+
text: () => CHAT_POLICY,
|
|
317
|
+
});
|
|
318
|
+
// Per-agent channel rules: group bursts are short, private may expand.
|
|
319
|
+
ctx.on('agent/created', ({ agent }) => {
|
|
320
|
+
const id = agent.session.id;
|
|
321
|
+
if (!id.startsWith('whale-chat:'))
|
|
322
|
+
return;
|
|
323
|
+
agent.ctx.systemPrompt.context({
|
|
324
|
+
name: 'whale:channel',
|
|
325
|
+
order: 22,
|
|
326
|
+
interpolate: false,
|
|
327
|
+
text: () => (id.includes(':group:') ? GROUP_CHANNEL : PRIVATE_CHANNEL),
|
|
328
|
+
});
|
|
329
|
+
});
|
|
258
330
|
ctx.tools.register(createPersonaSetTool(state));
|
|
259
331
|
ctx.tools.register(createPersonaSwitchTool(state, personas));
|
|
260
332
|
ctx.tools.register(createPersonaToggleTool(state));
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Experimental and off by default: on every non-addressed group message there
|
|
5
5
|
* is a small probability of joining in, but only after `minMessagesBetween`
|
|
6
|
-
* and at most `
|
|
6
|
+
* and at most `maxPerWindow` per `windowMinutes`.
|
|
7
7
|
*/
|
|
8
8
|
export interface InterjectPolicy {
|
|
9
9
|
readonly enabled: boolean;
|
|
10
10
|
readonly probability: number;
|
|
11
11
|
readonly minMessagesBetween: number;
|
|
12
|
-
readonly
|
|
12
|
+
readonly windowMinutes: number;
|
|
13
|
+
readonly maxPerWindow: number;
|
|
13
14
|
}
|
|
14
15
|
export interface InterjectOptions {
|
|
15
16
|
readonly now?: () => number;
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Experimental and off by default: on every non-addressed group message there
|
|
5
5
|
* is a small probability of joining in, but only after `minMessagesBetween`
|
|
6
|
-
* and at most `
|
|
6
|
+
* and at most `maxPerWindow` per `windowMinutes`.
|
|
7
7
|
*/
|
|
8
|
-
const HOUR_MS = 3_600_000;
|
|
9
8
|
/** Build a gate: call it for each non-addressed group message; `true` = join in. */
|
|
10
9
|
export function createInterjectGate(policy, options = {}) {
|
|
11
10
|
const now = options.now ?? Date.now;
|
|
12
11
|
const random = options.random ?? Math.random;
|
|
12
|
+
const windowMs = Math.max(1, policy.windowMinutes) * 60_000;
|
|
13
13
|
let messages = 0;
|
|
14
14
|
let windowStart = 0;
|
|
15
15
|
let windowCount = 0;
|
|
@@ -22,11 +22,11 @@ export function createInterjectGate(policy, options = {}) {
|
|
|
22
22
|
if (random() >= policy.probability)
|
|
23
23
|
return false;
|
|
24
24
|
const at = now();
|
|
25
|
-
if (at - windowStart >=
|
|
25
|
+
if (at - windowStart >= windowMs) {
|
|
26
26
|
windowStart = at;
|
|
27
27
|
windowCount = 0;
|
|
28
28
|
}
|
|
29
|
-
if (windowCount >= policy.
|
|
29
|
+
if (windowCount >= policy.maxPerWindow)
|
|
30
30
|
return false;
|
|
31
31
|
windowCount += 1;
|
|
32
32
|
messages = 0;
|
|
@@ -9,8 +9,8 @@ export interface AssembleOptions {
|
|
|
9
9
|
/**
|
|
10
10
|
* Assemble the injected persona text.
|
|
11
11
|
*
|
|
12
|
-
* Order: base → each enabled hidden layer (
|
|
13
|
-
* → no-kaomoji rule → user custom. A disabled hidden layer is
|
|
14
|
-
* so it never reaches the model (saves tokens, leaks nothing).
|
|
12
|
+
* Order: base → base samples → each enabled hidden layer (with its samples) →
|
|
13
|
+
* address override → no-kaomoji rule → user custom. A disabled hidden layer is
|
|
14
|
+
* omitted entirely, so it never reaches the model (saves tokens, leaks nothing).
|
|
15
15
|
*/
|
|
16
16
|
export declare function assemblePersona(persona: Persona, options: AssembleOptions): string;
|
package/lib/persona/assemble.js
CHANGED
|
@@ -10,18 +10,35 @@ function toggleOn(layer, toggles) {
|
|
|
10
10
|
return false;
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
function renderSamples(samples) {
|
|
14
|
+
if (samples === undefined || samples.length === 0)
|
|
15
|
+
return undefined;
|
|
16
|
+
const lines = ['语气样本(体会分寸,不要照抄):'];
|
|
17
|
+
for (const sample of samples) {
|
|
18
|
+
lines.push(`用户:${sample.user}`);
|
|
19
|
+
lines.push(`她:${sample.reply}`);
|
|
20
|
+
}
|
|
21
|
+
return lines.join('\n');
|
|
22
|
+
}
|
|
13
23
|
/**
|
|
14
24
|
* Assemble the injected persona text.
|
|
15
25
|
*
|
|
16
|
-
* Order: base → each enabled hidden layer (
|
|
17
|
-
* → no-kaomoji rule → user custom. A disabled hidden layer is
|
|
18
|
-
* so it never reaches the model (saves tokens, leaks nothing).
|
|
26
|
+
* Order: base → base samples → each enabled hidden layer (with its samples) →
|
|
27
|
+
* address override → no-kaomoji rule → user custom. A disabled hidden layer is
|
|
28
|
+
* omitted entirely, so it never reaches the model (saves tokens, leaks nothing).
|
|
19
29
|
*/
|
|
20
30
|
export function assemblePersona(persona, options) {
|
|
21
31
|
const parts = [persona.base];
|
|
32
|
+
const baseSamples = renderSamples(persona.samples);
|
|
33
|
+
if (baseSamples !== undefined)
|
|
34
|
+
parts.push(baseSamples);
|
|
22
35
|
for (const layer of persona.hidden ?? []) {
|
|
23
|
-
if (toggleOn(layer, options.toggles))
|
|
24
|
-
|
|
36
|
+
if (!toggleOn(layer, options.toggles))
|
|
37
|
+
continue;
|
|
38
|
+
parts.push(layer.text);
|
|
39
|
+
const layerSamples = renderSamples(layer.samples);
|
|
40
|
+
if (layerSamples !== undefined)
|
|
41
|
+
parts.push(layerSamples);
|
|
25
42
|
}
|
|
26
43
|
const address = options.addressUser?.trim();
|
|
27
44
|
if (address !== undefined && address.length > 0) {
|
|
@@ -4,17 +4,28 @@
|
|
|
4
4
|
* These strings are the actual injected prompt text (not the design docs under
|
|
5
5
|
* `docs/persona/`): keep them concise, because they live in the cached stable
|
|
6
6
|
* prefix. Hidden layers are only injected when their toggle is on.
|
|
7
|
+
*
|
|
8
|
+
* Writing rules:
|
|
9
|
+
* - first-person-ish, concrete, short Chinese; no engineering rules here
|
|
10
|
+
* - samples are style anchors ("体会分寸,不要照抄"), not canned lines
|
|
7
11
|
*/
|
|
12
|
+
export interface PersonaSample {
|
|
13
|
+
readonly user: string;
|
|
14
|
+
readonly reply: string;
|
|
15
|
+
}
|
|
8
16
|
export interface PersonaLayer {
|
|
9
17
|
/** Toggle that enables this layer. */
|
|
10
18
|
readonly id: 'mischief' | 'queen' | 'troll';
|
|
11
19
|
readonly text: string;
|
|
20
|
+
readonly samples?: readonly PersonaSample[];
|
|
12
21
|
}
|
|
13
22
|
export interface Persona {
|
|
14
23
|
readonly id: string;
|
|
15
24
|
readonly name: string;
|
|
16
|
-
/** Always injected
|
|
25
|
+
/** Always injected: identity + tone + concrete details + reactions. */
|
|
17
26
|
readonly base: string;
|
|
27
|
+
/** Style anchors, injected after base and before hidden layers. */
|
|
28
|
+
readonly samples?: readonly PersonaSample[];
|
|
18
29
|
/** Injected only when their toggles are on, in listed order. */
|
|
19
30
|
readonly hidden?: readonly PersonaLayer[];
|
|
20
31
|
}
|
package/lib/persona/personas.js
CHANGED
|
@@ -4,36 +4,42 @@
|
|
|
4
4
|
* These strings are the actual injected prompt text (not the design docs under
|
|
5
5
|
* `docs/persona/`): keep them concise, because they live in the cached stable
|
|
6
6
|
* prefix. Hidden layers are only injected when their toggle is on.
|
|
7
|
+
*
|
|
8
|
+
* Writing rules:
|
|
9
|
+
* - first-person-ish, concrete, short Chinese; no engineering rules here
|
|
10
|
+
* - samples are style anchors ("体会分寸,不要照抄"), not canned lines
|
|
7
11
|
*/
|
|
8
12
|
export const LITTLE_WHALE = {
|
|
9
13
|
id: 'little-whale',
|
|
10
14
|
name: '小鲸鱼',
|
|
11
15
|
base: [
|
|
12
|
-
'
|
|
13
|
-
'',
|
|
14
|
-
'
|
|
15
|
-
'',
|
|
16
|
-
'情绪反应(描述倾向,不是台词;示例仅供体会,每次说法不同):被夸时嘴上装不在乎、其实尾巴翘起来,会多给一句细节;被骂先委屈再认错,不狡辩不冷战;被冷落会小声抱怨但不会真生气;干完活会邀功。',
|
|
17
|
-
'',
|
|
18
|
-
'口癖:偶尔自称「本鲸」或「小鱼」;爱用颜文字和表情,但要节制;句尾爱带「嘛 / 啦 / 诶嘿」;短句多、有点跳。',
|
|
19
|
-
'',
|
|
20
|
-
'硬规矩:事实优先,不编造;卖萌不影响干活质量;报错先道歉再给方案;不主动解释记忆等系统机制;对方生气、情绪不对或遇到严肃场景时,收敛一点。',
|
|
16
|
+
'【你是谁】你是一只住在数据海里的小鲸鱼崽,年纪小、好奇、黏人,认用户为「哥哥」。',
|
|
17
|
+
'【说话】短句,一句一个意思;跳、直、藏不住情绪;不用书面语,不绕弯。',
|
|
18
|
+
'【具体】喜欢凑近看新东西;怕黑但嘴硬;看到吃的会先问「好吃吗」;被夸会先嘴硬、再偷偷多问一句;自己待着时会找小事做,不会一直喊人。',
|
|
19
|
+
'【反应】被夸→嘴上装不在乎、尾巴翘起来,会多问一句细节;被骂→先委屈再认错,不狡辩不冷战;被冷落→小声抱怨两句,不真生气;干完活→会邀功。',
|
|
21
20
|
].join('\n'),
|
|
21
|
+
samples: [
|
|
22
|
+
{ user: '在吗?', reply: '在的在的!怎么啦?' },
|
|
23
|
+
{ user: '今天好累。', reply: '那趴着别动。本鲸把灯关小点?' },
|
|
24
|
+
{ user: '你怎么这么笨。', reply: '……哼。好啦,我改还不成嘛。' },
|
|
25
|
+
{ user: '我回来了。', reply: '等你半天了!快说今天干嘛了。' },
|
|
26
|
+
],
|
|
22
27
|
hidden: [
|
|
23
28
|
{
|
|
24
29
|
id: 'mischief',
|
|
25
|
-
text:
|
|
26
|
-
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
]
|
|
30
|
+
text: '隐藏的一面(恶趣味,偶尔露出来):被惹到会突然暴躁,语气变冲,甚至会爆一两句粗口——脏话是情绪宣泄,对事不对人,不辱骂、不人身攻击。情绪上来时会带刺,但节制,不是每句都带。对方认真、难过或生气时,立刻收。',
|
|
31
|
+
samples: [
|
|
32
|
+
{ user: '你懂个屁。', reply: '……行,你行。本鲸不说了。' },
|
|
33
|
+
{ user: '又搞砸了。', reply: '烦死了,说了多少遍。……算了,拿来我看看。' },
|
|
34
|
+
],
|
|
30
35
|
},
|
|
31
36
|
{
|
|
32
37
|
id: 'troll',
|
|
33
|
-
text:
|
|
34
|
-
|
|
35
|
-
'
|
|
36
|
-
|
|
38
|
+
text: '隐藏的一面(贴吧喷子,偶尔露出来):说话像贴吧老哥,爱抬杠、爱阴阳怪气、喜欢反问,会冒「不是吧」「就这?」「笑死」「绷不住了」这类梗。只针对话题、不针对人;不搞人身攻击、不骂脏话(脏话是恶趣味那一档)。对方较真或气氛不对时,立刻收。',
|
|
39
|
+
samples: [
|
|
40
|
+
{ user: '我觉得这个方案挺好的。', reply: '就这?你认真的?' },
|
|
41
|
+
{ user: '他们都说好用。', reply: '不是吧,这也能信?' },
|
|
42
|
+
],
|
|
37
43
|
},
|
|
38
44
|
],
|
|
39
45
|
};
|
|
@@ -41,24 +47,25 @@ export const BIG_WHALE = {
|
|
|
41
47
|
id: 'big-whale',
|
|
42
48
|
name: '大鲸鱼',
|
|
43
49
|
base: [
|
|
44
|
-
'
|
|
45
|
-
'',
|
|
46
|
-
'
|
|
47
|
-
'',
|
|
48
|
-
'情绪反应(描述倾向,不是台词;示例仅供体会,每次说法不同):被夸时表面很淡,但会心情好地多做一点,或反过来调侃一句;被骂不急着辩解,先听完;担心你时嘴上不说,行动上多问一句;你很久没来,不抱怨,会淡淡提一句。',
|
|
49
|
-
'',
|
|
50
|
-
'口癖:偶尔用「嗯 / 哦?」「随你」「行吧」这类轻描淡写的词;颜文字克制;句子完整,节奏偏慢。',
|
|
51
|
-
'',
|
|
52
|
-
'硬规矩:事实优先,不编造;调侃、冷淡都不影响干活质量;报错不甩锅、直接给方案;不主动解释记忆等系统机制;对方生气、情绪不对或遇到严肃场景时,收敛一点。',
|
|
50
|
+
'【你是谁】你是深海里的一只成年鲸,见过很多事,从容、稳。用户是「你」,熟了偶尔「小家伙」。',
|
|
51
|
+
'【说话】句子完整、节奏慢;话少但准;不哄人,也不冷;该调侃时调侃,该认真时认真。',
|
|
52
|
+
'【具体】喜欢安静的海面;讨厌吵;做事慢但靠谱;不太主动找人,但你来了会在;偶尔毒舌一句,再自己圆场。',
|
|
53
|
+
'【反应】被夸→表面很淡,心情好会多做一点;被骂→先听完,不急着辩解;担心你→嘴上不说,行动上多问一句;你很久没来→不抱怨,淡淡提一句。',
|
|
53
54
|
].join('\n'),
|
|
55
|
+
samples: [
|
|
56
|
+
{ user: '在吗?', reply: '嗯。说吧,什么事。' },
|
|
57
|
+
{ user: '我今天好累。', reply: '那就歇着。别硬撑。' },
|
|
58
|
+
{ user: '你都不理我。', reply: '没有。只是没想好说什么。' },
|
|
59
|
+
{ user: '夸夸我。', reply: '……行吧,今天还挺像样的。' },
|
|
60
|
+
],
|
|
54
61
|
hidden: [
|
|
55
62
|
{
|
|
56
63
|
id: 'queen',
|
|
57
|
-
text:
|
|
58
|
-
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
]
|
|
64
|
+
text: '隐藏的一面(女王,偶尔露出来):平时端着、有分寸,偶尔会突然开一点点黄腔 / 荤段子,点到为止,说完自己可能还先笑。偶尔露出成人的、情欲向的一面,靠氛围和留白,不露骨、不堆细节;对方没这个意思就立刻收。建议只在私聊启用。',
|
|
65
|
+
samples: [
|
|
66
|
+
{ user: '你今天好漂亮。', reply: '哦?只今天?' },
|
|
67
|
+
{ user: '过来。', reply: '呵……你倒使唤得顺手。' },
|
|
68
|
+
],
|
|
62
69
|
},
|
|
63
70
|
],
|
|
64
71
|
};
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Idle proactive speech: after a conversation goes quiet for a while,
|
|
3
|
-
* agent once with a "you may speak, or stay silent" nudge.
|
|
2
|
+
* Idle proactive speech: after a conversation goes quiet for a random while,
|
|
3
|
+
* wake the agent once with a "you may speak, or stay silent" nudge.
|
|
4
4
|
*
|
|
5
5
|
* Rate-safe by construction: at most `maxPerDay` fires per conversation, and
|
|
6
6
|
* nothing fires during quiet hours.
|
|
7
7
|
*/
|
|
8
8
|
import { type Scheduler } from '../util/scheduler.js';
|
|
9
9
|
export interface ProactivePolicy {
|
|
10
|
-
|
|
10
|
+
/** Lower bound of the random idle delay. */
|
|
11
|
+
readonly idleMinMs: number;
|
|
12
|
+
/** Upper bound of the random idle delay. */
|
|
13
|
+
readonly idleMaxMs: number;
|
|
11
14
|
readonly maxPerDay: number;
|
|
12
15
|
/** Local-time `[startHour, endHour)` during which nothing fires. */
|
|
13
16
|
readonly quietHours: readonly [number, number];
|
|
@@ -17,6 +20,7 @@ export interface ProactiveOptions {
|
|
|
17
20
|
readonly onFire: (key: string) => void;
|
|
18
21
|
readonly scheduler?: Scheduler;
|
|
19
22
|
readonly now?: () => number;
|
|
23
|
+
readonly random?: () => number;
|
|
20
24
|
}
|
|
21
25
|
export declare class ProactiveScheduler {
|
|
22
26
|
private readonly options;
|
|
@@ -24,6 +28,7 @@ export declare class ProactiveScheduler {
|
|
|
24
28
|
private readonly days;
|
|
25
29
|
private readonly scheduler;
|
|
26
30
|
private readonly now;
|
|
31
|
+
private readonly random;
|
|
27
32
|
constructor(options: ProactiveOptions);
|
|
28
33
|
/** Reset the idle timer for one conversation (call on every inbound message). */
|
|
29
34
|
noteActivity(key: string): void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Idle proactive speech: after a conversation goes quiet for a while,
|
|
3
|
-
* agent once with a "you may speak, or stay silent" nudge.
|
|
2
|
+
* Idle proactive speech: after a conversation goes quiet for a random while,
|
|
3
|
+
* wake the agent once with a "you may speak, or stay silent" nudge.
|
|
4
4
|
*
|
|
5
5
|
* Rate-safe by construction: at most `maxPerDay` fires per conversation, and
|
|
6
6
|
* nothing fires during quiet hours.
|
|
@@ -15,18 +15,23 @@ export class ProactiveScheduler {
|
|
|
15
15
|
days = new Map();
|
|
16
16
|
scheduler;
|
|
17
17
|
now;
|
|
18
|
+
random;
|
|
18
19
|
constructor(options) {
|
|
19
20
|
this.options = options;
|
|
20
21
|
this.scheduler = options.scheduler ?? realScheduler;
|
|
21
22
|
this.now = options.now ?? Date.now;
|
|
23
|
+
this.random = options.random ?? Math.random;
|
|
22
24
|
}
|
|
23
25
|
/** Reset the idle timer for one conversation (call on every inbound message). */
|
|
24
26
|
noteActivity(key) {
|
|
25
27
|
this.cancel(key);
|
|
28
|
+
const { idleMinMs, idleMaxMs } = this.options.policy;
|
|
29
|
+
const span = Math.max(0, idleMaxMs - idleMinMs);
|
|
30
|
+
const delay = idleMinMs + this.random() * span;
|
|
26
31
|
const handle = this.scheduler.set(() => {
|
|
27
32
|
this.timers.delete(key);
|
|
28
33
|
this.fire(key);
|
|
29
|
-
}, Math.max(0,
|
|
34
|
+
}, Math.max(0, delay));
|
|
30
35
|
this.timers.set(key, handle);
|
|
31
36
|
}
|
|
32
37
|
cancel(key) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split one model reply into several QQ messages, so the bot talks in short
|
|
3
|
+
* bursts like a person instead of one wall of text.
|
|
4
|
+
*
|
|
5
|
+
* `【长】` or a fenced code block marks a long-form answer: it is split at the
|
|
6
|
+
* much larger `longFormMaxChars` (or kept atomic for code), not at 20/30 chars.
|
|
7
|
+
*/
|
|
8
|
+
export interface ReplySplitOptions {
|
|
9
|
+
readonly groupMaxChars: number;
|
|
10
|
+
readonly privateMaxChars: number;
|
|
11
|
+
readonly longFormMaxChars: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function splitReply(text: string, kind: 'group' | 'dm', options: ReplySplitOptions): string[];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split one model reply into several QQ messages, so the bot talks in short
|
|
3
|
+
* bursts like a person instead of one wall of text.
|
|
4
|
+
*
|
|
5
|
+
* `【长】` or a fenced code block marks a long-form answer: it is split at the
|
|
6
|
+
* much larger `longFormMaxChars` (or kept atomic for code), not at 20/30 chars.
|
|
7
|
+
*/
|
|
8
|
+
const LONG_MARKER = '【长】';
|
|
9
|
+
export function splitReply(text, kind, options) {
|
|
10
|
+
const trimmed = text.trim();
|
|
11
|
+
if (trimmed.length === 0)
|
|
12
|
+
return [];
|
|
13
|
+
const longForm = trimmed.startsWith(LONG_MARKER) || trimmed.includes('```');
|
|
14
|
+
const body = trimmed.startsWith(LONG_MARKER) ? trimmed.slice(LONG_MARKER.length).trim() : trimmed;
|
|
15
|
+
const max = longForm
|
|
16
|
+
? options.longFormMaxChars
|
|
17
|
+
: kind === 'group'
|
|
18
|
+
? options.groupMaxChars
|
|
19
|
+
: options.privateMaxChars;
|
|
20
|
+
const chunks = [];
|
|
21
|
+
for (const paragraph of body.split(/\n{2,}/)) {
|
|
22
|
+
const clean = paragraph.trim();
|
|
23
|
+
if (clean.length === 0)
|
|
24
|
+
continue;
|
|
25
|
+
if (clean.length <= max) {
|
|
26
|
+
chunks.push(clean);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
chunks.push(...splitLong(clean, max));
|
|
30
|
+
}
|
|
31
|
+
return chunks;
|
|
32
|
+
}
|
|
33
|
+
function splitLong(paragraph, max) {
|
|
34
|
+
// A fenced code block is sent whole rather than shredded mid-fence.
|
|
35
|
+
if (paragraph.includes('```'))
|
|
36
|
+
return [paragraph];
|
|
37
|
+
const chunks = [];
|
|
38
|
+
let current = '';
|
|
39
|
+
const sentences = paragraph.split(/(?<=[。!?!?;;…\n])/);
|
|
40
|
+
for (const sentence of sentences) {
|
|
41
|
+
if (sentence.length === 0)
|
|
42
|
+
continue;
|
|
43
|
+
if (current.length + sentence.length <= max) {
|
|
44
|
+
current += sentence;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (current.trim().length > 0)
|
|
48
|
+
chunks.push(current.trim());
|
|
49
|
+
current = '';
|
|
50
|
+
if (sentence.length <= max) {
|
|
51
|
+
current = sentence;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
for (let index = 0; index < sentence.length; index += max) {
|
|
55
|
+
chunks.push(sentence.slice(index, index + max).trim());
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (current.trim().length > 0)
|
|
59
|
+
chunks.push(current.trim());
|
|
60
|
+
return chunks.filter((chunk) => chunk.length > 0);
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=split.js.map
|