@geminilight/mindos 0.6.73 → 0.6.75

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 (257) hide show
  1. package/_standalone/.mindos-build-version +1 -1
  2. package/_standalone/.next/BUILD_ID +1 -1
  3. package/_standalone/.next/app-path-routes-manifest.json +30 -30
  4. package/_standalone/.next/build-manifest.json +3 -3
  5. package/_standalone/.next/cache/.previewinfo +1 -1
  6. package/_standalone/.next/cache/.rscinfo +1 -1
  7. package/_standalone/.next/cache/config.json +3 -3
  8. package/_standalone/.next/prerender-manifest.json +3 -3
  9. package/_standalone/.next/react-loadable-manifest.json +5 -5
  10. package/_standalone/.next/required-server-files.json +14 -1
  11. package/_standalone/.next/server/app/.well-known/agent-card.json/route_client-reference-manifest.js +1 -1
  12. package/_standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  13. package/_standalone/.next/server/app/_global-error.html +2 -2
  14. package/_standalone/.next/server/app/_global-error.rsc +1 -1
  15. package/_standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  16. package/_standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  17. package/_standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  18. package/_standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  19. package/_standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  20. package/_standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  21. package/_standalone/.next/server/app/_not-found/page.js +1 -1
  22. package/_standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  23. package/_standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  24. package/_standalone/.next/server/app/agents/[agentKey]/page.js +1 -1
  25. package/_standalone/.next/server/app/agents/[agentKey]/page.js.nft.json +1 -1
  26. package/_standalone/.next/server/app/agents/[agentKey]/page_client-reference-manifest.js +1 -1
  27. package/_standalone/.next/server/app/agents/page.js +1 -1
  28. package/_standalone/.next/server/app/agents/page.js.nft.json +1 -1
  29. package/_standalone/.next/server/app/agents/page_client-reference-manifest.js +1 -1
  30. package/_standalone/.next/server/app/api/a2a/agents/route_client-reference-manifest.js +1 -1
  31. package/_standalone/.next/server/app/api/a2a/delegations/route_client-reference-manifest.js +1 -1
  32. package/_standalone/.next/server/app/api/a2a/discover/route_client-reference-manifest.js +1 -1
  33. package/_standalone/.next/server/app/api/a2a/route_client-reference-manifest.js +1 -1
  34. package/_standalone/.next/server/app/api/acp/config/route_client-reference-manifest.js +1 -1
  35. package/_standalone/.next/server/app/api/acp/detect/route_client-reference-manifest.js +1 -1
  36. package/_standalone/.next/server/app/api/acp/install/route_client-reference-manifest.js +1 -1
  37. package/_standalone/.next/server/app/api/acp/registry/route_client-reference-manifest.js +1 -1
  38. package/_standalone/.next/server/app/api/acp/session/route_client-reference-manifest.js +1 -1
  39. package/_standalone/.next/server/app/api/agent-activity/route_client-reference-manifest.js +1 -1
  40. package/_standalone/.next/server/app/api/agents/copy-skill/route.js.nft.json +1 -1
  41. package/_standalone/.next/server/app/api/agents/copy-skill/route_client-reference-manifest.js +1 -1
  42. package/_standalone/.next/server/app/api/agents/custom/detect/route_client-reference-manifest.js +1 -1
  43. package/_standalone/.next/server/app/api/agents/custom/route_client-reference-manifest.js +1 -1
  44. package/_standalone/.next/server/app/api/ask/route.js +1 -1
  45. package/_standalone/.next/server/app/api/ask/route.js.nft.json +1 -1
  46. package/_standalone/.next/server/app/api/ask/route_client-reference-manifest.js +1 -1
  47. package/_standalone/.next/server/app/api/ask-sessions/route_client-reference-manifest.js +1 -1
  48. package/_standalone/.next/server/app/api/auth/route_client-reference-manifest.js +1 -1
  49. package/_standalone/.next/server/app/api/backlinks/route.js.nft.json +1 -1
  50. package/_standalone/.next/server/app/api/backlinks/route_client-reference-manifest.js +1 -1
  51. package/_standalone/.next/server/app/api/bootstrap/route.js.nft.json +1 -1
  52. package/_standalone/.next/server/app/api/bootstrap/route_client-reference-manifest.js +1 -1
  53. package/_standalone/.next/server/app/api/changes/route.js.nft.json +1 -1
  54. package/_standalone/.next/server/app/api/changes/route_client-reference-manifest.js +1 -1
  55. package/_standalone/.next/server/app/api/channels/verify/route.js.nft.json +1 -1
  56. package/_standalone/.next/server/app/api/channels/verify/route_client-reference-manifest.js +1 -1
  57. package/_standalone/.next/server/app/api/connect/route_client-reference-manifest.js +1 -1
  58. package/_standalone/.next/server/app/api/embedding/route.js.nft.json +1 -1
  59. package/_standalone/.next/server/app/api/embedding/route_client-reference-manifest.js +1 -1
  60. package/_standalone/.next/server/app/api/export/route.js.nft.json +1 -1
  61. package/_standalone/.next/server/app/api/export/route_client-reference-manifest.js +1 -1
  62. package/_standalone/.next/server/app/api/extract-pdf/route.js.nft.json +1 -1
  63. package/_standalone/.next/server/app/api/extract-pdf/route_client-reference-manifest.js +1 -1
  64. package/_standalone/.next/server/app/api/file/import/route.js.nft.json +1 -1
  65. package/_standalone/.next/server/app/api/file/import/route_client-reference-manifest.js +1 -1
  66. package/_standalone/.next/server/app/api/file/raw/route.js.nft.json +1 -1
  67. package/_standalone/.next/server/app/api/file/raw/route_client-reference-manifest.js +1 -1
  68. package/_standalone/.next/server/app/api/file/route.js.nft.json +1 -1
  69. package/_standalone/.next/server/app/api/file/route_client-reference-manifest.js +1 -1
  70. package/_standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  71. package/_standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  72. package/_standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  73. package/_standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  74. package/_standalone/.next/server/app/api/graph/route.js.nft.json +1 -1
  75. package/_standalone/.next/server/app/api/graph/route_client-reference-manifest.js +1 -1
  76. package/_standalone/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  77. package/_standalone/.next/server/app/api/im/activity/route_client-reference-manifest.js +1 -1
  78. package/_standalone/.next/server/app/api/im/config/route.js +1 -1
  79. package/_standalone/.next/server/app/api/im/config/route_client-reference-manifest.js +1 -1
  80. package/_standalone/.next/server/app/api/im/status/route.js +1 -7
  81. package/_standalone/.next/server/app/api/im/status/route.js.nft.json +1 -1
  82. package/_standalone/.next/server/app/api/im/status/route_client-reference-manifest.js +1 -1
  83. package/_standalone/.next/server/app/api/im/test/route.js.nft.json +1 -1
  84. package/_standalone/.next/server/app/api/im/test/route_client-reference-manifest.js +1 -1
  85. package/_standalone/.next/server/app/api/im/webhook/feishu/route.js +1 -7
  86. package/_standalone/.next/server/app/api/im/webhook/feishu/route.js.nft.json +1 -1
  87. package/_standalone/.next/server/app/api/im/webhook/feishu/route_client-reference-manifest.js +1 -1
  88. package/_standalone/.next/server/app/api/im/webhook-status/route.js +1 -7
  89. package/_standalone/.next/server/app/api/im/webhook-status/route.js.nft.json +1 -1
  90. package/_standalone/.next/server/app/api/im/webhook-status/route_client-reference-manifest.js +1 -1
  91. package/_standalone/.next/server/app/api/inbox/clip/route.js.nft.json +1 -1
  92. package/_standalone/.next/server/app/api/inbox/clip/route_client-reference-manifest.js +1 -1
  93. package/_standalone/.next/server/app/api/inbox/route.js.nft.json +1 -1
  94. package/_standalone/.next/server/app/api/inbox/route_client-reference-manifest.js +1 -1
  95. package/_standalone/.next/server/app/api/init/route.js.nft.json +1 -1
  96. package/_standalone/.next/server/app/api/init/route_client-reference-manifest.js +1 -1
  97. package/_standalone/.next/server/app/api/lint/route.js.nft.json +1 -1
  98. package/_standalone/.next/server/app/api/lint/route_client-reference-manifest.js +1 -1
  99. package/_standalone/.next/server/app/api/mcp/agents/route.js.nft.json +1 -1
  100. package/_standalone/.next/server/app/api/mcp/agents/route_client-reference-manifest.js +1 -1
  101. package/_standalone/.next/server/app/api/mcp/direct-tools/route_client-reference-manifest.js +1 -1
  102. package/_standalone/.next/server/app/api/mcp/install/route_client-reference-manifest.js +1 -1
  103. package/_standalone/.next/server/app/api/mcp/install-skill/route_client-reference-manifest.js +1 -1
  104. package/_standalone/.next/server/app/api/mcp/restart/route_client-reference-manifest.js +1 -1
  105. package/_standalone/.next/server/app/api/mcp/status/route_client-reference-manifest.js +1 -1
  106. package/_standalone/.next/server/app/api/mcp/tools/route_client-reference-manifest.js +1 -1
  107. package/_standalone/.next/server/app/api/mcp/uninstall/route_client-reference-manifest.js +1 -1
  108. package/_standalone/.next/server/app/api/monitoring/route.js.nft.json +1 -1
  109. package/_standalone/.next/server/app/api/monitoring/route_client-reference-manifest.js +1 -1
  110. package/_standalone/.next/server/app/api/recent-files/route.js.nft.json +1 -1
  111. package/_standalone/.next/server/app/api/recent-files/route_client-reference-manifest.js +1 -1
  112. package/_standalone/.next/server/app/api/restart/route_client-reference-manifest.js +1 -1
  113. package/_standalone/.next/server/app/api/search/route.js.nft.json +1 -1
  114. package/_standalone/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
  115. package/_standalone/.next/server/app/api/settings/list-models/route_client-reference-manifest.js +1 -1
  116. package/_standalone/.next/server/app/api/settings/reset-token/route_client-reference-manifest.js +1 -1
  117. package/_standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  118. package/_standalone/.next/server/app/api/settings/route_client-reference-manifest.js +1 -1
  119. package/_standalone/.next/server/app/api/settings/test-key/route_client-reference-manifest.js +1 -1
  120. package/_standalone/.next/server/app/api/setup/check-path/route_client-reference-manifest.js +1 -1
  121. package/_standalone/.next/server/app/api/setup/check-port/route_client-reference-manifest.js +1 -1
  122. package/_standalone/.next/server/app/api/setup/generate-token/route_client-reference-manifest.js +1 -1
  123. package/_standalone/.next/server/app/api/setup/ls/route_client-reference-manifest.js +1 -1
  124. package/_standalone/.next/server/app/api/setup/route_client-reference-manifest.js +1 -1
  125. package/_standalone/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
  126. package/_standalone/.next/server/app/api/space-overview/route.js.nft.json +1 -1
  127. package/_standalone/.next/server/app/api/space-overview/route_client-reference-manifest.js +1 -1
  128. package/_standalone/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
  129. package/_standalone/.next/server/app/api/tree-version/route.js.nft.json +1 -1
  130. package/_standalone/.next/server/app/api/tree-version/route_client-reference-manifest.js +1 -1
  131. package/_standalone/.next/server/app/api/uninstall/route_client-reference-manifest.js +1 -1
  132. package/_standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  133. package/_standalone/.next/server/app/api/update-check/route_client-reference-manifest.js +1 -1
  134. package/_standalone/.next/server/app/api/update-status/route_client-reference-manifest.js +1 -1
  135. package/_standalone/.next/server/app/api/workflows/route.js.nft.json +1 -1
  136. package/_standalone/.next/server/app/api/workflows/route_client-reference-manifest.js +1 -1
  137. package/_standalone/.next/server/app/changelog/page.js +1 -1
  138. package/_standalone/.next/server/app/changelog/page.js.nft.json +1 -1
  139. package/_standalone/.next/server/app/changelog/page_client-reference-manifest.js +1 -1
  140. package/_standalone/.next/server/app/changes/page.js +1 -1
  141. package/_standalone/.next/server/app/changes/page.js.nft.json +1 -1
  142. package/_standalone/.next/server/app/changes/page_client-reference-manifest.js +1 -1
  143. package/_standalone/.next/server/app/echo/[segment]/page.js +1 -1
  144. package/_standalone/.next/server/app/echo/[segment]/page.js.nft.json +1 -1
  145. package/_standalone/.next/server/app/echo/[segment]/page_client-reference-manifest.js +1 -1
  146. package/_standalone/.next/server/app/echo/page.js +1 -1
  147. package/_standalone/.next/server/app/echo/page.js.nft.json +1 -1
  148. package/_standalone/.next/server/app/echo/page_client-reference-manifest.js +1 -1
  149. package/_standalone/.next/server/app/explore/page.js +1 -1
  150. package/_standalone/.next/server/app/explore/page.js.nft.json +1 -1
  151. package/_standalone/.next/server/app/explore/page_client-reference-manifest.js +1 -1
  152. package/_standalone/.next/server/app/help/page.js +1 -1
  153. package/_standalone/.next/server/app/help/page.js.nft.json +1 -1
  154. package/_standalone/.next/server/app/help/page_client-reference-manifest.js +1 -1
  155. package/_standalone/.next/server/app/inbox/history/page.js +1 -1
  156. package/_standalone/.next/server/app/inbox/history/page.js.nft.json +1 -1
  157. package/_standalone/.next/server/app/inbox/history/page_client-reference-manifest.js +1 -1
  158. package/_standalone/.next/server/app/login/page.js +1 -1
  159. package/_standalone/.next/server/app/login/page.js.nft.json +1 -1
  160. package/_standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
  161. package/_standalone/.next/server/app/page.js +1 -1
  162. package/_standalone/.next/server/app/page.js.nft.json +1 -1
  163. package/_standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  164. package/_standalone/.next/server/app/setup/page.js +2 -2
  165. package/_standalone/.next/server/app/setup/page.js.nft.json +1 -1
  166. package/_standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  167. package/_standalone/.next/server/app/trash/page.js +3 -3
  168. package/_standalone/.next/server/app/trash/page.js.nft.json +1 -1
  169. package/_standalone/.next/server/app/trash/page_client-reference-manifest.js +1 -1
  170. package/_standalone/.next/server/app/view/[...path]/page.js +3 -3
  171. package/_standalone/.next/server/app/view/[...path]/page.js.nft.json +1 -1
  172. package/_standalone/.next/server/app/view/[...path]/page_client-reference-manifest.js +1 -1
  173. package/_standalone/.next/server/app-paths-manifest.json +30 -30
  174. package/_standalone/.next/server/chunks/1076.js +1 -1
  175. package/_standalone/.next/server/chunks/{2449.js → 2424.js} +2 -2
  176. package/_standalone/.next/server/chunks/2792.js +1 -0
  177. package/_standalone/.next/server/chunks/2885.js +7 -0
  178. package/_standalone/.next/server/chunks/3800.js +1 -1
  179. package/_standalone/.next/server/chunks/5299.js +1 -1
  180. package/_standalone/.next/server/chunks/5464.js +1 -1
  181. package/_standalone/.next/server/chunks/6022.js +28 -28
  182. package/_standalone/.next/server/chunks/6539.js +1 -1
  183. package/_standalone/.next/server/chunks/8388.js +2 -2
  184. package/_standalone/.next/server/middleware-build-manifest.js +1 -1
  185. package/_standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  186. package/_standalone/.next/server/pages/500.html +2 -2
  187. package/_standalone/.next/server/server-reference-manifest.js +1 -1
  188. package/_standalone/.next/server/server-reference-manifest.json +1 -1
  189. package/_standalone/.next/static/chunks/{5331.c89084fd7f67887d.js → 4342.ccc18d7a45f204a4.js} +5 -5
  190. package/_standalone/.next/static/chunks/{4094.09364c01df411380.js → 4496.f77a677ac2034e48.js} +1 -1
  191. package/_standalone/.next/static/chunks/{5358-15618fc9b20018ee.js → 5358-15ceeb8c1cd9889d.js} +6 -6
  192. package/_standalone/.next/static/chunks/{6902-edc5c487c696bd0b.js → 6902-53a31f849968ddf8.js} +1 -1
  193. package/_standalone/.next/static/chunks/9207-456dc6941d557f33.js +1 -0
  194. package/_standalone/.next/static/chunks/app/{layout-fcbde5bee626d21a.js → layout-09f08f52e3cbfc47.js} +66 -66
  195. package/_standalone/.next/static/chunks/app/setup/{page-e40f3c5704c3c857.js → page-64d7fa097ef3f367.js} +1 -1
  196. package/_standalone/.next/static/chunks/app/trash/page-8e0ea3ba71702757.js +1 -0
  197. package/_standalone/.next/static/chunks/app/view/[...path]/page-04f43178679a8bfb.js +12 -0
  198. package/_standalone/.next/static/chunks/{webpack-dc486b68118d1328.js → webpack-ec9c6076012b75f3.js} +1 -1
  199. package/_standalone/.next/trace +72 -72
  200. package/_standalone/__tests__/agent/provider-presets.test.ts +32 -0
  201. package/_standalone/__tests__/api/im-config-feishu-conversation.test.ts +39 -0
  202. package/_standalone/__tests__/api/im-webhook-feishu.test.ts +21 -12
  203. package/_standalone/__tests__/api/pi-subagents.test.ts +81 -0
  204. package/_standalone/__tests__/ask/file-chip-variants.test.tsx +18 -0
  205. package/_standalone/__tests__/ask/message-list-agent-attribution.test.tsx +2 -0
  206. package/_standalone/__tests__/im/feishu-dispatcher.test.ts +151 -0
  207. package/_standalone/__tests__/im/feishu-webhook.test.ts +85 -57
  208. package/_standalone/__tests__/im/feishu-ws-client.test.ts +91 -0
  209. package/_standalone/__tests__/settings/custom-provider-form.test.ts +30 -0
  210. package/_standalone/components/ask/FileChip.tsx +1 -1
  211. package/_standalone/components/ask/MessageList.tsx +2 -2
  212. package/_standalone/components/settings/CustomProviderFields.tsx +1 -1
  213. package/_standalone/components/settings/CustomProvidersCard.tsx +3 -0
  214. package/_standalone/components/settings/useCustomProviderForm.ts +78 -24
  215. package/_standalone/components/shared/ProviderSelect.tsx +6 -5
  216. package/_standalone/next.config.ts +28 -4
  217. package/_standalone/package-lock.json +20 -2
  218. package/_standalone/package.json +3 -1
  219. package/_standalone/scripts/feishu-long-connection.ts +32 -0
  220. package/_standalone/server.js +1 -1
  221. package/_standalone/tsconfig.tsbuildinfo +1 -1
  222. package/app/__tests__/agent/provider-presets.test.ts +32 -0
  223. package/app/__tests__/api/im-config-feishu-conversation.test.ts +39 -0
  224. package/app/__tests__/api/im-webhook-feishu.test.ts +21 -12
  225. package/app/__tests__/api/pi-subagents.test.ts +81 -0
  226. package/app/__tests__/ask/file-chip-variants.test.tsx +18 -0
  227. package/app/__tests__/ask/message-list-agent-attribution.test.tsx +2 -0
  228. package/app/__tests__/im/feishu-dispatcher.test.ts +151 -0
  229. package/app/__tests__/im/feishu-webhook.test.ts +85 -57
  230. package/app/__tests__/im/feishu-ws-client.test.ts +91 -0
  231. package/app/__tests__/settings/custom-provider-form.test.ts +30 -0
  232. package/app/app/api/ask/route.ts +2 -0
  233. package/app/app/api/im/config/route.ts +2 -0
  234. package/app/app/api/im/webhook/feishu/route.ts +4 -2
  235. package/app/components/ask/FileChip.tsx +1 -1
  236. package/app/components/ask/MessageList.tsx +2 -2
  237. package/app/components/settings/CustomProviderFields.tsx +1 -1
  238. package/app/components/settings/CustomProvidersCard.tsx +3 -0
  239. package/app/components/settings/useCustomProviderForm.ts +78 -24
  240. package/app/components/shared/ProviderSelect.tsx +6 -5
  241. package/app/lib/agent/providers.ts +44 -6
  242. package/app/lib/im/feishu-dispatcher.ts +111 -0
  243. package/app/lib/im/feishu-ws-client.ts +72 -0
  244. package/app/lib/im/types.ts +14 -0
  245. package/app/lib/im/webhook/feishu.ts +44 -55
  246. package/app/next.config.ts +28 -4
  247. package/app/package.json +3 -1
  248. package/app/scripts/feishu-long-connection.ts +32 -0
  249. package/bin/cli.js +3 -1
  250. package/bin/commands/feishu-ws.js +39 -0
  251. package/package.json +1 -1
  252. package/scripts/build-runtime-archive.sh +36 -2
  253. package/_standalone/.next/static/chunks/9207-3b19c55a3c974a09.js +0 -1
  254. package/_standalone/.next/static/chunks/app/trash/page-e623ff0ab35de002.js +0 -1
  255. package/_standalone/.next/static/chunks/app/view/[...path]/page-49c4eff6ffdb5168.js +0 -12
  256. /package/_standalone/.next/static/{Dn8EHqUedSzanCfrM8WWS → dZDCx13MSM8QVQk2QNRs8}/_buildManifest.js +0 -0
  257. /package/_standalone/.next/static/{Dn8EHqUedSzanCfrM8WWS → dZDCx13MSM8QVQk2QNRs8}/_ssgManifest.js +0 -0
