@soimy/dingtalk 3.6.10 → 3.7.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.
Files changed (163) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +11655 -10324
  3. package/dist/src/{ack-reaction-service.d.ts → ack-reaction/ack-reaction-service.d.ts} +1 -1
  4. package/dist/src/ack-reaction/dynamic-ack-reaction-controller.d.ts +3 -3
  5. package/dist/src/card/ask-user-question-context.d.ts +1 -1
  6. package/dist/src/card/ask-user-question-store.d.ts +1 -1
  7. package/dist/src/card/ask-user-question.d.ts +1 -1
  8. package/dist/src/card/card-action-handler.d.ts +2 -2
  9. package/dist/src/{card-draft-controller.d.ts → card/card-draft-controller.d.ts} +5 -1
  10. package/dist/src/card/card-run-registry.d.ts +2 -2
  11. package/dist/src/{card-service.d.ts → card/card-service.d.ts} +1 -1
  12. package/dist/src/card/card-stop-handler.d.ts +1 -1
  13. package/dist/src/card/card-streaming-mode.d.ts +1 -1
  14. package/dist/src/card/card-task-progress.d.ts +60 -0
  15. package/dist/src/channel.d.ts +6 -6
  16. package/dist/src/command/card-stop-command.d.ts +1 -1
  17. package/dist/src/{feedback-learning-service.d.ts → command/feedback-learning-service.d.ts} +1 -1
  18. package/dist/src/command/inbound-command-dispatch-service.d.ts +2 -2
  19. package/dist/src/{learning-command-service.d.ts → command/learning-command-service.d.ts} +1 -1
  20. package/dist/src/gateway/channel-gateway.d.ts +1 -1
  21. package/dist/src/{connection-manager.d.ts → gateway/connection-manager.d.ts} +1 -1
  22. package/dist/src/{docs-service.d.ts → gateway/docs-service.d.ts} +5 -5
  23. package/dist/src/{inbound-handler.d.ts → gateway/inbound-handler.d.ts} +1 -1
  24. package/dist/src/gateway/inbound-session-queue-dispatcher.d.ts +1 -1
  25. package/dist/src/messaging/attachment-text-extractor.d.ts +1 -1
  26. package/dist/src/messaging/btw-deliver.d.ts +1 -1
  27. package/dist/src/messaging/channel-outbound.d.ts +1 -1
  28. package/dist/src/messaging/inline-directives.d.ts +42 -0
  29. package/dist/src/{media-utils.d.ts → messaging/media-utils.d.ts} +1 -1
  30. package/dist/src/{message-context-store.d.ts → messaging/message-context-store.d.ts} +7 -1
  31. package/dist/src/{message-utils.d.ts → messaging/message-utils.d.ts} +1 -1
  32. package/dist/src/messaging/quoted-context.d.ts +2 -2
  33. package/dist/src/messaging/quoted-file-service.d.ts +1 -1
  34. package/dist/src/messaging/quoted-ref.d.ts +2 -2
  35. package/dist/src/{reply-strategy-card.d.ts → messaging/reply-strategy-card.d.ts} +1 -1
  36. package/dist/src/{reply-strategy-types.d.ts → messaging/reply-strategy-types.d.ts} +10 -1
  37. package/dist/src/{reply-strategy-with-reaction.d.ts → messaging/reply-strategy-with-reaction.d.ts} +1 -1
  38. package/dist/src/{reply-strategy.d.ts → messaging/reply-strategy.d.ts} +1 -1
  39. package/dist/src/{send-service.d.ts → messaging/send-service.d.ts} +1 -1
  40. package/dist/src/platform/channel-status.d.ts +1 -1
  41. package/dist/src/platform/config.d.ts +110 -0
  42. package/dist/src/platform/runtime-events.d.ts +67 -0
  43. package/dist/src/{types.d.ts → platform/types.d.ts} +75 -2
  44. package/dist/src/{persistence-store.d.ts → shared/persistence-store.d.ts} +1 -1
  45. package/dist/src/{utils.d.ts → shared/utils.d.ts} +1 -1
  46. package/dist/src/targeting/agent-name-matcher.d.ts +1 -1
  47. package/dist/src/targeting/agent-routing.d.ts +3 -3
  48. package/dist/src/{session-routing.d.ts → targeting/session-routing.d.ts} +1 -1
  49. package/index.ts +193 -69
  50. package/openclaw.plugin.json +158 -2
  51. package/package.json +16 -16
  52. package/src/{ack-reaction-classifier.ts → ack-reaction/ack-reaction-classifier.ts} +85 -13
  53. package/src/{ack-reaction-service.ts → ack-reaction/ack-reaction-service.ts} +13 -7
  54. package/src/ack-reaction/dynamic-ack-reaction-controller.ts +46 -31
  55. package/src/ack-reaction/dynamic-ack-reaction-progress.ts +4 -1
  56. package/src/card/ask-user-question-context.ts +1 -1
  57. package/src/card/ask-user-question-store.ts +2 -2
  58. package/src/card/ask-user-question.ts +11 -7
  59. package/src/card/card-action-handler.ts +2 -2
  60. package/src/{card-callback-service.ts → card/card-callback-service.ts} +7 -9
  61. package/src/card/card-draft-controller.ts +770 -0
  62. package/src/card/card-markdown-image-reroute.ts +8 -2
  63. package/src/card/card-run-registry.ts +17 -7
  64. package/src/{card-service.ts → card/card-service.ts} +91 -61
  65. package/src/card/card-stop-handler.ts +12 -8
  66. package/src/card/card-streaming-mode.ts +1 -1
  67. package/src/card/card-task-progress.ts +330 -0
  68. package/src/card/draft-stream-loop.ts +119 -0
  69. package/src/card/reasoning-answer-split.ts +125 -121
  70. package/src/card/reasoning-block-assembler.ts +122 -123
  71. package/src/{run-usage-store.ts → card/run-usage-store.ts} +18 -6
  72. package/src/card/statusline-renderer.ts +14 -5
  73. package/src/card/task-model-metadata.ts +8 -3
  74. package/src/channel.ts +14 -16
  75. package/src/command/card-stop-command.ts +2 -2
  76. package/src/{feedback-learning-service.ts → command/feedback-learning-service.ts} +36 -16
  77. package/src/{feedback-learning-store.ts → command/feedback-learning-store.ts} +133 -81
  78. package/src/command/inbound-command-dispatch-service.ts +12 -6
  79. package/src/{learning-command-service.ts → command/learning-command-service.ts} +36 -29
  80. package/src/{session-command-service.ts → command/session-command-service.ts} +3 -1
  81. package/src/gateway/channel-gateway.ts +106 -28
  82. package/src/{connection-manager.ts → gateway/connection-manager.ts} +45 -30
  83. package/src/{docs-service.ts → gateway/docs-service.ts} +5 -5
  84. package/src/{inbound-handler.ts → gateway/inbound-handler.ts} +144 -85
  85. package/src/gateway/inbound-session-queue-dispatcher.ts +18 -14
  86. package/src/{session-lock.ts → gateway/session-lock.ts} +14 -14
  87. package/src/messaging/attachment-text-extractor.ts +21 -7
  88. package/src/messaging/btw-deliver.ts +2 -2
  89. package/src/messaging/channel-actions.ts +15 -7
  90. package/src/messaging/channel-outbound.ts +16 -11
  91. package/src/messaging/inline-directives.ts +342 -0
  92. package/src/{media-utils.ts → messaging/media-utils.ts} +276 -72
  93. package/src/{message-context-store.ts → messaging/message-context-store.ts} +11 -2
  94. package/src/{message-utils.ts → messaging/message-utils.ts} +46 -11
  95. package/src/messaging/quoted-context.ts +2 -2
  96. package/src/messaging/quoted-file-service.ts +311 -294
  97. package/src/messaging/quoted-ref.ts +11 -7
  98. package/src/{reply-strategy-card.ts → messaging/reply-strategy-card.ts} +178 -101
  99. package/src/{reply-strategy-markdown.ts → messaging/reply-strategy-markdown.ts} +8 -2
  100. package/src/{reply-strategy-types.ts → messaging/reply-strategy-types.ts} +11 -1
  101. package/src/{reply-strategy-with-reaction.ts → messaging/reply-strategy-with-reaction.ts} +6 -1
  102. package/src/{reply-strategy.ts → messaging/reply-strategy.ts} +2 -2
  103. package/src/{send-service.ts → messaging/send-service.ts} +100 -65
  104. package/src/{auth.ts → platform/auth.ts} +2 -2
  105. package/src/platform/channel-status.ts +3 -3
  106. package/src/{config-schema.ts → platform/config-schema.ts} +81 -4
  107. package/src/{config.ts → platform/config.ts} +167 -19
  108. package/src/{device-registration.ts → platform/device-registration.ts} +5 -4
  109. package/src/{onboarding.ts → platform/onboarding.ts} +1 -4
  110. package/src/{plugin-sdk-channel-actions-augment.ts → platform/plugin-sdk-channel-actions-augment.ts} +6 -5
  111. package/src/platform/runtime-events.ts +202 -0
  112. package/src/{secret-input.ts → platform/secret-input.ts} +117 -11
  113. package/src/{types.ts → platform/types.ts} +77 -2
  114. package/src/{http-client.ts → shared/http-client.ts} +1 -3
  115. package/src/{persistence-store.ts → shared/persistence-store.ts} +6 -6
  116. package/src/{utils.ts → shared/utils.ts} +30 -13
  117. package/src/targeting/agent-name-matcher.ts +1 -1
  118. package/src/targeting/agent-routing.ts +7 -7
  119. package/src/targeting/group-members-store.ts +1 -1
  120. package/src/{session-peer-store.ts → targeting/session-peer-store.ts} +9 -4
  121. package/src/{session-routing.ts → targeting/session-routing.ts} +1 -1
  122. package/src/targeting/target-directory-adapter.ts +3 -3
  123. package/src/targeting/target-directory-store.ts +1 -1
  124. package/src/targeting/target-input.ts +1 -1
  125. package/dist/src/ack-reaction/dynamic-ack-reaction-events.d.ts +0 -35
  126. package/dist/src/config.d.ts +0 -59
  127. package/src/ack-reaction/dynamic-ack-reaction-events.ts +0 -123
  128. package/src/card-draft-controller.ts +0 -637
  129. package/src/draft-stream-loop.ts +0 -119
  130. /package/dist/src/{ack-reaction-classifier.d.ts → ack-reaction/ack-reaction-classifier.d.ts} +0 -0
  131. /package/dist/src/{card-callback-service.d.ts → card/card-callback-service.d.ts} +0 -0
  132. /package/dist/src/{draft-stream-loop.d.ts → card/draft-stream-loop.d.ts} +0 -0
  133. /package/dist/src/{run-usage-store.d.ts → card/run-usage-store.d.ts} +0 -0
  134. /package/dist/src/{feedback-learning-store.d.ts → command/feedback-learning-store.d.ts} +0 -0
  135. /package/dist/src/{session-command-service.d.ts → command/session-command-service.d.ts} +0 -0
  136. /package/dist/src/{session-lock.d.ts → gateway/session-lock.d.ts} +0 -0
  137. /package/dist/src/{proactive-risk-registry.d.ts → messaging/proactive-risk-registry.d.ts} +0 -0
  138. /package/dist/src/{reply-strategy-markdown.d.ts → messaging/reply-strategy-markdown.d.ts} +0 -0
  139. /package/dist/src/{access-control.d.ts → platform/access-control.d.ts} +0 -0
  140. /package/dist/src/{auth.d.ts → platform/auth.d.ts} +0 -0
  141. /package/dist/src/{config-schema.d.ts → platform/config-schema.d.ts} +0 -0
  142. /package/dist/src/{device-registration.d.ts → platform/device-registration.d.ts} +0 -0
  143. /package/dist/src/{logger-context.d.ts → platform/logger-context.d.ts} +0 -0
  144. /package/dist/src/{onboarding.d.ts → platform/onboarding.d.ts} +0 -0
  145. /package/dist/src/{plugin-sdk-channel-actions-augment.d.ts → platform/plugin-sdk-channel-actions-augment.d.ts} +0 -0
  146. /package/dist/src/{runtime.d.ts → platform/runtime.d.ts} +0 -0
  147. /package/dist/src/{secret-input.d.ts → platform/secret-input.d.ts} +0 -0
  148. /package/dist/src/{session-state.d.ts → platform/session-state.d.ts} +0 -0
  149. /package/dist/src/{signature.d.ts → platform/signature.d.ts} +0 -0
  150. /package/dist/src/{dedup.d.ts → shared/dedup.d.ts} +0 -0
  151. /package/dist/src/{http-client.d.ts → shared/http-client.d.ts} +0 -0
  152. /package/dist/src/{path-utils.d.ts → shared/path-utils.d.ts} +0 -0
  153. /package/dist/src/{peer-id-registry.d.ts → targeting/peer-id-registry.d.ts} +0 -0
  154. /package/dist/src/{session-peer-store.d.ts → targeting/session-peer-store.d.ts} +0 -0
  155. /package/src/{proactive-risk-registry.ts → messaging/proactive-risk-registry.ts} +0 -0
  156. /package/src/{access-control.ts → platform/access-control.ts} +0 -0
  157. /package/src/{logger-context.ts → platform/logger-context.ts} +0 -0
  158. /package/src/{runtime.ts → platform/runtime.ts} +0 -0
  159. /package/src/{session-state.ts → platform/session-state.ts} +0 -0
  160. /package/src/{signature.ts → platform/signature.ts} +0 -0
  161. /package/src/{dedup.ts → shared/dedup.ts} +0 -0
  162. /package/src/{path-utils.ts → shared/path-utils.ts} +0 -0
  163. /package/src/{peer-id-registry.ts → targeting/peer-id-registry.ts} +0 -0
