Skip to content

Commit 066f947

Browse files
committed
feat(super-magic-module): add user first message retrieval by topic ID in TaskMessageRepository and related services
1 parent 6daffe6 commit 066f947

File tree

6 files changed

+233
-30
lines changed

6 files changed

+233
-30
lines changed

backend/magic-service/app/Application/Chat/Service/MagicChatMessageAppService.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,21 @@ public function pullRecentMessage(MagicUserAuthorization $userAuthorization, Mes
155155
public function getConversations(MagicUserAuthorization $userAuthorization, ConversationListQueryDTO $queryDTO): ConversationsPageResponseDTO
156156
{
157157
$dataIsolation = $this->createDataIsolation($userAuthorization);
158-
return $this->magicConversationDomainService->getConversations($dataIsolation, $queryDTO);
158+
$result = $this->magicConversationDomainService->getConversations($dataIsolation, $queryDTO);
159+
$filterAccountEntity = $this->magicUserDomainService->getByAiCode($dataIsolation, 'SUPER_MAGIC');
160+
if (! empty($filterAccountEntity) && count($result->getItems()) > 0) {
161+
$filterItems = [];
162+
foreach ($result->getItems() as $item) {
163+
/**
164+
* @var MagicConversationEntity $item
165+
*/
166+
if ($item->getReceiveId() !== $filterAccountEntity->getUserId()) {
167+
$filterItems[] = $item;
168+
}
169+
}
170+
$result->setItems($filterItems);
171+
}
172+
return $result;
159173
}
160174

161175
public function getUserGroupConversation(UserGroupConversationQueryDTO $queryDTO): ?MagicConversationEntity

backend/super-magic-module/src/Application/SuperAgent/Service/TaskAppService.php

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
use Dtyq\SuperMagic\Infrastructure\ExternalAPI\Sandbox\Volcengine\SandboxService;
5151
// use Dtyq\BillingManager\Service\QuotaService;
5252
use Dtyq\SuperMagic\Infrastructure\ExternalAPI\Sandbox\WebSocket\WebSocketSession;
53+
use Dtyq\SuperMagic\Infrastructure\Utils\ToolFileIdMatcher;
5354
use Dtyq\SuperMagic\Interfaces\SuperAgent\DTO\TopicTaskMessageDTO;
5455
use Error;
5556
use Exception;
@@ -69,6 +70,11 @@ class TaskAppService extends AbstractAppService
6970
*/
7071
private MessageBuilderDomainService $messageBuilder;
7172

73+
/**
74+
* 工具文件ID匹配器
75+
*/
76+
private ToolFileIdMatcher $toolFileIdMatcher;
77+
7278
public function __construct(
7379
private readonly WorkspaceDomainService $workspaceDomainService,
7480
private readonly TopicDomainService $topicDomainService,
@@ -85,6 +91,7 @@ public function __construct(
8591
) {
8692
$this->messageBuilder = new MessageBuilderDomainService();
8793
$this->logger = $loggerFactory->get(get_class($this));
94+
$this->toolFileIdMatcher = new ToolFileIdMatcher($this->logger);
8895
}
8996

9097
/**
@@ -704,8 +711,8 @@ private function handleReceivedMessage(TopicTaskMessageDTO $messageDTO, TaskCont
704711
try {
705712
if ($tool !== null && ! empty($tool['attachments'])) {
706713
$this->processToolAttachments($tool, $taskContext);
707-
// 处理完附件后,检查是否需要特殊处理browser工具
708-
$this->matchFileIdForBrowserTool($tool);
714+
// 使用工具文件ID匹配器处理各种工具类型
715+
$this->toolFileIdMatcher->matchFileIdForTools($tool);
709716
}
710717

711718
// 处理消息附件
@@ -1218,33 +1225,6 @@ private function processSingleAttachment(array $attachment, TaskEntity $task, Da
12181225
return $attachment;
12191226
}
12201227

1221-
/**
1222-
* 为浏览器工具匹配file_id
1223-
* 特殊处理:当工具类型为browser且有file_key但没有file_id时(兼容前端的情况).
1224-
*/
1225-
private function matchFileIdForBrowserTool(?array &$tool): void
1226-
{
1227-
// 如果工具为空、不是浏览器类型、没有附件,或已有file_id,则不处理
1228-
if (empty($tool) || empty($tool['attachments'])) {
1229-
return;
1230-
}
1231-
if (empty($tool['detail']) || empty($tool['detail']['type']) || $tool['detail']['type'] !== 'browser' || empty($tool['detail']['data'])) {
1232-
return;
1233-
}
1234-
if (empty($tool['detail']['data']['file_key'])) {
1235-
return;
1236-
}
1237-
1238-
// 从附件中查找匹配file_key的file_id
1239-
$fileKey = $tool['detail']['data']['file_key'];
1240-
foreach ($tool['attachments'] as $attachment) {
1241-
if (! empty($attachment['file_key']) && $attachment['file_key'] === $fileKey && ! empty($attachment['file_id'])) {
1242-
$tool['detail']['data']['file_id'] = $attachment['file_id'];
1243-
break; // 找到匹配项后立即退出循环
1244-
}
1245-
}
1246-
}
1247-
12481228
/**
12491229
* 从WebSocket接收到的消息转换为统一消息格式.
12501230
*

backend/super-magic-module/src/Domain/SuperAgent/Repository/Facade/TaskMessageRepositoryInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,6 @@ public function findByTaskId(string $taskId): array;
4444
* @return array 返回包含消息列表和总数的数组 ['list' => TaskMessageEntity[], 'total' => int]
4545
*/
4646
public function findByTopicId(int $topicId, int $page = 1, int $pageSize = 20, bool $shouldPage = true, string $sortDirection = 'asc', bool $showInUi = true): array;
47+
48+
public function getUserFirstMessageByTopicId(int $topicId, string $userId): ?TaskMessageEntity;
4749
}

backend/super-magic-module/src/Domain/SuperAgent/Repository/Persistence/TaskMessageRepository.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,20 @@ public function findByTopicId(int $topicId, int $page = 1, int $pageSize = 20, b
105105
'total' => $total,
106106
];
107107
}
108+
109+
public function getUserFirstMessageByTopicId(int $topicId, string $userId): ?TaskMessageEntity
110+
{
111+
// 构建基础查询
112+
$query = $this->model::query()
113+
->where('topic_id', $topicId)
114+
->where('sender_type', 'user')
115+
->where('sender_uid', $userId)
116+
->orderBy('id', 'asc');
117+
$record = $query->first();
118+
119+
if (! $record) {
120+
return null;
121+
}
122+
return new TaskMessageEntity($record->toArray());
123+
}
108124
}

