@xopcai/xopc 0.0.14 → 0.0.16

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 (161) hide show
  1. package/dist/extensions/feishu/src/adapters/onboard-cli.d.ts +7 -0
  2. package/dist/extensions/feishu/src/adapters/onboard-cli.js +432 -0
  3. package/dist/extensions/feishu/src/adapters/onboard-cli.js.map +1 -0
  4. package/dist/extensions/feishu/src/auth/pairing.d.ts +7 -0
  5. package/dist/extensions/feishu/src/auth/pairing.js +45 -0
  6. package/dist/extensions/feishu/src/auth/pairing.js.map +1 -0
  7. package/dist/extensions/feishu/src/auth/paths.d.ts +2 -0
  8. package/dist/extensions/feishu/src/auth/paths.js +18 -0
  9. package/dist/extensions/feishu/src/auth/paths.js.map +1 -0
  10. package/dist/extensions/feishu/src/directory/directory-adapter.d.ts +2 -0
  11. package/dist/extensions/feishu/src/directory/directory-adapter.js +27 -0
  12. package/dist/extensions/feishu/src/directory/directory-adapter.js.map +1 -0
  13. package/dist/extensions/feishu/src/format.d.ts +21 -0
  14. package/dist/extensions/feishu/src/format.js +99 -0
  15. package/dist/extensions/feishu/src/format.js.map +1 -0
  16. package/dist/extensions/feishu/src/index.d.ts +5 -0
  17. package/dist/extensions/feishu/src/index.js +3 -0
  18. package/dist/extensions/feishu/src/outbound/actions.d.ts +51 -0
  19. package/dist/extensions/feishu/src/outbound/actions.js +62 -0
  20. package/dist/extensions/feishu/src/outbound/actions.js.map +1 -0
  21. package/dist/extensions/feishu/src/outbound/media-load.d.ts +12 -0
  22. package/dist/extensions/feishu/src/outbound/media-load.js +125 -0
  23. package/dist/extensions/feishu/src/outbound/media-load.js.map +1 -0
  24. package/dist/extensions/feishu/src/outbound/outbound-adapter.d.ts +2 -0
  25. package/dist/extensions/feishu/src/outbound/outbound-adapter.js +201 -0
  26. package/dist/extensions/feishu/src/outbound/outbound-adapter.js.map +1 -0
  27. package/dist/extensions/feishu/src/plugin.d.ts +70 -0
  28. package/dist/extensions/feishu/src/plugin.js +313 -0
  29. package/dist/extensions/feishu/src/plugin.js.map +1 -0
  30. package/dist/extensions/feishu/src/schema/config-schema.d.ts +215 -0
  31. package/dist/extensions/feishu/src/schema/config-schema.js +198 -0
  32. package/dist/extensions/feishu/src/schema/config-schema.js.map +1 -0
  33. package/dist/extensions/feishu/src/state/accounts.d.ts +38 -0
  34. package/dist/extensions/feishu/src/state/accounts.js +96 -0
  35. package/dist/extensions/feishu/src/state/accounts.js.map +1 -0
  36. package/dist/extensions/feishu/src/state/message-bindings.d.ts +11 -0
  37. package/dist/extensions/feishu/src/state/message-bindings.js +41 -0
  38. package/dist/extensions/feishu/src/state/message-bindings.js.map +1 -0
  39. package/dist/extensions/feishu/src/state/thread-bindings.js +46 -0
  40. package/dist/extensions/feishu/src/state/thread-bindings.js.map +1 -0
  41. package/dist/extensions/feishu/src/status/doctor.d.ts +2 -0
  42. package/dist/extensions/feishu/src/status/doctor.js +38 -0
  43. package/dist/extensions/feishu/src/status/doctor.js.map +1 -0
  44. package/dist/extensions/feishu/src/status/status-adapter.d.ts +3 -0
  45. package/dist/extensions/feishu/src/status/status-adapter.js +45 -0
  46. package/dist/extensions/feishu/src/status/status-adapter.js.map +1 -0
  47. package/dist/extensions/feishu/src/streaming/streaming-adapter.d.ts +3 -0
  48. package/dist/extensions/feishu/src/streaming/streaming-adapter.js +242 -0
  49. package/dist/extensions/feishu/src/streaming/streaming-adapter.js.map +1 -0
  50. package/dist/extensions/feishu/src/subagent-hooks.js +52 -0
  51. package/dist/extensions/feishu/src/subagent-hooks.js.map +1 -0
  52. package/dist/extensions/feishu/src/tools/docx/docx-batch-insert.js +95 -0
  53. package/dist/extensions/feishu/src/tools/docx/docx-batch-insert.js.map +1 -0
  54. package/dist/extensions/feishu/src/tools/docx/docx-color-text.js +75 -0
  55. package/dist/extensions/feishu/src/tools/docx/docx-color-text.js.map +1 -0
  56. package/dist/extensions/feishu/src/tools/docx/docx-table-ops.js +173 -0
  57. package/dist/extensions/feishu/src/tools/docx/docx-table-ops.js.map +1 -0
  58. package/dist/extensions/feishu/src/tools/docx/docx-types.js +1 -0
  59. package/dist/extensions/feishu/src/tools/tools.d.ts +5 -0
  60. package/dist/extensions/feishu/src/tools/tools.js +46 -0
  61. package/dist/extensions/feishu/src/tools/tools.js.map +1 -0
  62. package/dist/extensions/feishu/src/transport/client/client.d.ts +6 -0
  63. package/dist/extensions/feishu/src/transport/client/client.js +41 -0
  64. package/dist/extensions/feishu/src/transport/client/client.js.map +1 -0
  65. package/dist/extensions/feishu/src/transport/client/lark-sdk-logger.d.ts +13 -0
  66. package/dist/extensions/feishu/src/transport/client/lark-sdk-logger.js +104 -0
  67. package/dist/extensions/feishu/src/transport/client/lark-sdk-logger.js.map +1 -0
  68. package/dist/extensions/feishu/src/transport/reliability/dedupe.d.ts +7 -0
  69. package/dist/extensions/feishu/src/transport/reliability/dedupe.js +30 -0
  70. package/dist/extensions/feishu/src/transport/reliability/dedupe.js.map +1 -0
  71. package/dist/extensions/feishu/src/transport/socket-mode/monitor.d.ts +19 -0
  72. package/dist/extensions/feishu/src/transport/socket-mode/monitor.js +326 -0
  73. package/dist/extensions/feishu/src/transport/socket-mode/monitor.js.map +1 -0
  74. package/dist/extensions/feishu/src/transport/socket-mode/retry.d.ts +1 -0
  75. package/dist/extensions/feishu/src/transport/socket-mode/retry.js +10 -0
  76. package/dist/extensions/feishu/src/transport/socket-mode/retry.js.map +1 -0
  77. package/dist/extensions/feishu/src/transport/text/mentions.d.ts +1 -0
  78. package/dist/extensions/feishu/src/transport/text/mentions.js +9 -0
  79. package/dist/extensions/feishu/src/transport/text/mentions.js.map +1 -0
  80. package/dist/extensions/feishu/src/transport/webhook/monitor.d.ts +19 -0
  81. package/dist/extensions/feishu/src/transport/webhook/monitor.js +271 -0
  82. package/dist/extensions/feishu/src/transport/webhook/monitor.js.map +1 -0
  83. package/dist/extensions/feishu/src/ui/config-surface.d.ts +2 -0
  84. package/dist/extensions/feishu/src/ui/config-surface.js +6 -0
  85. package/dist/extensions/feishu/src/ui/config-surface.js.map +1 -0
  86. package/dist/extensions/feishu/xopc.extension.json +18 -0
  87. package/dist/extensions/telegram/xopc.extension.json +20 -0
  88. package/dist/extensions/weixin/xopc.extension.json +17 -0
  89. package/dist/gateway/static/root/assets/{agents-C2blSFQk.js → agents-Dy5cGVVQ.js} +2 -2
  90. package/dist/gateway/static/root/assets/{agents-C2blSFQk.js.map → agents-Dy5cGVVQ.js.map} +1 -1
  91. package/dist/gateway/static/root/assets/{apps-page-Dg7InQ41.js → apps-page-BOpDR0Lz.js} +2 -2
  92. package/dist/gateway/static/root/assets/{apps-page-Dg7InQ41.js.map → apps-page-BOpDR0Lz.js.map} +1 -1
  93. package/dist/gateway/static/root/assets/channels-settings-CrCesccB.js +9 -0
  94. package/dist/gateway/static/root/assets/channels-settings-CrCesccB.js.map +1 -0
  95. package/dist/gateway/static/root/assets/{cron-page-B-7O4_QQ.js → cron-page-B_XY0gPt.js} +2 -2
  96. package/dist/gateway/static/root/assets/{cron-page-B-7O4_QQ.js.map → cron-page-B_XY0gPt.js.map} +1 -1
  97. package/dist/gateway/static/root/assets/{cron-utils-DvRPM814.js → cron-utils-BYdnLwhl.js} +2 -2
  98. package/dist/gateway/static/root/assets/{cron-utils-DvRPM814.js.map → cron-utils-BYdnLwhl.js.map} +1 -1
  99. package/dist/gateway/static/root/assets/{dist-DYzyRkRh.js → dist-DvaA5uNp.js} +2 -2
  100. package/dist/gateway/static/root/assets/{dist-DYzyRkRh.js.map → dist-DvaA5uNp.js.map} +1 -1
  101. package/dist/gateway/static/root/assets/{extension-debug-page-DV-NwlaY.js → extension-debug-page-CPSk7gFW.js} +2 -2
  102. package/dist/gateway/static/root/assets/{extension-debug-page-DV-NwlaY.js.map → extension-debug-page-CPSk7gFW.js.map} +1 -1
  103. package/dist/gateway/static/root/assets/{extension-page-UUKLJwpo.js → extension-page-COdbk9I6.js} +2 -2
  104. package/dist/gateway/static/root/assets/{extension-page-UUKLJwpo.js.map → extension-page-COdbk9I6.js.map} +1 -1
  105. package/dist/gateway/static/root/assets/{extension-settings-page-R4VlbZOj.js → extension-settings-page-BlEz2Ily.js} +2 -2
  106. package/dist/gateway/static/root/assets/{extension-settings-page-R4VlbZOj.js.map → extension-settings-page-BlEz2Ily.js.map} +1 -1
  107. package/dist/gateway/static/root/assets/index-BQNdJlkw.css +1 -0
  108. package/dist/gateway/static/root/assets/index-tm9ZY35l.js +144 -0
  109. package/dist/gateway/static/root/assets/{index-BCVqNi5T.js.map → index-tm9ZY35l.js.map} +1 -1
  110. package/dist/gateway/static/root/assets/{logs-page-BgUDjMge.js → logs-page-LSa0jmLO.js} +2 -2
  111. package/dist/gateway/static/root/assets/{logs-page-BgUDjMge.js.map → logs-page-LSa0jmLO.js.map} +1 -1
  112. package/dist/gateway/static/root/assets/sessions-page-cn2fi_V3.js +2 -0
  113. package/dist/gateway/static/root/assets/sessions-page-cn2fi_V3.js.map +1 -0
  114. package/dist/gateway/static/root/assets/{settings-page-Doa_lzdW.js → settings-page-CyHd5szQ.js} +2 -2
  115. package/dist/gateway/static/root/assets/{settings-page-Doa_lzdW.js.map → settings-page-CyHd5szQ.js.map} +1 -1
  116. package/dist/gateway/static/root/assets/{skills-page-BdNqg2NG.js → skills-page-irjxwW9u.js} +2 -2
  117. package/dist/gateway/static/root/assets/{skills-page-BdNqg2NG.js.map → skills-page-irjxwW9u.js.map} +1 -1
  118. package/dist/gateway/static/root/channel-icons/feishu.svg +12 -0
  119. package/dist/gateway/static/root/channel-icons/lark.svg +12 -0
  120. package/dist/gateway/static/root/channel-icons/telegram.svg +1 -0
  121. package/dist/gateway/static/root/channel-icons/wechat.svg +1 -0
  122. package/dist/gateway/static/root/channel-icons/weixin.svg +1 -0
  123. package/dist/gateway/static/root/index.html +2 -2
  124. package/dist/package.js +1 -1
  125. package/dist/src/agent/agent-manager.d.ts +1 -0
  126. package/dist/src/agent/agent-manager.js +1 -0
  127. package/dist/src/agent/agent-manager.js.map +1 -1
  128. package/dist/src/agent/service.js +3 -0
  129. package/dist/src/agent/service.js.map +1 -1
  130. package/dist/src/agent/tools/delegate-tool.d.ts +8 -0
  131. package/dist/src/agent/tools/delegate-tool.js +60 -2
  132. package/dist/src/agent/tools/delegate-tool.js.map +1 -1
  133. package/dist/src/agent/tools/factory.d.ts +1 -0
  134. package/dist/src/agent/tools/factory.js +2 -0
  135. package/dist/src/agent/tools/factory.js.map +1 -1
  136. package/dist/src/channels/envelope-timestamp.d.ts +5 -0
  137. package/dist/src/channels/envelope-timestamp.js +10 -1
  138. package/dist/src/channels/envelope-timestamp.js.map +1 -1
  139. package/dist/src/channels/feishu/index.d.ts +5 -0
  140. package/dist/src/channels/feishu/index.js +4 -0
  141. package/dist/src/chat-commands/types.d.ts +1 -1
  142. package/dist/src/extensions/types/hooks.d.ts +46 -1
  143. package/dist/src/extensions/types/hooks.js +3 -0
  144. package/dist/src/extensions/types/hooks.js.map +1 -1
  145. package/dist/src/gateway/service.js +1 -1
  146. package/dist/src/generated/bundled-channel-plugins.d.ts +2 -1
  147. package/dist/src/generated/bundled-channel-plugins.js +8 -2
  148. package/dist/src/generated/bundled-channel-plugins.js.map +1 -1
  149. package/dist/src/providers/env-keys.js +1 -0
  150. package/dist/src/providers/env-keys.js.map +1 -1
  151. package/dist/src/providers/index.js +5 -0
  152. package/dist/src/providers/index.js.map +1 -1
  153. package/dist/src/session/session-title.js +2 -1
  154. package/dist/src/session/session-title.js.map +1 -1
  155. package/package.json +2 -2
  156. package/dist/gateway/static/root/assets/channels-settings-CHOL7G_P.js +0 -9
  157. package/dist/gateway/static/root/assets/channels-settings-CHOL7G_P.js.map +0 -1
  158. package/dist/gateway/static/root/assets/index-BCVqNi5T.js +0 -144
  159. package/dist/gateway/static/root/assets/index-XbYityMf.css +0 -1
  160. package/dist/gateway/static/root/assets/sessions-page-L2VUocA4.js +0 -2
  161. package/dist/gateway/static/root/assets/sessions-page-L2VUocA4.js.map +0 -1
