conversationApi.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import { apiUrl, getAgentHeaders } from "@/lib/config";
  2. import {
  3. ConversationDetail,
  4. ConversationSummary,
  5. } from "../types";
  6. export type ConversationListType = "visitor" | "internal";
  7. export type ConversationStatus = "open" | "closed";
  8. export interface ConversationListResponse {
  9. items: ConversationSummary[];
  10. total: number;
  11. page: number;
  12. page_size: number;
  13. has_more: boolean;
  14. total_unread: number;
  15. }
  16. const DEFAULT_PAGE_SIZE = 50;
  17. function normalizeConversationItem(item: ConversationSummary): ConversationSummary {
  18. return {
  19. ...item,
  20. unread_count: item.unread_count ?? 0,
  21. has_participated: item.has_participated ?? false,
  22. };
  23. }
  24. function parseConversationListPayload(data: unknown): ConversationListResponse {
  25. if (Array.isArray(data)) {
  26. const items = data.map((item) =>
  27. normalizeConversationItem(item as ConversationSummary)
  28. );
  29. return {
  30. items,
  31. total: items.length,
  32. page: 1,
  33. page_size: items.length,
  34. has_more: false,
  35. total_unread: items.reduce((sum, c) => sum + (c.unread_count ?? 0), 0),
  36. };
  37. }
  38. if (data && typeof data === "object") {
  39. const raw = data as Record<string, unknown>;
  40. const items = Array.isArray(raw.items)
  41. ? raw.items.map((item) =>
  42. normalizeConversationItem(item as ConversationSummary)
  43. )
  44. : [];
  45. return {
  46. items,
  47. total: Number(raw.total ?? items.length),
  48. page: Number(raw.page ?? 1),
  49. page_size: Number(raw.page_size ?? items.length),
  50. has_more: Boolean(raw.has_more),
  51. total_unread: Number(raw.total_unread ?? 0),
  52. };
  53. }
  54. return {
  55. items: [],
  56. total: 0,
  57. page: 1,
  58. page_size: DEFAULT_PAGE_SIZE,
  59. has_more: false,
  60. total_unread: 0,
  61. };
  62. }
  63. export async function fetchConversations(
  64. userId?: number,
  65. opts?: {
  66. type?: ConversationListType;
  67. status?: ConversationStatus;
  68. page?: number;
  69. page_size?: number;
  70. }
  71. ): Promise<ConversationListResponse> {
  72. const params = new URLSearchParams();
  73. if (userId) params.set("user_id", String(userId));
  74. if (opts?.type) params.set("type", opts.type);
  75. if (opts?.status) params.set("status", opts.status);
  76. params.set("page", String(opts?.page ?? 1));
  77. params.set("page_size", String(opts?.page_size ?? DEFAULT_PAGE_SIZE));
  78. const url = `${apiUrl("/conversations")}?${params.toString()}`;
  79. const res = await fetch(url, { cache: "no-store", headers: getAgentHeaders() });
  80. if (!res.ok) {
  81. throw new Error("获取对话列表失败");
  82. }
  83. const data = await res.json();
  84. return parseConversationListPayload(data);
  85. }
  86. /** 创建一条内部对话(知识库测试),返回新对话 ID */
  87. export async function initInternalConversation(userId: number): Promise<{ conversation_id: number }> {
  88. const res = await fetch(`${apiUrl("/conversations/internal")}?user_id=${userId}`, {
  89. method: "POST",
  90. headers: { "Content-Type": "application/json", ...getAgentHeaders() },
  91. });
  92. if (!res.ok) {
  93. const err = await res.json().catch(() => ({}));
  94. throw new Error((err as { error?: string }).error || "创建内部对话失败");
  95. }
  96. const data = await res.json();
  97. return { conversation_id: data.conversation_id };
  98. }
  99. export async function searchConversations(
  100. query: string,
  101. userId?: number,
  102. opts?: { status?: ConversationStatus; type?: ConversationListType }
  103. ): Promise<ConversationSummary[]> {
  104. const status = opts?.status ?? "open";
  105. const listType = opts?.type ?? "visitor";
  106. const params = new URLSearchParams({
  107. q: query,
  108. status,
  109. type: listType,
  110. });
  111. if (userId) {
  112. params.set("user_id", String(userId));
  113. }
  114. const url = `${apiUrl("/conversations/search")}?${params.toString()}`;
  115. const res = await fetch(url, {
  116. cache: "no-store",
  117. headers: getAgentHeaders(),
  118. });
  119. if (!res.ok) {
  120. throw new Error("搜索对话失败");
  121. }
  122. const data = await res.json();
  123. if (!Array.isArray(data)) {
  124. return [];
  125. }
  126. return data.map((item) => ({
  127. ...item,
  128. unread_count: item.unread_count ?? 0,
  129. has_participated: item.has_participated ?? false,
  130. }));
  131. }
  132. export async function fetchConversationDetail(
  133. conversationId: number,
  134. userId?: number
  135. ): Promise<ConversationDetail | null> {
  136. const url = userId
  137. ? `${apiUrl(`/conversations/${conversationId}`)}?user_id=${userId}`
  138. : apiUrl(`/conversations/${conversationId}`);
  139. const res = await fetch(url, { cache: "no-store", headers: getAgentHeaders() });
  140. if (!res.ok) {
  141. return null;
  142. }
  143. const data = await res.json();
  144. return {
  145. ...data,
  146. unread_count: data.unread_count ?? 0,
  147. };
  148. }
  149. /** 关闭会话(进入历史/归档)。访客再次发消息会自动 reopen(B 方案)。 */
  150. export async function closeConversation(conversationId: number): Promise<void> {
  151. const res = await fetch(apiUrl(`/conversations/${conversationId}/close`), {
  152. method: "POST",
  153. headers: getAgentHeaders(),
  154. });
  155. if (!res.ok) {
  156. const j = await res.json().catch(() => ({}));
  157. throw new Error((j as { error?: string }).error || `关闭会话失败(${res.status})`);
  158. }
  159. }
  160. export interface UpdateConversationContactPayload {
  161. email?: string;
  162. phone?: string;
  163. notes?: string;
  164. }
  165. export interface UpdateConversationContactResult {
  166. email: string;
  167. phone: string;
  168. notes: string;
  169. }
  170. export async function updateConversationContact(
  171. conversationId: number,
  172. payload: UpdateConversationContactPayload
  173. ): Promise<UpdateConversationContactResult> {
  174. const res = await fetch(
  175. apiUrl(`/conversations/${conversationId}/contact`),
  176. {
  177. method: "PUT",
  178. headers: { "Content-Type": "application/json", ...getAgentHeaders() },
  179. body: JSON.stringify(payload),
  180. }
  181. );
  182. if (!res.ok) {
  183. throw new Error("更新访客联系信息失败");
  184. }
  185. const data = await res.json();
  186. return {
  187. email: data.email ?? "",
  188. phone: data.phone ?? "",
  189. notes: data.notes ?? "",
  190. };
  191. }
  192. export interface AutoCloseConversationDaysPolicy {
  193. effective_days: number;
  194. env_days: number;
  195. persisted_in_database: boolean;
  196. }
  197. export async function fetchAutoCloseConversationDaysPolicy(): Promise<AutoCloseConversationDaysPolicy> {
  198. const res = await fetch(apiUrl("/conversations/maintenance/auto-close-days"), {
  199. cache: "no-store",
  200. headers: getAgentHeaders(),
  201. });
  202. if (!res.ok) {
  203. throw new Error("获取会话维护配置失败");
  204. }
  205. return res.json();
  206. }
  207. export async function putAutoCloseConversationDaysPolicy(
  208. inactiveDays: number
  209. ): Promise<{ effective_days: number }> {
  210. const res = await fetch(apiUrl("/conversations/maintenance/auto-close-days"), {
  211. method: "PUT",
  212. headers: { "Content-Type": "application/json", ...getAgentHeaders() },
  213. body: JSON.stringify({ inactive_days: inactiveDays }),
  214. });
  215. if (!res.ok) {
  216. const err = await res.json().catch(() => ({}));
  217. throw new Error((err as { error?: string }).error || "保存会话维护配置失败");
  218. }
  219. return res.json();
  220. }
  221. export async function deleteAutoCloseConversationDaysPolicy(): Promise<{ effective_days: number }> {
  222. const res = await fetch(apiUrl("/conversations/maintenance/auto-close-days"), {
  223. method: "DELETE",
  224. headers: getAgentHeaders(),
  225. });
  226. if (!res.ok) {
  227. const err = await res.json().catch(() => ({}));
  228. throw new Error((err as { error?: string }).error || "恢复默认配置失败");
  229. }
  230. return res.json();
  231. }