- 编辑旧消息会删除其后全部消息时,先弹确认告知影响范围 - 有待发送附件时切换会话,先确认再丢弃(避免静默删除已上传文件) - EditableCell 保存成功后提供 6 秒内"撤销"入口(把旧值重新保存), 重新编辑时自动清除 - 导入向导/重新上传显示上传进度条与百分比,不再"假死" - AI 附件上传透传进度给附件列表 - 租赁合同上传显示按钮 loading 与上传百分比
92 lines
3.2 KiB
TypeScript
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`;
|
|
}
|