@@ -0,0 +1,242 @@
1
+ import { createLogger } from "../../../../src/utils/logger/index.js";
2
+ import { init_logger } from "../../../../src/utils/logger.js";
3
+ import { resolveFeishuAccount } from "../state/accounts.js";
4
+ import { createFeishuClient } from "../transport/client/client.js";
5
+ import { getFeishuBindingByMessageId, recordFeishuMessageBinding } from "../state/message-bindings.js";
6
+ import { formatFeishuOutboundText } from "../format.js";
7
+ //#region extensions/feishu/src/streaming/streaming-adapter.ts
8
+ init_logger();
9
+ const log = createLogger("FeishuStreaming");
10
+ function createFeishuStreamingAdapter(getConfig) {
11
+ return { startStream(options) {
12
+ const account = resolveFeishuAccount(getConfig(), options.accountId ?? "default");
13
+ if (!account.configured) return null;
14
+ if (account.streaming !== true) return null;
15
+ const { api } = createFeishuClient(account);
16
+ let messageId;
17
+ let lastText = "";
18
+ let timer = null;
19
+ let aborted = false;
20
+ let editedAtLeastOnce = false;
21
+ let ready = null;
22
+ let cardId;
23
+ let cardSeq = 0;
24
+ const cardElementId = "md_1";
25
+ let fallbackSent = false;
26
+ const renderMode = account.renderMode ?? "auto";
27
+ const preferCard = renderMode === "card" || renderMode === "auto";
28
+ const formatStreamText = (text) => {
29
+ if (!text.trim()) return text;
30
+ if (text.trim() === "Thinking…") return text;
31
+ return formatFeishuOutboundText({
32
+ text,
33
+ renderMode,
34
+ forCardMarkdown: Boolean(preferCard && cardId)
35
+ });
36
+ };
37
+ const recordBindingIfReply = (childMessageId) => {
38
+ if (!childMessageId) return;
39
+ const parentId = options.replyToMessageId;
40
+ if (!parentId) return;
41
+ const parent = getFeishuBindingByMessageId(parentId);
42
+ if (!parent) return;
43
+ recordFeishuMessageBinding({
44
+ ...parent,
45
+ messageId: childMessageId
46
+ });
47
+ };
48
+ const edit = async (text) => {
49
+ if (ready) await ready;
50
+ if (!messageId) return;
51
+ const outbound = formatStreamText(text);
52
+ if (cardId && preferCard) {
53
+ cardSeq += 1;
54
+ await api.cardkit.v1.cardElement.content({
55
+ path: {
56
+ card_id: cardId,
57
+ element_id: cardElementId
58
+ },
59
+ data: {
60
+ content: outbound,
61
+ sequence: cardSeq,
62
+ uuid: `${cardId}:${cardSeq}`
63
+ }
64
+ });
65
+ } else await api.im.v1.message.update({
66
+ path: { message_id: messageId },
67
+ data: {
68
+ msg_type: "text",
69
+ content: JSON.stringify({ text: outbound })
70
+ }
71
+ });
72
+ editedAtLeastOnce = true;
73
+ };
74
+ const sendFallbackText = async (text) => {
75
+ if (fallbackSent) return;
76
+ fallbackSent = true;
77
+ const receive_id_type = options.chatId.startsWith("ou_") || options.chatId.startsWith("on_") ? "open_id" : "chat_id";
78
+ try {
79
+ const outbound = formatStreamText(text);
80
+ const res = options.replyToMessageId ? await api.im.message.reply({
81
+ path: { message_id: options.replyToMessageId },
82
+ data: {
83
+ msg_type: "text",
84
+ content: JSON.stringify({ text: outbound }),
85
+ ...options.threadId ? { reply_in_thread: true } : {}
86
+ }
87
+ }) : await api.im.message.create({
88
+ params: { receive_id_type },
89
+ data: {
90
+ receive_id: options.chatId,
91
+ msg_type: "text",
92
+ content: JSON.stringify({ text: outbound })
93
+ }
94
+ });
95
+ const mid = res?.data?.message_id ?? res?.message_id ?? void 0;
96
+ if (mid) recordBindingIfReply(mid);
97
+ editedAtLeastOnce = true;
98
+ messageId = mid ?? messageId;
99
+ cardId = void 0;
100
+ } catch (err) {
101
+ log.warn({
102
+ err,
103
+ accountId: account.accountId
104
+ }, "Feishu streaming fallback send failed");
105
+ }
106
+ };
107
+ const flush = async () => {
108
+ if (aborted) return;
109
+ if (ready) await ready;
110
+ if (!messageId) return;
111
+ const text = lastText;
112
+ if (!text.trim()) return;
113
+ try {
114
+ await edit(text);
115
+ } catch (err) {
116
+ log.warn({
117
+ err,
118
+ accountId: account.accountId,
119
+ messageId,
120
+ mode: preferCard ? "card" : "text"
121
+ }, "Feishu streaming edit failed");
122
+ if (preferCard) await sendFallbackText(text);
123
+ }
124
+ };
125
+ const start = async () => {
126
+ const receive_id_type = options.chatId.startsWith("ou_") || options.chatId.startsWith("on_") ? "open_id" : "chat_id";
127
+ const thinking = "Thinking…";
128
+ if (preferCard) {
129
+ const cardSpec = {
130
+ schema: "2.0",
131
+ config: { update_multi: true },
132
+ header: { title: {
133
+ tag: "plain_text",
134
+ content: "xopc"
135
+ } },
136
+ body: { elements: [{
137
+ tag: "markdown",
138
+ element_id: cardElementId,
139
+ content: thinking
140
+ }] }
141
+ };
142
+ const c = await api.cardkit.v1.card.create({ data: {
143
+ type: "card_json",
144
+ data: JSON.stringify(cardSpec)
145
+ } });
146
+ cardId = c?.data?.card_id ?? c?.card_id ?? void 0;
147
+ const res = options.replyToMessageId ? await api.im.message.reply({
148
+ path: { message_id: options.replyToMessageId },
149
+ data: {
150
+ msg_type: "interactive",
151
+ content: JSON.stringify({
152
+ type: "card",
153
+ data: { card_id: cardId }
154
+ }),
155
+ ...options.threadId ? { reply_in_thread: true } : {}
156
+ }
157
+ }) : await api.im.message.create({
158
+ params: { receive_id_type },
159
+ data: {
160
+ receive_id: options.chatId,
161
+ msg_type: "interactive",
162
+ content: JSON.stringify({
163
+ type: "card",
164
+ data: { card_id: cardId }
165
+ })
166
+ }
167
+ });
168
+ messageId = res?.data?.message_id ?? res?.message_id ?? void 0;
169
+ if (messageId) recordBindingIfReply(messageId);
170
+ log.info({
171
+ accountId: account.accountId,
172
+ chatId: options.chatId,
173
+ messageId,
174
+ mode: "card"
175
+ }, "Feishu streaming started");
176
+ return;
177
+ }
178
+ const res = options.replyToMessageId ? await api.im.message.reply({
179
+ path: { message_id: options.replyToMessageId },
180
+ data: {
181
+ msg_type: "text",
182
+ content: JSON.stringify({ text: thinking }),
183
+ ...options.threadId ? { reply_in_thread: true } : {}
184
+ }
185
+ }) : await api.im.message.create({
186
+ params: { receive_id_type },
187
+ data: {
188
+ receive_id: options.chatId,
189
+ msg_type: "text",
190
+ content: JSON.stringify({ text: thinking })
191
+ }
192
+ });
193
+ messageId = res?.data?.message_id ?? res?.message_id ?? void 0;
194
+ if (messageId) recordBindingIfReply(messageId);
195
+ log.info({
196
+ accountId: account.accountId,
197
+ chatId: options.chatId,
198
+ messageId,
199
+ mode: "text"
200
+ }, "Feishu streaming started");
201
+ };
202
+ ready = start().catch((err) => {
203
+ log.warn({
204
+ err,
205
+ accountId: account.accountId
206
+ }, "Feishu streaming start failed");
207
+ });
208
+ return {
209
+ update: (text) => {
210
+ lastText = text;
211
+ if (timer) clearTimeout(timer);
212
+ timer = setTimeout(() => {
213
+ flush().catch((err) => log.warn({ err }, "Feishu streaming flush failed"));
214
+ }, 800);
215
+ },
216
+ end: async () => {
217
+ if (timer) clearTimeout(timer);
218
+ await flush();
219
+ log.debug({
220
+ accountId: account.accountId,
221
+ chatId: options.chatId,
222
+ messageId,
223
+ editedAtLeastOnce,
224
+ lastTextLen: lastText.length,
225
+ mode: preferCard ? "card" : "text"
226
+ }, "Feishu streaming ended");
227
+ },
228
+ abort: async () => {
229
+ aborted = true;
230
+ if (timer) clearTimeout(timer);
231
+ },
232
+ messageId: () => void 0,
233
+ skipFinalOutbound: () => Boolean(messageId) && editedAtLeastOnce,
234
+ updateProgress: void 0,
235
+ setProgress: void 0
236
+ };
237
+ } };
238
+ }
239
+ //#endregion
240
+ export { createFeishuStreamingAdapter };
241
+
242
+ //# sourceMappingURL=streaming-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming-adapter.js","names":[],"sources":["../../../../../extensions/feishu/src/streaming/streaming-adapter.ts"],"sourcesContent":["import type {\n ChannelStreamHandle,\n ChannelStreamingAdapter,\n} from '@xopcai/xopc/channels/plugin-types.js';\nimport type { Config } from '@xopcai/xopc/config/schema.js';\nimport { createLogger } from '@xopcai/xopc/utils/logger.js';\n\nimport { resolveFeishuAccount } from '../state/accounts.js';\nimport { formatFeishuOutboundText } from '../format.js';\nimport { getFeishuBindingByMessageId, recordFeishuMessageBinding } from '../state/message-bindings.js';\nimport { createFeishuClient } from '../transport/client/client.js';\n\nconst log = createLogger('FeishuStreaming');\n\nexport function createFeishuStreamingAdapter(getConfig: () => Config): ChannelStreamingAdapter {\n return {\n startStream(options: {\n chatId: string;\n accountId?: string;\n threadId?: string;\n replyToMessageId?: string;\n parseMode?: 'Markdown' | 'HTML';\n }): ChannelStreamHandle | null {\n const cfg = getConfig();\n const account = resolveFeishuAccount(cfg, options.accountId ?? 'default');\n if (!account.configured) {\n return null;\n }\n // Feishu streaming is opt-in. When omitted/false, fall back to normal final outbound.\n if (account.streaming !== true) {\n return null;\n }\n const { api } = createFeishuClient(account);\n\n let messageId: string | undefined;\n let lastText = '';\n let timer: NodeJS.Timeout | null = null;\n let aborted = false;\n let editedAtLeastOnce = false;\n let ready: Promise<void> | null = null;\n let cardId: string | undefined;\n let cardSeq = 0;\n const cardElementId = 'md_1';\n let fallbackSent = false;\n\n const renderMode = account.renderMode ?? 'auto';\n const preferCard = renderMode === 'card' || renderMode === 'auto';\n\n const formatStreamText = (text: string) => {\n if (!text.trim()) return text;\n if (text.trim() === 'Thinking…') return text;\n const forCardMarkdown = Boolean(preferCard && cardId);\n return formatFeishuOutboundText({\n text,\n renderMode,\n forCardMarkdown,\n });\n };\n\n const recordBindingIfReply = (childMessageId: string) => {\n if (!childMessageId) return;\n const parentId = options.replyToMessageId;\n if (!parentId) return;\n const parent = getFeishuBindingByMessageId(parentId);\n if (!parent) return;\n recordFeishuMessageBinding({ ...parent, messageId: childMessageId });\n };\n\n const edit = async (text: string) => {\n if (ready) await ready;\n if (!messageId) return;\n const outbound = formatStreamText(text);\n if (cardId && preferCard) {\n cardSeq += 1;\n await (api as any).cardkit.v1.cardElement.content({\n path: { card_id: cardId, element_id: cardElementId },\n data: {\n content: outbound,\n sequence: cardSeq,\n uuid: `${cardId}:${cardSeq}`,\n },\n });\n } else {\n await (api as any).im.v1.message.update({\n path: { message_id: messageId },\n data: { msg_type: 'text', content: JSON.stringify({ text: outbound }) },\n });\n }\n editedAtLeastOnce = true;\n };\n\n const sendFallbackText = async (text: string) => {\n if (fallbackSent) return;\n fallbackSent = true;\n const receive_id_type =\n options.chatId.startsWith('ou_') || options.chatId.startsWith('on_') ? 'open_id' : 'chat_id';\n try {\n const outbound = formatStreamText(text);\n const res = options.replyToMessageId\n ? await (api as any).im.message.reply({\n path: { message_id: options.replyToMessageId },\n data: {\n msg_type: 'text',\n content: JSON.stringify({ text: outbound }),\n ...(options.threadId ? { reply_in_thread: true } : {}),\n },\n })\n : await (api as any).im.message.create({\n params: { receive_id_type },\n data: {\n receive_id: options.chatId,\n msg_type: 'text',\n content: JSON.stringify({ text: outbound }),\n },\n });\n const mid = res?.data?.message_id ?? res?.message_id ?? undefined;\n if (mid) recordBindingIfReply(mid);\n // Mark as delivered through the channel so the final outbound is skipped.\n editedAtLeastOnce = true;\n messageId = mid ?? messageId;\n // If we were in card mode, stop trying to update the card.\n cardId = undefined;\n } catch (err) {\n log.warn({ err, accountId: account.accountId }, 'Feishu streaming fallback send failed');\n }\n };\n\n const flush = async () => {\n if (aborted) return;\n if (ready) await ready;\n if (!messageId) return;\n const text = lastText;\n if (!text.trim()) return;\n try {\n await edit(text);\n } catch (err) {\n // Don't crash the agent loop. In card mode, card streaming can fail due to missing CardKit scopes;\n // fall back to sending a plain text reply so the user never gets stuck on \"Thinking…\".\n log.warn({ err, accountId: account.accountId, messageId, mode: preferCard ? 'card' : 'text' }, 'Feishu streaming edit failed');\n if (preferCard) {\n await sendFallbackText(text);\n }\n }\n };\n\n const start = async () => {\n const receive_id_type =\n options.chatId.startsWith('ou_') || options.chatId.startsWith('on_') ? 'open_id' : 'chat_id';\n const thinking = 'Thinking…';\n\n if (preferCard) {\n const cardSpec = {\n schema: '2.0',\n config: { update_multi: true },\n header: {\n title: { tag: 'plain_text', content: 'xopc' },\n },\n body: {\n elements: [\n {\n tag: 'markdown',\n element_id: cardElementId,\n content: thinking,\n },\n ],\n },\n };\n\n const c = await (api as any).cardkit.v1.card.create({\n data: { type: 'card_json', data: JSON.stringify(cardSpec) },\n });\n cardId = c?.data?.card_id ?? c?.card_id ?? undefined;\n\n const res = options.replyToMessageId\n ? await (api as any).im.message.reply({\n path: { message_id: options.replyToMessageId },\n data: {\n msg_type: 'interactive',\n content: JSON.stringify({ type: 'card', data: { card_id: cardId } }),\n ...(options.threadId ? { reply_in_thread: true } : {}),\n },\n })\n : await (api as any).im.message.create({\n params: { receive_id_type },\n data: {\n receive_id: options.chatId,\n msg_type: 'interactive',\n content: JSON.stringify({ type: 'card', data: { card_id: cardId } }),\n },\n });\n messageId = res?.data?.message_id ?? res?.message_id ?? undefined;\n if (messageId) recordBindingIfReply(messageId);\n log.info(\n { accountId: account.accountId, chatId: options.chatId, messageId, mode: 'card' },\n 'Feishu streaming started',\n );\n return;\n }\n\n const res = options.replyToMessageId\n ? await (api as any).im.message.reply({\n path: { message_id: options.replyToMessageId },\n data: {\n msg_type: 'text',\n content: JSON.stringify({ text: thinking }),\n ...(options.threadId ? { reply_in_thread: true } : {}),\n },\n })\n : await (api as any).im.message.create({\n params: { receive_id_type },\n data: {\n receive_id: options.chatId,\n msg_type: 'text',\n content: JSON.stringify({ text: thinking }),\n },\n });\n messageId = res?.data?.message_id ?? res?.message_id ?? undefined;\n if (messageId) recordBindingIfReply(messageId);\n log.info(\n { accountId: account.accountId, chatId: options.chatId, messageId, mode: 'text' },\n 'Feishu streaming started',\n );\n };\n\n ready = start().catch((err) => {\n log.warn({ err, accountId: account.accountId }, 'Feishu streaming start failed');\n });\n\n return {\n update: (text: string) => {\n lastText = text;\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => {\n void flush().catch((err) => log.warn({ err }, 'Feishu streaming flush failed'));\n }, 800);\n },\n end: async () => {\n if (timer) clearTimeout(timer);\n await flush();\n log.debug(\n {\n accountId: account.accountId,\n chatId: options.chatId,\n messageId,\n editedAtLeastOnce,\n lastTextLen: lastText.length,\n mode: preferCard ? 'card' : 'text',\n },\n 'Feishu streaming ended',\n );\n },\n abort: async () => {\n aborted = true;\n if (timer) clearTimeout(timer);\n },\n messageId: () => undefined,\n skipFinalOutbound: () => Boolean(messageId) && editedAtLeastOnce,\n updateProgress: undefined,\n setProgress: undefined,\n };\n },\n };\n}\n\n"],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import { bindFeishuConversation, unbindBySessionKey } from "./state/thread-bindings.js";
2
+ //#region extensions/feishu/src/subagent-hooks.ts
3
+ function normLower(v) {
4
+ return typeof v === "string" ? v.trim().toLowerCase() : "";
5
+ }
6
+ function stripProviderPrefix(raw) {
7
+ return raw.replace(/^(feishu|lark):/i, "").trim();
8
+ }
9
+ async function handleFeishuSubagentSpawning(event, ctx = {}) {
10
+ if (!event?.threadRequested) return void 0;
11
+ if (normLower(event?.requester?.channel) !== "feishu") return void 0;
12
+ const accountId = String(event?.requester?.accountId ?? "").trim() || "default";
13
+ const to = String(event?.requester?.to ?? "").trim();
14
+ const threadId = event?.requester?.threadId != null && event.requester.threadId !== "" ? String(event.requester.threadId).trim() : "";
15
+ const withoutProvider = to ? stripProviderPrefix(to) : "";
16
+ const isChatTarget = /^(chat|group|channel):/i.test(withoutProvider);
17
+ if (!to) return {
18
+ status: "error",
19
+ error: "Missing requester delivery target (to)"
20
+ };
21
+ if (isChatTarget && !threadId) return {
22
+ status: "error",
23
+ error: "Thread binding requires a topic threadId for chat targets"
24
+ };
25
+ bindFeishuConversation({
26
+ accountId,
27
+ conversationId: isChatTarget ? `${withoutProvider}|topic:${threadId}` : withoutProvider,
28
+ parentConversationId: isChatTarget ? withoutProvider : void 0,
29
+ targetSessionKey: String(event.childSessionKey),
30
+ metadata: {
31
+ requesterSessionKey: ctx.requesterSessionKey,
32
+ deliveryTo: to,
33
+ deliveryThreadId: threadId || void 0,
34
+ agentId: event?.agentId,
35
+ label: event?.label
36
+ }
37
+ });
38
+ return {
39
+ status: "ok",
40
+ threadBindingReady: true
41
+ };
42
+ }
43
+ function handleFeishuSubagentEnded(event) {
44
+ const accountId = String(event?.accountId ?? "").trim() || "default";
45
+ const sk = String(event?.targetSessionKey ?? "").trim();
46
+ if (!sk) return;
47
+ unbindBySessionKey(accountId, sk);
48
+ }
49
+ //#endregion
50
+ export { handleFeishuSubagentEnded, handleFeishuSubagentSpawning };
51
+
52
+ //# sourceMappingURL=subagent-hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subagent-hooks.js","names":[],"sources":["../../../../extensions/feishu/src/subagent-hooks.ts"],"sourcesContent":["import { bindFeishuConversation, unbindBySessionKey } from './state/thread-bindings.js';\n\nfunction normLower(v: unknown): string {\n return typeof v === 'string' ? v.trim().toLowerCase() : '';\n}\n\nfunction stripProviderPrefix(raw: string): string {\n return raw.replace(/^(feishu|lark):/i, '').trim();\n}\n\nexport async function handleFeishuSubagentSpawning(event: any, ctx: { requesterSessionKey?: string } = {}) {\n if (!event?.threadRequested) return undefined;\n const requesterChannel = normLower(event?.requester?.channel);\n if (requesterChannel !== 'feishu') return undefined;\n\n const accountId = String(event?.requester?.accountId ?? '').trim() || 'default';\n const to = String(event?.requester?.to ?? '').trim();\n const threadId =\n event?.requester?.threadId != null && event.requester.threadId !== ''\n ? String(event.requester.threadId).trim()\n : '';\n\n const withoutProvider = to ? stripProviderPrefix(to) : '';\n const isChatTarget = /^(chat|group|channel):/i.test(withoutProvider);\n\n // Minimal parity: only bind DM targets or topic thread targets.\n if (!to) return { status: 'error' as const, error: 'Missing requester delivery target (to)' };\n if (isChatTarget && !threadId) {\n return { status: 'error' as const, error: 'Thread binding requires a topic threadId for chat targets' };\n }\n\n const conversationId = isChatTarget ? `${withoutProvider}|topic:${threadId}` : withoutProvider;\n bindFeishuConversation({\n accountId,\n conversationId,\n parentConversationId: isChatTarget ? withoutProvider : undefined,\n targetSessionKey: String(event.childSessionKey),\n metadata: {\n requesterSessionKey: ctx.requesterSessionKey,\n deliveryTo: to,\n deliveryThreadId: threadId || undefined,\n agentId: event?.agentId,\n label: event?.label,\n },\n });\n\n return { status: 'ok' as const, threadBindingReady: true };\n}\n\nexport function handleFeishuSubagentEnded(event: any) {\n const accountId = String(event?.accountId ?? '').trim() || 'default';\n const sk = String(event?.targetSessionKey ?? '').trim();\n if (!sk) return;\n unbindBySessionKey(accountId, sk);\n}\n\n"],"mappings":";;AAEA,SAAS,UAAU,GAAoB;AACrC,QAAO,OAAO,MAAM,WAAW,EAAE,MAAM,CAAC,aAAa,GAAG;;AAG1D,SAAS,oBAAoB,KAAqB;AAChD,QAAO,IAAI,QAAQ,oBAAoB,GAAG,CAAC,MAAM;;AAGnD,eAAsB,6BAA6B,OAAY,MAAwC,EAAE,EAAE;AACzG,KAAI,CAAC,OAAO,gBAAiB,QAAO,KAAA;AAEpC,KADyB,UAAU,OAAO,WAAW,QACjC,KAAK,SAAU,QAAO,KAAA;CAE1C,MAAM,YAAY,OAAO,OAAO,WAAW,aAAa,GAAG,CAAC,MAAM,IAAI;CACtE,MAAM,KAAK,OAAO,OAAO,WAAW,MAAM,GAAG,CAAC,MAAM;CACpD,MAAM,WACJ,OAAO,WAAW,YAAY,QAAQ,MAAM,UAAU,aAAa,KAC/D,OAAO,MAAM,UAAU,SAAS,CAAC,MAAM,GACvC;CAEN,MAAM,kBAAkB,KAAK,oBAAoB,GAAG,GAAG;CACvD,MAAM,eAAe,0BAA0B,KAAK,gBAAgB;AAGpE,KAAI,CAAC,GAAI,QAAO;EAAE,QAAQ;EAAkB,OAAO;EAA0C;AAC7F,KAAI,gBAAgB,CAAC,SACnB,QAAO;EAAE,QAAQ;EAAkB,OAAO;EAA6D;AAIzG,wBAAuB;EACrB;EACA,gBAHqB,eAAe,GAAG,gBAAgB,SAAS,aAAa;EAI7E,sBAAsB,eAAe,kBAAkB,KAAA;EACvD,kBAAkB,OAAO,MAAM,gBAAgB;EAC/C,UAAU;GACR,qBAAqB,IAAI;GACzB,YAAY;GACZ,kBAAkB,YAAY,KAAA;GAC9B,SAAS,OAAO;GAChB,OAAO,OAAO;GACf;EACF,CAAC;AAEF,QAAO;EAAE,QAAQ;EAAe,oBAAoB;EAAM;;AAG5D,SAAgB,0BAA0B,OAAY;CACpD,MAAM,YAAY,OAAO,OAAO,aAAa,GAAG,CAAC,MAAM,IAAI;CAC3D,MAAM,KAAK,OAAO,OAAO,oBAAoB,GAAG,CAAC,MAAM;AACvD,KAAI,CAAC,GAAI;AACT,oBAAmB,WAAW,GAAG"}
@@ -0,0 +1,95 @@
1
+ import { cleanBlocksForDescendant } from "./docx-table-ops.js";
2
+ //#region extensions/feishu/src/tools/docx/docx-batch-insert.ts
3
+ const BATCH_SIZE = 1e3;
4
+ function readStringValue(value) {
5
+ return typeof value === "string" && value.trim() ? value : void 0;
6
+ }
7
+ function normalizeChildIds(children) {
8
+ if (Array.isArray(children)) return children;
9
+ const child = readStringValue(children);
10
+ return child ? [child] : void 0;
11
+ }
12
+ function toDescendantBlock(block) {
13
+ const children = normalizeChildIds(block.children);
14
+ return {
15
+ ...block,
16
+ ...children ? { children } : {}
17
+ };
18
+ }
19
+ function collectDescendants(blockMap, rootId) {
20
+ const result = [];
21
+ const visited = /* @__PURE__ */ new Set();
22
+ function collect(blockId) {
23
+ if (visited.has(blockId)) return;
24
+ visited.add(blockId);
25
+ const block = blockMap.get(blockId);
26
+ if (!block) return;
27
+ result.push(block);
28
+ const children = block.children;
29
+ if (Array.isArray(children)) for (const childId of children) collect(childId);
30
+ else if (typeof children === "string") collect(children);
31
+ }
32
+ collect(rootId);
33
+ return result;
34
+ }
35
+ async function insertBatch(client, docToken, blocks, firstLevelBlockIds, parentBlockId = docToken, index = -1) {
36
+ const descendants = cleanBlocksForDescendant(blocks);
37
+ if (descendants.length === 0) return [];
38
+ const res = await client.docx.documentBlockDescendant.create({
39
+ path: {
40
+ document_id: docToken,
41
+ block_id: parentBlockId
42
+ },
43
+ data: {
44
+ children_id: firstLevelBlockIds,
45
+ descendants: descendants.map(toDescendantBlock),
46
+ index
47
+ }
48
+ });
49
+ if (res.code !== 0) throw new Error(`${res.msg} (code: ${res.code})`);
50
+ return res.data?.children ?? [];
51
+ }
52
+ async function insertBlocksInBatches(client, docToken, blocks, firstLevelBlockIds, logger, parentBlockId = docToken, startIndex = -1) {
53
+ const allChildren = [];
54
+ const batches = [];
55
+ let currentBatch = {
56
+ firstLevelIds: [],
57
+ blocks: []
58
+ };
59
+ const usedBlockIds = /* @__PURE__ */ new Set();
60
+ const blockMap = /* @__PURE__ */ new Map();
61
+ for (const block of blocks) if (block.block_id) blockMap.set(block.block_id, block);
62
+ for (const firstLevelId of firstLevelBlockIds) {
63
+ const newBlocks = collectDescendants(blockMap, firstLevelId).filter((b) => b.block_id && !usedBlockIds.has(b.block_id));
64
+ if (newBlocks.length > 1e3) throw new Error(`Block "${firstLevelId}" has ${newBlocks.length} descendants, exceeds Feishu API limit ${BATCH_SIZE}. Split content.`);
65
+ if (currentBatch.blocks.length + newBlocks.length > 1e3 && currentBatch.blocks.length > 0) {
66
+ batches.push(currentBatch);
67
+ currentBatch = {
68
+ firstLevelIds: [],
69
+ blocks: []
70
+ };
71
+ }
72
+ currentBatch.firstLevelIds.push(firstLevelId);
73
+ for (const block of newBlocks) {
74
+ currentBatch.blocks.push(block);
75
+ if (block.block_id) usedBlockIds.add(block.block_id);
76
+ }
77
+ }
78
+ if (currentBatch.blocks.length > 0) batches.push(currentBatch);
79
+ let currentIndex = startIndex;
80
+ for (let i = 0; i < batches.length; i++) {
81
+ const batch = batches[i];
82
+ logger?.info?.(`feishu_doc: Inserting batch ${i + 1}/${batches.length} (${batch.blocks.length} blocks)...`);
83
+ const children = await insertBatch(client, docToken, batch.blocks, batch.firstLevelIds, parentBlockId, currentIndex);
84
+ allChildren.push(...children);
85
+ if (currentIndex !== -1) currentIndex += batch.firstLevelIds.length;
86
+ }
87
+ return {
88
+ children: allChildren,
89
+ skipped: []
90
+ };
91
+ }
92
+ //#endregion
93
+ export { BATCH_SIZE, insertBlocksInBatches };
94
+
95
+ //# sourceMappingURL=docx-batch-insert.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docx-batch-insert.js","names":[],"sources":["../../../../../../extensions/feishu/src/tools/docx/docx-batch-insert.ts"],"sourcesContent":["import type * as Lark from '@larksuiteoapi/node-sdk';\n\nimport { cleanBlocksForDescendant } from './docx-table-ops.js';\nimport type { FeishuDocxBlock, FeishuDocxBlockChild } from './docx-types.js';\n\nexport const BATCH_SIZE = 1000;\n\nfunction readStringValue(value: unknown): string | undefined {\n return typeof value === 'string' && value.trim() ? value : undefined;\n}\n\ntype DocxDescendantCreatePayload = NonNullable<\n Parameters<Lark.Client['docx']['documentBlockDescendant']['create']>[0]\n>;\ntype DocxDescendantCreateBlock = NonNullable<\n NonNullable<DocxDescendantCreatePayload['data']>['descendants']\n>[number];\n\nfunction normalizeChildIds(children: string[] | string | undefined): string[] | undefined {\n if (Array.isArray(children)) return children;\n const child = readStringValue(children);\n return child ? [child] : undefined;\n}\n\nfunction toDescendantBlock(block: FeishuDocxBlock): DocxDescendantCreateBlock {\n const children = normalizeChildIds(block.children);\n return {\n ...block,\n ...(children ? { children } : {}),\n } as DocxDescendantCreateBlock;\n}\n\nfunction collectDescendants(blockMap: Map<string, FeishuDocxBlock>, rootId: string): FeishuDocxBlock[] {\n const result: FeishuDocxBlock[] = [];\n const visited = new Set<string>();\n\n function collect(blockId: string) {\n if (visited.has(blockId)) return;\n visited.add(blockId);\n const block = blockMap.get(blockId);\n if (!block) return;\n result.push(block);\n const children = block.children;\n if (Array.isArray(children)) {\n for (const childId of children) collect(childId);\n } else if (typeof children === 'string') {\n collect(children);\n }\n }\n\n collect(rootId);\n return result;\n}\n\nasync function insertBatch(\n client: Lark.Client,\n docToken: string,\n blocks: FeishuDocxBlock[],\n firstLevelBlockIds: string[],\n parentBlockId: string = docToken,\n index: number = -1,\n): Promise<FeishuDocxBlockChild[]> {\n const descendants = cleanBlocksForDescendant(blocks);\n if (descendants.length === 0) return [];\n\n const res = await client.docx.documentBlockDescendant.create({\n path: { document_id: docToken, block_id: parentBlockId },\n data: {\n children_id: firstLevelBlockIds,\n descendants: descendants.map(toDescendantBlock),\n index,\n },\n });\n if (res.code !== 0) throw new Error(`${res.msg} (code: ${res.code})`);\n return res.data?.children ?? [];\n}\n\ntype Logger = { info?: (msg: string) => void };\n\nexport async function insertBlocksInBatches(\n client: Lark.Client,\n docToken: string,\n blocks: FeishuDocxBlock[],\n firstLevelBlockIds: string[],\n logger?: Logger,\n parentBlockId: string = docToken,\n startIndex: number = -1,\n): Promise<{ children: FeishuDocxBlockChild[]; skipped: string[] }> {\n const allChildren: FeishuDocxBlockChild[] = [];\n\n const batches: Array<{ firstLevelIds: string[]; blocks: FeishuDocxBlock[] }> = [];\n let currentBatch: { firstLevelIds: string[]; blocks: FeishuDocxBlock[] } = { firstLevelIds: [], blocks: [] };\n const usedBlockIds = new Set<string>();\n const blockMap = new Map<string, FeishuDocxBlock>();\n for (const block of blocks) {\n if (block.block_id) blockMap.set(block.block_id, block);\n }\n\n for (const firstLevelId of firstLevelBlockIds) {\n const descendants = collectDescendants(blockMap, firstLevelId);\n const newBlocks = descendants.filter((b) => b.block_id && !usedBlockIds.has(b.block_id));\n\n if (newBlocks.length > BATCH_SIZE) {\n throw new Error(\n `Block \"${firstLevelId}\" has ${newBlocks.length} descendants, exceeds Feishu API limit ${BATCH_SIZE}. Split content.`,\n );\n }\n\n if (currentBatch.blocks.length + newBlocks.length > BATCH_SIZE && currentBatch.blocks.length > 0) {\n batches.push(currentBatch);\n currentBatch = { firstLevelIds: [], blocks: [] };\n }\n\n currentBatch.firstLevelIds.push(firstLevelId);\n for (const block of newBlocks) {\n currentBatch.blocks.push(block);\n if (block.block_id) usedBlockIds.add(block.block_id);\n }\n }\n\n if (currentBatch.blocks.length > 0) batches.push(currentBatch);\n\n let currentIndex = startIndex;\n for (let i = 0; i < batches.length; i++) {\n const batch = batches[i]!;\n logger?.info?.(`feishu_doc: Inserting batch ${i + 1}/${batches.length} (${batch.blocks.length} blocks)...`);\n const children = await insertBatch(client, docToken, batch.blocks, batch.firstLevelIds, parentBlockId, currentIndex);\n allChildren.push(...children);\n if (currentIndex !== -1) currentIndex += batch.firstLevelIds.length;\n }\n\n return { children: allChildren, skipped: [] };\n}\n"],"mappings":"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"}
@@ -0,0 +1,75 @@
1
+ //#region extensions/feishu/src/tools/docx/docx-color-text.ts
2
+ const TEXT_COLOR = {
3
+ red: 1,
4
+ orange: 2,
5
+ yellow: 3,
6
+ green: 4,
7
+ blue: 5,
8
+ purple: 6,
9
+ grey: 7,
10
+ gray: 7
11
+ };
12
+ const BACKGROUND_COLOR = {
13
+ red: 1,
14
+ orange: 2,
15
+ yellow: 3,
16
+ green: 4,
17
+ blue: 5,
18
+ purple: 6,
19
+ grey: 7,
20
+ gray: 7
21
+ };
22
+ function normalizeLower(s) {
23
+ return (s ?? "").trim().toLowerCase();
24
+ }
25
+ function parseColorMarkup(content) {
26
+ const segments = [];
27
+ const KNOWN = "(?:bg:[a-z]+|bold|red|orange|yellow|green|blue|purple|gr[ae]y)";
28
+ const tagPattern = new RegExp(`\\[(${KNOWN}(?:\\s+${KNOWN})*)\\](.*?)\\[\\/(?:[^\\]]+)\\]|([^[]+|\\[)`, "gis");
29
+ let match;
30
+ while ((match = tagPattern.exec(content)) !== null) {
31
+ if (match[3] !== void 0) {
32
+ if (match[3]) segments.push({ text: match[3] });
33
+ continue;
34
+ }
35
+ const tagStr = normalizeLower(match[1] || "");
36
+ const text = match[2] || "";
37
+ const tags = tagStr.split(/\s+/).filter(Boolean);
38
+ const segment = { text };
39
+ for (const tag of tags) if (tag.startsWith("bg:")) {
40
+ const color = tag.slice(3);
41
+ if (BACKGROUND_COLOR[color]) segment.bgColor = BACKGROUND_COLOR[color];
42
+ } else if (tag === "bold") segment.bold = true;
43
+ else if (TEXT_COLOR[tag]) segment.textColor = TEXT_COLOR[tag];
44
+ if (text) segments.push(segment);
45
+ }
46
+ return segments;
47
+ }
48
+ async function updateColorText(client, docToken, blockId, content) {
49
+ const segments = parseColorMarkup(content);
50
+ const elements = segments.map((seg) => ({ text_run: {
51
+ content: seg.text,
52
+ text_element_style: {
53
+ ...seg.textColor ? { text_color: seg.textColor } : {},
54
+ ...seg.bgColor ? { background_color: seg.bgColor } : {},
55
+ ...seg.bold ? { bold: true } : {}
56
+ }
57
+ } }));
58
+ const res = await client.docx.documentBlock.patch({
59
+ path: {
60
+ document_id: docToken,
61
+ block_id: blockId
62
+ },
63
+ data: { update_text_elements: { elements } }
64
+ });
65
+ if (res.code !== 0) throw new Error(res.msg);
66
+ return {
67
+ success: true,
68
+ segments: segments.length,
69
+ block: res.data?.block
70
+ };
71
+ }
72
+ //#endregion
73
+ export { parseColorMarkup, updateColorText };
74
+
75
+ //# sourceMappingURL=docx-color-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docx-color-text.js","names":[],"sources":["../../../../../../extensions/feishu/src/tools/docx/docx-color-text.ts"],"sourcesContent":["import type * as Lark from '@larksuiteoapi/node-sdk';\n\nconst TEXT_COLOR: Record<string, number> = {\n red: 1,\n orange: 2,\n yellow: 3,\n green: 4,\n blue: 5,\n purple: 6,\n grey: 7,\n gray: 7,\n};\n\nconst BACKGROUND_COLOR: Record<string, number> = {\n red: 1,\n orange: 2,\n yellow: 3,\n green: 4,\n blue: 5,\n purple: 6,\n grey: 7,\n gray: 7,\n};\n\nfunction normalizeLower(s: string): string {\n return (s ?? '').trim().toLowerCase();\n}\n\ninterface Segment {\n text: string;\n textColor?: number;\n bgColor?: number;\n bold?: boolean;\n}\n\ntype DocxPatchPayload = NonNullable<Parameters<Lark.Client['docx']['documentBlock']['patch']>[0]>;\ntype DocxTextElement = NonNullable<\n NonNullable<NonNullable<DocxPatchPayload['data']>['update_text_elements']>['elements']\n>[number];\n\nexport function parseColorMarkup(content: string): Segment[] {\n const segments: Segment[] = [];\n const KNOWN = '(?:bg:[a-z]+|bold|red|orange|yellow|green|blue|purple|gr[ae]y)';\n const tagPattern = new RegExp(\n `\\\\[(${KNOWN}(?:\\\\s+${KNOWN})*)\\\\](.*?)\\\\[\\\\/(?:[^\\\\]]+)\\\\]|([^[]+|\\\\[)`,\n 'gis',\n );\n\n let match: RegExpExecArray | null;\n while ((match = tagPattern.exec(content)) !== null) {\n if (match[3] !== undefined) {\n if (match[3]) segments.push({ text: match[3] });\n continue;\n }\n const tagStr = normalizeLower(match[1] || '');\n const text = match[2] || '';\n const tags = tagStr.split(/\\s+/).filter(Boolean);\n const segment: Segment = { text };\n for (const tag of tags) {\n if (tag.startsWith('bg:')) {\n const color = tag.slice(3);\n if (BACKGROUND_COLOR[color]) segment.bgColor = BACKGROUND_COLOR[color];\n } else if (tag === 'bold') {\n segment.bold = true;\n } else if (TEXT_COLOR[tag]) {\n segment.textColor = TEXT_COLOR[tag];\n }\n }\n if (text) segments.push(segment);\n }\n return segments;\n}\n\nexport async function updateColorText(client: Lark.Client, docToken: string, blockId: string, content: string) {\n const segments = parseColorMarkup(content);\n const elements: DocxTextElement[] = segments.map((seg) => ({\n text_run: {\n content: seg.text,\n text_element_style: {\n ...(seg.textColor ? { text_color: seg.textColor } : {}),\n ...(seg.bgColor ? { background_color: seg.bgColor } : {}),\n ...(seg.bold ? { bold: true } : {}),\n },\n },\n }));\n\n const res = await client.docx.documentBlock.patch({\n path: { document_id: docToken, block_id: blockId },\n data: { update_text_elements: { elements } },\n });\n if (res.code !== 0) throw new Error(res.msg);\n return { success: true, segments: segments.length, block: res.data?.block };\n}\n\n"],"mappings":";AAEA,MAAM,aAAqC;CACzC,KAAK;CACL,QAAQ;CACR,QAAQ;CACR,OAAO;CACP,MAAM;CACN,QAAQ;CACR,MAAM;CACN,MAAM;CACP;AAED,MAAM,mBAA2C;CAC/C,KAAK;CACL,QAAQ;CACR,QAAQ;CACR,OAAO;CACP,MAAM;CACN,QAAQ;CACR,MAAM;CACN,MAAM;CACP;AAED,SAAS,eAAe,GAAmB;AACzC,SAAQ,KAAK,IAAI,MAAM,CAAC,aAAa;;AAevC,SAAgB,iBAAiB,SAA4B;CAC3D,MAAM,WAAsB,EAAE;CAC9B,MAAM,QAAQ;CACd,MAAM,aAAa,IAAI,OACrB,OAAO,MAAM,SAAS,MAAM,8CAC5B,MACD;CAED,IAAI;AACJ,SAAQ,QAAQ,WAAW,KAAK,QAAQ,MAAM,MAAM;AAClD,MAAI,MAAM,OAAO,KAAA,GAAW;AAC1B,OAAI,MAAM,GAAI,UAAS,KAAK,EAAE,MAAM,MAAM,IAAI,CAAC;AAC/C;;EAEF,MAAM,SAAS,eAAe,MAAM,MAAM,GAAG;EAC7C,MAAM,OAAO,MAAM,MAAM;EACzB,MAAM,OAAO,OAAO,MAAM,MAAM,CAAC,OAAO,QAAQ;EAChD,MAAM,UAAmB,EAAE,MAAM;AACjC,OAAK,MAAM,OAAO,KAChB,KAAI,IAAI,WAAW,MAAM,EAAE;GACzB,MAAM,QAAQ,IAAI,MAAM,EAAE;AAC1B,OAAI,iBAAiB,OAAQ,SAAQ,UAAU,iBAAiB;aACvD,QAAQ,OACjB,SAAQ,OAAO;WACN,WAAW,KACpB,SAAQ,YAAY,WAAW;AAGnC,MAAI,KAAM,UAAS,KAAK,QAAQ;;AAElC,QAAO;;AAGT,eAAsB,gBAAgB,QAAqB,UAAkB,SAAiB,SAAiB;CAC7G,MAAM,WAAW,iBAAiB,QAAQ;CAC1C,MAAM,WAA8B,SAAS,KAAK,SAAS,EACzD,UAAU;EACR,SAAS,IAAI;EACb,oBAAoB;GAClB,GAAI,IAAI,YAAY,EAAE,YAAY,IAAI,WAAW,GAAG,EAAE;GACtD,GAAI,IAAI,UAAU,EAAE,kBAAkB,IAAI,SAAS,GAAG,EAAE;GACxD,GAAI,IAAI,OAAO,EAAE,MAAM,MAAM,GAAG,EAAE;GACnC;EACF,EACF,EAAE;CAEH,MAAM,MAAM,MAAM,OAAO,KAAK,cAAc,MAAM;EAChD,MAAM;GAAE,aAAa;GAAU,UAAU;GAAS;EAClD,MAAM,EAAE,sBAAsB,EAAE,UAAU,EAAE;EAC7C,CAAC;AACF,KAAI,IAAI,SAAS,EAAG,OAAM,IAAI,MAAM,IAAI,IAAI;AAC5C,QAAO;EAAE,SAAS;EAAM,UAAU,SAAS;EAAQ,OAAO,IAAI,MAAM;EAAO"}