@hostwebhook/node-types 1.52.10 → 1.52.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/drive-operations.d.ts +81 -0
- package/dist/drive-operations.js +284 -1
- package/dist/drive-toolkit.d.ts +54 -0
- package/dist/drive-toolkit.js +176 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.js +10 -2
- package/dist/telegram-operations.d.ts +87 -0
- package/dist/telegram-operations.js +311 -1
- package/dist/telegram-toolkit.d.ts +52 -0
- package/dist/telegram-toolkit.js +186 -0
- package/package.json +1 -1
|
@@ -20,3 +20,84 @@ export declare const DRIVE_OPERATIONS: readonly ["upload", "download", "copy", "
|
|
|
20
20
|
export type DriveOperation = (typeof DRIVE_OPERATIONS)[number];
|
|
21
21
|
/** Type guard — useful when validating untrusted input (DTOs, tool calls). */
|
|
22
22
|
export declare function isDriveOperation(value: unknown): value is DriveOperation;
|
|
23
|
+
export type DriveParamType =
|
|
24
|
+
/** Live file picker (needs a credential) + templated paste. */
|
|
25
|
+
'driveFile'
|
|
26
|
+
/** Text input with `{{payload.*}}` autocomplete. */
|
|
27
|
+
| 'template'
|
|
28
|
+
/** Plain `<select>`; the labels are not the values, so `options` is required. */
|
|
29
|
+
| 'select'
|
|
30
|
+
/**
|
|
31
|
+
* Searchable mime-type dropdown. The option list is long and shared with
|
|
32
|
+
* other screens, so it stays in the dashboard (`lib/drive-mime-options`)
|
|
33
|
+
* and the schema only says "this field is a mime picker".
|
|
34
|
+
*/
|
|
35
|
+
| 'mimeSelect' | 'number'
|
|
36
|
+
/**
|
|
37
|
+
* A `<select>` whose values are `''` / `'true'` / `'false'` but which is
|
|
38
|
+
* stored as a BOOLEAN, or omitted entirely when `''`.
|
|
39
|
+
*
|
|
40
|
+
* This is the shape that keeps `starred` honest: the api applies it with
|
|
41
|
+
* `typeof config.starred === 'boolean'`, so "— don't change —" has to mean
|
|
42
|
+
* *no key at all*. A plain Switch cannot express that third state, and
|
|
43
|
+
* storing `''` or `'false'` would silently stop the star from ever changing.
|
|
44
|
+
*/
|
|
45
|
+
| 'booleanSelect'
|
|
46
|
+
/**
|
|
47
|
+
* Full-width explainer card that toggles: icon, title, prose and bullets,
|
|
48
|
+
* with the switch on the right. The card's rich body is JSX, so it lives in
|
|
49
|
+
* the page keyed by param name — the schema only declares the field.
|
|
50
|
+
*/
|
|
51
|
+
| 'switchCard'
|
|
52
|
+
/** Live multi-select of the starting folder's immediate children. */
|
|
53
|
+
| 'driveSubfolders';
|
|
54
|
+
export interface DriveParamSpec {
|
|
55
|
+
/** The `operationConfig` key. This is what the api reads — not a UI name. */
|
|
56
|
+
name: string;
|
|
57
|
+
label: string;
|
|
58
|
+
type: DriveParamType;
|
|
59
|
+
required?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Help text under the field. Plain text on purpose: the package holds no
|
|
62
|
+
* React, so the `<code>` spans the page used to have are gone. Deliberate.
|
|
63
|
+
*/
|
|
64
|
+
description?: string;
|
|
65
|
+
placeholder?: string;
|
|
66
|
+
/** Prefilled when the saved config has no value for this key. */
|
|
67
|
+
default?: string | number;
|
|
68
|
+
/** Required for `select` and `booleanSelect` — label and value differ. */
|
|
69
|
+
options?: ReadonlyArray<{
|
|
70
|
+
value: string;
|
|
71
|
+
label: string;
|
|
72
|
+
}>;
|
|
73
|
+
min?: number;
|
|
74
|
+
max?: number;
|
|
75
|
+
/**
|
|
76
|
+
* Renders in the "Advanced" tab instead of "Config". The page used to keep
|
|
77
|
+
* a two-name `ADVANCED_FIELDS` array while the advanced *tab* actually held
|
|
78
|
+
* four fields, so `recursive` and `subfolderIds` only showed up because
|
|
79
|
+
* `list` happened to also carry one of the two named fields. Marking all
|
|
80
|
+
* four here removes that coincidence.
|
|
81
|
+
*/
|
|
82
|
+
advanced?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Only shown when a sibling field holds one of these values. No other node's
|
|
85
|
+
* schema needed this yet; Drive has three cases (the two share targets and
|
|
86
|
+
* the subfolder scope).
|
|
87
|
+
*/
|
|
88
|
+
showWhen?: {
|
|
89
|
+
field: string;
|
|
90
|
+
in: ReadonlyArray<string>;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export interface DriveOperationSpec {
|
|
94
|
+
/** UI label for the operation dropdown row. */
|
|
95
|
+
label: string;
|
|
96
|
+
/** Sub-line under the label in the dropdown. */
|
|
97
|
+
description: string;
|
|
98
|
+
/** Drive API method it maps to (developers.google.com/drive/api/reference). */
|
|
99
|
+
apiMethod: string;
|
|
100
|
+
/** Parameter schema. Order matters: it is the on-screen order. */
|
|
101
|
+
params: DriveParamSpec[];
|
|
102
|
+
}
|
|
103
|
+
export declare const DRIVE_OPERATION_SPECS: Record<DriveOperation, DriveOperationSpec>;
|
package/dist/drive-operations.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DRIVE_OPERATIONS = void 0;
|
|
3
|
+
exports.DRIVE_OPERATION_SPECS = exports.DRIVE_OPERATIONS = void 0;
|
|
4
4
|
exports.isDriveOperation = isDriveOperation;
|
|
5
5
|
/**
|
|
6
6
|
* Google Drive operation enum — single source of truth across the API,
|
|
@@ -38,3 +38,286 @@ exports.DRIVE_OPERATIONS = [
|
|
|
38
38
|
function isDriveOperation(value) {
|
|
39
39
|
return typeof value === 'string' && exports.DRIVE_OPERATIONS.includes(value);
|
|
40
40
|
}
|
|
41
|
+
/* Fábricas de los campos que se repiten, para que un cambio de copy no haya
|
|
42
|
+
que perseguirlo por doce operaciones. */
|
|
43
|
+
const fileId = () => ({
|
|
44
|
+
name: 'fileId',
|
|
45
|
+
label: 'Drive File',
|
|
46
|
+
type: 'driveFile',
|
|
47
|
+
required: true,
|
|
48
|
+
description: 'Pick a file from Drive or paste a templated value like {{payload.driveFileId}}.',
|
|
49
|
+
});
|
|
50
|
+
const parentFolderId = () => ({
|
|
51
|
+
name: 'parentFolderId',
|
|
52
|
+
label: 'Parent Folder ID (override)',
|
|
53
|
+
type: 'template',
|
|
54
|
+
advanced: true,
|
|
55
|
+
placeholder: 'Empty = use Default Folder above',
|
|
56
|
+
description: "Folder id used for this operation only. Leave empty to use the action's Default Folder. Templates work — e.g. {{payload.folderId}}.",
|
|
57
|
+
});
|
|
58
|
+
/** El filtro por tipo de upload/list. En `export` el mismo `mimeType` es otra
|
|
59
|
+
cosa y se declara aparte. */
|
|
60
|
+
const mimeTypeFilter = () => ({
|
|
61
|
+
name: 'mimeType',
|
|
62
|
+
label: 'File type filter (optional)',
|
|
63
|
+
type: 'mimeSelect',
|
|
64
|
+
description: 'Restrict the operation to files of this type. Leave as "Any" to match every file.',
|
|
65
|
+
});
|
|
66
|
+
const ORDER_BY_OPTIONS = [
|
|
67
|
+
{ value: 'modifiedTime desc', label: 'Modified time (newest first)' },
|
|
68
|
+
{ value: 'modifiedTime', label: 'Modified time (oldest first)' },
|
|
69
|
+
{ value: 'createdTime desc', label: 'Created time (newest first)' },
|
|
70
|
+
{ value: 'createdTime', label: 'Created time (oldest first)' },
|
|
71
|
+
{ value: 'name', label: 'Name (A→Z)' },
|
|
72
|
+
{ value: 'name desc', label: 'Name (Z→A)' },
|
|
73
|
+
];
|
|
74
|
+
const EXPORT_FORMAT_OPTIONS = [
|
|
75
|
+
{ value: 'application/pdf', label: 'PDF (.pdf)' },
|
|
76
|
+
{
|
|
77
|
+
value: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
78
|
+
label: 'Word (.docx) — for Google Docs',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
value: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
82
|
+
label: 'Excel (.xlsx) — for Google Sheets',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
value: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
86
|
+
label: 'PowerPoint (.pptx) — for Google Slides',
|
|
87
|
+
},
|
|
88
|
+
{ value: 'text/plain', label: 'Plain text (.txt)' },
|
|
89
|
+
{ value: 'text/csv', label: 'CSV (.csv) — for Google Sheets' },
|
|
90
|
+
{ value: 'text/html', label: 'HTML (.html)' },
|
|
91
|
+
];
|
|
92
|
+
exports.DRIVE_OPERATION_SPECS = {
|
|
93
|
+
upload: {
|
|
94
|
+
label: 'Upload File',
|
|
95
|
+
description: 'Stream a file from a payload _file ref into Drive',
|
|
96
|
+
apiMethod: 'files.create',
|
|
97
|
+
params: [
|
|
98
|
+
{
|
|
99
|
+
name: 'fileName',
|
|
100
|
+
label: 'Destination File Name',
|
|
101
|
+
type: 'template',
|
|
102
|
+
placeholder: '{{payload.name}} — leave blank to use _file.originalName',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'sourceField',
|
|
106
|
+
label: 'Source Field (payload path)',
|
|
107
|
+
type: 'template',
|
|
108
|
+
placeholder: '_file',
|
|
109
|
+
default: '_file',
|
|
110
|
+
description: 'Where the _file ref lives in the incoming payload. Default _file. For nested refs use a dotted path (e.g. doc if upstream is { doc: { _file: ... } }).',
|
|
111
|
+
},
|
|
112
|
+
mimeTypeFilter(),
|
|
113
|
+
parentFolderId(),
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
download: {
|
|
117
|
+
label: 'Download File',
|
|
118
|
+
description: 'Fetch a Drive file → payload _file ref for downstream nodes',
|
|
119
|
+
apiMethod: 'files.get (alt=media)',
|
|
120
|
+
params: [fileId()],
|
|
121
|
+
},
|
|
122
|
+
list: {
|
|
123
|
+
label: 'List / Search',
|
|
124
|
+
description: 'List or search files (iterable output)',
|
|
125
|
+
apiMethod: 'files.list',
|
|
126
|
+
params: [
|
|
127
|
+
{
|
|
128
|
+
name: 'query',
|
|
129
|
+
label: 'Search Query',
|
|
130
|
+
type: 'template',
|
|
131
|
+
placeholder: 'Substring to match against file names — empty = list all',
|
|
132
|
+
},
|
|
133
|
+
mimeTypeFilter(),
|
|
134
|
+
{
|
|
135
|
+
name: 'pageSize',
|
|
136
|
+
label: 'Max Results',
|
|
137
|
+
type: 'number',
|
|
138
|
+
min: 1,
|
|
139
|
+
max: 100,
|
|
140
|
+
default: 50,
|
|
141
|
+
description: 'Up to 100 per call. Output is iterable — downstream nodes process each file independently.',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'orderBy',
|
|
145
|
+
label: 'Order By',
|
|
146
|
+
type: 'select',
|
|
147
|
+
default: 'modifiedTime desc',
|
|
148
|
+
options: ORDER_BY_OPTIONS,
|
|
149
|
+
},
|
|
150
|
+
parentFolderId(),
|
|
151
|
+
{ name: 'recursive', label: 'Search subfolders too', type: 'switchCard', advanced: true },
|
|
152
|
+
{
|
|
153
|
+
name: 'subfolderIds',
|
|
154
|
+
label: 'Limit to specific subfolders',
|
|
155
|
+
type: 'driveSubfolders',
|
|
156
|
+
advanced: true,
|
|
157
|
+
showWhen: { field: 'recursive', in: ['true'] },
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'downloadResults',
|
|
161
|
+
label: 'Auto-download matching files',
|
|
162
|
+
type: 'switchCard',
|
|
163
|
+
advanced: true,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
get: {
|
|
168
|
+
label: 'Get Metadata',
|
|
169
|
+
description: 'Read metadata for one file (no bytes)',
|
|
170
|
+
apiMethod: 'files.get',
|
|
171
|
+
params: [fileId()],
|
|
172
|
+
},
|
|
173
|
+
copy: {
|
|
174
|
+
label: 'Copy File',
|
|
175
|
+
description: 'Duplicate a file, optionally rename and/or move',
|
|
176
|
+
apiMethod: 'files.copy',
|
|
177
|
+
params: [
|
|
178
|
+
fileId(),
|
|
179
|
+
{
|
|
180
|
+
name: 'newName',
|
|
181
|
+
label: 'New File Name',
|
|
182
|
+
type: 'template',
|
|
183
|
+
placeholder: "Optional — defaults to 'Copy of <original>'",
|
|
184
|
+
},
|
|
185
|
+
parentFolderId(),
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
move: {
|
|
189
|
+
label: 'Move File',
|
|
190
|
+
description: 'Move a file to a different folder',
|
|
191
|
+
apiMethod: 'files.update (addParents / removeParents)',
|
|
192
|
+
params: [fileId(), parentFolderId()],
|
|
193
|
+
},
|
|
194
|
+
delete: {
|
|
195
|
+
label: 'Delete (Trash)',
|
|
196
|
+
description: 'Move to trash — recoverable for 30 days',
|
|
197
|
+
apiMethod: 'files.update (trashed) / files.delete when permanent',
|
|
198
|
+
params: [
|
|
199
|
+
fileId(),
|
|
200
|
+
{
|
|
201
|
+
name: 'permanent',
|
|
202
|
+
label: 'Delete Permanently?',
|
|
203
|
+
type: 'booleanSelect',
|
|
204
|
+
options: [
|
|
205
|
+
{ value: '', label: 'No — move to trash (recoverable)' },
|
|
206
|
+
{ value: 'true', label: '⚠️ Yes — erase forever (irreversible)' },
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
share: {
|
|
212
|
+
label: 'Share File',
|
|
213
|
+
description: 'Grant viewer / commenter / editor permission',
|
|
214
|
+
apiMethod: 'permissions.create',
|
|
215
|
+
params: [
|
|
216
|
+
fileId(),
|
|
217
|
+
{
|
|
218
|
+
name: 'type',
|
|
219
|
+
label: 'Share With',
|
|
220
|
+
type: 'select',
|
|
221
|
+
default: 'user',
|
|
222
|
+
options: [
|
|
223
|
+
{ value: 'user', label: 'Specific user (email)' },
|
|
224
|
+
{ value: 'group', label: 'Group (email)' },
|
|
225
|
+
{ value: 'domain', label: 'Entire domain' },
|
|
226
|
+
{ value: 'anyone', label: '⚠️ Anyone with the link (public)' },
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: 'role',
|
|
231
|
+
label: 'Permission Role',
|
|
232
|
+
type: 'select',
|
|
233
|
+
default: 'reader',
|
|
234
|
+
options: [
|
|
235
|
+
{ value: 'reader', label: 'Viewer (read-only)' },
|
|
236
|
+
{ value: 'commenter', label: 'Commenter' },
|
|
237
|
+
{ value: 'writer', label: 'Editor' },
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'emailAddress',
|
|
242
|
+
label: 'Email Address',
|
|
243
|
+
type: 'template',
|
|
244
|
+
placeholder: 'user@example.com or {{payload.email}}',
|
|
245
|
+
showWhen: { field: 'type', in: ['user', 'group'] },
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'domain',
|
|
249
|
+
label: 'Domain',
|
|
250
|
+
type: 'template',
|
|
251
|
+
placeholder: 'example.com',
|
|
252
|
+
showWhen: { field: 'type', in: ['domain'] },
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: 'sendNotificationEmail',
|
|
256
|
+
label: 'Notify Recipient',
|
|
257
|
+
type: 'booleanSelect',
|
|
258
|
+
options: [
|
|
259
|
+
{ value: '', label: "No — don't send email" },
|
|
260
|
+
{ value: 'true', label: 'Yes — send share notification email' },
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
},
|
|
265
|
+
update: {
|
|
266
|
+
label: 'Update Metadata',
|
|
267
|
+
description: 'Rename, set description, or star/unstar',
|
|
268
|
+
apiMethod: 'files.update',
|
|
269
|
+
params: [
|
|
270
|
+
fileId(),
|
|
271
|
+
{ name: 'name', label: 'New Name', type: 'template', placeholder: '{{payload.title}}' },
|
|
272
|
+
{
|
|
273
|
+
name: 'description',
|
|
274
|
+
label: 'Description (optional)',
|
|
275
|
+
type: 'template',
|
|
276
|
+
placeholder: '{{payload.description}}',
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: 'starred',
|
|
280
|
+
label: 'Starred (optional)',
|
|
281
|
+
type: 'booleanSelect',
|
|
282
|
+
options: [
|
|
283
|
+
{ value: '', label: "— don't change —" },
|
|
284
|
+
{ value: 'true', label: '⭐ Star' },
|
|
285
|
+
{ value: 'false', label: 'Un-star' },
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
createFolder: {
|
|
291
|
+
label: 'Create Folder',
|
|
292
|
+
description: 'Create a new folder, optionally inside a parent',
|
|
293
|
+
apiMethod: 'files.create (folder mimeType)',
|
|
294
|
+
params: [
|
|
295
|
+
/* Mismo `name` que en update, con otra etiqueta y otro placeholder: por
|
|
296
|
+
eso los params son por operación y no un registro global de campos. */
|
|
297
|
+
{ name: 'name', label: 'Folder Name', type: 'template', placeholder: 'Reports' },
|
|
298
|
+
parentFolderId(),
|
|
299
|
+
],
|
|
300
|
+
},
|
|
301
|
+
export: {
|
|
302
|
+
label: 'Export Google Doc',
|
|
303
|
+
description: 'Export a Doc / Sheet / Slide to PDF / DOCX / XLSX',
|
|
304
|
+
apiMethod: 'files.export',
|
|
305
|
+
params: [
|
|
306
|
+
fileId(),
|
|
307
|
+
{
|
|
308
|
+
name: 'mimeType',
|
|
309
|
+
label: 'Export Format',
|
|
310
|
+
type: 'select',
|
|
311
|
+
default: 'application/pdf',
|
|
312
|
+
options: EXPORT_FORMAT_OPTIONS,
|
|
313
|
+
description: 'Export only works on Google native docs (Docs, Sheets, Slides, Drawings). For regular files use Download instead.',
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
getPermissions: {
|
|
318
|
+
label: 'List Permissions',
|
|
319
|
+
description: 'List who has access (audit before changing)',
|
|
320
|
+
apiMethod: 'permissions.list',
|
|
321
|
+
params: [fileId()],
|
|
322
|
+
},
|
|
323
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drive AI Toolkit — las herramientas que un driveAction expone cuando
|
|
3
|
+
* `aiEnabled` está encendido.
|
|
4
|
+
*
|
|
5
|
+
* **Se escribe una sola vez**, como el de Sheets y el de Telegram. Antes vivía
|
|
6
|
+
* TRES veces, y las tres a mano:
|
|
7
|
+
*
|
|
8
|
+
* 1. `dashboard/components/drive-actions/drive-operations-schemas.ts`
|
|
9
|
+
* 2. `api/src/mcp-servers/toolkit-specs.ts`
|
|
10
|
+
* 3. la página de detalle, que pintaba las doce herramientas como doce
|
|
11
|
+
* `<li>` escritos a mano, con el número `12` también a mano en tres sitios
|
|
12
|
+
*
|
|
13
|
+
* La 1 y la 2 no habían derivado todavía (medido el 2026-08-10: cero
|
|
14
|
+
* diferencias, al contrario que Slack con 13 y WhatsApp con 4). Esto es lo que
|
|
15
|
+
* hace que sigan sin derivar.
|
|
16
|
+
*
|
|
17
|
+
* El ORDEN de este array importa: `AiToolkitPreview` pinta el encabezado de
|
|
18
|
+
* grupo sólo cuando cambia respecto a la fila anterior, así que las
|
|
19
|
+
* herramientas del mismo grupo tienen que ir seguidas. Los tres grupos son los
|
|
20
|
+
* que ya inventaba el JSX que sustituye.
|
|
21
|
+
*/
|
|
22
|
+
import type { DriveOperation } from './drive-operations';
|
|
23
|
+
export interface DriveToolkitParameter {
|
|
24
|
+
name: string;
|
|
25
|
+
type: 'string' | 'number' | 'boolean';
|
|
26
|
+
description: string;
|
|
27
|
+
required: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface DriveToolkitSpec {
|
|
30
|
+
operation: DriveOperation;
|
|
31
|
+
/** Etiqueta corta de la fila en la lista de herramientas. */
|
|
32
|
+
label: string;
|
|
33
|
+
/**
|
|
34
|
+
* Nombre con el que el LLM llama a la herramienta. Todas llevan `_drive_` o
|
|
35
|
+
* `_to_drive` para no chocar con las de Gmail, Sheets o Calendar cuando un
|
|
36
|
+
* mismo AI Node tiene varios toolkits encendidos.
|
|
37
|
+
*/
|
|
38
|
+
toolName: string;
|
|
39
|
+
/** Descripción (más reglas de uso) que ve el LLM. */
|
|
40
|
+
description: string;
|
|
41
|
+
parameters: DriveToolkitParameter[];
|
|
42
|
+
/** Encabezado de grupo de la página de detalle. El orden es el que agrupa. */
|
|
43
|
+
group?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Marca que la herramienta cambia algo difícil de deshacer, para que la página
|
|
46
|
+
* lo enseñe y el AI Node pida confirmación. Son **dos**, las mismas que ya
|
|
47
|
+
* estaban marcadas: `delete_drive_file` y `share_drive_file`. `move` no lo
|
|
48
|
+
* lleva, y es defendible — deshacerlo es otro move.
|
|
49
|
+
*/
|
|
50
|
+
destructive?: boolean;
|
|
51
|
+
}
|
|
52
|
+
export declare const DRIVE_TOOLKIT_SPECS: DriveToolkitSpec[];
|
|
53
|
+
/** Índice por nombre de herramienta, para enrutar un `tools/call` sin recorrer. */
|
|
54
|
+
export declare const DRIVE_TOOLKIT_BY_TOOL_NAME: Record<string, DriveToolkitSpec>;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Drive AI Toolkit — las herramientas que un driveAction expone cuando
|
|
4
|
+
* `aiEnabled` está encendido.
|
|
5
|
+
*
|
|
6
|
+
* **Se escribe una sola vez**, como el de Sheets y el de Telegram. Antes vivía
|
|
7
|
+
* TRES veces, y las tres a mano:
|
|
8
|
+
*
|
|
9
|
+
* 1. `dashboard/components/drive-actions/drive-operations-schemas.ts`
|
|
10
|
+
* 2. `api/src/mcp-servers/toolkit-specs.ts`
|
|
11
|
+
* 3. la página de detalle, que pintaba las doce herramientas como doce
|
|
12
|
+
* `<li>` escritos a mano, con el número `12` también a mano en tres sitios
|
|
13
|
+
*
|
|
14
|
+
* La 1 y la 2 no habían derivado todavía (medido el 2026-08-10: cero
|
|
15
|
+
* diferencias, al contrario que Slack con 13 y WhatsApp con 4). Esto es lo que
|
|
16
|
+
* hace que sigan sin derivar.
|
|
17
|
+
*
|
|
18
|
+
* El ORDEN de este array importa: `AiToolkitPreview` pinta el encabezado de
|
|
19
|
+
* grupo sólo cuando cambia respecto a la fila anterior, así que las
|
|
20
|
+
* herramientas del mismo grupo tienen que ir seguidas. Los tres grupos son los
|
|
21
|
+
* que ya inventaba el JSX que sustituye.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.DRIVE_TOOLKIT_BY_TOOL_NAME = exports.DRIVE_TOOLKIT_SPECS = void 0;
|
|
25
|
+
const p = (name, description, required = true, type = 'string') => ({ name, type, description, required });
|
|
26
|
+
const ENTRAN_Y_SALEN = 'Files in / out';
|
|
27
|
+
const DESCUBRIR = 'Discovery';
|
|
28
|
+
const MUTACIONES = 'Mutations (LLM confirms first)';
|
|
29
|
+
exports.DRIVE_TOOLKIT_SPECS = [
|
|
30
|
+
/* ── Files in / out ─────────────────────────────────────────────────── */
|
|
31
|
+
{
|
|
32
|
+
operation: 'upload',
|
|
33
|
+
label: 'Upload file',
|
|
34
|
+
toolName: 'upload_to_drive',
|
|
35
|
+
group: ENTRAN_Y_SALEN,
|
|
36
|
+
description: 'Upload a file from the current event payload into Google Drive. Requires an `_file` ref in the payload (created by an upstream node — multipart ingress, File Transform, or another Drive download). Streams from R2; never holds the full file in memory.',
|
|
37
|
+
parameters: [
|
|
38
|
+
p('fileName', 'Optional. Override the destination file name. Defaults to the _file ref originalName.', false),
|
|
39
|
+
p('sourceField', 'Optional. Payload field path holding the _file ref. Defaults to "_file".', false),
|
|
40
|
+
p('parentFolderId', 'Optional. Drive folder ID to upload into. Empty = root.', false),
|
|
41
|
+
p('mimeType', 'Optional. Override the MIME type. Defaults to the _file ref mimeType.', false),
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
operation: 'download',
|
|
46
|
+
label: 'Download file',
|
|
47
|
+
toolName: 'download_from_drive',
|
|
48
|
+
group: ENTRAN_Y_SALEN,
|
|
49
|
+
description: 'Download a Drive file to HostWebhook storage and return a `_file` ref the next pipeline node can consume (FileTransform, AI vision, HTTP body). Use the file ID from list_drive_files / search.',
|
|
50
|
+
parameters: [p('fileId', 'The Drive file ID to download.')],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
operation: 'export',
|
|
54
|
+
label: 'Export Google doc',
|
|
55
|
+
toolName: 'export_drive_file',
|
|
56
|
+
group: ENTRAN_Y_SALEN,
|
|
57
|
+
description: 'Export a Google native document (Doc / Sheet / Slide / Drawing) to a downloadable format like PDF or DOCX, returning a `_file` ref the next pipeline node can consume. Does NOT work on regular files (those use download_from_drive).',
|
|
58
|
+
parameters: [
|
|
59
|
+
p('fileId', 'The Drive file ID (must be a Google native doc).'),
|
|
60
|
+
/* De las dos copias me quedo con la de la api, que es la que dice qué
|
|
61
|
+
formato de origen va a cada destino. La del dashboard se había quedado
|
|
62
|
+
sin esa parte. Es la única deriva que tenían Drive y, como estaba en la
|
|
63
|
+
descripción de un PARÁMETRO, ningún conteo la miraba. */
|
|
64
|
+
p('mimeType', 'Target MIME type. Common: "application/pdf" (Doc/Slide → PDF), "application/vnd.openxmlformats-officedocument.wordprocessingml.document" (Doc → DOCX), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" (Sheet → XLSX), "text/plain", "text/csv".'),
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
/* ── Discovery ──────────────────────────────────────────────────────── */
|
|
68
|
+
{
|
|
69
|
+
operation: 'list',
|
|
70
|
+
label: 'List / search files',
|
|
71
|
+
toolName: 'list_drive_files',
|
|
72
|
+
group: DESCUBRIR,
|
|
73
|
+
description: "Search the user's Drive (or shared drive). Returns up to N matching files with id, name, mimeType, size, modifiedTime. Use this BEFORE any operation that needs a fileId — never invent IDs.",
|
|
74
|
+
parameters: [
|
|
75
|
+
p('query', 'Optional. Substring to match against file names.', false),
|
|
76
|
+
p('parentFolderId', 'Optional. Restrict search to a single folder.', false),
|
|
77
|
+
p('mimeType', 'Optional. Filter by MIME type (e.g. "application/pdf", "application/vnd.google-apps.folder").', false),
|
|
78
|
+
p('pageSize', 'Optional. Max results (default 50, max 100).', false, 'number'),
|
|
79
|
+
p('orderBy', 'Optional. Drive ordering string (default "modifiedTime desc").', false),
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
operation: 'get',
|
|
84
|
+
label: 'Get file metadata',
|
|
85
|
+
toolName: 'get_drive_file',
|
|
86
|
+
group: DESCUBRIR,
|
|
87
|
+
description: 'Read full metadata for one Drive file (name, mimeType, size, parents, owners, starred, trashed). Does NOT download the bytes — use download_from_drive for that.',
|
|
88
|
+
parameters: [p('fileId', 'The Drive file ID.')],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
operation: 'getPermissions',
|
|
92
|
+
label: 'List file permissions',
|
|
93
|
+
toolName: 'get_drive_permissions',
|
|
94
|
+
group: DESCUBRIR,
|
|
95
|
+
description: 'List who has access to a Drive file (id, type, role, emailAddress/domain, displayName). Use to audit sharing before changing permissions.',
|
|
96
|
+
parameters: [p('fileId', 'The Drive file ID.')],
|
|
97
|
+
},
|
|
98
|
+
/* ── Mutations ──────────────────────────────────────────────────────── */
|
|
99
|
+
{
|
|
100
|
+
operation: 'copy',
|
|
101
|
+
label: 'Copy file',
|
|
102
|
+
toolName: 'copy_drive_file',
|
|
103
|
+
group: MUTACIONES,
|
|
104
|
+
description: 'Duplicate a Drive file. Optionally rename and/or move into a different folder in one step.',
|
|
105
|
+
parameters: [
|
|
106
|
+
p('fileId', 'The source Drive file ID.'),
|
|
107
|
+
p('newName', 'Optional. Name for the copy. Defaults to "Copy of <original>".', false),
|
|
108
|
+
p('parentFolderId', 'Optional. Folder ID to place the copy into. Defaults to the source folder.', false),
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
operation: 'move',
|
|
113
|
+
label: 'Move file',
|
|
114
|
+
toolName: 'move_drive_file',
|
|
115
|
+
group: MUTACIONES,
|
|
116
|
+
description: 'Move a Drive file to a different folder. The current parents are removed; the file is added to the new parent.',
|
|
117
|
+
parameters: [
|
|
118
|
+
p('fileId', 'The Drive file ID to move.'),
|
|
119
|
+
p('parentFolderId', 'Destination folder ID.'),
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
operation: 'update',
|
|
124
|
+
label: 'Update file metadata',
|
|
125
|
+
toolName: 'update_drive_file',
|
|
126
|
+
group: MUTACIONES,
|
|
127
|
+
description: 'Rename a Drive file or change its description / starred status. Use list_drive_files first to find the fileId.',
|
|
128
|
+
parameters: [
|
|
129
|
+
p('fileId', 'The Drive file ID.'),
|
|
130
|
+
p('name', 'Optional. New name.', false),
|
|
131
|
+
p('description', 'Optional. New description.', false),
|
|
132
|
+
p('starred', 'Optional. Star/unstar.', false, 'boolean'),
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
operation: 'createFolder',
|
|
137
|
+
label: 'Create folder',
|
|
138
|
+
toolName: 'create_drive_folder',
|
|
139
|
+
group: MUTACIONES,
|
|
140
|
+
description: 'Create a new folder in Drive. Use this BEFORE upload_to_drive when the user asks to put a file in a specific named folder that does not yet exist.',
|
|
141
|
+
parameters: [
|
|
142
|
+
p('name', 'Folder name.'),
|
|
143
|
+
p('parentFolderId', 'Optional. Parent folder ID. Empty = root.', false),
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
operation: 'delete',
|
|
148
|
+
label: 'Delete file (trash)',
|
|
149
|
+
toolName: 'delete_drive_file',
|
|
150
|
+
group: MUTACIONES,
|
|
151
|
+
description: 'Move a Drive file to trash (recoverable for 30 days). Set permanent=true ONLY when the user explicitly confirms a permanent delete — irreversible. ALWAYS confirm with the user before calling this with permanent=true.',
|
|
152
|
+
parameters: [
|
|
153
|
+
p('fileId', 'The Drive file ID to delete.'),
|
|
154
|
+
p('permanent', 'Optional. true = bypass trash and erase forever (irreversible). Default false.', false, 'boolean'),
|
|
155
|
+
],
|
|
156
|
+
destructive: true,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
operation: 'share',
|
|
160
|
+
label: 'Share file',
|
|
161
|
+
toolName: 'share_drive_file',
|
|
162
|
+
group: MUTACIONES,
|
|
163
|
+
description: 'Grant a permission on a Drive file. CONFIRM with the user before calling this — sharing externally is sensitive. Public sharing (type="anyone") is especially dangerous; only use when the user explicitly says "make it public" or similar.',
|
|
164
|
+
parameters: [
|
|
165
|
+
p('fileId', 'The Drive file ID to share.'),
|
|
166
|
+
p('type', 'One of: "user" | "group" | "domain" | "anyone". Use "anyone" only with explicit user confirmation.'),
|
|
167
|
+
p('role', 'One of: "reader" | "commenter" | "writer".'),
|
|
168
|
+
p('emailAddress', 'Required when type=user|group.', false),
|
|
169
|
+
p('domain', 'Required when type=domain.', false),
|
|
170
|
+
p('sendNotificationEmail', 'Optional. true = email the recipient about the share.', false, 'boolean'),
|
|
171
|
+
],
|
|
172
|
+
destructive: true,
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
/** Índice por nombre de herramienta, para enrutar un `tools/call` sin recorrer. */
|
|
176
|
+
exports.DRIVE_TOOLKIT_BY_TOOL_NAME = Object.fromEntries(exports.DRIVE_TOOLKIT_SPECS.map((s) => [s.toolName, s]));
|
package/dist/index.d.ts
CHANGED
|
@@ -9,10 +9,14 @@ export { GMAIL_OPERATIONS, isGmailOperation, } from './gmail-operations';
|
|
|
9
9
|
export type { GmailOperation } from './gmail-operations';
|
|
10
10
|
export { GOOGLE_CALENDAR_OPERATIONS, isGoogleCalendarOperation, } from './calendar-operations';
|
|
11
11
|
export type { GoogleCalendarOperation } from './calendar-operations';
|
|
12
|
-
export { DRIVE_OPERATIONS, isDriveOperation, } from './drive-operations';
|
|
13
|
-
export type { DriveOperation } from './drive-operations';
|
|
14
|
-
export {
|
|
15
|
-
export type {
|
|
12
|
+
export { DRIVE_OPERATIONS, DRIVE_OPERATION_SPECS, isDriveOperation, } from './drive-operations';
|
|
13
|
+
export type { DriveOperation, DriveParamType, DriveParamSpec, DriveOperationSpec, } from './drive-operations';
|
|
14
|
+
export { DRIVE_TOOLKIT_SPECS, DRIVE_TOOLKIT_BY_TOOL_NAME, } from './drive-toolkit';
|
|
15
|
+
export type { DriveToolkitSpec, DriveToolkitParameter, } from './drive-toolkit';
|
|
16
|
+
export { TELEGRAM_OPERATIONS, TELEGRAM_OPERATION_SPECS, isTelegramOperation, } from './telegram-operations';
|
|
17
|
+
export type { TelegramOperation, TelegramParamSpec, TelegramOperationSpec, } from './telegram-operations';
|
|
18
|
+
export { TELEGRAM_TOOLKIT_SPECS, TELEGRAM_TOOLKIT_BY_TOOL_NAME, } from './telegram-toolkit';
|
|
19
|
+
export type { TelegramToolkitSpec, TelegramToolkitParameter, } from './telegram-toolkit';
|
|
16
20
|
export { WHATSAPP_OPERATIONS, isWhatsAppOperation, } from './whatsapp-operations';
|
|
17
21
|
export type { WhatsAppOperation } from './whatsapp-operations';
|
|
18
22
|
export { DISCORD_OPERATIONS, DISCORD_OPERATION_SPECS, isDiscordOperation, } from './discord-operations';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.isCredentialType = exports.getCredentialType = exports.credentialTypeValues = exports.CREDENTIAL_TYPE_VALUES = exports.CREDENTIAL_TYPES = exports.isGoogleContactsOperation = void 0;
|
|
3
|
+
exports.SHEETS_TOOLKIT_DEFAULTABLE = exports.SHEETS_TOOLKIT_BY_TOOL_NAME = exports.SHEETS_TOOLKIT_SPECS = exports.isSheetsOperation = exports.SHEETS_OPERATION_SPECS = exports.SHEETS_OPERATIONS = exports.isSlackOperation = exports.SLACK_OPERATION_SPECS = exports.SLACK_OPERATIONS = exports.isDiscordOperation = exports.DISCORD_OPERATION_SPECS = exports.DISCORD_OPERATIONS = exports.isWhatsAppOperation = exports.WHATSAPP_OPERATIONS = exports.TELEGRAM_TOOLKIT_BY_TOOL_NAME = exports.TELEGRAM_TOOLKIT_SPECS = exports.isTelegramOperation = exports.TELEGRAM_OPERATION_SPECS = exports.TELEGRAM_OPERATIONS = exports.DRIVE_TOOLKIT_BY_TOOL_NAME = exports.DRIVE_TOOLKIT_SPECS = exports.isDriveOperation = exports.DRIVE_OPERATION_SPECS = exports.DRIVE_OPERATIONS = exports.isGoogleCalendarOperation = exports.GOOGLE_CALENDAR_OPERATIONS = exports.isGmailOperation = exports.GMAIL_OPERATIONS = exports.NODE_TYPE_TO_PREFIX = exports.PREFIX_TO_NODE_TYPE = exports.NODE_STATE_KEYS = exports.NODE_COLORS = exports.NODE_DETAIL_PATHS = exports.getNodeRegistryEntry = exports.NODE_REGISTRY = exports.getNodeDispatchConfig = exports.getAllNodeCollections = exports.NODE_DISPATCH = exports.resolveNodeId = exports.PREFIX_TO_TYPE = exports.NODE_UI = exports.ALL_NODE_TYPES = exports.isNodeType = exports.isTerminal = exports.canSendToNodes = exports.canReceiveFromNodes = exports.canReceiveFrom = exports.NODE_CONNECTIONS = exports.iterableMeta = exports.singleMeta = void 0;
|
|
4
|
+
exports.isCredentialType = exports.getCredentialType = exports.credentialTypeValues = exports.CREDENTIAL_TYPE_VALUES = exports.CREDENTIAL_TYPES = exports.isGoogleContactsOperation = exports.GOOGLE_CONTACTS_DEFAULT_PERSON_FIELDS = exports.GOOGLE_CONTACTS_OPERATION_GROUPS = exports.GOOGLE_CONTACTS_OPERATION_SPECS = exports.GOOGLE_CONTACTS_OPERATIONS_V2 = exports.GOOGLE_CONTACTS_OPERATIONS_V1 = exports.GOOGLE_CONTACTS_OPERATIONS = void 0;
|
|
5
5
|
var types_1 = require("./types");
|
|
6
6
|
Object.defineProperty(exports, "singleMeta", { enumerable: true, get: function () { return types_1.singleMeta; } });
|
|
7
7
|
Object.defineProperty(exports, "iterableMeta", { enumerable: true, get: function () { return types_1.iterableMeta; } });
|
|
@@ -41,10 +41,18 @@ Object.defineProperty(exports, "GOOGLE_CALENDAR_OPERATIONS", { enumerable: true,
|
|
|
41
41
|
Object.defineProperty(exports, "isGoogleCalendarOperation", { enumerable: true, get: function () { return calendar_operations_1.isGoogleCalendarOperation; } });
|
|
42
42
|
var drive_operations_1 = require("./drive-operations");
|
|
43
43
|
Object.defineProperty(exports, "DRIVE_OPERATIONS", { enumerable: true, get: function () { return drive_operations_1.DRIVE_OPERATIONS; } });
|
|
44
|
+
Object.defineProperty(exports, "DRIVE_OPERATION_SPECS", { enumerable: true, get: function () { return drive_operations_1.DRIVE_OPERATION_SPECS; } });
|
|
44
45
|
Object.defineProperty(exports, "isDriveOperation", { enumerable: true, get: function () { return drive_operations_1.isDriveOperation; } });
|
|
46
|
+
var drive_toolkit_1 = require("./drive-toolkit");
|
|
47
|
+
Object.defineProperty(exports, "DRIVE_TOOLKIT_SPECS", { enumerable: true, get: function () { return drive_toolkit_1.DRIVE_TOOLKIT_SPECS; } });
|
|
48
|
+
Object.defineProperty(exports, "DRIVE_TOOLKIT_BY_TOOL_NAME", { enumerable: true, get: function () { return drive_toolkit_1.DRIVE_TOOLKIT_BY_TOOL_NAME; } });
|
|
45
49
|
var telegram_operations_1 = require("./telegram-operations");
|
|
46
50
|
Object.defineProperty(exports, "TELEGRAM_OPERATIONS", { enumerable: true, get: function () { return telegram_operations_1.TELEGRAM_OPERATIONS; } });
|
|
51
|
+
Object.defineProperty(exports, "TELEGRAM_OPERATION_SPECS", { enumerable: true, get: function () { return telegram_operations_1.TELEGRAM_OPERATION_SPECS; } });
|
|
47
52
|
Object.defineProperty(exports, "isTelegramOperation", { enumerable: true, get: function () { return telegram_operations_1.isTelegramOperation; } });
|
|
53
|
+
var telegram_toolkit_1 = require("./telegram-toolkit");
|
|
54
|
+
Object.defineProperty(exports, "TELEGRAM_TOOLKIT_SPECS", { enumerable: true, get: function () { return telegram_toolkit_1.TELEGRAM_TOOLKIT_SPECS; } });
|
|
55
|
+
Object.defineProperty(exports, "TELEGRAM_TOOLKIT_BY_TOOL_NAME", { enumerable: true, get: function () { return telegram_toolkit_1.TELEGRAM_TOOLKIT_BY_TOOL_NAME; } });
|
|
48
56
|
var whatsapp_operations_1 = require("./whatsapp-operations");
|
|
49
57
|
Object.defineProperty(exports, "WHATSAPP_OPERATIONS", { enumerable: true, get: function () { return whatsapp_operations_1.WHATSAPP_OPERATIONS; } });
|
|
50
58
|
Object.defineProperty(exports, "isWhatsAppOperation", { enumerable: true, get: function () { return whatsapp_operations_1.isWhatsAppOperation; } });
|
|
@@ -15,3 +15,90 @@ export declare const TELEGRAM_OPERATIONS: readonly ["sendMessage", "replyToMessa
|
|
|
15
15
|
export type TelegramOperation = (typeof TELEGRAM_OPERATIONS)[number];
|
|
16
16
|
/** Type guard — useful when validating untrusted input (DTOs, tool calls). */
|
|
17
17
|
export declare function isTelegramOperation(value: unknown): value is TelegramOperation;
|
|
18
|
+
/**
|
|
19
|
+
* Per-operation form schema, same idea as SLACK_OPERATION_SPECS and
|
|
20
|
+
* DISCORD_OPERATION_SPECS: the detail page renders it with one loop instead of
|
|
21
|
+
* a branch per operation. It replaces 12 branches and a local `OPERATIONS`
|
|
22
|
+
* array of 11 entries whose count had already drifted from the truth — the
|
|
23
|
+
* mode chip read "Choose 1 of 10 ops" while the page painted 11 cards.
|
|
24
|
+
*
|
|
25
|
+
* Two things about this node's schema are worth knowing before writing a
|
|
26
|
+
* renderer for it:
|
|
27
|
+
*
|
|
28
|
+
* 1. **`chatId` is per-operation, not global.** The page used to hoist one
|
|
29
|
+
* shared field to the top of every form, but `answerCallbackQuery` has no
|
|
30
|
+
* chat at all (it answers a callback id) and `forwardMessage` needs two
|
|
31
|
+
* chats with their own labels — "Destination" and "Source". So it is
|
|
32
|
+
* declared op by op, and where it repeats it repeats on purpose.
|
|
33
|
+
* 2. **Three booleans default to ON.** See the note on `default` below; a
|
|
34
|
+
* renderer that coerces with `!!config[name]` silently turns them off.
|
|
35
|
+
*/
|
|
36
|
+
export interface TelegramParamSpec {
|
|
37
|
+
/** Field key — the property name inside `operationConfig`. */
|
|
38
|
+
name: string;
|
|
39
|
+
/** UI label shown above the control. */
|
|
40
|
+
label: string;
|
|
41
|
+
/**
|
|
42
|
+
* Param type for form rendering.
|
|
43
|
+
*
|
|
44
|
+
* - `string` — one-line input with `{{payload.x}}` templating.
|
|
45
|
+
* - `text` — the same input, multiline; `rows` sets its height.
|
|
46
|
+
* - `select` — fixed option list (only `parseMode` today).
|
|
47
|
+
* - `boolean` — a Switch. Read it as `config[name] ?? default`, never as
|
|
48
|
+
* `!!config[name]`: see `default`.
|
|
49
|
+
* - `number` — numeric input clamped to `min`/`max` as it is typed.
|
|
50
|
+
* - `linesList` — **stored as an array of strings, edited as one per
|
|
51
|
+
* line.** Telegram's poll options. The cousin of Contacts' `csvList`,
|
|
52
|
+
* split on newlines instead of commas. A renderer that treats it as text
|
|
53
|
+
* turns the array into a string on the first save and `sendPoll` starts
|
|
54
|
+
* failing.
|
|
55
|
+
*/
|
|
56
|
+
type: 'string' | 'text' | 'select' | 'boolean' | 'number' | 'linesList';
|
|
57
|
+
/** Required by the operation. Metadata — the form does not gate on it. */
|
|
58
|
+
required?: boolean;
|
|
59
|
+
/** Help text under the control. Absent where the form shows none. */
|
|
60
|
+
description?: string;
|
|
61
|
+
/** Hint shown inside the input. May contain newlines for `linesList`. */
|
|
62
|
+
placeholder?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Value to show when the stored one is `undefined`.
|
|
65
|
+
*
|
|
66
|
+
* `includeDisapprove`, `isAnonymous` and `parseMode` all have one, and the
|
|
67
|
+
* first two default to **true**: the stored config only ever holds `false`
|
|
68
|
+
* when the user turned them off. `config[name] ?? default` reproduces that
|
|
69
|
+
* exactly; `!!config[name]` does not, and getting it wrong strips the
|
|
70
|
+
* Disapprove button off every approval flow that already exists.
|
|
71
|
+
*/
|
|
72
|
+
default?: string | number | boolean;
|
|
73
|
+
/** `select` only — the fixed option list, in display order. */
|
|
74
|
+
options?: string[];
|
|
75
|
+
/** `number` only — the input clamps to these while typing. */
|
|
76
|
+
min?: number;
|
|
77
|
+
max?: number;
|
|
78
|
+
/** `text` only — height of the textarea. */
|
|
79
|
+
rows?: number;
|
|
80
|
+
/**
|
|
81
|
+
* Renders side by side with the next field in a two-column row. Only the
|
|
82
|
+
* Approve / Disapprove button labels use it — they are one decision split
|
|
83
|
+
* in two, and stacking them reads as two unrelated fields.
|
|
84
|
+
*/
|
|
85
|
+
pairedWithNext?: boolean;
|
|
86
|
+
}
|
|
87
|
+
export interface TelegramOperationSpec {
|
|
88
|
+
/** UI label for the operation picker card. */
|
|
89
|
+
label: string;
|
|
90
|
+
/** Sub-line under the label in the picker. */
|
|
91
|
+
description: string;
|
|
92
|
+
/** Bot API method the operation maps to (core.telegram.org/bots/api). */
|
|
93
|
+
apiMethod: string;
|
|
94
|
+
/**
|
|
95
|
+
* The op accepts `reply_markup`, so the page offers the inline-keyboard
|
|
96
|
+
* builder for it. Used to live as a hardcoded array of five strings in the
|
|
97
|
+
* detail page. `sendAndWaitForResponse` is deliberately absent: it builds
|
|
98
|
+
* its own Approve / Disapprove keyboard and a second one would collide.
|
|
99
|
+
*/
|
|
100
|
+
supportsInlineKeyboard?: boolean;
|
|
101
|
+
/** Parameter schema. Order matters for form rendering. */
|
|
102
|
+
params: TelegramParamSpec[];
|
|
103
|
+
}
|
|
104
|
+
export declare const TELEGRAM_OPERATION_SPECS: Record<TelegramOperation, TelegramOperationSpec>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TELEGRAM_OPERATIONS = void 0;
|
|
3
|
+
exports.TELEGRAM_OPERATION_SPECS = exports.TELEGRAM_OPERATIONS = void 0;
|
|
4
4
|
exports.isTelegramOperation = isTelegramOperation;
|
|
5
5
|
/**
|
|
6
6
|
* Telegram Bot API operation enum — single source of truth across the API,
|
|
@@ -43,3 +43,313 @@ function isTelegramOperation(value) {
|
|
|
43
43
|
return (typeof value === 'string' &&
|
|
44
44
|
exports.TELEGRAM_OPERATIONS.includes(value));
|
|
45
45
|
}
|
|
46
|
+
/* Los campos que se repiten entre operaciones. Fábricas y no constantes
|
|
47
|
+
compartidas: cada op recibe su propia copia y puede cambiarle la etiqueta o
|
|
48
|
+
la ayuda sin tocar a las demás. */
|
|
49
|
+
const chatId = () => ({
|
|
50
|
+
name: 'chatId',
|
|
51
|
+
label: 'Chat ID',
|
|
52
|
+
type: 'string',
|
|
53
|
+
required: true,
|
|
54
|
+
description: 'Numeric chat id (from a Telegram trigger payload — usually {{payload.chatId}}) or @channelusername.',
|
|
55
|
+
placeholder: '{{payload.chatId}}',
|
|
56
|
+
});
|
|
57
|
+
const parseMode = (description) => ({
|
|
58
|
+
name: 'parseMode',
|
|
59
|
+
label: 'Parse mode',
|
|
60
|
+
type: 'select',
|
|
61
|
+
options: ['HTML', 'MarkdownV2', 'none'],
|
|
62
|
+
// Se enseña como VALOR, no como placeholder: el campo sale relleno con HTML
|
|
63
|
+
// aunque nunca se haya guardado nada.
|
|
64
|
+
default: 'HTML',
|
|
65
|
+
...(description ? { description } : {}),
|
|
66
|
+
});
|
|
67
|
+
const messageId = (label, description, placeholder = '{{payload.messageId}}') => ({
|
|
68
|
+
name: 'messageId',
|
|
69
|
+
label,
|
|
70
|
+
type: 'string',
|
|
71
|
+
required: true,
|
|
72
|
+
placeholder,
|
|
73
|
+
...(description ? { description } : {}),
|
|
74
|
+
});
|
|
75
|
+
const caption = (description, rows) => ({
|
|
76
|
+
name: 'caption',
|
|
77
|
+
label: 'Caption',
|
|
78
|
+
type: 'text',
|
|
79
|
+
rows,
|
|
80
|
+
description,
|
|
81
|
+
placeholder: rows > 2 ? 'Optional caption' : 'Optional',
|
|
82
|
+
});
|
|
83
|
+
exports.TELEGRAM_OPERATION_SPECS = {
|
|
84
|
+
sendMessage: {
|
|
85
|
+
label: 'Send message',
|
|
86
|
+
description: 'Text reply with optional inline keyboard',
|
|
87
|
+
apiMethod: 'POST /sendMessage',
|
|
88
|
+
supportsInlineKeyboard: true,
|
|
89
|
+
params: [
|
|
90
|
+
chatId(),
|
|
91
|
+
{
|
|
92
|
+
name: 'text',
|
|
93
|
+
label: 'Text',
|
|
94
|
+
type: 'text',
|
|
95
|
+
required: true,
|
|
96
|
+
rows: 4,
|
|
97
|
+
description: 'Message body. HTML by default — escape <, >, & in user-supplied content.',
|
|
98
|
+
placeholder: 'Hi {{payload.fromUsername}}, thanks for your message!',
|
|
99
|
+
},
|
|
100
|
+
parseMode('HTML / MarkdownV2 / none.'),
|
|
101
|
+
{
|
|
102
|
+
name: 'replyToMessageId',
|
|
103
|
+
label: 'Reply to message ID',
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: "Optional. Numeric id of the message you're replying to. For a fully-required quote-reply, use the dedicated 'Reply to message' op instead.",
|
|
106
|
+
placeholder: '{{payload.messageId}}',
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
replyToMessage: {
|
|
111
|
+
label: 'Reply to message',
|
|
112
|
+
description: "Quote-reply with the threaded indicator above the bot's message",
|
|
113
|
+
apiMethod: 'POST /sendMessage (reply_to_message_id)',
|
|
114
|
+
supportsInlineKeyboard: true,
|
|
115
|
+
params: [
|
|
116
|
+
chatId(),
|
|
117
|
+
messageId('Message ID to reply to', "Numeric id of the user message you're quote-replying to. From a Telegram trigger this is typically {{payload.message.message_id}}.", '{{payload.message.message_id}}'),
|
|
118
|
+
{
|
|
119
|
+
name: 'text',
|
|
120
|
+
label: 'Text',
|
|
121
|
+
type: 'text',
|
|
122
|
+
required: true,
|
|
123
|
+
rows: 4,
|
|
124
|
+
description: 'Reply body. HTML by default — escape <, >, & in user-supplied content.',
|
|
125
|
+
placeholder: 'Yes, I understood your question about {{payload.text}}',
|
|
126
|
+
},
|
|
127
|
+
parseMode('HTML / MarkdownV2 / none.'),
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
sendPhoto: {
|
|
131
|
+
label: 'Send photo',
|
|
132
|
+
description: 'Image by URL or _file ref',
|
|
133
|
+
apiMethod: 'POST /sendPhoto',
|
|
134
|
+
supportsInlineKeyboard: true,
|
|
135
|
+
params: [
|
|
136
|
+
chatId(),
|
|
137
|
+
{
|
|
138
|
+
name: 'photo',
|
|
139
|
+
label: 'Photo URL or file_id',
|
|
140
|
+
type: 'string',
|
|
141
|
+
required: true,
|
|
142
|
+
description: 'Public URL Telegram can fetch, or a file_id from a prior send.',
|
|
143
|
+
placeholder: '{{payload.attachment._file.downloadUrl}}',
|
|
144
|
+
},
|
|
145
|
+
caption('Optional. HTML supported. Max 1024 chars.', 3),
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
sendDocument: {
|
|
149
|
+
label: 'Send document',
|
|
150
|
+
description: 'File of any type by URL',
|
|
151
|
+
apiMethod: 'POST /sendDocument',
|
|
152
|
+
supportsInlineKeyboard: true,
|
|
153
|
+
params: [
|
|
154
|
+
chatId(),
|
|
155
|
+
{
|
|
156
|
+
name: 'document',
|
|
157
|
+
label: 'Document URL or file_id',
|
|
158
|
+
type: 'string',
|
|
159
|
+
required: true,
|
|
160
|
+
description: 'Public URL or file_id. Max 50MB when fetching by URL.',
|
|
161
|
+
placeholder: '{{payload.file._file.downloadUrl}}',
|
|
162
|
+
},
|
|
163
|
+
caption('Optional. HTML supported.', 2),
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
sendVoice: {
|
|
167
|
+
label: 'Send voice',
|
|
168
|
+
description: 'Send a voice bubble — pass a URL / file_id (or chain from the AI Node synthesize_voice tool)',
|
|
169
|
+
apiMethod: 'POST /sendVoice',
|
|
170
|
+
params: [
|
|
171
|
+
chatId(),
|
|
172
|
+
{
|
|
173
|
+
name: 'voice',
|
|
174
|
+
label: 'Voice URL or file_id',
|
|
175
|
+
type: 'string',
|
|
176
|
+
required: true,
|
|
177
|
+
description: 'OGG/OPUS audio URL Telegram can fetch, or a file_id from a prior /sendVoice for cheap re-sends. To synthesize from text, add the AI Node `synthesize_voice` tool (Tools tab → + Add → Voice synthesis) and pass its result here via `{{payload.voice}}` or chain through the LLM.',
|
|
178
|
+
placeholder: 'https://… .ogg or {{payload.voice}}',
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
},
|
|
182
|
+
sendAndWaitForResponse: {
|
|
183
|
+
label: 'Send & wait for response',
|
|
184
|
+
description: 'Sends a message with Approve / Disapprove buttons (and optional free-text reply) — pipeline pauses until the user responds',
|
|
185
|
+
apiMethod: 'POST /sendMessage + PendingApproval',
|
|
186
|
+
params: [
|
|
187
|
+
chatId(),
|
|
188
|
+
{
|
|
189
|
+
name: 'text',
|
|
190
|
+
label: 'Message',
|
|
191
|
+
type: 'text',
|
|
192
|
+
required: true,
|
|
193
|
+
rows: 4,
|
|
194
|
+
description: 'Body of the prompt shown above the Approve / Disapprove buttons. HTML supported by default.',
|
|
195
|
+
placeholder: 'Approve the ${{payload.amount}} payment to {{payload.vendor}}?',
|
|
196
|
+
},
|
|
197
|
+
parseMode('HTML / MarkdownV2 / none.'),
|
|
198
|
+
{
|
|
199
|
+
name: 'approveLabel',
|
|
200
|
+
label: 'Approve button label',
|
|
201
|
+
type: 'string',
|
|
202
|
+
description: 'Tap = approved.',
|
|
203
|
+
placeholder: 'Approve',
|
|
204
|
+
pairedWithNext: true,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'disapproveLabel',
|
|
208
|
+
label: 'Disapprove button label',
|
|
209
|
+
type: 'string',
|
|
210
|
+
description: "Tap = rejected. Hidden when 'Include disapprove' is off.",
|
|
211
|
+
placeholder: 'Reject',
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'includeDisapprove',
|
|
215
|
+
label: 'Include disapprove button',
|
|
216
|
+
type: 'boolean',
|
|
217
|
+
default: true,
|
|
218
|
+
description: 'Off = approval-only (single button). Default on.',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'allowFreeText',
|
|
222
|
+
label: 'Allow free-text reply',
|
|
223
|
+
type: 'boolean',
|
|
224
|
+
default: false,
|
|
225
|
+
description: "When on, a plain text reply in the same chat also resolves the wait — _waitResponse.responseText carries the typed message. Useful for capturing context ('approved with comment'). Default off.",
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'timeoutMinutes',
|
|
229
|
+
label: 'Timeout (minutes)',
|
|
230
|
+
type: 'number',
|
|
231
|
+
min: 1,
|
|
232
|
+
max: 20160,
|
|
233
|
+
default: 1440,
|
|
234
|
+
description: 'Pipeline auto-resumes (rejected) after this many minutes if no response. Default 1440 (24h).',
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
editMessageText: {
|
|
239
|
+
label: 'Edit message',
|
|
240
|
+
description: 'Replace text of a previously-sent message',
|
|
241
|
+
apiMethod: 'POST /editMessageText',
|
|
242
|
+
supportsInlineKeyboard: true,
|
|
243
|
+
params: [
|
|
244
|
+
chatId(),
|
|
245
|
+
messageId('Message ID', 'Id of the message to edit (from a prior sendMessage result).'),
|
|
246
|
+
{
|
|
247
|
+
name: 'text',
|
|
248
|
+
label: 'New text',
|
|
249
|
+
type: 'text',
|
|
250
|
+
required: true,
|
|
251
|
+
rows: 3,
|
|
252
|
+
description: 'Replacement text for the message.',
|
|
253
|
+
placeholder: 'Updated content',
|
|
254
|
+
},
|
|
255
|
+
// Sin ayuda a propósito: es la única op donde el formulario nunca la ha
|
|
256
|
+
// enseñado, y no me invento copy nueva al mudar el campo.
|
|
257
|
+
parseMode(),
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
deleteMessage: {
|
|
261
|
+
label: 'Delete message',
|
|
262
|
+
description: 'Remove a message from a chat',
|
|
263
|
+
apiMethod: 'POST /deleteMessage',
|
|
264
|
+
params: [
|
|
265
|
+
chatId(),
|
|
266
|
+
messageId('Message ID', 'Id of the message to delete.'),
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
answerCallbackQuery: {
|
|
270
|
+
label: 'Answer callback',
|
|
271
|
+
description: 'Acknowledge an inline-button press (REQUIRED after callback_query)',
|
|
272
|
+
apiMethod: 'POST /answerCallbackQuery',
|
|
273
|
+
params: [
|
|
274
|
+
{
|
|
275
|
+
name: 'callbackQueryId',
|
|
276
|
+
label: 'Callback query ID',
|
|
277
|
+
type: 'string',
|
|
278
|
+
required: true,
|
|
279
|
+
description: 'From the trigger payload — usually {{payload.callbackQueryId}} when responding to an inline-button press.',
|
|
280
|
+
placeholder: '{{payload.callbackQueryId}}',
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
name: 'text',
|
|
284
|
+
label: 'Text',
|
|
285
|
+
type: 'string',
|
|
286
|
+
description: 'Optional. Toast (or alert if showAlert is on). Max 200 chars.',
|
|
287
|
+
placeholder: 'Saved!',
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
name: 'showAlert',
|
|
291
|
+
label: 'Show as alert',
|
|
292
|
+
type: 'boolean',
|
|
293
|
+
default: false,
|
|
294
|
+
description: 'When on, shows a modal alert above the chat instead of a toast.',
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
forwardMessage: {
|
|
299
|
+
label: 'Forward message',
|
|
300
|
+
description: 'Forward an existing message to another chat',
|
|
301
|
+
apiMethod: 'POST /forwardMessage',
|
|
302
|
+
// Las tres sin ayuda: las etiquetas ya dicen cuál es cuál, y así estaba.
|
|
303
|
+
params: [
|
|
304
|
+
{
|
|
305
|
+
name: 'chatId',
|
|
306
|
+
label: 'Destination chat ID',
|
|
307
|
+
type: 'string',
|
|
308
|
+
required: true,
|
|
309
|
+
placeholder: '{{payload.targetChatId}}',
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
name: 'fromChatId',
|
|
313
|
+
label: 'Source chat ID',
|
|
314
|
+
type: 'string',
|
|
315
|
+
required: true,
|
|
316
|
+
placeholder: '{{payload.chatId}}',
|
|
317
|
+
},
|
|
318
|
+
messageId('Source message ID'),
|
|
319
|
+
],
|
|
320
|
+
},
|
|
321
|
+
sendPoll: {
|
|
322
|
+
label: 'Send poll',
|
|
323
|
+
description: 'Multi-option poll (regular or quiz)',
|
|
324
|
+
apiMethod: 'POST /sendPoll',
|
|
325
|
+
params: [
|
|
326
|
+
chatId(),
|
|
327
|
+
{
|
|
328
|
+
name: 'question',
|
|
329
|
+
label: 'Question',
|
|
330
|
+
type: 'text',
|
|
331
|
+
required: true,
|
|
332
|
+
rows: 2,
|
|
333
|
+
description: 'Max 300 chars.',
|
|
334
|
+
placeholder: 'Which option?',
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: 'options',
|
|
338
|
+
label: 'Options (2-10)',
|
|
339
|
+
type: 'linesList',
|
|
340
|
+
required: true,
|
|
341
|
+
rows: 5,
|
|
342
|
+
description: 'One per line. Max 100 chars each.',
|
|
343
|
+
placeholder: 'Option A\nOption B\nOption C',
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: 'isAnonymous',
|
|
347
|
+
label: 'Anonymous',
|
|
348
|
+
type: 'boolean',
|
|
349
|
+
// Defecto ENCENDIDO, como includeDisapprove.
|
|
350
|
+
default: true,
|
|
351
|
+
description: 'Default true. Set false to expose voter identities.',
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
},
|
|
355
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram AI Toolkit — las herramientas que un telegramAction expone cuando
|
|
3
|
+
* `aiEnabled` está encendido.
|
|
4
|
+
*
|
|
5
|
+
* **Se escribe una sola vez**, como el de Sheets. Antes vivía dos veces: una
|
|
6
|
+
* copia en `dashboard/components/telegram-actions/telegram-operations-schemas.ts`
|
|
7
|
+
* y otra en `api/src/mcp-servers/toolkit-specs.ts`, las dos a mano, las dos con
|
|
8
|
+
* el mismo TODO escrito pidiendo justo esto. Y ya habían derivado: el
|
|
9
|
+
* `messageId` de `reply_telegram_message` era `number` en el dashboard y
|
|
10
|
+
* `string` en la api, mientras en las otras tres ops que lo llevan era `number`
|
|
11
|
+
* en las dos. Aquí es `number` y punto.
|
|
12
|
+
*
|
|
13
|
+
* Hay una TERCERA copia que también desaparece: la página de detalle pintaba la
|
|
14
|
+
* lista de las diez herramientas como diez `<li>` escritos a mano. Ahora sale de
|
|
15
|
+
* aquí, contada con `.length`, que es lo que evita que un día anuncie diez y
|
|
16
|
+
* exponga otra cosa.
|
|
17
|
+
*
|
|
18
|
+
* La forma es la que ya consume `toolkitSpecToMcpTool` en la api, para que pueda
|
|
19
|
+
* usarse sin adaptador. Los textos van en inglés porque los lee el modelo y
|
|
20
|
+
* quien mire la lista de herramientas del servidor MCP.
|
|
21
|
+
*/
|
|
22
|
+
import type { TelegramOperation } from './telegram-operations';
|
|
23
|
+
export interface TelegramToolkitParameter {
|
|
24
|
+
name: string;
|
|
25
|
+
type: 'string' | 'number' | 'boolean';
|
|
26
|
+
description: string;
|
|
27
|
+
required: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface TelegramToolkitSpec {
|
|
30
|
+
operation: TelegramOperation;
|
|
31
|
+
/** Etiqueta corta de la fila en la lista de herramientas. */
|
|
32
|
+
label: string;
|
|
33
|
+
/**
|
|
34
|
+
* Nombre con el que el LLM llama a la herramienta. Lleva `_telegram_` para
|
|
35
|
+
* no chocar con el `send_email` de Gmail ni el `create_event` de Calendar
|
|
36
|
+
* cuando un mismo AI Node tiene varios toolkits encendidos.
|
|
37
|
+
*/
|
|
38
|
+
toolName: string;
|
|
39
|
+
/** Descripción (más reglas de uso) que ve el LLM. */
|
|
40
|
+
description: string;
|
|
41
|
+
parameters: TelegramToolkitParameter[];
|
|
42
|
+
/**
|
|
43
|
+
* Encabezado de grupo para la lista de la página de detalle. Las filas
|
|
44
|
+
* consecutivas que comparten grupo se pintan bajo un único título, así que el
|
|
45
|
+
* ORDEN de este array es el que agrupa. Es el mismo que enseñaba el JSX que
|
|
46
|
+
* sustituye.
|
|
47
|
+
*/
|
|
48
|
+
group?: string;
|
|
49
|
+
}
|
|
50
|
+
export declare const TELEGRAM_TOOLKIT_SPECS: TelegramToolkitSpec[];
|
|
51
|
+
/** Las herramientas por nombre, para despachar una llamada del modelo. */
|
|
52
|
+
export declare const TELEGRAM_TOOLKIT_BY_TOOL_NAME: Record<string, TelegramToolkitSpec>;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Telegram AI Toolkit — las herramientas que un telegramAction expone cuando
|
|
4
|
+
* `aiEnabled` está encendido.
|
|
5
|
+
*
|
|
6
|
+
* **Se escribe una sola vez**, como el de Sheets. Antes vivía dos veces: una
|
|
7
|
+
* copia en `dashboard/components/telegram-actions/telegram-operations-schemas.ts`
|
|
8
|
+
* y otra en `api/src/mcp-servers/toolkit-specs.ts`, las dos a mano, las dos con
|
|
9
|
+
* el mismo TODO escrito pidiendo justo esto. Y ya habían derivado: el
|
|
10
|
+
* `messageId` de `reply_telegram_message` era `number` en el dashboard y
|
|
11
|
+
* `string` en la api, mientras en las otras tres ops que lo llevan era `number`
|
|
12
|
+
* en las dos. Aquí es `number` y punto.
|
|
13
|
+
*
|
|
14
|
+
* Hay una TERCERA copia que también desaparece: la página de detalle pintaba la
|
|
15
|
+
* lista de las diez herramientas como diez `<li>` escritos a mano. Ahora sale de
|
|
16
|
+
* aquí, contada con `.length`, que es lo que evita que un día anuncie diez y
|
|
17
|
+
* exponga otra cosa.
|
|
18
|
+
*
|
|
19
|
+
* La forma es la que ya consume `toolkitSpecToMcpTool` en la api, para que pueda
|
|
20
|
+
* usarse sin adaptador. Los textos van en inglés porque los lee el modelo y
|
|
21
|
+
* quien mire la lista de herramientas del servidor MCP.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.TELEGRAM_TOOLKIT_BY_TOOL_NAME = exports.TELEGRAM_TOOLKIT_SPECS = void 0;
|
|
25
|
+
const p = (name, description, required = true, type = 'string') => ({ name, type, description, required });
|
|
26
|
+
/*
|
|
27
|
+
* `sendAndWaitForResponse` NO está aquí, y es una decisión, no un olvido: la op
|
|
28
|
+
* para el pipeline durante minutos u horas esperando que alguien pulse un botón,
|
|
29
|
+
* y por la vía de una herramienta no espera — el modelo recibe algo inmediato y
|
|
30
|
+
* acaba diciendo que ya se aprobó. Se usa en modo single-op, con un Conditional
|
|
31
|
+
* detrás leyendo `_waitResponse.approved`. La espera se resuelve por
|
|
32
|
+
* `service-triggers.processTelegramPush`, donde el LLM no participa.
|
|
33
|
+
*
|
|
34
|
+
* Ninguna lleva `destructive`. `delete_telegram_message` lo merecería —el
|
|
35
|
+
* toolkit de Drive marca sus borrados— pero hoy la spec de Telegram no tiene ese
|
|
36
|
+
* campo en ninguna de las dos capas, y añadirlo cambiaría cuándo el AI Node pide
|
|
37
|
+
* confirmación. Eso es otra decisión, no parte de mudar el esquema de sitio.
|
|
38
|
+
*/
|
|
39
|
+
exports.TELEGRAM_TOOLKIT_SPECS = [
|
|
40
|
+
{
|
|
41
|
+
operation: 'sendMessage',
|
|
42
|
+
label: 'Send message',
|
|
43
|
+
toolName: 'send_telegram_message',
|
|
44
|
+
group: 'Sending',
|
|
45
|
+
description: 'Send a text message via the Telegram bot to a chat. Use for replies, notifications, status updates, or any text-only delivery. ' +
|
|
46
|
+
'Supports HTML formatting (default parse mode). For inline buttons, supply the replyMarkup arg as a 2D array of buttons. ' +
|
|
47
|
+
'USAGE RULES: ' +
|
|
48
|
+
'(1) chatId is REQUIRED — when replying inside a flow triggered by a Telegram message, this is normally `{{payload.chatId}}` from the trigger payload. ' +
|
|
49
|
+
'(2) Telegram caps text at 4096 chars. Split longer messages or use sendDocument with a text file. ' +
|
|
50
|
+
'(3) For a quote-style reply that points at a SPECIFIC user message, prefer `reply_telegram_message` so Telegram renders the threaded indicator — this op sends a standalone message.',
|
|
51
|
+
parameters: [
|
|
52
|
+
p('chatId', 'Target chat id (number) or @channelusername (string).'),
|
|
53
|
+
p('text', 'Message text. HTML by default — escape <, >, & in user content.'),
|
|
54
|
+
p('parseMode', 'Optional. "HTML" (default), "MarkdownV2", or "none".', false),
|
|
55
|
+
p('replyToMessageId', 'Optional. Reply to a specific message id.', false),
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
operation: 'replyToMessage',
|
|
60
|
+
label: 'Reply to message',
|
|
61
|
+
toolName: 'reply_telegram_message',
|
|
62
|
+
group: 'Sending',
|
|
63
|
+
description: 'Reply to a SPECIFIC user message with the quote indicator visible. Telegram renders the bot\'s reply with a small "replying to..." card above it pointing back at the quoted message. ' +
|
|
64
|
+
'USAGE RULES: ' +
|
|
65
|
+
'(1) Use this in group chats where multiple users post in parallel — the indicator makes it clear which user the bot is answering. In 1:1 DMs only use it when context disambiguates between several recent questions. ' +
|
|
66
|
+
'(2) messageId is REQUIRED — pass the id of the user\'s message you\'re responding to (typically `{{payload.message.message_id}}` or `{{payload.message_id}}` from the trigger). ' +
|
|
67
|
+
'(3) chatId is REQUIRED and must match the chat the original message lives in. ' +
|
|
68
|
+
'(4) Same Telegram /sendMessage webhook underneath — text limit (4096 chars) and parse modes (HTML / MarkdownV2 / none) work the same.',
|
|
69
|
+
parameters: [
|
|
70
|
+
p('chatId', 'Target chat id (number) or @channelusername — must match the chat of the quoted message.'),
|
|
71
|
+
// `number`, como en las otras tres ops que llevan messageId. La api lo
|
|
72
|
+
// declaraba `string` por un cuarto argumento que se quedó sin escribir.
|
|
73
|
+
p('messageId', 'Numeric id of the message to quote-reply to. Telegram renders the threaded indicator from this.', true, 'number'),
|
|
74
|
+
p('text', 'Reply text. HTML by default.'),
|
|
75
|
+
p('parseMode', 'Optional. "HTML" (default), "MarkdownV2", or "none".', false),
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
operation: 'sendPhoto',
|
|
80
|
+
label: 'Send photo',
|
|
81
|
+
toolName: 'send_telegram_photo',
|
|
82
|
+
group: 'Sending',
|
|
83
|
+
description: 'Send a photo to a chat. The photo arg accepts a public URL (Telegram fetches it) or a Telegram file_id from a previously-sent photo (cheap re-send). ' +
|
|
84
|
+
'For HW-stored files use the `_file.downloadUrl` template. Max 10MB photo size when fetching by URL. Caption is optional and supports HTML.',
|
|
85
|
+
parameters: [
|
|
86
|
+
p('chatId', 'Target chat id or @channelusername.'),
|
|
87
|
+
p('photo', "Public URL OR file_id from a prior send. URLs must be reachable from Telegram's servers."),
|
|
88
|
+
p('caption', 'Optional caption (max 1024 chars, HTML supported).', false),
|
|
89
|
+
p('parseMode', 'Optional caption parse mode: HTML / MarkdownV2 / none.', false),
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
operation: 'sendDocument',
|
|
94
|
+
label: 'Send document',
|
|
95
|
+
toolName: 'send_telegram_document',
|
|
96
|
+
group: 'Sending',
|
|
97
|
+
description: 'Send a document (any file type) to a chat. Same URL-or-file_id contract as sendPhoto. Max 50MB when Telegram fetches by URL.',
|
|
98
|
+
parameters: [
|
|
99
|
+
p('chatId', 'Target chat id or @channelusername.'),
|
|
100
|
+
p('document', 'Public URL OR file_id.'),
|
|
101
|
+
p('caption', 'Optional caption (HTML supported).', false),
|
|
102
|
+
p('parseMode', 'Optional caption parse mode.', false),
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
operation: 'sendVoice',
|
|
107
|
+
label: 'Send voice',
|
|
108
|
+
toolName: 'send_telegram_voice',
|
|
109
|
+
group: 'Sending',
|
|
110
|
+
description: 'Send a voice message — Telegram renders it as a voice bubble with waveform. ' +
|
|
111
|
+
'USAGE RULES: ' +
|
|
112
|
+
'(1) chatId is REQUIRED — usually `{{payload.chatId}}` from the trigger. ' +
|
|
113
|
+
'(2) voice is REQUIRED — pass an OGG/OPUS audio URL Telegram can fetch, OR a `file_id` from a prior /sendVoice for cheap re-sends. ' +
|
|
114
|
+
'(3) To synthesize from text, FIRST call the `synthesize_voice` tool — it returns a JSON `_file` ref; pass its `downloadUrl` string here as `voice`. NEVER pass the raw `_file` object; Telegram needs a string. ' +
|
|
115
|
+
'(4) Use this when the user sent voice and wants voice back, or when the operator framed the bot as voice-first. For text replies prefer send_telegram_message.',
|
|
116
|
+
parameters: [
|
|
117
|
+
p('chatId', 'Target chat id (number) or @channelusername.'),
|
|
118
|
+
p('voice', 'OGG/OPUS audio URL or Telegram file_id. Use the `downloadUrl` returned by synthesize_voice when chaining from text.'),
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
operation: 'sendPoll',
|
|
123
|
+
label: 'Send poll',
|
|
124
|
+
toolName: 'send_telegram_poll',
|
|
125
|
+
group: 'Sending',
|
|
126
|
+
description: 'Send a poll to a chat. The options arg is an array of strings (2-10 options). For quiz-style polls (single correct answer), pass type="quiz" and a numeric correctOptionId. Polls require group/supergroup/channel chats — DMs reject them.',
|
|
127
|
+
parameters: [
|
|
128
|
+
p('chatId', 'Target chat id.'),
|
|
129
|
+
p('question', 'Poll question (max 300 chars).'),
|
|
130
|
+
p('options', 'Array of 2-10 answer strings. Pass as a JSON array: ["Option A","Option B"]. Each option max 100 chars.'),
|
|
131
|
+
p('isAnonymous', 'Optional. Default true. Set false to expose voter identities.', false, 'boolean'),
|
|
132
|
+
p('type', 'Optional. "regular" (default) or "quiz".', false),
|
|
133
|
+
p('correctOptionId', 'Required when type="quiz" — zero-indexed correct answer.', false, 'number'),
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
operation: 'forwardMessage',
|
|
138
|
+
label: 'Forward message',
|
|
139
|
+
toolName: 'forward_telegram_message',
|
|
140
|
+
group: 'Sending',
|
|
141
|
+
description: 'Forward an existing message from one chat to another. Preserves the original sender attribution. Use for announcements, escalations, or copying a message between groups the bot is in.',
|
|
142
|
+
parameters: [
|
|
143
|
+
p('chatId', 'Destination chat id.'),
|
|
144
|
+
p('fromChatId', 'Source chat id.'),
|
|
145
|
+
p('messageId', 'Source message id.', true, 'number'),
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
operation: 'editMessageText',
|
|
150
|
+
label: 'Edit message text',
|
|
151
|
+
toolName: 'edit_telegram_message',
|
|
152
|
+
group: 'Modify / acknowledge',
|
|
153
|
+
description: 'Replace the text of a previously-sent message. Use for progress updates, fixing typos, removing inline buttons after a press. The bot can only edit messages it sent within the last 48h.',
|
|
154
|
+
parameters: [
|
|
155
|
+
p('chatId', 'Chat id of the message.'),
|
|
156
|
+
p('messageId', 'Message id to edit (returned from a prior sendMessage).', true, 'number'),
|
|
157
|
+
p('text', 'New text for the message.'),
|
|
158
|
+
p('parseMode', 'Optional parse mode.', false),
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
operation: 'deleteMessage',
|
|
163
|
+
label: 'Delete message',
|
|
164
|
+
toolName: 'delete_telegram_message',
|
|
165
|
+
group: 'Modify / acknowledge',
|
|
166
|
+
description: 'Delete a message. Bots can delete their own messages anytime; messages by users only when the bot has Delete Messages admin permission in that chat. Within 48h for bot-own messages in private chats.',
|
|
167
|
+
parameters: [
|
|
168
|
+
p('chatId', 'Chat id where the message lives.'),
|
|
169
|
+
p('messageId', 'Message id to delete.', true, 'number'),
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
operation: 'answerCallbackQuery',
|
|
174
|
+
label: 'Answer callback query',
|
|
175
|
+
toolName: 'answer_telegram_callback',
|
|
176
|
+
group: 'Modify / acknowledge',
|
|
177
|
+
description: 'REQUIRED after a callback_query trigger fires (an inline-keyboard button press) — without this, the user sees a forever-spinner on the button. The text optionally shows as a toast or alert above the chat.',
|
|
178
|
+
parameters: [
|
|
179
|
+
p('callbackQueryId', 'The callback_query.id from the trigger payload — usually `{{payload.callbackQueryId}}`.'),
|
|
180
|
+
p('text', 'Optional toast/alert text shown to the user (max 200 chars).', false),
|
|
181
|
+
p('showAlert', 'Optional. true = modal alert; false (default) = toast notification.', false, 'boolean'),
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
/** Las herramientas por nombre, para despachar una llamada del modelo. */
|
|
186
|
+
exports.TELEGRAM_TOOLKIT_BY_TOOL_NAME = Object.fromEntries(exports.TELEGRAM_TOOLKIT_SPECS.map((s) => [s.toolName, s]));
|
package/package.json
CHANGED