Add private config setup for Zotero workflow

This commit is contained in:
qyh15 2026-05-19 23:44:55 +08:00
parent 70978a3642
commit ecb090e613
5 changed files with 127 additions and 3 deletions

2
.gitignore vendored
View File

@ -1,6 +1,8 @@
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
.env .env
config/config.local.json
config/*.secret.json
*.log *.log
*.tmp *.tmp
.zotero-ai-notes-index.json .zotero-ai-notes-index.json

View File

@ -10,6 +10,7 @@ Use this skill for the user's Zotero + Obsidian literature workflow. It covers Z
Core scripts: Core scripts:
```powershell ```powershell
py "$env:USERPROFILE\.codex\skills\zotero-obsidian-literature\scripts\init_private_config.py"
py "$env:USERPROFILE\.codex\skills\zotero-obsidian-literature\scripts\generate_zotero_ai_note.py" --vault "C:\Users\qyh15\Documents\Obsidian Vault" --item-key SXAIQUJT --skip-existing py "$env:USERPROFILE\.codex\skills\zotero-obsidian-literature\scripts\generate_zotero_ai_note.py" --vault "C:\Users\qyh15\Documents\Obsidian Vault" --item-key SXAIQUJT --skip-existing
py "$env:USERPROFILE\.codex\skills\zotero-obsidian-literature\scripts\audit_zotero_ai_notes.py" --vault "C:\Users\qyh15\Documents\Obsidian Vault" --keys-only py "$env:USERPROFILE\.codex\skills\zotero-obsidian-literature\scripts\audit_zotero_ai_notes.py" --vault "C:\Users\qyh15\Documents\Obsidian Vault" --keys-only
``` ```
@ -22,10 +23,22 @@ py "$env:USERPROFILE\.codex\skills\zotero-obsidian-literature\scripts\audit_zote
- `01 ...md` research-article template - `01 ...md` research-article template
- `02 ...md` review-article template - `02 ...md` review-article template
- `03 ...md` AI prompt - `03 ...md` AI prompt
- `ZOTERO_API_KEY` must be available in the process environment for Zotero Web API writes. - `ZOTERO_API_KEY` must be available for Zotero Web API writes.
- LLM settings come from `AWESOMEGPT_API_KEY`, `AWESOMEGPT_BASE_URL`, `AWESOMEGPT_MODEL`, or AwesomeGPT preferences in the default Zotero profile. - LLM settings come from `AWESOMEGPT_API_KEY`, `AWESOMEGPT_BASE_URL`, `AWESOMEGPT_MODEL`, the private local config, or AwesomeGPT preferences in the default Zotero profile.
Never store API keys in the skill, vault helper files, Git commits, zip files, terminal output, or chat. If keys appear in chat or logs, tell the user to rotate them. Never store API keys in `SKILL.md`, references, committed files, zip files, terminal output, or chat. If keys appear in chat or logs, tell the user to rotate them.
## Private Config
Use a local-only config for Zotero and DeepSeek/AwesomeGPT keys. The committed template is `config/config.template.json`; the real file is `config/config.local.json`, which is ignored by Git.
Initialize or update it with hidden prompts:
```powershell
py "$env:USERPROFILE\.codex\skills\zotero-obsidian-literature\scripts\init_private_config.py"
```
Do not print the config contents. When checking setup, report only whether each key is present. Environment variables override the private config; the private config overrides AwesomeGPT preferences for fields that are present.
## Zotero Note Workflows ## Zotero Note Workflows

View File

@ -0,0 +1,11 @@
{
"zotero": {
"api_key": "",
"user_id": ""
},
"awesomegpt": {
"api_key": "",
"base_url": "https://api.deepseek.com",
"model": "deepseek-v4-pro"
}
}

View File

@ -10,6 +10,9 @@ Required environment variables:
Optional: Optional:
ZOTERO_USER_ID If omitted, resolved from /keys/current ZOTERO_USER_ID If omitted, resolved from /keys/current
Private local config:
<skill>\config\config.local.json, created by scripts\init_private_config.py
""" """
from __future__ import annotations from __future__ import annotations
@ -37,6 +40,8 @@ except Exception:
LOCAL_ZOTERO = "http://127.0.0.1:23119/api/users/0" LOCAL_ZOTERO = "http://127.0.0.1:23119/api/users/0"
ZOTERO_WEB = "https://api.zotero.org" ZOTERO_WEB = "https://api.zotero.org"
DEFAULT_VAULT = Path.cwd() DEFAULT_VAULT = Path.cwd()
SKILL_DIR = Path(__file__).resolve().parents[1]
DEFAULT_PRIVATE_CONFIG = SKILL_DIR / "config" / "config.local.json"
def fail(message: str) -> None: def fail(message: str) -> None:
@ -57,6 +62,27 @@ def load_dotenv(path: Path) -> None:
os.environ.setdefault(key, value) os.environ.setdefault(key, value)
def load_private_config(path: Path) -> None:
if not path.exists():
return
try:
config = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
fail(f"invalid private config JSON at {path}: {exc}")
zotero = config.get("zotero", {}) if isinstance(config.get("zotero"), dict) else {}
awesomegpt = config.get("awesomegpt", {}) if isinstance(config.get("awesomegpt"), dict) else {}
mappings = {
"ZOTERO_API_KEY": zotero.get("api_key"),
"ZOTERO_USER_ID": zotero.get("user_id"),
"AWESOMEGPT_API_KEY": awesomegpt.get("api_key"),
"AWESOMEGPT_BASE_URL": awesomegpt.get("base_url"),
"AWESOMEGPT_MODEL": awesomegpt.get("model"),
}
for key, value in mappings.items():
if value:
os.environ.setdefault(key, str(value))
def zotero_profile_prefs() -> Path | None: def zotero_profile_prefs() -> Path | None:
profiles_ini = Path.home() / "AppData/Roaming/Zotero/Zotero/profiles.ini" profiles_ini = Path.home() / "AppData/Roaming/Zotero/Zotero/profiles.ini"
profiles_root = profiles_ini.parent profiles_root = profiles_ini.parent
@ -502,10 +528,12 @@ def main() -> None:
parser.add_argument("--dry-run", action="store_true", help="generate but do not write Zotero note") parser.add_argument("--dry-run", action="store_true", help="generate but do not write Zotero note")
parser.add_argument("--vault", default=str(DEFAULT_VAULT), help="Obsidian vault containing 00 Templater") parser.add_argument("--vault", default=str(DEFAULT_VAULT), help="Obsidian vault containing 00 Templater")
parser.add_argument("--env-file", help="Optional .env path; defaults to <vault>/.env") parser.add_argument("--env-file", help="Optional .env path; defaults to <vault>/.env")
parser.add_argument("--config", default=str(DEFAULT_PRIVATE_CONFIG), help="Private local config JSON")
args = parser.parse_args() args = parser.parse_args()
vault = Path(args.vault).expanduser().resolve() vault = Path(args.vault).expanduser().resolve()
load_dotenv(Path(args.env_file).expanduser().resolve() if args.env_file else vault / ".env") load_dotenv(Path(args.env_file).expanduser().resolve() if args.env_file else vault / ".env")
load_private_config(Path(args.config).expanduser().resolve())
load_awesomegpt_prefs() load_awesomegpt_prefs()
keys = list(args.item_key) keys = list(args.item_key)

View File

@ -0,0 +1,70 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import getpass
import json
import os
from pathlib import Path
SKILL_DIR = Path(__file__).resolve().parents[1]
DEFAULT_CONFIG = SKILL_DIR / "config" / "config.local.json"
def ask_secret(label: str, existing: str = "") -> str:
suffix = " [keep existing]" if existing else ""
value = getpass.getpass(f"{label}{suffix}: ").strip()
return value or existing
def ask_text(label: str, default: str = "") -> str:
suffix = f" [{default}]" if default else ""
value = input(f"{label}{suffix}: ").strip()
return value or default
def load_existing(path: Path) -> dict:
if not path.exists():
return {}
return json.loads(path.read_text(encoding="utf-8"))
def write_private_config(path: Path, data: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
try:
os.chmod(path, 0o600)
except OSError:
pass
def main() -> int:
parser = argparse.ArgumentParser(description="Create private local config for zotero-obsidian-literature.")
parser.add_argument("--config", default=str(DEFAULT_CONFIG))
args = parser.parse_args()
path = Path(args.config).expanduser().resolve()
existing = load_existing(path)
zotero = existing.get("zotero", {}) if isinstance(existing.get("zotero"), dict) else {}
llm = existing.get("awesomegpt", {}) if isinstance(existing.get("awesomegpt"), dict) else {}
data = {
"zotero": {
"api_key": ask_secret("Zotero Web API key", zotero.get("api_key", "")),
"user_id": ask_text("Zotero user ID, optional", zotero.get("user_id", "")),
},
"awesomegpt": {
"api_key": ask_secret("DeepSeek/AwesomeGPT API key", llm.get("api_key", "")),
"base_url": ask_text("DeepSeek/AwesomeGPT base URL", llm.get("base_url", "https://api.deepseek.com")),
"model": ask_text("DeepSeek/AwesomeGPT model", llm.get("model", "deepseek-v4-pro")),
},
}
write_private_config(path, data)
print(f"Wrote private config: {path}")
print("Do not commit this file. It is ignored by the skill .gitignore.")
return 0
if __name__ == "__main__":
raise SystemExit(main())