@dsh-plugin/dsh-synapse 0.0.0-stage → 0.1.0-dev.41044818627
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/LICENSE +661 -0
- package/NOTICE.md +18 -0
- package/README.md +136 -2
- package/cordis.patch.yml +3 -0
- package/lib/bot.cjs +8314 -0
- package/lib/bot.cjs.map +7 -0
- package/lib/build.json +1 -0
- package/lib/client.js +46 -0
- package/lib/host/agent.d.ts +21 -0
- package/lib/host/agent.js +151 -0
- package/lib/host/agent.js.map +1 -0
- package/lib/host/bot.d.ts +17 -0
- package/lib/host/bot.js +158 -0
- package/lib/host/bot.js.map +1 -0
- package/lib/host/client.d.ts +2 -0
- package/lib/host/client.js +19 -0
- package/lib/host/client.js.map +1 -0
- package/lib/host/config.d.ts +24 -0
- package/lib/host/config.js +192 -0
- package/lib/host/config.js.map +1 -0
- package/lib/host/files.d.ts +29 -0
- package/lib/host/files.js +188 -0
- package/lib/host/files.js.map +1 -0
- package/lib/host/gateway.d.ts +33 -0
- package/lib/host/gateway.js +237 -0
- package/lib/host/gateway.js.map +1 -0
- package/lib/host/github.d.ts +47 -0
- package/lib/host/github.js +219 -0
- package/lib/host/github.js.map +1 -0
- package/lib/host/http.d.ts +4 -0
- package/lib/host/http.js +41 -0
- package/lib/host/http.js.map +1 -0
- package/lib/host/index.d.ts +32 -0
- package/lib/host/index.js +192 -0
- package/lib/host/index.js.map +1 -0
- package/lib/host/inference-worker.d.ts +1 -0
- package/lib/host/inference-worker.js +72 -0
- package/lib/host/inference-worker.js.map +1 -0
- package/lib/host/mcp.d.ts +138 -0
- package/lib/host/mcp.js +104 -0
- package/lib/host/mcp.js.map +1 -0
- package/lib/host/memory.d.ts +26 -0
- package/lib/host/memory.js +155 -0
- package/lib/host/memory.js.map +1 -0
- package/lib/host/panel.d.ts +1 -0
- package/lib/host/panel.html +174 -0
- package/lib/host/panel.js +388 -0
- package/lib/host/panel.js.map +1 -0
- package/lib/host/runtime.d.ts +21 -0
- package/lib/host/runtime.js +145 -0
- package/lib/host/runtime.js.map +1 -0
- package/lib/host/types.d.ts +112 -0
- package/lib/host/types.js +2 -0
- package/lib/host/types.js.map +1 -0
- package/package.json +94 -4
- package/scripts/build.mjs +90 -0
- package/scripts/check.mjs +13 -0
- package/scripts/dump-npm-latest.mjs +49 -0
- package/scripts/model-smoke.mjs +25 -0
- package/src/agent.ts +203 -0
- package/src/bot.ts +173 -0
- package/src/client.ts +20 -0
- package/src/config.ts +217 -0
- package/src/files.ts +206 -0
- package/src/gateway.ts +276 -0
- package/src/github.ts +263 -0
- package/src/http.ts +41 -0
- package/src/index.ts +227 -0
- package/src/inference-worker.ts +89 -0
- package/src/mcp.ts +112 -0
- package/src/memory.ts +178 -0
- package/src/panel.html +174 -0
- package/src/panel.ts +440 -0
- package/src/runtime.ts +164 -0
- package/src/types.ts +106 -0
- package/test/core.test.mjs +279 -0
- package/test/fixtures/mcp-server.mjs +14 -0
- package/test/integration.test.mjs +253 -0
- package/test/panel.test.mjs +73 -0
- package/tsconfig.json +18 -0
- package/vendor/gugle-chat-bot/LICENSE +661 -0
- package/vendor/gugle-chat-bot/package.json +57 -0
- package/vendor/gugle-chat-bot/src/allow-rule.yml +9345 -0
- package/vendor/gugle-chat-bot/src/api.json +4285 -0
- package/vendor/gugle-chat-bot/src/command/index.ts +141 -0
- package/vendor/gugle-chat-bot/src/config/features.ts +548 -0
- package/vendor/gugle-chat-bot/src/config/index.ts +184 -0
- package/vendor/gugle-chat-bot/src/config/manager.ts +357 -0
- package/vendor/gugle-chat-bot/src/constants/index.ts +13 -0
- package/vendor/gugle-chat-bot/src/custom.ts +548 -0
- package/vendor/gugle-chat-bot/src/event/index.ts +118 -0
- package/vendor/gugle-chat-bot/src/features/admin/index.ts +414 -0
- package/vendor/gugle-chat-bot/src/features/bili/image/impl/video.ts +39 -0
- package/vendor/gugle-chat-bot/src/features/bili/image/index.ts +37 -0
- package/vendor/gugle-chat-bot/src/features/bili/index.ts +65 -0
- package/vendor/gugle-chat-bot/src/features/bili/template/style/video.css +97 -0
- package/vendor/gugle-chat-bot/src/features/bili/template/video.html +57 -0
- package/vendor/gugle-chat-bot/src/features/discord-bridge/index.ts +1128 -0
- package/vendor/gugle-chat-bot/src/features/discord-bridge/markdown.ts +105 -0
- package/vendor/gugle-chat-bot/src/features/discord-bridge/slash.ts +355 -0
- package/vendor/gugle-chat-bot/src/features/discord-bridge/source.ts +96 -0
- package/vendor/gugle-chat-bot/src/features/discord-bridge/template/message.html +14 -0
- package/vendor/gugle-chat-bot/src/features/discord-bridge/template/style/message.css +144 -0
- package/vendor/gugle-chat-bot/src/features/github/api.ts +93 -0
- package/vendor/gugle-chat-bot/src/features/github/binding.ts +161 -0
- package/vendor/gugle-chat-bot/src/features/github/image/impl/issue.ts +132 -0
- package/vendor/gugle-chat-bot/src/features/github/image/impl/pr.ts +144 -0
- package/vendor/gugle-chat-bot/src/features/github/image/impl/release.ts +160 -0
- package/vendor/gugle-chat-bot/src/features/github/image/index.ts +310 -0
- package/vendor/gugle-chat-bot/src/features/github/index.ts +302 -0
- package/vendor/gugle-chat-bot/src/features/github/template/issue.html +118 -0
- package/vendor/gugle-chat-bot/src/features/github/template/pull_request.html +125 -0
- package/vendor/gugle-chat-bot/src/features/github/template/release.html +53 -0
- package/vendor/gugle-chat-bot/src/features/github/template/style/issue.css +468 -0
- package/vendor/gugle-chat-bot/src/features/github/template/style/pull_request.css +469 -0
- package/vendor/gugle-chat-bot/src/features/github/template/style/release.css +412 -0
- package/vendor/gugle-chat-bot/src/features/hermes/client.ts +184 -0
- package/vendor/gugle-chat-bot/src/features/hermes/config.ts +210 -0
- package/vendor/gugle-chat-bot/src/features/hermes/context.ts +67 -0
- package/vendor/gugle-chat-bot/src/features/hermes/history.ts +86 -0
- package/vendor/gugle-chat-bot/src/features/hermes/index.ts +1533 -0
- package/vendor/gugle-chat-bot/src/features/hermes/message.ts +319 -0
- package/vendor/gugle-chat-bot/src/features/hermes/reaction.ts +181 -0
- package/vendor/gugle-chat-bot/src/features/hermes/render.ts +118 -0
- package/vendor/gugle-chat-bot/src/features/hermes/reply-units.ts +331 -0
- package/vendor/gugle-chat-bot/src/features/hermes/skills.ts +406 -0
- package/vendor/gugle-chat-bot/src/features/hermes/template/approval.html +30 -0
- package/vendor/gugle-chat-bot/src/features/hermes/template/progress.html +21 -0
- package/vendor/gugle-chat-bot/src/features/hermes/template/style/approval.css +81 -0
- package/vendor/gugle-chat-bot/src/features/hermes/template/style/progress.css +120 -0
- package/vendor/gugle-chat-bot/src/features/hermes/trigger.ts +86 -0
- package/vendor/gugle-chat-bot/src/features/hermes/types.ts +327 -0
- package/vendor/gugle-chat-bot/src/features/management/index.ts +156 -0
- package/vendor/gugle-chat-bot/src/features/minecraft/index.ts +127 -0
- package/vendor/gugle-chat-bot/src/features/modrinth/index.ts +71 -0
- package/vendor/gugle-chat-bot/src/features/parentheses/index.ts +117 -0
- package/vendor/gugle-chat-bot/src/features/peak-valley-timer/index.ts +452 -0
- package/vendor/gugle-chat-bot/src/features/poke/index.ts +24 -0
- package/vendor/gugle-chat-bot/src/features/version-tracker/index.ts +78 -0
- package/vendor/gugle-chat-bot/src/features/welcome/index.ts +58 -0
- package/vendor/gugle-chat-bot/src/github-ip.json +5814 -0
- package/vendor/gugle-chat-bot/src/image/index.ts +278 -0
- package/vendor/gugle-chat-bot/src/index.ts +695 -0
- package/vendor/gugle-chat-bot/src/logger/index.ts +54 -0
- package/vendor/gugle-chat-bot/src/result.json +359 -0
- package/vendor/gugle-chat-bot/src/type/bili.d.ts +825 -0
- package/vendor/gugle-chat-bot/src/type/event.d.ts +146 -0
- package/vendor/gugle-chat-bot/src/type/github.d.ts +446 -0
- package/vendor/gugle-chat-bot/src/type/index.d.ts +254 -0
- package/vendor/gugle-chat-bot/tsconfig.json +46 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { splitMessageText } from '@/features/hermes/trigger';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* QQ 侧 AI 回复的分句与「是否改用聊天记录(合并转发)发送」判定(纯函数,抽离自 HermesBridge 以便冒烟测试)。
|
|
5
|
+
*
|
|
6
|
+
* - `splitReplyIntoUnits`:一次性把完整回复切成语义单元(运行结束时使用);
|
|
7
|
+
* - `takeCompleteUnits`:流式场景下只取「已完整」的单元,未成形的内容留在 rest,
|
|
8
|
+
* 尤其要保住 `[SKILL:` / `MEDIA:` 标签与未闭合的代码围栏,防止半截标签被原样发到群里。
|
|
9
|
+
*
|
|
10
|
+
* 本模块不产生任何 IO。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface ReplyUnitOptions {
|
|
14
|
+
/** 单元最大长度:超过则用既有 splitMessageText 二次切分(通常传 config.maxMessageLength) */
|
|
15
|
+
maxUnitLength: number;
|
|
16
|
+
/** 单条超过该字数改用聊天记录发送;0 = 关闭该维度 */
|
|
17
|
+
compactMaxChars: number;
|
|
18
|
+
/** 单条超过该行数改用聊天记录发送;0 = 关闭该维度 */
|
|
19
|
+
compactMaxLines: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ReplyUnit {
|
|
23
|
+
text: string;
|
|
24
|
+
asForward: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 句子结束标点:命中后连同其后的收尾引号/括号一起切走 */
|
|
28
|
+
const SENTENCE_ENDINGS = '。!?!?;;…';
|
|
29
|
+
/** 句末标点后可一并切走的收尾引号与括号 */
|
|
30
|
+
const CLOSING_MARKS = '”’"\'」』))】]》〉';
|
|
31
|
+
/** 技能/媒体标签:命中即把该单元及其后的内容留在 rest,交由调用方在运行结束时统一处理 */
|
|
32
|
+
const RESERVED_LABEL_PATTERN = /\[SKILL:|MEDIA:/;
|
|
33
|
+
|
|
34
|
+
/** 单元在缓冲区中的位置(start/end 指向原始缓冲区下标,text 已裁剪首尾空白) */
|
|
35
|
+
interface ScannedUnit {
|
|
36
|
+
text: string;
|
|
37
|
+
start: number;
|
|
38
|
+
end: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 是否为空白字符(\s 语义,含制表符/换行/不换行空格) */
|
|
42
|
+
function isWhitespace(char: string): boolean {
|
|
43
|
+
return char !== '' && /\s/.test(char);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 是否为行内缩进字符(列表续行、围栏缩进判定用) */
|
|
47
|
+
function isIndent(char: string): boolean {
|
|
48
|
+
return char === ' ' || char === '\t' || char === '\r';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 从 from 起跳过全部空白,返回首个非空白字符下标 */
|
|
52
|
+
function skipWhitespace(source: string, from: number): number {
|
|
53
|
+
let index = from;
|
|
54
|
+
while (index < source.length && isWhitespace(source[index] ?? '')) index++;
|
|
55
|
+
return index;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** index 是否为行首内容位(其前缀只有缩进,即列表项/围栏可以出现的位置) */
|
|
59
|
+
function isLineHead(source: string, index: number): boolean {
|
|
60
|
+
let cursor = index - 1;
|
|
61
|
+
while (cursor >= 0 && isIndent(source[cursor] ?? '')) cursor--;
|
|
62
|
+
return cursor < 0 || source[cursor] === '\n';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 列表项前缀长度(`- ` / `* ` / `+ ` / `1. ` / `1、` 等);非列表项返回 0 */
|
|
66
|
+
function listPrefixLength(source: string, index: number): number {
|
|
67
|
+
const char = source[index] ?? '';
|
|
68
|
+
if (char === '-' || char === '*' || char === '+') {
|
|
69
|
+
const next = source[index + 1] ?? '';
|
|
70
|
+
return next === ' ' || next === '\t' ? 2 : 0;
|
|
71
|
+
}
|
|
72
|
+
if (char >= '0' && char <= '9') {
|
|
73
|
+
let cursor = index;
|
|
74
|
+
while (cursor < source.length && (source[cursor] ?? '') >= '0' && (source[cursor] ?? '') <= '9') cursor++;
|
|
75
|
+
const separator = source[cursor] ?? '';
|
|
76
|
+
if (separator !== '.' && separator !== '、' && separator !== ')' && separator !== ')') return 0;
|
|
77
|
+
const after = source[cursor + 1] ?? '';
|
|
78
|
+
return cursor - index + 1 + (after === ' ' || after === '\t' ? 1 : 0);
|
|
79
|
+
}
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 统计 index 起连续相同字符的长度 */
|
|
84
|
+
function countRun(source: string, index: number, char: string): number {
|
|
85
|
+
let cursor = index;
|
|
86
|
+
while (cursor < source.length && source[cursor] === char) cursor++;
|
|
87
|
+
return cursor - index;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* ASCII 句点是否可作为句末边界。
|
|
92
|
+
*
|
|
93
|
+
* 只在「其后是空白或缓冲区末尾」且「前一字符不是数字」时成立,避免把小数(3.14)、
|
|
94
|
+
* 版本号(v1.0)、IP(192.168.1.1)与域名(example.com)切碎——中文场景下这些比
|
|
95
|
+
* 英文句末出现得更频繁。缓冲区末尾的句点视为完整句(下一段文本会重新拼接)。
|
|
96
|
+
*/
|
|
97
|
+
function isAsciiPeriodBoundary(source: string, index: number): boolean {
|
|
98
|
+
const previous = index > 0 ? (source[index - 1] ?? '') : '';
|
|
99
|
+
if (previous >= '0' && previous <= '9') return false;
|
|
100
|
+
const next = source[index + 1];
|
|
101
|
+
if (next === undefined) return true;
|
|
102
|
+
return isWhitespace(next);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** index 是否为围栏行首(Markdown 允许至多 3 个空格缩进) */
|
|
106
|
+
function isFenceHead(source: string, index: number): boolean {
|
|
107
|
+
let cursor = index - 1;
|
|
108
|
+
let spaces = 0;
|
|
109
|
+
while (cursor >= 0 && isIndent(source[cursor] ?? '')) {
|
|
110
|
+
if (source[cursor] !== '\r') spaces++;
|
|
111
|
+
cursor--;
|
|
112
|
+
}
|
|
113
|
+
if (spaces > 3) return false;
|
|
114
|
+
return cursor < 0 || source[cursor] === '\n';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** start~stop 这一行是否为闭合围栏(同类标记且不少于开启长度,行内无其他内容) */
|
|
118
|
+
function isClosingFence(source: string, start: number, stop: number, marker: string, minLength: number): boolean {
|
|
119
|
+
let cursor = start;
|
|
120
|
+
while (cursor < stop && isIndent(source[cursor] ?? '')) cursor++;
|
|
121
|
+
let count = 0;
|
|
122
|
+
while (cursor < stop && source[cursor] === marker) {
|
|
123
|
+
count++;
|
|
124
|
+
cursor++;
|
|
125
|
+
}
|
|
126
|
+
if (count < minLength) return false;
|
|
127
|
+
while (cursor < stop && isIndent(source[cursor] ?? '')) cursor++;
|
|
128
|
+
return cursor >= stop;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 查找围栏块结束下标(不含),未闭合或围栏行尚未结束时返回 -1。
|
|
133
|
+
* 返回位置已包含闭合围栏行及其换行符。
|
|
134
|
+
*/
|
|
135
|
+
function findFenceEnd(source: string, index: number, marker: string, markerLength: number): number {
|
|
136
|
+
const lineEnd = source.indexOf('\n', index);
|
|
137
|
+
if (lineEnd < 0) return -1;
|
|
138
|
+
let cursor = lineEnd + 1;
|
|
139
|
+
while (cursor < source.length) {
|
|
140
|
+
const stop = source.indexOf('\n', cursor);
|
|
141
|
+
const lineStop = stop < 0 ? source.length : stop;
|
|
142
|
+
if (isClosingFence(source, cursor, lineStop, marker, markerLength)) {
|
|
143
|
+
return stop < 0 ? source.length : stop + 1;
|
|
144
|
+
}
|
|
145
|
+
if (stop < 0) return -1;
|
|
146
|
+
cursor = stop + 1;
|
|
147
|
+
}
|
|
148
|
+
return -1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 查找连续列表块的结束下标(含最后一行的换行符)。
|
|
153
|
+
* complete=false 表示列表块一直延伸到缓冲区末尾、仍可能继续增长(流式场景需留在 rest)。
|
|
154
|
+
*/
|
|
155
|
+
function findListBlockEnd(source: string, start: number): { end: number; complete: boolean } {
|
|
156
|
+
const length = source.length;
|
|
157
|
+
let cursor = start;
|
|
158
|
+
while (cursor < length) {
|
|
159
|
+
const lineEnd = source.indexOf('\n', cursor);
|
|
160
|
+
if (lineEnd < 0) return { end: length, complete: false };
|
|
161
|
+
const end = lineEnd + 1;
|
|
162
|
+
const next = end;
|
|
163
|
+
if (next >= length) return { end, complete: false };
|
|
164
|
+
if ((source[next] ?? '') === '\n') return { end, complete: true };
|
|
165
|
+
// 缩进续行或同类列表项 → 并入同一单元;普通新段落 → 列表块结束
|
|
166
|
+
if (!isLineHead(source, next) || listPrefixLength(source, next) > 0) {
|
|
167
|
+
cursor = next;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
return { end, complete: true };
|
|
171
|
+
}
|
|
172
|
+
return { end: length, complete: false };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 扫描缓冲区并切出语义单元。
|
|
177
|
+
*
|
|
178
|
+
* 边界规则:句末标点(含其后收尾引号/括号)、空行、换行、代码围栏、连续列表块。
|
|
179
|
+
* final=true 时末段即使没有边界也作为最后一个单元;final=false 时未成形内容全部留在
|
|
180
|
+
* consumed 之后(由 takeCompleteUnits 作为 rest 返回),未闭合的围栏亦不下发。
|
|
181
|
+
*/
|
|
182
|
+
function scanUnits(source: string, final: boolean): { units: ScannedUnit[]; consumed: number } {
|
|
183
|
+
const units: ScannedUnit[] = [];
|
|
184
|
+
const length = source.length;
|
|
185
|
+
let consumed = 0;
|
|
186
|
+
let unitStart = skipWhitespace(source, 0);
|
|
187
|
+
let index = unitStart;
|
|
188
|
+
|
|
189
|
+
/** 把 [unitStart, end) 作为一个单元收下,并跳到其后的首个非空白字符 */
|
|
190
|
+
const emit = (end: number): void => {
|
|
191
|
+
const text = source.slice(unitStart, end).trim();
|
|
192
|
+
if (text) units.push({ text, start: unitStart, end });
|
|
193
|
+
index = skipWhitespace(source, end);
|
|
194
|
+
unitStart = index;
|
|
195
|
+
if (units.length > 0) consumed = index;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
while (index < length) {
|
|
199
|
+
const char = source[index] ?? '';
|
|
200
|
+
|
|
201
|
+
// 代码围栏(``` / ~~~):整块原子,未闭合时停止扫描(非最终模式留给 rest)
|
|
202
|
+
if ((char === '`' || char === '~') && isFenceHead(source, index)) {
|
|
203
|
+
const runLength = countRun(source, index, char);
|
|
204
|
+
if (runLength >= 3) {
|
|
205
|
+
const fenceEnd = findFenceEnd(source, index, char, runLength);
|
|
206
|
+
if (fenceEnd >= 0) {
|
|
207
|
+
emit(fenceEnd);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (final) emit(length);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 列表块:连续列表项行合并为一个单元,避免逐条刷屏
|
|
216
|
+
if (
|
|
217
|
+
(char === '-' || char === '*' || char === '+' || (char >= '0' && char <= '9')) &&
|
|
218
|
+
isLineHead(source, index) &&
|
|
219
|
+
listPrefixLength(source, index) > 0
|
|
220
|
+
) {
|
|
221
|
+
const block = findListBlockEnd(source, index);
|
|
222
|
+
if (block.complete || final) {
|
|
223
|
+
emit(block.end);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 句末标点:连同后续的收尾引号/括号一起切走
|
|
230
|
+
if (SENTENCE_ENDINGS.includes(char) || (char === '.' && isAsciiPeriodBoundary(source, index))) {
|
|
231
|
+
let end = index + 1;
|
|
232
|
+
while (end < length && SENTENCE_ENDINGS.includes(source[end] ?? '')) end++;
|
|
233
|
+
while (end < length && CLOSING_MARKS.includes(source[end] ?? '')) end++;
|
|
234
|
+
emit(end);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 换行:段落边界;缓冲区以换行结尾时视为「尚未成形」,等后续内容一并判定
|
|
239
|
+
if (char === '\n') {
|
|
240
|
+
if (index + 1 < length || final) {
|
|
241
|
+
emit(index);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
index++;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 最终模式:末段没有边界也作为最后一个单元(否则调用方会丢字)
|
|
251
|
+
if (final && unitStart < length) {
|
|
252
|
+
const text = source.slice(unitStart).trim();
|
|
253
|
+
if (text) units.push({ text, start: unitStart, end: length });
|
|
254
|
+
consumed = length;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return { units, consumed };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** 单条是否应改用聊天记录(合并转发)发送:任一维度超限即命中,0 表示关闭该维度 */
|
|
261
|
+
export function shouldCompact(text: string, options: ReplyUnitOptions): boolean {
|
|
262
|
+
if (options.compactMaxChars > 0 && text.length > options.compactMaxChars) return true;
|
|
263
|
+
if (options.compactMaxLines > 0 && text.split('\n').length > options.compactMaxLines) return true;
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** 超长单元先用既有 splitMessageText 二次切分(maxUnitLength<=0 时不切) */
|
|
268
|
+
function splitByLength(text: string, maxUnitLength: number): string[] {
|
|
269
|
+
if (maxUnitLength <= 0) return [text];
|
|
270
|
+
return splitMessageText(text, maxUnitLength);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** 文本列表 → 回复单元:逐块独立判定 asForward,空块不产出单元 */
|
|
274
|
+
function toReplyUnits(texts: string[], options: ReplyUnitOptions): ReplyUnit[] {
|
|
275
|
+
const units: ReplyUnit[] = [];
|
|
276
|
+
for (const text of texts) {
|
|
277
|
+
for (const piece of splitByLength(text, options.maxUnitLength)) {
|
|
278
|
+
const trimmed = piece.trim();
|
|
279
|
+
if (!trimmed) continue;
|
|
280
|
+
units.push({ text: trimmed, asForward: shouldCompact(trimmed, options) });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return units;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 把完整回复切成回复单元(最终模式:运行结束、标签已处理完时使用)。
|
|
288
|
+
*
|
|
289
|
+
* 与 takeCompleteUnits 的区别是「末段一律成形」:缓冲区以未闭合围栏结尾时,整块(含围栏行)
|
|
290
|
+
* 作为一个单元发出;末尾没有边界的残句也照发。调用方用它对 rest 做收尾 flush 时不会丢字,
|
|
291
|
+
* 除非输入本身为空/纯空白(此时返回 [])。
|
|
292
|
+
* 单元长度超过 maxUnitLength 时按 splitMessageText 二次切分,拼接结果与输入一致(仅裁剪空白)。
|
|
293
|
+
*/
|
|
294
|
+
export function splitReplyIntoUnits(text: string, options: ReplyUnitOptions): ReplyUnit[] {
|
|
295
|
+
if (!text) return [];
|
|
296
|
+
return toReplyUnits(
|
|
297
|
+
scanUnits(text, true).units.map(unit => unit.text),
|
|
298
|
+
options
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* 流式取「已完整」的回复单元:缓冲区末尾未成形的内容(含未闭合围栏)留在 rest,
|
|
304
|
+
* 由调用方在运行结束时统一处理。
|
|
305
|
+
*
|
|
306
|
+
* 标签保留:一旦某个单元命中 `[SKILL:` 或 `MEDIA:`(子串匹配),从该单元起(含该单元)
|
|
307
|
+
* 及其后的全部内容都留在 rest——宁可多留,也绝不让标签经流式路径被原样发到群里。
|
|
308
|
+
* 注意 rest 只保证不丢字,不管标签是否闭合;标签的处理与最终下发由调用方负责。
|
|
309
|
+
*/
|
|
310
|
+
export function takeCompleteUnits(buffer: string, options: ReplyUnitOptions): { units: ReplyUnit[]; rest: string } {
|
|
311
|
+
if (!buffer) return { units: [], rest: '' };
|
|
312
|
+
const scanned = scanUnits(buffer, false);
|
|
313
|
+
|
|
314
|
+
let cut = scanned.units.length;
|
|
315
|
+
let restStart = scanned.consumed;
|
|
316
|
+
for (let index = 0; index < scanned.units.length; index++) {
|
|
317
|
+
const unit = scanned.units[index]!;
|
|
318
|
+
if (!RESERVED_LABEL_PATTERN.test(unit.text)) continue;
|
|
319
|
+
cut = index;
|
|
320
|
+
restStart = unit.start;
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
units: toReplyUnits(
|
|
326
|
+
scanned.units.slice(0, cut).map(unit => unit.text),
|
|
327
|
+
options
|
|
328
|
+
),
|
|
329
|
+
rest: buffer.slice(restStart)
|
|
330
|
+
};
|
|
331
|
+
}
|