@promptbook/cli 0.104.0-4 → 0.104.0-5
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/apps/agents-server/src/app/admin/messages/MessagesClient.tsx +294 -0
- package/apps/agents-server/src/app/admin/messages/page.tsx +13 -0
- package/apps/agents-server/src/app/admin/messages/send-email/SendEmailClient.tsx +104 -0
- package/apps/agents-server/src/app/admin/messages/send-email/actions.ts +35 -0
- package/apps/agents-server/src/app/admin/messages/send-email/page.tsx +13 -0
- package/apps/agents-server/src/app/agents/[agentName]/api/profile/route.ts +4 -0
- package/apps/agents-server/src/app/agents/[agentName]/images/default-avatar.png/route.ts +139 -0
- package/apps/agents-server/src/app/api/messages/route.ts +102 -0
- package/apps/agents-server/src/components/Header/Header.tsx +4 -0
- package/apps/agents-server/src/database/$provideSupabaseForBrowser.ts +3 -3
- package/apps/agents-server/src/database/$provideSupabaseForServer.ts +1 -1
- package/apps/agents-server/src/database/$provideSupabaseForWorker.ts +3 -3
- package/apps/agents-server/src/database/migrations/2025-11-0001-initial-schema.sql +1 -3
- package/apps/agents-server/src/database/migrations/2025-11-0002-metadata-table.sql +1 -3
- package/apps/agents-server/src/database/schema.ts +95 -4
- package/apps/agents-server/src/message-providers/email/zeptomail/ZeptomailMessageProvider.ts +32 -24
- package/apps/agents-server/src/utils/messages/sendMessage.ts +7 -7
- package/apps/agents-server/src/utils/messagesAdmin.ts +72 -0
- package/esm/index.es.js +8146 -8145
- package/esm/index.es.js.map +1 -1
- package/esm/typings/src/collection/agent-collection/constructors/agent-collection-in-supabase/AgentsDatabaseSchema.d.ts +18 -15
- package/esm/typings/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/umd/index.umd.js +8135 -8134
- package/umd/index.umd.js.map +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { $getTableName } from '../../../database/$getTableName';
|
|
3
|
+
import { $provideSupabase } from '../../../database/$provideSupabase';
|
|
4
|
+
import { isUserAdmin } from '../../../utils/isUserAdmin';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
7
|
+
const MAX_PAGE_SIZE = 100;
|
|
8
|
+
|
|
9
|
+
function parsePositiveInt(value: string | null, fallback: number): number {
|
|
10
|
+
if (!value) return fallback;
|
|
11
|
+
const parsed = parseInt(value, 10);
|
|
12
|
+
if (Number.isNaN(parsed) || parsed <= 0) return fallback;
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* List messages with filters, search and pagination.
|
|
18
|
+
*/
|
|
19
|
+
export async function GET(request: NextRequest) {
|
|
20
|
+
if (!(await isUserAdmin())) {
|
|
21
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const searchParams = request.nextUrl.searchParams;
|
|
26
|
+
|
|
27
|
+
const page = parsePositiveInt(searchParams.get('page'), 1);
|
|
28
|
+
const pageSize = Math.min(MAX_PAGE_SIZE, parsePositiveInt(searchParams.get('pageSize'), DEFAULT_PAGE_SIZE));
|
|
29
|
+
const search = searchParams.get('search')?.trim() || '';
|
|
30
|
+
const channel = searchParams.get('channel');
|
|
31
|
+
const direction = searchParams.get('direction');
|
|
32
|
+
|
|
33
|
+
const supabase = $provideSupabase();
|
|
34
|
+
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
36
|
+
let query = supabase.from(await $getTableName('Message')).select('*', { count: 'exact' });
|
|
37
|
+
|
|
38
|
+
if (channel) {
|
|
39
|
+
query = query.eq('channel', channel);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (direction) {
|
|
43
|
+
query = query.eq('direction', direction);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (search) {
|
|
47
|
+
// Search in content, subject (if in metadata?), sender/recipient emails
|
|
48
|
+
// Note: sender and recipients are JSONB, so ilike might not work directly on them unless cast to text
|
|
49
|
+
// Content is TEXT.
|
|
50
|
+
const escaped = search.replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
51
|
+
// Assuming simple search on content for now to avoid complexity with JSONB search in generic supabase client
|
|
52
|
+
query = query.ilike('content', `%${escaped}%`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Default sort by createdAt desc
|
|
56
|
+
query = query.order('createdAt', { ascending: false });
|
|
57
|
+
|
|
58
|
+
const from = (page - 1) * pageSize;
|
|
59
|
+
const to = from + pageSize - 1;
|
|
60
|
+
|
|
61
|
+
query = query.range(from, to);
|
|
62
|
+
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
const { data: messages, error, count } = (await query) as { data: any[]; error: any; count: number };
|
|
65
|
+
|
|
66
|
+
if (error) {
|
|
67
|
+
console.error('List messages error:', error);
|
|
68
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Fetch attempts for these messages
|
|
72
|
+
if (messages && messages.length > 0) {
|
|
73
|
+
const messageIds = messages.map((m) => m.id);
|
|
74
|
+
const { data: attempts, error: attemptsError } = await supabase
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
76
|
+
.from(await $getTableName('MessageSendAttempt'))
|
|
77
|
+
.select('*')
|
|
78
|
+
.in('messageId', messageIds);
|
|
79
|
+
|
|
80
|
+
if (attemptsError) {
|
|
81
|
+
console.error('Fetch message attempts error:', attemptsError);
|
|
82
|
+
// We don't fail the whole request, just log it.
|
|
83
|
+
} else {
|
|
84
|
+
// Attach attempts to messages
|
|
85
|
+
for (const message of messages) {
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
87
|
+
(message as any).sendAttempts = attempts?.filter((a: any) => a.messageId === message.id) || [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return NextResponse.json({
|
|
93
|
+
items: messages ?? [],
|
|
94
|
+
total: count ?? 0,
|
|
95
|
+
page,
|
|
96
|
+
pageSize,
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('List messages error:', error);
|
|
100
|
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -297,6 +297,10 @@ export function Header(props: HeaderProps) {
|
|
|
297
297
|
label: 'Chat history',
|
|
298
298
|
href: '/admin/chat-history',
|
|
299
299
|
},
|
|
300
|
+
{
|
|
301
|
+
label: 'Messages & Emails',
|
|
302
|
+
href: '/admin/messages',
|
|
303
|
+
},
|
|
300
304
|
{
|
|
301
305
|
label: 'Chat feedback',
|
|
302
306
|
href: '/admin/chat-feedback',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AgentsDatabaseSchema } from '@promptbook-local/types';
|
|
2
1
|
import { $isRunningInBrowser } from '@promptbook-local/utils';
|
|
3
2
|
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
|
3
|
+
import { AgentsServerDatabase } from './schema';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Internal cache for `$provideSupabaseForBrowser`
|
|
@@ -8,7 +8,7 @@ import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
|
|
8
8
|
* @private
|
|
9
9
|
* @singleton
|
|
10
10
|
*/
|
|
11
|
-
let supabase: SupabaseClient<
|
|
11
|
+
let supabase: SupabaseClient<AgentsServerDatabase>;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Get supabase client
|
|
@@ -27,7 +27,7 @@ export function $provideSupabaseForBrowser(): typeof supabase {
|
|
|
27
27
|
|
|
28
28
|
if (!supabase) {
|
|
29
29
|
// Create a single supabase client for interacting with your database
|
|
30
|
-
supabase = createClient<
|
|
30
|
+
supabase = createClient<AgentsServerDatabase>(
|
|
31
31
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
32
32
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
33
33
|
);
|
|
@@ -18,7 +18,7 @@ let supabase: SupabaseClient<AgentsServerDatabase>;
|
|
|
18
18
|
*
|
|
19
19
|
* @returns instance of supabase client
|
|
20
20
|
*/
|
|
21
|
-
export function $provideSupabaseForServer():
|
|
21
|
+
export function $provideSupabaseForServer(): SupabaseClient<AgentsServerDatabase> {
|
|
22
22
|
if (!$isRunningInNode()) {
|
|
23
23
|
throw new Error(
|
|
24
24
|
'Function `$provideSupabaseForServer` can not be used in browser, use `$provideSupabaseForBrowser` instead.',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AgentsDatabaseSchema } from '@promptbook-local/types';
|
|
2
1
|
import { $isRunningInWebWorker } from '@promptbook-local/utils';
|
|
3
2
|
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
|
3
|
+
import { AgentsServerDatabase } from './schema';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Internal cache for `$provideSupabaseForWorker`
|
|
@@ -8,7 +8,7 @@ import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
|
|
8
8
|
* @private
|
|
9
9
|
* @singleton
|
|
10
10
|
*/
|
|
11
|
-
let supabase: SupabaseClient<
|
|
11
|
+
let supabase: SupabaseClient<AgentsServerDatabase>;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Get supabase client
|
|
@@ -27,7 +27,7 @@ export function $provideSupabaseForWorker(): typeof supabase {
|
|
|
27
27
|
|
|
28
28
|
if (!supabase) {
|
|
29
29
|
// Create a single supabase client for interacting with your database
|
|
30
|
-
supabase = createClient<
|
|
30
|
+
supabase = createClient<AgentsServerDatabase>(
|
|
31
31
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
32
32
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
33
33
|
);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Source of truth: `schema.sql` *(do not edit table structure here manually)*
|
|
4
4
|
*
|
|
5
5
|
* [💽] Prompt:
|
|
6
|
-
* Re-generate supabase typescript schema from
|
|
6
|
+
* Re-generate supabase typescript schema from `./migrations/*.sql`
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
// Json helper (Supabase style)
|
|
@@ -120,7 +120,14 @@ export type AgentsServerDatabase = {
|
|
|
120
120
|
agentSource?: string;
|
|
121
121
|
promptbookEngineVersion?: string;
|
|
122
122
|
};
|
|
123
|
-
Relationships: [
|
|
123
|
+
Relationships: [
|
|
124
|
+
{
|
|
125
|
+
foreignKeyName: 'AgentHistory_agentName_fkey';
|
|
126
|
+
columns: ['agentName'];
|
|
127
|
+
referencedRelation: 'Agent';
|
|
128
|
+
referencedColumns: ['agentName'];
|
|
129
|
+
},
|
|
130
|
+
];
|
|
124
131
|
};
|
|
125
132
|
ChatHistory: {
|
|
126
133
|
Row: {
|
|
@@ -174,7 +181,14 @@ export type AgentsServerDatabase = {
|
|
|
174
181
|
source?: 'AGENT_PAGE_CHAT' | 'OPENAI_API_COMPATIBILITY' | null;
|
|
175
182
|
apiKey?: string | null;
|
|
176
183
|
};
|
|
177
|
-
Relationships: [
|
|
184
|
+
Relationships: [
|
|
185
|
+
{
|
|
186
|
+
foreignKeyName: 'ChatHistory_agentName_fkey';
|
|
187
|
+
columns: ['agentName'];
|
|
188
|
+
referencedRelation: 'Agent';
|
|
189
|
+
referencedColumns: ['agentName'];
|
|
190
|
+
},
|
|
191
|
+
];
|
|
178
192
|
};
|
|
179
193
|
ChatFeedback: {
|
|
180
194
|
Row: {
|
|
@@ -228,7 +242,14 @@ export type AgentsServerDatabase = {
|
|
|
228
242
|
language?: string | null;
|
|
229
243
|
platform?: string | null;
|
|
230
244
|
};
|
|
231
|
-
Relationships: [
|
|
245
|
+
Relationships: [
|
|
246
|
+
{
|
|
247
|
+
foreignKeyName: 'ChatFeedback_agentName_fkey';
|
|
248
|
+
columns: ['agentName'];
|
|
249
|
+
referencedRelation: 'Agent';
|
|
250
|
+
referencedColumns: ['agentName'];
|
|
251
|
+
},
|
|
252
|
+
];
|
|
232
253
|
};
|
|
233
254
|
User: {
|
|
234
255
|
Row: {
|
|
@@ -408,6 +429,76 @@ export type AgentsServerDatabase = {
|
|
|
408
429
|
},
|
|
409
430
|
];
|
|
410
431
|
};
|
|
432
|
+
Message: {
|
|
433
|
+
Row: {
|
|
434
|
+
id: number;
|
|
435
|
+
createdAt: string;
|
|
436
|
+
channel: string;
|
|
437
|
+
direction: string;
|
|
438
|
+
sender: Json;
|
|
439
|
+
recipients: Json | null;
|
|
440
|
+
content: string;
|
|
441
|
+
threadId: string | null;
|
|
442
|
+
metadata: Json | null;
|
|
443
|
+
};
|
|
444
|
+
Insert: {
|
|
445
|
+
id?: number;
|
|
446
|
+
createdAt?: string;
|
|
447
|
+
channel: string;
|
|
448
|
+
direction: string;
|
|
449
|
+
sender: Json;
|
|
450
|
+
recipients?: Json | null;
|
|
451
|
+
content: string;
|
|
452
|
+
threadId?: string | null;
|
|
453
|
+
metadata?: Json | null;
|
|
454
|
+
};
|
|
455
|
+
Update: {
|
|
456
|
+
id?: number;
|
|
457
|
+
createdAt?: string;
|
|
458
|
+
channel?: string;
|
|
459
|
+
direction?: string;
|
|
460
|
+
sender?: Json;
|
|
461
|
+
recipients?: Json | null;
|
|
462
|
+
content?: string;
|
|
463
|
+
threadId?: string | null;
|
|
464
|
+
metadata?: Json | null;
|
|
465
|
+
};
|
|
466
|
+
Relationships: [];
|
|
467
|
+
};
|
|
468
|
+
MessageSendAttempt: {
|
|
469
|
+
Row: {
|
|
470
|
+
id: number;
|
|
471
|
+
createdAt: string;
|
|
472
|
+
messageId: number;
|
|
473
|
+
providerName: string;
|
|
474
|
+
isSuccessful: boolean;
|
|
475
|
+
raw: Json | null;
|
|
476
|
+
};
|
|
477
|
+
Insert: {
|
|
478
|
+
id?: number;
|
|
479
|
+
createdAt?: string;
|
|
480
|
+
messageId: number;
|
|
481
|
+
providerName: string;
|
|
482
|
+
isSuccessful: boolean;
|
|
483
|
+
raw?: Json | null;
|
|
484
|
+
};
|
|
485
|
+
Update: {
|
|
486
|
+
id?: number;
|
|
487
|
+
createdAt?: string;
|
|
488
|
+
messageId?: number;
|
|
489
|
+
providerName?: string;
|
|
490
|
+
isSuccessful?: boolean;
|
|
491
|
+
raw?: Json | null;
|
|
492
|
+
};
|
|
493
|
+
Relationships: [
|
|
494
|
+
{
|
|
495
|
+
foreignKeyName: 'MessageSendAttempt_messageId_fkey';
|
|
496
|
+
columns: ['messageId'];
|
|
497
|
+
referencedRelation: 'Message';
|
|
498
|
+
referencedColumns: ['id'];
|
|
499
|
+
},
|
|
500
|
+
];
|
|
501
|
+
};
|
|
411
502
|
};
|
|
412
503
|
Views: Record<string, never>;
|
|
413
504
|
Functions: Record<string, never>;
|
package/apps/agents-server/src/message-providers/email/zeptomail/ZeptomailMessageProvider.ts
CHANGED
|
@@ -10,34 +10,42 @@ export class ZeptomailMessageProvider implements MessageProvider {
|
|
|
10
10
|
constructor(private readonly apiKey: string) {}
|
|
11
11
|
|
|
12
12
|
public async send(message: OutboundEmail): Promise<really_any> {
|
|
13
|
-
|
|
13
|
+
try {
|
|
14
|
+
const client = new SendMailClient({ url: 'api.zeptomail.com/', token: this.apiKey });
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
const sender = message.sender as really_any;
|
|
17
|
+
const recipients = (Array.isArray(message.recipients) ? message.recipients : [message.recipients]).filter(
|
|
18
|
+
Boolean,
|
|
19
|
+
) as really_any[];
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
const textbody = removeMarkdownFormatting(message.content);
|
|
22
|
+
const htmlbody = await marked.parse(message.content);
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
},
|
|
28
|
-
to: recipients.map((r) => ({
|
|
29
|
-
email_address: {
|
|
30
|
-
address: r.email || r.baseEmail || r,
|
|
31
|
-
name: r.name || r.fullName || undefined,
|
|
24
|
+
const response = await client.sendMail({
|
|
25
|
+
from: {
|
|
26
|
+
address: sender.email || sender.baseEmail || sender,
|
|
27
|
+
name: sender.name || sender.fullName || undefined,
|
|
32
28
|
},
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
to: recipients.map((r) => ({
|
|
30
|
+
email_address: {
|
|
31
|
+
address: r.email || r.baseEmail || r,
|
|
32
|
+
name: r.name || r.fullName || undefined,
|
|
33
|
+
},
|
|
34
|
+
})),
|
|
35
|
+
subject: message.metadata?.subject || 'No Subject',
|
|
36
|
+
textbody,
|
|
37
|
+
htmlbody,
|
|
38
|
+
track_clicks: true,
|
|
39
|
+
track_opens: true,
|
|
40
|
+
});
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
return response;
|
|
43
|
+
} catch (raw: really_any) {
|
|
44
|
+
if (!('error' in raw)) {
|
|
45
|
+
throw raw;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new Error(raw.error.message, raw.error.details);
|
|
49
|
+
}
|
|
42
50
|
}
|
|
43
51
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { really_any } from '@promptbook-local/types';
|
|
2
|
+
import { serializeError } from '@promptbook-local/utils';
|
|
3
|
+
import { assertsError } from '../../../../../src/errors/assertsError';
|
|
2
4
|
import { $getTableName } from '../../database/$getTableName';
|
|
3
5
|
import { $provideSupabaseForServer } from '../../database/$provideSupabaseForServer';
|
|
4
6
|
import { EMAIL_PROVIDERS } from '../../message-providers';
|
|
@@ -9,15 +11,11 @@ import { OutboundEmail } from '../../message-providers/email/_common/Email';
|
|
|
9
11
|
*/
|
|
10
12
|
export async function sendMessage(message: OutboundEmail): Promise<void> {
|
|
11
13
|
const supabase = await $provideSupabaseForServer();
|
|
12
|
-
// @ts-expect-error: Tables are not yet in types
|
|
13
|
-
const messageTable = await $getTableName('Message');
|
|
14
|
-
// @ts-expect-error: Tables are not yet in types
|
|
15
|
-
const messageSendAttemptTable = await $getTableName('MessageSendAttempt');
|
|
16
14
|
|
|
17
15
|
// 1. Insert message
|
|
18
16
|
const { data: insertedMessage, error: insertError } = await supabase
|
|
19
17
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
-
.from(
|
|
18
|
+
.from(await $getTableName('Message'))
|
|
21
19
|
.insert({
|
|
22
20
|
channel: message.channel || 'UNKNOWN',
|
|
23
21
|
direction: message.direction || 'OUTBOUND',
|
|
@@ -56,17 +54,19 @@ export async function sendMessage(message: OutboundEmail): Promise<void> {
|
|
|
56
54
|
let raw: really_any = null;
|
|
57
55
|
|
|
58
56
|
try {
|
|
57
|
+
console.log(`📤 Sending email via ${providerName}`);
|
|
59
58
|
raw = await provider.send(message);
|
|
60
59
|
isSuccessful = true;
|
|
61
60
|
isSent = true;
|
|
62
61
|
} catch (error) {
|
|
62
|
+
assertsError(error);
|
|
63
63
|
console.error(`Failed to send email via ${providerName}`, error);
|
|
64
|
-
raw = { error:
|
|
64
|
+
raw = { error: serializeError(error) };
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// 3. Log attempt
|
|
68
68
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
-
await supabase.from(
|
|
69
|
+
await supabase.from(await $getTableName('MessageSendAttempt')).insert({
|
|
70
70
|
// @ts-expect-error: insertedMessage is any
|
|
71
71
|
messageId: insertedMessage.id,
|
|
72
72
|
providerName,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Json } from '../database/schema';
|
|
2
|
+
|
|
3
|
+
export type MessageRow = {
|
|
4
|
+
id: number;
|
|
5
|
+
createdAt: string;
|
|
6
|
+
channel: string;
|
|
7
|
+
direction: string;
|
|
8
|
+
sender: Json;
|
|
9
|
+
recipients: Json;
|
|
10
|
+
content: string;
|
|
11
|
+
threadId: string | null;
|
|
12
|
+
metadata: Json;
|
|
13
|
+
// Joined fields
|
|
14
|
+
sendAttempts?: MessageSendAttemptRow[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type MessageSendAttemptRow = {
|
|
18
|
+
id: number;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
messageId: number;
|
|
21
|
+
providerName: string;
|
|
22
|
+
isSuccessful: boolean;
|
|
23
|
+
raw: Json;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type MessagesListResponse = {
|
|
27
|
+
items: MessageRow[];
|
|
28
|
+
total: number;
|
|
29
|
+
page: number;
|
|
30
|
+
pageSize: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type MessagesListParams = {
|
|
34
|
+
page?: number;
|
|
35
|
+
pageSize?: number;
|
|
36
|
+
search?: string;
|
|
37
|
+
channel?: string;
|
|
38
|
+
direction?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build query string for messages listing.
|
|
43
|
+
*/
|
|
44
|
+
function buildQuery(params: MessagesListParams): string {
|
|
45
|
+
const searchParams = new URLSearchParams();
|
|
46
|
+
|
|
47
|
+
if (params.page && params.page > 0) searchParams.set('page', String(params.page));
|
|
48
|
+
if (params.pageSize && params.pageSize > 0) searchParams.set('pageSize', String(params.pageSize));
|
|
49
|
+
if (params.search) searchParams.set('search', params.search);
|
|
50
|
+
if (params.channel) searchParams.set('channel', params.channel);
|
|
51
|
+
if (params.direction) searchParams.set('direction', params.direction);
|
|
52
|
+
|
|
53
|
+
const qs = searchParams.toString();
|
|
54
|
+
return qs ? `?${qs}` : '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Fetch messages from the admin API.
|
|
59
|
+
*/
|
|
60
|
+
export async function $fetchMessages(params: MessagesListParams = {}): Promise<MessagesListResponse> {
|
|
61
|
+
const qs = buildQuery(params);
|
|
62
|
+
const response = await fetch(`/api/messages${qs}`, {
|
|
63
|
+
method: 'GET',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const data = await response.json().catch(() => ({}));
|
|
68
|
+
throw new Error(data.error || 'Failed to load messages');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (await response.json()) as MessagesListResponse;
|
|
72
|
+
}
|