backend/super-magic-module/src/Domain/SuperAgent/Service/TaskDomainService.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,4 +693,9 @@ public function getTaskNumByTopicId(int $topicId): int
693693
{
694694
return $this->taskRepository->getTaskCountByTopicId($topicId);
695695
}
696+
697+
public function getUserFirstMessageByTopicId(int $topicId, string $userId): ?TaskMessageEntity
698+
{
699+
return $this->messageRepository->getUserFirstMessageByTopicId($topicId, $userId);
700+
}
696701
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* Copyright (c) The Magic , Distributed under the software license
6+
*/
7+
8+
namespace Dtyq\SuperMagic\Infrastructure\Utils;
9+
10+
use Psr\Log\LoggerInterface;
11+
use Throwable;
12+
13+
/**
14+
* Tool File ID Matcher Utility
15+
* Handles file ID matching for various tool types with their attachments.
16+
*/
17+
class ToolFileIdMatcher
18+
{
19+
private ?LoggerInterface $logger;
20+
21+
public function __construct(?LoggerInterface $logger = null)
22+
{
23+
$this->logger = $logger;
24+
}
25+
26+
/**
27+
* Match file_id for various tool types
28+
* Handles special processing for different tool types that need file_id matching from attachments.
29+
*/
30+
public function matchFileIdForTools(?array &$tool): void
31+
{
32+
if (empty($tool) || empty($tool['attachments']) || empty($tool['detail'])) {
33+
return;
34+
}
35+
36+
$toolType = $tool['detail']['type'] ?? '';
37+
if (empty($toolType)) {
38+
return;
39+
}
40+
41+
try {
42+
$matcher = $this->getToolFileIdMatcher($toolType);
43+
if ($matcher !== null) {
44+
$this->log('debug', "Processing file ID matching for tool type: {$toolType}");
45+
$matcher($tool);
46+
} else {
47+
$this->log('debug', "No file ID matcher found for tool type: {$toolType}");
48+
}
49+
} catch (Throwable $e) {
50+
$this->log('error', "Error matching file ID for tool type {$toolType}: " . $e->getMessage());
51+
}
52+
}
53+
54+
/**
55+
* Get the appropriate file ID matcher for the given tool type.
56+
*/
57+
private function getToolFileIdMatcher(string $toolType): ?callable
58+
{
59+
$matchers = [
60+
'browser' => [$this, 'matchBrowserToolFileId'],
61+
'image' => [$this, 'matchImageToolFileId'],
62+
];
63+
64+
return $matchers[$toolType] ?? null;
65+
}
66+
67+
/**
68+
* Match file_id for browser tool
69+
* Special handling: when tool type is browser and has file_key but no file_id (for frontend compatibility).
70+
*/
71+
private function matchBrowserToolFileId(array &$tool): void
72+
{
73+
if (empty($tool['detail']['data']['file_key'])) {
74+
return;
75+
}
76+
77+
$fileKey = $tool['detail']['data']['file_key'];
78+
foreach ($tool['attachments'] as $attachment) {
79+
if ($this->isFileKeyMatch($attachment, $fileKey)) {
80+
$tool['detail']['data']['file_id'] = $attachment['file_id'];
81+
$this->log('debug', "Browser tool file ID matched: {$attachment['file_id']} for file_key: {$fileKey}");
82+
break; // Exit loop immediately after finding match
83+
}
84+
}
85+
}
86+
87+
/**
88+
* Match file_id for image tool
89+
* Fuzzy matching: match attachments by file_name using fuzzy matching against file_key.
90+
*/
91+
private function matchImageToolFileId(array &$tool): void
92+
{
93+
if (empty($tool['detail']['data']['file_name'])) {
94+
return;
95+
}
96+
97+
$fileName = $tool['detail']['data']['file_name'];
98+
foreach ($tool['attachments'] as $attachment) {
99+
if ($this->isFileNameMatch($attachment, $fileName)) {
100+
$tool['detail']['data']['file_id'] = $attachment['file_id'];
101+
$this->log('debug', "Image tool file ID matched: {$attachment['file_id']} for file_name: {$fileName}");
102+
break; // Exit loop immediately after finding match
103+
}
104+
}
105+
}
106+
107+
/**
108+
* Check if attachment matches the given file key.
109+
*/
110+
private function isFileKeyMatch(array $attachment, string $fileKey): bool
111+
{
112+
return !empty($attachment['file_key'])
113+
&& $attachment['file_key'] === $fileKey
114+
&& !empty($attachment['file_id']);
115+
}
116+
117+
/**
118+
* Check if attachment matches the given file name using fuzzy matching.
119+
* Supports multiple matching strategies for better compatibility.
120+
*/
121+
private function isFileNameMatch(array $attachment, string $fileName): bool
122+
{
123+
if (empty($attachment['file_key']) || empty($attachment['file_id'])) {
124+
return false;
125+
}
126+
127+
// Extract filename from file_key path
128+
$attachmentFileName = basename($attachment['file_key']);
129+
130+
// Strategy 1: Exact match
131+
if ($attachmentFileName === $fileName) {
132+
return true;
133+
}
134+
135+
// Strategy 2: Case-insensitive match
136+
if (strcasecmp($attachmentFileName, $fileName) === 0) {
137+
return true;
138+
}
139+
140+
// Strategy 3: Match without extension
141+
$attachmentBaseName = pathinfo($attachmentFileName, PATHINFO_FILENAME);
142+
$targetBaseName = pathinfo($fileName, PATHINFO_FILENAME);
143+
if (strcasecmp($attachmentBaseName, $targetBaseName) === 0) {
144+
return true;
145+
}
146+
147+
// Strategy 4: Fuzzy match using similar_text for partial matches
148+
$similarity = 0;
149+
similar_text(strtolower($attachmentFileName), strtolower($fileName), $similarity);
150+
if ($similarity >= 90) { // 90% similarity threshold
151+
return true;
152+
}
153+
154+
return false;
155+
}
156+
157+
/**
158+
* Add support for new tool types by registering custom matchers.
159+
*
160+
* @param string $toolType The tool type identifier
161+
* @param callable $matcher The matcher function that takes array &$tool as parameter
162+
*/
163+
public function registerToolMatcher(string $toolType, callable $matcher): void
164+
{
165+
// This could be implemented if dynamic registration is needed in the future
166+
// For now, matchers are statically defined in getToolFileIdMatcher()
167+
}
168+
169+
/**
170+
* Get all supported tool types.
171+
*/
172+
public function getSupportedToolTypes(): array
173+
{
174+
return ['browser', 'image'];
175+
}
176+
177+
/**
178+
* Log message if logger is available.
179+
*/
180+
private function log(string $level, string $message): void
181+
{
182+
if ($this->logger !== null) {
183+
$this->logger->{$level}($message);
184+
}
185+
}
186+
}

0 commit comments

Comments
 (0)