@@ -0,0 +1,342 @@
1
+ /**
2
+ * Inline directive parsing (local implementation).
3
+ *
4
+ * OpenClaw retired the `openclaw/plugin-sdk/text-runtime` subpath in
5
+ * 2026.8.x (docs/plugins/sdk-migration.md: "The August 15 compatibility
6
+ * subpaths ... `text-runtime` ... were retired early"). Its replacement
7
+ * `openclaw/plugin-sdk/text-chunking` only re-exports the directive-tag
8
+ * *stripping* helpers and the `InlineDirectiveParseResult` type - not
9
+ * `parseInlineDirectives` itself.
10
+ *
11
+ * This module reproduces the previous host behaviour inside the plugin so no
12
+ * removed SDK internals are imported. Code-region awareness (fenced,
13
+ * indented, and inline code) is sourced from the public SDK helpers
14
+ * `findCodeRegions` / `isInsideCode` (available since 2026.7.1-2), plus a
15
+ * lenient indented-block fallback matching the host whitespace normalizer so
16
+ * directives inside code are never treated as real voice/reply commands.
17
+ *
18
+ * The shipped 2026.7.1-2 SDK `findCodeRegions` is a simplified heuristic: it
19
+ * pairs backtick runs of different lengths, lets shorter fences close longer
20
+ * ones, treats escaped backticks as real delimiters, and pairs backticks
21
+ * inside indented code with prose. `findDelimiterCodeRegions` therefore layers
22
+ * a CommonMark-style fence/inline-code supplement over the SDK regions (union,
23
+ * conservative: in doubt the text stays literal) so protection holds on every
24
+ * supported host. Ponytail: remove the supplement once the minimum host is
25
+ * 2026.8+ and its SDK regions are verified to cover these cases.
26
+ */
27
+
28
+ import { findCodeRegions, isInsideCode, type CodeRegion } from "openclaw/plugin-sdk/text-chunking";
29
+
30
+ const AUDIO_TAG_RE = /\[\[\s*audio_as_voice\s*\]\]/gi;
31
+ const REPLY_TAG_RE = /\[\[\s*(?:reply_to_current|reply_to\s*:\s*([^\]\n]+))\s*\]\]/gi;
32
+ const MAX_REPLY_DIRECTIVE_ID_LENGTH = 256;
33
+ const BLOCK_SENTINEL_SEED = "\uE000";
34
+ /** Lenient indented-code-block lines (4 spaces or tab), same rule as the host whitespace normalizer. */
35
+ const INDENTED_CODE_LINE_RE = /(?:^|\n)((?: {4}|\t)[^\n]*)(?:\n(?: {4}|\t)[^\n]*)*/g;
36
+
37
+ export type ParseInlineDirectivesOptions = {
38
+ currentMessageId?: string;
39
+ stripAudioTag?: boolean;
40
+ stripReplyTags?: boolean;
41
+ };
42
+
43
+ export type InlineDirectiveParseResult = {
44
+ text: string;
45
+ audioAsVoice: boolean;
46
+ replyToId?: string;
47
+ replyToExplicitId?: string;
48
+ replyToCurrent: boolean;
49
+ hasAudioTag: boolean;
50
+ hasReplyTag: boolean;
51
+ };
52
+
53
+ function normalizeOptionalString(value: unknown): string | undefined {
54
+ if (typeof value !== "string") {
55
+ return undefined;
56
+ }
57
+ const trimmed = value.trim();
58
+ return trimmed ? trimmed : undefined;
59
+ }
60
+
61
+ /**
62
+ * CommonMark code regions (fenced / indented / inline) from the SDK, extended
63
+ * with a lenient indented-block fallback for lines indented by 4 spaces or a
64
+ * tab that follow a paragraph without a blank separator (the host normalizer
65
+ * protects those too).
66
+ */
67
+ function resolveCodeRegions(text: string): CodeRegion[] {
68
+ const indentedRegions: CodeRegion[] = [];
69
+ const indentRe = new RegExp(INDENTED_CODE_LINE_RE.source, "g");
70
+ let match: RegExpExecArray | null;
71
+ while ((match = indentRe.exec(text)) !== null) {
72
+ const start = match.index + (match[0].charCodeAt(0) === 10 ? 1 : 0);
73
+ indentedRegions.push({ start, end: match.index + match[0].length });
74
+ }
75
+ const sdkRegions = findCodeRegions(text).filter(
76
+ (region) =>
77
+ (text[region.start] !== "`" || !isEscaped(text, region.start)) &&
78
+ !indentedRegions.some(
79
+ (indented) => region.start >= indented.start && region.start < indented.end,
80
+ ),
81
+ );
82
+ const knownRegions = mergeCodeRegions([...sdkRegions, ...indentedRegions]);
83
+ return mergeCodeRegions([...knownRegions, ...findDelimiterCodeRegions(text, knownRegions)]);
84
+ }
85
+
86
+ function isEscaped(text: string, offset: number): boolean {
87
+ let slashes = 0;
88
+ for (let index = offset - 1; text[index] === "\\"; index--) {
89
+ slashes++;
90
+ }
91
+ return slashes % 2 === 1;
92
+ }
93
+
94
+ function isStrictlyInsideCode(offset: number, regions: CodeRegion[]): boolean {
95
+ return regions.some((region) => offset > region.start && offset < region.end);
96
+ }
97
+
98
+ // ponytail: small 2026.7.x delimiter supplement; delete when the minimum host is 2026.8+.
99
+ function findDelimiterCodeRegions(text: string, knownRegions: CodeRegion[]): CodeRegion[] {
100
+ const regions: CodeRegion[] = [];
101
+ let match: RegExpExecArray | null;
102
+ const fences = /^( {0,3})(`{3,}|~{3,})[^\n]*(?:\n|$)/gm;
103
+ let open: { char: string; length: number; start: number } | undefined;
104
+ while ((match = fences.exec(text)) !== null) {
105
+ const delimiter = match[2];
106
+ if (!open) {
107
+ const suffix = match[0].slice(match[1].length + delimiter.length).trim();
108
+ if (
109
+ isStrictlyInsideCode(match.index, knownRegions) ||
110
+ (delimiter[0] === "`" && suffix.includes("`"))
111
+ ) {
112
+ continue;
113
+ }
114
+ open = { char: delimiter[0], length: delimiter.length, start: match.index };
115
+ } else if (
116
+ delimiter[0] === open.char &&
117
+ delimiter.length >= open.length &&
118
+ match[0].slice(match[1].length + delimiter.length).trim() === ""
119
+ ) {
120
+ regions.push({ start: open.start, end: fences.lastIndex });
121
+ open = undefined;
122
+ }
123
+ }
124
+ if (open) {
125
+ regions.push({ start: open.start, end: text.length });
126
+ }
127
+
128
+ const fencedRegions = [...regions];
129
+ const pendingTicks = new Map<number, number>();
130
+ const ticks = /`+/g;
131
+ while ((match = ticks.exec(text)) !== null) {
132
+ const length = match[0].length;
133
+ if (
134
+ isEscaped(text, match.index) ||
135
+ isInsideCode(match.index, fencedRegions) ||
136
+ (pendingTicks.get(length) === undefined && isStrictlyInsideCode(match.index, knownRegions))
137
+ ) {
138
+ continue;
139
+ }
140
+ const start = pendingTicks.get(length);
141
+ if (start === undefined) {
142
+ pendingTicks.set(length, match.index);
143
+ continue;
144
+ }
145
+ regions.push({ start, end: ticks.lastIndex });
146
+ for (const [pendingLength, pendingStart] of pendingTicks) {
147
+ if (pendingStart >= start) {
148
+ pendingTicks.delete(pendingLength);
149
+ }
150
+ }
151
+ }
152
+ return regions;
153
+ }
154
+
155
+ function mergeCodeRegions(regions: CodeRegion[]): CodeRegion[] {
156
+ if (regions.length <= 1) {
157
+ return regions;
158
+ }
159
+ const sorted = [...regions].toSorted((a, b) => a.start - b.start || a.end - b.end);
160
+ const merged: CodeRegion[] = [];
161
+ for (const region of sorted) {
162
+ const last = merged[merged.length - 1];
163
+ if (last && region.start <= last.end) {
164
+ last.end = Math.max(last.end, region.end);
165
+ } else {
166
+ merged.push({ start: region.start, end: region.end });
167
+ }
168
+ }
169
+ return merged;
170
+ }
171
+
172
+ function createBlockSentinel(text: string): string {
173
+ let sentinel = BLOCK_SENTINEL_SEED;
174
+ while (text.includes(sentinel)) {
175
+ sentinel += BLOCK_SENTINEL_SEED;
176
+ }
177
+ return sentinel;
178
+ }
179
+
180
+ function normalizeDirectiveWhitespace(text: string, codeRegions: CodeRegion[] = []): string {
181
+ const blockSentinel = createBlockSentinel(text);
182
+ const blockPlaceholderRe = new RegExp(`${blockSentinel}(\\d+)${blockSentinel}`, "g");
183
+ const blocks: string[] = [];
184
+
185
+ let masked = "";
186
+ let cursor = 0;
187
+ for (const region of codeRegions) {
188
+ if (region.start < cursor) {
189
+ continue;
190
+ }
191
+ blocks.push(text.slice(region.start, region.end));
192
+ masked += `${text.slice(cursor, region.start)}${blockSentinel}${blocks.length - 1}${blockSentinel}`;
193
+ cursor = region.end;
194
+ }
195
+ masked += text.slice(cursor);
196
+
197
+ return masked
198
+ .replace(/\r\n/g, "\n")
199
+ .replace(/([^\s])[ \t]{2,}([^\s])/g, "$1 $2")
200
+ .replace(/^\n+/, "")
201
+ .replace(/^[ \t](?=\S)/, "")
202
+ .replace(/[ \t]+\n/g, "\n")
203
+ .replace(/\n{3,}/g, "\n\n")
204
+ .trimEnd()
205
+ .replace(blockPlaceholderRe, (_full, index: string) => blocks[Number(index)] ?? "");
206
+ }
207
+
208
+ function replacementPreservesWordBoundary(source: string, offset: number, length: number): string {
209
+ const before = source[offset - 1];
210
+ const after = source[offset + length];
211
+ return before && after && !/\s/u.test(before) && !/\s/u.test(after) ? " " : "";
212
+ }
213
+
214
+ function stripUnsafeReplyDirectiveChars(value: string): string {
215
+ const chars: string[] = [];
216
+ for (const ch of value) {
217
+ const code = ch.charCodeAt(0);
218
+ if (
219
+ (code >= 0 && code <= 31) ||
220
+ code === 127 ||
221
+ (code >= 128 && code <= 159) ||
222
+ ch === "[" ||
223
+ ch === "]"
224
+ ) {
225
+ continue;
226
+ }
227
+ chars.push(ch);
228
+ }
229
+ return chars.join("");
230
+ }
231
+
232
+ export function sanitizeReplyDirectiveId(rawReplyToId: string | undefined): string | undefined {
233
+ const trimmed = rawReplyToId?.trim();
234
+ if (!trimmed) {
235
+ return undefined;
236
+ }
237
+ const sanitized = stripUnsafeReplyDirectiveChars(trimmed).trim();
238
+ if (!sanitized) {
239
+ return undefined;
240
+ }
241
+ const chars = Array.from(sanitized);
242
+ if (chars.length > MAX_REPLY_DIRECTIVE_ID_LENGTH) {
243
+ return chars.slice(0, MAX_REPLY_DIRECTIVE_ID_LENGTH).join("");
244
+ }
245
+ return sanitized;
246
+ }
247
+
248
+ export function parseInlineDirectives(
249
+ text: string | undefined,
250
+ options: ParseInlineDirectivesOptions = {},
251
+ ): InlineDirectiveParseResult {
252
+ const { currentMessageId, stripAudioTag = true, stripReplyTags = true } = options;
253
+
254
+ if (!text) {
255
+ return {
256
+ text: "",
257
+ audioAsVoice: false,
258
+ replyToCurrent: false,
259
+ hasAudioTag: false,
260
+ hasReplyTag: false,
261
+ };
262
+ }
263
+
264
+ if (!text.includes("[[")) {
265
+ return {
266
+ text: normalizeDirectiveWhitespace(text, resolveCodeRegions(text)),
267
+ audioAsVoice: false,
268
+ replyToCurrent: false,
269
+ hasAudioTag: false,
270
+ hasReplyTag: false,
271
+ };
272
+ }
273
+
274
+ // Directives inside code regions are literal text: never voice/reply semantics.
275
+ // Stripping a tag shortens the string, so every pass re-derives regions from
276
+ // the text it actually runs against; offsets from an earlier pass are stale
277
+ // once a preceding tag has been removed.
278
+ const codeRegions = resolveCodeRegions(text);
279
+ let cleaned = text;
280
+ let audioAsVoice = false;
281
+ let hasAudioTag = false;
282
+ let hasReplyTag = false;
283
+ let sawCurrent = false;
284
+ let lastExplicitId: string | undefined;
285
+
286
+ cleaned = cleaned.replace(AUDIO_TAG_RE, (match: string, offset: number, source: string) => {
287
+ if (isInsideCode(offset, codeRegions)) {
288
+ return match;
289
+ }
290
+ audioAsVoice = true;
291
+ hasAudioTag = true;
292
+ return stripAudioTag ? replacementPreservesWordBoundary(source, offset, match.length) : match;
293
+ });
294
+ const codeRegionsAfterAudioStrip = resolveCodeRegions(cleaned);
295
+
296
+ cleaned = cleaned.replace(
297
+ REPLY_TAG_RE,
298
+ (match: string, idRaw: string | undefined, offset: number, source: string) => {
299
+ if (isInsideCode(offset, codeRegionsAfterAudioStrip)) {
300
+ return match;
301
+ }
302
+ hasReplyTag = true;
303
+ if (idRaw === undefined) {
304
+ sawCurrent = true;
305
+ } else {
306
+ const id = sanitizeReplyDirectiveId(idRaw);
307
+ if (id) {
308
+ lastExplicitId = id;
309
+ }
310
+ }
311
+ return stripReplyTags
312
+ ? replacementPreservesWordBoundary(source, offset, match.length)
313
+ : match;
314
+ },
315
+ );
316
+
317
+ // Early return when `[[...]]` matched no known directive: keep the original
318
+ // text untouched instead of running whitespace normalization.
319
+ if (!hasAudioTag && !hasReplyTag) {
320
+ return {
321
+ text,
322
+ audioAsVoice: false,
323
+ replyToCurrent: false,
324
+ hasAudioTag: false,
325
+ hasReplyTag: false,
326
+ };
327
+ }
328
+
329
+ cleaned = normalizeDirectiveWhitespace(cleaned, resolveCodeRegions(cleaned));
330
+ const replyToId =
331
+ lastExplicitId ?? (sawCurrent ? normalizeOptionalString(currentMessageId) : undefined);
332
+
333
+ return {
334
+ text: cleaned,
335
+ audioAsVoice,
336
+ replyToId,
337
+ replyToExplicitId: lastExplicitId,
338
+ replyToCurrent: sawCurrent,
339
+ hasAudioTag,
340
+ hasReplyTag,
341
+ };
342
+ }