@swarmclawai/swarmclaw 1.2.3 → 1.2.5

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 (273) hide show
  1. package/README.md +20 -0
  2. package/bin/daemon-cmd.js +169 -0
  3. package/bin/server-cmd.js +3 -0
  4. package/bin/swarmclaw.js +11 -0
  5. package/package.json +17 -16
  6. package/src/app/api/agents/[id]/clone/route.ts +3 -32
  7. package/src/app/api/agents/[id]/route.ts +6 -158
  8. package/src/app/api/agents/[id]/status/route.ts +2 -3
  9. package/src/app/api/agents/[id]/thread/route.ts +4 -17
  10. package/src/app/api/agents/bulk/route.ts +5 -47
  11. package/src/app/api/agents/route.ts +5 -119
  12. package/src/app/api/agents/trash/route.ts +13 -24
  13. package/src/app/api/auth/route.ts +3 -9
  14. package/src/app/api/autonomy/estop/route.ts +5 -5
  15. package/src/app/api/chatrooms/[id]/chat/route.ts +11 -5
  16. package/src/app/api/chatrooms/[id]/route.ts +23 -2
  17. package/src/app/api/chatrooms/route.ts +13 -2
  18. package/src/app/api/chats/[id]/clear/route.ts +2 -13
  19. package/src/app/api/chats/[id]/deploy/route.ts +2 -3
  20. package/src/app/api/chats/[id]/edit-resend/route.ts +7 -13
  21. package/src/app/api/chats/[id]/mailbox/route.ts +6 -8
  22. package/src/app/api/chats/[id]/queue/route.ts +17 -64
  23. package/src/app/api/chats/[id]/retry/route.ts +4 -22
  24. package/src/app/api/chats/[id]/route.ts +10 -138
  25. package/src/app/api/chats/heartbeat/route.ts +2 -1
  26. package/src/app/api/chats/migrate-messages/route.ts +7 -0
  27. package/src/app/api/chats/route.ts +13 -134
  28. package/src/app/api/connectors/[id]/access/route.ts +12 -229
  29. package/src/app/api/connectors/[id]/doctor/route.ts +1 -1
  30. package/src/app/api/connectors/[id]/health/route.ts +12 -39
  31. package/src/app/api/connectors/[id]/route.ts +14 -122
  32. package/src/app/api/connectors/[id]/webhook/route.ts +1 -1
  33. package/src/app/api/connectors/doctor/route.ts +1 -1
  34. package/src/app/api/connectors/route.ts +12 -70
  35. package/src/app/api/credentials/[id]/route.ts +2 -4
  36. package/src/app/api/credentials/route.ts +10 -19
  37. package/src/app/api/daemon/health-check/route.ts +3 -4
  38. package/src/app/api/daemon/route.ts +10 -8
  39. package/src/app/api/documents/route.ts +11 -10
  40. package/src/app/api/external-agents/route.ts +3 -3
  41. package/src/app/api/gateways/[id]/health/route.ts +2 -3
  42. package/src/app/api/gateways/[id]/route.ts +7 -122
  43. package/src/app/api/gateways/route.ts +3 -103
  44. package/src/app/api/mcp-servers/[id]/tools/route.ts +5 -5
  45. package/src/app/api/openclaw/dashboard-url/route.ts +8 -16
  46. package/src/app/api/openclaw/directory/route.ts +2 -2
  47. package/src/app/api/openclaw/history/route.ts +3 -5
  48. package/src/app/api/providers/[id]/models/route.test.ts +60 -0
  49. package/src/app/api/providers/[id]/models/route.ts +33 -1
  50. package/src/app/api/providers/[id]/route.test.ts +49 -0
  51. package/src/app/api/providers/[id]/route.ts +30 -1
  52. package/src/app/api/providers/ollama/route.ts +6 -5
  53. package/src/app/api/schedules/[id]/route.ts +14 -108
  54. package/src/app/api/schedules/[id]/run/route.ts +6 -67
  55. package/src/app/api/schedules/route.ts +9 -51
  56. package/src/app/api/settings/route.ts +4 -3
  57. package/src/app/api/setup/check-provider/route.ts +15 -1
  58. package/src/app/api/setup/openclaw-device/route.ts +2 -2
  59. package/src/app/api/system/status/route.ts +2 -2
  60. package/src/app/api/tasks/[id]/route.ts +16 -202
  61. package/src/app/api/tasks/bulk/route.ts +5 -86
  62. package/src/app/api/tasks/metrics/route.ts +2 -1
  63. package/src/app/api/tasks/route.ts +11 -171
  64. package/src/app/api/upload/route.ts +1 -1
  65. package/src/app/api/uploads/[filename]/route.ts +1 -1
  66. package/src/app/api/uploads/route.ts +1 -1
  67. package/src/app/api/webhooks/[id]/history/route.ts +2 -2
  68. package/src/app/layout.tsx +9 -6
  69. package/src/app/protocols/page.tsx +71 -89
  70. package/src/app/tasks/page.tsx +32 -32
  71. package/src/cli/index.js +1 -0
  72. package/src/cli/spec.js +1 -0
  73. package/src/components/agents/agent-sheet.tsx +51 -25
  74. package/src/components/agents/inspector-panel.tsx +15 -4
  75. package/src/components/auth/setup-wizard/index.tsx +27 -18
  76. package/src/components/auth/setup-wizard/shared.tsx +2 -2
  77. package/src/components/auth/setup-wizard/step-agents.tsx +51 -38
  78. package/src/components/auth/setup-wizard/step-connect.tsx +48 -17
  79. package/src/components/auth/setup-wizard/types.ts +6 -4
  80. package/src/components/auth/setup-wizard/utils.test.ts +38 -8
  81. package/src/components/auth/setup-wizard/utils.ts +14 -8
  82. package/src/components/chatrooms/chatroom-sheet.tsx +16 -276
  83. package/src/components/connectors/connector-list.tsx +26 -40
  84. package/src/components/connectors/connector-sheet.tsx +95 -149
  85. package/src/components/gateways/gateway-sheet.tsx +61 -110
  86. package/src/components/layout/live-query-sync.tsx +121 -0
  87. package/src/components/protocols/structured-session-launcher.tsx +24 -45
  88. package/src/components/providers/app-query-provider.tsx +17 -0
  89. package/src/components/providers/provider-list.tsx +150 -77
  90. package/src/components/providers/provider-sheet.tsx +102 -77
  91. package/src/components/shared/model-combobox.tsx +5 -4
  92. package/src/components/skills/skill-list.tsx +5 -18
  93. package/src/components/skills/skill-sheet.tsx +21 -20
  94. package/src/components/skills/skills-workspace.tsx +48 -87
  95. package/src/components/tasks/task-card.tsx +20 -13
  96. package/src/components/tasks/task-column.tsx +22 -7
  97. package/src/components/tasks/task-list.tsx +8 -11
  98. package/src/components/tasks/task-sheet.tsx +111 -103
  99. package/src/features/agents/queries.ts +20 -0
  100. package/src/features/chatrooms/queries.ts +20 -0
  101. package/src/features/chats/queries.ts +27 -0
  102. package/src/features/connectors/queries.ts +145 -0
  103. package/src/features/credentials/queries.ts +37 -0
  104. package/src/features/extensions/queries.ts +26 -0
  105. package/src/features/external-agents/queries.ts +36 -0
  106. package/src/features/gateways/queries.ts +274 -0
  107. package/src/features/missions/queries.ts +23 -0
  108. package/src/features/projects/queries.ts +20 -0
  109. package/src/features/protocols/queries.ts +149 -0
  110. package/src/features/providers/queries.ts +142 -0
  111. package/src/features/settings/queries.ts +20 -0
  112. package/src/features/skills/queries.ts +182 -0
  113. package/src/features/tasks/queries.ts +189 -0
  114. package/src/hooks/use-ws.ts +3 -2
  115. package/src/lib/agent-provider-options.test.ts +152 -0
  116. package/src/lib/agent-provider-options.ts +84 -0
  117. package/src/lib/app/api-client.ts +2 -2
  118. package/src/lib/providers/index.test.ts +78 -0
  119. package/src/lib/providers/index.ts +13 -10
  120. package/src/lib/query/client.ts +17 -0
  121. package/src/lib/server/agents/agent-runtime-config.ts +6 -6
  122. package/src/lib/server/agents/agent-service.ts +429 -0
  123. package/src/lib/server/agents/agent-thread-session.ts +6 -5
  124. package/src/lib/server/agents/autonomy-contract.ts +1 -4
  125. package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
  126. package/src/lib/server/agents/delegation-advisory.ts +251 -0
  127. package/src/lib/server/agents/main-agent-loop.ts +98 -40
  128. package/src/lib/server/agents/subagent-runtime.ts +12 -0
  129. package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
  130. package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
  131. package/src/lib/server/build-llm.ts +7 -15
  132. package/src/lib/server/capability-router.test.ts +70 -1
  133. package/src/lib/server/capability-router.ts +24 -99
  134. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
  135. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
  136. package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
  137. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
  138. package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
  139. package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
  140. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
  141. package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
  142. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
  143. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
  144. package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
  145. package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
  146. package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
  147. package/src/lib/server/chat-execution/message-classifier.ts +74 -32
  148. package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
  149. package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
  150. package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
  151. package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
  152. package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
  153. package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
  154. package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
  155. package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
  156. package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
  157. package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
  158. package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
  159. package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
  160. package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
  161. package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
  162. package/src/lib/server/chats/chat-session-service.ts +410 -0
  163. package/src/lib/server/connectors/access.ts +1 -1
  164. package/src/lib/server/connectors/commands.ts +7 -6
  165. package/src/lib/server/connectors/connector-inbound.ts +14 -7
  166. package/src/lib/server/connectors/connector-outbound.ts +16 -11
  167. package/src/lib/server/connectors/connector-service.ts +453 -0
  168. package/src/lib/server/connectors/delivery.ts +17 -12
  169. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
  170. package/src/lib/server/connectors/media.ts +1 -1
  171. package/src/lib/server/connectors/response-media.ts +1 -1
  172. package/src/lib/server/connectors/session-consolidation.ts +11 -7
  173. package/src/lib/server/connectors/session.ts +9 -7
  174. package/src/lib/server/connectors/voice-note.ts +2 -1
  175. package/src/lib/server/context-manager.ts +20 -1
  176. package/src/lib/server/cost.ts +2 -3
  177. package/src/lib/server/credentials/credential-repository.ts +43 -4
  178. package/src/lib/server/credentials/credential-service.ts +112 -0
  179. package/src/lib/server/daemon/admin-metadata.ts +64 -0
  180. package/src/lib/server/daemon/controller.ts +577 -0
  181. package/src/lib/server/daemon/daemon-runtime.ts +352 -0
  182. package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
  183. package/src/lib/server/daemon/types.ts +101 -0
  184. package/src/lib/server/embeddings.ts +3 -9
  185. package/src/lib/server/eval/agent-regression.ts +3 -2
  186. package/src/lib/server/eval/runner.ts +2 -2
  187. package/src/lib/server/execution-brief.test.ts +167 -0
  188. package/src/lib/server/execution-brief.ts +295 -0
  189. package/src/lib/server/execution-engine/chat-turn.ts +9 -0
  190. package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
  191. package/src/lib/server/execution-engine/index.ts +35 -0
  192. package/src/lib/server/execution-engine/task-attempt.ts +303 -0
  193. package/src/lib/server/execution-engine/types.ts +33 -0
  194. package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
  195. package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
  196. package/src/lib/server/memory/session-archive-memory.ts +12 -10
  197. package/src/lib/server/messages/message-repository.ts +330 -0
  198. package/src/lib/server/missions/mission-service/core.ts +8 -6
  199. package/src/lib/server/openclaw/agent-resolver.ts +2 -3
  200. package/src/lib/server/openclaw/doctor.ts +1 -1
  201. package/src/lib/server/openclaw/gateway.test.ts +10 -1
  202. package/src/lib/server/openclaw/gateway.ts +5 -14
  203. package/src/lib/server/openclaw/health.ts +3 -11
  204. package/src/lib/server/openclaw/sync.ts +8 -6
  205. package/src/lib/server/persistence/storage-context.ts +3 -0
  206. package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
  207. package/src/lib/server/protocols/protocol-normalization.ts +1 -1
  208. package/src/lib/server/protocols/protocol-queries.ts +13 -7
  209. package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
  210. package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
  211. package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
  212. package/src/lib/server/protocols/protocol-swarm.ts +8 -8
  213. package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
  214. package/src/lib/server/protocols/protocol-templates.ts +4 -2
  215. package/src/lib/server/protocols/protocol-types.ts +10 -7
  216. package/src/lib/server/provider-endpoint.ts +7 -12
  217. package/src/lib/server/provider-model-discovery.ts +2 -11
  218. package/src/lib/server/query-expansion.ts +5 -6
  219. package/src/lib/server/run-context.test.ts +365 -0
  220. package/src/lib/server/run-context.ts +367 -0
  221. package/src/lib/server/runtime/heartbeat-service.ts +7 -5
  222. package/src/lib/server/runtime/queue/core.ts +61 -190
  223. package/src/lib/server/runtime/run-ledger.ts +8 -0
  224. package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
  225. package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
  226. package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
  227. package/src/lib/server/schedules/schedule-route-service.ts +230 -0
  228. package/src/lib/server/service-result.ts +16 -0
  229. package/src/lib/server/session-note.ts +2 -3
  230. package/src/lib/server/session-reset-policy.ts +4 -3
  231. package/src/lib/server/session-tools/connector.ts +9 -6
  232. package/src/lib/server/session-tools/context-mgmt.ts +58 -9
  233. package/src/lib/server/session-tools/crud.ts +162 -10
  234. package/src/lib/server/session-tools/delegate.ts +1 -1
  235. package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
  236. package/src/lib/server/session-tools/memory.ts +6 -4
  237. package/src/lib/server/session-tools/session-info.test.ts +56 -0
  238. package/src/lib/server/session-tools/session-info.ts +119 -12
  239. package/src/lib/server/session-tools/skill-runtime.ts +3 -1
  240. package/src/lib/server/session-tools/skills.ts +15 -15
  241. package/src/lib/server/session-tools/subagent.test.ts +115 -1
  242. package/src/lib/server/session-tools/subagent.ts +125 -7
  243. package/src/lib/server/session-tools/team-context.ts +4 -3
  244. package/src/lib/server/session-tools/wallet.ts +0 -58
  245. package/src/lib/server/sessions/session-lineage.ts +55 -0
  246. package/src/lib/server/sessions/session-repository.ts +2 -2
  247. package/src/lib/server/skills/learned-skills.ts +24 -23
  248. package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
  249. package/src/lib/server/skills/skill-repository.ts +136 -13
  250. package/src/lib/server/skills/skill-suggestions.ts +25 -28
  251. package/src/lib/server/storage-normalization.test.ts +42 -215
  252. package/src/lib/server/storage-normalization.ts +98 -0
  253. package/src/lib/server/storage.ts +19 -0
  254. package/src/lib/server/structured-extract.ts +3 -14
  255. package/src/lib/server/tasks/task-followups.ts +16 -11
  256. package/src/lib/server/tasks/task-result.test.ts +25 -29
  257. package/src/lib/server/tasks/task-result.ts +5 -9
  258. package/src/lib/server/tasks/task-route-service.ts +449 -0
  259. package/src/lib/server/text-normalization.ts +41 -0
  260. package/src/lib/server/tool-planning.ts +6 -42
  261. package/src/lib/server/upload-path.ts +5 -0
  262. package/src/lib/server/working-state/extraction.ts +614 -0
  263. package/src/lib/server/working-state/normalization.ts +866 -0
  264. package/src/lib/server/working-state/prompt.ts +60 -0
  265. package/src/lib/server/working-state/repository.ts +38 -0
  266. package/src/lib/server/working-state/service.test.ts +253 -0
  267. package/src/lib/server/working-state/service.ts +293 -0
  268. package/src/lib/validation/schemas.ts +1 -0
  269. package/src/lib/ws-client.ts +3 -3
  270. package/src/stores/slices/task-slice.ts +1 -4
  271. package/src/stores/use-chatroom-store.ts +2 -2
  272. package/src/types/index.ts +288 -22
  273. package/src/views/settings/section-providers.tsx +2 -2
