@openclaw/zalouser 2026.3.10 → 2026.3.12
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/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/channel.sendpayload.test.ts +36 -4
- package/src/channel.test.ts +46 -11
- package/src/channel.ts +21 -6
- package/src/config-schema.ts +1 -0
- package/src/group-policy.test.ts +12 -0
- package/src/group-policy.ts +7 -4
- package/src/monitor.group-gating.test.ts +109 -1
- package/src/monitor.ts +24 -14
- package/src/send.test.ts +247 -9
- package/src/send.ts +187 -2
- package/src/text-styles.test.ts +203 -0
- package/src/text-styles.ts +537 -0
- package/src/types.ts +7 -0
- package/src/zalo-js.ts +48 -2
- package/src/zca-client.ts +33 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
import { TextStyle, type Style } from "./zca-client.js";
|
|
2
|
+
|
|
3
|
+
type InlineStyle = (typeof TextStyle)[keyof typeof TextStyle];
|
|
4
|
+
|
|
5
|
+
type LineStyle = {
|
|
6
|
+
lineIndex: number;
|
|
7
|
+
style: InlineStyle;
|
|
8
|
+
indentSize?: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type Segment = {
|
|
12
|
+
text: string;
|
|
13
|
+
styles: InlineStyle[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type InlineMarker = {
|
|
17
|
+
pattern: RegExp;
|
|
18
|
+
extractText: (match: RegExpExecArray) => string;
|
|
19
|
+
resolveStyles?: (match: RegExpExecArray) => InlineStyle[];
|
|
20
|
+
literal?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type ResolvedInlineMatch = {
|
|
24
|
+
match: RegExpExecArray;
|
|
25
|
+
marker: InlineMarker;
|
|
26
|
+
styles: InlineStyle[];
|
|
27
|
+
text: string;
|
|
28
|
+
priority: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type FenceMarker = {
|
|
32
|
+
char: "`" | "~";
|
|
33
|
+
length: number;
|
|
34
|
+
indent: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type ActiveFence = FenceMarker & {
|
|
38
|
+
quoteIndent: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const TAG_STYLE_MAP: Record<string, InlineStyle | null> = {
|
|
42
|
+
red: TextStyle.Red,
|
|
43
|
+
orange: TextStyle.Orange,
|
|
44
|
+
yellow: TextStyle.Yellow,
|
|
45
|
+
green: TextStyle.Green,
|
|
46
|
+
small: null,
|
|
47
|
+
big: TextStyle.Big,
|
|
48
|
+
underline: TextStyle.Underline,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const INLINE_MARKERS: InlineMarker[] = [
|
|
52
|
+
{
|
|
53
|
+
pattern: /`([^`\n]+)`/g,
|
|
54
|
+
extractText: (match) => match[0],
|
|
55
|
+
literal: true,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
pattern: /\\([*_~#\\{}>+\-`])/g,
|
|
59
|
+
extractText: (match) => match[1],
|
|
60
|
+
literal: true,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
pattern: new RegExp(`\\{(${Object.keys(TAG_STYLE_MAP).join("|")})\\}(.+?)\\{/\\1\\}`, "g"),
|
|
64
|
+
extractText: (match) => match[2],
|
|
65
|
+
resolveStyles: (match) => {
|
|
66
|
+
const style = TAG_STYLE_MAP[match[1]];
|
|
67
|
+
return style ? [style] : [];
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
pattern: /(?<!\*)\*\*\*(?=\S)([^\n]*?\S)(?<!\*)\*\*\*(?!\*)/g,
|
|
72
|
+
extractText: (match) => match[1],
|
|
73
|
+
resolveStyles: () => [TextStyle.Bold, TextStyle.Italic],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
pattern: /(?<!\*)\*\*(?![\s*])([^\n]*?\S)(?<!\*)\*\*(?!\*)/g,
|
|
77
|
+
extractText: (match) => match[1],
|
|
78
|
+
resolveStyles: () => [TextStyle.Bold],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
pattern: /(?<![\w_])__(?![\s_])([^\n]*?\S)(?<!_)__(?![\w_])/g,
|
|
82
|
+
extractText: (match) => match[1],
|
|
83
|
+
resolveStyles: () => [TextStyle.Bold],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
pattern: /(?<!~)~~(?=\S)([^\n]*?\S)(?<!~)~~(?!~)/g,
|
|
87
|
+
extractText: (match) => match[1],
|
|
88
|
+
resolveStyles: () => [TextStyle.StrikeThrough],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
pattern: /(?<!\*)\*(?![\s*])([^\n]*?\S)(?<!\*)\*(?!\*)/g,
|
|
92
|
+
extractText: (match) => match[1],
|
|
93
|
+
resolveStyles: () => [TextStyle.Italic],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
pattern: /(?<![\w_])_(?![\s_])([^\n]*?\S)(?<!_)_(?![\w_])/g,
|
|
97
|
+
extractText: (match) => match[1],
|
|
98
|
+
resolveStyles: () => [TextStyle.Italic],
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
export function parseZalouserTextStyles(input: string): { text: string; styles: Style[] } {
|
|
103
|
+
const allStyles: Style[] = [];
|
|
104
|
+
|
|
105
|
+
const escapeMap: string[] = [];
|
|
106
|
+
const lines = input.replace(/\r\n?/g, "\n").split("\n");
|
|
107
|
+
const lineStyles: LineStyle[] = [];
|
|
108
|
+
const processedLines: string[] = [];
|
|
109
|
+
let activeFence: ActiveFence | null = null;
|
|
110
|
+
|
|
111
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
112
|
+
const rawLine = lines[lineIndex];
|
|
113
|
+
const { text: unquotedLine, indent: baseIndent } = stripQuotePrefix(rawLine);
|
|
114
|
+
|
|
115
|
+
if (activeFence) {
|
|
116
|
+
const codeLine =
|
|
117
|
+
activeFence.quoteIndent > 0
|
|
118
|
+
? stripQuotePrefix(rawLine, activeFence.quoteIndent).text
|
|
119
|
+
: rawLine;
|
|
120
|
+
if (isClosingFence(codeLine, activeFence)) {
|
|
121
|
+
activeFence = null;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
processedLines.push(
|
|
125
|
+
escapeLiteralText(
|
|
126
|
+
normalizeCodeBlockLeadingWhitespace(stripCodeFenceIndent(codeLine, activeFence.indent)),
|
|
127
|
+
escapeMap,
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let line = unquotedLine;
|
|
134
|
+
const openingFence = resolveOpeningFence(rawLine);
|
|
135
|
+
if (openingFence) {
|
|
136
|
+
const fenceLine = openingFence.quoteIndent > 0 ? unquotedLine : rawLine;
|
|
137
|
+
if (!hasClosingFence(lines, lineIndex + 1, openingFence)) {
|
|
138
|
+
processedLines.push(escapeLiteralText(fenceLine, escapeMap));
|
|
139
|
+
activeFence = openingFence;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
activeFence = openingFence;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const outputLineIndex = processedLines.length;
|
|
147
|
+
if (isIndentedCodeBlockLine(line)) {
|
|
148
|
+
if (baseIndent > 0) {
|
|
149
|
+
lineStyles.push({
|
|
150
|
+
lineIndex: outputLineIndex,
|
|
151
|
+
style: TextStyle.Indent,
|
|
152
|
+
indentSize: baseIndent,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
processedLines.push(escapeLiteralText(normalizeCodeBlockLeadingWhitespace(line), escapeMap));
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const { text: markdownLine, size: markdownPadding } = stripOptionalMarkdownPadding(line);
|
|
160
|
+
|
|
161
|
+
const headingMatch = markdownLine.match(/^(#{1,4})\s(.*)$/);
|
|
162
|
+
if (headingMatch) {
|
|
163
|
+
const depth = headingMatch[1].length;
|
|
164
|
+
lineStyles.push({ lineIndex: outputLineIndex, style: TextStyle.Bold });
|
|
165
|
+
if (depth === 1) {
|
|
166
|
+
lineStyles.push({ lineIndex: outputLineIndex, style: TextStyle.Big });
|
|
167
|
+
}
|
|
168
|
+
if (baseIndent > 0) {
|
|
169
|
+
lineStyles.push({
|
|
170
|
+
lineIndex: outputLineIndex,
|
|
171
|
+
style: TextStyle.Indent,
|
|
172
|
+
indentSize: baseIndent,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
processedLines.push(headingMatch[2]);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const indentMatch = markdownLine.match(/^(\s+)(.*)$/);
|
|
180
|
+
let indentLevel = 0;
|
|
181
|
+
let content = markdownLine;
|
|
182
|
+
if (indentMatch) {
|
|
183
|
+
indentLevel = clampIndent(indentMatch[1].length);
|
|
184
|
+
content = indentMatch[2];
|
|
185
|
+
}
|
|
186
|
+
const totalIndent = Math.min(5, baseIndent + indentLevel);
|
|
187
|
+
|
|
188
|
+
if (/^[-*+]\s\[[ xX]\]\s/.test(content)) {
|
|
189
|
+
if (totalIndent > 0) {
|
|
190
|
+
lineStyles.push({
|
|
191
|
+
lineIndex: outputLineIndex,
|
|
192
|
+
style: TextStyle.Indent,
|
|
193
|
+
indentSize: totalIndent,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
processedLines.push(content);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const orderedListMatch = content.match(/^(\d+)\.\s(.*)$/);
|
|
201
|
+
if (orderedListMatch) {
|
|
202
|
+
if (totalIndent > 0) {
|
|
203
|
+
lineStyles.push({
|
|
204
|
+
lineIndex: outputLineIndex,
|
|
205
|
+
style: TextStyle.Indent,
|
|
206
|
+
indentSize: totalIndent,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
lineStyles.push({ lineIndex: outputLineIndex, style: TextStyle.OrderedList });
|
|
210
|
+
processedLines.push(orderedListMatch[2]);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const unorderedListMatch = content.match(/^[-*+]\s(.*)$/);
|
|
215
|
+
if (unorderedListMatch) {
|
|
216
|
+
if (totalIndent > 0) {
|
|
217
|
+
lineStyles.push({
|
|
218
|
+
lineIndex: outputLineIndex,
|
|
219
|
+
style: TextStyle.Indent,
|
|
220
|
+
indentSize: totalIndent,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
lineStyles.push({ lineIndex: outputLineIndex, style: TextStyle.UnorderedList });
|
|
224
|
+
processedLines.push(unorderedListMatch[1]);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (markdownPadding > 0) {
|
|
229
|
+
if (baseIndent > 0) {
|
|
230
|
+
lineStyles.push({
|
|
231
|
+
lineIndex: outputLineIndex,
|
|
232
|
+
style: TextStyle.Indent,
|
|
233
|
+
indentSize: baseIndent,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
processedLines.push(line);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (totalIndent > 0) {
|
|
241
|
+
lineStyles.push({
|
|
242
|
+
lineIndex: outputLineIndex,
|
|
243
|
+
style: TextStyle.Indent,
|
|
244
|
+
indentSize: totalIndent,
|
|
245
|
+
});
|
|
246
|
+
processedLines.push(content);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
processedLines.push(line);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const segments = parseInlineSegments(processedLines.join("\n"));
|
|
254
|
+
|
|
255
|
+
let plainText = "";
|
|
256
|
+
for (const segment of segments) {
|
|
257
|
+
const start = plainText.length;
|
|
258
|
+
plainText += segment.text;
|
|
259
|
+
for (const style of segment.styles) {
|
|
260
|
+
allStyles.push({ start, len: segment.text.length, st: style } as Style);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (escapeMap.length > 0) {
|
|
265
|
+
const escapeRegex = /\x01(\d+)\x02/g;
|
|
266
|
+
const shifts: Array<{ pos: number; delta: number }> = [];
|
|
267
|
+
let cumulativeDelta = 0;
|
|
268
|
+
|
|
269
|
+
for (const match of plainText.matchAll(escapeRegex)) {
|
|
270
|
+
const escapeIndex = Number.parseInt(match[1], 10);
|
|
271
|
+
cumulativeDelta += match[0].length - escapeMap[escapeIndex].length;
|
|
272
|
+
shifts.push({ pos: (match.index ?? 0) + match[0].length, delta: cumulativeDelta });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for (const style of allStyles) {
|
|
276
|
+
let startDelta = 0;
|
|
277
|
+
let endDelta = 0;
|
|
278
|
+
const end = style.start + style.len;
|
|
279
|
+
for (const shift of shifts) {
|
|
280
|
+
if (shift.pos <= style.start) {
|
|
281
|
+
startDelta = shift.delta;
|
|
282
|
+
}
|
|
283
|
+
if (shift.pos <= end) {
|
|
284
|
+
endDelta = shift.delta;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
style.start -= startDelta;
|
|
288
|
+
style.len -= endDelta - startDelta;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
plainText = plainText.replace(
|
|
292
|
+
escapeRegex,
|
|
293
|
+
(_match, index) => escapeMap[Number.parseInt(index, 10)],
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const finalLines = plainText.split("\n");
|
|
298
|
+
let offset = 0;
|
|
299
|
+
for (let lineIndex = 0; lineIndex < finalLines.length; lineIndex += 1) {
|
|
300
|
+
const lineLength = finalLines[lineIndex].length;
|
|
301
|
+
if (lineLength > 0) {
|
|
302
|
+
for (const lineStyle of lineStyles) {
|
|
303
|
+
if (lineStyle.lineIndex !== lineIndex) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (lineStyle.style === TextStyle.Indent) {
|
|
308
|
+
allStyles.push({
|
|
309
|
+
start: offset,
|
|
310
|
+
len: lineLength,
|
|
311
|
+
st: TextStyle.Indent,
|
|
312
|
+
indentSize: lineStyle.indentSize,
|
|
313
|
+
});
|
|
314
|
+
} else {
|
|
315
|
+
allStyles.push({ start: offset, len: lineLength, st: lineStyle.style } as Style);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
offset += lineLength + 1;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return { text: plainText, styles: allStyles };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function clampIndent(spaceCount: number): number {
|
|
326
|
+
return Math.min(5, Math.max(1, Math.floor(spaceCount / 2)));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function stripOptionalMarkdownPadding(line: string): { text: string; size: number } {
|
|
330
|
+
const match = line.match(/^( {1,3})(?=\S)/);
|
|
331
|
+
if (!match) {
|
|
332
|
+
return { text: line, size: 0 };
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
text: line.slice(match[1].length),
|
|
336
|
+
size: match[1].length,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function hasClosingFence(lines: string[], startIndex: number, fence: ActiveFence): boolean {
|
|
341
|
+
for (let index = startIndex; index < lines.length; index += 1) {
|
|
342
|
+
const candidate =
|
|
343
|
+
fence.quoteIndent > 0 ? stripQuotePrefix(lines[index], fence.quoteIndent).text : lines[index];
|
|
344
|
+
if (isClosingFence(candidate, fence)) {
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function resolveOpeningFence(line: string): ActiveFence | null {
|
|
352
|
+
const directFence = parseFenceMarker(line);
|
|
353
|
+
if (directFence) {
|
|
354
|
+
return { ...directFence, quoteIndent: 0 };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const quoted = stripQuotePrefix(line);
|
|
358
|
+
if (quoted.indent === 0) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const quotedFence = parseFenceMarker(quoted.text);
|
|
363
|
+
if (!quotedFence) {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
...quotedFence,
|
|
369
|
+
quoteIndent: quoted.indent,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function stripQuotePrefix(
|
|
374
|
+
line: string,
|
|
375
|
+
maxDepth = Number.POSITIVE_INFINITY,
|
|
376
|
+
): { text: string; indent: number } {
|
|
377
|
+
let cursor = 0;
|
|
378
|
+
while (cursor < line.length && cursor < 3 && line[cursor] === " ") {
|
|
379
|
+
cursor += 1;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
let removedDepth = 0;
|
|
383
|
+
let consumedCursor = cursor;
|
|
384
|
+
while (removedDepth < maxDepth && consumedCursor < line.length && line[consumedCursor] === ">") {
|
|
385
|
+
removedDepth += 1;
|
|
386
|
+
consumedCursor += 1;
|
|
387
|
+
if (line[consumedCursor] === " ") {
|
|
388
|
+
consumedCursor += 1;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (removedDepth === 0) {
|
|
393
|
+
return { text: line, indent: 0 };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
text: line.slice(consumedCursor),
|
|
398
|
+
indent: Math.min(5, removedDepth),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function parseFenceMarker(line: string): FenceMarker | null {
|
|
403
|
+
const match = line.match(/^([ ]{0,3})(`{3,}|~{3,})(.*)$/);
|
|
404
|
+
if (!match) {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const marker = match[2];
|
|
409
|
+
const char = marker[0];
|
|
410
|
+
if (char !== "`" && char !== "~") {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
char,
|
|
416
|
+
length: marker.length,
|
|
417
|
+
indent: match[1].length,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function isClosingFence(line: string, fence: FenceMarker): boolean {
|
|
422
|
+
const match = line.match(/^([ ]{0,3})(`{3,}|~{3,})[ \t]*$/);
|
|
423
|
+
if (!match) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
return match[2][0] === fence.char && match[2].length >= fence.length;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function escapeLiteralText(input: string, escapeMap: string[]): string {
|
|
430
|
+
return input.replace(/[\\*_~{}`]/g, (ch) => {
|
|
431
|
+
const index = escapeMap.length;
|
|
432
|
+
escapeMap.push(ch);
|
|
433
|
+
return `\x01${index}\x02`;
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function parseInlineSegments(text: string, inheritedStyles: InlineStyle[] = []): Segment[] {
|
|
438
|
+
const segments: Segment[] = [];
|
|
439
|
+
let cursor = 0;
|
|
440
|
+
|
|
441
|
+
while (cursor < text.length) {
|
|
442
|
+
const nextMatch = findNextInlineMatch(text, cursor);
|
|
443
|
+
if (!nextMatch) {
|
|
444
|
+
pushSegment(segments, text.slice(cursor), inheritedStyles);
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (nextMatch.match.index > cursor) {
|
|
449
|
+
pushSegment(segments, text.slice(cursor, nextMatch.match.index), inheritedStyles);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const combinedStyles = [...inheritedStyles, ...nextMatch.styles];
|
|
453
|
+
if (nextMatch.marker.literal) {
|
|
454
|
+
pushSegment(segments, nextMatch.text, combinedStyles);
|
|
455
|
+
} else {
|
|
456
|
+
segments.push(...parseInlineSegments(nextMatch.text, combinedStyles));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
cursor = nextMatch.match.index + nextMatch.match[0].length;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return segments;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function findNextInlineMatch(text: string, startIndex: number): ResolvedInlineMatch | null {
|
|
466
|
+
let bestMatch: ResolvedInlineMatch | null = null;
|
|
467
|
+
|
|
468
|
+
for (const [priority, marker] of INLINE_MARKERS.entries()) {
|
|
469
|
+
const regex = new RegExp(marker.pattern.source, marker.pattern.flags);
|
|
470
|
+
regex.lastIndex = startIndex;
|
|
471
|
+
const match = regex.exec(text);
|
|
472
|
+
if (!match) {
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (
|
|
477
|
+
bestMatch &&
|
|
478
|
+
(match.index > bestMatch.match.index ||
|
|
479
|
+
(match.index === bestMatch.match.index && priority > bestMatch.priority))
|
|
480
|
+
) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
bestMatch = {
|
|
485
|
+
match,
|
|
486
|
+
marker,
|
|
487
|
+
text: marker.extractText(match),
|
|
488
|
+
styles: marker.resolveStyles?.(match) ?? [],
|
|
489
|
+
priority,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return bestMatch;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function pushSegment(segments: Segment[], text: string, styles: InlineStyle[]): void {
|
|
497
|
+
if (!text) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const lastSegment = segments.at(-1);
|
|
502
|
+
if (lastSegment && sameStyles(lastSegment.styles, styles)) {
|
|
503
|
+
lastSegment.text += text;
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
segments.push({
|
|
508
|
+
text,
|
|
509
|
+
styles: [...styles],
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function sameStyles(left: InlineStyle[], right: InlineStyle[]): boolean {
|
|
514
|
+
return left.length === right.length && left.every((style, index) => style === right[index]);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function normalizeCodeBlockLeadingWhitespace(line: string): string {
|
|
518
|
+
return line.replace(/^[ \t]+/, (leadingWhitespace) =>
|
|
519
|
+
leadingWhitespace.replace(/\t/g, "\u00A0\u00A0\u00A0\u00A0").replace(/ /g, "\u00A0"),
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function isIndentedCodeBlockLine(line: string): boolean {
|
|
524
|
+
return /^(?: {4,}|\t)/.test(line);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function stripCodeFenceIndent(line: string, indent: number): string {
|
|
528
|
+
let consumed = 0;
|
|
529
|
+
let cursor = 0;
|
|
530
|
+
|
|
531
|
+
while (cursor < line.length && consumed < indent && line[cursor] === " ") {
|
|
532
|
+
cursor += 1;
|
|
533
|
+
consumed += 1;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return line.slice(cursor);
|
|
537
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Style } from "./zca-client.js";
|
|
2
|
+
|
|
1
3
|
export type ZcaFriend = {
|
|
2
4
|
userId: string;
|
|
3
5
|
displayName: string;
|
|
@@ -59,6 +61,10 @@ export type ZaloSendOptions = {
|
|
|
59
61
|
caption?: string;
|
|
60
62
|
isGroup?: boolean;
|
|
61
63
|
mediaLocalRoots?: readonly string[];
|
|
64
|
+
textMode?: "markdown" | "plain";
|
|
65
|
+
textChunkMode?: "length" | "newline";
|
|
66
|
+
textChunkLimit?: number;
|
|
67
|
+
textStyles?: Style[];
|
|
62
68
|
};
|
|
63
69
|
|
|
64
70
|
export type ZaloSendResult = {
|
|
@@ -91,6 +97,7 @@ type ZalouserSharedConfig = {
|
|
|
91
97
|
enabled?: boolean;
|
|
92
98
|
name?: string;
|
|
93
99
|
profile?: string;
|
|
100
|
+
dangerouslyAllowNameMatching?: boolean;
|
|
94
101
|
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
|
95
102
|
allowFrom?: Array<string | number>;
|
|
96
103
|
historyLimit?: number;
|
package/src/zalo-js.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
} from "./types.js";
|
|
21
21
|
import {
|
|
22
22
|
LoginQRCallbackEventType,
|
|
23
|
+
TextStyle,
|
|
23
24
|
ThreadType,
|
|
24
25
|
Zalo,
|
|
25
26
|
type API,
|
|
@@ -136,6 +137,39 @@ function toErrorMessage(error: unknown): string {
|
|
|
136
137
|
return String(error);
|
|
137
138
|
}
|
|
138
139
|
|
|
140
|
+
function clampTextStyles(
|
|
141
|
+
text: string,
|
|
142
|
+
styles?: ZaloSendOptions["textStyles"],
|
|
143
|
+
): ZaloSendOptions["textStyles"] {
|
|
144
|
+
if (!styles || styles.length === 0) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
const maxLength = text.length;
|
|
148
|
+
const clamped = styles
|
|
149
|
+
.map((style) => {
|
|
150
|
+
const start = Math.max(0, Math.min(style.start, maxLength));
|
|
151
|
+
const end = Math.min(style.start + style.len, maxLength);
|
|
152
|
+
if (end <= start) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
if (style.st === TextStyle.Indent) {
|
|
156
|
+
return {
|
|
157
|
+
start,
|
|
158
|
+
len: end - start,
|
|
159
|
+
st: style.st,
|
|
160
|
+
indentSize: style.indentSize,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
start,
|
|
165
|
+
len: end - start,
|
|
166
|
+
st: style.st,
|
|
167
|
+
};
|
|
168
|
+
})
|
|
169
|
+
.filter((style): style is NonNullable<typeof style> => style !== null);
|
|
170
|
+
return clamped.length > 0 ? clamped : undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
139
173
|
function toNumberId(value: unknown): string {
|
|
140
174
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
141
175
|
return String(Math.trunc(value));
|
|
@@ -1018,11 +1052,16 @@ export async function sendZaloTextMessage(
|
|
|
1018
1052
|
kind: media.kind,
|
|
1019
1053
|
});
|
|
1020
1054
|
const payloadText = (text || options.caption || "").slice(0, 2000);
|
|
1055
|
+
const textStyles = clampTextStyles(payloadText, options.textStyles);
|
|
1021
1056
|
|
|
1022
1057
|
if (media.kind === "audio") {
|
|
1023
1058
|
let textMessageId: string | undefined;
|
|
1024
1059
|
if (payloadText) {
|
|
1025
|
-
const textResponse = await api.sendMessage(
|
|
1060
|
+
const textResponse = await api.sendMessage(
|
|
1061
|
+
textStyles ? { msg: payloadText, styles: textStyles } : payloadText,
|
|
1062
|
+
trimmedThreadId,
|
|
1063
|
+
type,
|
|
1064
|
+
);
|
|
1026
1065
|
textMessageId = extractSendMessageId(textResponse);
|
|
1027
1066
|
}
|
|
1028
1067
|
|
|
@@ -1055,6 +1094,7 @@ export async function sendZaloTextMessage(
|
|
|
1055
1094
|
const response = await api.sendMessage(
|
|
1056
1095
|
{
|
|
1057
1096
|
msg: payloadText,
|
|
1097
|
+
...(textStyles ? { styles: textStyles } : {}),
|
|
1058
1098
|
attachments: [
|
|
1059
1099
|
{
|
|
1060
1100
|
data: media.buffer,
|
|
@@ -1071,7 +1111,13 @@ export async function sendZaloTextMessage(
|
|
|
1071
1111
|
return { ok: true, messageId: extractSendMessageId(response) };
|
|
1072
1112
|
}
|
|
1073
1113
|
|
|
1074
|
-
const
|
|
1114
|
+
const payloadText = text.slice(0, 2000);
|
|
1115
|
+
const textStyles = clampTextStyles(payloadText, options.textStyles);
|
|
1116
|
+
const response = await api.sendMessage(
|
|
1117
|
+
textStyles ? { msg: payloadText, styles: textStyles } : payloadText,
|
|
1118
|
+
trimmedThreadId,
|
|
1119
|
+
type,
|
|
1120
|
+
);
|
|
1075
1121
|
return { ok: true, messageId: extractSendMessageId(response) };
|
|
1076
1122
|
} catch (error) {
|
|
1077
1123
|
return { ok: false, error: toErrorMessage(error) };
|
package/src/zca-client.ts
CHANGED
|
@@ -28,6 +28,39 @@ export const Reactions = ReactionsRuntime as Record<string, string> & {
|
|
|
28
28
|
NONE: string;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
// Mirror zca-js sendMessage style constants locally because the package root
|
|
32
|
+
// typing surface does not consistently expose TextStyle/Style to tsgo.
|
|
33
|
+
export const TextStyle = {
|
|
34
|
+
Bold: "b",
|
|
35
|
+
Italic: "i",
|
|
36
|
+
Underline: "u",
|
|
37
|
+
StrikeThrough: "s",
|
|
38
|
+
Red: "c_db342e",
|
|
39
|
+
Orange: "c_f27806",
|
|
40
|
+
Yellow: "c_f7b503",
|
|
41
|
+
Green: "c_15a85f",
|
|
42
|
+
Small: "f_13",
|
|
43
|
+
Big: "f_18",
|
|
44
|
+
UnorderedList: "lst_1",
|
|
45
|
+
OrderedList: "lst_2",
|
|
46
|
+
Indent: "ind_$",
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
type TextStyleValue = (typeof TextStyle)[keyof typeof TextStyle];
|
|
50
|
+
|
|
51
|
+
export type Style =
|
|
52
|
+
| {
|
|
53
|
+
start: number;
|
|
54
|
+
len: number;
|
|
55
|
+
st: Exclude<TextStyleValue, typeof TextStyle.Indent>;
|
|
56
|
+
}
|
|
57
|
+
| {
|
|
58
|
+
start: number;
|
|
59
|
+
len: number;
|
|
60
|
+
st: typeof TextStyle.Indent;
|
|
61
|
+
indentSize?: number;
|
|
62
|
+
};
|
|
63
|
+
|
|
31
64
|
export type Credentials = {
|
|
32
65
|
imei: string;
|
|
33
66
|
cookie: unknown;
|