contacts: support multiple address book collections
Migadu exposes "family" and "business" address books via CardDAV. The contacts script now searches all subdirs under contacts/ and adds new contacts to the "family" collection by default.
This commit is contained in:
2
TOOLS.md
2
TOOLS.md
@@ -51,7 +51,7 @@ cat msg.txt | $HIMALAYA template send # 发送邮件(校验收件人)
|
|||||||
### 通讯录(contacts)
|
### 通讯录(contacts)
|
||||||
|
|
||||||
**目录**: `~/.openclaw/workspace/skills/contacts/`
|
**目录**: `~/.openclaw/workspace/skills/contacts/`
|
||||||
**数据**: `~/.openclaw/workspace/contacts/default/`(vCard .vcf 文件,CardDAV 同步)
|
**数据**: `~/.openclaw/workspace/contacts/`(vCard .vcf 文件,按 Migadu 地址簿分目录,CardDAV 同步)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
CONTACTS=~/.openclaw/workspace/skills/contacts/scripts/contacts.sh
|
CONTACTS=~/.openclaw/workspace/skills/contacts/scripts/contacts.sh
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ $CONTACTS_DIR/scripts/contacts.sh list
|
|||||||
- [ ] `contact add` prints "Added contact: ..."
|
- [ ] `contact add` prints "Added contact: ..."
|
||||||
- [ ] Second `contact add` prints "Updated contact: ... — added ..."
|
- [ ] Second `contact add` prints "Updated contact: ... — added ..."
|
||||||
- [ ] `contact list` shows all contacts with email types
|
- [ ] `contact list` shows all contacts with email types
|
||||||
- [ ] `.vcf` files created in `~/.openclaw/workspace/contacts/default/`
|
- [ ] `.vcf` files created in `~/.openclaw/workspace/contacts/family/`
|
||||||
|
|
||||||
## 2. Recipient Resolution (Send Validation)
|
## 2. Recipient Resolution (Send Validation)
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ LLMs can hallucinate email addresses — inventing plausible-looking addresses f
|
|||||||
|
|
||||||
- Python 3 (no external dependencies)
|
- Python 3 (no external dependencies)
|
||||||
- `vdirsyncer` configured with a CardDAV pair for contacts sync
|
- `vdirsyncer` configured with a CardDAV pair for contacts sync
|
||||||
- Contacts directory: `~/.openclaw/workspace/contacts/default/`
|
- Contacts directory: `~/.openclaw/workspace/contacts/` (subdirs per address book, e.g. `family/`, `business/`)
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -64,4 +64,4 @@ Unknown addresses are **rejected** with the available contacts list shown.
|
|||||||
|
|
||||||
## Data
|
## Data
|
||||||
|
|
||||||
Contacts are stored as vCard 3.0 `.vcf` files in `~/.openclaw/workspace/contacts/default/`, synced to Migadu CardDAV via vdirsyncer. Filenames are `{uid}.vcf`.
|
Contacts are stored as vCard 3.0 `.vcf` files in `~/.openclaw/workspace/contacts/<collection>/`, synced to Migadu CardDAV via vdirsyncer. Collections match Migadu address books (e.g. `family/`, `business/`). New contacts are added to `family/` by default. The resolve command searches all collections.
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ from pathlib import Path
|
|||||||
# Config
|
# Config
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
CONTACTS_DIR = Path.home() / ".openclaw" / "workspace" / "contacts" / "default"
|
CONTACTS_ROOT = Path.home() / ".openclaw" / "workspace" / "contacts"
|
||||||
|
DEFAULT_COLLECTION = "family" # default address book for new contacts
|
||||||
PRODID = "-//OpenClaw//Contacts//EN"
|
PRODID = "-//OpenClaw//Contacts//EN"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -57,17 +58,20 @@ def _parse_vcf(path):
|
|||||||
|
|
||||||
|
|
||||||
def _load_contacts():
|
def _load_contacts():
|
||||||
"""Load all contacts from CONTACTS_DIR. Returns list of parsed contact dicts."""
|
"""Load all contacts from all collections under CONTACTS_ROOT."""
|
||||||
if not CONTACTS_DIR.is_dir():
|
if not CONTACTS_ROOT.is_dir():
|
||||||
return []
|
return []
|
||||||
contacts = []
|
contacts = []
|
||||||
for vcf_path in sorted(CONTACTS_DIR.glob("*.vcf")):
|
for subdir in sorted(CONTACTS_ROOT.iterdir()):
|
||||||
try:
|
if not subdir.is_dir():
|
||||||
contact = _parse_vcf(vcf_path)
|
|
||||||
if contact["fn"] or contact["emails"]:
|
|
||||||
contacts.append(contact)
|
|
||||||
except Exception:
|
|
||||||
continue
|
continue
|
||||||
|
for vcf_path in sorted(subdir.glob("*.vcf")):
|
||||||
|
try:
|
||||||
|
contact = _parse_vcf(vcf_path)
|
||||||
|
if contact["fn"] or contact["emails"]:
|
||||||
|
contacts.append(contact)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
return contacts
|
return contacts
|
||||||
|
|
||||||
|
|
||||||
@@ -225,7 +229,8 @@ def cmd_list(args):
|
|||||||
|
|
||||||
def cmd_add(args):
|
def cmd_add(args):
|
||||||
"""Add a new contact (creates or updates a .vcf file)."""
|
"""Add a new contact (creates or updates a .vcf file)."""
|
||||||
CONTACTS_DIR.mkdir(parents=True, exist_ok=True)
|
target_dir = CONTACTS_ROOT / DEFAULT_COLLECTION
|
||||||
|
target_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
emails = [{"address": args.email, "type": args.type or ""}]
|
emails = [{"address": args.email, "type": args.type or ""}]
|
||||||
nickname = args.nickname or ""
|
nickname = args.nickname or ""
|
||||||
@@ -251,7 +256,7 @@ def cmd_add(args):
|
|||||||
# New contact
|
# New contact
|
||||||
uid = f"{uuid.uuid4()}@openclaw"
|
uid = f"{uuid.uuid4()}@openclaw"
|
||||||
vcf_str = _build_vcf(args.name, emails, nickname, uid)
|
vcf_str = _build_vcf(args.name, emails, nickname, uid)
|
||||||
dest = CONTACTS_DIR / f"{uid}.vcf"
|
dest = target_dir / f"{uid}.vcf"
|
||||||
dest.write_text(vcf_str, encoding="utf-8")
|
dest.write_text(vcf_str, encoding="utf-8")
|
||||||
print(f"Added contact: {args.name} <{args.email}>")
|
print(f"Added contact: {args.name} <{args.email}>")
|
||||||
_sync_contacts()
|
_sync_contacts()
|
||||||
|
|||||||
Reference in New Issue
Block a user