cc-lark 0.1.1

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.
Files changed (298) hide show
  1. package/.github/workflows/ci.yml +47 -0
  2. package/.github/workflows/release.yml +47 -0
  3. package/.github/workflows/sync-upstream.yml +127 -0
  4. package/.prettierrc.json +7 -0
  5. package/README.md +214 -0
  6. package/dist/core/api-error.d.ts +193 -0
  7. package/dist/core/api-error.d.ts.map +1 -0
  8. package/dist/core/api-error.js +263 -0
  9. package/dist/core/api-error.js.map +1 -0
  10. package/dist/core/auth-errors.d.ts +13 -0
  11. package/dist/core/auth-errors.d.ts.map +1 -0
  12. package/dist/core/auth-errors.js +14 -0
  13. package/dist/core/auth-errors.js.map +1 -0
  14. package/dist/core/config.d.ts +60 -0
  15. package/dist/core/config.d.ts.map +1 -0
  16. package/dist/core/config.js +115 -0
  17. package/dist/core/config.js.map +1 -0
  18. package/dist/core/device-flow.d.ts +80 -0
  19. package/dist/core/device-flow.d.ts.map +1 -0
  20. package/dist/core/device-flow.js +231 -0
  21. package/dist/core/device-flow.js.map +1 -0
  22. package/dist/core/index.d.ts +16 -0
  23. package/dist/core/index.d.ts.map +1 -0
  24. package/dist/core/index.js +16 -0
  25. package/dist/core/index.js.map +1 -0
  26. package/dist/core/lark-client.d.ts +136 -0
  27. package/dist/core/lark-client.d.ts.map +1 -0
  28. package/dist/core/lark-client.js +315 -0
  29. package/dist/core/lark-client.js.map +1 -0
  30. package/dist/core/token-store.d.ts +67 -0
  31. package/dist/core/token-store.d.ts.map +1 -0
  32. package/dist/core/token-store.js +215 -0
  33. package/dist/core/token-store.js.map +1 -0
  34. package/dist/core/types.d.ts +286 -0
  35. package/dist/core/types.d.ts.map +1 -0
  36. package/dist/core/types.js +11 -0
  37. package/dist/core/types.js.map +1 -0
  38. package/dist/core/uat-client.d.ts +64 -0
  39. package/dist/core/uat-client.d.ts.map +1 -0
  40. package/dist/core/uat-client.js +227 -0
  41. package/dist/core/uat-client.js.map +1 -0
  42. package/dist/core/version.d.ts +26 -0
  43. package/dist/core/version.d.ts.map +1 -0
  44. package/dist/core/version.js +50 -0
  45. package/dist/core/version.js.map +1 -0
  46. package/dist/index.d.ts +12 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +116 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/tools/bitable/app.d.ts +20 -0
  51. package/dist/tools/bitable/app.d.ts.map +1 -0
  52. package/dist/tools/bitable/app.js +301 -0
  53. package/dist/tools/bitable/app.js.map +1 -0
  54. package/dist/tools/bitable/field.d.ts +19 -0
  55. package/dist/tools/bitable/field.d.ts.map +1 -0
  56. package/dist/tools/bitable/field.js +315 -0
  57. package/dist/tools/bitable/field.js.map +1 -0
  58. package/dist/tools/bitable/index.d.ts +21 -0
  59. package/dist/tools/bitable/index.d.ts.map +1 -0
  60. package/dist/tools/bitable/index.js +39 -0
  61. package/dist/tools/bitable/index.js.map +1 -0
  62. package/dist/tools/bitable/record.d.ts +22 -0
  63. package/dist/tools/bitable/record.d.ts.map +1 -0
  64. package/dist/tools/bitable/record.js +434 -0
  65. package/dist/tools/bitable/record.js.map +1 -0
  66. package/dist/tools/bitable/table.d.ts +21 -0
  67. package/dist/tools/bitable/table.d.ts.map +1 -0
  68. package/dist/tools/bitable/table.js +361 -0
  69. package/dist/tools/bitable/table.js.map +1 -0
  70. package/dist/tools/calendar/calendar.d.ts +18 -0
  71. package/dist/tools/calendar/calendar.d.ts.map +1 -0
  72. package/dist/tools/calendar/calendar.js +192 -0
  73. package/dist/tools/calendar/calendar.js.map +1 -0
  74. package/dist/tools/calendar/event.d.ts +20 -0
  75. package/dist/tools/calendar/event.d.ts.map +1 -0
  76. package/dist/tools/calendar/event.js +465 -0
  77. package/dist/tools/calendar/event.js.map +1 -0
  78. package/dist/tools/calendar/index.d.ts +19 -0
  79. package/dist/tools/calendar/index.d.ts.map +1 -0
  80. package/dist/tools/calendar/index.js +37 -0
  81. package/dist/tools/calendar/index.js.map +1 -0
  82. package/dist/tools/chat/chat.d.ts +11 -0
  83. package/dist/tools/chat/chat.d.ts.map +1 -0
  84. package/dist/tools/chat/chat.js +106 -0
  85. package/dist/tools/chat/chat.js.map +1 -0
  86. package/dist/tools/chat/index.d.ts +11 -0
  87. package/dist/tools/chat/index.d.ts.map +1 -0
  88. package/dist/tools/chat/index.js +20 -0
  89. package/dist/tools/chat/index.js.map +1 -0
  90. package/dist/tools/chat/members.d.ts +9 -0
  91. package/dist/tools/chat/members.d.ts.map +1 -0
  92. package/dist/tools/chat/members.js +80 -0
  93. package/dist/tools/chat/members.js.map +1 -0
  94. package/dist/tools/common/get-user.d.ts +11 -0
  95. package/dist/tools/common/get-user.d.ts.map +1 -0
  96. package/dist/tools/common/get-user.js +112 -0
  97. package/dist/tools/common/get-user.js.map +1 -0
  98. package/dist/tools/common/index.d.ts +11 -0
  99. package/dist/tools/common/index.d.ts.map +1 -0
  100. package/dist/tools/common/index.js +20 -0
  101. package/dist/tools/common/index.js.map +1 -0
  102. package/dist/tools/common/search-user.d.ts +9 -0
  103. package/dist/tools/common/search-user.d.ts.map +1 -0
  104. package/dist/tools/common/search-user.js +88 -0
  105. package/dist/tools/common/search-user.js.map +1 -0
  106. package/dist/tools/doc/create.d.ts +17 -0
  107. package/dist/tools/doc/create.d.ts.map +1 -0
  108. package/dist/tools/doc/create.js +159 -0
  109. package/dist/tools/doc/create.js.map +1 -0
  110. package/dist/tools/doc/fetch.d.ts +17 -0
  111. package/dist/tools/doc/fetch.d.ts.map +1 -0
  112. package/dist/tools/doc/fetch.js +123 -0
  113. package/dist/tools/doc/fetch.js.map +1 -0
  114. package/dist/tools/doc/index.d.ts +21 -0
  115. package/dist/tools/doc/index.d.ts.map +1 -0
  116. package/dist/tools/doc/index.js +33 -0
  117. package/dist/tools/doc/index.js.map +1 -0
  118. package/dist/tools/doc/shared.d.ts +69 -0
  119. package/dist/tools/doc/shared.d.ts.map +1 -0
  120. package/dist/tools/doc/shared.js +172 -0
  121. package/dist/tools/doc/shared.js.map +1 -0
  122. package/dist/tools/doc/update.d.ts +25 -0
  123. package/dist/tools/doc/update.d.ts.map +1 -0
  124. package/dist/tools/doc/update.js +208 -0
  125. package/dist/tools/doc/update.js.map +1 -0
  126. package/dist/tools/drive/file.d.ts +13 -0
  127. package/dist/tools/drive/file.d.ts.map +1 -0
  128. package/dist/tools/drive/file.js +212 -0
  129. package/dist/tools/drive/file.js.map +1 -0
  130. package/dist/tools/drive/index.d.ts +12 -0
  131. package/dist/tools/drive/index.d.ts.map +1 -0
  132. package/dist/tools/drive/index.js +25 -0
  133. package/dist/tools/drive/index.js.map +1 -0
  134. package/dist/tools/im/format-messages.d.ts +99 -0
  135. package/dist/tools/im/format-messages.d.ts.map +1 -0
  136. package/dist/tools/im/format-messages.js +277 -0
  137. package/dist/tools/im/format-messages.js.map +1 -0
  138. package/dist/tools/im/helpers.d.ts +53 -0
  139. package/dist/tools/im/helpers.d.ts.map +1 -0
  140. package/dist/tools/im/helpers.js +85 -0
  141. package/dist/tools/im/helpers.js.map +1 -0
  142. package/dist/tools/im/index.d.ts +25 -0
  143. package/dist/tools/im/index.d.ts.map +1 -0
  144. package/dist/tools/im/index.js +44 -0
  145. package/dist/tools/im/index.js.map +1 -0
  146. package/dist/tools/im/message-read.d.ts +19 -0
  147. package/dist/tools/im/message-read.d.ts.map +1 -0
  148. package/dist/tools/im/message-read.js +526 -0
  149. package/dist/tools/im/message-read.js.map +1 -0
  150. package/dist/tools/im/message.d.ts +22 -0
  151. package/dist/tools/im/message.d.ts.map +1 -0
  152. package/dist/tools/im/message.js +233 -0
  153. package/dist/tools/im/message.js.map +1 -0
  154. package/dist/tools/im/resource.d.ts +19 -0
  155. package/dist/tools/im/resource.d.ts.map +1 -0
  156. package/dist/tools/im/resource.js +185 -0
  157. package/dist/tools/im/resource.js.map +1 -0
  158. package/dist/tools/im/time-utils.d.ts +70 -0
  159. package/dist/tools/im/time-utils.d.ts.map +1 -0
  160. package/dist/tools/im/time-utils.js +277 -0
  161. package/dist/tools/im/time-utils.js.map +1 -0
  162. package/dist/tools/index.d.ts +85 -0
  163. package/dist/tools/index.d.ts.map +1 -0
  164. package/dist/tools/index.js +135 -0
  165. package/dist/tools/index.js.map +1 -0
  166. package/dist/tools/oauth.d.ts +15 -0
  167. package/dist/tools/oauth.d.ts.map +1 -0
  168. package/dist/tools/oauth.js +379 -0
  169. package/dist/tools/oauth.js.map +1 -0
  170. package/dist/tools/search/doc-search.d.ts +9 -0
  171. package/dist/tools/search/doc-search.d.ts.map +1 -0
  172. package/dist/tools/search/doc-search.js +219 -0
  173. package/dist/tools/search/doc-search.js.map +1 -0
  174. package/dist/tools/search/index.d.ts +11 -0
  175. package/dist/tools/search/index.d.ts.map +1 -0
  176. package/dist/tools/search/index.js +18 -0
  177. package/dist/tools/search/index.js.map +1 -0
  178. package/dist/tools/sheets/index.d.ts +11 -0
  179. package/dist/tools/sheets/index.d.ts.map +1 -0
  180. package/dist/tools/sheets/index.js +18 -0
  181. package/dist/tools/sheets/index.js.map +1 -0
  182. package/dist/tools/sheets/sheet.d.ts +11 -0
  183. package/dist/tools/sheets/sheet.d.ts.map +1 -0
  184. package/dist/tools/sheets/sheet.js +332 -0
  185. package/dist/tools/sheets/sheet.js.map +1 -0
  186. package/dist/tools/task/index.d.ts +12 -0
  187. package/dist/tools/task/index.d.ts.map +1 -0
  188. package/dist/tools/task/index.js +30 -0
  189. package/dist/tools/task/index.js.map +1 -0
  190. package/dist/tools/task/task.d.ts +13 -0
  191. package/dist/tools/task/task.d.ts.map +1 -0
  192. package/dist/tools/task/task.js +225 -0
  193. package/dist/tools/task/task.js.map +1 -0
  194. package/dist/tools/task/tasklist.d.ts +13 -0
  195. package/dist/tools/task/tasklist.d.ts.map +1 -0
  196. package/dist/tools/task/tasklist.js +206 -0
  197. package/dist/tools/task/tasklist.js.map +1 -0
  198. package/dist/tools/wiki/index.d.ts +11 -0
  199. package/dist/tools/wiki/index.d.ts.map +1 -0
  200. package/dist/tools/wiki/index.js +20 -0
  201. package/dist/tools/wiki/index.js.map +1 -0
  202. package/dist/tools/wiki/node.d.ts +11 -0
  203. package/dist/tools/wiki/node.d.ts.map +1 -0
  204. package/dist/tools/wiki/node.js +112 -0
  205. package/dist/tools/wiki/node.js.map +1 -0
  206. package/dist/tools/wiki/space.d.ts +11 -0
  207. package/dist/tools/wiki/space.d.ts.map +1 -0
  208. package/dist/tools/wiki/space.js +125 -0
  209. package/dist/tools/wiki/space.js.map +1 -0
  210. package/dist/utils/index.d.ts +8 -0
  211. package/dist/utils/index.d.ts.map +1 -0
  212. package/dist/utils/index.js +8 -0
  213. package/dist/utils/index.js.map +1 -0
  214. package/dist/utils/logger.d.ts +36 -0
  215. package/dist/utils/logger.d.ts.map +1 -0
  216. package/dist/utils/logger.js +101 -0
  217. package/dist/utils/logger.js.map +1 -0
  218. package/eslint.config.js +13 -0
  219. package/package.json +54 -0
  220. package/skills/feishu-bitable/SKILL.md +248 -0
  221. package/skills/feishu-bitable/references/examples.md +813 -0
  222. package/skills/feishu-bitable/references/field-properties.md +763 -0
  223. package/skills/feishu-bitable/references/record-values.md +911 -0
  224. package/skills/feishu-calendar/SKILL.md +244 -0
  225. package/skills/feishu-channel-rules/SKILL.md +18 -0
  226. package/skills/feishu-channel-rules/references/markdown-syntax.md +138 -0
  227. package/skills/feishu-create-doc/SKILL.md +719 -0
  228. package/skills/feishu-fetch-doc/SKILL.md +93 -0
  229. package/skills/feishu-im-read/SKILL.md +163 -0
  230. package/skills/feishu-task/SKILL.md +293 -0
  231. package/skills/feishu-troubleshoot/SKILL.md +70 -0
  232. package/skills/feishu-update-doc/SKILL.md +285 -0
  233. package/src/core/api-error.ts +342 -0
  234. package/src/core/auth-errors.ts +27 -0
  235. package/src/core/config.ts +134 -0
  236. package/src/core/device-flow.ts +314 -0
  237. package/src/core/index.ts +16 -0
  238. package/src/core/lark-client.ts +391 -0
  239. package/src/core/token-store.ts +249 -0
  240. package/src/core/types.ts +302 -0
  241. package/src/core/uat-client.ts +298 -0
  242. package/src/core/version.ts +53 -0
  243. package/src/index.ts +138 -0
  244. package/src/tools/bitable/app.ts +390 -0
  245. package/src/tools/bitable/field.ts +406 -0
  246. package/src/tools/bitable/index.ts +43 -0
  247. package/src/tools/bitable/record.ts +559 -0
  248. package/src/tools/bitable/table.ts +472 -0
  249. package/src/tools/calendar/calendar.ts +254 -0
  250. package/src/tools/calendar/event.ts +606 -0
  251. package/src/tools/calendar/index.ts +41 -0
  252. package/src/tools/chat/chat.ts +127 -0
  253. package/src/tools/chat/index.ts +24 -0
  254. package/src/tools/chat/members.ts +93 -0
  255. package/src/tools/common/get-user.ts +127 -0
  256. package/src/tools/common/index.ts +24 -0
  257. package/src/tools/common/search-user.ts +99 -0
  258. package/src/tools/doc/create.ts +184 -0
  259. package/src/tools/doc/fetch.ts +149 -0
  260. package/src/tools/doc/index.ts +38 -0
  261. package/src/tools/doc/shared.ts +228 -0
  262. package/src/tools/doc/update.ts +240 -0
  263. package/src/tools/drive/file.ts +265 -0
  264. package/src/tools/drive/index.ts +29 -0
  265. package/src/tools/im/format-messages.ts +391 -0
  266. package/src/tools/im/helpers.ts +109 -0
  267. package/src/tools/im/index.ts +49 -0
  268. package/src/tools/im/message-read.ts +676 -0
  269. package/src/tools/im/message.ts +303 -0
  270. package/src/tools/im/resource.ts +225 -0
  271. package/src/tools/im/time-utils.ts +347 -0
  272. package/src/tools/index.ts +205 -0
  273. package/src/tools/oauth.ts +460 -0
  274. package/src/tools/search/doc-search.ts +250 -0
  275. package/src/tools/search/index.ts +22 -0
  276. package/src/tools/sheets/index.ts +22 -0
  277. package/src/tools/sheets/sheet.ts +382 -0
  278. package/src/tools/task/index.ts +34 -0
  279. package/src/tools/task/task.ts +265 -0
  280. package/src/tools/task/tasklist.ts +262 -0
  281. package/src/tools/wiki/index.ts +24 -0
  282. package/src/tools/wiki/node.ts +131 -0
  283. package/src/tools/wiki/space.ts +152 -0
  284. package/src/utils/index.ts +8 -0
  285. package/src/utils/logger.ts +132 -0
  286. package/tests/core/config.test.ts +238 -0
  287. package/tests/core/device-flow.test.ts +490 -0
  288. package/tests/core/lark-client.test.ts +378 -0
  289. package/tests/core/token-store.test.ts +438 -0
  290. package/tests/index.test.ts +360 -0
  291. package/tests/tools/doc/create.test.ts +224 -0
  292. package/tests/tools/doc/fetch.test.ts +182 -0
  293. package/tests/tools/doc/shared.test.ts +183 -0
  294. package/tests/tools/doc/update.test.ts +330 -0
  295. package/tests/tools/im/format-messages.test.ts +184 -0
  296. package/tests/tools/im/time-utils.test.ts +178 -0
  297. package/tests/utils/logger.test.ts +140 -0
  298. package/tsconfig.json +20 -0
