Backoffice stage 2: chats (support + concierge, files)
- ChatThread per client/provider and channel; messages get files, internal notes, source (bot/mini app) and delivery errors; reply templates. Migration moves old concierge messages into threads. - Routing: subscribers write to the concierge, others to support; /support and /concierge in the client bot; staff can take, transfer, close and move threads. - Telegram: photos/documents from clients and providers, replies with the channel header, files sent from storage, notifications only to the assignee or the channel's staff, "Reply" into a specific thread. - New admin: /chats with list, conversation, notes, files, templates, client panel; "Message" button on the client card; dashboard counts threads awaiting reply. - Client mini app: in-app chat screen sharing the same conversation. - Old admin keeps working on the same data (notes hidden there). - Tests for routing, notes, assign/close/move, lists, recipients; separate test DB via TEST_DB_NAME; dev:chats-demo script. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d452824c62
commit
23ffb2bf1d
46 files changed
+4176
-144
No files matched your search
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Приватное объектное хранилище (S3-совместимое) для файлов чатов — общее для API и воркера.
|
||||
* Настройки — из S3_* в .env.
|
||||
*/
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
|
||||
let client: S3Client | null = null;
|
||||
function s3() {
|
||||
client ??= new S3Client({
|
||||
endpoint: process.env.S3_ENDPOINT ?? 'http://localhost:9000',
|
||||
region: process.env.S3_REGION ?? 'us-east-1',
|
||||
forcePathStyle: true,
|
||||
credentials: { accessKeyId: process.env.S3_ACCESS_KEY ?? '', secretAccessKey: process.env.S3_SECRET_KEY ?? '' },
|
||||
});
|
||||
return client;
|
||||
}
|
||||
const bucket = () => process.env.S3_BUCKET ?? 'barev-private';
|
||||
|
||||
/** Расширение из имени файла (только буквы/цифры), иначе bin */
|
||||
function extOf(name: string | null | undefined) {
|
||||
const m = name?.match(/\.([a-z0-9]{1,8})$/i);
|
||||
return m ? m[1]!.toLowerCase() : 'bin';
|
||||
}
|
||||
|
||||
export async function putFile(prefix: string, body: Buffer, mime: string, fileName?: string | null) {
|
||||
const key = `${prefix}/${randomUUID()}.${extOf(fileName)}`;
|
||||
await s3().send(
|
||||
new PutObjectCommand({
|
||||
Bucket: bucket(),
|
||||
Key: key,
|
||||
Body: body,
|
||||
ContentType: mime,
|
||||
...(process.env.NODE_ENV === 'production' ? { ServerSideEncryption: 'AES256' as const } : {}),
|
||||
}),
|
||||
);
|
||||
return key;
|
||||
}
|
||||
|
||||
export async function getFile(key: string): Promise<Buffer> {
|
||||
const res = await s3().send(new GetObjectCommand({ Bucket: bucket(), Key: key }));
|
||||
return Buffer.from(await res.Body!.transformToByteArray());
|
||||
}
|
||||
Reference in new issue
Block a user