From ecb090e613cd12202bc476fdfb5233e28e4d7860 Mon Sep 17 00:00:00 2001 From: qyh15 Date: Tue, 19 May 2026 23:44:55 +0800 Subject: [PATCH] Add private config setup for Zotero workflow --- .gitignore | 2 + SKILL.md | 19 ++++++-- config/config.template.json | 11 +++++ scripts/generate_zotero_ai_note.py | 28 ++++++++++++ scripts/init_private_config.py | 70 ++++++++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 config/config.template.json create mode 100644 scripts/init_private_config.py diff --git a/.gitignore b/.gitignore index b8c8130..7843709 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ __pycache__/ *.py[cod] .env +config/config.local.json +config/*.secret.json *.log *.tmp .zotero-ai-notes-index.json diff --git a/SKILL.md b/SKILL.md index 90b8dcc..28dd034 100644 --- a/SKILL.md +++ b/SKILL.md @@ -10,6 +10,7 @@ Use this skill for the user's Zotero + Obsidian literature workflow. It covers Z Core scripts: ```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\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 - `02 ...md` review-article template - `03 ...md` AI prompt -- `ZOTERO_API_KEY` must be available in the process environment 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. +- `ZOTERO_API_KEY` must be available for Zotero Web API writes. +- 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 diff --git a/config/config.template.json b/config/config.template.json new file mode 100644 index 0000000..a3445eb --- /dev/null +++ b/config/config.template.json @@ -0,0 +1,11 @@ +{ + "zotero": { + "api_key": "", + "user_id": "" + }, + "awesomegpt": { + "api_key": "", + "base_url": "https://api.deepseek.com", + "model": "deepseek-v4-pro" + } +} diff --git a/scripts/generate_zotero_ai_note.py b/scripts/generate_zotero_ai_note.py index 5a37667..e11670c 100644 --- a/scripts/generate_zotero_ai_note.py +++ b/scripts/generate_zotero_ai_note.py @@ -10,6 +10,9 @@ Required environment variables: Optional: ZOTERO_USER_ID If omitted, resolved from /keys/current + +Private local config: + \config\config.local.json, created by scripts\init_private_config.py """ from __future__ import annotations @@ -37,6 +40,8 @@ except Exception: LOCAL_ZOTERO = "http://127.0.0.1:23119/api/users/0" ZOTERO_WEB = "https://api.zotero.org" 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: @@ -57,6 +62,27 @@ def load_dotenv(path: Path) -> None: 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: profiles_ini = Path.home() / "AppData/Roaming/Zotero/Zotero/profiles.ini" 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("--vault", default=str(DEFAULT_VAULT), help="Obsidian vault containing 00 Templater") parser.add_argument("--env-file", help="Optional .env path; defaults to /.env") + parser.add_argument("--config", default=str(DEFAULT_PRIVATE_CONFIG), help="Private local config JSON") args = parser.parse_args() vault = Path(args.vault).expanduser().resolve() 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() keys = list(args.item_key) diff --git a/scripts/init_private_config.py b/scripts/init_private_config.py new file mode 100644 index 0000000..7c878aa --- /dev/null +++ b/scripts/init_private_config.py @@ -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())