chore: rename service folders to lowercase

This commit is contained in:
Ruslan Bakiev
2026-02-20 12:10:25 +07:00
parent 0fdf5cf021
commit 46cca064df
71 changed files with 10 additions and 10 deletions

View File

@@ -0,0 +1,39 @@
import { readBody } from "h3";
import { graphql } from "graphql";
import { getAuthContext } from "../utils/auth";
import { crmGraphqlRoot, crmGraphqlSchema } from "../graphql/schema";
type GraphqlBody = {
query?: string;
operationName?: string;
variables?: Record<string, unknown>;
};
export default defineEventHandler(async (event) => {
const body = await readBody<GraphqlBody>(event);
if (!body?.query || !body.query.trim()) {
throw createError({ statusCode: 400, statusMessage: "GraphQL query is required" });
}
let auth = null;
try {
auth = await getAuthContext(event);
} catch {
auth = null;
}
const result = await graphql({
schema: crmGraphqlSchema,
source: body.query,
rootValue: crmGraphqlRoot,
contextValue: { auth, event },
variableValues: body.variables,
operationName: body.operationName,
});
return {
data: result.data ?? null,
errors: result.errors?.map((error) => ({ message: error.message })) ?? undefined,
};
});

View File

@@ -0,0 +1,60 @@
import { readBody } from "h3";
import { getAuthContext } from "../../../utils/auth";
import { prisma } from "../../../utils/prisma";
import { enqueueOutboundDelivery } from "../../../queues/outboundDelivery";
type EnqueueBody = {
omniMessageId?: string;
endpoint?: string;
method?: "POST" | "PUT" | "PATCH";
headers?: Record<string, string>;
payload?: unknown;
provider?: string;
channel?: string;
attempts?: number;
};
export default defineEventHandler(async (event) => {
const auth = await getAuthContext(event);
const body = await readBody<EnqueueBody>(event);
const omniMessageId = String(body?.omniMessageId ?? "").trim();
const endpoint = String(body?.endpoint ?? "").trim();
if (!omniMessageId) {
throw createError({ statusCode: 400, statusMessage: "omniMessageId is required" });
}
if (!endpoint) {
throw createError({ statusCode: 400, statusMessage: "endpoint is required" });
}
const msg = await prisma.omniMessage.findFirst({
where: { id: omniMessageId, teamId: auth.teamId },
select: { id: true },
});
if (!msg) {
throw createError({ statusCode: 404, statusMessage: "omni message not found" });
}
const attempts = Math.max(1, Math.min(Number(body?.attempts ?? 12), 50));
const job = await enqueueOutboundDelivery(
{
omniMessageId,
endpoint,
method: body?.method ?? "POST",
headers: body?.headers ?? {},
payload: body?.payload ?? {},
provider: body?.provider ?? undefined,
channel: body?.channel ?? undefined,
},
{
attempts,
},
);
return {
ok: true,
queue: "omni-outbound",
jobId: job.id,
omniMessageId,
};
});

View File

@@ -0,0 +1,32 @@
import { readBody } from "h3";
import { getAuthContext } from "../../../utils/auth";
import { prisma } from "../../../utils/prisma";
import { enqueueTelegramSend } from "../../../queues/telegramSend";
export default defineEventHandler(async (event) => {
const auth = await getAuthContext(event);
const body = await readBody<{ omniMessageId?: string; attempts?: number }>(event);
const omniMessageId = String(body?.omniMessageId ?? "").trim();
if (!omniMessageId) {
throw createError({ statusCode: 400, statusMessage: "omniMessageId is required" });
}
const msg = await prisma.omniMessage.findFirst({
where: { id: omniMessageId, teamId: auth.teamId, channel: "TELEGRAM", direction: "OUT" },
select: { id: true },
});
if (!msg) {
throw createError({ statusCode: 404, statusMessage: "telegram outbound message not found" });
}
const attempts = Math.max(1, Math.min(Number(body?.attempts ?? 12), 50));
const job = await enqueueTelegramSend({ omniMessageId }, { attempts });
return {
ok: true,
queue: "omni-outbound",
jobId: job.id,
omniMessageId,
};
});

View File