@@ -6,6 +6,7 @@ import {
6
6
  loadAgents, loadCredentials, decryptKey, loadSettings, loadSkills,
7
7
  loadChatrooms, saveChatrooms,
8
8
  } from '../storage'
9
+ import { getMessages } from '@/lib/server/messages/message-repository'
9
10
  import { dedup, errorMessage, hmrSingleton } from '@/lib/shared-utils'
10
11
  import path from 'path'
11
12
  import { streamAgentChat } from '@/lib/server/chat-execution/stream-agent-chat'
@@ -22,7 +23,10 @@ import {
22
23
  resolveApiKey as resolveApiKeyHelper,
23
24
  } from '@/lib/server/chatrooms/chatroom-helpers'
24
25
  import { filterHealthyChatroomAgents } from '@/lib/server/chatrooms/chatroom-health'
25
- import { evaluateRoutingRules } from '@/lib/server/chatrooms/chatroom-routing'
26
+ import {
27
+ ensureChatroomRoutingGuidance,
28
+ selectChatroomRecipients,
29
+ } from '@/lib/server/chatrooms/chatroom-routing'
26
30
  import { markProviderFailure, markProviderSuccess } from '../provider-health'
27
31
  import { buildIdentityContinuityContext } from '../identity-continuity'
28
32
  import { buildRuntimeSkillPromptBlocks, resolveRuntimeSkills } from '@/lib/server/skills/runtime-skill-resolver'
