feat(ssa): Complete T-test end-to-end testing with 9 bug fixes - Phase 1 core 85% complete. R service: missing value auto-filter. Backend: error handling, variable matching, dynamic filename. Frontend: module activation, session isolation, error propagation. Full flow verified.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
138
backend/src/modules/ssa/executor/RClientService.ts
Normal file
138
backend/src/modules/ssa/executor/RClientService.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* R 服务客户端
|
||||
* 负责调用 R Docker 服务执行统计分析
|
||||
*
|
||||
* 遵循规范:
|
||||
* - 使用统一日志服务 @/common/logging
|
||||
* - 使用统一存储服务 @/common/storage(OSS 存储规范)
|
||||
*/
|
||||
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { logger } from '../../../common/logging/index.js';
|
||||
import { storage } from '../../../common/storage/index.js';
|
||||
import { prisma } from '../../../config/database.js';
|
||||
|
||||
export class RClientService {
|
||||
private client: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
const baseURL = process.env.R_SERVICE_URL || 'http://localhost:8082';
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL,
|
||||
timeout: 120000, // 120 秒超时
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async execute(sessionId: string, plan: any, session: any): Promise<any> {
|
||||
const startTime = Date.now();
|
||||
|
||||
// 构建请求体(使用统一存储服务)
|
||||
const requestBody = {
|
||||
data_source: await this.buildDataSource(session),
|
||||
params: plan.params,
|
||||
guardrails: plan.guardrails || {
|
||||
check_normality: true,
|
||||
auto_fix: true
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
logger.info('[SSA:RClient] Calling R service', {
|
||||
sessionId,
|
||||
toolCode: plan.tool_code,
|
||||
endpoint: `/api/v1/skills/${plan.tool_code}`,
|
||||
requestBody
|
||||
});
|
||||
|
||||
const response = await this.client.post(
|
||||
`/api/v1/skills/${plan.tool_code}`,
|
||||
requestBody
|
||||
);
|
||||
|
||||
const executionMs = Date.now() - startTime;
|
||||
|
||||
logger.info('[SSA:RClient] R service response', {
|
||||
sessionId,
|
||||
status: response.data?.status,
|
||||
hasResults: !!response.data?.results,
|
||||
executionMs
|
||||
});
|
||||
|
||||
// 记录执行日志(失败不阻塞主流程)
|
||||
try {
|
||||
await prisma.ssaExecutionLog.create({
|
||||
data: {
|
||||
sessionId,
|
||||
toolCode: plan.tool_code,
|
||||
inputParams: plan.params,
|
||||
outputStatus: response.data.status,
|
||||
outputResult: response.data.results,
|
||||
traceLog: response.data.trace_log || [],
|
||||
executionMs
|
||||
}
|
||||
});
|
||||
} catch (logError) {
|
||||
logger.warn('[SSA:RClient] Failed to save execution log', { error: logError });
|
||||
}
|
||||
|
||||
// 添加执行耗时到返回结果
|
||||
return {
|
||||
...response.data,
|
||||
executionMs
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
logger.error('R service call failed', { sessionId, toolCode: plan.tool_code, error: error.message });
|
||||
|
||||
// 502/504 特殊处理(R 服务崩溃或超时)
|
||||
const statusCode = error.response?.status;
|
||||
if (statusCode === 502 || statusCode === 504) {
|
||||
throw new Error('统计服务繁忙或数据异常,请稍后重试');
|
||||
}
|
||||
|
||||
// 提取 R 服务返回的用户友好提示
|
||||
const userHint = error.response?.data?.user_hint;
|
||||
if (userHint) {
|
||||
throw new Error(userHint);
|
||||
}
|
||||
|
||||
throw new Error(`R service error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建数据源(仅支持 OSS)
|
||||
*
|
||||
* 设计说明:SSA 场景下用户必须上传数据文件,文件存入 OSS,
|
||||
* R 服务通过预签名 URL 从 OSS 下载数据。
|
||||
*/
|
||||
private async buildDataSource(session: any): Promise<{ type: string; oss_url: string }> {
|
||||
const ossKey = session.dataOssKey;
|
||||
|
||||
if (!ossKey) {
|
||||
logger.error('[SSA:RClient] No data uploaded', { sessionId: session.id });
|
||||
throw new Error('请先上传数据文件');
|
||||
}
|
||||
|
||||
logger.info('[SSA:RClient] Building OSS data source', { sessionId: session.id, ossKey });
|
||||
const signedUrl = await storage.getUrl(ossKey);
|
||||
|
||||
return {
|
||||
type: 'oss',
|
||||
oss_url: signedUrl
|
||||
};
|
||||
}
|
||||
|
||||
async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
const res = await this.client.get('/health');
|
||||
return res.data.status === 'ok';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user