@ihazz/bitrix24 1.1.12 → 1.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -4
- package/dist/src/api.d.ts +10 -5
- package/dist/src/api.d.ts.map +1 -1
- package/dist/src/api.js +42 -8
- package/dist/src/api.js.map +1 -1
- package/dist/src/channel.d.ts +18 -1
- package/dist/src/channel.d.ts.map +1 -1
- package/dist/src/channel.js +1253 -42
- package/dist/src/channel.js.map +1 -1
- package/dist/src/i18n.js +68 -68
- package/dist/src/i18n.js.map +1 -1
- package/dist/src/inbound-handler.js +85 -7
- package/dist/src/inbound-handler.js.map +1 -1
- package/dist/src/media-service.d.ts +2 -0
- package/dist/src/media-service.d.ts.map +1 -1
- package/dist/src/media-service.js +117 -14
- package/dist/src/media-service.js.map +1 -1
- package/dist/src/message-utils.d.ts.map +1 -1
- package/dist/src/message-utils.js +73 -3
- package/dist/src/message-utils.js.map +1 -1
- package/dist/src/runtime.d.ts +1 -0
- package/dist/src/runtime.d.ts.map +1 -1
- package/dist/src/runtime.js.map +1 -1
- package/dist/src/send-service.d.ts +1 -0
- package/dist/src/send-service.d.ts.map +1 -1
- package/dist/src/send-service.js +26 -3
- package/dist/src/send-service.js.map +1 -1
- package/dist/src/state-paths.d.ts +1 -0
- package/dist/src/state-paths.d.ts.map +1 -1
- package/dist/src/state-paths.js +9 -0
- package/dist/src/state-paths.js.map +1 -1
- package/dist/src/types.d.ts +92 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api.ts +62 -13
- package/src/channel.ts +1734 -76
- package/src/i18n.ts +68 -68
- package/src/inbound-handler.ts +110 -7
- package/src/media-service.ts +146 -15
- package/src/message-utils.ts +90 -3
- package/src/runtime.ts +1 -0
- package/src/send-service.ts +40 -2
- package/src/state-paths.ts +11 -0
- package/src/types.ts +122 -0
package/src/i18n.ts
CHANGED
|
@@ -21,7 +21,7 @@ function resolve<T>(dict: Record<string, T>, lang: string | undefined): T {
|
|
|
21
21
|
|
|
22
22
|
const I18N_MEDIA_DOWNLOAD_FAILED: Record<string, (files: string) => string> = {
|
|
23
23
|
en: (f) => `⚠️ Could not download the file(s): ${f}.\n\nPlease check that the bot is in the required chat and has access to these files.`,
|
|
24
|
-
ru: (f) => `⚠️
|
|
24
|
+
ru: (f) => `⚠️ \u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b\u044b: ${f}.\n\n\u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435, \u0447\u0442\u043e \u0431\u043e\u0442 \u0441\u043e\u0441\u0442\u043e\u0438\u0442 \u0432 \u043d\u0443\u0436\u043d\u043e\u043c \u0447\u0430\u0442\u0435 \u0438 \u0443 \u043d\u0435\u0433\u043e \u0435\u0441\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u044d\u0442\u0438\u043c \u0444\u0430\u0439\u043b\u0430\u043c.`,
|
|
25
25
|
de: (f) => `⚠️ Die Datei(en) konnten nicht heruntergeladen werden: ${f}.\n\nBitte prüfen Sie, ob der Bot im erforderlichen Chat ist und Zugriff auf diese Dateien hat.`,
|
|
26
26
|
es: (f) => `⚠️ No se pudo descargar el/los archivo(s): ${f}.\n\nCompruebe que el bot este en el chat necesario y tenga acceso a estos archivos.`,
|
|
27
27
|
fr: (f) => `⚠️ Impossible de telecharger le ou les fichiers : ${f}.\n\nVerifiez que le bot se trouve dans le bon chat et qu il a acces a ces fichiers.`,
|
|
@@ -36,7 +36,7 @@ export function mediaDownloadFailed(lang: string | undefined, fileNames: string)
|
|
|
36
36
|
|
|
37
37
|
const I18N_GROUP_CHAT_UNSUPPORTED: Record<string, string> = {
|
|
38
38
|
en: 'I work only in a direct chat. Please message me there.',
|
|
39
|
-
ru: '
|
|
39
|
+
ru: '\u042f \u0440\u0430\u0431\u043e\u0442\u0430\u044e \u0442\u043e\u043b\u044c\u043a\u043e \u0432 \u043b\u0438\u0447\u043d\u043e\u043c \u0447\u0430\u0442\u0435. \u041d\u0430\u043f\u0438\u0448\u0438\u0442\u0435 \u043c\u043d\u0435 \u0442\u0430\u043c.',
|
|
40
40
|
de: 'Ich arbeite nur in einem direkten Chat. Bitte schreiben Sie mir dort.',
|
|
41
41
|
es: 'Solo trabajo en un chat personal. Escribame alli.',
|
|
42
42
|
fr: 'Je travaille uniquement dans un chat direct. Ecrivez-moi la-bas.',
|
|
@@ -51,7 +51,7 @@ export function groupChatUnsupported(lang: string | undefined): string {
|
|
|
51
51
|
|
|
52
52
|
const I18N_PERSONAL_BOT_OWNER_ONLY: Record<string, string> = {
|
|
53
53
|
en: 'This is a personal bot. Only the bot owner can message it.',
|
|
54
|
-
ru: '
|
|
54
|
+
ru: '\u042d\u0442\u043e \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0431\u043e\u0442. \u041f\u0438\u0441\u0430\u0442\u044c \u0435\u043c\u0443 \u043c\u043e\u0436\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u043b\u0430\u0434\u0435\u043b\u0435\u0446.',
|
|
55
55
|
de: 'Dies ist ein persoenlicher Bot. Nur der Bot-Besitzer kann ihm schreiben.',
|
|
56
56
|
es: 'Este es un bot personal. Solo el propietario del bot puede escribirle.',
|
|
57
57
|
fr: 'Ce bot est personnel. Seul son proprietaire peut lui ecrire.',
|
|
@@ -64,7 +64,7 @@ export function personalBotOwnerOnly(lang: string | undefined): string {
|
|
|
64
64
|
|
|
65
65
|
const I18N_ACCESS_APPROVED: Record<string, string> = {
|
|
66
66
|
en: 'Access to the bot has been approved.',
|
|
67
|
-
ru: '
|
|
67
|
+
ru: '\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0431\u043e\u0442\u0443 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d.',
|
|
68
68
|
de: 'Der Zugriff auf den Bot wurde bestaetigt.',
|
|
69
69
|
es: 'El acceso al bot ha sido aprobado.',
|
|
70
70
|
fr: 'L acces au bot a ete approuve.',
|
|
@@ -77,7 +77,7 @@ export function accessApproved(lang: string | undefined): string {
|
|
|
77
77
|
|
|
78
78
|
const I18N_OWNER_AND_ALLOWED_USERS_ONLY: Record<string, string> = {
|
|
79
79
|
en: 'This bot is available only to the bot owner and users with approved access.',
|
|
80
|
-
ru: '
|
|
80
|
+
ru: '\u042d\u0442\u043e\u0442 \u0431\u043e\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u043b\u0430\u0434\u0435\u043b\u044c\u0446\u0443 \u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\u043c \u0441 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u044b\u043c \u0434\u043e\u0441\u0442\u0443\u043f\u043e\u043c.',
|
|
81
81
|
de: 'Dieser Bot ist nur fuer den Bot-Besitzer und Benutzer mit bestaetigtem Zugriff verfuegbar.',
|
|
82
82
|
es: 'Este bot esta disponible solo para el propietario del bot y los usuarios con acceso confirmado.',
|
|
83
83
|
fr: 'Ce bot est disponible uniquement pour le proprietaire du bot et les utilisateurs disposant d un acces confirme.',
|
|
@@ -90,7 +90,7 @@ export function ownerAndAllowedUsersOnly(lang: string | undefined): string {
|
|
|
90
90
|
|
|
91
91
|
const I18N_ACCESS_DENIED: Record<string, string> = {
|
|
92
92
|
en: 'You do not have access to this bot.',
|
|
93
|
-
ru: '
|
|
93
|
+
ru: '\u0423 \u0432\u0430\u0441 \u043d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u044d\u0442\u043e\u043c\u0443 \u0431\u043e\u0442\u0443.',
|
|
94
94
|
de: 'Sie haben keinen Zugriff auf diesen Bot.',
|
|
95
95
|
es: 'No tienes acceso a este bot.',
|
|
96
96
|
fr: 'Vous n avez pas acces a ce bot.',
|
|
@@ -103,7 +103,7 @@ export function accessDenied(lang: string | undefined): string {
|
|
|
103
103
|
|
|
104
104
|
const I18N_GROUP_PAIRING_PENDING: Record<string, string> = {
|
|
105
105
|
en: 'To use this bot in group chats, access must be approved first. Please ask the bot owner to confirm your Bitrix24 pairing in OpenClaw.',
|
|
106
|
-
ru: '
|
|
106
|
+
ru: '\u0427\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0431\u043e\u0442\u043e\u043c \u0432 \u0433\u0440\u0443\u043f\u043f\u043e\u0432\u044b\u0445 \u0447\u0430\u0442\u0430\u0445, \u0441\u043d\u0430\u0447\u0430\u043b\u0430 \u043d\u0443\u0436\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f. \u041f\u043e\u043f\u0440\u043e\u0441\u0438\u0442\u0435 \u0432\u043b\u0430\u0434\u0435\u043b\u044c\u0446\u0430 \u0431\u043e\u0442\u0430 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u0432\u0430\u0448\u0443 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0443 Bitrix24 \u0432 OpenClaw.',
|
|
107
107
|
de: 'Um diesen Bot in Gruppenchats zu verwenden, muss der Zugriff zuerst bestaetigt werden. Bitten Sie den Bot-Besitzer, Ihr Bitrix24-Pairing in OpenClaw zu bestaetigen.',
|
|
108
108
|
es: 'Para usar este bot en chats grupales, primero hay que confirmar el acceso. Pida al propietario del bot que confirme su vinculacion de Bitrix24 en OpenClaw.',
|
|
109
109
|
fr: 'Pour utiliser ce bot dans les chats de groupe, l acces doit d abord etre confirme. Demandez au proprietaire du bot de confirmer votre association Bitrix24 dans OpenClaw.',
|
|
@@ -129,11 +129,11 @@ const I18N_WATCH_OWNER_DM_NOTICE: Record<string, (params: {
|
|
|
129
129
|
},
|
|
130
130
|
ru: ({ chatRef, topicsRef, sourceKind }) => {
|
|
131
131
|
const topicsSuffix = topicsRef
|
|
132
|
-
? `
|
|
132
|
+
? ` \u0421\u043e\u0432\u043f\u0430\u0432\u0448\u0438\u0435 \u0442\u0435\u043c\u044b: ${topicsRef}`
|
|
133
133
|
: '';
|
|
134
134
|
return sourceKind === 'dm'
|
|
135
|
-
?
|
|
136
|
-
:
|
|
135
|
+
? `\u0421\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u043e \u043f\u0440\u0430\u0432\u0438\u043b\u043e \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u0432 \u043b\u0438\u0447\u043d\u043e\u043c \u0447\u0430\u0442\u0435 \u0441 ${chatRef}.${topicsSuffix}`
|
|
136
|
+
: `\u0421\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u043e \u043f\u0440\u0430\u0432\u0438\u043b\u043e \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u0432 \u0447\u0430\u0442\u0435 ${chatRef}.${topicsSuffix}`;
|
|
137
137
|
},
|
|
138
138
|
de: ({ chatRef, topicsRef, sourceKind }) => {
|
|
139
139
|
const topicsSuffix = topicsRef
|
|
@@ -184,7 +184,7 @@ export function watchOwnerDmNotice(
|
|
|
184
184
|
|
|
185
185
|
const I18N_WELCOME: Record<string, (botName: string) => string> = {
|
|
186
186
|
en: (n) => `Hi! I'm ${n} — your AI agent in Bitrix24. I can help solve any question or task, find the information you need, and remind you about a meeting. I keep track of context and suggest ready-to-use actions right in the chat. Shall we begin? ✨`,
|
|
187
|
-
ru: (n) =>
|
|
187
|
+
ru: (n) => `\u041f\u0440\u0438\u0432\u0435\u0442! \u042f ${n} — \u0432\u0430\u0448 AI-\u0430\u0433\u0435\u043d\u0442 \u0432 \u0411\u0438\u0442\u0440\u0438\u043a\u044124. \u041f\u043e\u043c\u043e\u0433\u0443 \u0440\u0435\u0448\u0438\u0442\u044c \u043b\u044e\u0431\u043e\u0439 \u0432\u043e\u043f\u0440\u043e\u0441 \u0438\u043b\u0438 \u0437\u0430\u0434\u0430\u0447\u0443, \u043d\u0430\u0439\u0434\u0443 \u043d\u0443\u0436\u043d\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0438 \u043d\u0430\u043f\u043e\u043c\u043d\u044e \u043e \u0432\u0441\u0442\u0440\u0435\u0447\u0435. \u0423\u0447\u0438\u0442\u044b\u0432\u0430\u044e \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442 \u0438 \u043f\u0440\u0435\u0434\u043b\u0430\u0433\u0430\u044e \u0433\u043e\u0442\u043e\u0432\u044b\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043f\u0440\u044f\u043c\u043e \u0432 \u0447\u0430\u0442\u0435. \u041d\u0430\u0447\u043d\u0451\u043c? ✨`,
|
|
188
188
|
de: (n) => `Hallo! Ich bin ${n} — Ihr KI-Agent in Bitrix24. Ich helfe Ihnen bei jeder Frage oder Aufgabe, finde die benoetigten Informationen und erinnere Sie an einen Termin. Ich beruecksichtige den Kontext und schlage direkt im Chat passende Aktionen vor. Legen wir los? ✨`,
|
|
189
189
|
es: (n) => `¡Hola! Soy ${n} — su agente de IA en Bitrix24. Le ayudare a resolver cualquier pregunta o tarea, encontrar la informacion necesaria y recordarle una reunion. Tengo en cuenta el contexto y propongo acciones listas para usar directamente en el chat. ¿Empezamos? ✨`,
|
|
190
190
|
fr: (n) => `Bonjour ! Je suis ${n} — votre agent IA dans Bitrix24. Je peux vous aider a resoudre n importe quelle question ou tache, trouver les informations utiles et vous rappeler une reunion. Je tiens compte du contexte et je propose des actions pretes a l emploi directement dans le chat. On commence ? ✨`,
|
|
@@ -197,7 +197,7 @@ export function welcomeMessage(lang: string | undefined, botName: string): strin
|
|
|
197
197
|
|
|
198
198
|
const I18N_PAIRING_WELCOME: Record<string, (botName: string) => string> = {
|
|
199
199
|
en: (n) => `${n} is ready. Send any message, and I will return a pairing code. Then approve it in OpenClaw with: openclaw pairing approve bitrix24 <CODE>`,
|
|
200
|
-
ru: (n) => `${n}
|
|
200
|
+
ru: (n) => `${n} \u0433\u043e\u0442\u043e\u0432 \u043a \u0440\u0430\u0431\u043e\u0442\u0435. \u041e\u0442\u043f\u0440\u0430\u0432\u044c\u0442\u0435 \u043b\u044e\u0431\u043e\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435, \u0438 \u044f \u043f\u0440\u0438\u0448\u043b\u044e \u043a\u043e\u0434 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438. \u0417\u0430\u0442\u0435\u043c \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0435\u0433\u043e \u0432 OpenClaw \u043a\u043e\u043c\u0430\u043d\u0434\u043e\u0439: openclaw pairing approve bitrix24 <CODE>`,
|
|
201
201
|
de: (n) => `${n} ist bereit. Senden Sie eine beliebige Nachricht, und ich sende einen Pairing-Code. Bestaetigen Sie ihn danach in OpenClaw mit: openclaw pairing approve bitrix24 <CODE>`,
|
|
202
202
|
es: (n) => `${n} esta listo. Envie cualquier mensaje y recibira un codigo de vinculacion. Despues, confirmelo en OpenClaw con: openclaw pairing approve bitrix24 <CODE>`,
|
|
203
203
|
fr: (n) => `${n} est pret. Envoyez n importe quel message et je vous enverrai un code d association. Confirmez-le ensuite dans OpenClaw avec : openclaw pairing approve bitrix24 <CODE>`,
|
|
@@ -210,7 +210,7 @@ export function pairingWelcomeMessage(lang: string | undefined, botName: string)
|
|
|
210
210
|
|
|
211
211
|
const I18N_ONBOARDING_DISCLAIMER: Record<string, string> = {
|
|
212
212
|
en: 'The agent is an artificial intelligence. Please critically review any important information and the results of its actions, and supervise the agent while it works.',
|
|
213
|
-
ru: '
|
|
213
|
+
ru: '\u0410\u0433\u0435\u043d\u0442 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0438\u0441\u043a\u0443\u0441\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u043c \u0438\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u043e\u043c. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0439\u0442\u0435 \u043a\u0440\u0438\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0432\u0430\u0436\u043d\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0438 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u044b \u0435\u0433\u043e \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u0443\u0439\u0442\u0435 \u0440\u0430\u0431\u043e\u0442\u0443 \u0430\u0433\u0435\u043d\u0442\u0430.',
|
|
214
214
|
de: 'Der Agent ist eine kuenstliche Intelligenz. Bitte pruefen Sie kritisch alle wichtigen Informationen und die Ergebnisse seiner Aktionen und beaufsichtigen Sie seine Arbeit.',
|
|
215
215
|
es: 'El agente es una inteligencia artificial. Revise de forma critica cualquier informacion importante y los resultados de sus acciones, y supervise su trabajo.',
|
|
216
216
|
fr: 'L agent est une intelligence artificielle. Veuillez verifier de facon critique toute information importante et les resultats de ses actions, et superviser son travail.',
|
|
@@ -268,12 +268,12 @@ const I18N_COMMAND_GROUP_LABELS: Record<string, Record<CommandGroup, string>> =
|
|
|
268
268
|
export: 'Export',
|
|
269
269
|
},
|
|
270
270
|
ru: {
|
|
271
|
-
status: '
|
|
272
|
-
session: '
|
|
273
|
-
options: '
|
|
274
|
-
management: '
|
|
275
|
-
tools: '
|
|
276
|
-
export: '
|
|
271
|
+
status: '\u0421\u043f\u0440\u0430\u0432\u043a\u0430 \u0438 \u0441\u0442\u0430\u0442\u0443\u0441',
|
|
272
|
+
session: '\u0421\u0435\u0441\u0441\u0438\u044f',
|
|
273
|
+
options: '\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b',
|
|
274
|
+
management: '\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435',
|
|
275
|
+
tools: '\u0418\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u044b',
|
|
276
|
+
export: '\u042d\u043a\u0441\u043f\u043e\u0440\u0442',
|
|
277
277
|
},
|
|
278
278
|
de: {
|
|
279
279
|
status: 'Hilfe und Status',
|
|
@@ -317,10 +317,10 @@ const I18N_COMMAND_HELP_TEXT_LABELS: Record<string, CommandHelpTextLabels> = {
|
|
|
317
317
|
paramsLabel: 'Params',
|
|
318
318
|
},
|
|
319
319
|
ru: {
|
|
320
|
-
conciseHeader: '
|
|
321
|
-
fullHeader: '
|
|
322
|
-
fullListLabel: '
|
|
323
|
-
paramsLabel: '
|
|
320
|
+
conciseHeader: '\u041e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u043a\u043e\u043c\u0430\u043d\u0434\u044b',
|
|
321
|
+
fullHeader: '\u0414\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0435 \u043a\u043e\u043c\u0430\u043d\u0434\u044b',
|
|
322
|
+
fullListLabel: '\u041f\u043e\u043b\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a',
|
|
323
|
+
paramsLabel: '\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b',
|
|
324
324
|
},
|
|
325
325
|
de: {
|
|
326
326
|
conciseHeader: 'Wichtige Befehle',
|
|
@@ -382,36 +382,36 @@ const I18N_COMMAND_TEXTS: Record<string, CommandTextMap> = {
|
|
|
382
382
|
'export-session': { description: 'Export session to HTML' },
|
|
383
383
|
},
|
|
384
384
|
ru: {
|
|
385
|
-
help: { description: '
|
|
386
|
-
commands: { description: '
|
|
387
|
-
status: { description: '
|
|
388
|
-
context: { description: '
|
|
389
|
-
whoami: { description: '
|
|
390
|
-
usage: { description: '
|
|
391
|
-
new: { description: '
|
|
392
|
-
reset: { description: '
|
|
393
|
-
stop: { description: '
|
|
394
|
-
compact: { description: '
|
|
395
|
-
session: { description: '
|
|
396
|
-
model: { description: '
|
|
397
|
-
models: { description: '
|
|
398
|
-
think: { description: '
|
|
399
|
-
verbose: { description: '
|
|
400
|
-
reasoning: { description: '
|
|
401
|
-
elevated: { description: '
|
|
402
|
-
exec: { description: '
|
|
403
|
-
queue: { description: '
|
|
404
|
-
config: { description: '
|
|
405
|
-
debug: { description: '
|
|
406
|
-
approve: { description: '
|
|
407
|
-
activation: { description: '
|
|
408
|
-
send: { description: '
|
|
409
|
-
subagents: { description: '
|
|
410
|
-
kill: { description: '
|
|
411
|
-
steer: { description: '
|
|
412
|
-
skill: { description: '
|
|
413
|
-
restart: { description: '
|
|
414
|
-
'export-session': { description: '
|
|
385
|
+
help: { description: '\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0435 \u043a\u043e\u043c\u0430\u043d\u0434\u044b' },
|
|
386
|
+
commands: { description: '\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0432\u0441\u0435 \u043a\u043e\u043c\u0430\u043d\u0434\u044b' },
|
|
387
|
+
status: { description: '\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0441\u0442\u0430\u0442\u0443\u0441' },
|
|
388
|
+
context: { description: '\u041e\u0431\u044a\u044f\u0441\u043d\u0438\u0442\u044c, \u043a\u0430\u043a \u0444\u043e\u0440\u043c\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442' },
|
|
389
|
+
whoami: { description: '\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0432\u0430\u0448 ID' },
|
|
390
|
+
usage: { description: '\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0438 \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c', params: 'off | tokens | full | cost' },
|
|
391
|
+
new: { description: '\u041d\u0430\u0447\u0430\u0442\u044c \u043d\u043e\u0432\u0443\u044e \u0441\u0435\u0441\u0441\u0438\u044e' },
|
|
392
|
+
reset: { description: '\u0421\u0431\u0440\u043e\u0441\u0438\u0442\u044c \u0442\u0435\u043a\u0443\u0449\u0443\u044e \u0441\u0435\u0441\u0441\u0438\u044e' },
|
|
393
|
+
stop: { description: '\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0437\u0430\u043f\u0443\u0441\u043a' },
|
|
394
|
+
compact: { description: '\u0421\u0436\u0430\u0442\u044c \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442 \u0441\u0435\u0441\u0441\u0438\u0438', params: 'instructions' },
|
|
395
|
+
session: { description: '\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u0441\u0441\u0438\u0438', params: 'ttl | ...' },
|
|
396
|
+
model: { description: '\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0438\u043b\u0438 \u0441\u043c\u0435\u043d\u0438\u0442\u044c \u043c\u043e\u0434\u0435\u043b\u044c', params: 'model name' },
|
|
397
|
+
models: { description: '\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043c\u043e\u0434\u0435\u043b\u0438', params: 'provider' },
|
|
398
|
+
think: { description: '\u0423\u0440\u043e\u0432\u0435\u043d\u044c \u0440\u0430\u0437\u043c\u044b\u0448\u043b\u0435\u043d\u0438\u0439', params: 'off | low | medium | high' },
|
|
399
|
+
verbose: { description: '\u041f\u043e\u0434\u0440\u043e\u0431\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c', params: 'on | off' },
|
|
400
|
+
reasoning: { description: '\u0412\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u044c \u0440\u0430\u0441\u0441\u0443\u0436\u0434\u0435\u043d\u0438\u0439', params: 'on | off | stream' },
|
|
401
|
+
elevated: { description: '\u0420\u0435\u0436\u0438\u043c \u0441 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u044b\u043c\u0438 \u043f\u0440\u0430\u0432\u0430\u043c\u0438', params: 'on | off | ask | full' },
|
|
402
|
+
exec: { description: '\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f', params: 'host | security | ask | node' },
|
|
403
|
+
queue: { description: '\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043e\u0447\u0435\u0440\u0435\u0434\u0438', params: 'mode | debounce | cap | drop' },
|
|
404
|
+
config: { description: '\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0438\u043b\u0438 \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e', params: 'show | get | set | unset' },
|
|
405
|
+
debug: { description: '\u041e\u0442\u043b\u0430\u0434\u043e\u0447\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438', params: 'show | reset | set | unset' },
|
|
406
|
+
approve: { description: '\u041e\u0434\u043e\u0431\u0440\u0438\u0442\u044c \u0438\u043b\u0438 \u043e\u0442\u043a\u043b\u043e\u043d\u0438\u0442\u044c \u0437\u0430\u043f\u0440\u043e\u0441\u044b' },
|
|
407
|
+
activation: { description: '\u0420\u0435\u0436\u0438\u043c \u0430\u043a\u0442\u0438\u0432\u0430\u0446\u0438\u0438 \u0432 \u0433\u0440\u0443\u043f\u043f\u0430\u0445', params: 'mention | always' },
|
|
408
|
+
send: { description: '\u041f\u043e\u043b\u0438\u0442\u0438\u043a\u0430 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438', params: 'on | off | inherit' },
|
|
409
|
+
subagents: { description: '\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u0443\u0431\u0430\u0433\u0435\u043d\u0442\u0430\u043c\u0438' },
|
|
410
|
+
kill: { description: '\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0443\u0431\u0430\u0433\u0435\u043d\u0442\u0430', params: 'id | all' },
|
|
411
|
+
steer: { description: '\u041d\u0430\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u0441\u0443\u0431\u0430\u0433\u0435\u043d\u0442\u0430', params: 'message' },
|
|
412
|
+
skill: { description: '\u0417\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043d\u0430\u0432\u044b\u043a', params: 'name' },
|
|
413
|
+
restart: { description: '\u041f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c OpenClaw' },
|
|
414
|
+
'export-session': { description: '\u042d\u043a\u0441\u043f\u043e\u0440\u0442 \u0441\u0435\u0441\u0441\u0438\u0438 \u0432 HTML' },
|
|
415
415
|
},
|
|
416
416
|
de: {
|
|
417
417
|
help: { description: 'Verfuegbare Befehle anzeigen' },
|
|
@@ -550,9 +550,9 @@ const I18N_MODELS_COMMAND_REPLY_LABELS: Record<string, ModelsCommandReplyLabels>
|
|
|
550
550
|
switchModel: 'Switch model',
|
|
551
551
|
},
|
|
552
552
|
ru: {
|
|
553
|
-
header: '
|
|
554
|
-
showProviderModels: '
|
|
555
|
-
switchModel: '
|
|
553
|
+
header: '\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u044b',
|
|
554
|
+
showProviderModels: '\u041c\u043e\u0434\u0435\u043b\u0438 \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430',
|
|
555
|
+
switchModel: '\u0421\u043c\u0435\u043d\u0438\u0442\u044c \u043c\u043e\u0434\u0435\u043b\u044c',
|
|
556
556
|
},
|
|
557
557
|
de: {
|
|
558
558
|
header: 'Anbieter',
|
|
@@ -582,8 +582,8 @@ const I18N_NEW_SESSION_REPLY_TEXTS: Record<string, NewSessionReplyTexts> = {
|
|
|
582
582
|
active: 'You are now in a new session.',
|
|
583
583
|
},
|
|
584
584
|
ru: {
|
|
585
|
-
started: '
|
|
586
|
-
active: '
|
|
585
|
+
started: '\u041d\u0430\u0447\u0430\u0442\u0430 \u043d\u043e\u0432\u0430\u044f \u0441\u0435\u0441\u0441\u0438\u044f.',
|
|
586
|
+
active: '\u0422\u0435\u043f\u0435\u0440\u044c \u0432\u044b \u0432 \u043d\u043e\u0432\u043e\u0439 \u0441\u0435\u0441\u0441\u0438\u0438.',
|
|
587
587
|
},
|
|
588
588
|
de: {
|
|
589
589
|
started: 'Neue Sitzung gestartet.',
|
|
@@ -686,11 +686,11 @@ const I18N_COMMAND_KEYBOARD_LABELS: Record<string, CommandKeyboardLabels> = {
|
|
|
686
686
|
models: 'Models',
|
|
687
687
|
},
|
|
688
688
|
ru: {
|
|
689
|
-
help: '
|
|
690
|
-
status: '
|
|
691
|
-
commands: '
|
|
692
|
-
newSession: '
|
|
693
|
-
models: '
|
|
689
|
+
help: '\u0421\u043f\u0440\u0430\u0432\u043a\u0430',
|
|
690
|
+
status: '\u0421\u0442\u0430\u0442\u0443\u0441',
|
|
691
|
+
commands: '\u041a\u043e\u043c\u0430\u043d\u0434\u044b',
|
|
692
|
+
newSession: '\u041d\u043e\u0432\u0430\u044f \u0441\u0435\u0441\u0441\u0438\u044f',
|
|
693
|
+
models: '\u041c\u043e\u0434\u0435\u043b\u0438',
|
|
694
694
|
},
|
|
695
695
|
de: {
|
|
696
696
|
help: 'Hilfe',
|
|
@@ -735,11 +735,11 @@ const I18N_WELCOME_KEYBOARD_LABELS: Record<string, WelcomeKeyboardLabels> = {
|
|
|
735
735
|
commands: 'Commands',
|
|
736
736
|
},
|
|
737
737
|
ru: {
|
|
738
|
-
todayTasks: '
|
|
739
|
-
stalledDeals: '
|
|
740
|
-
help: '
|
|
741
|
-
newSession: '
|
|
742
|
-
commands: '
|
|
738
|
+
todayTasks: '\u0417\u0430\u0434\u0430\u0447\u0438 \u043d\u0430 \u0441\u0435\u0433\u043e\u0434\u043d\u044f',
|
|
739
|
+
stalledDeals: '\u0417\u0430\u0432\u0438\u0441\u0448\u0438\u0435 \u0441\u0434\u0435\u043b\u043a\u0438',
|
|
740
|
+
help: '\u041f\u043e\u043c\u043e\u0449\u044c',
|
|
741
|
+
newSession: '\u041d\u043e\u0432\u0430\u044f \u0441\u0435\u0441\u0441\u0438\u044f',
|
|
742
|
+
commands: '\u041a\u043e\u043c\u0430\u043d\u0434\u044b',
|
|
743
743
|
},
|
|
744
744
|
de: {
|
|
745
745
|
todayTasks: 'Heutige Aufgaben',
|
package/src/inbound-handler.ts
CHANGED
|
@@ -466,6 +466,82 @@ function normalizeMessageParams(params: B24V2MessageParams | undefined): Record<
|
|
|
466
466
|
return params;
|
|
467
467
|
}
|
|
468
468
|
|
|
469
|
+
function normalizeFileRecords(rawFiles: unknown): Record<string, Record<string, unknown>> {
|
|
470
|
+
if (!rawFiles || typeof rawFiles !== 'object') {
|
|
471
|
+
return {};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (Array.isArray(rawFiles)) {
|
|
475
|
+
return rawFiles.reduce<Record<string, Record<string, unknown>>>((acc, item) => {
|
|
476
|
+
if (!item || typeof item !== 'object') {
|
|
477
|
+
return acc;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const normalizedItem = item as Record<string, unknown>;
|
|
481
|
+
const rawId = normalizedItem.id ?? normalizedItem.ID;
|
|
482
|
+
if (rawId == null) {
|
|
483
|
+
return acc;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
acc[String(rawId)] = normalizedItem;
|
|
487
|
+
return acc;
|
|
488
|
+
}, {});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return rawFiles as Record<string, Record<string, unknown>>;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function normalizeFileExtension(rawExtension: unknown, fileName?: string): string {
|
|
495
|
+
const explicitExtension = typeof rawExtension === 'string'
|
|
496
|
+
? rawExtension.trim().replace(/^\.+/, '')
|
|
497
|
+
: '';
|
|
498
|
+
if (explicitExtension) {
|
|
499
|
+
return explicitExtension;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const normalizedName = typeof fileName === 'string' ? fileName.trim() : '';
|
|
503
|
+
const lastDotIndex = normalizedName.lastIndexOf('.');
|
|
504
|
+
if (lastDotIndex <= 0 || lastDotIndex === normalizedName.length - 1) {
|
|
505
|
+
return '';
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return normalizedName.slice(lastDotIndex + 1).trim().replace(/^\.+/, '');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function normalizeFileName(params: {
|
|
512
|
+
fileId: string;
|
|
513
|
+
rawName: unknown;
|
|
514
|
+
rawExtension: unknown;
|
|
515
|
+
}): string {
|
|
516
|
+
const normalizedName = typeof params.rawName === 'string'
|
|
517
|
+
? params.rawName.trim()
|
|
518
|
+
: '';
|
|
519
|
+
if (normalizedName) {
|
|
520
|
+
return normalizedName;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const extension = normalizeFileExtension(params.rawExtension);
|
|
524
|
+
return extension ? `file_${params.fileId}.${extension}` : `file_${params.fileId}`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function normalizeMediaType(rawType: unknown, extension: string): 'image' | 'file' {
|
|
528
|
+
const normalizedType = typeof rawType === 'string'
|
|
529
|
+
? rawType.trim().toLowerCase()
|
|
530
|
+
: '';
|
|
531
|
+
|
|
532
|
+
if (normalizedType === 'image') {
|
|
533
|
+
return 'image';
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg', 'tiff', 'tif', 'ico'].includes(
|
|
537
|
+
extension.toLowerCase(),
|
|
538
|
+
)) {
|
|
539
|
+
return 'image';
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return 'file';
|
|
543
|
+
}
|
|
544
|
+
|
|
469
545
|
function extractFilesFromParams(params: B24V2MessageParams | undefined): B24MediaItem[] {
|
|
470
546
|
const normalizedParams = normalizeMessageParams(params);
|
|
471
547
|
|
|
@@ -475,13 +551,40 @@ function extractFilesFromParams(params: B24V2MessageParams | undefined): B24Medi
|
|
|
475
551
|
const fileIds = Array.isArray(rawFileIds) ? rawFileIds : [rawFileIds];
|
|
476
552
|
if (!fileIds.length) return [];
|
|
477
553
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
554
|
+
const fileRecords = normalizeFileRecords(normalizedParams.FILES);
|
|
555
|
+
|
|
556
|
+
return fileIds.map((id) => {
|
|
557
|
+
const fileId = String(id);
|
|
558
|
+
const fileRecord = fileRecords[fileId] ?? {};
|
|
559
|
+
const viewerAttrs = fileRecord.viewerAttrs;
|
|
560
|
+
const viewerTitle = viewerAttrs && typeof viewerAttrs === 'object'
|
|
561
|
+
? (viewerAttrs as Record<string, unknown>).title
|
|
562
|
+
: undefined;
|
|
563
|
+
const rawName = fileRecord.name ?? fileRecord.NAME ?? viewerTitle;
|
|
564
|
+
const name = normalizeFileName({
|
|
565
|
+
fileId,
|
|
566
|
+
rawName,
|
|
567
|
+
rawExtension: fileRecord.extension ?? fileRecord.EXTENSION,
|
|
568
|
+
});
|
|
569
|
+
const extension = normalizeFileExtension(
|
|
570
|
+
fileRecord.extension ?? fileRecord.EXTENSION,
|
|
571
|
+
name,
|
|
572
|
+
);
|
|
573
|
+
const rawSize = Number(fileRecord.size ?? fileRecord.SIZE ?? 0);
|
|
574
|
+
const size = Number.isFinite(rawSize) && rawSize > 0 ? rawSize : 0;
|
|
575
|
+
const urlDownload = typeof fileRecord.urlDownload === 'string'
|
|
576
|
+
? fileRecord.urlDownload
|
|
577
|
+
: undefined;
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
id: fileId,
|
|
581
|
+
name,
|
|
582
|
+
extension,
|
|
583
|
+
size,
|
|
584
|
+
type: normalizeMediaType(fileRecord.type ?? fileRecord.TYPE, extension),
|
|
585
|
+
...(urlDownload ? { urlDownload } : {}),
|
|
586
|
+
};
|
|
587
|
+
});
|
|
485
588
|
}
|
|
486
589
|
|
|
487
590
|
function extractReplyToMessageId(params: B24V2MessageParams | undefined): string | undefined {
|
package/src/media-service.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { isIP } from 'node:net';
|
|
3
3
|
import { createReadStream, createWriteStream } from 'node:fs';
|
|
4
|
-
import { writeFile, mkdir, stat, unlink, rename, realpath } from 'node:fs/promises';
|
|
5
|
-
import { join, basename, relative, sep } from 'node:path';
|
|
4
|
+
import { copyFile, writeFile, mkdir, stat, unlink, rename, realpath } from 'node:fs/promises';
|
|
5
|
+
import { isAbsolute, join, basename, relative, resolve as resolvePath, sep } from 'node:path';
|
|
6
6
|
import { Readable, Transform } from 'node:stream';
|
|
7
7
|
import { pipeline } from 'node:stream/promises';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
8
9
|
import { Bitrix24Api } from './api.js';
|
|
9
10
|
import type { BotContext } from './api.js';
|
|
10
11
|
import type { Logger } from './types.js';
|
|
11
|
-
import { resolveManagedMediaDir } from './state-paths.js';
|
|
12
|
+
import { resolveManagedMediaDir, resolveTrustedWorkspaceDirs } from './state-paths.js';
|
|
12
13
|
import { defaultLogger, maskUrlForLog, serializeError } from './utils.js';
|
|
13
14
|
|
|
14
15
|
export interface DownloadedMedia {
|
|
@@ -22,6 +23,11 @@ export interface UploadedMedia {
|
|
|
22
23
|
messageId?: number;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
type ResolvedUploadPath = {
|
|
27
|
+
path: string;
|
|
28
|
+
cleanup: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
25
31
|
const MIME_MAP: Record<string, string> = {
|
|
26
32
|
jpg: 'image/jpeg',
|
|
27
33
|
jpeg: 'image/jpeg',
|
|
@@ -66,6 +72,11 @@ function mimeFromExtension(ext: string): string {
|
|
|
66
72
|
return MIME_MAP[ext.toLowerCase()] ?? 'application/octet-stream';
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
function isPathInside(rootPath: string, filePath: string): boolean {
|
|
76
|
+
const rel = relative(rootPath, filePath);
|
|
77
|
+
return !rel.startsWith('..') && !rel.startsWith(sep);
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
function normalizeResponseContentType(contentType: string | null): string | undefined {
|
|
70
81
|
if (!contentType) {
|
|
71
82
|
return undefined;
|
|
@@ -75,6 +86,23 @@ function normalizeResponseContentType(contentType: string | null): string | unde
|
|
|
75
86
|
return normalized ? normalized : undefined;
|
|
76
87
|
}
|
|
77
88
|
|
|
89
|
+
function normalizeLocalUploadPath(localPath: string): string | null {
|
|
90
|
+
const trimmed = localPath.trim();
|
|
91
|
+
if (!trimmed) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (trimmed.startsWith('file://')) {
|
|
96
|
+
try {
|
|
97
|
+
return fileURLToPath(trimmed);
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return trimmed;
|
|
104
|
+
}
|
|
105
|
+
|
|
78
106
|
function isPrivateHost(hostname: string): boolean {
|
|
79
107
|
if (hostname === 'localhost' || hostname === '::1') return true;
|
|
80
108
|
|
|
@@ -140,6 +168,7 @@ const MAX_FILE_SIZE = 100 * 1024 * 1024;
|
|
|
140
168
|
const DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
141
169
|
|
|
142
170
|
const EMPTY_BUFFER = Buffer.alloc(0);
|
|
171
|
+
const MANAGED_FILE_NAME_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}_(.+)$/i;
|
|
143
172
|
|
|
144
173
|
class MaxFileSizeExceededError extends Error {
|
|
145
174
|
readonly size: number;
|
|
@@ -241,19 +270,112 @@ export class MediaService {
|
|
|
241
270
|
return encoded;
|
|
242
271
|
}
|
|
243
272
|
|
|
244
|
-
private async
|
|
245
|
-
|
|
273
|
+
private async stageWorkspaceUploadPath(localPath: string): Promise<string> {
|
|
274
|
+
const { savePath, tempPath } = this.buildManagedMediaPath(basename(localPath), 'file');
|
|
246
275
|
|
|
247
276
|
try {
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
277
|
+
const fileStat = await stat(localPath);
|
|
278
|
+
if (fileStat.size > MAX_FILE_SIZE) {
|
|
279
|
+
throw new MaxFileSizeExceededError(fileStat.size, MAX_FILE_SIZE);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
await copyFile(localPath, tempPath);
|
|
283
|
+
await rename(tempPath, savePath);
|
|
284
|
+
return savePath;
|
|
285
|
+
} catch (error) {
|
|
286
|
+
await unlink(tempPath).catch(() => undefined);
|
|
287
|
+
await unlink(savePath).catch(() => undefined);
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private async resolveManagedUploadPath(localPath: string): Promise<ResolvedUploadPath | null> {
|
|
293
|
+
await this.ensureDir();
|
|
294
|
+
|
|
295
|
+
const normalizedInputPath = normalizeLocalUploadPath(localPath);
|
|
296
|
+
if (!normalizedInputPath) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const resolvedMediaDir = await realpath(resolveManagedMediaDir()).catch(() => null);
|
|
301
|
+
if (!resolvedMediaDir) {
|
|
255
302
|
return null;
|
|
256
303
|
}
|
|
304
|
+
|
|
305
|
+
const configuredWorkspaceRoots = resolveTrustedWorkspaceDirs();
|
|
306
|
+
const resolvedWorkspaceRoots = (await Promise.all(
|
|
307
|
+
configuredWorkspaceRoots.map(async (workspaceRoot) => {
|
|
308
|
+
try {
|
|
309
|
+
return await realpath(workspaceRoot);
|
|
310
|
+
} catch {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}),
|
|
314
|
+
)).filter((workspaceRoot): workspaceRoot is string => Boolean(workspaceRoot));
|
|
315
|
+
|
|
316
|
+
const candidatePaths = new Set<string>();
|
|
317
|
+
candidatePaths.add(normalizedInputPath);
|
|
318
|
+
|
|
319
|
+
if (!isAbsolute(normalizedInputPath)) {
|
|
320
|
+
candidatePaths.add(join(resolveManagedMediaDir(), normalizedInputPath));
|
|
321
|
+
for (const workspaceRoot of configuredWorkspaceRoots) {
|
|
322
|
+
candidatePaths.add(resolvePath(workspaceRoot, normalizedInputPath));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (isAbsolute(normalizedInputPath)) {
|
|
327
|
+
const looksLikeWorkspacePath = configuredWorkspaceRoots.some((workspaceRoot) =>
|
|
328
|
+
isPathInside(workspaceRoot, normalizedInputPath),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
if (looksLikeWorkspacePath) {
|
|
332
|
+
candidatePaths.add(join(resolveManagedMediaDir(), basename(normalizedInputPath)));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
for (const candidatePath of candidatePaths) {
|
|
337
|
+
let filePath: string;
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
filePath = await realpath(candidatePath);
|
|
341
|
+
} catch {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (isPathInside(resolvedMediaDir, filePath)) {
|
|
346
|
+
return { path: filePath, cleanup: false };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
for (const resolvedWorkspaceRoot of resolvedWorkspaceRoots) {
|
|
350
|
+
if (!isPathInside(resolvedWorkspaceRoot, filePath)) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
path: await this.stageWorkspaceUploadPath(filePath),
|
|
356
|
+
cleanup: true,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private resolveOutboundFileName(fileName: string, managedPath: string): string {
|
|
365
|
+
const requestedBaseName = basename(fileName).trim();
|
|
366
|
+
const managedBaseName = basename(managedPath).trim();
|
|
367
|
+
const managedMatch = MANAGED_FILE_NAME_RE.exec(managedBaseName);
|
|
368
|
+
const logicalManagedName = managedMatch?.[1]?.trim() || managedBaseName;
|
|
369
|
+
|
|
370
|
+
if (requestedBaseName && requestedBaseName !== managedBaseName) {
|
|
371
|
+
return requestedBaseName;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (requestedBaseName === managedBaseName && managedMatch?.[1]?.trim()) {
|
|
375
|
+
return managedMatch[1].trim();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return requestedBaseName || logicalManagedName;
|
|
257
379
|
}
|
|
258
380
|
|
|
259
381
|
private async fetchDownloadResponse(params: {
|
|
@@ -425,6 +547,7 @@ export class MediaService {
|
|
|
425
547
|
message?: string;
|
|
426
548
|
}): Promise<UploadedMedia> {
|
|
427
549
|
const { localPath, fileName, webhookUrl, bot, dialogId, message } = params;
|
|
550
|
+
let stagedUploadPath: string | null = null;
|
|
428
551
|
|
|
429
552
|
try {
|
|
430
553
|
const managedPath = await this.resolveManagedUploadPath(localPath);
|
|
@@ -435,9 +558,12 @@ export class MediaService {
|
|
|
435
558
|
});
|
|
436
559
|
return { ok: false };
|
|
437
560
|
}
|
|
561
|
+
if (managedPath.cleanup) {
|
|
562
|
+
stagedUploadPath = managedPath.path;
|
|
563
|
+
}
|
|
438
564
|
|
|
439
565
|
// Check file size before reading
|
|
440
|
-
const fileStat = await stat(managedPath);
|
|
566
|
+
const fileStat = await stat(managedPath.path);
|
|
441
567
|
if (fileStat.size > MAX_FILE_SIZE) {
|
|
442
568
|
this.logger.warn('File too large to upload', {
|
|
443
569
|
fileName,
|
|
@@ -448,10 +574,11 @@ export class MediaService {
|
|
|
448
574
|
}
|
|
449
575
|
|
|
450
576
|
// Encode incrementally to avoid holding both the raw file and base64 string in memory.
|
|
451
|
-
const base64Content = await this.encodeFileToBase64(managedPath);
|
|
577
|
+
const base64Content = await this.encodeFileToBase64(managedPath.path);
|
|
578
|
+
const outboundFileName = this.resolveOutboundFileName(fileName, managedPath.path);
|
|
452
579
|
|
|
453
580
|
const result = await this.api.uploadFile(webhookUrl, bot, dialogId, {
|
|
454
|
-
name:
|
|
581
|
+
name: outboundFileName,
|
|
455
582
|
content: base64Content,
|
|
456
583
|
message,
|
|
457
584
|
});
|
|
@@ -469,6 +596,10 @@ export class MediaService {
|
|
|
469
596
|
error: serializeError(err),
|
|
470
597
|
});
|
|
471
598
|
return { ok: false };
|
|
599
|
+
} finally {
|
|
600
|
+
if (stagedUploadPath) {
|
|
601
|
+
await unlink(stagedUploadPath).catch(() => undefined);
|
|
602
|
+
}
|
|
472
603
|
}
|
|
473
604
|
}
|
|
474
605
|
|