@hed-hog/core 0.0.364 → 0.0.366
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/ai.controller.d.ts +4 -4
- package/dist/ai/ai.module.d.ts.map +1 -1
- package/dist/ai/ai.module.js +2 -0
- package/dist/ai/ai.module.js.map +1 -1
- package/dist/ai/ai.service.d.ts +7 -5
- package/dist/ai/ai.service.d.ts.map +1 -1
- package/dist/ai/ai.service.js +63 -31
- package/dist/ai/ai.service.js.map +1 -1
- package/dist/ai-instruction/ai-execution.service.d.ts +45 -0
- package/dist/ai-instruction/ai-execution.service.d.ts.map +1 -0
- package/dist/ai-instruction/ai-execution.service.js +203 -0
- package/dist/ai-instruction/ai-execution.service.js.map +1 -0
- package/dist/ai-instruction/ai-instruction.controller.d.ts +25 -0
- package/dist/ai-instruction/ai-instruction.controller.d.ts.map +1 -0
- package/dist/ai-instruction/ai-instruction.controller.js +117 -0
- package/dist/ai-instruction/ai-instruction.controller.js.map +1 -0
- package/dist/ai-instruction/ai-instruction.module.d.ts +3 -0
- package/dist/ai-instruction/ai-instruction.module.d.ts.map +1 -0
- package/dist/ai-instruction/ai-instruction.module.js +27 -0
- package/dist/ai-instruction/ai-instruction.module.js.map +1 -0
- package/dist/ai-instruction/ai-instruction.service.d.ts +38 -0
- package/dist/ai-instruction/ai-instruction.service.d.ts.map +1 -0
- package/dist/ai-instruction/ai-instruction.service.js +289 -0
- package/dist/ai-instruction/ai-instruction.service.js.map +1 -0
- package/dist/ai-instruction/ai-prompt-builder.service.d.ts +53 -0
- package/dist/ai-instruction/ai-prompt-builder.service.d.ts.map +1 -0
- package/dist/ai-instruction/ai-prompt-builder.service.js +132 -0
- package/dist/ai-instruction/ai-prompt-builder.service.js.map +1 -0
- package/dist/ai-instruction/dto/create-ai-instruction.dto.d.ts +12 -0
- package/dist/ai-instruction/dto/create-ai-instruction.dto.d.ts.map +1 -0
- package/dist/ai-instruction/dto/create-ai-instruction.dto.js +57 -0
- package/dist/ai-instruction/dto/create-ai-instruction.dto.js.map +1 -0
- package/dist/ai-instruction/dto/update-ai-instruction.dto.d.ts +10 -0
- package/dist/ai-instruction/dto/update-ai-instruction.dto.d.ts.map +1 -0
- package/dist/ai-instruction/dto/update-ai-instruction.dto.js +56 -0
- package/dist/ai-instruction/dto/update-ai-instruction.dto.js.map +1 -0
- package/dist/ai-instruction/types/ai-instruction.types.d.ts +78 -0
- package/dist/ai-instruction/types/ai-instruction.types.d.ts.map +1 -0
- package/dist/ai-instruction/types/ai-instruction.types.js +3 -0
- package/dist/ai-instruction/types/ai-instruction.types.js.map +1 -0
- package/dist/auth/auth.controller.d.ts +13 -5
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +39 -20
- package/dist/auth/auth.controller.js.map +1 -1
- package/dist/auth/auth.service.d.ts +18 -7
- package/dist/auth/auth.service.d.ts.map +1 -1
- package/dist/auth/auth.service.js +20 -16
- package/dist/auth/auth.service.js.map +1 -1
- package/dist/core.module.d.ts.map +1 -1
- package/dist/core.module.js +3 -0
- package/dist/core.module.js.map +1 -1
- package/dist/file/file.service.d.ts +37 -0
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +94 -4
- package/dist/file/file.service.js.map +1 -1
- package/dist/file/provider/abstract.provider.d.ts +6 -2
- package/dist/file/provider/abstract.provider.d.ts.map +1 -1
- package/dist/file/provider/abstract.provider.js.map +1 -1
- package/dist/file/provider/azure.provider.d.ts +4 -1
- package/dist/file/provider/azure.provider.d.ts.map +1 -1
- package/dist/file/provider/azure.provider.js +7 -2
- package/dist/file/provider/azure.provider.js.map +1 -1
- package/dist/file/provider/gcs.provider.d.ts +4 -1
- package/dist/file/provider/gcs.provider.d.ts.map +1 -1
- package/dist/file/provider/gcs.provider.js +7 -2
- package/dist/file/provider/gcs.provider.js.map +1 -1
- package/dist/file/provider/local.provider.d.ts +2 -2
- package/dist/file/provider/local.provider.d.ts.map +1 -1
- package/dist/file/provider/local.provider.js +7 -2
- package/dist/file/provider/local.provider.js.map +1 -1
- package/dist/file/provider/s3.provider.d.ts +4 -1
- package/dist/file/provider/s3.provider.d.ts.map +1 -1
- package/dist/file/provider/s3.provider.js +9 -6
- package/dist/file/provider/s3.provider.js.map +1 -1
- package/dist/file/provider/spaces.provider.d.ts +4 -1
- package/dist/file/provider/spaces.provider.d.ts.map +1 -1
- package/dist/file/provider/spaces.provider.js +9 -5
- package/dist/file/provider/spaces.provider.js.map +1 -1
- package/dist/integration-profile/integration-profile.controller.d.ts +3 -3
- package/dist/integration-profile/integration-profile.service.d.ts +3 -3
- package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.service.js +1 -10
- package/dist/integration-profile/integration-profile.service.js.map +1 -1
- package/dist/mcp-chat/mcp-chat.controller.d.ts +2 -2
- package/dist/mcp-chat/mcp-chat.module.d.ts.map +1 -1
- package/dist/mcp-chat/mcp-chat.module.js +2 -0
- package/dist/mcp-chat/mcp-chat.module.js.map +1 -1
- package/dist/mcp-chat/mcp-chat.service.d.ts +8 -3
- package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
- package/dist/mcp-chat/mcp-chat.service.js +100 -20
- package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
- package/dist/notification/notification.service.d.ts +1 -0
- package/dist/notification/notification.service.d.ts.map +1 -1
- package/dist/notification/notification.service.js +29 -1
- package/dist/notification/notification.service.js.map +1 -1
- package/dist/oauth/oauth.service.d.ts +1 -0
- package/dist/oauth/oauth.service.d.ts.map +1 -1
- package/dist/oauth/oauth.service.js +55 -1
- package/dist/oauth/oauth.service.js.map +1 -1
- package/dist/profile/events/profile-avatar-updated.event.d.ts +7 -0
- package/dist/profile/events/profile-avatar-updated.event.d.ts.map +1 -0
- package/dist/profile/events/profile-avatar-updated.event.js +12 -0
- package/dist/profile/events/profile-avatar-updated.event.js.map +1 -0
- package/dist/profile/events/profile-email-changed.event.d.ts +7 -0
- package/dist/profile/events/profile-email-changed.event.d.ts.map +1 -0
- package/dist/profile/events/profile-email-changed.event.js +12 -0
- package/dist/profile/events/profile-email-changed.event.js.map +1 -0
- package/dist/profile/events/profile-updated.event.d.ts +7 -0
- package/dist/profile/events/profile-updated.event.d.ts.map +1 -0
- package/dist/profile/events/profile-updated.event.js +12 -0
- package/dist/profile/events/profile-updated.event.js.map +1 -0
- package/dist/profile/profile.service.d.ts +3 -1
- package/dist/profile/profile.service.d.ts.map +1 -1
- package/dist/profile/profile.service.js +28 -17
- package/dist/profile/profile.service.js.map +1 -1
- package/dist/session/session.controller.d.ts +1 -0
- package/dist/session/session.controller.d.ts.map +1 -1
- package/dist/session/session.service.d.ts +9 -4
- package/dist/session/session.service.d.ts.map +1 -1
- package/dist/session/session.service.js +61 -51
- package/dist/session/session.service.js.map +1 -1
- package/dist/setting/setting.controller.d.ts +5 -5
- package/dist/setting/setting.service.d.ts +5 -5
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +2 -0
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/task/task.service.d.ts +8 -11
- package/dist/task/task.service.d.ts.map +1 -1
- package/dist/task/task.service.js +10 -16
- package/dist/task/task.service.js.map +1 -1
- package/dist/user/user.controller.d.ts +1 -0
- package/dist/user/user.controller.d.ts.map +1 -1
- package/dist/user/user.service.d.ts +3 -0
- package/dist/user/user.service.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-command.registry.d.ts +9 -2
- package/dist/webhook-integration/webhook-command.registry.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-command.registry.js.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.js +15 -7
- package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
- package/hedhog/data/integration_provider.yaml +17 -0
- package/hedhog/data/integration_type.yaml +6 -0
- package/hedhog/data/route.yaml +86 -2
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +109 -0
- package/hedhog/frontend/app/configurations/layout.tsx.ejs +6 -6
- package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +169 -14
- package/hedhog/frontend/messages/en.json +11 -2
- package/hedhog/frontend/messages/pt.json +11 -2
- package/hedhog/table/ai_execution.yaml +79 -0
- package/hedhog/table/ai_execution_step.yaml +65 -0
- package/hedhog/table/ai_instruction.yaml +43 -0
- package/hedhog/table/ai_instruction_version.yaml +30 -0
- package/hedhog/table/ai_model_pricing.yaml +35 -0
- package/hedhog/table/user_session.yaml +2 -0
- package/package.json +3 -2
- package/src/ai/ai.module.ts +2 -0
- package/src/ai/ai.service.ts +64 -31
- package/src/ai-instruction/ai-execution.service.ts +244 -0
- package/src/ai-instruction/ai-instruction.controller.ts +73 -0
- package/src/ai-instruction/ai-instruction.module.ts +14 -0
- package/src/ai-instruction/ai-instruction.service.ts +343 -0
- package/src/ai-instruction/ai-prompt-builder.service.ts +142 -0
- package/src/ai-instruction/dto/create-ai-instruction.dto.ts +36 -0
- package/src/ai-instruction/dto/update-ai-instruction.dto.ts +35 -0
- package/src/ai-instruction/types/ai-instruction.types.ts +90 -0
- package/src/auth/auth.controller.ts +23 -5
- package/src/auth/auth.service.ts +26 -15
- package/src/core.module.ts +3 -0
- package/src/file/file.service.ts +136 -4
- package/src/file/provider/abstract.provider.ts +4 -2
- package/src/file/provider/azure.provider.ts +6 -3
- package/src/file/provider/gcs.provider.ts +5 -3
- package/src/file/provider/local.provider.ts +7 -3
- package/src/file/provider/s3.provider.ts +8 -6
- package/src/file/provider/spaces.provider.ts +8 -7
- package/src/integration-profile/integration-profile.service.ts +1 -10
- package/src/mcp-chat/mcp-chat.module.ts +19 -17
- package/src/mcp-chat/mcp-chat.service.ts +95 -9
- package/src/notification/notification.service.ts +32 -1
- package/src/oauth/oauth.service.ts +74 -1
- package/src/profile/events/profile-avatar-updated.event.ts +8 -0
- package/src/profile/events/profile-email-changed.event.ts +8 -0
- package/src/profile/events/profile-updated.event.ts +8 -0
- package/src/profile/profile.service.ts +25 -16
- package/src/session/session.service.ts +70 -52
- package/src/setting/setting.service.ts +10 -8
- package/src/task/task.service.ts +10 -16
- package/src/webhook-integration/webhook-command.registry.ts +10 -2
- package/src/webhook-integration/webhook-integration.service.ts +33 -6
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
NotFoundException,
|
|
12
12
|
} from '@nestjs/common';
|
|
13
13
|
import axios from 'axios';
|
|
14
|
+
import { AiExecutionService } from '../ai-instruction/ai-execution.service';
|
|
15
|
+
import { AiPromptBuilderService } from '../ai-instruction/ai-prompt-builder.service';
|
|
14
16
|
import { buildAiConfigFromIntegration } from '../integration-profile/integration-profile.utils';
|
|
15
17
|
import { McpRegistryService, McpToolEntry } from '../mcp/mcp-registry.service';
|
|
16
18
|
import { McpContext } from '../mcp/types/mcp-context.type';
|
|
@@ -81,13 +83,18 @@ type ConversationHistoryItem = {
|
|
|
81
83
|
const MAX_OPENAI_TOOLS = 128;
|
|
82
84
|
const TARGET_SELECTED_TOOLS = 112;
|
|
83
85
|
const MAX_ITERATIONS = 20;
|
|
86
|
+
// Maximum number of history messages to send per turn.
|
|
87
|
+
// Older messages beyond this window are dropped to limit prompt tokens.
|
|
88
|
+
const HISTORY_MESSAGE_LIMIT = 40;
|
|
84
89
|
const FORCE_FINISH_THRESHOLD = 2;
|
|
85
90
|
const MIN_DOMAIN_CONFIDENCE = 2;
|
|
86
91
|
const AMBIGUOUS_DOMAIN_DELTA = 1;
|
|
87
92
|
|
|
88
|
-
|
|
93
|
+
// SYSTEM_PROMPT is now managed via AiInstructionService (slug: system.global).
|
|
94
|
+
// This fallback is used only if the DB is unreachable during startup.
|
|
95
|
+
const SYSTEM_PROMPT_FALLBACK =
|
|
89
96
|
'You are a helpful assistant with access to tools from the HedHog system. ' +
|
|
90
|
-
|
|
97
|
+
"Use the available tools to answer the user's questions accurately. " +
|
|
91
98
|
'Always respond in the same language the user writes in.';
|
|
92
99
|
|
|
93
100
|
const sanitize = (n: string) => n.replace(/\./g, '__');
|
|
@@ -242,6 +249,10 @@ export class McpChatService {
|
|
|
242
249
|
private readonly setting: SettingService,
|
|
243
250
|
@Inject(forwardRef(() => McpRegistryService))
|
|
244
251
|
private readonly registry: McpRegistryService,
|
|
252
|
+
@Inject(forwardRef(() => AiPromptBuilderService))
|
|
253
|
+
private readonly promptBuilder: AiPromptBuilderService,
|
|
254
|
+
@Inject(forwardRef(() => AiExecutionService))
|
|
255
|
+
private readonly aiExecution: AiExecutionService,
|
|
245
256
|
) {}
|
|
246
257
|
|
|
247
258
|
async listConversations(userId: number) {
|
|
@@ -311,10 +322,33 @@ export class McpChatService {
|
|
|
311
322
|
});
|
|
312
323
|
}
|
|
313
324
|
|
|
314
|
-
|
|
315
|
-
|
|
325
|
+
const executionId = await this.aiExecution.start({
|
|
326
|
+
userId,
|
|
327
|
+
conversationId,
|
|
328
|
+
trigger: 'manual',
|
|
329
|
+
contextSlug: 'mcp-chat',
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
if (provider === 'gemini') {
|
|
334
|
+
return await this.runGeminiLoop(conversationId, userId, locale, message, apiKey, executionId);
|
|
335
|
+
}
|
|
336
|
+
return await this.runOpenAiLoop(conversationId, userId, locale, message, apiKey, executionId);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
await this.aiExecution.finish(executionId, 'failed', String(err));
|
|
339
|
+
throw err;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private async getSystemPrompt(locale?: string): Promise<string> {
|
|
344
|
+
try {
|
|
345
|
+
const built = await this.promptBuilder.build({
|
|
346
|
+
locale,
|
|
347
|
+
});
|
|
348
|
+
return built.systemPrompt || SYSTEM_PROMPT_FALLBACK;
|
|
349
|
+
} catch {
|
|
350
|
+
return SYSTEM_PROMPT_FALLBACK;
|
|
316
351
|
}
|
|
317
|
-
return this.runOpenAiLoop(conversationId, userId, locale, message, apiKey);
|
|
318
352
|
}
|
|
319
353
|
|
|
320
354
|
// ── OpenAI loop ──────────────────────────────────────────────────────────
|
|
@@ -325,14 +359,22 @@ export class McpChatService {
|
|
|
325
359
|
locale: string,
|
|
326
360
|
message: string,
|
|
327
361
|
apiKey: string,
|
|
362
|
+
executionId: number,
|
|
328
363
|
) {
|
|
329
|
-
const
|
|
364
|
+
const systemPrompt = await this.getSystemPrompt(locale);
|
|
365
|
+
|
|
366
|
+
const allHistory = await this.prisma.mcp_conversation_message.findMany({
|
|
330
367
|
where: { conversation_id: conversationId },
|
|
331
368
|
orderBy: { created_at: 'asc' },
|
|
332
369
|
});
|
|
370
|
+
// Limit to the most recent messages to avoid token explosion on long conversations.
|
|
371
|
+
// We take the tail rather than summarize to ensure tool_call/tool_result pairs stay intact.
|
|
372
|
+
const history = allHistory.length > HISTORY_MESSAGE_LIMIT
|
|
373
|
+
? allHistory.slice(allHistory.length - HISTORY_MESSAGE_LIMIT)
|
|
374
|
+
: allHistory;
|
|
333
375
|
|
|
334
376
|
const openAiMessages: OpenAiMessage[] = [
|
|
335
|
-
{ role: 'system', content:
|
|
377
|
+
{ role: 'system', content: systemPrompt },
|
|
336
378
|
...this.toOpenAiMessages(history),
|
|
337
379
|
];
|
|
338
380
|
|
|
@@ -351,10 +393,12 @@ export class McpChatService {
|
|
|
351
393
|
const tools = toolSelection.tools;
|
|
352
394
|
|
|
353
395
|
const callSignatures = new Set<string>();
|
|
396
|
+
let stepOrder = 0;
|
|
354
397
|
let maxIterations = MAX_ITERATIONS;
|
|
355
398
|
while (maxIterations-- > 0) {
|
|
356
399
|
const forceFinish = maxIterations < FORCE_FINISH_THRESHOLD;
|
|
357
400
|
let response: any;
|
|
401
|
+
const stepStart = Date.now();
|
|
358
402
|
try {
|
|
359
403
|
response = await axios.post(
|
|
360
404
|
'https://api.openai.com/v1/chat/completions',
|
|
@@ -389,6 +433,20 @@ export class McpChatService {
|
|
|
389
433
|
);
|
|
390
434
|
}
|
|
391
435
|
|
|
436
|
+
const usage = response.data?.usage;
|
|
437
|
+
await this.aiExecution.recordStep({
|
|
438
|
+
executionId,
|
|
439
|
+
stepOrder: stepOrder++,
|
|
440
|
+
type: 'prompt',
|
|
441
|
+
provider: 'openai',
|
|
442
|
+
model: 'gpt-4o-mini',
|
|
443
|
+
durationMs: Date.now() - stepStart,
|
|
444
|
+
usage: usage
|
|
445
|
+
? { input: usage.prompt_tokens ?? 0, output: usage.completion_tokens ?? 0, total: usage.total_tokens ?? 0 }
|
|
446
|
+
: undefined,
|
|
447
|
+
inputSummary: openAiMessages[openAiMessages.length - 1]?.content?.toString()?.slice(0, 200),
|
|
448
|
+
});
|
|
449
|
+
|
|
392
450
|
const choice = response.data?.choices?.[0];
|
|
393
451
|
const assistantMsg = choice?.message;
|
|
394
452
|
|
|
@@ -492,12 +550,15 @@ export class McpChatService {
|
|
|
492
550
|
},
|
|
493
551
|
});
|
|
494
552
|
|
|
553
|
+
await this.aiExecution.finish(executionId, 'completed');
|
|
554
|
+
|
|
495
555
|
return this.prisma.mcp_conversation_message.findMany({
|
|
496
556
|
where: { conversation_id: conversationId },
|
|
497
557
|
orderBy: { created_at: 'asc' },
|
|
498
558
|
});
|
|
499
559
|
}
|
|
500
560
|
|
|
561
|
+
await this.aiExecution.finish(executionId, 'failed', 'Maximum tool call iterations exceeded.');
|
|
501
562
|
throw new BadRequestException('Maximum tool call iterations exceeded.');
|
|
502
563
|
}
|
|
503
564
|
|
|
@@ -509,11 +570,17 @@ export class McpChatService {
|
|
|
509
570
|
locale: string,
|
|
510
571
|
message: string,
|
|
511
572
|
apiKey: string,
|
|
573
|
+
executionId: number,
|
|
512
574
|
) {
|
|
513
|
-
const
|
|
575
|
+
const systemPrompt = await this.getSystemPrompt(locale);
|
|
576
|
+
|
|
577
|
+
const allHistory = await this.prisma.mcp_conversation_message.findMany({
|
|
514
578
|
where: { conversation_id: conversationId },
|
|
515
579
|
orderBy: { created_at: 'asc' },
|
|
516
580
|
});
|
|
581
|
+
const history = allHistory.length > HISTORY_MESSAGE_LIMIT
|
|
582
|
+
? allHistory.slice(allHistory.length - HISTORY_MESSAGE_LIMIT)
|
|
583
|
+
: allHistory;
|
|
517
584
|
|
|
518
585
|
const geminiContents: GeminiContent[] = this.toGeminiContents(history);
|
|
519
586
|
const toolSelection = await this.buildGeminiTools(
|
|
@@ -523,6 +590,7 @@ export class McpChatService {
|
|
|
523
590
|
history,
|
|
524
591
|
);
|
|
525
592
|
if (toolSelection.clarification) {
|
|
593
|
+
await this.aiExecution.finish(executionId, 'completed');
|
|
526
594
|
return this.recordAssistantAndReturnHistory(
|
|
527
595
|
conversationId,
|
|
528
596
|
toolSelection.clarification,
|
|
@@ -530,17 +598,19 @@ export class McpChatService {
|
|
|
530
598
|
}
|
|
531
599
|
const functionDeclarations = toolSelection.tools;
|
|
532
600
|
|
|
601
|
+
let stepOrder = 0;
|
|
533
602
|
let maxIterations = MAX_ITERATIONS;
|
|
534
603
|
let callIndex = 0;
|
|
535
604
|
|
|
536
605
|
while (maxIterations-- > 0) {
|
|
537
606
|
const forceFinish = maxIterations < FORCE_FINISH_THRESHOLD;
|
|
538
607
|
let response: any;
|
|
608
|
+
const stepStart = Date.now();
|
|
539
609
|
try {
|
|
540
610
|
response = await axios.post(
|
|
541
611
|
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`,
|
|
542
612
|
{
|
|
543
|
-
systemInstruction: { parts: [{ text:
|
|
613
|
+
systemInstruction: { parts: [{ text: systemPrompt }] },
|
|
544
614
|
contents: geminiContents,
|
|
545
615
|
tools:
|
|
546
616
|
!forceFinish && functionDeclarations.length > 0
|
|
@@ -572,6 +642,19 @@ export class McpChatService {
|
|
|
572
642
|
);
|
|
573
643
|
}
|
|
574
644
|
|
|
645
|
+
const geminiUsage = response.data?.usageMetadata;
|
|
646
|
+
await this.aiExecution.recordStep({
|
|
647
|
+
executionId,
|
|
648
|
+
stepOrder: stepOrder++,
|
|
649
|
+
type: 'prompt',
|
|
650
|
+
provider: 'gemini',
|
|
651
|
+
model: 'gemini-2.0-flash',
|
|
652
|
+
durationMs: Date.now() - stepStart,
|
|
653
|
+
usage: geminiUsage
|
|
654
|
+
? { input: geminiUsage.promptTokenCount ?? 0, output: geminiUsage.candidatesTokenCount ?? 0, total: geminiUsage.totalTokenCount ?? 0 }
|
|
655
|
+
: undefined,
|
|
656
|
+
});
|
|
657
|
+
|
|
575
658
|
const candidate = response.data?.candidates?.[0];
|
|
576
659
|
const parts: GeminiPart[] = candidate?.content?.parts ?? [];
|
|
577
660
|
|
|
@@ -649,12 +732,15 @@ export class McpChatService {
|
|
|
649
732
|
},
|
|
650
733
|
});
|
|
651
734
|
|
|
735
|
+
await this.aiExecution.finish(executionId, 'completed');
|
|
736
|
+
|
|
652
737
|
return this.prisma.mcp_conversation_message.findMany({
|
|
653
738
|
where: { conversation_id: conversationId },
|
|
654
739
|
orderBy: { created_at: 'asc' },
|
|
655
740
|
});
|
|
656
741
|
}
|
|
657
742
|
|
|
743
|
+
await this.aiExecution.finish(executionId, 'failed', 'Maximum tool call iterations exceeded.');
|
|
658
744
|
throw new BadRequestException('Maximum tool call iterations exceeded.');
|
|
659
745
|
}
|
|
660
746
|
|
|
@@ -52,7 +52,7 @@ export class NotificationService implements OnModuleInit, OnModuleDestroy {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
async onModuleInit() {
|
|
55
|
-
const databaseUrl = String(process.env.DATABASE_URL || '');
|
|
55
|
+
const databaseUrl = String(process.env.DIRECT_DATABASE_URL || process.env.DATABASE_URL || '');
|
|
56
56
|
if (!databaseUrl.startsWith('postgresql://')) {
|
|
57
57
|
this.logger.warn(
|
|
58
58
|
'Cluster notification stream disabled because DATABASE_URL is not PostgreSQL',
|
|
@@ -94,6 +94,7 @@ export class NotificationService implements OnModuleInit, OnModuleDestroy {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
async list(userId: number, paginationParams: any) {
|
|
97
|
+
await this.cleanupStaleProgressNotifications(userId);
|
|
97
98
|
return this.pagination.paginate(
|
|
98
99
|
this.prisma.notification,
|
|
99
100
|
paginationParams,
|
|
@@ -104,6 +105,36 @@ export class NotificationService implements OnModuleInit, OnModuleDestroy {
|
|
|
104
105
|
);
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
private async cleanupStaleProgressNotifications(userId: number) {
|
|
109
|
+
const stale = await this.prisma.$queryRaw<{ id: number }[]>`
|
|
110
|
+
SELECT n.id
|
|
111
|
+
FROM notification n
|
|
112
|
+
WHERE n.user_id = ${userId}
|
|
113
|
+
AND n.progress IS NOT NULL
|
|
114
|
+
AND n.progress < 100
|
|
115
|
+
AND n.finished_at IS NULL
|
|
116
|
+
AND (n.action_data->>'queueJobId') IS NOT NULL
|
|
117
|
+
AND NOT EXISTS (
|
|
118
|
+
SELECT 1 FROM queue_job qj
|
|
119
|
+
WHERE qj.id = (n.action_data->>'queueJobId')::int
|
|
120
|
+
)
|
|
121
|
+
`;
|
|
122
|
+
|
|
123
|
+
if (!stale.length) return;
|
|
124
|
+
|
|
125
|
+
await this.prisma.notification.updateMany({
|
|
126
|
+
where: { id: { in: stale.map((n) => n.id) } },
|
|
127
|
+
data: {
|
|
128
|
+
progress: 100,
|
|
129
|
+
type: 'error' as any,
|
|
130
|
+
body: 'O processamento foi interrompido (job não encontrado).',
|
|
131
|
+
finished_at: new Date(),
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await this.broadcastNotificationEvent(userId, { type: 'notification.updated' });
|
|
136
|
+
}
|
|
137
|
+
|
|
107
138
|
async unreadCount(userId: number) {
|
|
108
139
|
const count = await this.prisma.notification.count({
|
|
109
140
|
where: { user_id: userId, status: 'unread' },
|
|
@@ -94,7 +94,8 @@ export class OAuthService {
|
|
|
94
94
|
throw new BadRequestException('Invalid mobile redirect URI.');
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
const
|
|
97
|
+
const callbackPath = `/callback/${String(provider)}/login`;
|
|
98
|
+
const authUrl = await this.getAuthUrl(provider, callbackPath);
|
|
98
99
|
const flow = 'login';
|
|
99
100
|
const { token, signature } = await this.issueMobileState(
|
|
100
101
|
provider,
|
|
@@ -153,6 +154,46 @@ export class OAuthService {
|
|
|
153
154
|
);
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
// Mobile browser flow: Microsoft redirected to backend callback with hhmob state.
|
|
158
|
+
// Complete auth here and redirect the browser to the mobile app URI with tokens.
|
|
159
|
+
if (state?.startsWith('hhmob.') && !redirectUri) {
|
|
160
|
+
const mobileRedirectUri = await this.lookupMobileRedirectUri(state, provider);
|
|
161
|
+
if (!mobileRedirectUri) {
|
|
162
|
+
throw new BadRequestException(
|
|
163
|
+
getLocaleText(
|
|
164
|
+
'oauth.callback.invalidState',
|
|
165
|
+
locale,
|
|
166
|
+
'OAuth callback state is invalid or expired. Start the sign-in flow again.',
|
|
167
|
+
),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const callbackKey = this.buildCallbackKey(provider, type, code);
|
|
172
|
+
const tokens = (await this.callbackCoordinator.execute(callbackKey, async () => {
|
|
173
|
+
const profile = await this.getProvider(provider).getProfile(code, type);
|
|
174
|
+
return this.finalizeCallback({
|
|
175
|
+
res,
|
|
176
|
+
locale,
|
|
177
|
+
ipAddress,
|
|
178
|
+
userAgent,
|
|
179
|
+
provider,
|
|
180
|
+
type,
|
|
181
|
+
profile,
|
|
182
|
+
userId,
|
|
183
|
+
includeRefreshTokenInBody: true,
|
|
184
|
+
});
|
|
185
|
+
})) as { accessToken: string; refreshToken?: string };
|
|
186
|
+
|
|
187
|
+
const params = new URLSearchParams({ accessToken: tokens.accessToken });
|
|
188
|
+
if (tokens.refreshToken) params.set('refreshToken', tokens.refreshToken);
|
|
189
|
+
|
|
190
|
+
this.logger.log(
|
|
191
|
+
`OAuth mobile browser callback completed: provider=${provider} type=${type}`,
|
|
192
|
+
);
|
|
193
|
+
(res as any).redirect(`${mobileRedirectUri}?${params.toString()}`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
156
197
|
const callbackKey = this.buildCallbackKey(provider, type, code);
|
|
157
198
|
|
|
158
199
|
return await this.callbackCoordinator.execute(callbackKey, async () => {
|
|
@@ -321,6 +362,38 @@ export class OAuthService {
|
|
|
321
362
|
return Number(consumedRows) > 0;
|
|
322
363
|
}
|
|
323
364
|
|
|
365
|
+
private async lookupMobileRedirectUri(
|
|
366
|
+
encodedState: string,
|
|
367
|
+
provider: user_account_provider_52222e2ecb_enum,
|
|
368
|
+
): Promise<string | null> {
|
|
369
|
+
if (!encodedState?.startsWith('hhmob.')) return null;
|
|
370
|
+
|
|
371
|
+
const parts = encodedState.split('.');
|
|
372
|
+
const stateToken = parts[1];
|
|
373
|
+
const signature = parts[2];
|
|
374
|
+
|
|
375
|
+
if (!stateToken || !signature) return null;
|
|
376
|
+
|
|
377
|
+
const hasValidSignature = this.security.verifyHmacSha256(
|
|
378
|
+
`oauth-mobile-state:${stateToken}`,
|
|
379
|
+
signature,
|
|
380
|
+
);
|
|
381
|
+
if (!hasValidSignature) return null;
|
|
382
|
+
|
|
383
|
+
const tokenHash = this.security.hashSha256(stateToken);
|
|
384
|
+
const record = await this.prisma.oauth_mobile_state_token.findFirst({
|
|
385
|
+
where: {
|
|
386
|
+
token_hash: tokenHash,
|
|
387
|
+
provider: String(provider).toLowerCase(),
|
|
388
|
+
expires_at: { gt: new Date() },
|
|
389
|
+
consumed_at: null,
|
|
390
|
+
},
|
|
391
|
+
select: { redirect_uri: true },
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
return record?.redirect_uri ?? null;
|
|
395
|
+
}
|
|
396
|
+
|
|
324
397
|
private async finalizeCallback({
|
|
325
398
|
res,
|
|
326
399
|
locale,
|
|
@@ -2,6 +2,7 @@ import { getLocaleText } from '@hed-hog/api-locale';
|
|
|
2
2
|
import { PrismaService, user } from '@hed-hog/api-prisma';
|
|
3
3
|
import { User } from '@hed-hog/api-types';
|
|
4
4
|
import { BadRequestException, forwardRef, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
|
5
|
+
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
5
6
|
import { JwtService } from '@nestjs/jwt';
|
|
6
7
|
import type {
|
|
7
8
|
VerifiedAuthenticationResponse,
|
|
@@ -27,6 +28,9 @@ import { EmailVerificationDto } from './dto/email-verification-confirm.dto';
|
|
|
27
28
|
import { SendEmailVerificationDto } from './dto/send-email-verification.dto';
|
|
28
29
|
import { SetPreferencesDto } from './dto/set-preferences.dto';
|
|
29
30
|
import { UpdateDto } from './dto/update.dto';
|
|
31
|
+
import { PROFILE_AVATAR_UPDATED, ProfileAvatarUpdatedEvent } from './events/profile-avatar-updated.event';
|
|
32
|
+
import { PROFILE_EMAIL_CHANGED, ProfileEmailChangedEvent } from './events/profile-email-changed.event';
|
|
33
|
+
import { PROFILE_UPDATED, ProfileUpdatedEvent } from './events/profile-updated.event';
|
|
30
34
|
|
|
31
35
|
@Injectable()
|
|
32
36
|
export class ProfileService {
|
|
@@ -44,6 +48,7 @@ export class ProfileService {
|
|
|
44
48
|
private readonly token: TokenService,
|
|
45
49
|
@Inject(forwardRef(() => MailService))
|
|
46
50
|
private readonly mail: MailService,
|
|
51
|
+
private readonly eventEmitter: EventEmitter2,
|
|
47
52
|
) {}
|
|
48
53
|
|
|
49
54
|
async changeEmail(locale: string, userId: number, { email, password, pin }: ChangeEmailDto) {
|
|
@@ -237,6 +242,7 @@ export class ProfileService {
|
|
|
237
242
|
}
|
|
238
243
|
|
|
239
244
|
const verifiedAt = new Date();
|
|
245
|
+
let userIdentifierId = challenge.user_identifier_id;
|
|
240
246
|
|
|
241
247
|
await this.prisma.$transaction(async (tx) => {
|
|
242
248
|
|
|
@@ -244,8 +250,6 @@ export class ProfileService {
|
|
|
244
250
|
where: { id: challengeId },
|
|
245
251
|
})
|
|
246
252
|
|
|
247
|
-
let userIdentifierId = challenge.user_identifier_id;
|
|
248
|
-
|
|
249
253
|
if (!userIdentifierId) {
|
|
250
254
|
const userIdentifier = await tx.user_identifier.findFirst({
|
|
251
255
|
where: { user_id: userId, type: 'email', verified_at: null },
|
|
@@ -277,18 +281,19 @@ export class ProfileService {
|
|
|
277
281
|
}
|
|
278
282
|
});
|
|
279
283
|
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
await tx.user_identifier.deleteMany({
|
|
283
|
-
where: {
|
|
284
|
-
user_id: userId,
|
|
285
|
-
type: 'email',
|
|
286
|
-
id: { not: userIdentifierId }
|
|
287
|
-
}
|
|
288
|
-
});
|
|
284
|
+
// Keep previous emails intact so the user can still authenticate with the old address.
|
|
285
|
+
// The verified identifier above becomes the new active email, but older verified emails remain valid aliases.
|
|
289
286
|
|
|
290
287
|
});
|
|
291
288
|
|
|
289
|
+
const confirmedIdentifier = await this.prisma.user_identifier.findUnique({
|
|
290
|
+
where: { id: userIdentifierId },
|
|
291
|
+
select: { value: true },
|
|
292
|
+
});
|
|
293
|
+
if (confirmedIdentifier?.value) {
|
|
294
|
+
this.eventEmitter.emit(PROFILE_EMAIL_CHANGED, new ProfileEmailChangedEvent(userId, confirmedIdentifier.value));
|
|
295
|
+
}
|
|
296
|
+
|
|
292
297
|
return { success: true };
|
|
293
298
|
}
|
|
294
299
|
|
|
@@ -338,16 +343,20 @@ export class ProfileService {
|
|
|
338
343
|
}
|
|
339
344
|
|
|
340
345
|
async update(userid: number, { name }: UpdateDto) {
|
|
341
|
-
|
|
346
|
+
const result = await this.prisma.user.update({
|
|
342
347
|
where: { id: userid },
|
|
343
|
-
data: {
|
|
344
|
-
name,
|
|
345
|
-
},
|
|
348
|
+
data: { name },
|
|
346
349
|
});
|
|
350
|
+
if (name) {
|
|
351
|
+
this.eventEmitter.emit(PROFILE_UPDATED, new ProfileUpdatedEvent(userid, name));
|
|
352
|
+
}
|
|
353
|
+
return result;
|
|
347
354
|
}
|
|
348
355
|
|
|
349
356
|
async updateAvatar(locale: string, userId: number, file: MulterFile) {
|
|
350
|
-
|
|
357
|
+
const result = await this.user.changeAvatar(locale, userId, file);
|
|
358
|
+
this.eventEmitter.emit(PROFILE_AVATAR_UPDATED, new ProfileAvatarUpdatedEvent(userId, result?.id ?? null));
|
|
359
|
+
return result;
|
|
351
360
|
}
|
|
352
361
|
|
|
353
362
|
async changePassword(locale:string, userId: number, {currentPassword, newPassword}: ChangePasswordDto, ipAddress?: string, userAgent?: string) {
|