mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-08-15 01:12:06 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76f077e12a |
@@ -422,12 +422,17 @@ We recommend creating **one Slack app** that handles both. The App Manifest belo
|
||||
|
||||
2. Apple Notes is detected automatically when Full Disk Access is granted
|
||||
|
||||
OpenJarvis searches an indexed snapshot rather than querying Notes.app live.
|
||||
After creating notes, open **Data Sources** and click **Re-sync** on Apple Notes
|
||||
before searching for the new content.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| "Not connected" despite Full Disk Access | Restart your terminal app after granting access |
|
||||
| Notes content is garbled | Some very old notes may have encoding issues. Most notes should be clean. |
|
||||
| New notes are missing | In **Data Sources**, click **Re-sync** on Apple Notes to refresh the index. |
|
||||
| Missing notes | Only notes stored locally or in iCloud are indexed. Notes in third-party accounts (Gmail, Exchange) may not appear. |
|
||||
|
||||
---
|
||||
|
||||
@@ -9,8 +9,9 @@ System Settings → Privacy & Security → Full Disk Access.
|
||||
|
||||
Timestamp notes
|
||||
---------------
|
||||
The Notes database stores modification timestamps as seconds since the Apple
|
||||
epoch of 2001-01-01 00:00:00 UTC. Conversion formula::
|
||||
Modern Notes schemas store note modification timestamps in
|
||||
``ZMODIFICATIONDATE1``; older schemas use ``ZMODIFICATIONDATE``. Both are
|
||||
seconds since the Apple epoch of 2001-01-01 00:00:00 UTC. Conversion formula::
|
||||
|
||||
dt = datetime(2001, 1, 1, tzinfo=utc) + timedelta(seconds=ZMODIFICATIONDATE)
|
||||
|
||||
@@ -171,25 +172,46 @@ class AppleNotesConnector(BaseConnector):
|
||||
return
|
||||
|
||||
try:
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT n.ZIDENTIFIER, "
|
||||
" COALESCE(n.ZTITLE1, n.ZTITLE, '') AS title, "
|
||||
" n.ZMODIFICATIONDATE, d.ZDATA "
|
||||
"FROM ZICCLOUDSYNCINGOBJECT n "
|
||||
"JOIN ZICNOTEDATA d ON d.ZNOTE = n.Z_PK "
|
||||
"ORDER BY n.ZMODIFICATIONDATE ASC"
|
||||
).fetchall()
|
||||
except sqlite3.OperationalError:
|
||||
# Older macOS schemas may lack ZTITLE1
|
||||
rows = conn.execute(
|
||||
"SELECT n.ZIDENTIFIER, "
|
||||
" COALESCE(n.ZTITLE, '') AS title, "
|
||||
" n.ZMODIFICATIONDATE, d.ZDATA "
|
||||
"FROM ZICCLOUDSYNCINGOBJECT n "
|
||||
"JOIN ZICNOTEDATA d ON d.ZNOTE = n.Z_PK "
|
||||
"ORDER BY n.ZMODIFICATIONDATE ASC"
|
||||
object_columns = {
|
||||
row[1]
|
||||
for row in conn.execute(
|
||||
"PRAGMA table_info(ZICCLOUDSYNCINGOBJECT)"
|
||||
).fetchall()
|
||||
}
|
||||
|
||||
title_columns = [
|
||||
f"n.{column}"
|
||||
for column in ("ZTITLE1", "ZTITLE")
|
||||
if column in object_columns
|
||||
]
|
||||
title_expr = (
|
||||
f"COALESCE({', '.join(title_columns)}, '')" if title_columns else "''"
|
||||
)
|
||||
|
||||
# Modern Apple Notes stores a note's modification timestamp in
|
||||
# ZMODIFICATIONDATE1. ZMODIFICATIONDATE is still present in some
|
||||
# schemas, but applies to other cloud-sync object types and can be
|
||||
# NULL for notes. Treating that NULL as zero makes incremental
|
||||
# syncs incorrectly discard newly-created notes as 2001-era data.
|
||||
modification_columns = [
|
||||
f"n.{column}"
|
||||
for column in ("ZMODIFICATIONDATE1", "ZMODIFICATIONDATE")
|
||||
if column in object_columns
|
||||
]
|
||||
modification_expr = (
|
||||
f"COALESCE({', '.join(modification_columns)}, 0)"
|
||||
if modification_columns
|
||||
else "0"
|
||||
)
|
||||
|
||||
rows = conn.execute(
|
||||
"SELECT n.ZIDENTIFIER, "
|
||||
f" {title_expr} AS title, "
|
||||
f" {modification_expr} AS modification_date, d.ZDATA "
|
||||
"FROM ZICCLOUDSYNCINGOBJECT n "
|
||||
"JOIN ZICNOTEDATA d ON d.ZNOTE = n.Z_PK "
|
||||
"ORDER BY modification_date ASC"
|
||||
).fetchall()
|
||||
|
||||
self._items_total = len(rows)
|
||||
synced = 0
|
||||
|
||||
@@ -8,6 +8,7 @@ from __future__ import annotations
|
||||
|
||||
import gzip
|
||||
import sqlite3
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
@@ -24,7 +25,8 @@ from openjarvis.core.registry import ConnectorRegistry
|
||||
def _create_fake_notes_db(db_path: Path) -> None:
|
||||
"""Populate a SQLite file with the Apple Notes schema and sample rows."""
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
conn.executescript("""
|
||||
conn.executescript(
|
||||
"""
|
||||
CREATE TABLE ZICCLOUDSYNCINGOBJECT (
|
||||
Z_PK INTEGER PRIMARY KEY,
|
||||
ZTITLE TEXT,
|
||||
@@ -38,7 +40,8 @@ def _create_fake_notes_db(db_path: Path) -> None:
|
||||
ZDATA BLOB,
|
||||
ZNOTE INTEGER
|
||||
);
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
# Note 1 — Shopping List
|
||||
html1 = "<html><body><h1>Shopping List</h1><p>Milk, eggs, bread</p></body></html>"
|
||||
@@ -208,3 +211,61 @@ def test_registry() -> None:
|
||||
assert ConnectorRegistry.contains("apple_notes")
|
||||
cls = ConnectorRegistry.get("apple_notes")
|
||||
assert cls.connector_id == "apple_notes"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 10 — modern modification timestamp drives incremental sync
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_incremental_sync_uses_modern_note_modification_date(tmp_path: Path) -> None:
|
||||
"""Modern Notes rows use ZMODIFICATIONDATE1 for incremental sync."""
|
||||
db_path = tmp_path / "ModernNoteStore.sqlite"
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
conn.executescript(
|
||||
"""
|
||||
CREATE TABLE ZICCLOUDSYNCINGOBJECT (
|
||||
Z_PK INTEGER PRIMARY KEY,
|
||||
ZTITLE TEXT,
|
||||
ZTITLE1 TEXT,
|
||||
ZMODIFICATIONDATE REAL,
|
||||
ZMODIFICATIONDATE1 REAL,
|
||||
ZIDENTIFIER TEXT
|
||||
);
|
||||
CREATE TABLE ZICNOTEDATA (
|
||||
Z_PK INTEGER PRIMARY KEY,
|
||||
ZDATA BLOB,
|
||||
ZNOTE INTEGER
|
||||
);
|
||||
"""
|
||||
)
|
||||
compressed = gzip.compress(b"<p>New movie list</p>")
|
||||
conn.execute(
|
||||
"INSERT INTO ZICCLOUDSYNCINGOBJECT VALUES "
|
||||
"(1, NULL, 'Movies', NULL, 800000000.0, 'note-modern')"
|
||||
)
|
||||
conn.execute("INSERT INTO ZICNOTEDATA VALUES (1, ?, 1)", (compressed,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
from openjarvis.connectors.apple_notes import AppleNotesConnector # noqa: PLC0415
|
||||
|
||||
connector = AppleNotesConnector(db_path=str(db_path))
|
||||
docs = list(connector.sync(since=datetime(2026, 1, 1, tzinfo=timezone.utc)))
|
||||
|
||||
assert [doc.doc_id for doc in docs] == ["apple_notes:note-modern"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 11 — legacy modification timestamp remains supported
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_incremental_sync_falls_back_to_legacy_modification_date(connector) -> None:
|
||||
"""Older Notes rows continue to use ZMODIFICATIONDATE."""
|
||||
docs = list(connector.sync(since=datetime(2023, 1, 1, tzinfo=timezone.utc)))
|
||||
|
||||
assert {doc.doc_id for doc in docs} == {
|
||||
"apple_notes:note-001",
|
||||
"apple_notes:note-002",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user