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