fix(agents): add payment gate on invoke_assessment_expert + progress streaming for assessment

Two hardening fixes for the professional assessment pipeline:
1. Code-level payment verification before dispatching invoke_assessment_expert
   (prevents bypassing the prompt-only gate)
2. Thread onProgress callback through direct tool chain so run_professional_assessment
   streams agent_progress events during the 30-45s assessment expert execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-08 17:16:28 -08:00
parent e809740fdb
commit a72e718510
2 changed files with 34 additions and 3 deletions

View File

@ -65,6 +65,9 @@ import { ToolType } from '../hooks/tool-hooks.types';
// Redis (optional checkpoint persistence)
import { RedisClientService } from '../../cache/redis-client.service';
// Payment (for code-level gate on assessment)
import { PaymentClientService } from '../../payment/payment-client.service';
// ============================================================
// Compatibility Types (与 ClaudeAgentServiceV2 的 StreamChunk 兼容)
// ============================================================
@ -163,6 +166,8 @@ export class CoordinatorAgentService implements OnModuleInit {
private readonly toolHooks: ToolHooksService,
// Redis (optional — for checkpoint persistence)
@Optional() private readonly redisClient?: RedisClientService,
// Payment (optional — for code-level gate on invoke_assessment_expert)
@Optional() private readonly paymentClient?: PaymentClientService,
) {}
onModuleInit() {
@ -563,6 +568,23 @@ export class CoordinatorAgentService implements OnModuleInit {
let result: { output: string; isError: boolean };
if (toolType === 'agent') {
// ── Fix 2: Code-level payment gate on invoke_assessment_expert ──
if (toolName === 'invoke_assessment_expert' && this.paymentClient) {
const orders = await this.paymentClient.getUserOrders(userId);
const hasPaid = orders.some(
(o) => o.serviceType === 'ASSESSMENT' && (o.status === 'PAID' || o.status === 'COMPLETED'),
);
if (!hasPaid) {
return {
output: JSON.stringify({
error: true,
message: '评估服务需要付费。请使用 run_professional_assessment 工具进行正式评估(会自动验证支付状态),或先引导用户完成支付。',
}),
isError: true,
};
}
}
// 为 Agent 调用创建 onProgress callback将进度事件推送到 sink
const onProgress = progressEventSink
? this.createAgentProgressCallback(toolName, progressEventSink)
@ -571,7 +593,11 @@ export class CoordinatorAgentService implements OnModuleInit {
} else if (toolType === 'mcp') {
result = await this.mcpClient.executeTool(toolName, toolInput);
} else {
result = await this.executeDirectTool(toolName, toolInput, userId, conversationId);
// ── Fix 1: Progress feedback for run_professional_assessment ──
const directOnProgress = (toolName === 'run_professional_assessment' && progressEventSink)
? this.createAgentProgressCallback('invoke_assessment_expert', progressEventSink)
: undefined;
result = await this.executeDirectTool(toolName, toolInput, userId, conversationId, directOnProgress);
}
// PostToolUse hook — 成功
@ -725,6 +751,7 @@ export class CoordinatorAgentService implements OnModuleInit {
toolInput: Record<string, unknown>,
userId: string,
conversationId: string,
onProgress?: (text: string) => void,
): Promise<{ output: string; isError: boolean }> {
// 构建旧版 ConversationContextImmigrationToolsService 需要)
const legacyContext: OldConversationContext = {
@ -737,6 +764,7 @@ export class CoordinatorAgentService implements OnModuleInit {
toolName,
toolInput,
legacyContext,
onProgress,
);
// ImmigrationToolsService 返回结构化对象,序列化为 string 供 Claude 消费

View File

@ -310,6 +310,7 @@ export class ImmigrationToolsService {
toolName: string,
input: Record<string, unknown>,
context: ConversationContext,
onProgress?: (text: string) => void,
): Promise<unknown> {
console.log(`[Tool] Executing ${toolName} with input:`, JSON.stringify(input));
@ -369,7 +370,7 @@ export class ImmigrationToolsService {
return this.queryUserArtifacts(input, context);
case 'run_professional_assessment':
return this.runProfessionalAssessment(input, context);
return this.runProfessionalAssessment(input, context, onProgress);
default:
return { error: `Unknown tool: ${toolName}` };
@ -1434,6 +1435,7 @@ export class ImmigrationToolsService {
private async runProfessionalAssessment(
input: Record<string, unknown>,
context: ConversationContext,
onProgress?: (text: string) => void,
): Promise<unknown> {
const { userInfo, targetCategories, conversationContext } = input as {
userInfo: Record<string, unknown>;
@ -1543,11 +1545,12 @@ export class ImmigrationToolsService {
let assessmentResult: string;
try {
const opts = onProgress ? { onProgress } : undefined;
assessmentResult = await this.assessmentExpert.executeAssessment({
userInfo,
targetCategories,
conversationContext,
});
}, opts);
} catch (error) {
console.error('[Tool:run_professional_assessment] Assessment expert failed:', error);
return {