Files
gongxue-base/apps/admin/src/components/AiChat/api.ts
wangziqi 8dc72d6e1b fix(admin): 编辑消息/切换会话保护、行内编辑撤销与上传进度反馈
- 编辑旧消息会删除其后全部消息时,先弹确认告知影响范围
- 有待发送附件时切换会话,先确认再丢弃(避免静默删除已上传文件)
- EditableCell 保存成功后提供 6 秒内"撤销"入口(把旧值重新保存),
  重新编辑时自动清除
- 导入向导/重新上传显示上传进度条与百分比,不再"假死"
- AI 附件上传透传进度给附件列表
- 租赁合同上传显示按钮 loading 与上传百分比
2026-08-07 17:38:21 +08:00

92 lines
3.2 KiB
TypeScript

import api from '../../api';
import type {
AiApiResponse,
AiAttachment,
AiConversation,
AiMessagePage,
AiReviewSchema,
AiReviewSection,
AiReviewSectionType,
AiSkill,
} from './types';
const basePath = '/ai/chat/conversations';
export const aiChatApi = {
listSkills: async () => (await api.get<AiApiResponse<AiSkill[]>>('/ai/chat/skills')).data,
listConversations: async () => (await api.get<AiApiResponse<AiConversation[]>>(basePath)).data,
createConversation: async (input?: { title?: string; lockedSkillKey?: string | null }) =>
(await api.post<AiApiResponse<AiConversation>>(basePath, input ?? {})).data,
updateConversation: async (
id: number,
input: { title?: string; lockedSkillKey?: string | null },
) => (await api.patch<AiApiResponse<AiConversation>>(`${basePath}/${id}`, input)).data,
deleteConversation: (id: number) => api.delete<void>(`${basePath}/${id}`),
deleteAllConversations: async () =>
(await api.delete<{ success: boolean; data: { deleted: number } }>(basePath)).data,
deleteMessage: async (conversationId: number, messageId: number) =>
(
await api.delete<AiApiResponse<{ deletedIds: number[] }>>(
`${basePath}/${conversationId}/messages/${messageId}`,
)
).data,
uploadAttachment: async (
file: File,
onProgress?: (percent: number) => void,
): Promise<AiAttachment> => {
const form = new FormData();
form.append('file', file);
return (
await api.post<AiApiResponse<AiAttachment>>('/ai/chat/attachments', form, {
timeout: 120_000,
onUploadProgress: (event) => {
if (!onProgress || !event.total) return;
onProgress(Math.min(Math.round((event.loaded / event.total) * 100), 100));
},
})
).data;
},
deleteAttachment: (id: number) => api.delete<void>(`/ai/chat/attachments/${id}`),
confirmReviewStep: async (
reviewId: string,
sectionKey: AiReviewSection['key'],
): Promise<AiReviewSchema> =>
(
await api.post<AiApiResponse<AiReviewSchema>>(
`/ai/chat/reviews/${reviewId}/steps/${sectionKey}/confirm`,
)
).data,
confirmReviewGroup: async (
reviewId: string,
type: AiReviewSectionType,
): Promise<AiReviewSchema> =>
(
await api.post<AiApiResponse<AiReviewSchema>>(
`/ai/chat/reviews/${reviewId}/types/${type}/confirm`,
)
).data,
listMessages: async (id: number): Promise<AiMessagePage> => {
const first = (
await api.get<AiApiResponse<AiMessagePage>>(`${basePath}/${id}/messages`, {
params: { page: 1, limit: 100 },
})
).data;
const pageCount = Math.ceil(first.total / first.limit);
if (pageCount <= 1) return first;
const rest = await Promise.all(
Array.from({ length: pageCount - 1 }, (_, index) =>
api
.get<AiApiResponse<AiMessagePage>>(`${basePath}/${id}/messages`, {
params: { page: index + 2, limit: first.limit },
})
.then((response) => response.data),
),
);
return { ...first, items: [first, ...rest].flatMap((page) => page.items) };
},
};
export function conversationStreamUrl(id: number): string {
return `/api${basePath}/${id}/stream`;
}