콘텐츠로 이동

데이터 허브

데이터 허브는 데스크톱 데이터 페이지 뒤에 있는 문서 코퍼스입니다. 문서마다 변경되지 않는 원본, 변환된 마크다운 본문, 주석 카드가 있고, 그 위에 컬렉션·태그·검색 프리셋·인제스트 파이프라인·생성 위키가 올라갑니다. 관리 API는 이 전부를 노출합니다. /data 아래 66개 엔드포인트가 있으며 Tauri 명령과 완전한 대응 관계를 이룹니다.

이 문서의 모든 엔드포인트는 관리 API 기본 주소(기본값 http://127.0.0.1:8001/api/v1)에서 접근할 수 있고, /api/docs의 Swagger UI에서 Data Documents, Data Organization, Data Retrieval, Data Pipelines, Data Maintenance, Data Wiki 태그로 확인할 수 있습니다. aigo data 명령 그룹이 같은 엔드포인트를 감싸며, 플래그는 CLI 레퍼런스에 있습니다.

헤드리스에서의 동작

이 표면 위에 무언가를 만들기 전에 알아 두어야 할 것이 세 가지 있습니다.

코퍼스 전체가 헤드리스에서 동작합니다. aigo-server는 데스크톱 앱이 설치하는 것과 같은 공유 이벤트 이미터로 데이터 허브를 초기화합니다. 따라서 인제스트, 변환, 검색, 리트리벌, 폴더 가져오기, 감시 스캔, 임베딩, 유지보수, 위키가 모두 헤드리스 서버에서 실행되고, 아래의 다섯 data:* 이벤트도 데스크톱 웹뷰가 아니라 SSE 스트림으로 전달됩니다. 이 계열에는 데스크톱 전용 엔드포인트가 없습니다.

경로는 API 호스트의 경로입니다. POST /data/folder-import, POST /data/folder-import-runs, POST /data/watch-folderspath를 받아 서버 자신의 파일 시스템에서 해석합니다. 원격 aigo-server라면 그것은 호출자의 디스크가 아니라 서버의 디스크이고, 내 워크스테이션에 있는 경로는 대개 거기에 없습니다. restrictToPermittedFolders는 루트를 cowork 허용 폴더 안으로 좁히지만 기본값은 false입니다. 데스크톱 대화상자에서 폴더를 고르는 행위 자체가 동의이고, 사전 등록을 요구하면 첫 가져오기가 아예 불가능해지기 때문입니다. 이 필드는 FolderImportRequest의 것이라 폴더 가져오기 라우트 두 개에만 닿습니다. CreateWatchFolderRequest에는 같은 필드가 없고 알 수 없는 필드를 거부하지도 않으므로, POST /data/watch-folders로 보내면 거부되지 않고 버려집니다.

모델이 필요한 작업은 실패가 아니라 상태로 알립니다. 요약 초안, 위키 초안, 임베딩 실행, 하이브리드 리랭크는 모두 추론 라우터를 통해 모델이 필요합니다. 사용할 모델이 없어도 요청 자체는 성공합니다. POST /data/documents/{id}/generate-summary는 이유를 담은 실패 상태 잡과 함께 202로 응답하고, GET /data/wiki/statushasModel: false를 알리며, GET /data/search는 렉시컬 인덱스만으로 답합니다. 클라이언트는 이를 렌더링해야 하고, 전송 오류로 보고 재시도해서는 안 됩니다.

인증과 스코프

인증 방식은 관리 API의 나머지와 같습니다. X-API-Key 헤더, Authorization: Bearer 토큰, 또는 POST /api/v1/auth/login으로 받은 aigo_session 쿠키를 사용합니다.

액세스 키는 스코프를 가지며, 이 표면은 두 가지를 씁니다.

  • 모든 GETdata_read.
  • 모든 POST, PUT, DELETEdata_write. 예외가 둘 있습니다.

예외 둘은 아무것도 바꾸지 않으면서 의도적으로 data_readPOST 경로입니다. POST /data/grounding-contextGET /memory/context와 똑같이 리트리벌 블록을 만들 뿐이고, 남기는 lastReferencedAt 각인은 내용 변경이 아니라 읽기 경로의 사용 카운터입니다. POST /data/citations/export는 카드 메타데이터를 참고문헌으로 렌더링만 합니다. 둘이 POST인 이유는 페이로드(사용자 메시지 전체, 또는 문서 ID 목록)가 URL에 들어갈 것이 아니기 때문입니다. 권위 있는 표는 src-tauri/crates/aigo-rest/src/route_scope.rsROUTE_MANIFEST이며, 아래 표의 스코프 열은 거기에서 그대로 옮긴 값입니다.

스코프가 걸린 데이터 허브 스트림은 SSE와 WebSocket 전송을 제공합니다. GET /data/eventsGET /data/wsdata:*만 실어 나르며 data_read를 요구하고, 전역 /events 쌍은 모든 도메인의 이벤트를 실어 나르며 admin을 요구합니다. 이슈 #5040 이후로는 데스크톱 앱이 내장 관리 API를 함께 띄운 경우에도 양쪽 모두에 실립니다. 데스크톱의 모든 하위 시스템이 공유 싱크 하나로 이벤트를 발행하고, 그 서버가 떠 있는 동안 자신의 이벤트 버스를 두 번째 표면으로 붙이기 때문입니다.

관리형 설치에서는 관리자가 features.hiddenPages/data 페이지 ID를 넣어 데이터 페이지를 숨길 수 있습니다. 이 게이트는 /data, /memory, /text-creations, /artifact-creations 경로 접두사와 대응하는 Tauri 명령까지 함께 막으므로, 페이지를 숨기면 API도 거부됩니다. 접두사가 세 개 더 붙는 이유는 그 표면들의 소유자가 데이터 페이지이기 때문입니다. 별도의 /memory 페이지를 숨기면 그 페이지만 사라지고 데이터 페이지의 게이트는 약해지지 않습니다.

리소스 모델

문서

문서는 이 표면 전체가 올라앉은 세 겹입니다. 업로드하거나 받아 온 그대로의 변경되지 않는 원본, 변환된 마크다운 본문, 주석 카드입니다. ID는 UUID이고, handle은 짧은 숫자 라벨(D42)로 카드 안의 [[handle]] 링크가 가리키는 값이자 GET /data/documents/by-handle/{handle}이 해석하는 값입니다. 문서는 kind, source, status, 중복 판정에 쓰는 콘텐츠 해시, 컬렉션 소속, 태그, 그리고 타임스탬프를 가집니다. 타임스탬프에는 그라운딩이 문서를 포함할 때 찍는 lastReferencedAt도 있습니다.

본문과 카드

본문(GET /data/documents/{id}/body)은 변환된 마크다운이고 그 외에는 아무것도 아닙니다. 카드(GET /data/documents/{id}/card)는 주석 계층입니다. 요약, 핵심 포인트, 메모, 그리고 인용 내보내기가 읽는 서지 필드를 담는 자유 형식 metadata 객체로 이루어집니다. summaryGeneratedkeyPointsGenerated는 그 필드를 모델이 썼는지 알려 주며, 이후 초안이 사람이 손대지 않은 곳만 채울 수 있게 하는 근거입니다.

리비전

카드 쓰기와 본문 재변환은 매번 리비전을 남깁니다. GET /data/documents/{id}/revisions가 최신순으로 나열하고, 각각 revertible 플래그를 가집니다. 본문 리비전은 카드 스냅숏이 없으므로 POST /data/documents/{id}/revisions/{revision_id}/revert가 거부합니다. 되돌리기는 카드를 복원할 뿐 본문은 복원하지 않습니다.

컬렉션과 태그

컬렉션은 설명, 활성 문서 수, 그리고 sensitive 플래그를 가진 이름 있는 묶음입니다. 민감 표시가 된 컬렉션은 암묵적 그라운딩과 명시하지 않은 도구 읽기에서 제외됩니다. 컬렉션을 삭제하면 소속 문서는 연결만 끊기고 삭제되지 않습니다. 태그는 자유 형식이고 자체 수명 주기가 없습니다. GET /data/tags는 살아 있는 문서가 최소 하나 달고 있는 태그만 나열합니다.

청크와 검색 프리셋

청크는 리트리벌 단위입니다. 각 청크는 순번, 헤딩 경로, 본문 텍스트, 토큰 수, 본문 안의 UTF-16 문자 오프셋, 그리고 이미 벡터를 가진 임베딩 모델 목록을 담습니다. 검색 프리셋은 리트리벌이 동작하는 범위와 예산입니다. 컬렉션 목록(비어 있으면 전체이지 없음이 아닙니다), lexical 또는 hybridmode, include·exclude·prefer 중 하나인 wikiPages 정책, topK, tokenBudget, 그리고 선택적 임베딩 모델로 구성됩니다. 프리셋 하나는 내장 기본값이며 삭제할 수 없습니다.

인제스트 잡

비동기 작업 단위는 모두 인제스트 잡입니다. 변환, 요약 초안, 위키 작업이며 kind(ingest, summary, wiki)로 구분합니다. 잡은 status(queued, converting, indexing, completed, failed, duplicate), 소수 progress, 만들었거나 대상으로 삼은 문서, 실패했을 때의 error, 그리고 "끝났지만 할 일이 없었다"를 담는 message를 가집니다. errormessage는 별개 필드이며 동시에 설정될 수 있어, 치명적 실패로 멈춘 위키 실행은 무엇을 썼는지와 왜 멈췄는지를 함께 보고합니다.

잡에는 시도 횟수 카운터가 없습니다. 재시도는 POST /data/jobs/{id}/retry가 주도하며, 입력이 변경되지 않는 원본이라 멱등합니다. 잡이 몇 번 실행됐는지를 알려 주는 필드는 와이어에 존재하지 않습니다.

폴더 가져오기 실행

실행은 영속적이고 재개 가능하며 배치로 나뉜 폴더 가져오기입니다. 상태는 scanning, running, paused, completed, completed_with_errors, cancelled, interrupted 중 하나이고(와이어 값은 snake_case입니다), 결과별 카운터(대기, 완료, 중복, 실패, 건너뜀, 거부), 현재 배치, 실패와 건너뛴 항목의 제한된 표본, 상한에서 멈췄을 때의 moreFiles, 그리고 resume이 거부하는 절대 안전 상한에 걸렸을 때의 resumeBlocked를 가집니다.

감시 폴더와 유지보수 일정

감시 폴더는 유지보수 일정이 다시 스캔하는 디렉터리입니다. 자체 재귀·숨김 파일 설정, 가져온 문서에 적용할 태그와 컬렉션, 활성화 플래그, 연속 실패 횟수, 백오프를 위한 다음 시도 시각을 가집니다. 유지보수 일정에는 cron으로 도는 독립 작업이 네 개(감시 스캔, URL 재수집, 휴지통 비우기, 위키 갱신) 있고, 각각 활성화 플래그·cron 식·마지막 실행 시각을 가지며, 여기에 각 작업이 필요로 하는 수치 설정과 cron 식을 해석할 시간대가 더해집니다. 마지막 실행 시각 네 개는 러너의 소유이며 쓸 수 없습니다.

위키 페이지

위키 페이지는 source: "wiki"를 가진 평범한 문서로, 위키 파이프라인이 코퍼스의 클러스터에서 생성합니다. GET /data/wiki/pages는 페이지마다 유형, 신선도, 인용 수, 역링크 수를 담은 요약을 돌려줍니다. 호출자가 source=wiki를 명시하지 않는 한 위키 페이지는 일반 문서 목록에서 제외되며, 그것이 생성된 페이지를 라이브러리 화면 밖에 두는 방식입니다.

와이어 열거 값

모두 와이어에서 snake_case이며, 철자가 틀리면 보정되지 않고 거부됩니다.

  • DocumentStatus: processing, ready, warning, failed, trashed.
  • DataSource: upload, url, folder, chat, agent, wiki.
  • DocumentKind: markdown, text, html, pdf, docx, spreadsheet, code, presentation, workbook, word_processing, ebook, other.
  • IngestPhase: queued, converting, indexing, completed, failed, duplicate.
  • DataJobKind: ingest, summary, wiki.
  • 리트리벌 mode: lexical, hybrid. wikiPages: include, exclude, prefer.
  • 인용 format: bibtex, csl_json.

엔드포인트

아래 경로는 /api/v1 접두사를 생략했습니다. {id}는 주변 경로가 달리 말하지 않는 한 문서 ID입니다.

문서

메서드 경로 스코프 본문 또는 쿼리 반환
GET /data/documents data_read ?status=&source=&collectionId=&tag=&includeTrashed=&limit=&offset= Document[]
POST /data/documents data_write IngestDocumentRequest Document (201)
GET /data/document-counts data_read - DocumentLifecycleCounts
POST /data/documents/bulk-trash data_write BulkTrashDocumentsRequest BulkDocumentLifecycleResult
POST /data/documents/bulk-restore data_write BulkRestoreDocumentsRequest BulkDocumentLifecycleResult
POST /data/documents/bulk-organize data_write BulkOrganizeDocumentsRequest BulkDocumentLifecycleResult
GET /data/documents/{id} data_read - Document
PUT /data/documents/{id} data_write UpdateDocumentRequest Document
DELETE /data/documents/{id} data_write - Document (휴지통으로 이동)
GET /data/documents/{id}/body data_read - DocumentBody
GET /data/documents/{id}/card data_read - DocumentCard
PUT /data/documents/{id}/card data_write UpdateCardRequest DocumentCard
POST /data/documents/{id}/restore data_write - Document
POST /data/documents/{id}/reconvert data_write - Document
POST /data/documents/{id}/generate-summary data_write GenerateSummaryRequest IngestJob (202)
GET /data/documents/{id}/related data_read - RelatedDocuments
GET /data/documents/{id}/revisions data_read - DocumentRevision[]
POST /data/documents/{id}/revisions/{revision_id}/revert data_write - DocumentCard
GET /data/search data_read ?query=&collectionId=&tag=&limit=&presetId= SearchHit[]
GET /data/documents/by-handle/{handle} data_read - Document

DELETE /data/documents/{id}는 소프트 삭제입니다. 휴지통으로 옮긴 문서를 돌려주고, POST /data/documents/{id}/restore가 되살립니다. 영구 삭제는 POST /data/trash/purge 하나뿐입니다. PUT /data/documents/{id}는 태그 집합과 컬렉션 집합을 통째로 교체하므로 문서가 유지해야 할 값을 모두 적어야 하고, 증분 형태는 POST /data/documents/bulk-organize입니다.

PUT /data/documents/{id}/card는 선택적 expectedUpdatedAt을 받습니다. 이 값이 있으면 저장된 카드가 여전히 그 타임스탬프를 가질 때만 쓰기가 진행되므로, 동시 편집은 덮어쓰이지 않고 보고됩니다. 거부는 메시지에 DATA_CARD_CONFLICT가 들어간 400입니다. 이 엔드포인트는 일반 검증 실패에도 400을 쓰므로, 상태 코드가 아니라 이 토큰으로 판별하세요.

컬렉션과 태그

메서드 경로 스코프 본문 또는 쿼리 반환
GET /data/collections data_read - Collection[]
POST /data/collections data_write CreateCollectionRequest Collection (201)
PUT /data/collections/{id} data_write UpdateCollectionRequest Collection
DELETE /data/collections/{id} data_write - DeleteCollectionResult
POST /data/collections/{id}/generate-summaries data_write GenerateSummaryRequest CollectionSummaryBatch (202)
GET /data/tags data_read - TagSummary[]

POST /data/collections/{id}/generate-summaries는 요약이 없는 컬렉션 문서에 대해 한 라운드만 초안을 만듭니다. 응답에 nextOffset이 있으니, 스캔 창보다 큰 컬렉션은 그 값을 startOffset으로 되먹여 소진하면 됩니다. 이 한계는 방금 초안이 실패한 문서가 곧바로 다시 잡히는 것도 막아 줍니다.

리트리벌·임베딩·그라운딩·인용

메서드 경로 스코프 본문 또는 쿼리 반환
GET /data/documents/{id}/chunks data_read - DocumentChunk[]
GET /data/presets data_read - RetrievalPreset[]
POST /data/presets data_write CreateRetrievalPresetRequest RetrievalPreset (201)
PUT /data/presets/{id} data_write UpdateRetrievalPresetRequest RetrievalPreset
DELETE /data/presets/{id} data_write - { deleted }
POST /data/embeddings/run data_write StartEmbeddingRunRequest EmbeddingRunStatus
GET /data/embeddings/status data_read ?presetId=&model= EmbeddingRunStatus
POST /data/embeddings/cancel data_write - EmbeddingRunStatus
POST /data/grounding-context data_read GroundingContextRequest GroundingContext
POST /data/citations/export data_read ExportCitationsRequest CitationExport

저장된 프리셋이 없는 presetIdGET /data/search, POST /data/embeddings/run, GET /data/embeddings/status에서 404입니다. 알 수 없는 ID에 대해 내장 프리셋으로 폴백하지 않습니다. 프리셋의 컬렉션 목록은 프라이버시 경계이고, 조용히 다른 범위로 바꾸면 호출자가 요청한 것보다 넓게 리트리벌하게 되기 때문입니다.

임베딩 진행은 이벤트이면서 폴링 대상입니다. data:embedding-progress는 상태 엔드포인트가 돌려주는 것과 같은 EmbeddingRunStatus를 싣고 있어, 이벤트를 놓치고 폴링한 클라이언트도 동일한 값을 봅니다. 취소해도 이미 쓴 벡터는 유지되고, 이후 실행이 그 지점부터 이어 갑니다.

인제스트 잡·URL 인제스트·폴더 가져오기

메서드 경로 스코프 본문 또는 쿼리 반환
POST /data/ingest-url data_write IngestUrlRequest IngestJob (202)
POST /data/documents/{id}/refetch data_write - IngestJob (202)
GET /data/jobs data_read - IngestJob[]
POST /data/jobs data_write IngestDocumentRequest IngestJob (202)
POST /data/jobs/{id}/retry data_write - IngestJob
POST /data/backfill data_write - IngestJob[]
POST /data/folder-import data_write FolderImportRequest FolderImportResult
GET /data/folder-import-runs data_read ?limit= FolderImportRun[]
POST /data/folder-import-runs data_write FolderImportRequest FolderImportRun (202)
GET /data/folder-import-runs/{id} data_read - FolderImportRun
POST /data/folder-import-runs/{id}/cancel data_write - FolderImportRun
POST /data/folder-import-runs/{id}/resume data_write - FolderImportRun

POST /data/documentsPOST /data/jobs는 같은 IngestDocumentRequest를 받습니다. 앞의 것은 문서가 만들어진 뒤 그 문서로 답하고, 뒤의 것은 큐에 넣은 잡으로 답합니다. POST /data/ingest-urlhttp://https://만 받고, URL을 4096자로 제한하며, 가져오기를 공용 SSRF 가드에 통과시킵니다.

두 가지 폴더 가져오기 형태는 서로 대체할 수 없습니다. POST /data/folder-import는 요청 안에서 폴더 전체를 훑고 건너뛴 항목을 모두 돌려주며 기록을 남기지 않습니다. POST /data/folder-import-runs는 나열·조회·취소·재개할 수 있는 영속 배치 실행을 시작합니다. 파일 상한에서 멈춘 실행은 moreFiles: true를 보고하고 resume이 멈춘 지점부터 이어 가며, 절대 안전 상한에서 멈춘 실행은 resumeBlocked: true를 보고하고 resume은 이를 거부해 새 가져오기를 하도록 합니다.

유지보수·감시 폴더·휴지통·닥터·지표

메서드 경로 스코프 본문 또는 쿼리 반환
GET /data/maintenance data_read - DataMaintenanceSettings
PUT /data/maintenance data_write UpdateDataMaintenanceSettingsRequest DataMaintenanceSettings
GET /data/watch-folders data_read - WatchFolder[]
POST /data/watch-folders data_write CreateWatchFolderRequest WatchFolder (201)
POST /data/watch-folders/scan data_write - WatchScanSummary
PUT /data/watch-folders/{id} data_write UpdateWatchFolderRequest WatchFolder
DELETE /data/watch-folders/{id} data_write - { deleted }
POST /data/trash/purge data_write PurgeTrashRequest PurgeTrashResult
POST /data/doctor data_write RunDataDoctorRequest DataDoctorReport
GET /data/metrics data_read - DataMetricsSnapshot

감시 폴더의 path는 수정할 수 없습니다. 다른 폴더는 다른 감시이고, 경로를 제자리에서 바꾸면 스캔이 이어 가는 기존 가져오기 집합의 키가 어긋납니다. DELETE /data/watch-folders/{id}DELETE /data/presets/{id}는 없는 ID에 404를 돌려주지 않고 { "deleted": <boolean> }으로 답하므로, 아무것도 지워지지 않았다는 신호는 그 불리언뿐입니다.

POST /data/trash/purge는 이 표면에서 유일한 영구 삭제입니다. 원본, 본문, 카드, 청크, 벡터, 리비전, 링크를 제거하며 PurgeTrashResult가 각각의 개수를 셉니다. 한 번의 호출은 문서 500개까지만 지우고 더 남아 있으면 truncated를 세우므로, 휴지통이 크면 purgeAll이라도 여러 번 호출해야 비워집니다. 세 가지 범위는 조합을 거부하는 대신 우선순위로 해석되므로, documentIds·purgeAll·retentionDays 중 정확히 하나만 보내야 합니다.

POST /data/doctorrepairs가 없으면 보고만 하고, 있으면 복구합니다. 복구 이름은 reindex_search, prune_orphan_rows, quarantine_orphan_files, fail_stuck_jobs, complete_interrupted_purges, resync_frontmatter, backfill_wiki_meta입니다. 이 중 둘은 데이터를 지웁니다. prune_orphan_rows는 소유자가 사라진 파생 행을 제거하고, complete_interrupted_purges는 이미 진행 중이던 영구 삭제를 마무리합니다.

GET /data/metrics는 프로세스 로컬 카운터를 읽으며 개발 빌드에서만 동작합니다. 릴리스 빌드는 측정한 척하는 대신 구조적으로 0인 값과 기록 비활성 플래그로 답합니다.

위키

메서드 경로 스코프 본문 또는 쿼리 반환
POST /data/wiki/build data_write WikiBuildRequest IngestJob (202)
POST /data/wiki/update data_write WikiRefreshRequest IngestJob (202)
POST /data/wiki/continue data_write WikiBuildRequest IngestJob (202)
POST /data/wiki/pages/{id}/refresh data_write WikiRefreshRequest IngestJob (202)
GET /data/wiki/pages data_read - WikiPageSummary[]
GET /data/wiki/status data_read - WikiStatus
GET /data/wiki/pages/{id}/related data_read - RelatedDocuments

위키를 바꾸는 모든 동사는 잡을 큐에 넣고 그 잡으로 답합니다. 페이지 작성이 모델 호출이고, 로컬 모델에서 코퍼스 빌드는 기계를 몇 분 동안 붙드는 일이기 때문입니다. 같은 범위에서 이미 실행 중인 작업을 다시 요청해도 오류가 아닙니다. 응답은 이미 진행 중인 잡이므로, 재시도는 두 번째 빌드가 아니라 멱등한 동작입니다. 네 동사의 본문은 모두 선택이고, 두 요청 타입은 서로 바꿔 쓸 수 없습니다. buildcontinuecollectionId가 있는 WikiBuildRequest를 받고, updaterefresh는 그것이 없는 WikiRefreshRequest를 받습니다. build는 범위 안의 계획된 페이지를 모두 다시 쓰고, update는 인용한 출처가 바뀐 페이지만 다시 쓰며, continue는 지난 실행이 페이지 없이 남긴 계획 클러스터를 작성합니다. 모델이 있는지 알려 주는 것은 GET /data/wiki/status이므로, 아무것도 만들지 못할 빌드는 시작하기 전에 알 수 있습니다.

이벤트 스트림

메서드 경로 스코프 본문 또는 쿼리 반환
GET /data/events data_read ?types= text/event-stream
GET /data/ws data_read ?types=, ?since= WebSocket (101)

이벤트를 참고하세요.

요청 타입이 받는 것과 받지 않는 것

이 중 몇 가지는 있을 법한 요청 필드가 실제로는 없는 자리입니다. 여기 있는 타입 대부분이 알 수 없는 필드를 거부하지 않으므로, 그런 필드를 보낸 클라이언트는 오류를 받지 못하고 값만 조용히 버려집니다. 필터가 반영된다고 가정하기 전에 이 절을 확인하세요.

대량 선택은 세 가지 종류이고, 두 엔드포인트는 첫 번째만 받습니다. selection은 태그 붙은 유니온입니다. {"kind": "document_ids", "documentIds": [...]}, {"kind": "all_live"}, {"kind": "matching_query", "query": {...}}. POST /data/documents/bulk-trash는 셋 다 받습니다. POST /data/documents/bulk-restorePOST /data/documents/bulk-organizeall_livematching_query이름을 들어 거부하며, 어느 종류가 거부됐는지 오류가 말해 줍니다. 즉 복원과 정리는 명시적 ID 목록만 받습니다. 복원은 디스크의 카드와 본문에서 각 문서의 검색 투영을 다시 만들기 때문에 무제한 형태가 없고, 정리는 문서별 증분을 적용하기 때문에 코퍼스 전체 선택으로는 뜻이 서지 않습니다.

선택 쿼리는 목록 쿼리가 아닙니다. DocumentSelectionQuerydeny_unknown_fields이고 status, collectionId, tag, includeTrashed만 가집니다. limitoffsetsource도 없습니다. 그래서 목록 쿼리를 그대로 보내면 페이징이 조용히 버려지는 대신 크게 거부되며, 그렇지 않았다면 호출자가 적어 둔 것보다 훨씬 넓은 집합에 작용했을 것입니다. source가 없는 것도 의도입니다. 해석은 언제나 라이브러리의 위키 제외를 적용하므로, 쿼리 범위 선택은 어떻게 적어도 생성된 위키 페이지에 닿지 못합니다.

그라운딩은 필터가 아니라 프리셋을 받습니다. POST /data/grounding-contextGroundingContextRequest를 역직렬화하며, 필드는 query, presetId, memoryBudgetTokens, groundingFraction, recordReference입니다. 리트리벌 요청이 아닙니다. collectionIdtaglimit도 없고, 이 타입은 알 수 없는 필드를 거부하지도 않으므로 그런 값을 보내면 아무 말 없이 버려집니다. 컬렉션 범위는 프리셋에 두세요. memoryBudgetTokens는 호출자가 이번 턴에 메모리 주입으로 쓸 양이고 서버가 이를 공동 예산에서 뺍니다. recordReference: false는 문서에 참조 각인을 남기지 않는 미리보기를 만듭니다.

인용 내보내기는 documentIds가 비어 있지 않으면 collectionId를 무시합니다. ExportCitationsRequest는 둘 다 담지만, 해석기는 지정된 문서를 바로 돌려주고 컬렉션 범위 목록에는 아예 도달하지 않습니다. 둘을 함께 보내도 오류는 아니고 ID 목록만 반영되므로, 둘을 상호 배타로 다루세요. documentIds가 비어 있으면 내보내기는 살아 있는 문서 전체를 대상으로 하고 collectionId로 좁힐 수 있으며, 문서 2000개에서 멈추고 truncated: true를 답합니다.

폴더 가져오기는 거부하지 않고 잘라 맞추며, 영속 실행은 maxFiles를 아예 무시합니다. maxDepth는 두 폴더 가져오기 라우트 모두에서 최대 24로 잘립니다. maxFiles가 1에서 500 사이로 잘리는 것은 POST /data/folder-import뿐입니다. maxFiles: 0을 요청하면 1이 되고 10000을 요청하면 500이 되며, 두 경우 모두 값이 바뀌었다는 말 없이 성공 응답이 옵니다. POST /data/folder-import-runs는 자르는 것이 아니라 갈아치웁니다. 요청을 저장하기 전에, 그리고 resume할 때마다 자체 배치 크기를 넣으므로 거기에 보낸 maxFiles는 가져오기의 상한이 전혀 아니며, 실행은 폴더를 다 소진하거나 누군가 취소할 때까지 배치로 계속됩니다. 실제로 무슨 일이 일어났는지는 실행의 카운터를 읽으세요. 와이어에서 recursive 기본값은 true이고 includeHidden 기본값은 false이며, 후자를 켜도 표준 무시 디렉터리 목록은 그대로 적용됩니다.

두 가지 거부는 400 안에 안정적인 토큰을 담아 옵니다. 공유 라이프사이클을 점유하는 요청은 문서 인제스트·복원·폴더 가져오기·감시 스캔이 실행 중이면 모두 거부하며, 메시지에 DATA_BULK_LIFECYCLE_BUSY가 들어갑니다. 문서를 수집·변환·가져오기·스캔·초안 작성하거나 그 밖에 라이프사이클을 움직이는 엔드포인트는 모두 같은 점유를 요구하므로, 해당 범위는 대량 엔드포인트 셋보다 넓습니다. POST /data/documents, POST /data/jobs, POST /data/ingest-url, POST /data/documents/{id}/refetch, POST /data/backfill, POST /data/folder-import, POST /data/folder-import-runs와 그 resume, refresh 하나가 아니라 위키 동사 네 개 전부, DELETE /data/documents/{id}, POST /data/documents/{id}/restore, /reconvert, /generate-summary, POST /data/collections/{id}/generate-summaries, POST /data/jobs/{id}/retry, POST /data/folder-import-runs/{id}/cancel, POST /data/watch-folders/scan이 모두 여기에 듭니다. 경로 목록이 아니라 토큰으로 분기하세요. 오래된 카드 쓰기에는 DATA_CARD_CONFLICT가 들어갑니다. 둘 다 409가 아닙니다. 이 표면은 두 경우 모두 400을 돌려주므로 상태가 아니라 토큰으로 판별해야 합니다. GET /data/document-countsallLiveTrashBlockednonTerminalJobCount를 보고하므로, 코퍼스 전체 휴지통 이동이 거부될 상황인지 미리 알 수 있습니다.

세 엔드포인트는 보이는 것보다 적게 받습니다. GET /data/jobs에는 쿼리 추출기 자체가 없습니다. 필터도 페이징도 없고, 최신순 최대 200행이며 잘렸다는 플래그도 없으므로 가득 찬 한 페이지는 "최소한 이만큼"을 뜻합니다. POST /data/backfill은 본문을 받지 않습니다. 본문이 없거나 실패한 문서에 대해 재인제스트를 큐에 넣고, 호출당 최대 100개이며 재개 가능하므로 그것들이 끝난 뒤 다시 실행하면 멈춘 지점부터 이어 갑니다. POST /data/wiki/pages/{id}/refreshPOST /data/wiki/update는 둘 다 languagemodel만 있는 WikiRefreshRequest를 받고 범위 필드가 없습니다. refresh는 경로의 페이지 ID가 범위 전부이고, update는 언제나 코퍼스 전체입니다. 어느 쪽에 collectionId를 보내도 아무 말 없이 버려집니다. 그 필드는 WikiBuildRequest에만 있습니다.

이벤트

다섯 개이며 모두 런타임 중립 이벤트 이미터를 통해 전달됩니다. 그래서 데스크톱 앱은 Tauri로, 헤드리스 클라이언트는 SSE로 동일한 페이로드를 받습니다.

GET /data/eventsGET /data/ws는 스코프가 걸린 스트림의 SSE와 WebSocket 전송입니다. GET /data/documents와 같은 data_read를 요구하고 data:*만 실어 나릅니다. 메모리, 스쿼드, 스케줄러의 이벤트는 이 연결로 넘어오지 않습니다. GET /events도 여전히 같은 다섯 이벤트를 다른 모든 것과 함께 전달하지만 admin을 요구하므로, 데이터 허브만 읽는 키라면 스코프가 걸린 쪽을 쓰세요.

?types=로 쉼표로 구분한 부분집합만 받을 수 있습니다(예: ?types=data:ingest-progress,data:documents-changed). data: 계열이 아닌 이름을 넘기면 아무것도 오지 않는 연결이 열리는 대신 400으로 거절합니다. Last-Event-ID?since=를 지원하고, 잘린 이어받기는 stream:gap으로, 뒤처진 소비자는 stream:lagged로 알린다. GET /events와 같으며, 이어받은 이벤트도 실시간 스트림과 같은 도메인 필터를 통과한다. 이벤트 스트림을 참고하세요.

curl -N -H "X-API-Key: $AIGO_API_KEY" \
  "http://127.0.0.1:8001/api/v1/data/events"

이 스트림은 데스크톱 앱에 내장된 관리 API에서도 동작합니다. 데스크톱의 모든 하위 시스템이 공유 이벤트 싱크 하나로 발행하고, 내장 서버는 시작할 때 자신의 이벤트 버스를 두 번째 표면으로 붙였다가 멈출 때 놓습니다(이슈 #5040). 그래서 재시작하면 전달 대상이 새 서버의 버스로 옮겨 가고, 그 동안에도 데스크톱 UI는 계속 이벤트를 받습니다. 이 수정 전에는 그 런타임에서 이 스트림이 열린 채 아무것도 오지 않았고, GET /events도 이 이벤트들에 대해서는 마찬가지였습니다.

이벤트 페이로드 발생 시점
data:documents-changed { documentId? } 문서 생성·수정·휴지통 이동·복원이 성공했을 때. 변경이 한 문서로 좁혀졌을 때 documentId가 실립니다.
data:ingest-progress IngestProgressEvent 인제스트·요약·위키 잡의 단계나 진행률이 바뀌었을 때.
data:collections-changed { collectionId? } 컬렉션 생성·수정·삭제가 성공했을 때. 삭제는 소속 문서의 collectionIds도 바꾸므로, 리스너는 이 이벤트에서 문서 목록도 함께 갱신합니다.
data:folder-import-progress FolderImportRun 영속 폴더 가져오기 실행이 진행됐을 때. 페이로드는 실행 전체입니다.
data:embedding-progress EmbeddingRunStatus 코퍼스 임베딩 실행이 진행됐을 때.

IngestProgressEventdocumentId, jobId, folderImportRunId, kind, phase, progress, fileName, 그리고 선택적 message를 담고, failed 단계에서는 error를, duplicate 단계에서는 duplicateOfDocumentId를 함께 담습니다. folderImportRunId는 대량 파일 작업을 일반 업로드 진행 표시에서 떼어 놓는 판별자입니다.

모든 이벤트에는 폴링 대응물이 있고 페이로드가 엔드포인트 응답과 필드 단위로 일치하므로, SSE 연결을 유지할 수 없는 클라이언트가 잃는 것은 즉시성뿐입니다.

실습

모두 AIGO=http://127.0.0.1:8001/api/v1KEY에 담긴 액세스 키를 전제합니다.

파일 인제스트

텍스트 형식이면 본문을 그대로 보내고, PDF나 DOCX 같은 바이너리 형식이면 contentBase64를 씁니다. 서버는 filename에서 종류와 원본 확장자를 추론합니다.

curl -sS -X POST "$AIGO/data/documents" \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d '{
        "title": "Routing notes",
        "content": "# Routing notes\n\nUpstream selection is ...",
        "filename": "routing-notes.md",
        "source": "upload",
        "tags": ["routing"]
      }'

응답은 생성된 Document(201)입니다. 모델로 카드 초안을 만들고, 잡이 끝나면 카드를 다시 읽습니다.

DOC=$(curl -sS "$AIGO/data/documents?limit=1" -H "X-API-Key: $KEY" | jq -r '.[0].id')
curl -sS -X POST "$AIGO/data/documents/$DOC/generate-summary" \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' -d '{}'
curl -sS "$AIGO/data/documents/$DOC/card" -H "X-API-Key: $KEY"

status가 이미 failed인 잡과 함께 202가 오면 사용할 모델이 없었다는 뜻입니다. 재시도할 요청이 아니라 렌더링할 상태입니다.

검색과 그라운딩

먼저 렉시컬 검색을, 그다음 같은 메시지에 대해 채팅 턴이 주입했을 그라운딩 블록을 만듭니다.

curl -sS -G "$AIGO/data/search" -H "X-API-Key: $KEY" \
  --data-urlencode 'query=upstream selection' --data-urlencode 'limit=5'

curl -sS -X POST "$AIGO/data/grounding-context" \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d '{"query": "how does upstream selection work?", "recordReference": false}'

GroundingContext.content가 블록 자체이고, sources·budget·mode·degraded가 무엇이 들어갔는지와 왜 요청보다 작을 수 있는지를 말해 줍니다. 컬렉션으로 좁히려면 프리셋을 만들고 presetId를 넘기세요. 요청 타입에는 컬렉션 필드가 없고, 거기에 보낸 값은 오류 없이 버려집니다.

폴더 가져오기 실행과 폴링

경로는 API 호스트에서 해석됩니다.

RUN=$(curl -sS -X POST "$AIGO/data/folder-import-runs" \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -d '{"path": "/srv/corpus/handbook", "recursive": true, "tags": ["handbook"]}' \
  | jq -r '.id')

until curl -sS "$AIGO/data/folder-import-runs/$RUN" -H "X-API-Key: $KEY" \
  | jq -e '.state as $s | ["completed","completed_with_errors","cancelled","paused","interrupted"] | index($s)' >/dev/null; do
  sleep 2
done
curl -sS "$AIGO/data/folder-import-runs/$RUN" -H "X-API-Key: $KEY" | jq '{state, queuedCount, completedCount, failedCount, moreFiles, resumeBlocked}'

위 루프는 실행이 스스로 벗어나지 않는 상태를 기다리며, 여기에는 pausedinterrupted도 들어갑니다. 둘 다 누군가 resume을 호출하기 전에는 진행되지 않으므로, 종료 상태 셋만 기다리는 루프는 영영 돌아오지 않습니다. moreFiles: true는 실행이 파일 상한에서 멈췄다는 뜻이고 POST /data/folder-import-runs/{id}/resume가 이어 갑니다. resumeBlocked: true는 절대 안전 상한에서 멈췄다는 뜻이며 새 가져오기가 답입니다. aigo data folder-import show <RUN_ID> --follow가 이 루프를 대신 돌고, 실행이 실패나 정지 상태로 끝나면 3으로 종료합니다.

위키 빌드

모델 없이 빌드하면 페이지가 만들어지지 않으므로, 시작하기 전에 모델부터 확인합니다.

curl -sS "$AIGO/data/wiki/status" -H "X-API-Key: $KEY" | jq '{hasModel, pageCount, unwrittenClusters, activeJobId}'

curl -sS -X POST "$AIGO/data/wiki/build" \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' -d '{}'

빈 본문은 코퍼스 전체를 대상으로 빌드하며 민감 컬렉션은 제외합니다. 좁히려면 collectionId를 넘기세요. 응답은 큐에 들어간 잡이고, 실행 중에는 GET /data/wiki/statusactiveJobId를 보고합니다. 클러스터가 아직 남은 채로 빌드가 멈췄다면, POST /data/wiki/continue가 이미 있는 페이지를 다시 만들지 않고 남은 것만 작성합니다.

제한

서버에서 강제되며 두 전송 방식에서 동일합니다.

  • 요청 본문: /data 라우터 전체에 68 MiB, 인제스트하는 파일 하나에 50 MiB.
  • 명시적 대량 선택: 요청당 문서 ID 500개. 결과에 담기는 문서별 실패 표본: 25개.
  • 문서 목록 한 페이지: 1000행.
  • 잡 목록: 최신순 200행, 페이징 없음. 폴더 가져오기 실행 목록: limit이 1에서 50 사이로 잘리며 기본값은 50.
  • 검색: limit이 100건으로 잘림.
  • 백필: 호출당 잡 100개, 재개 가능.
  • 폴더 가져오기: 깊이 최대 24, 실행당 파일 최대 500개, 스캔당 항목 최대 50000개.
  • 인용 내보내기: 문서 2000개, 그 이상은 truncated: true. 휴지통 비우기: 호출당 문서 500개, 그 이상은 truncated.
  • URL 인제스트: URL 4096자, httphttps만, 공용 SSRF 가드 뒤.

함께 보기

  • 데이터 허브: 이 엔드포인트들이 뒤를 받치는 데스크톱 페이지.
  • 데이터 위키: 위키 파이프라인이 무엇을 생성하고 무엇을 쓸지 어떻게 정하는지.
  • CLI 레퍼런스: 이 페이지의 요청/응답 엔드포인트 65개를 모두 다루는 aigo data 명령 그룹.