@@ -630,11 +634,14 @@ async function routeMessageToChatroom(connector: Connector, msg: InboundMessage)
630
634
  const threadContextBlock = buildConnectorThreadContextBlock(msg)
631
635
 
632
636
  // Parse mentions from the message text
637
+ ensureChatroomRoutingGuidance(chatroom, agents)
633
638
  let mentions = parseMentions(msg.text || '', agents, chatroom.agentIds)
634
- // Routing rules: if no explicit mentions, evaluate keyword/capability rules
635
- if (mentions.length === 0 && chatroom.routingRules?.length) {
636
- const agentList = chatroom.agentIds.map((id) => agents[id]).filter(Boolean)
637
- mentions = evaluateRoutingRules(msg.text || '', chatroom.routingRules, agentList)
639
+ if (mentions.length === 0 && !chatroom.autoAddress) {
640
+ mentions = await selectChatroomRecipients({
641
+ text: msg.text || '',
642
+ chatroom,
643
+ agentsById: agents,
644
+ })
638
645
  }
639
646
  // Auto-address: if enabled and still no mentions, address all agents
640
647
  if (chatroom.autoAddress && mentions.length === 0) {
@@ -1245,7 +1252,7 @@ If media sending fails, report the exact error and retry with a corrected path/t
1245
1252
  }
1246
1253
  }