@@ -0,0 +1,91 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import type { FeishuConfig } from '@/lib/im/types';
3
+
4
+ const { startMock, closeMock, wsClientCtor, registerMock } = vi.hoisted(() => {
5
+ const startMock = vi.fn().mockResolvedValue(undefined);
6
+ const closeMock = vi.fn();
7
+ const wsClientCtor = vi.fn();
8
+ const registerMock = vi.fn(function () { return this; });
9
+ return { startMock, closeMock, wsClientCtor, registerMock };
10
+ });
11
+
12
+ vi.mock('@larksuiteoapi/node-sdk', () => ({
13
+ LoggerLevel: { info: 'info' },
14
+ EventDispatcher: class MockEventDispatcher {
15
+ register(handles: unknown) {
16
+ registerMock(handles);
17
+ return this;
18
+ }
19
+ },
20
+ WSClient: class MockWSClient {
21
+ constructor(params: unknown) {
22
+ wsClientCtor(params);
23
+ }
24
+
25
+ async start(params: unknown) {
26
+ return await startMock(params);
27
+ }
28
+
29
+ close(params?: unknown) {
30
+ closeMock(params);
31
+ }
32
+ },
33
+ }));
34
+
35
+ describe('Feishu WS client manager', () => {
36
+ beforeEach(async () => {
37
+ vi.clearAllMocks();
38
+ const mod = await import('@/lib/im/feishu-ws-client');
39
+ mod.__resetFeishuWSClientForTests();
40
+ });
41
+
42
+ it('starts a WS client once for long connection', async () => {
43
+ const mod = await import('@/lib/im/feishu-ws-client');
44
+ const config: FeishuConfig = {
45
+ app_id: 'cli_xxx',
46
+ app_secret: 'secret',
47
+ conversation: { enabled: true, transport: 'long_connection' },
48
+ };
49
+
50
+ await mod.startFeishuWSClient(config);
51
+ await mod.startFeishuWSClient(config);
52
+
53
+ expect(wsClientCtor).toHaveBeenCalledTimes(1);
54
+ expect(wsClientCtor).toHaveBeenCalledWith(expect.objectContaining({
55
+ appId: 'cli_xxx',
56
+ appSecret: 'secret',
57
+ autoReconnect: true,
58
+ }));
59
+ expect(startMock).toHaveBeenCalledTimes(1);
60
+ expect(registerMock).toHaveBeenCalledWith(expect.objectContaining({
61
+ 'im.message.receive_v1': expect.any(Function),
62
+ }));
63
+ expect(mod.getFeishuWSClientStatus().running).toBe(true);
64
+ });
65
+
66
+ it('stops the running WS client', async () => {
67
+ const mod = await import('@/lib/im/feishu-ws-client');
68
+ await mod.startFeishuWSClient({
69
+ app_id: 'cli_xxx',
70
+ app_secret: 'secret',
71
+ conversation: { enabled: true, transport: 'long_connection' },
72
+ });
73
+
74
+ mod.stopFeishuWSClient();
75
+
76
+ expect(closeMock).toHaveBeenCalled();
77
+ expect(mod.getFeishuWSClientStatus().running).toBe(false);
78
+ });
79
+
80
+ it('reports configuration errors before trying to connect', async () => {
81
+ const mod = await import('@/lib/im/feishu-ws-client');
82
+
83
+ await expect(mod.startFeishuWSClient({
84
+ app_id: '',
85
+ app_secret: 'secret',
86
+ conversation: { enabled: true, transport: 'long_connection' },
87
+ })).rejects.toThrow('Feishu App ID and App Secret are required');
88
+
89
+ expect(wsClientCtor).not.toHaveBeenCalled();
90
+ });
91
+ });
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildDefaultProviderName } from '@/components/settings/useCustomProviderForm';
3
+
4
+
5
+ describe('buildDefaultProviderName', () => {
6
+ it('uses the protocol display name by default', () => {
7
+ expect(buildDefaultProviderName('openai', [], undefined, 'en')).toBe('OpenAI');
8
+ });
9
+
10
+ it('appends numeric suffixes when the default name already exists', () => {
11
+ expect(buildDefaultProviderName('openai', ['OpenAI'], undefined, 'en')).toBe('OpenAI 2');
12
+ expect(buildDefaultProviderName('openai', ['OpenAI', 'OpenAI 2'], undefined, 'en')).toBe('OpenAI 3');
13
+ });
14
+
15
+ it('ignores the currently edited provider name when computing duplicates', () => {
16
+ expect(buildDefaultProviderName('openai', ['OpenAI'], 'OpenAI', 'en')).toBe('OpenAI');
17
+ });
18
+
19
+ it('uses localized protocol name in Chinese', () => {
20
+ expect(buildDefaultProviderName('minimax-cn', [], undefined, 'zh')).toBe('MiniMax (国内版)');
21
+ });
22
+
23
+ it('uses LM Studio as the default generated name', () => {
24
+ expect(buildDefaultProviderName('lm-studio', [], undefined, 'en')).toBe('LM Studio');
25
+ });
26
+
27
+ it('uses vLLM as the default generated name', () => {
28
+ expect(buildDefaultProviderName('vllm', [], undefined, 'en')).toBe('vLLM');
29
+ });
30
+ });
@@ -648,6 +648,8 @@ export async function POST(req: NextRequest) {
648
648
  path.join(projectRoot, 'app', 'node_modules', 'pi-mcp-adapter', 'index.ts'),
649
649
  // IM extension: 8-platform IM integration (Telegram, Feishu, Discord, Slack, etc.)
650
650
  path.join(projectRoot, 'app', 'lib', 'im', 'index.ts'),
651
+ // pi-subagents: task delegation to subagents with chains, parallel, async support
652
+ path.join(projectRoot, 'app', 'node_modules', 'pi-subagents', 'index.ts'),
651
653
  ],
