@glwhappen/web-code 1.32.1 → 1.32.3
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/assets/{index-CfT-2Nkf.js → index-B_OAjmCW.js} +270 -265
- package/dist/assets/index-CF_baW77.css +32 -0
- package/dist/index.html +2 -2
- package/dist-server/server/modules/database/index.js +1 -0
- package/dist-server/server/modules/database/index.js.map +1 -1
- package/dist-server/server/modules/database/migrations.js +3 -1
- package/dist-server/server/modules/database/migrations.js.map +1 -1
- package/dist-server/server/modules/database/repositories/queued-messages.db.integration.test.js +73 -0
- package/dist-server/server/modules/database/repositories/queued-messages.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/database/repositories/queued-messages.db.js +80 -0
- package/dist-server/server/modules/database/repositories/queued-messages.db.js.map +1 -0
- package/dist-server/server/modules/database/schema.js +18 -0
- package/dist-server/server/modules/database/schema.js.map +1 -1
- package/dist-server/server/modules/projects/index.js +1 -0
- package/dist-server/server/modules/projects/index.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +87 -0
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +1 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/package.json +1 -1
- package/server/modules/database/index.ts +1 -0
- package/server/modules/database/migrations.ts +3 -0
- package/server/modules/database/repositories/queued-messages.db.integration.test.ts +84 -0
- package/server/modules/database/repositories/queued-messages.db.ts +154 -0
- package/server/modules/database/schema.ts +19 -0
- package/server/modules/projects/index.ts +3 -0
- package/server/modules/providers/provider.routes.ts +109 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +1 -1
- package/dist/assets/index-Ct6oPUQk.css +0 -32
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { getConnection } from '@/modules/database/connection.js';
|
|
4
|
+
|
|
5
|
+
export type QueuedMessageRow = {
|
|
6
|
+
id: string;
|
|
7
|
+
user_id: number;
|
|
8
|
+
session_id: string;
|
|
9
|
+
provider: string;
|
|
10
|
+
content: string;
|
|
11
|
+
permission_mode: string | null;
|
|
12
|
+
model: string | null;
|
|
13
|
+
metadata_json: string | null;
|
|
14
|
+
created_at: string;
|
|
15
|
+
updated_at: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type QueuedMessage = {
|
|
19
|
+
id: string;
|
|
20
|
+
userId: number;
|
|
21
|
+
sessionId: string;
|
|
22
|
+
provider: string;
|
|
23
|
+
content: string;
|
|
24
|
+
permissionMode: string | null;
|
|
25
|
+
model: string | null;
|
|
26
|
+
metadata: unknown;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
updatedAt: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type CreateQueuedMessageInput = {
|
|
32
|
+
provider: string;
|
|
33
|
+
content: string;
|
|
34
|
+
permissionMode?: string | null;
|
|
35
|
+
model?: string | null;
|
|
36
|
+
metadata?: unknown;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type UpdateQueuedMessageInput = Partial<Pick<CreateQueuedMessageInput, 'content' | 'permissionMode' | 'model' | 'metadata'>>;
|
|
40
|
+
|
|
41
|
+
function serializeMetadata(metadata: unknown): string | null {
|
|
42
|
+
if (metadata === undefined || metadata === null) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return JSON.stringify(metadata);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseMetadata(metadataJson: string | null): unknown {
|
|
49
|
+
if (!metadataJson) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(metadataJson);
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function mapQueuedMessage(row: QueuedMessageRow): QueuedMessage {
|
|
61
|
+
return {
|
|
62
|
+
id: row.id,
|
|
63
|
+
userId: row.user_id,
|
|
64
|
+
sessionId: row.session_id,
|
|
65
|
+
provider: row.provider,
|
|
66
|
+
content: row.content,
|
|
67
|
+
permissionMode: row.permission_mode,
|
|
68
|
+
model: row.model,
|
|
69
|
+
metadata: parseMetadata(row.metadata_json),
|
|
70
|
+
createdAt: row.created_at,
|
|
71
|
+
updatedAt: row.updated_at,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const queuedMessagesDb = {
|
|
76
|
+
listBySession(userId: number, sessionId: string): QueuedMessage[] {
|
|
77
|
+
const db = getConnection();
|
|
78
|
+
const rows = db
|
|
79
|
+
.prepare(
|
|
80
|
+
`SELECT id, user_id, session_id, provider, content, permission_mode, model, metadata_json, created_at, updated_at
|
|
81
|
+
FROM queued_messages
|
|
82
|
+
WHERE user_id = ? AND session_id = ?
|
|
83
|
+
ORDER BY rowid ASC`
|
|
84
|
+
)
|
|
85
|
+
.all(userId, sessionId) as QueuedMessageRow[];
|
|
86
|
+
|
|
87
|
+
return rows.map(mapQueuedMessage);
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
create(userId: number, sessionId: string, input: CreateQueuedMessageInput): QueuedMessage {
|
|
91
|
+
const db = getConnection();
|
|
92
|
+
const id = randomUUID();
|
|
93
|
+
db.prepare(
|
|
94
|
+
`INSERT INTO queued_messages (id, user_id, session_id, provider, content, permission_mode, model, metadata_json)
|
|
95
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
96
|
+
).run(
|
|
97
|
+
id,
|
|
98
|
+
userId,
|
|
99
|
+
sessionId,
|
|
100
|
+
input.provider,
|
|
101
|
+
input.content,
|
|
102
|
+
input.permissionMode ?? null,
|
|
103
|
+
input.model ?? null,
|
|
104
|
+
serializeMetadata(input.metadata),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return this.getById(userId, sessionId, id)!;
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
getById(userId: number, sessionId: string, id: string): QueuedMessage | null {
|
|
111
|
+
const db = getConnection();
|
|
112
|
+
const row = db
|
|
113
|
+
.prepare(
|
|
114
|
+
`SELECT id, user_id, session_id, provider, content, permission_mode, model, metadata_json, created_at, updated_at
|
|
115
|
+
FROM queued_messages
|
|
116
|
+
WHERE user_id = ? AND session_id = ? AND id = ?
|
|
117
|
+
LIMIT 1`
|
|
118
|
+
)
|
|
119
|
+
.get(userId, sessionId, id) as QueuedMessageRow | undefined;
|
|
120
|
+
|
|
121
|
+
return row ? mapQueuedMessage(row) : null;
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
update(userId: number, sessionId: string, id: string, input: UpdateQueuedMessageInput): QueuedMessage | null {
|
|
125
|
+
const existing = this.getById(userId, sessionId, id);
|
|
126
|
+
if (!existing) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const db = getConnection();
|
|
131
|
+
db.prepare(
|
|
132
|
+
`UPDATE queued_messages
|
|
133
|
+
SET content = ?, permission_mode = ?, model = ?, metadata_json = ?, updated_at = CURRENT_TIMESTAMP
|
|
134
|
+
WHERE user_id = ? AND session_id = ? AND id = ?`
|
|
135
|
+
).run(
|
|
136
|
+
input.content ?? existing.content,
|
|
137
|
+
input.permissionMode ?? existing.permissionMode,
|
|
138
|
+
input.model ?? existing.model,
|
|
139
|
+
input.metadata === undefined ? serializeMetadata(existing.metadata) : serializeMetadata(input.metadata),
|
|
140
|
+
userId,
|
|
141
|
+
sessionId,
|
|
142
|
+
id,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return this.getById(userId, sessionId, id);
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
delete(userId: number, sessionId: string, id: string): boolean {
|
|
149
|
+
const db = getConnection();
|
|
150
|
+
return db
|
|
151
|
+
.prepare('DELETE FROM queued_messages WHERE user_id = ? AND session_id = ? AND id = ?')
|
|
152
|
+
.run(userId, sessionId, id).changes > 0;
|
|
153
|
+
},
|
|
154
|
+
};
|
|
@@ -102,6 +102,22 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
102
102
|
);
|
|
103
103
|
`;
|
|
104
104
|
|
|
105
|
+
export const QUEUED_MESSAGES_TABLE_SCHEMA_SQL = `
|
|
106
|
+
CREATE TABLE IF NOT EXISTS queued_messages (
|
|
107
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
108
|
+
user_id INTEGER NOT NULL,
|
|
109
|
+
session_id TEXT NOT NULL,
|
|
110
|
+
provider TEXT NOT NULL,
|
|
111
|
+
content TEXT NOT NULL,
|
|
112
|
+
permission_mode TEXT,
|
|
113
|
+
model TEXT,
|
|
114
|
+
metadata_json TEXT,
|
|
115
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
116
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
117
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
118
|
+
);
|
|
119
|
+
`;
|
|
120
|
+
|
|
105
121
|
export const LAST_SCANNED_AT_SQL = `
|
|
106
122
|
CREATE TABLE IF NOT EXISTS scan_state (
|
|
107
123
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
@@ -153,6 +169,9 @@ CREATE INDEX IF NOT EXISTS idx_session_ids_lookup ON sessions(session_id);
|
|
|
153
169
|
-- NOTE: This index is created in migrations after sessions is rebuilt to include project_path.
|
|
154
170
|
-- Creating it here can fail on upgraded installs where the legacy sessions table has no project_path.
|
|
155
171
|
|
|
172
|
+
${QUEUED_MESSAGES_TABLE_SCHEMA_SQL}
|
|
173
|
+
CREATE INDEX IF NOT EXISTS idx_queued_messages_session ON queued_messages(user_id, session_id, created_at);
|
|
174
|
+
|
|
156
175
|
${LAST_SCANNED_AT_SQL}
|
|
157
176
|
|
|
158
177
|
${APP_CONFIG_TABLE_SCHEMA_SQL}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import express, { type Request, type Response } from 'express';
|
|
2
2
|
|
|
3
|
+
import { queuedMessagesDb } from '@/modules/database/index.js';
|
|
3
4
|
import { providerAuthService } from '@/modules/providers/services/provider-auth.service.js';
|
|
4
5
|
import { providerMcpService } from '@/modules/providers/services/mcp.service.js';
|
|
5
6
|
import { providerSkillsService } from '@/modules/providers/services/skills.service.js';
|
|
@@ -52,6 +53,8 @@ const normalizeProviderParam = (value: unknown): string =>
|
|
|
52
53
|
readPathParam(value, 'provider').trim().toLowerCase();
|
|
53
54
|
|
|
54
55
|
const SESSION_ID_PATTERN = /^[a-zA-Z0-9._-]{1,120}$/;
|
|
56
|
+
const QUEUED_MESSAGE_ID_PATTERN = /^[a-zA-Z0-9._-]{1,120}$/;
|
|
57
|
+
const MAX_QUEUED_MESSAGE_LENGTH = 100_000;
|
|
55
58
|
|
|
56
59
|
const parseSessionId = (value: unknown): string => {
|
|
57
60
|
const sessionId = readPathParam(value, 'sessionId').trim();
|
|
@@ -65,6 +68,18 @@ const parseSessionId = (value: unknown): string => {
|
|
|
65
68
|
return sessionId;
|
|
66
69
|
};
|
|
67
70
|
|
|
71
|
+
const parseQueuedMessageId = (value: unknown): string => {
|
|
72
|
+
const queueId = readPathParam(value, 'queueId').trim();
|
|
73
|
+
if (!QUEUED_MESSAGE_ID_PATTERN.test(queueId)) {
|
|
74
|
+
throw new AppError('Invalid queueId.', {
|
|
75
|
+
code: 'INVALID_QUEUE_ID',
|
|
76
|
+
statusCode: 400,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return queueId;
|
|
81
|
+
};
|
|
82
|
+
|
|
68
83
|
const readOptionalQueryString = (value: unknown): string | undefined => {
|
|
69
84
|
if (typeof value !== 'string') {
|
|
70
85
|
return undefined;
|
|
@@ -206,6 +221,46 @@ const parseProvider = (value: unknown): LLMProvider => {
|
|
|
206
221
|
});
|
|
207
222
|
};
|
|
208
223
|
|
|
224
|
+
const parseQueuedMessagePayload = (payload: unknown) => {
|
|
225
|
+
if (!payload || typeof payload !== 'object') {
|
|
226
|
+
throw new AppError('Request body must be an object.', {
|
|
227
|
+
code: 'INVALID_REQUEST_BODY',
|
|
228
|
+
statusCode: 400,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const body = payload as Record<string, unknown>;
|
|
233
|
+
const content = typeof body.content === 'string' ? body.content.trim() : '';
|
|
234
|
+
if (!content) {
|
|
235
|
+
throw new AppError('content is required.', {
|
|
236
|
+
code: 'QUEUED_MESSAGE_CONTENT_REQUIRED',
|
|
237
|
+
statusCode: 400,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (content.length > MAX_QUEUED_MESSAGE_LENGTH) {
|
|
241
|
+
throw new AppError('content is too long.', {
|
|
242
|
+
code: 'QUEUED_MESSAGE_CONTENT_TOO_LONG',
|
|
243
|
+
statusCode: 400,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const provider = typeof body.provider === 'string' ? body.provider.trim().toLowerCase() : '';
|
|
248
|
+
if (provider !== 'claude' && provider !== 'codex' && provider !== 'cursor' && provider !== 'gemini') {
|
|
249
|
+
throw new AppError('provider is invalid.', {
|
|
250
|
+
code: 'INVALID_PROVIDER',
|
|
251
|
+
statusCode: 400,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
content,
|
|
257
|
+
provider,
|
|
258
|
+
permissionMode: typeof body.permissionMode === 'string' ? body.permissionMode : null,
|
|
259
|
+
model: typeof body.model === 'string' ? body.model : null,
|
|
260
|
+
metadata: body.metadata,
|
|
261
|
+
};
|
|
262
|
+
};
|
|
263
|
+
|
|
209
264
|
const parseSessionRenameSummary = (payload: unknown): string => {
|
|
210
265
|
if (!payload || typeof payload !== 'object') {
|
|
211
266
|
throw new AppError('Request body must be an object.', {
|
|
@@ -391,6 +446,60 @@ router.put(
|
|
|
391
446
|
}),
|
|
392
447
|
);
|
|
393
448
|
|
|
449
|
+
router.get(
|
|
450
|
+
'/sessions/:sessionId/queued-messages',
|
|
451
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
452
|
+
const userId = getAuthenticatedUserId(req);
|
|
453
|
+
const sessionId = parseSessionId(req.params.sessionId);
|
|
454
|
+
const messages = queuedMessagesDb.listBySession(userId, sessionId);
|
|
455
|
+
res.json(createApiSuccessResponse({ messages }));
|
|
456
|
+
}),
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
router.post(
|
|
460
|
+
'/sessions/:sessionId/queued-messages',
|
|
461
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
462
|
+
const userId = getAuthenticatedUserId(req);
|
|
463
|
+
const sessionId = parseSessionId(req.params.sessionId);
|
|
464
|
+
const message = queuedMessagesDb.create(userId, sessionId, parseQueuedMessagePayload(req.body));
|
|
465
|
+
res.status(201).json(createApiSuccessResponse({ message }));
|
|
466
|
+
}),
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
router.put(
|
|
470
|
+
'/sessions/:sessionId/queued-messages/:queueId',
|
|
471
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
472
|
+
const userId = getAuthenticatedUserId(req);
|
|
473
|
+
const sessionId = parseSessionId(req.params.sessionId);
|
|
474
|
+
const queueId = parseQueuedMessageId(req.params.queueId);
|
|
475
|
+
const message = queuedMessagesDb.update(userId, sessionId, queueId, parseQueuedMessagePayload(req.body));
|
|
476
|
+
if (!message) {
|
|
477
|
+
throw new AppError('Queued message not found.', {
|
|
478
|
+
code: 'QUEUED_MESSAGE_NOT_FOUND',
|
|
479
|
+
statusCode: 404,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
res.json(createApiSuccessResponse({ message }));
|
|
483
|
+
}),
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
router.delete(
|
|
487
|
+
'/sessions/:sessionId/queued-messages/:queueId',
|
|
488
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
489
|
+
const userId = getAuthenticatedUserId(req);
|
|
490
|
+
const sessionId = parseSessionId(req.params.sessionId);
|
|
491
|
+
const queueId = parseQueuedMessageId(req.params.queueId);
|
|
492
|
+
const deleted = queuedMessagesDb.delete(userId, sessionId, queueId);
|
|
493
|
+
if (!deleted) {
|
|
494
|
+
throw new AppError('Queued message not found.', {
|
|
495
|
+
code: 'QUEUED_MESSAGE_NOT_FOUND',
|
|
496
|
+
statusCode: 404,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
res.json(createApiSuccessResponse({ deleted: true }));
|
|
500
|
+
}),
|
|
501
|
+
);
|
|
502
|
+
|
|
394
503
|
router.get(
|
|
395
504
|
'/sessions/:sessionId/messages',
|
|
396
505
|
asyncHandler(async (req: Request, res: Response) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { WebSocket } from 'ws';
|
|
2
2
|
|
|
3
|
-
import { assertUserOwnsProjectPath } from '@/modules/projects/
|
|
3
|
+
import { assertUserOwnsProjectPath } from '@/modules/projects/index.js';
|
|
4
4
|
import { connectedClients } from '@/modules/websocket/services/websocket-state.service.js';
|
|
5
5
|
import { WebSocketWriter } from '@/modules/websocket/services/websocket-writer.service.js';
|
|
6
6
|
import type {
|