groups.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import type {
  2. GroupAgent, GroupCapabilities, GroupInteraction, GroupMessagePage, GroupNode, GroupRoomDetail, GroupRoomPage, GroupRun, GroupTopicPage, GroupTopicSummary, JsonValue, UploadReference,
  3. } from '@shared/types'
  4. import { apiRequest, apiUrl, unwrapData } from './client'
  5. import {
  6. normalizeGroupAgent, normalizeGroupCapabilities, normalizeGroupInteraction, normalizeGroupMessage,
  7. normalizeGroupRoom, normalizeGroupRoomDetail, normalizeGroupRun, normalizeGroupTopic, record, values,
  8. } from '@/utils/normalize'
  9. import { createUuid } from '@/utils/id'
  10. const BASE = '/api/app/groups'
  11. function requestId(): string { return createUuid() }
  12. export async function getGroupCapabilities(): Promise<GroupCapabilities> {
  13. return normalizeGroupCapabilities(unwrapData(await apiRequest<unknown>(`${BASE}/capabilities`)))
  14. }
  15. export async function getGroupNodes(): Promise<GroupNode[]> {
  16. const payload = record(unwrapData(await apiRequest<unknown>(`${BASE}/nodes`)))
  17. return values(payload.items ?? payload.nodes).flatMap(value => {
  18. const node = record(value)
  19. const nodeId = String(node.nodeId ?? node.node_id ?? '')
  20. if (!nodeId) return []
  21. return [{
  22. nodeId,
  23. name: String(node.name ?? nodeId),
  24. serverUrl: String(node.serverUrl ?? node.server_url ?? ''),
  25. fingerprint: String(node.fingerprint ?? ''),
  26. createdAt: Number(node.createdAt ?? node.created_at ?? 0),
  27. updatedAt: Number(node.updatedAt ?? node.updated_at ?? 0),
  28. profiles: values(node.profiles).flatMap(profileValue => {
  29. const profile = record(profileValue)
  30. const name = String(profile.name ?? '')
  31. return name ? [{
  32. name,
  33. displayName: String(profile.displayName ?? profile.display_name ?? name),
  34. model: String(profile.model ?? ''),
  35. }] : []
  36. }),
  37. }]
  38. })
  39. }
  40. export async function getGroupRooms(cursor?: string, limit = 50, archived = false): Promise<GroupRoomPage> {
  41. const payload = unwrapData(await apiRequest<unknown>(apiUrl(`${BASE}/rooms`, { cursor, limit: Math.max(1, Math.min(100, limit)), archived })))
  42. const source = record(payload)
  43. return {
  44. items: values(source.items ?? source.rooms ?? payload).map(normalizeGroupRoom).filter(room => room.id),
  45. nextCursor: typeof source.nextCursor === 'string' ? source.nextCursor : typeof source.next_cursor === 'string' ? source.next_cursor : null,
  46. }
  47. }
  48. export async function getGroupRoom(roomId: string): Promise<GroupRoomDetail> {
  49. return normalizeGroupRoomDetail(unwrapData(await apiRequest<unknown>(`${BASE}/rooms/${encodeURIComponent(roomId)}`)))
  50. }
  51. export async function getGroupTopics(roomId: string, cursor?: string, limit = 50, archived = false): Promise<GroupTopicPage> {
  52. const payload = unwrapData(await apiRequest<unknown>(apiUrl(`${BASE}/rooms/${encodeURIComponent(roomId)}/topics`, {
  53. cursor, limit: Math.max(1, Math.min(100, limit)), archived,
  54. })))
  55. const source = record(payload)
  56. return {
  57. items: values(source.items ?? source.topics ?? payload).map(normalizeGroupTopic).filter(topic => topic.id && topic.roomId),
  58. nextCursor: typeof source.nextCursor === 'string' ? source.nextCursor : typeof source.next_cursor === 'string' ? source.next_cursor : null,
  59. }
  60. }
  61. export async function getPinnedGroupTopics(limit = 100): Promise<GroupTopicPage> {
  62. const payload = unwrapData(await apiRequest<unknown>(apiUrl(`${BASE}/topics/pinned`, { limit: Math.max(1, Math.min(100, limit)) })))
  63. const source = record(payload)
  64. return { items: values(source.items ?? source.topics ?? payload).map(normalizeGroupTopic).filter(topic => topic.id && topic.roomId), nextCursor: null }
  65. }
  66. export async function updateGroupTopic(roomId: string, topicId: string, input: { title?: string; pinned?: boolean }): Promise<GroupTopicSummary> {
  67. return normalizeGroupTopic(unwrapData(await apiRequest<unknown>(`${BASE}/rooms/${encodeURIComponent(roomId)}/topics/${encodeURIComponent(topicId)}`, {
  68. method: 'PATCH', body: { requestId: requestId(), ...input } as JsonValue,
  69. })))
  70. }
  71. export async function archiveGroupTopic(roomId: string, topicId: string): Promise<GroupTopicSummary> {
  72. return normalizeGroupTopic(unwrapData(await apiRequest<unknown>(`${BASE}/rooms/${encodeURIComponent(roomId)}/topics/${encodeURIComponent(topicId)}`, {
  73. method: 'DELETE', body: { requestId: requestId() } as JsonValue,
  74. })))
  75. }
  76. export async function restoreGroupTopic(roomId: string, topicId: string): Promise<GroupTopicSummary> {
  77. return normalizeGroupTopic(unwrapData(await apiRequest<unknown>(`${BASE}/rooms/${encodeURIComponent(roomId)}/topics/${encodeURIComponent(topicId)}/restore`, {
  78. method: 'POST', body: { requestId: requestId() } as JsonValue,
  79. })))
  80. }
  81. export async function markGroupTopicRead(roomId: string, topicId: string, throughSeq: number): Promise<void> {
  82. await apiRequest(`${BASE}/rooms/${encodeURIComponent(roomId)}/topics/${encodeURIComponent(topicId)}/read`, {
  83. method: 'PATCH', body: { requestId: requestId(), throughSeq } as JsonValue,
  84. })
  85. }
  86. export interface AgentSeed {
  87. profile: string
  88. nodeId?: string
  89. nodeLabel?: string
  90. displayName: string
  91. description?: string
  92. replyWithoutMention?: boolean
  93. isHost?: boolean
  94. model?: string | null
  95. provider?: string | null
  96. reasoningEffort?: string | null
  97. fastMode?: boolean | null
  98. }
  99. export async function createGroupRoom(input: { name: string; cwd?: string; instructions?: string; agents: AgentSeed[]; maxReplyRounds?: number; orchestrationMode?: 'free' | 'host' }): Promise<GroupRoomDetail> {
  100. return normalizeGroupRoomDetail(unwrapData(await apiRequest<unknown>(`${BASE}/rooms`, {
  101. method: 'POST', body: { requestId: requestId(), cwd: '', maxReplyRounds: 3, ...input } as unknown as JsonValue,
  102. })))
  103. }
  104. export async function updateGroupRoom(roomId: string, input: { name?: string; cwd?: string; instructions?: string; maxReplyRounds?: number; orchestrationMode?: 'free' | 'host' }): Promise<GroupRoomDetail> {
  105. return normalizeGroupRoomDetail(unwrapData(await apiRequest<unknown>(`${BASE}/rooms/${encodeURIComponent(roomId)}`, {
  106. method: 'PATCH', body: { requestId: requestId(), ...input } as JsonValue,
  107. })))
  108. }
  109. export async function archiveGroupRoom(roomId: string): Promise<GroupRoomDetail> {
  110. return normalizeGroupRoomDetail(unwrapData(await apiRequest<unknown>(`${BASE}/rooms/${encodeURIComponent(roomId)}`, {
  111. method: 'DELETE', body: { requestId: requestId() },
  112. })))
  113. }
  114. export async function restoreGroupRoom(roomId: string): Promise<GroupRoomDetail> {
  115. return normalizeGroupRoomDetail(unwrapData(await apiRequest<unknown>(`${BASE}/rooms/${encodeURIComponent(roomId)}/restore`, {
  116. method: 'POST', body: { requestId: requestId() } as JsonValue,
  117. })))
  118. }
  119. export async function addGroupAgent(roomId: string, input: AgentSeed): Promise<GroupAgent> {
  120. return normalizeGroupAgent(unwrapData(await apiRequest<unknown>(`${BASE}/rooms/${encodeURIComponent(roomId)}/agents`, {
  121. method: 'POST', body: { requestId: requestId(), description: '', replyWithoutMention: false, ...input } as JsonValue,
  122. })))
  123. }
  124. export async function updateGroupAgent(roomId: string, agentId: string, input: Partial<Omit<AgentSeed, 'profile'>> & { enabled?: boolean }): Promise<GroupAgent> {
  125. return normalizeGroupAgent(unwrapData(await apiRequest<unknown>(`${BASE}/rooms/${encodeURIComponent(roomId)}/agents/${encodeURIComponent(agentId)}`, {
  126. method: 'PATCH', body: { requestId: requestId(), ...input } as JsonValue,
  127. })))
  128. }
  129. export async function removeGroupAgent(roomId: string, agentId: string): Promise<GroupAgent> {
  130. return normalizeGroupAgent(unwrapData(await apiRequest<unknown>(`${BASE}/rooms/${encodeURIComponent(roomId)}/agents/${encodeURIComponent(agentId)}`, {
  131. method: 'DELETE', body: { requestId: requestId() },
  132. })))
  133. }
  134. export async function interruptGroupAgent(roomId: string, agentId: string): Promise<unknown> {
  135. return unwrapData(await apiRequest(`${BASE}/rooms/${encodeURIComponent(roomId)}/agents/${encodeURIComponent(agentId)}/interrupt`, {
  136. method: 'POST', body: { requestId: requestId() },
  137. }))
  138. }
  139. export async function getGroupMessages(roomId: string, options: { topicId?: string; beforeSeq?: number; afterSeq?: number; limit?: number } = {}): Promise<GroupMessagePage> {
  140. if (options.beforeSeq && options.afterSeq) throw new Error('beforeSeq 与 afterSeq 不能同时使用')
  141. const payload = unwrapData(await apiRequest<unknown>(apiUrl(`${BASE}/rooms/${encodeURIComponent(roomId)}/messages`, {
  142. topicId: options.topicId, beforeSeq: options.beforeSeq, afterSeq: options.afterSeq, limit: Math.max(1, Math.min(100, options.limit ?? 100)),
  143. })))
  144. const source = record(payload)
  145. return { items: values(source.items ?? source.messages ?? payload).map(normalizeGroupMessage).filter(message => message.id) }
  146. }
  147. export async function sendGroupMessage(
  148. roomId: string,
  149. content: string,
  150. mentionAgentIds: string[],
  151. clientMessageId: string,
  152. topicId?: string,
  153. uploadIds: string[] = [],
  154. stableRequestId = requestId(),
  155. ): Promise<{ message: ReturnType<typeof normalizeGroupMessage>; runs: GroupRun[] }> {
  156. const payload = record(unwrapData(await apiRequest<unknown>(`${BASE}/rooms/${encodeURIComponent(roomId)}/messages`, {
  157. method: 'POST', body: { requestId: stableRequestId, clientMessageId, topicId, content, mentionAgentIds, uploadIds } as JsonValue,
  158. })))
  159. return { message: normalizeGroupMessage(payload.message), runs: values(payload.runs).map(normalizeGroupRun).filter(run => run.id) }
  160. }
  161. export async function approveGroupInteraction(roomId: string, interactionId: string, choice: 'once' | 'session' | 'always' | 'deny'): Promise<GroupInteraction | unknown> {
  162. const payload = unwrapData(await apiRequest<unknown>(`${BASE}/rooms/${encodeURIComponent(roomId)}/interactions/${encodeURIComponent(interactionId)}/approval`, {
  163. method: 'POST', body: { requestId: requestId(), choice, permanent: choice === 'always' },
  164. }))
  165. const source = record(payload)
  166. return source.interaction ? normalizeGroupInteraction(source.interaction) : payload
  167. }
  168. export async function clarifyGroupInteraction(roomId: string, interactionId: string, response: string): Promise<GroupInteraction | unknown> {
  169. const payload = unwrapData(await apiRequest<unknown>(`${BASE}/rooms/${encodeURIComponent(roomId)}/interactions/${encodeURIComponent(interactionId)}/clarification`, {
  170. method: 'POST', body: { requestId: requestId(), response },
  171. }))
  172. const source = record(payload)
  173. return source.interaction ? normalizeGroupInteraction(source.interaction) : payload
  174. }
  175. export async function uploadGroupFiles(roomId: string, files: File[]): Promise<UploadReference[]> {
  176. const form = new FormData()
  177. form.set('roomId', roomId)
  178. for (const file of files) form.append('files', file, file.name)
  179. const payload = unwrapData(await apiRequest<unknown>('/api/app/group-uploads', {
  180. method: 'POST', body: form, timeoutMs: 120_000,
  181. }))
  182. const source = record(payload)
  183. return values(source.files ?? source.items ?? source.uploads ?? payload).map(value => {
  184. const item = record(value)
  185. return {
  186. id: String(item.id ?? ''), name: String(item.name ?? item.filename ?? ''),
  187. mimeType: String(item.mimeType ?? item.mime_type ?? 'application/octet-stream'),
  188. size: Number(item.size ?? 0), markdown: typeof item.markdown === 'string' ? item.markdown : undefined,
  189. refText: typeof item.refText === 'string' ? item.refText : typeof item.ref_text === 'string' ? item.ref_text : undefined,
  190. }
  191. }).filter(item => item.id && item.name)
  192. }