diff --git a/src/main/db/migrations/0021_aromatic_pyro.sql b/src/main/db/migrations/0021_aromatic_pyro.sql
new file mode 100644
index 0000000..98a5838
--- /dev/null
+++ b/src/main/db/migrations/0021_aromatic_pyro.sql
@@ -0,0 +1,12 @@
+CREATE TABLE `folder_watches` (
+ `id` text PRIMARY KEY NOT NULL,
+ `notebook_id` text NOT NULL,
+ `path` text NOT NULL,
+ `created_at` integer NOT NULL,
+ `updated_at` integer NOT NULL,
+ FOREIGN KEY (`notebook_id`) REFERENCES `notebooks`(`id`) ON UPDATE no action ON DELETE cascade
+);
+--> statement-breakpoint
+CREATE UNIQUE INDEX `idx_folder_watches_notebook_path` ON `folder_watches` (`notebook_id`,`path`);--> statement-breakpoint
+ALTER TABLE `documents` ADD `source_state` text DEFAULT 'available' NOT NULL;--> statement-breakpoint
+ALTER TABLE `documents` ADD `source_mtime_ms` integer;
\ No newline at end of file
diff --git a/src/main/db/migrations/meta/0021_snapshot.json b/src/main/db/migrations/meta/0021_snapshot.json
new file mode 100644
index 0000000..11bb8ef
--- /dev/null
+++ b/src/main/db/migrations/meta/0021_snapshot.json
@@ -0,0 +1,1827 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "baab174f-764a-417c-8f5c-c1a56c3320fe",
+ "prevId": "7afec921-a432-43f3-aafe-054c28f80f89",
+ "tables": {
+ "anki_cards": {
+ "name": "anki_cards",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "notebook_id": {
+ "name": "notebook_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 1
+ },
+ "cards_data": {
+ "name": "cards_data",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "chunk_mapping": {
+ "name": "chunk_mapping",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'generating'"
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "idx_ankicards_notebook": {
+ "name": "idx_ankicards_notebook",
+ "columns": [
+ "notebook_id",
+ "updated_at"
+ ],
+ "isUnique": false
+ },
+ "idx_ankicards_version": {
+ "name": "idx_ankicards_version",
+ "columns": [
+ "notebook_id",
+ "version"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "anki_cards_notebook_id_notebooks_id_fk": {
+ "name": "anki_cards_notebook_id_notebooks_id_fk",
+ "tableFrom": "anki_cards",
+ "tableTo": "notebooks",
+ "columnsFrom": [
+ "notebook_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "chat_messages": {
+ "name": "chat_messages",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "reasoning_content": {
+ "name": "reasoning_content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "finish_reason": {
+ "name": "finish_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "usage": {
+ "name": "usage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "finished_at": {
+ "name": "finished_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "attempt_of": {
+ "name": "attempt_of",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "idx_messages_session": {
+ "name": "idx_messages_session",
+ "columns": [
+ "session_id",
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "chat_messages_session_id_chat_sessions_id_fk": {
+ "name": "chat_messages_session_id_chat_sessions_id_fk",
+ "tableFrom": "chat_messages",
+ "tableTo": "chat_sessions",
+ "columnsFrom": [
+ "session_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "chat_sessions": {
+ "name": "chat_sessions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "notebook_id": {
+ "name": "notebook_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "summary": {
+ "name": "summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "total_tokens": {
+ "name": "total_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'active'"
+ },
+ "parent_session_id": {
+ "name": "parent_session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "retrieval_scope": {
+ "name": "retrieval_scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "idx_sessions_notebook": {
+ "name": "idx_sessions_notebook",
+ "columns": [
+ "notebook_id",
+ "updated_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "chat_sessions_notebook_id_notebooks_id_fk": {
+ "name": "chat_sessions_notebook_id_notebooks_id_fk",
+ "tableFrom": "chat_sessions",
+ "tableTo": "notebooks",
+ "columnsFrom": [
+ "notebook_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "chat_sessions_parent_session_id_chat_sessions_id_fk": {
+ "name": "chat_sessions_parent_session_id_chat_sessions_id_fk",
+ "tableFrom": "chat_sessions",
+ "tableTo": "chat_sessions",
+ "columnsFrom": [
+ "parent_session_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "chunk_blocks": {
+ "name": "chunk_blocks",
+ "columns": {
+ "chunk_id": {
+ "name": "chunk_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "block_id": {
+ "name": "block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "start_in_block": {
+ "name": "start_in_block",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "end_in_block": {
+ "name": "end_in_block",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "idx_chunk_blocks_block": {
+ "name": "idx_chunk_blocks_block",
+ "columns": [
+ "block_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "chunk_blocks_chunk_id_chunks_id_fk": {
+ "name": "chunk_blocks_chunk_id_chunks_id_fk",
+ "tableFrom": "chunk_blocks",
+ "tableTo": "chunks",
+ "columnsFrom": [
+ "chunk_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "chunk_blocks_block_id_document_blocks_id_fk": {
+ "name": "chunk_blocks_block_id_document_blocks_id_fk",
+ "tableFrom": "chunk_blocks",
+ "tableTo": "document_blocks",
+ "columnsFrom": [
+ "block_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "chunk_blocks_chunk_id_block_id_pk": {
+ "columns": [
+ "chunk_id",
+ "block_id"
+ ],
+ "name": "chunk_blocks_chunk_id_block_id_pk"
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "chunks": {
+ "name": "chunks",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "document_id": {
+ "name": "document_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "notebook_id": {
+ "name": "notebook_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "chunk_index": {
+ "name": "chunk_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "start_offset": {
+ "name": "start_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "end_offset": {
+ "name": "end_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "page_start": {
+ "name": "page_start",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "page_end": {
+ "name": "page_end",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "token_count": {
+ "name": "token_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "idx_chunks_document": {
+ "name": "idx_chunks_document",
+ "columns": [
+ "document_id"
+ ],
+ "isUnique": false
+ },
+ "idx_chunks_notebook": {
+ "name": "idx_chunks_notebook",
+ "columns": [
+ "notebook_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "chunks_document_id_documents_id_fk": {
+ "name": "chunks_document_id_documents_id_fk",
+ "tableFrom": "chunks",
+ "tableTo": "documents",
+ "columnsFrom": [
+ "document_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "chunks_notebook_id_notebooks_id_fk": {
+ "name": "chunks_notebook_id_notebooks_id_fk",
+ "tableFrom": "chunks",
+ "tableTo": "notebooks",
+ "columnsFrom": [
+ "notebook_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "document_blocks": {
+ "name": "document_blocks",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "document_id": {
+ "name": "document_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "page": {
+ "name": "page",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "level": {
+ "name": "level",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "text": {
+ "name": "text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "start_offset": {
+ "name": "start_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "end_offset": {
+ "name": "end_offset",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "bbox": {
+ "name": "bbox",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "idx_blocks_document_order": {
+ "name": "idx_blocks_document_order",
+ "columns": [
+ "document_id",
+ "order"
+ ],
+ "isUnique": false
+ },
+ "idx_blocks_document_page": {
+ "name": "idx_blocks_document_page",
+ "columns": [
+ "document_id",
+ "page"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "document_blocks_document_id_documents_id_fk": {
+ "name": "document_blocks_document_id_documents_id_fk",
+ "tableFrom": "document_blocks",
+ "tableTo": "documents",
+ "columnsFrom": [
+ "document_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "documents": {
+ "name": "documents",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "notebook_id": {
+ "name": "notebook_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source_uri": {
+ "name": "source_uri",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "local_file_path": {
+ "name": "local_file_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "source_note_id": {
+ "name": "source_note_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "structure": {
+ "name": "structure",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "content_hash": {
+ "name": "content_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'pending'"
+ },
+ "source_state": {
+ "name": "source_state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'available'"
+ },
+ "source_mtime_ms": {
+ "name": "source_mtime_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "chunk_count": {
+ "name": "chunk_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "idx_documents_notebook": {
+ "name": "idx_documents_notebook",
+ "columns": [
+ "notebook_id",
+ "updated_at"
+ ],
+ "isUnique": false
+ },
+ "idx_documents_status": {
+ "name": "idx_documents_status",
+ "columns": [
+ "status"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "documents_notebook_id_notebooks_id_fk": {
+ "name": "documents_notebook_id_notebooks_id_fk",
+ "tableFrom": "documents",
+ "tableTo": "notebooks",
+ "columnsFrom": [
+ "notebook_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "documents_source_note_id_notes_id_fk": {
+ "name": "documents_source_note_id_notes_id_fk",
+ "tableFrom": "documents",
+ "tableTo": "notes",
+ "columnsFrom": [
+ "source_note_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "embeddings": {
+ "name": "embeddings",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "chunk_id": {
+ "name": "chunk_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "notebook_id": {
+ "name": "notebook_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "dimensions": {
+ "name": "dimensions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "idx_embeddings_chunk": {
+ "name": "idx_embeddings_chunk",
+ "columns": [
+ "chunk_id"
+ ],
+ "isUnique": false
+ },
+ "idx_embeddings_notebook": {
+ "name": "idx_embeddings_notebook",
+ "columns": [
+ "notebook_id"
+ ],
+ "isUnique": false
+ },
+ "idx_embeddings_model": {
+ "name": "idx_embeddings_model",
+ "columns": [
+ "model"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "embeddings_chunk_id_chunks_id_fk": {
+ "name": "embeddings_chunk_id_chunks_id_fk",
+ "tableFrom": "embeddings",
+ "tableTo": "chunks",
+ "columnsFrom": [
+ "chunk_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "embeddings_notebook_id_notebooks_id_fk": {
+ "name": "embeddings_notebook_id_notebooks_id_fk",
+ "tableFrom": "embeddings",
+ "tableTo": "notebooks",
+ "columnsFrom": [
+ "notebook_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "folder_watches": {
+ "name": "folder_watches",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "notebook_id": {
+ "name": "notebook_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "idx_folder_watches_notebook_path": {
+ "name": "idx_folder_watches_notebook_path",
+ "columns": [
+ "notebook_id",
+ "path"
+ ],
+ "isUnique": true
+ }
+ },
+ "foreignKeys": {
+ "folder_watches_notebook_id_notebooks_id_fk": {
+ "name": "folder_watches_notebook_id_notebooks_id_fk",
+ "tableFrom": "folder_watches",
+ "tableTo": "notebooks",
+ "columnsFrom": [
+ "notebook_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "ingestion_runs": {
+ "name": "ingestion_runs",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "document_id": {
+ "name": "document_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "stage": {
+ "name": "stage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "progress": {
+ "name": "progress",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "finished_at": {
+ "name": "finished_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "idx_ingestion_runs_document": {
+ "name": "idx_ingestion_runs_document",
+ "columns": [
+ "document_id",
+ "started_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "ingestion_runs_document_id_documents_id_fk": {
+ "name": "ingestion_runs_document_id_documents_id_fk",
+ "tableFrom": "ingestion_runs",
+ "tableTo": "documents",
+ "columnsFrom": [
+ "document_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "items": {
+ "name": "items",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "notebook_id": {
+ "name": "notebook_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "resource_id": {
+ "name": "resource_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "idx_items_notebook_order": {
+ "name": "idx_items_notebook_order",
+ "columns": [
+ "notebook_id",
+ "order"
+ ],
+ "isUnique": false
+ },
+ "idx_items_type": {
+ "name": "idx_items_type",
+ "columns": [
+ "type"
+ ],
+ "isUnique": false
+ },
+ "idx_items_resource": {
+ "name": "idx_items_resource",
+ "columns": [
+ "type",
+ "resource_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "items_notebook_id_notebooks_id_fk": {
+ "name": "items_notebook_id_notebooks_id_fk",
+ "tableFrom": "items",
+ "tableTo": "notebooks",
+ "columnsFrom": [
+ "notebook_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "mind_maps": {
+ "name": "mind_maps",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "notebook_id": {
+ "name": "notebook_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 1
+ },
+ "tree_data": {
+ "name": "tree_data",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "chunk_mapping": {
+ "name": "chunk_mapping",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'generating'"
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "idx_mindmaps_notebook": {
+ "name": "idx_mindmaps_notebook",
+ "columns": [
+ "notebook_id",
+ "updated_at"
+ ],
+ "isUnique": false
+ },
+ "idx_mindmaps_version": {
+ "name": "idx_mindmaps_version",
+ "columns": [
+ "notebook_id",
+ "version"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "mind_maps_notebook_id_notebooks_id_fk": {
+ "name": "mind_maps_notebook_id_notebooks_id_fk",
+ "tableFrom": "mind_maps",
+ "tableTo": "notebooks",
+ "columnsFrom": [
+ "notebook_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "notebook_embedding_spaces": {
+ "name": "notebook_embedding_spaces",
+ "columns": {
+ "notebook_id": {
+ "name": "notebook_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "space_id": {
+ "name": "space_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "backend": {
+ "name": "backend",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "revision": {
+ "name": "revision",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "dimensions": {
+ "name": "dimensions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "notebook_embedding_spaces_notebook_id_notebooks_id_fk": {
+ "name": "notebook_embedding_spaces_notebook_id_notebooks_id_fk",
+ "tableFrom": "notebook_embedding_spaces",
+ "tableTo": "notebooks",
+ "columnsFrom": [
+ "notebook_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "notebooks": {
+ "name": "notebooks",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "idx_notebooks_updated": {
+ "name": "idx_notebooks_updated",
+ "columns": [
+ "updated_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "notes": {
+ "name": "notes",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "notebook_id": {
+ "name": "notebook_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "idx_notes_notebook": {
+ "name": "idx_notes_notebook",
+ "columns": [
+ "notebook_id",
+ "updated_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "notes_notebook_id_notebooks_id_fk": {
+ "name": "notes_notebook_id_notebooks_id_fk",
+ "tableFrom": "notes",
+ "tableTo": "notebooks",
+ "columnsFrom": [
+ "notebook_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "quiz_sessions": {
+ "name": "quiz_sessions",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "quiz_id": {
+ "name": "quiz_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "notebook_id": {
+ "name": "notebook_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "answers": {
+ "name": "answers",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "score": {
+ "name": "score",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "total_questions": {
+ "name": "total_questions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "correct_count": {
+ "name": "correct_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "idx_quiz_sessions_quiz": {
+ "name": "idx_quiz_sessions_quiz",
+ "columns": [
+ "quiz_id",
+ "created_at"
+ ],
+ "isUnique": false
+ },
+ "idx_quiz_sessions_notebook": {
+ "name": "idx_quiz_sessions_notebook",
+ "columns": [
+ "notebook_id",
+ "created_at"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "quiz_sessions_quiz_id_quizzes_id_fk": {
+ "name": "quiz_sessions_quiz_id_quizzes_id_fk",
+ "tableFrom": "quiz_sessions",
+ "tableTo": "quizzes",
+ "columnsFrom": [
+ "quiz_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "quiz_sessions_notebook_id_notebooks_id_fk": {
+ "name": "quiz_sessions_notebook_id_notebooks_id_fk",
+ "tableFrom": "quiz_sessions",
+ "tableTo": "notebooks",
+ "columnsFrom": [
+ "notebook_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ },
+ "quizzes": {
+ "name": "quizzes",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "notebook_id": {
+ "name": "notebook_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 1
+ },
+ "questions_data": {
+ "name": "questions_data",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "chunk_mapping": {
+ "name": "chunk_mapping",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'generating'"
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {
+ "idx_quizzes_notebook": {
+ "name": "idx_quizzes_notebook",
+ "columns": [
+ "notebook_id",
+ "updated_at"
+ ],
+ "isUnique": false
+ },
+ "idx_quizzes_version": {
+ "name": "idx_quizzes_version",
+ "columns": [
+ "notebook_id",
+ "version"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "quizzes_notebook_id_notebooks_id_fk": {
+ "name": "quizzes_notebook_id_notebooks_id_fk",
+ "tableFrom": "quizzes",
+ "tableTo": "notebooks",
+ "columnsFrom": [
+ "notebook_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "checkConstraints": {}
+ }
+ },
+ "views": {},
+ "enums": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/src/main/db/migrations/meta/_journal.json b/src/main/db/migrations/meta/_journal.json
index db04dc0..fdbf467 100644
--- a/src/main/db/migrations/meta/_journal.json
+++ b/src/main/db/migrations/meta/_journal.json
@@ -148,6 +148,13 @@
"when": 1790587593731,
"tag": "0020_normal_nextwave",
"breakpoints": true
+ },
+ {
+ "idx": 21,
+ "version": "6",
+ "when": 1790594238292,
+ "tag": "0021_aromatic_pyro",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/src/main/db/schema.ts b/src/main/db/schema.ts
index 9d8736d..baba4e4 100644
--- a/src/main/db/schema.ts
+++ b/src/main/db/schema.ts
@@ -1,4 +1,4 @@
-import { sqliteTable, text, integer, index, primaryKey } from 'drizzle-orm/sqlite-core'
+import { sqliteTable, text, integer, index, primaryKey, uniqueIndex } from 'drizzle-orm/sqlite-core'
import type { QuizQuestion } from '../../shared/types/quiz'
import type {
ChatExecutionError,
@@ -165,6 +165,15 @@ export const documents = sqliteTable(
status: text('status', { enum: ['pending', 'processing', 'indexed', 'failed'] })
.notNull()
.default('pending'),
+ /**
+ * 来源文件本身的状态(#158),与 `status`(索引状态)分开:
+ * 文件被删是 `missing`,索引只是过期;笔记、摘录、citation 都还在。
+ */
+ sourceState: text('source_state', { enum: ['available', 'missing', 'changed'] })
+ .notNull()
+ .default('available'),
+ /** 上次导入/刷新时来源文件的 mtime(ms);watch 靠它判断文件是否被改过。 */
+ sourceMtimeMs: integer('source_mtime_ms'),
errorMessage: text('error_message'),
chunkCount: integer('chunk_count').default(0),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
@@ -179,6 +188,31 @@ export const documents = sqliteTable(
export type Document = typeof documents.$inferSelect
export type NewDocument = typeof documents.$inferInsert
+/**
+ * 被监听的文件夹(#158)。
+ *
+ * 文件夹不是 document(document 是一份来源文件),所以监听关系单独一张表:一个
+ * notebook 可以监听多个文件夹,同一个(notebook, path)不会重复(唯一索引)。
+ */
+export const folderWatches = sqliteTable(
+ 'folder_watches',
+ {
+ id: text('id').primaryKey(),
+ notebookId: text('notebook_id')
+ .notNull()
+ .references(() => notebooks.id, { onDelete: 'cascade' }),
+ path: text('path').notNull(),
+ createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
+ updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull()
+ },
+ (table) => ({
+ uniquePath: uniqueIndex('idx_folder_watches_notebook_path').on(table.notebookId, table.path)
+ })
+)
+
+export type FolderWatch = typeof folderWatches.$inferSelect
+export type NewFolderWatch = typeof folderWatches.$inferInsert
+
/**
* 索引尝试记录(#95)。
*
diff --git a/src/main/index.ts b/src/main/index.ts
index a6e24cf..c3b1497 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -137,6 +137,12 @@ app.whenReady().then(async () => {
knowledgeService = new KnowledgeService(embeddingService)
Logger.info('Main', 'Knowledge Service initialized')
+ // Watched folders (#158): reconcile once so changes made while the app was closed
+ // are picked up, then keep them current live.
+ Logger.info('Main', 'Reconciling watched folders...')
+ await knowledgeService.reconcileWatchedFolders()
+ knowledgeService.startFolderWatching()
+
// Initialize Update Service
Logger.info('Main', 'Initializing Update Service...')
updateService = new UpdateService()
@@ -245,6 +251,7 @@ app.on('before-quit', () => {
}
Logger.info('Main', 'Closing database connection...')
+ knowledgeService?.stopFolderWatching()
closeDatabase()
})
diff --git a/src/main/ipc/knowledgeHandlers.ts b/src/main/ipc/knowledgeHandlers.ts
index f624cf9..8b32cc0 100644
--- a/src/main/ipc/knowledgeHandlers.ts
+++ b/src/main/ipc/knowledgeHandlers.ts
@@ -337,64 +337,92 @@ export function registerKnowledgeHandlers(knowledgeService: KnowledgeService) {
})
// 文件夹导入:一次快照(#98)
- ipcMain.handle(
- 'knowledge:add-folder',
- async (event: IpcMainInvokeEvent, args: unknown) => {
- const validated = await validate(KnowledgeSchemas.addFolder, async (params) => {
- Logger.debug('KnowledgeHandlers', 'add-folder:', params)
+ ipcMain.handle('knowledge:add-folder', async (event: IpcMainInvokeEvent, args: unknown) => {
+ const validated = await validate(KnowledgeSchemas.addFolder, async (params) => {
+ Logger.debug('KnowledgeHandlers', 'add-folder:', params)
- try {
- const result = await knowledgeService.addFolder(
- params.notebookId,
- params.folderPath,
- (stage, progress) => {
- event.sender.send('knowledge:index-progress', {
- notebookId: params.notebookId,
- stage,
- progress
- })
- }
- )
- return { success: true, ...result }
- } catch (error) {
- Logger.error('KnowledgeHandlers', 'Error adding folder:', error)
- return { success: false, added: [], skipped: [], failed: [], error: (error as Error).message }
+ try {
+ const result = await knowledgeService.addFolder(
+ params.notebookId,
+ params.folderPath,
+ (stage, progress) => {
+ event.sender.send('knowledge:index-progress', {
+ notebookId: params.notebookId,
+ stage,
+ progress
+ })
+ }
+ )
+
+ // 监听(#158)在快照之后建立:持久化监听关系、立刻对齐一次、开始实时监听。
+ if (params.watch) {
+ await knowledgeService.watchFolder(params.notebookId, params.folderPath)
}
- })(event, args)
- return validated
- }
+ return { success: true, ...result }
+ } catch (error) {
+ Logger.error('KnowledgeHandlers', 'Error adding folder:', error)
+ return {
+ success: false,
+ added: [],
+ skipped: [],
+ failed: [],
+ error: (error as Error).message
+ }
+ }
+ })(event, args)
+
+ return validated
+ })
+
+ // 监听文件夹(#158)
+ ipcMain.handle(
+ 'knowledge:list-folder-watches',
+ validate(KnowledgeSchemas.listFolderWatches, async (params) => {
+ return knowledgeService.listFolderWatches(params.notebookId)
+ })
)
- // 批量导入一组文件(#98,拖放/多选共用)
ipcMain.handle(
- 'knowledge:add-files',
- async (event: IpcMainInvokeEvent, args: unknown) => {
- const validated = await validate(KnowledgeSchemas.addFiles, async (params) => {
- Logger.debug('KnowledgeHandlers', 'add-files:', params.paths.length)
+ 'knowledge:unwatch-folder',
+ validate(KnowledgeSchemas.unwatchFolder, async (params) => {
+ knowledgeService.unwatchFolder(params.watchId)
+ return { success: true }
+ })
+ )
- try {
- const result = await knowledgeService.addDocumentsFromPaths(
- params.notebookId,
- params.paths,
- (stage, progress) => {
- event.sender.send('knowledge:index-progress', {
- notebookId: params.notebookId,
- stage,
- progress
- })
- }
- )
- return { success: true, ...result }
- } catch (error) {
- Logger.error('KnowledgeHandlers', 'Error adding files:', error)
- return { success: false, added: [], skipped: [], failed: [], error: (error as Error).message }
+ // 批量导入一组文件(#98,拖放/多选共用)
+ ipcMain.handle('knowledge:add-files', async (event: IpcMainInvokeEvent, args: unknown) => {
+ const validated = await validate(KnowledgeSchemas.addFiles, async (params) => {
+ Logger.debug('KnowledgeHandlers', 'add-files:', params.paths.length)
+
+ try {
+ const result = await knowledgeService.addDocumentsFromPaths(
+ params.notebookId,
+ params.paths,
+ (stage, progress) => {
+ event.sender.send('knowledge:index-progress', {
+ notebookId: params.notebookId,
+ stage,
+ progress
+ })
+ }
+ )
+ return { success: true, ...result }
+ } catch (error) {
+ Logger.error('KnowledgeHandlers', 'Error adding files:', error)
+ return {
+ success: false,
+ added: [],
+ skipped: [],
+ failed: [],
+ error: (error as Error).message
}
- })(event, args)
+ }
+ })(event, args)
- return validated
- }
- )
+ return validated
+ })
// 打开文档源文件
ipcMain.handle(
diff --git a/src/main/ipc/validation.ts b/src/main/ipc/validation.ts
index fee8194..e409938 100644
--- a/src/main/ipc/validation.ts
+++ b/src/main/ipc/validation.ts
@@ -146,10 +146,20 @@ export const KnowledgeSchemas = {
filePath: z.string().min(1, '文件路径不能为空')
}),
- // 批量/文件夹导入(#98)
+ // 批量/文件夹导入(#98),以及监听文件夹(#158)
addFolder: z.object({
notebookId: z.string().min(1, '笔记本 ID 不能为空'),
- folderPath: z.string().min(1, '文件夹路径不能为空')
+ folderPath: z.string().min(1, '文件夹路径不能为空'),
+ /** true 时把文件夹改为监听来源(#158):持续对齐,而不仅仅是一次快照。 */
+ watch: z.boolean().optional()
+ }),
+
+ listFolderWatches: z.object({
+ notebookId: z.string().min(1, '笔记本 ID 不能为空')
+ }),
+
+ unwatchFolder: z.object({
+ watchId: z.string().min(1, '监听 ID 不能为空')
}),
addFiles: z.object({
diff --git a/src/main/services/KnowledgeService.ts b/src/main/services/KnowledgeService.ts
index 9a38a45..521b050 100644
--- a/src/main/services/KnowledgeService.ts
+++ b/src/main/services/KnowledgeService.ts
@@ -5,7 +5,7 @@
import { createHash } from 'crypto'
import { app } from 'electron'
-import { join, basename } from 'path'
+import { join, basename, sep } from 'path'
import { mkdir, copyFile, unlink, stat } from 'fs/promises'
import {
getDatabase,
@@ -60,6 +60,15 @@ import type {
Retriever
} from './retrieval'
import { WebFetchService } from './WebFetchService'
+import type { WatchedDocument } from './ingestion/folderWatch'
+import {
+ FolderWatchService,
+ addFolderWatch,
+ listFolderWatchesForNotebook,
+ removeFolderWatch,
+ type FolderWatchRecord,
+ type ReconcileResult
+} from './ingestion/folderWatch'
import { scanFolder } from './ingestion/folderScan'
import { deleteDocumentChunksFts, ensureChunksFts, indexChunksFts, searchChunksFts } from './fts'
import { vectorStoreManager } from '../vectorstore'
@@ -182,6 +191,7 @@ export class KnowledgeService {
private fileParserService: FileParserService
private webFetchService: WebFetchService
private knowledgeFilesDir: string
+ private folderWatch: FolderWatchService
constructor(embeddingService: EmbeddingService) {
this.embeddingService = embeddingService
@@ -192,6 +202,8 @@ export class KnowledgeService {
// 知识库文件存储目录
this.knowledgeFilesDir = join(app.getPath('userData'), 'knowledge-files')
this.ensureKnowledgeFilesDir()
+ // 监听服务只持有本服务的引用,构造本身没有副作用(不会开数据库、不会开始监听)。
+ this.folderWatch = new FolderWatchService(this)
}
/**
@@ -468,6 +480,9 @@ export class KnowledgeService {
fileSize: parseResult.metadata?.fileSize as number | undefined,
metadata: parseResult.metadata,
errorMessage: null,
+ // 文件回到 available,并记下这次看到的 mtime:watch(#158)靠它判断是否被改过。
+ sourceState: 'available',
+ sourceMtimeMs: (await stat(filePath).catch(() => null))?.mtimeMs,
updatedAt: new Date()
})
.where(eq(documents.id, documentId))
@@ -1175,6 +1190,89 @@ export class KnowledgeService {
return runsFor(documentId)
}
+ /** 当前所有已注册 loader 认识的扩展名(#98 / #158 的扫描用它)。 */
+ supportedExtensions(): string[] {
+ return this.fileParserService.supportedExtensions()
+ }
+
+ /**
+ * 一个被监听文件夹下的所有 source(#158)。
+ *
+ * 按 `sourceUri` 前缀匹配:文件导入后 sourceUri 仍是原路径,所以「这个文件夹下的
+ * 来源」可以直接算出来,不需要另存一份父子关系。
+ */
+ getWatchedDocuments(notebookId: string, folderPath: string): WatchedDocument[] {
+ const prefix = folderPath.endsWith(sep) ? folderPath : folderPath + sep
+
+ return getDatabase()
+ .select({
+ documentId: documents.id,
+ sourceUri: documents.sourceUri,
+ sourceMtimeMs: documents.sourceMtimeMs,
+ sourceState: documents.sourceState
+ })
+ .from(documents)
+ .where(eq(documents.notebookId, notebookId))
+ .all()
+ .filter(
+ (row): row is WatchedDocument =>
+ typeof row.sourceUri === 'string' &&
+ (row.sourceUri === folderPath || row.sourceUri.startsWith(prefix))
+ )
+ }
+
+ /** 标记来源文件本身的状态(#158):`missing` 不删行,也不碰笔记与 citation。 */
+ markSourceState(documentId: string, state: 'available' | 'missing' | 'changed'): void {
+ getDatabase()
+ .update(documents)
+ .set({ sourceState: state, updatedAt: new Date() })
+ .where(eq(documents.id, documentId))
+ .run()
+ }
+
+ /**
+ * 来源文件变了:重新拷贝、解析并索引**同一个** documentId(#158)。
+ *
+ * 与 reindex 的区别:reindex 用的是已持久化的 content,而这里文件本身被改过,必须
+ * 重新解析。来源身份不变,所以历史 citation 与摘录仍然指向同一个来源。
+ */
+ async refreshDocumentFromFile(documentId: string, filePath: string): Promise
{t('sourceMissing')}
+ )} diff --git a/src/renderer/src/locales/en-US/ui.json b/src/renderer/src/locales/en-US/ui.json index f5d6b82..607b48e 100644 --- a/src/renderer/src/locales/en-US/ui.json +++ b/src/renderer/src/locales/en-US/ui.json @@ -86,6 +86,8 @@ "retryDocument": "Try again", "reindexDocument": "Re-index", "uploadFolder": "Add folder", + "watchFolder": "Watch folder", + "sourceMissing": "The file is no longer at its path", "dropToImport": "Drop files or a folder to import", "importAdded": "Imported {{count}}", "importSkipped": "skipped {{count}} (already imported)", diff --git a/src/renderer/src/locales/zh-CN/ui.json b/src/renderer/src/locales/zh-CN/ui.json index dbbb6e1..89e74ff 100644 --- a/src/renderer/src/locales/zh-CN/ui.json +++ b/src/renderer/src/locales/zh-CN/ui.json @@ -86,6 +86,8 @@ "retryDocument": "重试", "reindexDocument": "重建索引", "uploadFolder": "添加文件夹", + "watchFolder": "监听文件夹", + "sourceMissing": "文件已不在原路径", "dropToImport": "拖入文件或文件夹以导入", "importAdded": "已导入 {{count}} 个", "importSkipped": "跳过 {{count}} 个(已存在)", diff --git a/src/renderer/src/store/knowledgeStore.ts b/src/renderer/src/store/knowledgeStore.ts index a49c1d8..31bb093 100644 --- a/src/renderer/src/store/knowledgeStore.ts +++ b/src/renderer/src/store/knowledgeStore.ts @@ -70,8 +70,12 @@ interface KnowledgeStore { notebookId: string, noteId: string ) => Promise<{ success: boolean; documentId?: string; error?: string }> - /** 文件夹导入:一次快照(#98)。 */ - addFolder: (notebookId: string, folderPath: string) => Promise