mirror of
https://github.com/pionxzh/chatgpt-exporter.git
synced 2026-07-23 09:00:51 -05:00
fix: handle alphanumeric cursor pagination for project conversations
Project conversations API switched from numeric offsets to opaque alphanumeric cursors. The old code discarded the cursor and fell back to offset += limit, so only the first page was ever exported. - Add `cursor?: string | null` to `ApiConversations` interface - Update `projectConversationsApi` to accept `string | number` cursor - Expose the API-returned cursor in `fetchProjectConversations` - Use cursor-based advancement in `fetchAllConversations` for project requests, with numeric offset fallback for regular conversations - Add early-exit guard when API returns neither total nor next cursor Closes #341 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -21,3 +21,4 @@ node_modules
|
||||
*.sw?
|
||||
|
||||
tsconfig.tsbuildinfo
|
||||
.claude
|
||||
|
||||
23
src/api.ts
23
src/api.ts
@@ -301,6 +301,7 @@ export interface ApiConversations {
|
||||
limit: number
|
||||
offset: number
|
||||
total: number | null
|
||||
cursor?: string | null
|
||||
}
|
||||
|
||||
/// "Gizmos" are what OpenAI calls "projects" or other GPTs in the UI
|
||||
@@ -402,7 +403,7 @@ const conversationApi = (id: string) => urlcat(apiUrl, '/conversation/:id', { id
|
||||
const conversationsApi = (offset: number, limit: number) => urlcat(apiUrl, '/conversations', { offset, limit })
|
||||
const fileDownloadApi = (id: string) => urlcat(apiUrl, '/files/download/:id', { id, post_id: '', inline: false })
|
||||
const projectsApi = (cursor: number | null) => urlcat(apiUrl, '/gizmos/snorlax/sidebar', { conversations_per_gizmo: 0, cursor })
|
||||
const projectConversationsApi = (gizmo: string, offset: number, limit: number) => urlcat(apiUrl, '/gizmos/:gizmo/conversations', { gizmo, cursor: offset, limit })
|
||||
const projectConversationsApi = (gizmo: string, cursor: string | number, limit: number) => urlcat(apiUrl, '/gizmos/:gizmo/conversations', { gizmo, cursor, limit })
|
||||
const accountsCheckApi = urlcat(apiUrl, '/accounts/check/v4-2023-04-27')
|
||||
|
||||
export async function getCurrentChatId(): Promise<string> {
|
||||
@@ -534,15 +535,16 @@ async function fetchConversations(offset = 0, limit = 20, project: string | null
|
||||
return fetchApi(url)
|
||||
}
|
||||
|
||||
async function fetchProjectConversations(project: string, offset = 0, limit = 20): Promise<ApiConversations> {
|
||||
const url = projectConversationsApi(project, offset, limit)
|
||||
const { items } = await fetchApi< { items: ApiConversationItem[]; cursor: number | null }>(url)
|
||||
async function fetchProjectConversations(project: string, cursor: string | number = 0, limit = 20): Promise<ApiConversations> {
|
||||
const url = projectConversationsApi(project, cursor, limit)
|
||||
const { items, cursor: nextCursor } = await fetchApi<{ items: ApiConversationItem[]; cursor: string | null }>(url)
|
||||
return {
|
||||
has_missing_conversations: false,
|
||||
items,
|
||||
limit,
|
||||
offset,
|
||||
offset: typeof cursor === 'number' ? cursor : 0,
|
||||
total: null,
|
||||
cursor: nextCursor ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,11 +552,12 @@ export async function fetchAllConversations(project: string | null = null, maxCo
|
||||
const conversations: ApiConversationItem[] = []
|
||||
const limit = project === null ? 100 : 50 // gizmos api uses a smaller limit
|
||||
let offset = 0
|
||||
let cursor: string | number = 0 // project conversations use alphanumeric cursors
|
||||
while (true) {
|
||||
try {
|
||||
const result = project === null
|
||||
? await fetchConversations(offset, limit)
|
||||
: await fetchProjectConversations(project, offset, limit)
|
||||
: await fetchProjectConversations(project, cursor, limit)
|
||||
if (!result.items) {
|
||||
// Handle potential API errors or empty responses
|
||||
console.warn('fetchAllConversations received no items at offset:', offset)
|
||||
@@ -562,11 +565,19 @@ export async function fetchAllConversations(project: string | null = null, maxCo
|
||||
}
|
||||
conversations.push(...result.items)
|
||||
if (result.items.length === 0) break
|
||||
// Stop if the API signals no more pages (no total count and no next cursor)
|
||||
if (result.total == null && result.cursor == null) break
|
||||
// Stop if we've reached the total reported by the API OR the user-defined limit
|
||||
if (result.total !== null && offset + limit >= result.total) break
|
||||
if (conversations.length >= maxConversations) break
|
||||
// Use the alphanumeric cursor for project conversations, fall back to numeric offset otherwise
|
||||
if (result.cursor != null) {
|
||||
cursor = result.cursor
|
||||
}
|
||||
else {
|
||||
offset += limit
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error fetching conversations batch:', error)
|
||||
break
|
||||
|
||||
Reference in New Issue
Block a user