@@ -38,6 +38,17 @@ type LarkResponse<T> = {
3838 data ?: T ;
3939} ;
4040
41+ const threadMessageCache = new Map < string , string [ ] > ( ) ;
42+
43+ function rememberThreadMessage ( threadId : string , messageId : string ) : void {
44+ if ( ! threadId || ! messageId ) return ;
45+ const existing = threadMessageCache . get ( threadId ) ?? [ ] ;
46+ if ( ! existing . includes ( messageId ) ) {
47+ existing . push ( messageId ) ;
48+ threadMessageCache . set ( threadId , existing . slice ( - 50 ) ) ;
49+ }
50+ }
51+
4152function requireString ( value : unknown , label : string ) : string {
4253 if ( ! value || typeof value !== "string" ) {
4354 throw new Error ( `${ label } is required` ) ;
@@ -89,7 +100,7 @@ function getLarkCredentials(payload: LarkActionRequest): { appId: string; appSec
89100}
90101
91102async function larkRequest < T > (
92- method : "GET" | "POST" | "PATCH" ,
103+ method : "GET" | "POST" | "PATCH" | "PUT" ,
93104 path : string ,
94105 token : string ,
95106 body ?: Record < string , unknown >
@@ -104,7 +115,18 @@ async function larkRequest<T>(
104115 } ) ;
105116
106117 if ( ! response . ok ) {
107- throw new Error ( `Lark API ${ response . status } ${ response . statusText } ` ) ;
118+ let detail = "" ;
119+ try {
120+ const errorPayload = await response . json ( ) as { code ?: number ; msg ?: string } ;
121+ if ( typeof errorPayload . msg === "string" && errorPayload . msg . trim ( ) . length > 0 ) {
122+ detail = `: ${ errorPayload . msg } ` ;
123+ } else if ( typeof errorPayload . code === "number" ) {
124+ detail = `: code ${ errorPayload . code } ` ;
125+ }
126+ } catch {
127+ // ignore malformed error body
128+ }
129+ throw new Error ( `Lark API ${ response . status } ${ response . statusText } ${ detail } ` ) ;
108130 }
109131
110132 const payload = await response . json ( ) as LarkResponse < T > ;
@@ -142,23 +164,43 @@ async function postTextMessage(params: {
142164 text : string ;
143165 threadId ?: string ;
144166} ) : Promise < { messageId : string ; channelId : string } > {
145- const data = await larkRequest < { message_id ?: string } > (
146- "POST" ,
147- "/open-apis/im/v1/messages?receive_id_type=chat_id" ,
148- params . token ,
149- {
150- receive_id : params . channelId ,
151- msg_type : "text" ,
152- content : JSON . stringify ( { text : params . text } ) ,
153- ...( params . threadId ? { root_id : params . threadId } : { } ) ,
154- }
155- ) ;
167+ const data = params . threadId
168+ ? await larkRequest < { message_id ?: string } > (
169+ "POST" ,
170+ `/open-apis/im/v1/messages/${ encodeURIComponent ( params . threadId ) } /reply` ,
171+ params . token ,
172+ {
173+ msg_type : "text" ,
174+ content : JSON . stringify ( { text : params . text } ) ,
175+ reply_in_thread : true ,
176+ }
177+ )
178+ : await larkRequest < { message_id ?: string } > (
179+ "POST" ,
180+ "/open-apis/im/v1/messages?receive_id_type=chat_id" ,
181+ params . token ,
182+ {
183+ receive_id : params . channelId ,
184+ msg_type : "text" ,
185+ content : JSON . stringify ( { text : params . text } ) ,
186+ }
187+ ) ;
156188 return {
157189 messageId : data . message_id ?? "" ,
158190 channelId : params . channelId ,
159191 } ;
160192}
161193
194+ async function getMessageById ( token : string , messageId : string ) : Promise < Record < string , unknown > | null > {
195+ const data = await larkRequest < { items ?: Array < Record < string , unknown > > } > (
196+ "GET" ,
197+ `/open-apis/im/v1/messages/${ encodeURIComponent ( messageId ) } ` ,
198+ token
199+ ) ;
200+ const item = Array . isArray ( data . items ) ? data . items [ 0 ] : null ;
201+ return item ?? null ;
202+ }
203+
162204async function handleLarkAction ( payload : LarkActionRequest ) : Promise < unknown > {
163205 const { appId, appSecret } = getLarkCredentials ( payload ) ;
164206 const token = await getTenantAccessToken ( appId , appSecret ) ;
@@ -182,26 +224,45 @@ async function handleLarkAction(payload: LarkActionRequest): Promise<unknown> {
182224 case "post_message" : {
183225 const channelId = requireString ( payload . channelId , "channelId" ) ;
184226 const text = requireString ( payload . text , "text" ) ;
185- return postTextMessage ( {
227+ const message = await postTextMessage ( {
186228 token,
187229 channelId,
188230 text,
189231 threadId : payload . threadId ,
190232 } ) ;
233+ if ( payload . threadId && message . messageId ) {
234+ rememberThreadMessage ( payload . threadId , message . messageId ) ;
235+ }
236+ return message ;
191237 }
192238
193239 case "update_message" : {
194240 const messageId = requireString ( payload . messageId , "messageId" ) ;
195241 const text = requireString ( payload . text , "text" ) ;
196- await larkRequest < Record < string , unknown > > (
197- "PATCH" ,
198- `/open-apis/im/v1/messages/${ encodeURIComponent ( messageId ) } ` ,
199- token ,
200- {
201- msg_type : "text" ,
202- content : JSON . stringify ( { text } ) ,
242+ const body = {
243+ msg_type : "text" ,
244+ content : JSON . stringify ( { text } ) ,
245+ } ;
246+ try {
247+ await larkRequest < Record < string , unknown > > (
248+ "PATCH" ,
249+ `/open-apis/im/v1/messages/${ encodeURIComponent ( messageId ) } ` ,
250+ token ,
251+ body
252+ ) ;
253+ } catch ( patchError ) {
254+ const patchMessage = patchError instanceof Error ? patchError . message : String ( patchError ) ;
255+ if ( ! patchMessage . includes ( "400" ) ) {
256+ throw patchError ;
203257 }
204- ) ;
258+
259+ await larkRequest < Record < string , unknown > > (
260+ "PUT" ,
261+ `/open-apis/im/v1/messages/${ encodeURIComponent ( messageId ) } ` ,
262+ token ,
263+ body
264+ ) ;
265+ }
205266 return {
206267 status : "message_updated" ,
207268 messageId,
@@ -229,12 +290,54 @@ async function handleLarkAction(payload: LarkActionRequest): Promise<unknown> {
229290
230291 case "get_thread_messages" : {
231292 const threadId = requireString ( payload . threadId , "threadId" ) ;
232- const data = await larkRequest < { items ?: unknown [ ] } > (
233- "GET" ,
234- `/open-apis/im/v1/messages/${ encodeURIComponent ( threadId ) } /replies?page_size=${ Math . min ( Math . max ( payload . limit ?? 20 , 1 ) , 50 ) } ` ,
235- token
236- ) ;
237- return { messages : data . items ?? [ ] } ;
293+ const channelId = payload . channelId ?. trim ( ) ;
294+ const limit = Math . min ( Math . max ( payload . limit ?? 20 , 1 ) , 50 ) ;
295+
296+ let threadConversationId = "" ;
297+ try {
298+ const root = await getMessageById ( token , threadId ) ;
299+ threadConversationId = typeof root ?. thread_id === "string" ? root . thread_id : "" ;
300+ } catch {
301+ threadConversationId = "" ;
302+ }
303+
304+ if ( channelId ) {
305+ const data = await larkRequest < {
306+ items ?: Array < Record < string , unknown > > ;
307+ } > (
308+ "GET" ,
309+ `/open-apis/im/v1/messages?container_id_type=chat&container_id=${ encodeURIComponent ( channelId ) } &page_size=50` ,
310+ token
311+ ) ;
312+ const messages = ( data . items ?? [ ] )
313+ . filter ( ( item ) => {
314+ const messageId = typeof item . message_id === "string" ? item . message_id : "" ;
315+ const rootId = typeof item . root_id === "string" ? item . root_id : "" ;
316+ const parentId = typeof item . parent_id === "string" ? item . parent_id : "" ;
317+ const itemThreadId = typeof item . thread_id === "string" ? item . thread_id : "" ;
318+ if ( threadConversationId ) {
319+ return itemThreadId === threadConversationId ;
320+ }
321+ return messageId === threadId || rootId === threadId || parentId === threadId ;
322+ } )
323+ . slice ( - limit ) ;
324+ if ( messages . length > 0 ) {
325+ return { messages } ;
326+ }
327+ }
328+
329+ const cachedIds = threadMessageCache . get ( threadId ) ?? [ ] ;
330+ const uniqueIds = [ threadId , ...cachedIds ] . filter ( ( id , index , arr ) => arr . indexOf ( id ) === index ) ;
331+ const messages : Array < Record < string , unknown > > = [ ] ;
332+ for ( const id of uniqueIds . slice ( - limit ) ) {
333+ try {
334+ const item = await getMessageById ( token , id ) ;
335+ if ( item ) messages . push ( item ) ;
336+ } catch {
337+ // ignore single message lookup failures
338+ }
339+ }
340+ return { messages } ;
238341 }
239342
240343 case "get_user_info" : {
@@ -314,26 +417,40 @@ async function handleLarkAction(payload: LarkActionRequest): Promise<unknown> {
314417
315418 const threadId = payload . threadId ?. trim ( ) ;
316419 if ( payload . initialComment ?. trim ( ) ) {
317- await postTextMessage ( {
420+ const comment = await postTextMessage ( {
318421 token,
319422 channelId,
320423 text : payload . initialComment . trim ( ) ,
321424 threadId,
322425 } ) ;
426+ if ( threadId && comment . messageId ) {
427+ rememberThreadMessage ( threadId , comment . messageId ) ;
428+ }
323429 }
324430
325431 const message = await larkRequest < { message_id ?: string } > (
326432 "POST" ,
327- "/open-apis/im/v1/messages?receive_id_type=chat_id" ,
433+ threadId
434+ ? `/open-apis/im/v1/messages/${ encodeURIComponent ( threadId ) } /reply`
435+ : "/open-apis/im/v1/messages?receive_id_type=chat_id" ,
328436 token ,
329- {
330- receive_id : channelId ,
331- msg_type : "file" ,
332- content : JSON . stringify ( { file_key : uploadPayload . data . file_key } ) ,
333- ...( threadId ? { root_id : threadId } : { } ) ,
334- }
437+ threadId
438+ ? {
439+ msg_type : "file" ,
440+ content : JSON . stringify ( { file_key : uploadPayload . data . file_key } ) ,
441+ reply_in_thread : true ,
442+ }
443+ : {
444+ receive_id : channelId ,
445+ msg_type : "file" ,
446+ content : JSON . stringify ( { file_key : uploadPayload . data . file_key } ) ,
447+ }
335448 ) ;
336449
450+ if ( threadId && message . message_id ) {
451+ rememberThreadMessage ( threadId , message . message_id ) ;
452+ }
453+
337454 return {
338455 status : "file_uploaded" ,
339456 messageId : message . message_id ?? "" ,
0 commit comments