@0xmaxma/claude-gateway 2.0.3 → 2.0.4
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 +1 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +63 -16
- package/dist/agent/runner.js.map +1 -1
- package/dist/orchestration/bridge.d.ts +2 -1
- package/dist/orchestration/bridge.d.ts.map +1 -1
- package/dist/orchestration/bridge.js +65 -55
- package/dist/orchestration/bridge.js.map +1 -1
- package/dist/orchestration/channel-input-media.d.ts +6 -0
- package/dist/orchestration/channel-input-media.d.ts.map +1 -1
- package/dist/orchestration/channel-input-media.js +35 -11
- package/dist/orchestration/channel-input-media.js.map +1 -1
- package/dist/orchestration/channel-media-error.d.ts +11 -0
- package/dist/orchestration/channel-media-error.d.ts.map +1 -0
- package/dist/orchestration/channel-media-error.js +38 -0
- package/dist/orchestration/channel-media-error.js.map +1 -0
- package/dist/orchestration/channel-media.d.ts +1 -1
- package/dist/orchestration/channel-media.d.ts.map +1 -1
- package/dist/orchestration/channel-media.js +37 -7
- package/dist/orchestration/channel-media.js.map +1 -1
- package/dist/orchestration/conversation-intake.d.ts +1 -1
- package/dist/orchestration/conversation-intake.d.ts.map +1 -1
- package/dist/orchestration/conversation-intake.js +1 -1
- package/dist/orchestration/delivery.d.ts +3 -0
- package/dist/orchestration/delivery.d.ts.map +1 -1
- package/dist/orchestration/delivery.js +14 -5
- package/dist/orchestration/delivery.js.map +1 -1
- package/dist/orchestration/inference-errors.d.ts.map +1 -1
- package/dist/orchestration/inference-errors.js +58 -3
- package/dist/orchestration/inference-errors.js.map +1 -1
- package/dist/orchestration/process-turn.d.ts.map +1 -1
- package/dist/orchestration/process-turn.js +27 -4
- package/dist/orchestration/process-turn.js.map +1 -1
- package/dist/orchestration/runtime.d.ts +2 -0
- package/dist/orchestration/runtime.d.ts.map +1 -1
- package/dist/orchestration/runtime.js +92 -47
- package/dist/orchestration/runtime.js.map +1 -1
- package/dist/orchestration/store.d.ts +17 -0
- package/dist/orchestration/store.d.ts.map +1 -1
- package/dist/orchestration/store.js +28 -0
- package/dist/orchestration/store.js.map +1 -1
- package/mcp/tools/discord/inbound.ts +1 -1
- package/mcp/tools/discord/receiver-server.ts +1 -1
- package/mcp/tools/discord/types.ts +1 -1
- package/mcp/tools/receiver-spool.test.ts +8 -2
- package/mcp/tools/receiver-spool.ts +198 -25
- package/mcp/tools/telegram/media-group.ts +3 -3
- package/mcp/tools/telegram/receiver-server.ts +1 -1
- package/mcp/types.ts +1 -1
- package/package.json +1 -1
|
@@ -1,55 +1,228 @@
|
|
|
1
1
|
import { mediaGroupKey, mergeMediaGroup, type ChannelInput } from './telegram/media-group';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
-
import { mkdirSync, readdirSync, openSync, closeSync, writeFileSync, fsyncSync, renameSync, readFileSync, unlinkSync, statSync } from 'node:fs';
|
|
3
|
+
import { mkdirSync, readdirSync, openSync, closeSync, writeFileSync, fsyncSync, renameSync, readFileSync, unlinkSync, statSync, existsSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
type RetryState = { queuedAt: number; attempts: number; nextAttemptAt: number; recoveryBatch?: string; sealedMessageIds?: string[] };
|
|
7
|
+
type Entry = { file: string; payload: string; input: ChannelInput; modified: number; state: RetryState; albumMessageIds?: string[] };
|
|
8
|
+
const MAX_BACKOFF_MS = 5 * 60_000;
|
|
9
|
+
const STALE_STARTUP_AGE_MS = 5 * 60_000;
|
|
10
|
+
|
|
11
|
+
/** Persist raw provider payloads until SQLite admission. Retry state is separate
|
|
12
|
+
* so old journals remain readable and provider deduplication identities stay intact. */
|
|
8
13
|
export class ReceiverSpool {
|
|
9
14
|
private active = false;
|
|
15
|
+
private cursor = 0;
|
|
16
|
+
private lastQueuedAt = 0;
|
|
17
|
+
private reportedInvalidEntry = false;
|
|
10
18
|
private readonly timer: ReturnType<typeof setInterval>;
|
|
11
19
|
constructor(private readonly directory: string, private readonly callback: string, private readonly request: typeof fetch = fetch, private readonly mediaGroupWaitMs = 2000) {
|
|
12
20
|
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
21
|
+
this.pruneAlbumReceipts();
|
|
22
|
+
const snapshot = this.entries();
|
|
23
|
+
this.lastQueuedAt = Math.max(0, ...snapshot.map(entry => entry.state.queuedAt));
|
|
24
|
+
const batches = new Map<string, string>();
|
|
25
|
+
for (const entry of snapshot) {
|
|
26
|
+
if (entry.state.recoveryBatch) batches.set(this.conversation(entry.input), entry.state.recoveryBatch);
|
|
27
|
+
}
|
|
28
|
+
for (const entry of snapshot) {
|
|
29
|
+
const conversation = this.conversation(entry.input);
|
|
30
|
+
if (Date.now() - entry.state.queuedAt > STALE_STARTUP_AGE_MS && !batches.has(conversation)) batches.set(conversation, randomUUID());
|
|
31
|
+
}
|
|
32
|
+
// Quarantine the entire startup conversation, including its newer tail.
|
|
33
|
+
// Persist the batch before sending anything so retries/restarts share one notice.
|
|
34
|
+
for (const entry of snapshot) {
|
|
35
|
+
const batch = batches.get(this.conversation(entry.input));
|
|
36
|
+
if (batch) this.saveState(entry.file, { ...entry.state, recoveryBatch: batch });
|
|
37
|
+
}
|
|
13
38
|
this.timer = setInterval(() => { void this.flush(); }, 1000); this.timer.unref();
|
|
14
39
|
void this.flush();
|
|
15
40
|
}
|
|
41
|
+
private conversation(input: ChannelInput): string {
|
|
42
|
+
const m = input?.meta ?? {};
|
|
43
|
+
return JSON.stringify([m.source ?? m.channel ?? '', m.account_id ?? '', m.chat_id ?? '', m.message_thread_id ?? m.thread_id ?? m.thread_ts ?? '']);
|
|
44
|
+
}
|
|
45
|
+
private atomicWrite(file: string, payload: string): void {
|
|
46
|
+
const temporary = join(this.directory, `${randomUUID()}.tmp`);
|
|
47
|
+
const descriptor = openSync(temporary, 'wx', 0o600);
|
|
48
|
+
try { writeFileSync(descriptor, payload); fsyncSync(descriptor); } finally { closeSync(descriptor); }
|
|
49
|
+
renameSync(temporary, join(this.directory, file));
|
|
50
|
+
this.syncDirectory();
|
|
51
|
+
}
|
|
52
|
+
private state(file: string, queuedAt: number): RetryState {
|
|
53
|
+
try {
|
|
54
|
+
const state = JSON.parse(readFileSync(join(this.directory, `${file}.retry`), 'utf8')) as RetryState;
|
|
55
|
+
const timestamp = (value: number) => Number.isFinite(value) && value >= 0 && value <= Number.MAX_SAFE_INTEGER;
|
|
56
|
+
if (!state || !timestamp(state.queuedAt) || !timestamp(state.nextAttemptAt)
|
|
57
|
+
|| !Number.isSafeInteger(state.attempts) || state.attempts < 0
|
|
58
|
+
|| (state.sealedMessageIds !== undefined && (!Array.isArray(state.sealedMessageIds) || state.sealedMessageIds.length > 10 || state.sealedMessageIds.some(id => typeof id !== 'string' || !id)))
|
|
59
|
+
|| (state.recoveryBatch !== undefined && (typeof state.recoveryBatch !== 'string' || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(state.recoveryBatch)))) {
|
|
60
|
+
throw new Error('Invalid ingress retry state');
|
|
61
|
+
}
|
|
62
|
+
return state;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
66
|
+
return { queuedAt, attempts: 0, nextAttemptAt: 0 };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
private saveState(file: string, state: RetryState): void { this.atomicWrite(`${file}.retry`, JSON.stringify(state)); }
|
|
70
|
+
private entries(): Entry[] {
|
|
71
|
+
const entries: Entry[] = [];
|
|
72
|
+
const blocked = new Set<string>();
|
|
73
|
+
for (const file of readdirSync(this.directory).filter(file => /^[a-f0-9]{64}\.json$/.test(file))) {
|
|
74
|
+
let input: ChannelInput | undefined;
|
|
75
|
+
try {
|
|
76
|
+
const location = join(this.directory, file);
|
|
77
|
+
const modified = statSync(location).mtimeMs;
|
|
78
|
+
const payload = readFileSync(location, 'utf8');
|
|
79
|
+
input = JSON.parse(payload) as ChannelInput;
|
|
80
|
+
if (!input || typeof input !== 'object' || typeof input.content !== 'string'
|
|
81
|
+
|| !input.meta || typeof input.meta !== 'object' || Array.isArray(input.meta)
|
|
82
|
+
|| Object.values(input.meta).some(value => typeof value !== 'string')) {
|
|
83
|
+
input = undefined;
|
|
84
|
+
throw new Error('Invalid ingress payload');
|
|
85
|
+
}
|
|
86
|
+
// Validate nested album metadata inside the per-record boundary, so a
|
|
87
|
+
// damaged album cannot abort delivery for unrelated conversations.
|
|
88
|
+
let albumMessageIds: string[] | undefined;
|
|
89
|
+
if (mediaGroupKey(input)) {
|
|
90
|
+
const ids: unknown = JSON.parse(input.meta.message_ids_json);
|
|
91
|
+
if (!Array.isArray(ids) || !ids.length || ids.length > 10 || ids.some(id=>typeof id !== 'string' || !id)) {
|
|
92
|
+
throw new Error('Invalid ingress album members');
|
|
93
|
+
}
|
|
94
|
+
albumMessageIds = ids;
|
|
95
|
+
}
|
|
96
|
+
entries.push({ file, modified, payload, input, state: this.state(file, modified), albumMessageIds });
|
|
97
|
+
} catch {
|
|
98
|
+
// An identifiable conversation must not skip its damaged head. Other
|
|
99
|
+
// conversations remain usable; preserve all bytes for operator recovery.
|
|
100
|
+
if (input) blocked.add(this.conversation(input));
|
|
101
|
+
if (!this.reportedInvalidEntry) {
|
|
102
|
+
this.reportedInvalidEntry = true;
|
|
103
|
+
process.stderr.write('Receiver spool retained an unreadable or invalid entry; inspect the local journal.\n');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return entries.filter(entry => !blocked.has(this.conversation(entry.input)))
|
|
108
|
+
.sort((a, b) => a.state.queuedAt - b.state.queuedAt || a.file.localeCompare(b.file));
|
|
109
|
+
}
|
|
110
|
+
|
|
16
111
|
enqueue(input: unknown): void {
|
|
17
|
-
const group=mediaGroupKey(input as ChannelInput);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
112
|
+
const group = mediaGroupKey(input as ChannelInput);
|
|
113
|
+
let albumKey = group;
|
|
114
|
+
if (group) {
|
|
115
|
+
const messageId = (input as ChannelInput).meta.message_id;
|
|
116
|
+
const initial = this.state(`${group}.json`, Date.now());
|
|
117
|
+
if (initial.sealedMessageIds) {
|
|
118
|
+
if (initial.sealedMessageIds.includes(messageId)) return;
|
|
119
|
+
// An attempted body is immutable: late members use their own stable
|
|
120
|
+
// first-message identity, never an expanded body under an existing ID.
|
|
121
|
+
albumKey = createHash('sha256').update(JSON.stringify([group, messageId])).digest('hex');
|
|
122
|
+
}
|
|
123
|
+
const target = `${albumKey}.json`;
|
|
124
|
+
const state = this.state(target, Date.now());
|
|
125
|
+
if (state.sealedMessageIds?.includes(messageId)) return;
|
|
126
|
+
let prior: ChannelInput | undefined;
|
|
127
|
+
try { prior = JSON.parse(readFileSync(join(this.directory, target), 'utf8')); }
|
|
128
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
|
|
129
|
+
input = mergeMediaGroup(prior, input as ChannelInput);
|
|
22
130
|
}
|
|
23
131
|
const payload = JSON.stringify(input);
|
|
24
132
|
if (Buffer.byteLength(payload) > 131072) throw new Error('Ingress payload too large');
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
133
|
+
const key = albumKey ?? createHash('sha256').update(payload).digest('hex');
|
|
134
|
+
const file = `${key}.json`;
|
|
135
|
+
const location = join(this.directory, file);
|
|
136
|
+
const present = existsSync(location);
|
|
137
|
+
if (!present && readdirSync(this.directory).filter(file => file.endsWith('.json')).length >= 1000) throw new Error('Ingress spool full');
|
|
138
|
+
// Preserve admission order even when an album's quiet window is reset.
|
|
139
|
+
const queuedAt = present ? statSync(location).mtimeMs : Math.max(Date.now(), this.lastQueuedAt + 0.01);
|
|
140
|
+
const state = this.state(file, queuedAt);
|
|
141
|
+
this.lastQueuedAt = Math.max(this.lastQueuedAt, state.queuedAt);
|
|
142
|
+
this.saveState(file, state);
|
|
143
|
+
this.atomicWrite(file, payload);
|
|
32
144
|
void this.flush();
|
|
33
145
|
}
|
|
146
|
+
private pruneAlbumReceipts(): void {
|
|
147
|
+
const receipts = readdirSync(this.directory).filter(file => /^[a-f0-9]{64}\.json\.retry$/.test(file)
|
|
148
|
+
&& !existsSync(join(this.directory, file.slice(0, -6))))
|
|
149
|
+
.map(file => ({ file, modified: statSync(join(this.directory, file)).mtimeMs }))
|
|
150
|
+
.sort((a, b) => b.modified - a.modified);
|
|
151
|
+
let changed = false;
|
|
152
|
+
for (const [index, receipt] of receipts.entries()) {
|
|
153
|
+
if (index < 1000 && Date.now() - receipt.modified < 24 * 60 * 60_000) continue;
|
|
154
|
+
// Do not remove an unknown/corrupt sidecar as part of receipt cleanup.
|
|
155
|
+
try {
|
|
156
|
+
if (!this.state(receipt.file.slice(0, -6), receipt.modified).sealedMessageIds) continue;
|
|
157
|
+
unlinkSync(join(this.directory, receipt.file)); changed = true;
|
|
158
|
+
} catch { /* Preserve unrecognized bytes for operator recovery. */ }
|
|
159
|
+
}
|
|
160
|
+
if (changed) this.syncDirectory();
|
|
161
|
+
}
|
|
34
162
|
private syncDirectory(): void {
|
|
35
163
|
const descriptor = openSync(this.directory, 'r');
|
|
36
164
|
try { fsyncSync(descriptor); } finally { closeSync(descriptor); }
|
|
37
165
|
}
|
|
166
|
+
private defer(entry: Entry, response?: Response): void {
|
|
167
|
+
const attempts = Math.min(Number.MAX_SAFE_INTEGER, entry.state.attempts + 1);
|
|
168
|
+
const exponential = Math.min(MAX_BACKOFF_MS, 1000 * 2 ** Math.min(attempts - 1, 9));
|
|
169
|
+
const header = response?.headers.get('retry-after');
|
|
170
|
+
const retryAfter = header ? (/^\d+(\.\d+)?$/.test(header) ? Number(header) * 1000 : Date.parse(header) - Date.now()) : 0;
|
|
171
|
+
// Bound our exponential policy; honor a longer server-requested pause.
|
|
172
|
+
const delay = Math.max(exponential, Number.isFinite(retryAfter) ? Math.min(24 * 60 * 60_000, retryAfter) : 0);
|
|
173
|
+
this.saveState(entry.file, { ...entry.state, attempts, nextAttemptAt: Date.now() + delay });
|
|
174
|
+
}
|
|
38
175
|
async flush(): Promise<void> {
|
|
39
176
|
if (this.active) return;
|
|
40
177
|
this.active = true;
|
|
41
178
|
try {
|
|
42
|
-
const
|
|
43
|
-
for (const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
179
|
+
const conversations = new Map<string, Entry[]>();
|
|
180
|
+
for (const entry of this.entries()) {
|
|
181
|
+
const conversation = this.conversation(entry.input);
|
|
182
|
+
const entries = conversations.get(conversation) ?? [];
|
|
183
|
+
entries.push(entry);
|
|
184
|
+
conversations.set(conversation, entries);
|
|
185
|
+
}
|
|
186
|
+
const groups = [...conversations.values()];
|
|
187
|
+
// Rotate conversation heads, then drain successful conversations round-robin.
|
|
188
|
+
// Failed/waiting heads block only their own ordered tail.
|
|
189
|
+
const start = this.cursor % Math.max(groups.length, 1);
|
|
190
|
+
this.cursor = start + Math.min(100, groups.length);
|
|
191
|
+
const pending = [...groups.slice(start), ...groups.slice(0, start)];
|
|
192
|
+
let attempts = 0;
|
|
193
|
+
while (pending.length && attempts < 100) {
|
|
194
|
+
const group = pending.shift()!;
|
|
195
|
+
const entry = group.shift()!;
|
|
196
|
+
if (entry.state.nextAttemptAt > Date.now()) continue;
|
|
197
|
+
// Another callback may have yielded while this unsealed album grew.
|
|
198
|
+
// Refresh its full body and quiet window next pass before sealing it.
|
|
199
|
+
if (readFileSync(join(this.directory, entry.file), 'utf8') !== entry.payload) continue;
|
|
200
|
+
if (mediaGroupKey(entry.input) && Date.now() - entry.modified < this.mediaGroupWaitMs) continue;
|
|
201
|
+
const body = entry.state.recoveryBatch ? JSON.stringify({ ...entry.input, meta: { ...entry.input.meta, ingress_recovery_batch: entry.state.recoveryBatch } }) : entry.payload;
|
|
202
|
+
if (entry.albumMessageIds && !entry.state.sealedMessageIds) {
|
|
203
|
+
entry.state = { ...entry.state, sealedMessageIds: entry.albumMessageIds };
|
|
204
|
+
this.saveState(entry.file, entry.state);
|
|
205
|
+
}
|
|
206
|
+
attempts++;
|
|
207
|
+
let response: Response;
|
|
208
|
+
try {
|
|
209
|
+
response = await this.request(this.callback, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body, signal: AbortSignal.timeout(10000) });
|
|
210
|
+
} catch { this.defer(entry); continue; }
|
|
211
|
+
if (!response.ok) { this.defer(entry, response); continue; }
|
|
212
|
+
const location = join(this.directory, entry.file);
|
|
213
|
+
if (readFileSync(location, 'utf8') === entry.payload) {
|
|
214
|
+
unlinkSync(location);
|
|
215
|
+
const retry = join(this.directory, `${entry.file}.retry`);
|
|
216
|
+
// Pending retries never expire. Acknowledged album receipts retain
|
|
217
|
+
// member IDs for bounded platform-redelivery deduplication.
|
|
218
|
+
if (entry.state.sealedMessageIds) this.saveState(entry.file, { ...entry.state, attempts: 0, nextAttemptAt: 0 });
|
|
219
|
+
else if (existsSync(retry)) unlinkSync(retry);
|
|
220
|
+
this.syncDirectory();
|
|
221
|
+
if (entry.state.sealedMessageIds) this.pruneAlbumReceipts();
|
|
222
|
+
if (group.length) pending.push(group);
|
|
223
|
+
}
|
|
51
224
|
}
|
|
52
|
-
} catch { /* persisted
|
|
225
|
+
} catch { /* Keep persisted input on filesystem failure. */ }
|
|
53
226
|
finally { this.active = false; }
|
|
54
227
|
}
|
|
55
228
|
close(): void { clearInterval(this.timer); }
|
|
@@ -9,12 +9,12 @@ export function mergeMediaGroup(prior:ChannelInput|undefined,input:ChannelInput)
|
|
|
9
9
|
const ids:string[]=prior?JSON.parse(prior.meta.message_ids_json):[];
|
|
10
10
|
if(ids.includes(input.meta.message_id))return prior!;
|
|
11
11
|
if(ids.length>=10)throw Error('Telegram media group exceeds 10 items');
|
|
12
|
-
const parts:Array<{id:string;caption:string;ref?:string;path?:string;name?:string}> = prior?JSON.parse(prior.meta.media_group_parts_json):[];
|
|
13
|
-
parts.push({id:input.meta.message_id,caption:input.meta.media_caption??'',ref:input.meta.attachment_file_id,path:input.meta.image_path,name:input.meta.attachment_name});
|
|
12
|
+
const parts:Array<{id:string;caption:string;ref?:string;path?:string;name?:string;size?:number}> = prior?JSON.parse(prior.meta.media_group_parts_json):[];
|
|
13
|
+
parts.push({id:input.meta.message_id,caption:input.meta.media_caption??'',ref:input.meta.attachment_file_id,path:input.meta.image_path,name:input.meta.attachment_name,size:input.meta.attachment_size===undefined?undefined:Number(input.meta.attachment_size)});
|
|
14
14
|
parts.sort((a,b)=>Number(a.id)-Number(b.id));
|
|
15
15
|
const meta:Record<string,string>={...(prior?.meta??input.meta),media_group_id:input.meta.media_group_id,message_id:parts[0]!.id,
|
|
16
16
|
message_ids_json:JSON.stringify(parts.map(p=>p.id)),media_group_parts_json:JSON.stringify(parts),
|
|
17
|
-
attachments_json:JSON.stringify(parts.map(({ref,path,name})=>({ref,path,name})))};
|
|
17
|
+
attachments_json:JSON.stringify(parts.map(({ref,path,name,size})=>({ref,path,name,size})))};
|
|
18
18
|
// Album files must not turn a mixed album into a single-file voice-note turn.
|
|
19
19
|
delete meta.attachment_file_id;delete meta.image_path;delete meta.attachment_kind;
|
|
20
20
|
return {content:parts.map(p=>p.caption).filter(Boolean).join('\n\n')||'[Media album attached]',meta};
|
|
@@ -1086,7 +1086,7 @@ async function handleInbound(
|
|
|
1086
1086
|
replied_user: replyMsg.from?.username ?? String(replyMsg.from?.id ?? ''),
|
|
1087
1087
|
...((replyMsg.text || replyMsg.caption) ? { replied_text: replyMsg.text || replyMsg.caption } : {}),
|
|
1088
1088
|
...(repliedImagePath ? { replied_image_path: repliedImagePath } : {}),
|
|
1089
|
-
...(orchestrationSpool && replyMsg.photo ? {replied_attachment_file_id:replyMsg.photo[replyMsg.photo.length-1]!.file_id} : {}),
|
|
1089
|
+
...(orchestrationSpool && replyMsg.photo ? {replied_attachment_file_id:replyMsg.photo[replyMsg.photo.length-1]!.file_id,...(replyMsg.photo[replyMsg.photo.length-1]!.file_size!==undefined?{replied_attachment_size:String(replyMsg.photo[replyMsg.photo.length-1]!.file_size)}:{})} : {}),
|
|
1090
1090
|
...(repliedAttachment ? {
|
|
1091
1091
|
replied_attachment_kind: repliedAttachment.kind,
|
|
1092
1092
|
replied_attachment_file_id: repliedAttachment.file_id,
|
package/mcp/types.ts
CHANGED
|
@@ -16,7 +16,7 @@ export type InboundMessage = {
|
|
|
16
16
|
replyToMessageId?: string;
|
|
17
17
|
repliedText?: string;
|
|
18
18
|
repliedSender?: string;
|
|
19
|
-
attachments?: Array<{url:string;name?:string;kind?:string;quoted?:boolean}>;
|
|
19
|
+
attachments?: Array<{url:string;name?:string;kind?:string;quoted?:boolean;size?:number}>;
|
|
20
20
|
threadId?: string;
|
|
21
21
|
attachmentFileId?: string;
|
|
22
22
|
attachmentKind?: 'voice' | 'file';
|