|
1 | 1 | package conversation |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "encoding/json" |
4 | 5 | "errors" |
| 6 | + "fmt" |
5 | 7 | "net/http" |
6 | 8 | "strconv" |
| 9 | + "time" |
7 | 10 |
|
8 | 11 | appconversation "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/application/conversation" |
9 | 12 | "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/shared/response" |
@@ -187,6 +190,84 @@ func (h *Handler) ExportConversation(c *gin.Context) { |
187 | 190 | response.Success(c, toConversationExportResponse(item)) |
188 | 191 | } |
189 | 192 |
|
| 193 | +type userExportManifest struct { |
| 194 | + Type string `json:"_type"` |
| 195 | + Complete bool `json:"complete"` |
| 196 | + Exported int64 `json:"exported"` |
| 197 | + Failed int `json:"failed"` |
| 198 | + FailedIDs []uint `json:"failedIDs,omitempty"` |
| 199 | + Error string `json:"error,omitempty"` |
| 200 | +} |
| 201 | + |
| 202 | +// ExportAllConversations godoc |
| 203 | +// @Summary 导出当前用户全部对话 |
| 204 | +// @Description 流式导出当前用户全部会话及消息为 NDJSON 文件 |
| 205 | +// @Tags chat |
| 206 | +// @Produce application/x-ndjson |
| 207 | +// @Security BearerAuth |
| 208 | +// @Success 200 {string} string "NDJSON stream" |
| 209 | +// @Failure 500 {object} ErrorDoc |
| 210 | +// @Router /conversations/export [get] |
| 211 | +func (h *Handler) ExportAllConversations(c *gin.Context) { |
| 212 | + userID := middleware.MustUserID(c) |
| 213 | + |
| 214 | + h.recordAudit(c, "export_all_conversations", "conversation", "", map[string]interface{}{"scope": "user"}) |
| 215 | + |
| 216 | + c.Header("Content-Type", "application/x-ndjson") |
| 217 | + c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="my-conversations-%s.jsonl"`, time.Now().UTC().Format("20060102-150405"))) |
| 218 | + c.Header("Cache-Control", "no-store") |
| 219 | + c.Status(http.StatusOK) |
| 220 | + |
| 221 | + const batchSize = 50 |
| 222 | + var lastID uint |
| 223 | + encoder := json.NewEncoder(c.Writer) |
| 224 | + exported := int64(0) |
| 225 | + var failedIDs []uint |
| 226 | + writeManifest := func(complete bool, exportErr string) { |
| 227 | + _ = encoder.Encode(userExportManifest{ |
| 228 | + Type: "export_manifest", |
| 229 | + Complete: complete, |
| 230 | + Exported: exported, |
| 231 | + Failed: len(failedIDs), |
| 232 | + FailedIDs: failedIDs, |
| 233 | + Error: exportErr, |
| 234 | + }) |
| 235 | + c.Writer.Flush() |
| 236 | + } |
| 237 | + |
| 238 | + for { |
| 239 | + if c.Request.Context().Err() != nil { |
| 240 | + return |
| 241 | + } |
| 242 | + conversations, err := h.service.ListUserConversationsAfterID(c.Request.Context(), userID, lastID, batchSize) |
| 243 | + if err != nil { |
| 244 | + writeManifest(false, "failed to list conversations") |
| 245 | + return |
| 246 | + } |
| 247 | + if len(conversations) == 0 { |
| 248 | + break |
| 249 | + } |
| 250 | + for i := range conversations { |
| 251 | + result, err := h.service.ExportUserConversationData(c.Request.Context(), userID, &conversations[i]) |
| 252 | + if err != nil { |
| 253 | + failedIDs = append(failedIDs, conversations[i].ID) |
| 254 | + continue |
| 255 | + } |
| 256 | + if err := encoder.Encode(ToConversationExportResponse(result)); err != nil { |
| 257 | + return |
| 258 | + } |
| 259 | + exported++ |
| 260 | + } |
| 261 | + c.Writer.Flush() |
| 262 | + lastID = conversations[len(conversations)-1].ID |
| 263 | + if len(conversations) < batchSize { |
| 264 | + break |
| 265 | + } |
| 266 | + } |
| 267 | + |
| 268 | + writeManifest(true, "") |
| 269 | +} |
| 270 | + |
190 | 271 | // RenameConversation godoc |
191 | 272 | // @Summary 重命名会话 |
192 | 273 | // @Description 修改指定会话标题 |
|
0 commit comments