@@ -0,0 +1,676 @@
1
+ /**
2
+ * Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * IM message read tools - Get/search Feishu messages with user identity.
6
+ *
7
+ * Tools:
8
+ * - feishu_im_get_messages: Get chat/thread messages
9
+ * - feishu_im_get_thread_messages: Get thread messages
10
+ * - feishu_im_search_messages: Search messages across chats
11
+ *
12
+ * Adapted from openclaw-lark for MCP Server architecture.
13
+ */
14
+
15
+ import { z } from 'zod';
16
+ import type { ToolRegistry } from '../index.js';
17
+ import { LarkClient } from '../../core/lark-client.js';
18
+ import { getValidAccessToken, NeedAuthorizationError } from '../../core/uat-client.js';
19
+ import { assertLarkOk } from '../../core/api-error.js';
20
+ import { json, jsonError, getCachedUserName, setCachedUserNames, type ToolResult } from './helpers.js';
21
+ import { parseTimeRangeToSeconds, dateTimeToSecondsString, millisStringToDateTime } from './time-utils.js';
22
+ import {
23
+ formatMessageList,
24
+ type FormattedMessage,
25
+ type ApiMessageItem,
26
+ extractMentionOpenId,
27
+ } from './format-messages.js';
28
+ import { logger } from '../../utils/logger.js';
29
+
30
+ const log = logger('tools:im:message-read');
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Shared schema components (raw shapes for ZodRawShapeCompat)
34
+ // ---------------------------------------------------------------------------
35
+
36
+ const sortRuleShape = {
37
+ sort_rule: z
38
+ .enum(['create_time_asc', 'create_time_desc'])
39
+ .optional()
40
+ .describe('Sort order: create_time_asc (oldest first) or create_time_desc (newest first, default)'),
41
+ };
42
+
43
+ const paginationShape = {
44
+ page_size: z.number().min(1).max(50).optional().describe('Number of results per page (1-50), default 50'),
45
+ page_token: z.string().optional().describe('Pagination token for next page'),
46
+ };
47
+
48
+ const timeRangeShape = {
49
+ relative_time: z
50
+ .string()
51
+ .optional()
52
+ .describe(
53
+ 'Relative time range: today / yesterday / day_before_yesterday / this_week / last_week / this_month / last_month / last_{N}_{unit} (unit: minutes/hours/days). Mutually exclusive with start_time/end_time.'
54
+ ),
55
+ start_time: z
56
+ .string()
57
+ .optional()
58
+ .describe('Start time (ISO 8601 format, e.g., 2026-02-27T00:00:00+08:00). Mutually exclusive with relative_time.'),
59
+ end_time: z
60
+ .string()
61
+ .optional()
62
+ .describe('End time (ISO 8601 format, e.g., 2026-02-27T23:59:59+08:00). Mutually exclusive with relative_time.'),
63
+ };
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // feishu_im_get_messages
67
+ // ---------------------------------------------------------------------------
68
+
69
+ const getMessagesShape = {
70
+ open_id: z
71
+ .string()
72
+ .optional()
73
+ .describe('User open_id (ou_xxx) to get 1-on-1 chat messages. Mutually exclusive with chat_id.'),
74
+ chat_id: z
75
+ .string()
76
+ .optional()
77
+ .describe('Chat ID (oc_xxx) to get group or 1-on-1 chat messages. Mutually exclusive with open_id.'),
78
+ ...sortRuleShape,
79
+ ...paginationShape,
80
+ ...timeRangeShape,
81
+ };
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // feishu_im_get_thread_messages
85
+ // ---------------------------------------------------------------------------
86
+
87
+ const getThreadMessagesShape = {
88
+ thread_id: z.string().describe('Thread ID (omt_xxx format)'),
89
+ ...sortRuleShape,
90
+ ...paginationShape,
91
+ };
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // feishu_im_search_messages
95
+ // ---------------------------------------------------------------------------
96
+
97
+ const searchMessagesShape = {
98
+ query: z
99
+ .string()
100
+ .optional()
101
+ .describe('Search keyword to match message content. Can be empty string for no content filter.'),
102
+ sender_ids: z
103
+ .array(z.string())
104
+ .optional()
105
+ .describe("Sender open_id list (ou_xxx). Use search_user tool to find open_id by name if needed."),
106
+ chat_id: z.string().optional().describe('Chat ID (oc_xxx) to limit search scope'),
107
+ mention_ids: z.array(z.string()).optional().describe('Mentioned user open_id list (ou_xxx)'),
108
+ message_type: z
109
+ .enum(['file', 'image', 'media'])
110
+ .optional()
111
+ .describe('Message type filter: file / image / media. Empty for all types.'),
112
+ sender_type: z.enum(['user', 'bot', 'all']).optional().describe('Sender type: user / bot / all. Default user.'),
113
+ chat_type: z.enum(['group', 'p2p']).optional().describe('Chat type: group (group chat) / p2p (private chat)'),
114
+ ...timeRangeShape,
115
+ ...paginationShape,
116
+ };
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Helper functions
120
+ // ---------------------------------------------------------------------------
121
+
122
+ function sortRuleToSortType(rule?: 'create_time_asc' | 'create_time_desc'): 'ByCreateTimeAsc' | 'ByCreateTimeDesc' {
123
+ return rule === 'create_time_asc' ? 'ByCreateTimeAsc' : 'ByCreateTimeDesc';
124
+ }
125
+
126
+ function resolveTimeRange(
127
+ p: { relative_time?: string; start_time?: string; end_time?: string },
128
+ logInfo: (msg: string) => void
129
+ ): { start?: string; end?: string } {
130
+ if (p.relative_time) {
131
+ const range = parseTimeRangeToSeconds(p.relative_time);
132
+ logInfo(`relative_time="${p.relative_time}" -> start=${range.start}, end=${range.end}`);
133
+ return range;
134
+ }
135
+ return {
136
+ start: p.start_time ? dateTimeToSecondsString(p.start_time) : undefined,
137
+ end: p.end_time ? dateTimeToSecondsString(p.end_time) : undefined,
138
+ };
139
+ }
140
+
141
+ type AuthResult = { accessToken: string; userOpenId: string };
142
+
143
+ /**
144
+ * Get authorization and access token for handlers.
145
+ */
146
+ async function getAuth(
147
+ config: import('../../core/types.js').FeishuConfig
148
+ ): Promise<AuthResult | ToolResult> {
149
+ const { appId, appSecret, brand } = config;
150
+ if (!appId || !appSecret) {
151
+ return jsonError('Missing FEISHU_APP_ID or FEISHU_APP_SECRET.');
152
+ }
153
+
154
+ const { listStoredTokens } = await import('../../core/token-store.js');
155
+ const tokens = await listStoredTokens(appId);
156
+ if (tokens.length === 0) {
157
+ return jsonError(
158
+ 'No user authorization found. Please use the feishu_oauth tool with action="authorize" to authorize a user first.'
159
+ );
160
+ }
161
+
162
+ const userOpenId = tokens[0].userOpenId;
163
+
164
+ try {
165
+ const accessToken = await getValidAccessToken({
166
+ userOpenId,
167
+ appId,
168
+ appSecret,
169
+ domain: brand ?? 'feishu',
170
+ });
171
+ return { accessToken, userOpenId };
172
+ } catch (err) {
173
+ if (err instanceof NeedAuthorizationError) {
174
+ return jsonError(
175
+ `User authorization required or expired. Please use feishu_oauth tool with action="authorize" to re-authorize.`,
176
+ { userOpenId }
177
+ );
178
+ }
179
+ throw err;
180
+ }
181
+ }
182
+
183
+ function isAuthResult(result: AuthResult | ToolResult): result is AuthResult {
184
+ return 'accessToken' in result;
185
+ }
186
+
187
+ /**
188
+ * Resolve P2P chat_id from open_id.
189
+ */
190
+ async function resolveP2PChatId(
191
+ sdk: LarkClient['sdk'],
192
+ openId: string,
193
+ accessToken: string,
194
+ logInfo: (msg: string) => void
195
+ ): Promise<string> {
196
+ const Lark = await import('@larksuiteoapi/node-sdk');
197
+ const opts = Lark.withUserAccessToken(accessToken);
198
+
199
+ const res = await sdk.request<{
200
+ code?: number;
201
+ msg?: string;
202
+ data?: { p2p_chats?: Array<{ chat_id: string }> };
203
+ }>({
204
+ method: 'POST',
205
+ url: '/open-apis/im/v1/chat_p2p/batch_query?user_id_type=open_id',
206
+ data: { chatter_ids: [openId] },
207
+ }, opts);
208
+
209
+ const chats = res.data?.p2p_chats;
210
+ if (!chats?.length) {
211
+ logInfo(`batch_query: no p2p chat found for open_id=${openId}`);
212
+ throw new Error(`No 1-on-1 chat found with open_id=${openId}. You may not have chat history with this user.`);
213
+ }
214
+
215
+ logInfo(`batch_query: resolved chat_id=${chats[0].chat_id}`);
216
+ return chats[0].chat_id;
217
+ }
218
+
219
+ /**
220
+ * Batch resolve user names via API.
221
+ */
222
+ async function batchResolveUserNames(
223
+ sdk: LarkClient['sdk'],
224
+ openIds: string[],
225
+ accessToken: string
226
+ ): Promise<Map<string, string>> {
227
+ const result = new Map<string, string>();
228
+ if (openIds.length === 0) return result;
229
+
230
+ // Check cache first
231
+ const missing: string[] = [];
232
+ for (const id of openIds) {
233
+ const cached = getCachedUserName(id);
234
+ if (cached) {
235
+ result.set(id, cached);
236
+ } else {
237
+ missing.push(id);
238
+ }
239
+ }
240
+
241
+ if (missing.length === 0) return result;
242
+
243
+ // Batch query (50 at a time)
244
+ const BATCH_SIZE = 50;
245
+ const Lark = await import('@larksuiteoapi/node-sdk');
246
+ const opts = Lark.withUserAccessToken(accessToken);
247
+
248
+ for (let i = 0; i < missing.length; i += BATCH_SIZE) {
249
+ const chunk = missing.slice(i, i + BATCH_SIZE);
250
+ try {
251
+ const queryParams = new URLSearchParams();
252
+ queryParams.set('user_id_type', 'open_id');
253
+ chunk.forEach((id) => queryParams.append('user_ids', id));
254
+
255
+ const res = await sdk.request<{
256
+ code?: number;
257
+ data?: { items?: Array<{ open_id?: string; name?: string; display_name?: string; nickname?: string; en_name?: string }> };
258
+ }>({
259
+ method: 'GET',
260
+ url: `/open-apis/contact/v3/users/batch_get?${queryParams.toString()}`,
261
+ }, opts);
262
+
263
+ if (res.code === 0 && res.data?.items) {
264
+ for (const item of res.data.items) {
265
+ const openId = item.open_id;
266
+ const name = item.name || item.display_name || item.nickname || item.en_name;
267
+ if (openId && name) {
268
+ result.set(openId, name);
269
+ }
270
+ }
271
+ }
272
+ } catch (err) {
273
+ log.warn('Failed to batch resolve user names', { error: err instanceof Error ? err.message : String(err) });
274
+ }
275
+ }
276
+
277
+ // Cache the results
278
+ if (result.size > 0) {
279
+ setCachedUserNames(result);
280
+ }
281
+
282
+ return result;
283
+ }
284
+
285
+ // ---------------------------------------------------------------------------
286
+ // Tool registration
287
+ // ---------------------------------------------------------------------------
288
+
289
+ /**
290
+ * Register all IM message read tools.
291
+ */
292
+ export function registerImMessageReadTools(registry: ToolRegistry): void {
293
+ registerGetMessages(registry);
294
+ registerGetThreadMessages(registry);
295
+ registerSearchMessages(registry);
296
+ log.debug('IM message read tools registered');
297
+ }
298
+
299
+ function registerGetMessages(registry: ToolRegistry): void {
300
+ registry.register({
301
+ name: 'feishu_im_get_messages',
302
+ description: [
303
+ 'Get chat history messages with user identity.',
304
+ '',
305
+ 'Usage:',
306
+ '- Use chat_id to get group/private chat messages',
307
+ '- Use open_id to get 1-on-1 chat messages with a specific user (auto-resolves chat_id)',
308
+ '- Supports time range filter: relative_time (e.g., today, last_3_days) or start_time/end_time (ISO 8601)',
309
+ '- Supports pagination: page_size + page_token',
310
+ '',
311
+ 'Constraints:',
312
+ '- Must provide either open_id or chat_id (not both)',
313
+ '- relative_time and start_time/end_time are mutually exclusive',
314
+ '- page_size range 1-50, default 50',
315
+ '',
316
+ 'Returns message list with message_id, msg_type, content (AI-readable text), sender, create_time, etc.',
317
+ '',
318
+ 'Requires OAuth authorization (use feishu_oauth tool with action="authorize" first).',
319
+ ].join('\n'),
320
+ inputSchema: getMessagesShape,
321
+ handler: async (args, context) => {
322
+ const p = args as z.infer<ReturnType<typeof z.object<typeof getMessagesShape>>>;
323
+
324
+ if (!context.larkClient) {
325
+ return jsonError('LarkClient not initialized. Check FEISHU_APP_ID and FEISHU_APP_SECRET.');
326
+ }
327
+
328
+ // Validate parameters
329
+ if (p.open_id && p.chat_id) {
330
+ return jsonError('Cannot provide both open_id and chat_id, please provide only one');
331
+ }
332
+ if (!p.open_id && !p.chat_id) {
333
+ return jsonError('Either open_id or chat_id is required');
334
+ }
335
+ if (p.relative_time && (p.start_time || p.end_time)) {
336
+ return jsonError('Cannot use both relative_time and start_time/end_time');
337
+ }
338
+
339
+ const authResult = await getAuth(context.config);
340
+ if (!isAuthResult(authResult)) return authResult;
341
+ const { accessToken } = authResult;
342
+
343
+ const logInfo = (msg: string) => log.info(msg);
344
+
345
+ let chatId = p.chat_id ?? '';
346
+ if (p.open_id) {
347
+ logInfo(`Resolving P2P chat for open_id=${p.open_id}`);
348
+ chatId = await resolveP2PChatId(context.larkClient.sdk, p.open_id, accessToken, logInfo);
349
+ }
350
+
351
+ const time = resolveTimeRange(p, logInfo);
352
+ logInfo(`list: chat_id=${chatId}, sort=${p.sort_rule ?? 'create_time_desc'}, page_size=${p.page_size ?? 50}`);
353
+
354
+ const Lark = await import('@larksuiteoapi/node-sdk');
355
+ const opts = Lark.withUserAccessToken(accessToken);
356
+
357
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
358
+ const res = await context.larkClient.sdk.im.v1.message.list(
359
+ {
360
+ params: {
361
+ container_id_type: 'chat',
362
+ container_id: chatId,
363
+ start_time: time.start,
364
+ end_time: time.end,
365
+ sort_type: sortRuleToSortType(p.sort_rule),
366
+ page_size: p.page_size ?? 50,
367
+ page_token: p.page_token,
368
+ card_msg_content_type: 'raw_card_content',
369
+ } as any,
370
+ },
371
+ opts
372
+ );
373
+
374
+ assertLarkOk(res);
375
+
376
+ const items = (res.data?.items ?? []) as ApiMessageItem[];
377
+ const nameResolver = (id: string) => getCachedUserName(id);
378
+ const batchResolver = async (ids: string[]) => {
379
+ await batchResolveUserNames(context.larkClient!.sdk, ids, accessToken);
380
+ };
381
+
382
+ const messages = await formatMessageList(items, 'default', nameResolver, batchResolver);
383
+ const hasMore = res.data?.has_more ?? false;
384
+ const pageToken = res.data?.page_token;
385
+
386
+ logInfo(`list: returned ${messages.length} messages, has_more=${hasMore}`);
387
+
388
+ return json({ messages, has_more: hasMore, page_token: pageToken });
389
+ },
390
+ });
391
+ }
392
+
393
+ function registerGetThreadMessages(registry: ToolRegistry): void {
394
+ registry.register({
395
+ name: 'feishu_im_get_thread_messages',
396
+ description: [
397
+ 'Get messages in a thread with user identity.',
398
+ '',
399
+ 'Usage:',
400
+ '- Use thread_id (omt_xxx) to get all messages in a thread',
401
+ '- Supports pagination: page_size + page_token',
402
+ '',
403
+ 'Note: Thread messages do not support time range filtering (Lark API limitation)',
404
+ '',
405
+ 'Returns message list in the same format as feishu_im_get_messages.',
406
+ '',
407
+ 'Requires OAuth authorization (use feishu_oauth tool with action="authorize" first).',
408
+ ].join('\n'),
409
+ inputSchema: getThreadMessagesShape,
410
+ handler: async (args, context) => {
411
+ const p = args as z.infer<ReturnType<typeof z.object<typeof getThreadMessagesShape>>>;
412
+
413
+ if (!context.larkClient) {
414
+ return jsonError('LarkClient not initialized. Check FEISHU_APP_ID and FEISHU_APP_SECRET.');
415
+ }
416
+
417
+ const authResult = await getAuth(context.config);
418
+ if (!isAuthResult(authResult)) return authResult;
419
+ const { accessToken } = authResult;
420
+
421
+ log.info(`list: thread_id=${p.thread_id}, sort=${p.sort_rule ?? 'create_time_desc'}, page_size=${p.page_size ?? 50}`);
422
+
423
+ const Lark = await import('@larksuiteoapi/node-sdk');
424
+ const opts = Lark.withUserAccessToken(accessToken);
425
+
426
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
427
+ const res = await context.larkClient.sdk.im.v1.message.list(
428
+ {
429
+ params: {
430
+ container_id_type: 'thread',
431
+ container_id: p.thread_id,
432
+ sort_type: sortRuleToSortType(p.sort_rule),
433
+ page_size: p.page_size ?? 50,
434
+ page_token: p.page_token,
435
+ card_msg_content_type: 'raw_card_content',
436
+ } as any,
437
+ },
438
+ opts
439
+ );
440
+
441
+ assertLarkOk(res);
442
+
443
+ const items = (res.data?.items ?? []) as ApiMessageItem[];
444
+ const nameResolver = (id: string) => getCachedUserName(id);
445
+ const batchResolver = async (ids: string[]) => {
446
+ await batchResolveUserNames(context.larkClient!.sdk, ids, accessToken);
447
+ };
448
+
449
+ const messages = await formatMessageList(items, 'default', nameResolver, batchResolver);
450
+ const hasMore = res.data?.has_more ?? false;
451
+ const pageToken = res.data?.page_token;
452
+
453
+ log.info(`list: returned ${messages.length} messages, has_more=${hasMore}`);
454
+
455
+ return json({ messages, has_more: hasMore, page_token: pageToken });
456
+ },
457
+ });
458
+ }
459
+
460
+ function registerSearchMessages(registry: ToolRegistry): void {
461
+ registry.register({
462
+ name: 'feishu_im_search_messages',
463
+ description: [
464
+ 'Search messages across chats with user identity.',
465
+ '',
466
+ 'Usage:',
467
+ '- Search by keyword in message content',
468
+ '- Filter by sender, mentioned users, message type',
469
+ '- Filter by time range: relative_time or start_time/end_time',
470
+ '- Limit search to a specific chat (chat_id)',
471
+ '- Supports pagination: page_size + page_token',
472
+ '',
473
+ 'Constraints:',
474
+ '- All parameters are optional but at least one filter should be provided',
475
+ '- relative_time and start_time/end_time are mutually exclusive',
476
+ '- page_size range 1-50, default 50',
477
+ '',
478
+ 'Returns message list with chat_id, chat_type (p2p/group), chat_name.',
479
+ 'For p2p chats, includes chat_partner with open_id and name.',
480
+ 'Use chat_id and thread_id from results with feishu_im_get_messages / feishu_im_get_thread_messages for context.',
481
+ '',
482
+ 'Requires OAuth authorization (use feishu_oauth tool with action="authorize" first).',
483
+ ].join('\n'),
484
+ inputSchema: searchMessagesShape,
485
+ handler: async (args, context) => {
486
+ const p = args as z.infer<ReturnType<typeof z.object<typeof searchMessagesShape>>>;
487
+
488
+ if (!context.larkClient) {
489
+ return jsonError('LarkClient not initialized. Check FEISHU_APP_ID and FEISHU_APP_SECRET.');
490
+ }
491
+
492
+ if (p.relative_time && (p.start_time || p.end_time)) {
493
+ return jsonError('Cannot use both relative_time and start_time/end_time');
494
+ }
495
+
496
+ const authResult = await getAuth(context.config);
497
+ if (!isAuthResult(authResult)) return authResult;
498
+ const { accessToken } = authResult;
499
+
500
+ const logInfo = (msg: string) => log.info(msg);
501
+
502
+ // Resolve time range
503
+ const time = resolveTimeRange(p, logInfo);
504
+ const searchData: Record<string, unknown> = {
505
+ query: p.query ?? '',
506
+ start_time: time.start ?? '978307200', // Default to 2001-01-01
507
+ end_time: time.end ?? Math.floor(Date.now() / 1000).toString(),
508
+ };
509
+ if (p.sender_ids?.length) searchData.from_ids = p.sender_ids;
510
+ if (p.chat_id) searchData.chat_ids = [p.chat_id];
511
+ if (p.mention_ids?.length) searchData.at_chatter_ids = p.mention_ids;
512
+ if (p.message_type) searchData.message_type = p.message_type;
513
+ if (p.sender_type && p.sender_type !== 'all') searchData.from_type = p.sender_type;
514
+ if (p.chat_type) searchData.chat_type = p.chat_type === 'group' ? 'group_chat' : 'p2p_chat';
515
+
516
+ logInfo(`search: query="${p.query ?? ''}", page_size=${p.page_size ?? 50}`);
517
+
518
+ const Lark = await import('@larksuiteoapi/node-sdk');
519
+ const opts = Lark.withUserAccessToken(accessToken);
520
+
521
+ // Step 1: Search for message IDs
522
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
523
+ const searchRes = await (context.larkClient.sdk as any).search.message.create(
524
+ {
525
+ data: searchData,
526
+ params: {
527
+ user_id_type: 'open_id',
528
+ page_size: p.page_size ?? 50,
529
+ page_token: p.page_token,
530
+ },
531
+ },
532
+ opts
533
+ );
534
+
535
+ assertLarkOk(searchRes);
536
+
537
+ const messageIds: string[] = searchRes.data?.items ?? [];
538
+ const hasMore = searchRes.data?.has_more ?? false;
539
+ const pageToken = searchRes.data?.page_token;
540
+ logInfo(`search: found ${messageIds.length} IDs, has_more=${hasMore}`);
541
+
542
+ if (messageIds.length === 0) {
543
+ return json({ messages: [], has_more: hasMore, page_token: pageToken });
544
+ }
545
+
546
+ // Step 2: Batch get message details
547
+ const queryStr = messageIds.map((id) => `message_ids=${encodeURIComponent(id)}`).join('&');
548
+ const mgetRes = await context.larkClient.sdk.request<{
549
+ code?: number;
550
+ msg?: string;
551
+ data?: { items?: ApiMessageItem[] };
552
+ }>({
553
+ method: 'GET',
554
+ url: `/open-apis/im/v1/messages/mget?${queryStr}&user_id_type=open_id&card_msg_content_type=raw_card_content`,
555
+ }, opts);
556
+
557
+ const items = mgetRes.data?.items ?? [];
558
+ logInfo(`mget: ${items.length} details`);
559
+
560
+ // Step 3: Batch get chat info
561
+ const chatIds = [...new Set(items.map((i) => i.chat_id).filter(Boolean))] as string[];
562
+ const chatMap = await fetchChatContexts(context.larkClient.sdk, accessToken, chatIds, logInfo);
563
+
564
+ // Step 4: Format messages
565
+ const nameResolver = (id: string) => getCachedUserName(id);
566
+ const batchResolver = async (ids: string[]) => {
567
+ await batchResolveUserNames(context.larkClient!.sdk, ids, accessToken);
568
+ };
569
+ const messages = await formatMessageList(items, 'default', nameResolver, batchResolver);
570
+
571
+ // Step 5: Resolve p2p target names
572
+ const p2pTargetIds = [...new Set([...chatMap.values()].map((c) => c.p2p_target_id).filter(Boolean))] as string[];
573
+ if (p2pTargetIds.length > 0) {
574
+ await batchResolveUserNames(context.larkClient.sdk, p2pTargetIds, accessToken);
575
+ }
576
+
577
+ // Step 6: Enrich messages with chat info
578
+ const enrichedMessages = enrichMessages(messages, items, chatMap, nameResolver);
579
+
580
+ logInfo(`result: ${enrichedMessages.length} messages, has_more=${hasMore}`);
581
+
582
+ return json({ messages: enrichedMessages, has_more: hasMore, page_token: pageToken });
583
+ },
584
+ });
585
+ }
586
+
587
+ // ---------------------------------------------------------------------------
588
+ // Helpers for search
589
+ // ---------------------------------------------------------------------------
590
+
591
+ interface ChatContext {
592
+ name: string;
593
+ chat_mode: string;
594
+ p2p_target_id?: string;
595
+ }
596
+
597
+ async function fetchChatContexts(
598
+ sdk: LarkClient['sdk'],
599
+ accessToken: string,
600
+ chatIds: string[],
601
+ logInfo: (msg: string) => void
602
+ ): Promise<Map<string, ChatContext>> {
603
+ const map = new Map<string, ChatContext>();
604
+ if (chatIds.length === 0) return map;
605
+
606
+ try {
607
+ logInfo(`batch_query: requesting ${chatIds.length} chat_ids`);
608
+ const Lark = await import('@larksuiteoapi/node-sdk');
609
+ const opts = Lark.withUserAccessToken(accessToken);
610
+
611
+ const res = await sdk.request<{
612
+ code?: number;
613
+ msg?: string;
614
+ data?: {
615
+ items?: Array<{
616
+ chat_id?: string;
617
+ name?: string;
618
+ chat_mode?: string;
619
+ p2p_target_id?: string;
620
+ }>;
621
+ };
622
+ }>({
623
+ method: 'POST',
624
+ url: '/open-apis/im/v1/chats/batch_query?user_id_type=open_id',
625
+ data: { chat_ids: chatIds },
626
+ }, opts);
627
+
628
+ logInfo(`batch_query: response code=${res.code}, items=${res.data?.items?.length ?? 0}`);
629
+ if (res.code !== 0) {
630
+ log.warn(`batch_query: API error code=${res.code}, msg=${res.msg}`);
631
+ }
632
+ for (const c of res.data?.items ?? []) {
633
+ if (c.chat_id) {
634
+ map.set(c.chat_id, {
635
+ name: c.name ?? '',
636
+ chat_mode: c.chat_mode ?? '',
637
+ p2p_target_id: c.p2p_target_id,
638
+ });
639
+ }
640
+ }
641
+ } catch (err) {
642
+ logInfo(`batch_query chats failed: ${err}`);
643
+ }
644
+ return map;
645
+ }
646
+
647
+ function enrichMessages(
648
+ messages: FormattedMessage[],
649
+ items: ApiMessageItem[],
650
+ chatMap: Map<string, ChatContext>,
651
+ nameResolver: (openId: string) => string | undefined
652
+ ): FormattedMessage[] {
653
+ return messages.map((msg, idx) => {
654
+ const chatId = items[idx]?.chat_id;
655
+ const ctx = chatId ? chatMap.get(chatId) : undefined;
656
+ if (!chatId || !ctx) return { ...msg, chat_id: chatId };
657
+
658
+ if (ctx.chat_mode === 'p2p' && ctx.p2p_target_id) {
659
+ const name = nameResolver(ctx.p2p_target_id);
660
+ return {
661
+ ...msg,
662
+ chat_id: chatId,
663
+ chat_type: 'p2p' as const,
664
+ chat_name: name || undefined,
665
+ chat_partner: { open_id: ctx.p2p_target_id, name: name || undefined },
666
+ };
667
+ }
668
+
669
+ return {
670
+ ...msg,
671
+ chat_id: chatId,
672
+ chat_type: ctx.chat_mode as 'p2p' | 'group',
673
+ chat_name: ctx.name || undefined,
674
+ };
675
+ });
676
+ }