@@ -0,0 +1,182 @@
import { readBody } from "h3";
import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
import { getAuthContext } from "../utils/auth";
import { prisma } from "../utils/prisma";
import { buildChangeSet, captureSnapshot } from "../utils/changeSet";
import { persistChatMessage, runCrmAgentFor, type AgentTraceEvent } from "../agent/crmAgent";
import type { ChangeSet } from "../utils/changeSet";
function extractMessageText(message: any): string {
if (!message || !Array.isArray(message.parts)) return "";
return message.parts
.filter((part: any) => part?.type === "text" && typeof part.text === "string")
.map((part: any) => part.text)
.join("")
.trim();
}
function getLastUserText(messages: any[]): string {
for (let i = messages.length - 1; i >= 0; i -= 1) {
const message = messages[i];
if (message?.role !== "user") continue;
const text = extractMessageText(message);
if (text) return text;
}
return "";
}
function humanizeTraceText(trace: AgentTraceEvent): string {
if (trace.toolRun?.name) {
return `Использую инструмент: ${trace.toolRun.name}`;
}
const text = (trace.text ?? "").trim();
if (!text) return "Агент работает с данными CRM.";
if (text.toLowerCase().includes("ошиб")) return "Возникла ошибка шага, пробую другой путь.";
if (text.toLowerCase().includes("итог")) return "Готовлю финальный ответ.";
return text;
}
function renderChangeSetSummary(changeSet: ChangeSet): string {
const totals = { created: 0, updated: 0, deleted: 0 };
for (const item of changeSet.items) {
if (item.action === "created") totals.created += 1;
else if (item.action === "updated") totals.updated += 1;
else if (item.action === "deleted") totals.deleted += 1;
}
const byEntity = new Map<string, number>();
for (const item of changeSet.items) {
byEntity.set(item.entity, (byEntity.get(item.entity) ?? 0) + 1);
}
const lines = [
"Technical change summary",
`Total: ${changeSet.items.length} · Created: ${totals.created} · Updated: ${totals.updated} · Archived: ${totals.deleted}`,
...[...byEntity.entries()].map(([entity, count]) => `- ${entity}: ${count}`),
];
return lines.join("\n");
}
export default defineEventHandler(async (event) => {
const auth = await getAuthContext(event);
const body = await readBody<{ messages?: any[] }>(event);
const messages = Array.isArray(body?.messages) ? body.messages : [];
const userText = getLastUserText(messages);
if (!userText) {
throw createError({ statusCode: 400, statusMessage: "Last user message is required" });
}
const requestId = `req_${Date.now()}_${Math.floor(Math.random() * 1_000_000)}`;
const stream = createUIMessageStream({
execute: async ({ writer }) => {
const textId = `text-${Date.now()}`;
writer.write({ type: "start" });
try {
const snapshotBefore = await captureSnapshot(prisma, auth.teamId);
await persistChatMessage({
teamId: auth.teamId,
conversationId: auth.conversationId,
authorUserId: auth.userId,
role: "USER",
text: userText,
requestId,
eventType: "user",
phase: "final",
transient: false,
});
const reply = await runCrmAgentFor({
teamId: auth.teamId,
userId: auth.userId,
userText,
requestId,
conversationId: auth.conversationId,
onTrace: async (trace: AgentTraceEvent) => {
writer.write({
type: "data-agent-log",
data: {
requestId,
at: new Date().toISOString(),
text: humanizeTraceText(trace),
},
});
},
});
const snapshotAfter = await captureSnapshot(prisma, auth.teamId);
const changeSet = buildChangeSet(snapshotBefore, snapshotAfter);
await persistChatMessage({
teamId: auth.teamId,
conversationId: auth.conversationId,
authorUserId: null,
role: "ASSISTANT",
text: reply.text,
requestId,
eventType: "assistant",
phase: "final",
transient: false,
});
if (changeSet) {
await persistChatMessage({
teamId: auth.teamId,
conversationId: auth.conversationId,
authorUserId: null,
role: "ASSISTANT",
text: renderChangeSetSummary(changeSet),
requestId,
eventType: "note",
phase: "final",
transient: false,
messageKind: "change_set_summary",
changeSet,
});
}
writer.write({ type: "text-start", id: textId });
writer.write({ type: "text-delta", id: textId, delta: reply.text });
writer.write({ type: "text-end", id: textId });
writer.write({ type: "finish", finishReason: "stop" });
} catch (error: any) {
const errorText = String(error?.message ?? error);
await persistChatMessage({
teamId: auth.teamId,
conversationId: auth.conversationId,
authorUserId: null,
role: "ASSISTANT",
text: errorText,
requestId,
eventType: "assistant",
phase: "error",
transient: false,
});
writer.write({
type: "data-agent-log",
data: {
requestId,
at: new Date().toISOString(),
text: "Ошибка выполнения агентского цикла.",
},
});
writer.write({ type: "text-start", id: textId });
writer.write({
type: "text-delta",
id: textId,
delta: errorText,
});
writer.write({ type: "text-end", id: textId });
writer.write({ type: "finish", finishReason: "stop" });
}
},
});
return createUIMessageStreamResponse({ stream });
});

View File

@@ -0,0 +1,62 @@
import { readBody } from "h3";
import { getAuthContext } from "../utils/auth";
import { transcribeWithWhisper } from "../utils/whisper";
type TranscribeBody = {
audioBase64?: string;
sampleRate?: number;
language?: string;
};
function decodeBase64Pcm16(audioBase64: string) {
const pcmBuffer = Buffer.from(audioBase64, "base64");
if (pcmBuffer.length < 2) return new Float32Array();
const sampleCount = Math.floor(pcmBuffer.length / 2);
const out = new Float32Array(sampleCount);
for (let i = 0; i < sampleCount; i += 1) {
const lo = pcmBuffer[i * 2]!;
const hi = pcmBuffer[i * 2 + 1]!;
const int16 = (hi << 8) | lo;
const signed = int16 >= 0x8000 ? int16 - 0x10000 : int16;
out[i] = signed / 32768;
}
return out;
}
export default defineEventHandler(async (event) => {
await getAuthContext(event);
const body = await readBody<TranscribeBody>(event);
const audioBase64 = String(body?.audioBase64 ?? "").trim();
const sampleRateRaw = Number(body?.sampleRate ?? 0);
const language = String(body?.language ?? "").trim() || undefined;
if (!audioBase64) {
throw createError({ statusCode: 400, statusMessage: "audioBase64 is required" });
}
if (!Number.isFinite(sampleRateRaw) || sampleRateRaw < 8000 || sampleRateRaw > 48000) {
throw createError({ statusCode: 400, statusMessage: "sampleRate must be between 8000 and 48000" });
}
const samples = decodeBase64Pcm16(audioBase64);
if (!samples.length) {
throw createError({ statusCode: 400, statusMessage: "Audio is empty" });
}
const maxSamples = Math.floor(sampleRateRaw * 120);
if (samples.length > maxSamples) {
throw createError({ statusCode: 413, statusMessage: "Audio is too long (max 120s)" });
}
const text = await transcribeWithWhisper({
samples,
sampleRate: sampleRateRaw,
language,
});
return { text };
});