652
654
  });
653
655
  await resourceLoader.reload();
@@ -35,6 +35,7 @@ export async function PUT(req: NextRequest) {
35
35
  credentials?: Record<string, string>;
36
36
  conversation?: {
37
37
  enabled?: boolean;
38
+ transport?: 'webhook' | 'long_connection';
38
39
  encrypt_key?: string;
39
40
  verification_token?: string;
40
41
  public_base_url?: string;
@@ -69,6 +70,7 @@ export async function PUT(req: NextRequest) {
69
70
  merged.conversation = {
70
71
  ...(merged.conversation ?? {}),
71
72
  enabled: Boolean(conversation.enabled),
73
+ transport: conversation.transport ?? merged.conversation?.transport ?? 'webhook',
72
74
  encrypt_key: conversation.encrypt_key ?? merged.conversation?.encrypt_key,
73
75
  verification_token: conversation.verification_token ?? merged.conversation?.verification_token,
74
76
  public_base_url: conversation.public_base_url ?? merged.conversation?.public_base_url,
@@ -1,6 +1,6 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { getPlatformConfig } from '@/lib/im/config';
3
- import { handleFeishuWebhook } from '@/lib/im/webhook/feishu';
3
+ import { dispatchFeishuWebhook } from '@/lib/im/feishu-dispatcher';
4
4
 
5
5
  export async function POST(req: NextRequest) {
6
6
  try {
@@ -10,9 +10,11 @@ export async function POST(req: NextRequest) {
10
10
  }
11
11
 
12
12
  const body = await req.json();
13
- const result = await handleFeishuWebhook({
13
+ const headers = Object.fromEntries(req.headers.entries());
14
+ const result = await dispatchFeishuWebhook({
14
15
  config,
15
16
  body,
17
+ headers,
16
18
  });
17
19
 
18
20
  return NextResponse.json(result.body, { status: result.status });
@@ -29,7 +29,7 @@ const VARIANT_ICON = {
29
29
  upload: { icon: Paperclip, cls: 'text-muted-foreground' },
30
30
  image: { icon: ImageIcon, cls: 'text-muted-foreground' },
31
31
  skill: { icon: Zap, cls: 'text-[var(--amber)]' },
32
- agent: { icon: Bot, cls: 'text-muted-foreground' },
32
+ agent: { icon: Bot, cls: 'text-[var(--amber)]' },
33
33
  } as const;
34
34
 
35
35
  const VARIANT_STYLE = {
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useRef, useEffect, memo, useState, useCallback } from 'react';
4
- import { Sparkles, Loader2, AlertCircle, Wrench, WifiOff, Zap, Copy, Check, ArrowDown, FolderInput, Search, PenLine, Lightbulb, FileText, Paperclip } from 'lucide-react';
4
+ import { Sparkles, Loader2, AlertCircle, Wrench, WifiOff, Zap, Copy, Check, ArrowDown, FolderInput, Search, PenLine, Lightbulb, FileText, Paperclip, Bot } from 'lucide-react';
5
5
  import ReactMarkdown from 'react-markdown';
6
6
  import remarkGfm from 'remark-gfm';
7
7
  import type { Message, ImagePart } from '@/lib/types';
@@ -111,7 +111,7 @@ function AssistantAgentBadge({ agentName }: { agentName?: string }) {
111
111
  if (!agentName) return null;
112
112
  return (
113
113
  <div className="mb-2 inline-flex items-center gap-1 rounded-full border border-[var(--amber)]/15 bg-[var(--amber)]/8 px-2 py-0.5 text-[10px] font-medium tracking-wide text-[var(--amber)]">
114
- <Sparkles size={10} className="shrink-0" />
114
+ <Bot size={10} className="shrink-0" />
115
115
  <span>{agentName}</span>
116
116
  </div>
117
117
  );
@@ -25,7 +25,7 @@ export default function CustomProviderFields({
25
25
 
26
26
  const nameLabel = locale === 'zh' ? '名称' : 'Name';
27
27
  const protocolLabel = locale === 'zh' ? '协议' : 'Protocol';
28
- const namePlaceholder = locale === 'zh' ? '输入名称' : 'Enter name';
28
+ const namePlaceholder = locale === 'zh' ? '可选,默认使用协议名称' : 'Optional, defaults to protocol name';
29
29
 
30
30
  const nameHint = form.isDuplicateName
31
31
  ? (locale === 'zh' ? '名称已存在' : 'Name already exists')
@@ -119,6 +119,9 @@ export default function CustomProvidersCard({
119
119
  onClose={() => setIsModalOpen(false)}
120
120
  onSave={handleSaveProvider}
121
121
  initialProvider={editingProvider ?? undefined}
122
+ existingNames={providers
123
+ .filter((p) => p.id !== editingId)
124
+ .map((p) => p.name)}
122
125
  t={t}
123
126
  />
124
127
 
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
- import { useState, useCallback } from 'react';
4
- import { type ProviderId } from '@/lib/agent/providers';
3
+ import { useState, useCallback, useMemo, useEffect } from 'react';
4
+ import { type ProviderId, PROVIDER_PRESETS } from '@/lib/agent/providers';
5
5
  import { type Provider, generateProviderId } from '@/lib/custom-endpoints';
6
6
 
7
7
  export type TestState = 'idle' | 'testing' | 'ok' | 'error';
@@ -32,6 +32,29 @@ export interface CustomProviderFormState {
32
32
  handleSave: () => void;
33
33
  }
34
34
 
35
+ export function buildDefaultProviderName(
36
+ protocol: ProviderId,
37
+ existingNames: string[] = [],
38
+ excludeName?: string,
39
+ locale?: string,
40
+ ): string {
41
+ const preset = PROVIDER_PRESETS[protocol];
42
+ const baseName = locale === 'zh' ? preset.nameZh : preset.name;
43
+ const normalizedExisting = new Set(
44
+ existingNames
45
+ .filter((name) => name && name !== excludeName)
46
+ .map((name) => name.trim().toLowerCase()),
47
+ );
48
+
49
+ if (!normalizedExisting.has(baseName.trim().toLowerCase())) return baseName;
50
+
51
+ let index = 2;
52
+ while (normalizedExisting.has(`${baseName} ${index}`.toLowerCase())) {
53
+ index++;
54
+ }
55
+ return `${baseName} ${index}`;
56
+ }
57
+
35
58
  /**
36
59
  * Shared form state + test/save logic for provider forms.
37
60
  * Used by both the inline form (AiTab) and the modal (ProviderModal).
@@ -47,27 +70,64 @@ export function useCustomProviderForm({
47
70
  locale: string;
48
71
  existingNames?: string[];
49
72
  }): CustomProviderFormState {
50
- const [name, setName] = useState(initial?.name ?? '');
51
- const [protocol, setProtocol] = useState<ProviderId>(initial?.protocol ?? 'openai');
73
+ const initialName = useMemo(
74
+ () => initial?.name ?? buildDefaultProviderName(initial?.protocol ?? 'openai', existingNames, initial?.name, locale),
75
+ [initial?.name, initial?.protocol, existingNames, locale],
76
+ );
77
+
78
+ const [name, setNameState] = useState(initialName);
79
+ const [protocol, setProtocolState] = useState<ProviderId>(initial?.protocol ?? 'openai');
52
80
  const [apiKey, setApiKey] = useState(initial?.apiKey ?? '');
53
81
  const [model, setModel] = useState(initial?.model ?? '');
54
82
  const [baseUrl, setBaseUrl] = useState(initial?.baseUrl ?? '');
55
83
  const [testResult, setTestResult] = useState<TestResult>({ state: 'idle' });
84
+ const [nameTouched, setNameTouched] = useState(!!initial?.name);
85
+
86
+ const autoName = useMemo(
87
+ () => buildDefaultProviderName(protocol, existingNames, initial?.name, locale),
88
+ [protocol, existingNames, initial?.name, locale],
89
+ );
90
+
91
+ useEffect(() => {
92
+ if (!nameTouched) {
93
+ setNameState(autoName);
94
+ }
95
+ }, [autoName, nameTouched]);
96
+
97
+ const setName = useCallback((value: string) => {
98
+ setNameTouched(true);
99
+ setNameState(value);
100
+ }, []);
101
+
102
+ const setProtocol = useCallback((value: ProviderId) => {
103
+ setProtocolState(value);
104
+ setTestResult({ state: 'idle' });
105
+ }, []);
56
106
 
57
107
  // Check for duplicate name (exclude the provider being edited)
58
108
  const trimmedName = name.trim();
59
- const isDuplicateName = !!(trimmedName && existingNames?.some(
60
- n => n.toLowerCase() === trimmedName.toLowerCase(),
109
+ const effectiveName = trimmedName || autoName;
110
+ const isDuplicateName = !!(effectiveName && existingNames?.some(
111
+ n => n !== initial?.name && n.toLowerCase() === effectiveName.toLowerCase(),
61
112
  ));
62
113
 
63
- const canSave = !!(trimmedName && baseUrl.trim() && model.trim() && !isDuplicateName);
114
+ const canSave = !!(baseUrl.trim() && model.trim() && !isDuplicateName);
115
+
116
+ const requiredFieldsMessage = locale === 'zh'
117
+ ? '接口地址和模型为必填'
118
+ : 'Base URL and model are required';
119
+
120
+ const duplicateNameMessage = locale === 'zh'
121
+ ? '名称已存在,请使用其他名称'
122
+ : 'Name already exists, please use a different name';
64
123
 
65
124
  const handleTest = useCallback(async () => {
66
- if (!canSave) {
67
- setTestResult({
68
- state: 'error',
69
- error: locale === 'zh' ? '名称、接口地址和模型为必填' : 'Name, base URL, and model are required',
70
- });
125
+ if (isDuplicateName) {
126
+ setTestResult({ state: 'error', error: duplicateNameMessage });
127
+ return;
128
+ }
129
+ if (!baseUrl.trim() || !model.trim()) {
130
+ setTestResult({ state: 'error', error: requiredFieldsMessage });
71
131
  return;
72
132
  }
73
133
  setTestResult({ state: 'testing' });
@@ -90,32 +150,26 @@ export function useCustomProviderForm({
90
150
  } catch {
91
151
  setTestResult({ state: 'error', code: 'network_error', error: 'Network error' });
92
152
  }
93
- }, [canSave, apiKey, model, baseUrl, protocol, locale, initial?.id]);
153
+ }, [isDuplicateName, duplicateNameMessage, baseUrl, model, initial?.id, apiKey, protocol, requiredFieldsMessage]);
94
154
 
95
155
  const handleSave = useCallback(() => {
96
156
  if (isDuplicateName) {
97
- setTestResult({
98
- state: 'error',
99
- error: locale === 'zh' ? '名称已存在,请使用其他名称' : 'Name already exists, please use a different name',
100
- });
157
+ setTestResult({ state: 'error', error: duplicateNameMessage });
101
158
  return;
102
159
  }
103
- if (!canSave) {
104
- setTestResult({
105
- state: 'error',
106
- error: locale === 'zh' ? '名称、接口地址和模型为必填' : 'Name, base URL, and model are required',
107
- });
160
+ if (!baseUrl.trim() || !model.trim()) {
161
+ setTestResult({ state: 'error', error: requiredFieldsMessage });
108
162
  return;
109
163
  }
110
164
  onSave({
111
165
  id: initial?.id || generateProviderId(),
112
- name: name.trim(),
166
+ name: effectiveName,
113
167
  protocol,
114
168
  apiKey,
115
169
  model: model.trim(),
116
170
  baseUrl: baseUrl.trim(),
117
171
  });
118
- }, [canSave, isDuplicateName, name, protocol, apiKey, model, baseUrl, initial?.id, onSave, locale]);
172
+ }, [isDuplicateName, duplicateNameMessage, baseUrl, model, requiredFieldsMessage, onSave, initial?.id, effectiveName, protocol, apiKey]);
119
173
 
120
174
  return {
121
175
  name, setName,
@@ -39,7 +39,8 @@ export default function ProviderSelect({
39
39
  : [];
40
40
 
41
41
  // Add panel shows ALL providers as protocol templates (can add multiple of the same type)
42
- const { primary: primaryItems, more: moreItems } = groups;
42
+ const { primary: primaryItems, local: localItems, more: moreItems } = groups;
43
+ const secondaryItems = [...localItems, ...moreItems];
43
44
 
44
45
  /* ── Compact tab button (for legacy builtin-only mode) ── */
45
46
  const renderCompactTab = (id: ProviderId) => {
@@ -175,7 +176,7 @@ export default function ProviderSelect({
175
176
  </div>
176
177
 
177
178
  {/* More toggle */}
178
- {moreItems.length > 0 && (
179
+ {secondaryItems.length > 0 && (
179
180
  <>
180
181
  <button
181
182
  type="button"
@@ -186,13 +187,13 @@ export default function ProviderSelect({
186
187
  {showMore
187
188
  ? (locale === 'zh' ? '收起' : 'Show less')
188
189
  : (locale === 'zh'
189
- ? `更多 (${moreItems.length})`
190
- : `More (${moreItems.length})`)}
190
+ ? `更多 (${secondaryItems.length})`
191
+ : `More (${secondaryItems.length})`)}
191
192
  </button>
192
193
 
193
194
  {showMore && (
194
195
  <div className={compact ? 'flex flex-wrap gap-2' : 'grid grid-cols-1 gap-2'}>
195
- {moreItems.map(id => compact ? renderCompactTab(id) : renderCard(id))}
196
+ {secondaryItems.map(id => compact ? renderCompactTab(id) : renderCard(id))}
196
197
  </div>
197
198
  )}
198
199
  </>
@@ -18,7 +18,7 @@ export type ProviderId =
18
18
  | 'xai' | 'openrouter' | 'mistral' | 'deepseek'
19
19
  | 'zai' | 'zai-cn' | 'kimi-coding'
20
20
  | 'cerebras' | 'minimax' | 'minimax-cn' | 'huggingface'
21
- | 'ollama';
21
+ | 'ollama' | 'lm-studio' | 'vllm';
22
22
 
23
23
  /**
24
24
  * UI/UX metadata for each provider.
@@ -44,7 +44,7 @@ export interface ProviderPreset {
44
44
  supportsThinking: boolean;
45
45
  supportsListModels: boolean;
46
46
  signupUrl?: string;
47
- category: 'primary' | 'more';
47
+ category: 'primary' | 'local' | 'more';
48
48
  }
49
49
 
50
50
  export const PROVIDER_PRESETS: Record<ProviderId, ProviderPreset> = {
@@ -242,7 +242,41 @@ export const PROVIDER_PRESETS: Record<ProviderId, ProviderPreset> = {
242
242
  supportsThinking: false,
243
243
  supportsListModels: true,
244
244
  signupUrl: 'https://ollama.com/download',
245
- category: 'more',
245
+ category: 'local',
246
+ },
247
+ 'lm-studio': {
248
+ id: 'lm-studio',
249
+ name: 'LM Studio',
250
+ nameZh: 'LM Studio (本地)',
251
+ shortLabel: 'LM Studio',
252
+ description: 'Local OpenAI-compatible server',
253
+ descriptionZh: '本地 OpenAI 兼容服务',
254
+ defaultModel: 'local-model',
255
+ piProviderOverride: 'openai' as KnownProvider,
256
+ fixedBaseUrl: 'http://localhost:1234/v1',
257
+ apiKeyFallback: 'lm-studio',
258
+ supportsBaseUrl: true,
259
+ supportsThinking: false,
260
+ supportsListModels: true,
261
+ signupUrl: 'https://lmstudio.ai/',
262
+ category: 'local',
263
+ },
264
+ vllm: {
265
+ id: 'vllm',
266
+ name: 'vLLM',
267
+ nameZh: 'vLLM (本地)',
268
+ shortLabel: 'vLLM',
269
+ description: 'Local OpenAI-compatible server',
270
+ descriptionZh: '本地 OpenAI 兼容服务',
271
+ defaultModel: 'local-model',
272
+ piProviderOverride: 'openai' as KnownProvider,
273
+ fixedBaseUrl: 'http://localhost:8000/v1',
274
+ apiKeyFallback: 'vllm',
275
+ supportsBaseUrl: true,
276
+ supportsThinking: false,
277
+ supportsListModels: true,
278
+ signupUrl: 'https://docs.vllm.ai/',
279
+ category: 'local',
246
280
  },
247
281
  };
248
282
 
@@ -256,15 +290,19 @@ export function getPreset(id: ProviderId): ProviderPreset {
256
290
  return PROVIDER_PRESETS[id] ?? PROVIDER_PRESETS.anthropic;
257
291
  }
258
292
 
259
- export function groupedProviders(): { primary: ProviderId[]; more: ProviderId[] } {
293
+ export function groupedProviders(): { primary: ProviderId[]; local: ProviderId[]; more: ProviderId[] } {
260
294
  const primary: ProviderId[] = [];
295
+ const local: ProviderId[] = [];
261
296
  const more: ProviderId[] = [];
262
297
  for (const id of ALL_PROVIDER_IDS) {
263
- if (PROVIDER_PRESETS[id].category === 'primary') primary.push(id);
298
+ const category = PROVIDER_PRESETS[id].category;
299
+ if (category === 'primary') primary.push(id);
300
+ else if (category === 'local') local.push(id);
264
301
  else more.push(id);
265
302
  }
303
+ local.sort((a, b) => PROVIDER_PRESETS[a].name.localeCompare(PROVIDER_PRESETS[b].name));
266
304
  more.sort((a, b) => PROVIDER_PRESETS[a].name.localeCompare(PROVIDER_PRESETS[b].name));
267
- return { primary, more };
305
+ return { primary, local, more };
268
306
  }
269
307
 
270
308
  // ---------------------------------------------------------------------------
@@ -0,0 +1,111 @@
1
+ import type { FeishuConfig, FeishuWebhookDispatchResult } from './types';
2
+ import { buildFeishuWebhookStatus, handleFeishuMessageReceiveEvent } from './webhook/feishu';
3
+
4
+ type FeishuHeaders = Record<string, string>;
5
+ type FeishuBody = Record<string, unknown>;
6
+ type FeishuDispatcher = {
7
+ encryptKey?: string;
8
+ register(handles: Record<string, (data: unknown) => unknown>): FeishuDispatcher;
9
+ invoke(data: unknown, params?: { needCheck?: boolean }): Promise<unknown>;
10
+ };
11
+
12
+ type LarkSdkModule = typeof import('@larksuiteoapi/node-sdk');
13
+
14
+ let cachedSdk: LarkSdkModule | null = null;
15
+ let cachedDispatcher: { key: string; dispatcher: FeishuDispatcher } | null = null;
16
+
17
+ function buildDispatcherKey(config: FeishuConfig): string {
18
+ return JSON.stringify({
19
+ encryptKey: config.conversation?.encrypt_key ?? '',
20
+ verificationToken: config.conversation?.verification_token ?? '',
21
+ });
22
+ }
23
+
24
+ function buildPayload(body: FeishuBody, headers: FeishuHeaders): FeishuBody {
25
+ return Object.assign(Object.create({ headers }), body);
26
+ }
27
+
28
+ function normalizeDispatcherBody(result: unknown): Record<string, unknown> {
29
+ if (result && typeof result === 'object' && !Array.isArray(result)) {
30
+ return result as Record<string, unknown>;
31
+ }
32
+ if (result == null) {
33
+ return { ok: true };
34
+ }
35
+ return { ok: false, error: `Unexpected Feishu dispatcher result: ${String(result)}` };
36
+ }
37
+
38
+ async function getLarkSdk(): Promise<LarkSdkModule> {
39
+ if (cachedSdk) return cachedSdk;
40
+ cachedSdk = await import('@larksuiteoapi/node-sdk');
41
+ return cachedSdk;
42
+ }
43
+
44
+ async function getDispatcher(config: FeishuConfig): Promise<FeishuDispatcher> {
45
+ const key = buildDispatcherKey(config);
46
+ if (cachedDispatcher?.key === key) {
47
+ return cachedDispatcher.dispatcher;
48
+ }
49
+
50
+ const lark = await getLarkSdk();
51
+ const dispatcher = new lark.EventDispatcher({
52
+ encryptKey: config.conversation?.encrypt_key,
53
+ verificationToken: config.conversation?.verification_token,
54
+ }).register({
55
+ 'im.message.receive_v1': (event: unknown) => handleFeishuMessageReceiveEvent(event as import('./types').FeishuSdkMessageEvent),
56
+ }) as FeishuDispatcher;
57
+
58
+ cachedDispatcher = { key, dispatcher };
59
+ return dispatcher;
60
+ }
61
+
62
+ function shouldHandleChallenge(body: FeishuBody): boolean {
63
+ return typeof body.challenge === 'string'
64
+ || body.type === 'url_verification'
65
+ || typeof body.encrypt === 'string';
66
+ }
67
+
68
+ export async function dispatchFeishuWebhook(params: {
69
+ config: FeishuConfig;
70
+ body: FeishuBody;
71
+ headers: FeishuHeaders;
72
+ }): Promise<FeishuWebhookDispatchResult> {
73
+ const status = buildFeishuWebhookStatus(params.config);
74
+ if (status.state !== 'ready') {
75
+ return {
76
+ status: 202,
77
+ body: { ok: false, ignored: true, reason: status.lastError ?? 'Webhook is not ready' },
78
+ };
79
+ }
80
+
81
+ const payload = buildPayload(params.body, params.headers);
82
+ if (shouldHandleChallenge(params.body)) {
83
+ const lark = await getLarkSdk();
84
+ const { isChallenge, challenge } = lark.generateChallenge(payload, {
85
+ encryptKey: params.config.conversation?.encrypt_key ?? '',
86
+ });
87
+
88
+ if (isChallenge) {
89
+ return {
90
+ status: 200,
91
+ body: challenge,
92
+ };
93
+ }
94
+ }
95
+
96
+ const dispatcher = await getDispatcher(params.config);
97
+ const result = await dispatcher.invoke(payload);
98
+
99
+ if (typeof result === 'undefined') {
100
+ return {
101
+ status: 401,
102
+ body: { ok: false, error: 'Invalid Feishu webhook signature or payload.' },
103
+ };
104
+ }
105
+
106
+ const normalized = normalizeDispatcherBody(result);
107
+ return {
108
+ status: normalized.ok === false ? 500 : 202,
109
+ body: normalized,
110
+ };
111
+ }
@@ -0,0 +1,72 @@
1
+ import * as Lark from '@larksuiteoapi/node-sdk';
2
+ import type { FeishuConfig, FeishuSdkMessageEvent } from './types';
3
+ import { handleFeishuMessageReceiveEvent } from './webhook/feishu';
4
+
5
+ type FeishuWSRuntime = {
6
+ client: Lark.WSClient;
7
+ startedAt: string;
8
+ };
9
+
10
+ let runtime: FeishuWSRuntime | null = null;
11
+ let lastError: string | undefined;
12
+
13
+ function assertFeishuWSConfig(config: FeishuConfig): void {
14
+ if (!config.app_id?.trim() || !config.app_secret?.trim()) {
15
+ throw new Error('Feishu App ID and App Secret are required for long connection mode');
16
+ }
17
+ }
18
+
19
+ function createDispatcher(): Lark.EventDispatcher {
20
+ return new Lark.EventDispatcher({}).register({
21
+ 'im.message.receive_v1': (event: unknown) => handleFeishuMessageReceiveEvent(event as FeishuSdkMessageEvent),
22
+ });
23
+ }
24
+
25
+ export async function startFeishuWSClient(config: FeishuConfig): Promise<void> {
26
+ if (runtime) return;
27
+
28
+ assertFeishuWSConfig(config);
29
+ lastError = undefined;
30
+
31
+ const client = new Lark.WSClient({
32
+ appId: config.app_id,
33
+ appSecret: config.app_secret,
34
+ autoReconnect: true,
35
+ loggerLevel: Lark.LoggerLevel.info,
36
+ });
37
+
38
+ try {
39
+ await client.start({
40
+ eventDispatcher: createDispatcher(),
41
+ });
42
+ runtime = {
43
+ client,
44
+ startedAt: new Date().toISOString(),
45
+ };
46
+ } catch (error) {
47
+ lastError = error instanceof Error ? error.message : String(error);
48
+ throw error;
49
+ }
50
+ }
51
+
52
+ export function stopFeishuWSClient(): void {
53
+ runtime?.client.close();
54
+ runtime = null;
55
+ }
56
+
57
+ export function getFeishuWSClientStatus(): {
58
+ running: boolean;
59
+ startedAt?: string;
60
+ lastError?: string;
61
+ } {
62
+ return {
63
+ running: runtime !== null,
64
+ startedAt: runtime?.startedAt,
65
+ lastError,
66
+ };
67
+ }
68
+
69
+ export function __resetFeishuWSClientForTests(): void {
70
+ runtime = null;
71
+ lastError = undefined;
72
+ }