1247
1254
  },
1248
- history: modelHistoryTailWithAttribution(session.messages, 50, 48_000),
1255
+ history: modelHistoryTailWithAttribution(getMessages(session.id), 50, 48_000),
1249
1256
  })
1250
1257
  settledConnectorToolEvents = [
1251
1258
  ...pruneIncompleteToolEvents(streamedConnectorToolEvents),
@@ -1300,7 +1307,7 @@ If media sending fails, report the exact error and retry with a corrected path/t
1300
1307
  }
1301
1308
  },
1302
1309
  active: new Map(),
1303
- loadHistory: () => modelHistoryTailWithAttribution(session.messages, 50, 48_000),
1310
+ loadHistory: () => modelHistoryTailWithAttribution(getMessages(session.id), 50, 48_000),
1304
1311
  })
1305
1312
  mediaExtractionText = fullText
1306
1313
  }
@@ -3,6 +3,7 @@ import {
3
3
  loadConnectors,
4
4
  loadSession, upsertSession,
5
5
  } from '../storage'
6
+ import { getMessages, replaceMessageAt } from '@/lib/server/messages/message-repository'
6
7
  import { errorMessage } from '@/lib/shared-utils'
7
8
  import path from 'path'
8
9
  import { notify } from '../ws-hub'
@@ -247,7 +248,7 @@ export async function sendConnectorMessage(params: {
247
248
  lastOutboundAt: Date.now(),
248
249
  lastOutboundMessageId: result?.messageId || session.connectorContext?.lastOutboundMessageId || null,
249
250
  }
250
- const history = Array.isArray(session.messages) ? session.messages : []
251
+ const history = getMessages(session.id)
251
252
  for (let i = history.length - 1; i >= 0; i -= 1) {
252
253
  const entry = history[i]
253
254
  if (entry?.role !== 'assistant') continue
@@ -255,17 +256,21 @@ export async function sendConnectorMessage(params: {
255
256
  if (source.connectorId !== connectorId) continue
256
257
  if (source.channelId !== channelId) continue
257
258
  if (!source.messageId && result?.messageId) {
258
- entry.source = {
259
- platform: source.platform || connector.platform,
260
- connectorId: source.connectorId || connectorId,
261
- connectorName: source.connectorName || connector.name,
262
- channelId: source.channelId || channelId,
263
- senderId: source.senderId,
264
- senderName: source.senderName,
265
- messageId: result.messageId,
266
- threadId: source.threadId || params.threadId,
267
- replyToMessageId: source.replyToMessageId || params.replyToMessageId,
259
+ const updatedEntry = {
260
+ ...entry,
261
+ source: {
262
+ platform: source.platform || connector.platform,
263
+ connectorId: source.connectorId || connectorId,
264
+ connectorName: source.connectorName || connector.name,
265
+ channelId: source.channelId || channelId,
266
+ senderId: source.senderId,
267
+ senderName: source.senderName,
268
+ messageId: result.messageId,
269
+ threadId: source.threadId || params.threadId,
270
+ replyToMessageId: source.replyToMessageId || params.replyToMessageId,
271
+ },
268
272
  }
273
+ replaceMessageAt(session.id, i, updatedEntry)
269
274
  }
270
275
  break
271
276
  }
@@ -0,0 +1,453 @@
1
+ import { genId } from '@/lib/id'
2
+ import { logActivity } from '@/lib/server/activity/activity-log'
3
+ import {
4
+ deleteConnector,
5
+ loadConnector,
6
+ loadConnectorHealth,
7
+ loadConnectors,
8
+ upsertConnector,
9
+ } from '@/lib/server/connectors/connector-repository'
10
+ import {
11
+ buildConnectorAccessSnapshot,
12
+ resolveConnectorOwnerSenderId,
13
+ } from '@/lib/server/connectors/access'
14
+ import {
15
+ addAllowedSender,
16
+ approvePairingCode,
17
+ approvePendingSender,
18
+ clearSenderAddressingOverride,
19
+ normalizeSenderId,
20
+ parseAllowFromCsv,
21
+ parseDmAddressingMode,
22
+ parsePairingPolicy,
23
+ removeAllowedSender,
24
+ rejectPendingSender,
25
+ setSenderAddressingOverride,
26
+ senderMatchesAnyEntry,
27
+ } from '@/lib/server/connectors/pairing'
28
+ import {
29
+ ensureDaemonProcessRunning,
30
+ getDaemonConnectorRuntime,
31
+ listDaemonConnectorRuntime,
32
+ runDaemonConnectorAction,
33
+ } from '@/lib/server/daemon/controller'
34
+ import { log } from '@/lib/server/logger'
35
+ import { serviceFail, serviceOk } from '@/lib/server/service-result'
36
+ import { notify } from '@/lib/server/ws-hub'
37
+ import { errorMessage } from '@/lib/shared-utils'
38
+ import type {
39
+ Connector,
40
+ ConnectorAccessMutationAction,
41
+ ConnectorAccessMutationResponse,
42
+ ConnectorHealthEvent,
43
+ } from '@/types'
44
+ import type { ServiceResult } from '@/lib/server/service-result'
45
+ import type { DaemonConnectorRuntimeState } from '@/lib/server/daemon/types'
46
+
47
+ function cloneConnector<T extends Connector>(connector: T): T {
48
+ return {
49
+ ...connector,
50
+ config: connector.config ? { ...connector.config } : {},
51
+ }
52
+ }
53
+
54
+ function persistConnector(connector: Connector): void {
55
+ connector.updatedAt = Date.now()
56
+ upsertConnector(connector.id, connector)
57
+ }
58
+
59
+ function applyRuntimeFields(connector: Connector, runtime: DaemonConnectorRuntimeState | null): Connector {
60
+ connector.status = runtime?.status
61
+ ? runtime.status
62
+ : connector.lastError
63
+ ? 'error'
64
+ : 'stopped'
65
+
66
+ if (connector.platform === 'whatsapp') {
67
+ connector.authenticated = runtime?.authenticated
68
+ connector.hasCredentials = runtime?.hasCredentials
69
+ if (runtime?.qrDataUrl) connector.qrDataUrl = runtime.qrDataUrl
70
+ else delete connector.qrDataUrl
71
+ }
72
+
73
+ if (runtime?.reconnectAttempts !== undefined) {
74
+ const ext = connector as unknown as Record<string, unknown>
75
+ ext.reconnectAttempts = runtime.reconnectAttempts
76
+ ext.nextRetryAt = runtime.nextRetryAt
77
+ ext.reconnectError = runtime.reconnectError
78
+ ext.reconnectExhausted = runtime.reconnectExhausted
79
+ }
80
+
81
+ if (runtime?.presence && connector.status === 'running') {
82
+ connector.presence = runtime.presence
83
+ } else {
84
+ delete connector.presence
85
+ }
86
+
87
+ return connector
88
+ }
89
+
90
+ function setConnectorSenderList(connector: Connector, key: string, values: string[]): void {
91
+ if (!connector.config) connector.config = {}
92
+ if (values.length === 0) {
93
+ delete connector.config[key]
94
+ return
95
+ }
96
+ connector.config[key] = values.join(',')
97
+ }
98
+
99
+ function addConnectorSenderListEntry(connector: Connector, key: string, senderId: string): boolean {
100
+ const normalized = normalizeSenderId(senderId)
101
+ if (!normalized) return false
102
+ const current = parseAllowFromCsv(connector.config?.[key])
103
+ if (senderMatchesAnyEntry(normalized, current)) return false
104
+ setConnectorSenderList(connector, key, [...current, normalized])
105
+ return true
106
+ }
107
+
108
+ function removeConnectorSenderListEntry(connector: Connector, key: string, senderId: string): boolean {
109
+ const normalized = normalizeSenderId(senderId)
110
+ if (!normalized) return false
111
+ const current = parseAllowFromCsv(connector.config?.[key])
112
+ const next = current.filter((entry) => !senderMatchesAnyEntry(normalized, [entry]))
113
+ if (next.length === current.length) return false
114
+ setConnectorSenderList(connector, key, next)
115
+ return true
116
+ }
117
+
118
+ function requireSenderId(body: Record<string, unknown>): string {
119
+ const senderId = typeof body.senderId === 'string' ? body.senderId.trim() : ''
120
+ if (!senderId) throw new Error('senderId is required for this action')
121
+ return senderId
122
+ }
123
+
124
+ export async function listConnectorsWithRuntime(): Promise<Record<string, Connector>> {
125
+ await ensureDaemonProcessRunning('api/connectors:get')
126
+ const connectors = Object.fromEntries(
127
+ Object.entries(loadConnectors()).map(([id, connector]) => [id, cloneConnector(connector)]),
128
+ ) as Record<string, Connector>
129
+ const runtimeByConnector = await listDaemonConnectorRuntime()
130
+ for (const connector of Object.values(connectors)) {
131
+ applyRuntimeFields(connector, runtimeByConnector[connector.id] || null)
132
+ }
133
+ return connectors
134
+ }
135
+
136
+ export async function getConnectorWithRuntime(id: string): Promise<Connector | null> {
137
+ await ensureDaemonProcessRunning('api/connectors/[id]:get')
138
+ const connector = loadConnector(id)
139
+ if (!connector) return null
140
+ const current = cloneConnector(connector)
141
+ return applyRuntimeFields(current, await getDaemonConnectorRuntime(id))
142
+ }
143
+
144
+ export function createConnector(body: Record<string, unknown>): Connector {
145
+ const id = genId()
146
+ const connector: Connector = {
147
+ id,
148
+ name: (body.name as string) || `${String(body.platform || '')} Connector`,
149
+ platform: body.platform as Connector['platform'],
150
+ agentId: (body.agentId as string | null | undefined) || null,
151
+ chatroomId: (body.chatroomId as string | null | undefined) || null,
152
+ credentialId: (body.credentialId as string | null | undefined) || null,
153
+ config: body.config && typeof body.config === 'object' && !Array.isArray(body.config)
154
+ ? body.config as Record<string, string>
155
+ : {},
156
+ isEnabled: false,
157
+ status: 'stopped',
158
+ lastError: null,
159
+ createdAt: Date.now(),
160
+ updatedAt: Date.now(),
161
+ }
162
+ upsertConnector(id, connector)
163
+ notify('connectors')
164
+ return connector
165
+ }
166
+
167
+ export async function autoStartConnectorIfNeeded(connector: Connector, body: Record<string, unknown>): Promise<void> {
168
+ const hasCredentials = connector.platform === 'whatsapp'
169
+ || connector.platform === 'openclaw'
170
+ || (connector.platform === 'bluebubbles' && (!!connector.credentialId || !!connector.config.password))
171
+ || !!connector.credentialId
172
+ if (!hasCredentials || body.autoStart === false) return
173
+ try {
174
+ await runDaemonConnectorAction(connector.id, 'start', 'connectors:auto-start')
175
+ } catch (err: unknown) {
176
+ log.warn('connectors', `Auto-start failed for connector ${connector.id}`, errorMessage(err))
177
+ }
178
+ }
179
+
180
+ export async function updateConnectorFromRoute(id: string, body: Record<string, unknown>): Promise<ServiceResult<Connector>> {
181
+ await ensureDaemonProcessRunning('api/connectors/[id]:put')
182
+ const connector = loadConnector(id)
183
+ if (!connector) return serviceFail(404, 'Connector not found')
184
+
185
+ if (body.action === 'start' || body.action === 'stop' || body.action === 'repair') {
186
+ try {
187
+ if (body.action === 'start') {
188
+ await runDaemonConnectorAction(id, 'start', 'api/connectors/[id]:action:start')
189
+ logActivity({ entityType: 'connector', entityId: id, action: 'started', actor: 'user', summary: `Connector started: "${connector.name}"` })
190
+ } else if (body.action === 'stop') {
191
+ await runDaemonConnectorAction(id, 'stop', 'api/connectors/[id]:action:stop')
192
+ logActivity({ entityType: 'connector', entityId: id, action: 'stopped', actor: 'user', summary: `Connector stopped: "${connector.name}"` })
193
+ } else {
194
+ await runDaemonConnectorAction(id, 'repair', 'api/connectors/[id]:action:repair')
195
+ logActivity({ entityType: 'connector', entityId: id, action: 'started', actor: 'user', summary: `Connector repaired: "${connector.name}"` })
196
+ }
197
+ } catch (err: unknown) {
198
+ log.error('connectors', `Action failed for connector ${id}`, errorMessage(err))
199
+ return serviceFail(500, 'Connector action failed')
200
+ }
201
+ notify('connectors')
202
+ const updated = await getConnectorWithRuntime(id)
203
+ return serviceOk(updated || connector)
204
+ }
205
+
206
+ const next = cloneConnector(connector)
207
+ if (body.name !== undefined) next.name = typeof body.name === 'string' ? body.name : next.name
208
+ if (body.agentId !== undefined) next.agentId = typeof body.agentId === 'string' || body.agentId === null ? body.agentId : next.agentId
209
+ if (body.chatroomId !== undefined) next.chatroomId = typeof body.chatroomId === 'string' || body.chatroomId === null ? body.chatroomId : next.chatroomId
210
+ if (body.credentialId !== undefined) next.credentialId = typeof body.credentialId === 'string' || body.credentialId === null ? body.credentialId : next.credentialId
211
+ if (body.config !== undefined) next.config = body.config && typeof body.config === 'object' && !Array.isArray(body.config) ? body.config as Record<string, string> : next.config
212
+ if (body.isEnabled !== undefined) next.isEnabled = typeof body.isEnabled === 'boolean' ? body.isEnabled : next.isEnabled
213
+ persistConnector(next)
214
+
215
+ try {
216
+ const runtime = await getDaemonConnectorRuntime(id)
217
+ const wasRunning = runtime?.status === 'running'
218
+ const shouldStop = body.isEnabled === false
219
+ const shouldReload = wasRunning && (
220
+ body.name !== undefined
221
+ || body.agentId !== undefined
222
+ || body.chatroomId !== undefined
223
+ || body.credentialId !== undefined
224
+ || body.config !== undefined
225
+ || body.isEnabled !== undefined
226
+ )
227
+ const shouldStart = body.isEnabled === true && !wasRunning
228
+ if (shouldStop) {
229
+ await runDaemonConnectorAction(id, 'stop', 'api/connectors/[id]:reload:stop')
230
+ } else if (shouldReload || shouldStart) {
231
+ await runDaemonConnectorAction(id, 'start', 'api/connectors/[id]:reload:start')
232
+ }
233
+ } catch (err: unknown) {
234
+ log.warn('connectors', `Failed to reload connector ${id} after update`, errorMessage(err))
235
+ }
236
+
237
+ notify('connectors')
238
+ return serviceOk(await getConnectorWithRuntime(id) || next)
239
+ }
240
+
241
+ export async function deleteConnectorFromRoute(id: string): Promise<ServiceResult<{ ok: true }>> {
242
+ const connector = loadConnector(id)
243
+ if (!connector) return serviceFail(404, 'Connector not found')
244
+ try {
245
+ await runDaemonConnectorAction(id, 'stop', 'api/connectors/[id]:delete')
246
+ } catch (err: unknown) {
247
+ log.warn('connectors', `Failed to stop connector ${id} during delete`, errorMessage(err))
248
+ }
249
+ try {
250
+ const { clearConnectorPairingState } = await import('@/lib/server/connectors/pairing')
251
+ clearConnectorPairingState(id)
252
+ } catch (err: unknown) {
253
+ log.warn('connectors', `Failed to clear pairing state for ${id}`, errorMessage(err))
254
+ }
255
+ deleteConnector(id)
256
+ notify('connectors')
257
+ return serviceOk({ ok: true as const })
258
+ }
259
+
260
+ export function getConnectorHealthForApi(id: string): { events: ConnectorHealthEvent[]; uptimePercent: number } | null {
261
+ const connector = loadConnector(id)
262
+ if (!connector) return null
263
+ const allHealth = loadConnectorHealth()
264
+ const events: ConnectorHealthEvent[] = []
265
+ for (const raw of Object.values(allHealth)) {
266
+ const entry = raw as ConnectorHealthEvent
267
+ if (entry.connectorId !== id) continue
268
+ events.push(entry)
269
+ }
270
+ events.sort((a, b) => a.timestamp.localeCompare(b.timestamp))
271
+ return { events, uptimePercent: computeUptime(events) }
272
+ }
273
+
274
+ function computeUptime(events: ConnectorHealthEvent[]): number {
275
+ if (events.length === 0) return 0
276
+ const firstTime = new Date(events[0].timestamp).getTime()
277
+ const now = Date.now()
278
+ const totalMs = now - firstTime
279
+ if (totalMs <= 0) return 100
280
+ let uptimeMs = 0
281
+ let lastUpAt: number | null = null
282
+ for (const event of events) {
283
+ const time = new Date(event.timestamp).getTime()
284
+ if (event.event === 'started' || event.event === 'reconnected') {
285
+ if (lastUpAt === null) lastUpAt = time
286
+ } else if (event.event === 'stopped' || event.event === 'error' || event.event === 'disconnected') {
287
+ if (lastUpAt !== null) {
288
+ uptimeMs += time - lastUpAt
289
+ lastUpAt = null
290
+ }
291
+ }
292
+ }
293
+ if (lastUpAt !== null) uptimeMs += now - lastUpAt
294
+ return Math.round((uptimeMs / totalMs) * 10000) / 100
295
+ }
296
+
297
+ export async function updateConnectorAccess(
298
+ connectorId: string,
299
+ body: Record<string, unknown>,
300
+ ): Promise<ServiceResult<ConnectorAccessMutationResponse>> {
301
+ await ensureDaemonProcessRunning('api/connectors/[id]/access:put')
302
+ const connector = loadConnector(connectorId)
303
+ if (!connector) return serviceFail(404, 'Connector not found')
304
+ const current = cloneConnector(connector)
305
+
306
+ try {
307
+ const action = typeof body.action === 'string' ? body.action.trim().toLowerCase() as ConnectorAccessMutationAction : null
308
+ if (!action) {
309
+ return serviceFail(400, 'Missing access action')
310
+ }
311
+ let connectorChanged = false
312
+ let responseSenderId = typeof body.senderId === 'string' ? body.senderId.trim() : ''
313
+ const responseSenderIdAlt = typeof body.senderIdAlt === 'string' ? body.senderIdAlt.trim() : ''
314
+ let summary = `Updated access controls for "${current.name}".`
315
+
316
+ switch (action) {
317
+ case 'set_policy': {
318
+ const rawPolicy = typeof body.dmPolicy === 'string' ? body.dmPolicy.trim() : ''
319
+ if (!rawPolicy) delete current.config.dmPolicy
320
+ else current.config.dmPolicy = parsePairingPolicy(rawPolicy, 'open')
321
+ connectorChanged = true
322
+ summary = `Updated DM policy for "${current.name}".`
323
+ break
324
+ }
325
+ case 'set_dm_addressing_mode': {
326
+ const rawMode = typeof body.dmAddressingMode === 'string' ? body.dmAddressingMode.trim() : ''
327
+ const nextMode = parseDmAddressingMode(rawMode || 'open', 'open')
328
+ if (nextMode === 'open') delete current.config.dmAddressingMode
329
+ else current.config.dmAddressingMode = nextMode
330
+ connectorChanged = true
331
+ summary = `Updated DM addressing mode for "${current.name}" to ${nextMode}.`
332
+ break
333
+ }
334
+ case 'allow_sender': {
335
+ const senderId = requireSenderId(body)
336
+ addAllowedSender(current.id, senderId)
337
+ connectorChanged = removeConnectorSenderListEntry(current, 'denyFrom', senderId) || connectorChanged
338
+ summary = `Allowed sender ${normalizeSenderId(senderId)} on "${current.name}".`
339
+ break
340
+ }
341
+ case 'remove_allowed_sender': {
342
+ const senderId = requireSenderId(body)
343
+ removeAllowedSender(current.id, senderId)
344
+ connectorChanged = removeConnectorSenderListEntry(current, 'allowFrom', senderId) || connectorChanged
345
+ summary = `Removed connector-managed access for ${normalizeSenderId(senderId)} on "${current.name}".`
346
+ break
347
+ }
348
+ case 'block_sender': {
349
+ const senderId = requireSenderId(body)
350
+ connectorChanged = addConnectorSenderListEntry(current, 'denyFrom', senderId) || connectorChanged
351
+ connectorChanged = removeConnectorSenderListEntry(current, 'allowFrom', senderId) || connectorChanged
352
+ removeAllowedSender(current.id, senderId)
353
+ rejectPendingSender(current.id, senderId)
354
+ const ownerSenderId = resolveConnectorOwnerSenderId(current)
355
+ if (ownerSenderId && senderMatchesAnyEntry(senderId, [ownerSenderId])) {
356
+ delete current.config.ownerSenderId
357
+ connectorChanged = true
358
+ }
359
+ summary = `Blocked sender ${normalizeSenderId(senderId)} on "${current.name}".`
360
+ break
361
+ }
362
+ case 'unblock_sender': {
363
+ const senderId = requireSenderId(body)
364
+ connectorChanged = removeConnectorSenderListEntry(current, 'denyFrom', senderId) || connectorChanged
365
+ summary = `Removed sender ${normalizeSenderId(senderId)} from the deny list on "${current.name}".`
366
+ break
367
+ }
368
+ case 'approve_pairing': {
369
+ if (typeof body.code === 'string' && body.code.trim()) {
370
+ const approved = approvePairingCode(current.id, body.code)
371
+ if (!approved.ok) {
372
+ return serviceFail(400, approved.reason || 'Pairing approval failed.')
373
+ }
374
+ if (approved.senderId) {
375
+ responseSenderId = approved.senderId
376
+ connectorChanged = removeConnectorSenderListEntry(current, 'denyFrom', approved.senderId) || connectorChanged
377
+ }
378
+ summary = `Approved pairing on "${current.name}".`
379
+ } else {
380
+ const senderId = requireSenderId(body)
381
+ const approved = approvePendingSender(current.id, senderId)
382
+ if (!approved.ok) {
383
+ return serviceFail(400, approved.reason || 'Pairing approval failed.')
384
+ }
385
+ connectorChanged = removeConnectorSenderListEntry(current, 'denyFrom', senderId) || connectorChanged
386
+ summary = `Approved pairing for ${normalizeSenderId(senderId)} on "${current.name}".`
387
+ }
388
+ break
389
+ }
390
+ case 'reject_pairing': {
391
+ const senderId = requireSenderId(body)
392
+ rejectPendingSender(current.id, senderId)
393
+ summary = `Rejected pairing for ${normalizeSenderId(senderId)} on "${current.name}".`
394
+ break
395
+ }
396
+ case 'set_owner': {
397
+ const senderId = requireSenderId(body)
398
+ const normalized = normalizeSenderId(senderId)
399
+ if (!normalized) return serviceFail(400, 'Could not normalize owner sender ID')
400
+ current.config.ownerSenderId = normalized
401
+ connectorChanged = true
402
+ connectorChanged = removeConnectorSenderListEntry(current, 'denyFrom', normalized) || connectorChanged
403
+ summary = `Set connector owner for "${current.name}" to ${normalized}.`
404
+ break
405
+ }
406
+ case 'clear_owner': {
407
+ if (current.config?.ownerSenderId) {
408
+ delete current.config.ownerSenderId
409
+ connectorChanged = true
410
+ }
411
+ summary = `Cleared connector owner override for "${current.name}".`
412
+ break
413
+ }
414
+ case 'set_sender_dm_addressing': {
415
+ const senderId = requireSenderId(body)
416
+ const rawMode = typeof body.dmAddressingMode === 'string' ? body.dmAddressingMode.trim() : ''
417
+ const nextMode = parseDmAddressingMode(rawMode || 'open', 'open')
418
+ setSenderAddressingOverride(current.id, senderId, nextMode)
419
+ summary = `Updated DM addressing override for ${normalizeSenderId(senderId)} on "${current.name}" to ${nextMode}.`
420
+ break
421
+ }
422
+ case 'clear_sender_dm_addressing': {
423
+ const senderId = requireSenderId(body)
424
+ clearSenderAddressingOverride(current.id, senderId)
425
+ summary = `Cleared DM addressing override for ${normalizeSenderId(senderId)} on "${current.name}".`
426
+ break
427
+ }
428
+ default:
429
+ return serviceFail(400, `Unsupported access action: ${action}`)
430
+ }
431
+
432
+ if (connectorChanged) persistConnector(current)
433
+ logActivity({
434
+ entityType: 'connector',
435
+ entityId: current.id,
436
+ action: 'access-updated',
437
+ actor: 'user',
438
+ summary,
439
+ detail: { action },
440
+ })
441
+ notify('connectors')
442
+ return serviceOk({
443
+ ok: true,
444
+ snapshot: buildConnectorAccessSnapshot({
445
+ connector: current,
446
+ senderId: responseSenderId || null,
447
+ senderIdAlt: responseSenderIdAlt || null,
448
+ }),
449
+ })
450
+ } catch (err: unknown) {
451
+ return serviceFail(400, errorMessage(err))
452
+ }
453
+ }
@@ -1,5 +1,6 @@
1
1
  import type { Connector, MessageSource } from '@/types'
2
- import { loadConnectors } from '../storage'
2
+ import { loadConnectors } from './connector-repository'
3
+ import { getMessages, replaceMessageAt } from '@/lib/server/messages/message-repository'
3
4
  import { notify } from '../ws-hub'
4
5
  import { resolveConnectorSessionPolicy, shouldReplyToInboundMessage } from './policy'
5
6
  import { runningConnectors } from './runtime-state'
@@ -68,7 +69,7 @@ export async function recordConnectorOutboundDelivery(params: {
68
69
  lastOutboundMessageId: params.messageId || session.connectorContext?.lastOutboundMessageId || null,
69
70
  threadId: params.inbound.threadId || session.connectorContext?.threadId || null,
70
71
  }
71
- const history = Array.isArray(session.messages) ? session.messages : []
72
+ const history = getMessages(session.id)
72
73
  for (let i = history.length - 1; i >= 0; i -= 1) {
73
74
  const entry = history[i]
74
75
  if (entry?.role !== 'assistant') continue
@@ -76,17 +77,21 @@ export async function recordConnectorOutboundDelivery(params: {
76
77
  if (source.connectorId !== connector.id) continue
77
78
  if (source.channelId !== params.inbound.channelId) continue
78
79
  if (!source.messageId && params.messageId) {
79
- entry.source = {
80
- platform: source.platform || connector.platform,
81
- connectorId: source.connectorId || connector.id,
82
- connectorName: source.connectorName || connector.name,
83
- channelId: source.channelId || params.inbound.channelId,
84
- senderId: source.senderId,
85
- senderName: source.senderName,
86
- messageId: params.messageId,
87
- replyToMessageId: source.replyToMessageId || params.inbound.messageId,
88
- threadId: source.threadId || params.inbound.threadId,
80
+ const updatedEntry = {
81
+ ...entry,
82
+ source: {
83
+ platform: source.platform || connector.platform,
84
+ connectorId: source.connectorId || connector.id,
85
+ connectorName: source.connectorName || connector.name,
86
+ channelId: source.channelId || params.inbound.channelId,
87
+ senderId: source.senderId,
88
+ senderName: source.senderName,
89
+ messageId: params.messageId,
90
+ replyToMessageId: source.replyToMessageId || params.inbound.messageId,
91
+ threadId: source.threadId || params.inbound.threadId,
92
+ },
89
93
  }
94
+ replaceMessageAt(session.id, i, updatedEntry)
90
95
  }
91
96
  break
92
97
  }
@@ -1,7 +1,8 @@
1
1
  import { log } from '@/lib/server/logger'
2
2
  import fs from 'node:fs'
3
3
  import path from 'node:path'
4
- import { decryptKey, loadCredentials, loadSettings } from '../storage'
4
+ import { listCredentialIdsByProvider, resolveCredentialSecret } from '@/lib/server/credentials/credential-service'
5
+ import { loadSettings } from '../settings/settings-repository'
5
6
  import { mimeFromPath } from './media'
6
7
  import type { InboundMessage, InboundMedia } from './types'
7
8
  import { errorMessage } from '@/lib/shared-utils'
@@ -90,25 +91,15 @@ function resolveOpenAiApiKey(preferredCredentialId?: string | null): string | nu
90
91
  const envKey = String(process.env.SWARMCLAW_OPENAI_STT_API_KEY || process.env.OPENAI_API_KEY || '').trim()
91
92
  if (envKey) return envKey
92
93
 
93
- const creds = loadCredentials() as Record<string, { provider?: string; encryptedKey?: string }>
94
94
  const candidates: string[] = []
95
95
  if (preferredCredentialId) candidates.push(preferredCredentialId)
96
- for (const [id, cred] of Object.entries(creds)) {
97
- const provider = String(cred?.provider || '').trim().toLowerCase()
98
- if (provider === 'openai') candidates.push(id)
99
- }
96
+ candidates.push(...listCredentialIdsByProvider('openai'))
100
97
  const seen = new Set<string>()
101
98
  for (const id of candidates) {
102
99
  if (!id || seen.has(id)) continue
103
100
  seen.add(id)
104
- const cred = creds[id]
105
- const provider = String(cred?.provider || '').trim().toLowerCase()
106
- if (provider !== 'openai') continue
107
- if (!cred?.encryptedKey) continue
108
- try {
109
- const decrypted = decryptKey(cred.encryptedKey).trim()
110
- if (decrypted) return decrypted
111
- } catch { /* ignore invalid credential */ }
101
+ const decrypted = resolveCredentialSecret(id)?.trim()
102
+ if (decrypted) return decrypted
112
103
  }
113
104
 
114
105
  return null
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs'
2
2
  import path from 'path'
3
3
  import { genId } from '@/lib/id'
4
- import { UPLOAD_DIR } from '../storage'
4
+ import { UPLOAD_DIR } from '../upload-path'
5
5
  import type { InboundMedia, InboundMediaType } from './types'
6
6
 
7
7
  const MIME_EXT_MAP: Record<string, string> = {
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs'
2
2
  import path from 'path'
3
- import { UPLOAD_DIR } from '../storage'
3
+ import { UPLOAD_DIR } from '../upload-path'
4
4
  import { safeJsonParseObject } from '../json-utils'
5
5
  import type { InboundMessage, InboundMedia } from './types'
6
6