Livrare Lot 3 (Frontend): aplicație web, aplicație mobilă Android, extensie browser
- Surse complete web (React/Vite) + mobil (React Native/Expo) + extensie (MV3) - Documentație de livrare: ghid utilizare, matrice trasabilitate cerințe, raport testare furnizor - Artefacte binare: imagine Docker didi-frontend:lot3-1.0, APK, extensie v3.2.6 + SHA256SUMS - Configurare adresă platformă externalizată (build args / .env / config.js) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
cec967f953
321 changed files with 80506 additions and 0 deletions
136
android/didi-app-source/src/api/analysis.ts
Normal file
136
android/didi-app-source/src/api/analysis.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { skillsApi } from './axios';
|
||||
import { TIMEOUTS } from './config';
|
||||
import type {
|
||||
UploadResponse,
|
||||
AnalysisSession,
|
||||
V3Response,
|
||||
V3QueueStatus,
|
||||
V3AsyncSubmitData,
|
||||
} from '../types/analysis';
|
||||
|
||||
export async function uploadMedia(
|
||||
fileUri: string,
|
||||
fileName: string,
|
||||
fileType: string,
|
||||
userId: string
|
||||
): Promise<string> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', {
|
||||
uri: fileUri,
|
||||
name: fileName,
|
||||
type: fileType,
|
||||
} as any);
|
||||
formData.append('user_id', userId);
|
||||
|
||||
const response = await skillsApi.post<V3Response<UploadResponse['data']>>('media/upload', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: TIMEOUTS.UPLOAD,
|
||||
});
|
||||
|
||||
const data = response.data?.data;
|
||||
return data?.public_url || '';
|
||||
}
|
||||
|
||||
const ALLOWED_SKILLS = ['techniques', 'claims', 'ai-tampered', 'domain', 'context', 'source-assessment'];
|
||||
|
||||
export async function analyzeSkill(
|
||||
skill: string,
|
||||
body: { text?: string; media_url?: string; user_id?: string; [key: string]: any }
|
||||
): Promise<AnalysisSession> {
|
||||
if (!ALLOWED_SKILLS.includes(skill)) {
|
||||
throw new Error(`Invalid skill: ${skill}`);
|
||||
}
|
||||
const endpoint = body.media_url
|
||||
? `${skill}/analyze-media`
|
||||
: `${skill}/analyze`;
|
||||
const response = await skillsApi.post<V3Response>(
|
||||
endpoint,
|
||||
body,
|
||||
{ timeout: TIMEOUTS.SKILL }
|
||||
);
|
||||
return response.data?.data || response.data as any;
|
||||
}
|
||||
|
||||
export async function analyzePipeline(
|
||||
body: { text?: string; media_url?: string; media_type?: string; user_id?: string; plan_type?: number; [key: string]: any }
|
||||
): Promise<V3Response> {
|
||||
const endpoint = body.media_url
|
||||
? 'pipeline/analyze-media'
|
||||
: 'pipeline/analyze';
|
||||
const response = await skillsApi.post<V3Response>(
|
||||
endpoint,
|
||||
body,
|
||||
{ timeout: TIMEOUTS.SKILL }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function submitPipelineAsync(
|
||||
body: Record<string, any>
|
||||
): Promise<{ session_id: string; async: boolean; data: V3AsyncSubmitData | AnalysisSession }> {
|
||||
const response = await skillsApi.post<V3Response<V3AsyncSubmitData>>(
|
||||
'pipeline/analyze-async',
|
||||
body,
|
||||
{ timeout: TIMEOUTS.SKILL }
|
||||
);
|
||||
|
||||
const res = response.data;
|
||||
|
||||
// Async path: 202 — session_id is inside data
|
||||
if (res.async && res.data?.session_id) {
|
||||
return { session_id: res.data.session_id, async: true, data: res.data };
|
||||
}
|
||||
|
||||
// Sync fallback: RabbitMQ down — full result returned directly
|
||||
return { session_id: (res.data as any)?.session_id || '', async: false, data: res.data };
|
||||
}
|
||||
|
||||
export async function submitSkillAsync(
|
||||
skill: string,
|
||||
body: Record<string, any>
|
||||
): Promise<{ session_id: string; async: boolean; data: V3AsyncSubmitData | AnalysisSession }> {
|
||||
if (!ALLOWED_SKILLS.includes(skill)) {
|
||||
throw new Error(`Invalid skill: ${skill}`);
|
||||
}
|
||||
const response = await skillsApi.post<V3Response<V3AsyncSubmitData>>(
|
||||
`${skill}/analyze-async`,
|
||||
body,
|
||||
{ timeout: TIMEOUTS.SKILL }
|
||||
);
|
||||
|
||||
const res = response.data;
|
||||
|
||||
// Async path: session_id is inside data
|
||||
if (res.async && res.data?.session_id) {
|
||||
return { session_id: res.data.session_id, async: true, data: res.data };
|
||||
}
|
||||
|
||||
// Sync fallback: full result returned directly
|
||||
return { session_id: (res.data as any)?.session_id || '', async: false, data: res.data };
|
||||
}
|
||||
|
||||
export async function pollQueueStatus(sessionId: string, signal?: AbortSignal): Promise<V3QueueStatus> {
|
||||
const response = await skillsApi.get<V3Response<AnalysisSession>>(
|
||||
`pipeline/${encodeURIComponent(sessionId)}/queue-status`,
|
||||
{ signal }
|
||||
);
|
||||
|
||||
const session = response.data?.data;
|
||||
const queue = session?._queue;
|
||||
|
||||
return {
|
||||
session,
|
||||
progress: queue?.progress || 0,
|
||||
total_components: queue?.total_components || 5,
|
||||
completed_components: queue?.completed_components || [],
|
||||
status: session?.status || 'pending',
|
||||
};
|
||||
}
|
||||
|
||||
export async function getPipelineResult(sessionId: string, signal?: AbortSignal): Promise<AnalysisSession> {
|
||||
const response = await skillsApi.get<V3Response>(
|
||||
`pipeline/${encodeURIComponent(sessionId)}/result`,
|
||||
{ signal }
|
||||
);
|
||||
return response.data?.data || response.data as any;
|
||||
}
|
||||
41
android/didi-app-source/src/api/auth.ts
Normal file
41
android/didi-app-source/src/api/auth.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import axios from 'axios';
|
||||
import { AUTH_CONFIG, getRedirectUri } from './config';
|
||||
import { mainApi } from './axios';
|
||||
import { TokenResponse, UserProfile } from '../types/auth';
|
||||
|
||||
export async function exchangeCodeForTokens(
|
||||
code: string,
|
||||
codeVerifier: string
|
||||
): Promise<TokenResponse> {
|
||||
const params = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: AUTH_CONFIG.CLIENT_ID,
|
||||
code,
|
||||
redirect_uri: getRedirectUri(),
|
||||
code_verifier: codeVerifier,
|
||||
});
|
||||
|
||||
const response = await axios.post(AUTH_CONFIG.TOKEN_URL, params.toString(), {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function fetchProfile(): Promise<UserProfile> {
|
||||
const response = await mainApi.get('auth/me');
|
||||
// API returns { success: true, data: { ... } }
|
||||
return response.data?.data || response.data;
|
||||
}
|
||||
|
||||
export async function fetchSubscriptionUsage() {
|
||||
const response = await mainApi.get('subscriptions/usage');
|
||||
// API returns { success: true, data: { credits, plan, subscription } }
|
||||
return response.data?.data || response.data;
|
||||
}
|
||||
|
||||
export async function fetchCredits(): Promise<{ credits_remained: number; credits_spent: number }> {
|
||||
const response = await mainApi.get('auth/credits');
|
||||
return response.data?.data || response.data;
|
||||
}
|
||||
62
android/didi-app-source/src/api/axios.ts
Normal file
62
android/didi-app-source/src/api/axios.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import axios from 'axios';
|
||||
import { Platform } from 'react-native';
|
||||
import { API_CONFIG } from './config';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
|
||||
const USER_AGENT =
|
||||
'Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36 DidiApp/1.0';
|
||||
|
||||
// Browser forbids setting User-Agent — only set on native
|
||||
const nativeHeaders = Platform.OS !== 'web' ? { 'User-Agent': USER_AGENT } : {};
|
||||
|
||||
if (Platform.OS !== 'web') {
|
||||
axios.defaults.headers.common['User-Agent'] = USER_AGENT;
|
||||
}
|
||||
|
||||
export const skillsApi = axios.create({
|
||||
baseURL: API_CONFIG.SKILLS_API,
|
||||
timeout: 300000,
|
||||
headers: nativeHeaders,
|
||||
});
|
||||
|
||||
export const mainApi = axios.create({
|
||||
baseURL: API_CONFIG.MAIN_API,
|
||||
timeout: 30000,
|
||||
headers: nativeHeaders,
|
||||
});
|
||||
|
||||
// Request interceptor - attach token & auto-refresh
|
||||
const attachToken = async (config: any) => {
|
||||
const { accessToken, expiresAt, refreshAccessToken } = useAuthStore.getState();
|
||||
|
||||
if (expiresAt && Date.now() > expiresAt - 30000) {
|
||||
const newToken = await refreshAccessToken();
|
||||
if (newToken) {
|
||||
config.headers.Authorization = `Bearer ${newToken}`;
|
||||
}
|
||||
} else if (accessToken) {
|
||||
config.headers.Authorization = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
// Response interceptor - retry on 401 (max once to prevent infinite loops)
|
||||
const handle401 = async (error: any) => {
|
||||
const config = error.config;
|
||||
if (error.response?.status === 401 && !config?._retried) {
|
||||
config._retried = true;
|
||||
const newToken = await useAuthStore.getState().refreshAccessToken();
|
||||
if (newToken) {
|
||||
config.headers.Authorization = `Bearer ${newToken}`;
|
||||
return axios.request(config);
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
};
|
||||
|
||||
skillsApi.interceptors.request.use(attachToken);
|
||||
skillsApi.interceptors.response.use((r) => r, handle401);
|
||||
|
||||
mainApi.interceptors.request.use(attachToken);
|
||||
mainApi.interceptors.response.use((r) => r, handle401);
|
||||
58
android/didi-app-source/src/api/config.ts
Normal file
58
android/didi-app-source/src/api/config.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// Adresa platformei DiDi se setează EXCLUSIV la build, prin variabila de mediu
|
||||
// EXPO_PUBLIC_DIDI_BASE_URL (fișierul .env din rădăcina proiectului — vezi .env.example).
|
||||
// Toate endpoint-urile (auth prin prefixul /auth al Keycloak, API-uri prin gateway) derivă
|
||||
// din această singură valoare; nu hardcoda adrese în cod.
|
||||
const DIDI_BASE_URL = (process.env.EXPO_PUBLIC_DIDI_BASE_URL ?? '').replace(/\/+$/, '');
|
||||
if (!DIDI_BASE_URL) {
|
||||
console.error('[CONFIG] EXPO_PUBLIC_DIDI_BASE_URL nu este setat — vezi .env.example');
|
||||
}
|
||||
const AUTH_BASE_URL = `${DIDI_BASE_URL}/auth`;
|
||||
const KEYCLOAK_REALM = 'didi-clients';
|
||||
const REALM_BASE = `${AUTH_BASE_URL}/realms/${KEYCLOAK_REALM}`;
|
||||
const OIDC_BASE = `${REALM_BASE}/protocol/openid-connect`;
|
||||
|
||||
export const AUTH_CONFIG = {
|
||||
AUTH_BASE_URL,
|
||||
KEYCLOAK_REALM,
|
||||
CLIENT_ID: 'didi-mobile-app',
|
||||
REDIRECT_URI: 'didi://callback',
|
||||
REDIRECT_URI_WEB: typeof window !== 'undefined' && typeof window.location !== 'undefined'
|
||||
? `${window.location.origin}/auth/callback`
|
||||
: 'http://localhost:8081/auth/callback',
|
||||
SCOPES: 'openid profile email roles',
|
||||
|
||||
AUTH_URL: `${OIDC_BASE}/auth`,
|
||||
TOKEN_URL: `${OIDC_BASE}/token`,
|
||||
REGISTER_URL: `${OIDC_BASE}/registrations`,
|
||||
USERINFO_URL: `${OIDC_BASE}/userinfo`,
|
||||
LOGOUT_URL: `${OIDC_BASE}/logout`,
|
||||
PASSWORD_URL: `${REALM_BASE}/account/credentials/password`,
|
||||
};
|
||||
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
export function getRedirectUri(): string {
|
||||
return Platform.OS === 'web' ? AUTH_CONFIG.REDIRECT_URI_WEB : AUTH_CONFIG.REDIRECT_URI;
|
||||
}
|
||||
|
||||
export const API_CONFIG = {
|
||||
MAIN_API: `${DIDI_BASE_URL}/api/`,
|
||||
MAIN_API_V1: `${DIDI_BASE_URL}/api/v1/`,
|
||||
SKILLS_API: `${DIDI_BASE_URL}/agent-v3/api/v3/`,
|
||||
SCRAPER_API: `${DIDI_BASE_URL}/scraper/`,
|
||||
};
|
||||
|
||||
export const TIMEOUTS = {
|
||||
AUTH: 30000,
|
||||
PIPELINE_TEXT: 120000,
|
||||
PIPELINE_URL: 120000,
|
||||
PIPELINE_MEDIA: 300000,
|
||||
SKILL: 300000,
|
||||
UPLOAD: 60000,
|
||||
SCRAPER: 120000,
|
||||
};
|
||||
|
||||
export const POLL_CONFIG = {
|
||||
INTERVAL: 2500,
|
||||
MAX_DURATION: 600000,
|
||||
};
|
||||
42
android/didi-app-source/src/api/history.ts
Normal file
42
android/didi-app-source/src/api/history.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { skillsApi } from './axios';
|
||||
import type {
|
||||
AnalysisSession,
|
||||
V3HistoryResponse,
|
||||
V3Response,
|
||||
} from '../types/analysis';
|
||||
|
||||
export async function getHistory(
|
||||
userId: string,
|
||||
page: number = 1,
|
||||
limit: number = 20
|
||||
): Promise<{ items: AnalysisSession[]; pagination: V3HistoryResponse['data']['pagination'] }> {
|
||||
const params = new URLSearchParams({
|
||||
user_id: userId,
|
||||
page: String(page),
|
||||
limit: String(limit),
|
||||
});
|
||||
const response = await skillsApi.get<V3HistoryResponse>(
|
||||
`pipeline/history?${params.toString()}`
|
||||
);
|
||||
const data = response.data?.data || response.data;
|
||||
return {
|
||||
items: Array.isArray((data as any)?.items) ? (data as any).items : [],
|
||||
pagination: (data as any)?.pagination || { page, limit, total: 0, pages: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
export async function getHistoryDetail(
|
||||
sessionId: string,
|
||||
userId: string
|
||||
): Promise<AnalysisSession> {
|
||||
const response = await skillsApi.get<V3Response>(
|
||||
`pipeline/history/${encodeURIComponent(sessionId)}?user_id=${encodeURIComponent(userId)}`
|
||||
);
|
||||
return response.data?.data || response.data as any;
|
||||
}
|
||||
|
||||
export async function deleteHistoryItem(sessionId: string, userId: string): Promise<void> {
|
||||
await skillsApi.delete(
|
||||
`pipeline/history/${encodeURIComponent(sessionId)}?user_id=${encodeURIComponent(userId)}`
|
||||
);
|
||||
}
|
||||
48
android/didi-app-source/src/api/techniqueDefinitions.ts
Normal file
48
android/didi-app-source/src/api/techniqueDefinitions.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { skillsApi } from './axios';
|
||||
import type { TechniqueDefinition } from '../types/analysis';
|
||||
|
||||
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
let cache: TechniqueDefinition[] | null = null;
|
||||
let cacheTimestamp = 0;
|
||||
let pendingRequest: Promise<TechniqueDefinition[]> | null = null;
|
||||
|
||||
export async function getTechniqueDefinitions(): Promise<TechniqueDefinition[]> {
|
||||
// Return cache if fresh
|
||||
if (cache && Date.now() - cacheTimestamp < CACHE_TTL) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
// Deduplicate concurrent requests
|
||||
if (pendingRequest) return pendingRequest;
|
||||
|
||||
pendingRequest = (async () => {
|
||||
try {
|
||||
const response = await skillsApi.get<{ success: boolean; data: TechniqueDefinition[] }>(
|
||||
'techniques/definitions',
|
||||
{ timeout: 15000 }
|
||||
);
|
||||
const defs = response.data?.data || [];
|
||||
cache = defs;
|
||||
cacheTimestamp = Date.now();
|
||||
return defs;
|
||||
} catch {
|
||||
// If endpoint is down, return cached data or empty — zero errors
|
||||
return cache || [];
|
||||
} finally {
|
||||
pendingRequest = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return pendingRequest;
|
||||
}
|
||||
|
||||
/** Build a lookup map: technique_name → definition */
|
||||
export async function getTechniqueDefinitionsMap(): Promise<Map<string, TechniqueDefinition>> {
|
||||
const defs = await getTechniqueDefinitions();
|
||||
const map = new Map<string, TechniqueDefinition>();
|
||||
for (const d of defs) {
|
||||
map.set(d.technique_name, d);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue