Files
mkd/services/domaGraphQLClient.ts
2026-02-04 00:17:04 +05:00

627 lines
18 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* GraphQL клиент для работы с Doma AI API
* Документация: https://developers.doma.ai/ru/docs/api/about
*/
import { settingsService, DomaAISettings } from './settingsService';
import { getAuthToken } from './apiClient';
export interface DomaGraphQLResponse<T> {
data?: T;
errors?: Array<{
message: string;
extensions?: any;
}>;
}
export interface DomaAuthResponse {
authenticateUserWithPassword: {
token: string;
item: {
id: string;
name: string;
email?: string;
phone?: string;
};
};
}
export interface DomaTicket {
id: string;
number: string;
status: {
id: string;
name: string;
type: string;
};
details?: string;
description?: string;
createdAt: string;
updatedAt: string;
deadline?: string;
property?: {
id: string;
address: string;
unitName?: string;
};
client?: {
id: string;
name: string;
phone?: string;
};
assignee?: {
id: string;
name: string;
};
category?: {
id: string;
name: string;
};
}
export interface DomaTicketsResponse {
objs: DomaTicket[];
meta?: {
count: number;
};
}
class DomaGraphQLClient {
private apiUrl: string | null = null;
private token: string | null = null;
private tokenStorageKey = 'doma_ai_token';
constructor() {
// Восстанавливаем токен из localStorage, если есть
const storedToken = localStorage.getItem(this.tokenStorageKey);
if (storedToken) {
this.token = storedToken;
}
// Настраиваем URL бэкенда, через который проксируем запросы в Doma AI
this.updateApiUrl();
}
/**
* Обновляет URL API до нашего backend-прокси, а не напрямую до Doma AI
*/
private updateApiUrl(): void {
const backendBaseUrl = import.meta.env.VITE_API_BASE_URL;
if (backendBaseUrl) {
// Все запросы к Doma AI теперь идут через backend-прокси
this.apiUrl = `${backendBaseUrl.replace(/\/$/, '')}/doma/graphql`;
return;
}
this.apiUrl = null;
console.warn(
'[DomaGraphQLClient] VITE_API_BASE_URL не задан. ' +
'Настройте URL бэкенда в переменных окружения фронтенда.'
);
}
/**
* Устанавливает настройки API из настроек интеграций
*/
updateSettings(): void {
this.updateApiUrl();
}
/**
* Аутентификация пользователя через email и пароль
* Если параметры не переданы, пытается получить из настроек
*/
async authenticate(email?: string, password?: string): Promise<string> {
// Если параметры не переданы, пытаемся получить из настроек
if (!email || !password) {
const settings = settingsService.getDomaAISettings();
if (settings?.email && settings?.password) {
email = settings.email;
password = settings.password;
} else {
throw new Error('Учетные данные не указаны. Укажите email и пароль в настройках интеграций.');
}
}
// Обновляем URL перед авторизацией
this.updateApiUrl();
const mutation = `
mutation AuthenticateUser($email: String!, $password: String!) {
authenticateUserWithPassword(email: $email, password: $password) {
token
item {
id
name
email
phone
}
}
}
`;
const response = await this.request<DomaAuthResponse>(mutation, {
email,
password,
});
if (response.errors) {
throw new Error(response.errors[0]?.message || 'Ошибка аутентификации');
}
if (response.data?.authenticateUserWithPassword?.token) {
this.token = response.data.authenticateUserWithPassword.token;
localStorage.setItem(this.tokenStorageKey, this.token);
return this.token;
}
throw new Error('Не удалось получить токен авторизации');
}
/**
* Аутентификация через телефон и пароль
* Если параметры не переданы, пытается получить из настроек
*/
async authenticateWithPhone(phone?: string, password?: string): Promise<string> {
// Если параметры не переданы, пытаемся получить из настроек
if (!phone || !password) {
const settings = settingsService.getDomaAISettings();
if (settings?.phone && settings?.password) {
phone = settings.phone;
password = settings.password;
} else {
throw new Error('Учетные данные не указаны. Укажите телефон и пароль в настройках интеграций.');
}
}
// Обновляем URL перед авторизацией
this.updateApiUrl();
const mutation = `
mutation AuthenticateUserWithPhone($phone: String!, $password: String!) {
authenticateUserWithPhoneAndPassword(phone: $phone, password: $password) {
token
item {
id
name
email
phone
}
}
}
`;
interface PhoneAuthResponse {
authenticateUserWithPhoneAndPassword: {
token: string;
item: {
id: string;
name: string;
email?: string;
phone?: string;
};
};
}
const response = await this.request<PhoneAuthResponse>(mutation, {
phone,
password,
});
if (response.errors) {
throw new Error(response.errors[0]?.message || 'Ошибка аутентификации');
}
if (response.data?.authenticateUserWithPhoneAndPassword?.token) {
this.token = response.data.authenticateUserWithPhoneAndPassword.token;
localStorage.setItem(this.tokenStorageKey, this.token);
return this.token;
}
throw new Error('Не удалось получить токен авторизации');
}
/**
* Проверка текущего пользователя
*/
async getAuthenticatedUser() {
const query = `
query {
authenticatedUser {
id
name
email
phone
type
}
}
`;
const response = await this.request(query);
return response.data;
}
/**
* Получение списка заявок (tickets)
* В Doma AI заявки могут называться Ticket, ServiceRequest или аналогично
*/
async getTickets(filters?: {
status?: string;
assignee?: string;
property?: string;
limit?: number;
skip?: number;
}): Promise<DomaTicket[]> {
// Пробуем разные возможные названия типов заявок в Doma AI
// В зависимости от вашей схемы это может быть Ticket, ServiceRequest, Request и т.д.
const query = `
query GetTickets($where: TicketWhereInput, $first: Int, $skip: Int) {
objs: tickets(where: $where, first: $first, skip: $skip, orderBy: createdAt_DESC) {
id
number
status {
id
name
type
}
details
description
createdAt
updatedAt
deadline
property {
id
address
unitName
}
client {
id
name
phone
}
assignee {
id
name
}
category {
id
name
}
}
}
`;
const variables: any = {
first: filters?.limit || 100,
skip: filters?.skip || 0,
};
if (filters) {
variables.where = {};
if (filters.status) {
variables.where.status = { id: filters.status };
}
if (filters.assignee) {
variables.where.assignee = { id: filters.assignee };
}
if (filters.property) {
variables.where.property = { id: filters.property };
}
}
try {
const response = await this.request<{ objs: DomaTicket[] }>(query, variables);
if (response.errors) {
console.warn('[DomaGraphQLClient] Ошибка при получении заявок:', response.errors);
// Пробуем альтернативный запрос
return await this.getTicketsAlternative(filters);
}
return response.data?.objs || [];
} catch (error) {
console.error('[DomaGraphQLClient] Ошибка при запросе заявок:', error);
// Пробуем альтернативный запрос
return await this.getTicketsAlternative(filters);
}
}
/**
* Альтернативный запрос заявок (если основная схема не работает)
*/
private async getTicketsAlternative(filters?: any): Promise<DomaTicket[]> {
// Пробуем запрос через ServiceRequest или другие возможные типы
const alternativeQueries = [
// Вариант 1: ServiceRequest
`
query GetServiceRequests($where: ServiceRequestWhereInput, $first: Int, $skip: Int) {
objs: serviceRequests(where: $where, first: $first, skip: $skip, orderBy: createdAt_DESC) {
id
number
status {
id
name
type
}
details
description
createdAt
updatedAt
deadline
property {
id
address
unitName
}
client {
id
name
phone
}
assignee {
id
name
}
category {
id
name
}
}
}
`,
// Вариант 2: Request
`
query GetRequests($where: RequestWhereInput, $first: Int, $skip: Int) {
objs: requests(where: $where, first: $first, skip: $skip, orderBy: createdAt_DESC) {
id
number
status {
id
name
type
}
details
description
createdAt
updatedAt
deadline
property {
id
address
unitName
}
client {
id
name
phone
}
assignee {
id
name
}
category {
id
name
}
}
}
`,
];
for (const query of alternativeQueries) {
try {
const variables: any = {
first: filters?.limit || 100,
skip: filters?.skip || 0,
};
if (filters) {
variables.where = {};
if (filters.status) {
variables.where.status = { id: filters.status };
}
}
const response = await this.request<{ objs: DomaTicket[] }>(query, variables);
if (response.data?.objs) {
return response.data.objs;
}
} catch (error) {
console.warn('[DomaGraphQLClient] Альтернативный запрос не сработал:', error);
continue;
}
}
return [];
}
/**
* Базовый метод для выполнения GraphQL запросов
*/
private async request<T>(
query: string,
variables?: Record<string, any>
): Promise<DomaGraphQLResponse<T>> {
if (!this.apiUrl) {
throw new Error('VITE_DOMA_AI_API_URL не настроен. Укажите URL вашего инстанса Doma AI.');
}
const headers: HeadersInit = {
'Content-Type': 'application/json',
};
const portalToken = getAuthToken();
if (portalToken) {
(headers as Record<string, string>)['Authorization'] = `Bearer ${portalToken}`;
}
// Важно: авторизационный токен к Doma AI больше не отправляем из браузера напрямую.
// Вместо этого передаём его в config.body на наш backend-прокси.
// Читаем актуальные настройки интеграции
const settings = settingsService.getDomaAISettings();
const config = {
apiUrl: settings?.apiUrl,
token: settings?.token || this.token || null,
};
try {
const bodyPayload = {
query,
variables: variables || {},
config,
};
console.log('[DomaGraphQLClient] Запрос к Doma AI', {
url: this.apiUrl,
hasToken: !!this.token,
// Лучше не логировать полный запрос, чтобы не засорять консоль,
// показываем только первые символы
queryPreview: query.slice(0, 100),
});
const response = await fetch(this.apiUrl, {
method: 'POST',
headers,
body: JSON.stringify(bodyPayload),
});
const rawText = await response.text();
console.log('[DomaGraphQLClient] Ответ Doma AI', {
status: response.status,
ok: response.ok,
length: rawText.length,
preview: rawText.slice(0, 300),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
if (!rawText) {
// Пустой ответ от Doma AI
return {} as DomaGraphQLResponse<T>;
}
const result: DomaGraphQLResponse<T> = JSON.parse(rawText);
// Если получили ошибку авторизации, очищаем токен
if (result.errors?.some(e => e.message.includes('auth') || e.message.includes('unauthorized'))) {
this.token = null;
localStorage.removeItem(this.tokenStorageKey);
throw new Error('Требуется повторная авторизация');
}
return result;
} catch (error) {
console.error('[DomaGraphQLClient] Ошибка запроса:', error);
throw error;
}
}
/**
* Установка токена вручную (если токен получен извне)
*/
setToken(token: string): void {
this.token = token;
localStorage.setItem(this.tokenStorageKey, token);
}
/**
* Очистка токена (выход)
*/
clearToken(): void {
this.token = null;
localStorage.removeItem(this.tokenStorageKey);
}
/**
* Проверка наличия токена
*/
hasToken(): boolean {
return !!this.token;
}
/**
* Обновление заявки (ticket) в Doma AI
* @param ticketId - ID заявки в Doma AI (UUID)
* @param data - Данные для обновления
*/
async updateTicket(
ticketId: string,
data: {
status?: { id?: string; type?: string };
statusReason?: string;
deferredUntil?: string;
lastCommentWithResidentTypeAt?: string;
}
): Promise<{ id: string; number: string }> {
const mutation = `
mutation UpdateTicket($id: ID!, $data: TicketUpdateInput!) {
obj: updateTicket(id: $id, data: $data) {
id
number
status {
id
name
type
}
updatedAt
}
}
`;
const variables: any = {
id: ticketId,
data: {},
};
// Маппинг локальных статусов в статусы Doma.AI
const statusTypeMap: Record<string, string> = {
new: 'new_or_reopened',
in_progress: 'processing',
deferred: 'deferred',
done: 'completed',
canceled: 'canceled',
};
if (data.status?.type) {
const domaStatusType = statusTypeMap[data.status.type] || data.status.type;
variables.data.status = { type: domaStatusType };
} else if (data.status?.id) {
variables.data.status = { id: data.status.id };
}
if (data.statusReason) {
variables.data.statusReason = data.statusReason;
}
if (data.deferredUntil) {
variables.data.deferredUntil = data.deferredUntil;
}
if (data.lastCommentWithResidentTypeAt) {
variables.data.lastCommentWithResidentTypeAt = data.lastCommentWithResidentTypeAt;
}
// Устанавливаем дату обновления статуса
variables.data.statusUpdatedAt = new Date().toISOString();
const response = await this.request<{ obj: { id: string; number: string } }>(mutation, variables);
if (response.errors) {
throw new Error(response.errors[0]?.message || 'Ошибка при обновлении заявки');
}
if (!response.data?.obj) {
throw new Error('Не удалось обновить заявку');
}
return response.data.obj;
}
}
export const domaGraphQLClient = new DomaGraphQLClient();