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:
Yanxin Lu
2026-03-31 13:33:04 -07:00
parent 3825f3dcdb
commit 0910bd5d5c
4 changed files with 20 additions and 15 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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.

View File

@@ -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()