From f14378c0b4941e4789afccdde2e28c8d1718a17e Mon Sep 17 00:00:00 2001 From: qyh15 Date: Sun, 5 Jul 2026 17:04:08 +0800 Subject: [PATCH] Use direct LLM config for qqzot --- README.md | 20 ++++++++++++++ SKILL.md | 18 ++++++++++-- scripts/generate_zotero_ai_note.py | 42 ++++++++++++++++++++++------ scripts/summarize_zotero_table.py | 44 ++++++++++++++++++++++++------ 4 files changed, 105 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index d3867c5..8268556 100644 --- a/README.md +++ b/README.md @@ -55,3 +55,23 @@ QQzot 不负责: - 需要调用 LLM 生成笔记或表格时,使用 AwesomeGPT 或 DeepSeek 相关环境配置。 不要把 API key 写入 skill 文件、Obsidian 笔记、Git 提交、终端输出或聊天记录。 +## LLM 直接配置 + +`qqzot` 默认使用脚本直接读取环境变量或 `.env`,不再默认读取 Zotero 的 AwesomeGPT/ZoteroGPT preferences。 + +推荐配置: + +```powershell +$env:DEEPSEEK_API_KEY="..." +$env:DEEPSEEK_MODEL="deepseek-chat" +``` + +也可以使用中性命名: + +```powershell +$env:QQZOT_LLM_API_KEY="..." +$env:QQZOT_LLM_BASE_URL="https://api.deepseek.com" +$env:QQZOT_LLM_MODEL="deepseek-chat" +``` + +只有显式加 `--use-awesomegpt-prefs` 时,脚本才会读取 AwesomeGPT/ZoteroGPT 的旧配置。 diff --git a/SKILL.md b/SKILL.md index 5b99f6c..637e5b8 100644 --- a/SKILL.md +++ b/SKILL.md @@ -27,8 +27,12 @@ Boundary: - Default Obsidian vault, only when a script needs templates or an output path: `C:\Users\qyh15\Documents\Obsidian Vault`. - `ZOTERO_API_KEY` is required only 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 direct script configuration, not Zotero AwesomeGPT by + default. Prefer `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`, and optional + `DEEPSEEK_MODEL`; or use `QQZOT_LLM_API_KEY`, `QQZOT_LLM_BASE_URL`, and + `QQZOT_LLM_MODEL` for a provider-neutral config. +- AwesomeGPT/ZoteroGPT preferences are legacy fallback only. Use + `--use-awesomegpt-prefs` explicitly if the user asks to reuse those settings. Never store API keys in skill files, vault helpers, Git commits, zip files, terminal output, or chat. If keys appear in logs or chat, tell the user to @@ -53,6 +57,16 @@ Use the smallest Zotero operation that answers the request: State clearly before live writes that Zotero child notes will be created. Use `--dry-run` for first-time validation or template changes. +Default LLM config should be direct: + +```powershell +$env:DEEPSEEK_API_KEY="..." +$env:DEEPSEEK_MODEL="deepseek-chat" +``` + +Do not rely on AwesomeGPT preferences unless the command includes +`--use-awesomegpt-prefs`. + Single item: ```powershell diff --git a/scripts/generate_zotero_ai_note.py b/scripts/generate_zotero_ai_note.py index e44ec17..58c91b7 100644 --- a/scripts/generate_zotero_ai_note.py +++ b/scripts/generate_zotero_ai_note.py @@ -3,11 +3,14 @@ Generate an AI literature note from Zotero metadata and save it as a Zotero child note. Required environment variables: - AWESOMEGPT_API_KEY DeepSeek/OpenAI-compatible API key - AWESOMEGPT_BASE_URL Example: https://api.deepseek.com - AWESOMEGPT_MODEL Example: deepseek-v4-pro + DEEPSEEK_API_KEY or QQZOT_LLM_API_KEY ZOTERO_API_KEY Zotero Web API key with library write permission +Optional LLM environment variables: + DEEPSEEK_BASE_URL or QQZOT_LLM_BASE_URL, default: https://api.deepseek.com + DEEPSEEK_MODEL or QQZOT_LLM_MODEL, default: deepseek-chat + OPENAI_API_KEY / OPENAI_BASE_URL / OPENAI_MODEL for other OpenAI-compatible APIs + Optional: ZOTERO_USER_ID If omitted, resolved from /keys/current """ @@ -57,6 +60,14 @@ def load_dotenv(path: Path) -> None: os.environ.setdefault(key, value) +def first_env(*names: str) -> str: + for name in names: + value = os.environ.get(name) + if value: + return value + return "" + + def zotero_profile_prefs() -> Path | None: profiles_ini = Path.home() / "AppData/Roaming/Zotero/Zotero/profiles.ini" profiles_root = profiles_ini.parent @@ -444,11 +455,20 @@ def build_prompt(item: dict[str, Any], bibtex: str, fulltext: str, vault: Path) def call_llm(prompt: str) -> str: - api_key = os.environ.get("AWESOMEGPT_API_KEY") - base_url = (os.environ.get("AWESOMEGPT_BASE_URL") or "").rstrip("/") - model = os.environ.get("AWESOMEGPT_MODEL") + api_key = first_env("QQZOT_LLM_API_KEY", "DEEPSEEK_API_KEY", "OPENAI_API_KEY", "AWESOMEGPT_API_KEY") + base_url = first_env("QQZOT_LLM_BASE_URL", "DEEPSEEK_BASE_URL", "OPENAI_BASE_URL", "AWESOMEGPT_BASE_URL") + model = first_env("QQZOT_LLM_MODEL", "DEEPSEEK_MODEL", "OPENAI_MODEL", "AWESOMEGPT_MODEL") + if api_key and not base_url and os.environ.get("DEEPSEEK_API_KEY"): + base_url = "https://api.deepseek.com" + if api_key and not model and os.environ.get("DEEPSEEK_API_KEY"): + model = "deepseek-chat" + base_url = base_url.rstrip("/") if not api_key or not base_url or not model: - fail("AWESOMEGPT_API_KEY, AWESOMEGPT_BASE_URL, and AWESOMEGPT_MODEL are required") + fail( + "LLM config is required. Set DEEPSEEK_API_KEY plus optional " + "DEEPSEEK_BASE_URL/DEEPSEEK_MODEL, or set QQZOT_LLM_API_KEY, " + "QQZOT_LLM_BASE_URL, and QQZOT_LLM_MODEL." + ) if not base_url.endswith("/v1"): base_url = base_url + "/v1" payload = { @@ -527,11 +547,17 @@ 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( + "--use-awesomegpt-prefs", + action="store_true", + help="Opt in to reading Zotero AwesomeGPT/ZoteroGPT preferences as a legacy config source.", + ) 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_awesomegpt_prefs() + if args.use_awesomegpt_prefs: + load_awesomegpt_prefs() keys = list(args.item_key) if args.item_keys: diff --git a/scripts/summarize_zotero_table.py b/scripts/summarize_zotero_table.py index 65c41fc..76241c8 100644 --- a/scripts/summarize_zotero_table.py +++ b/scripts/summarize_zotero_table.py @@ -3,12 +3,15 @@ Summarize Zotero items into a Markdown table using an OpenAI-compatible LLM. Required environment variables: - AWESOMEGPT_API_KEY DeepSeek/OpenAI-compatible API key - AWESOMEGPT_BASE_URL Example: https://api.deepseek.com - AWESOMEGPT_MODEL Example: deepseek-v4-pro + DEEPSEEK_API_KEY or QQZOT_LLM_API_KEY + +Optional LLM environment variables: + DEEPSEEK_BASE_URL or QQZOT_LLM_BASE_URL, default: https://api.deepseek.com + DEEPSEEK_MODEL or QQZOT_LLM_MODEL, default: deepseek-chat + OPENAI_API_KEY / OPENAI_BASE_URL / OPENAI_MODEL for other OpenAI-compatible APIs Optional: - Load from /.env or AwesomeGPT preferences in the default Zotero profile. + Load from /.env. """ from __future__ import annotations @@ -63,6 +66,14 @@ def load_dotenv(path: Path) -> None: os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'")) +def first_env(*names: str) -> str: + for name in names: + value = os.environ.get(name) + if value: + return value + return "" + + def zotero_profile_prefs() -> Path | None: profiles_ini = Path.home() / "AppData/Roaming/Zotero/Zotero/profiles.ini" profiles_root = profiles_ini.parent @@ -388,11 +399,20 @@ def build_prompt(records: list[dict[str, Any]], columns: list[str]) -> str: def call_llm(prompt: str) -> str: - api_key = os.environ.get("AWESOMEGPT_API_KEY") - base_url = (os.environ.get("AWESOMEGPT_BASE_URL") or "").rstrip("/") - model = os.environ.get("AWESOMEGPT_MODEL") + api_key = first_env("QQZOT_LLM_API_KEY", "DEEPSEEK_API_KEY", "OPENAI_API_KEY", "AWESOMEGPT_API_KEY") + base_url = first_env("QQZOT_LLM_BASE_URL", "DEEPSEEK_BASE_URL", "OPENAI_BASE_URL", "AWESOMEGPT_BASE_URL") + model = first_env("QQZOT_LLM_MODEL", "DEEPSEEK_MODEL", "OPENAI_MODEL", "AWESOMEGPT_MODEL") + if api_key and not base_url and os.environ.get("DEEPSEEK_API_KEY"): + base_url = "https://api.deepseek.com" + if api_key and not model and os.environ.get("DEEPSEEK_API_KEY"): + model = "deepseek-chat" + base_url = base_url.rstrip("/") if not api_key or not base_url or not model: - fail("AWESOMEGPT_API_KEY, AWESOMEGPT_BASE_URL, and AWESOMEGPT_MODEL are required") + fail( + "LLM config is required. Set DEEPSEEK_API_KEY plus optional " + "DEEPSEEK_BASE_URL/DEEPSEEK_MODEL, or set QQZOT_LLM_API_KEY, " + "QQZOT_LLM_BASE_URL, and QQZOT_LLM_MODEL." + ) if not base_url.endswith("/v1"): base_url = base_url + "/v1" payload = { @@ -458,11 +478,17 @@ def main() -> None: parser.add_argument("--vault", default=str(DEFAULT_VAULT), help="Obsidian vault containing optional .env") parser.add_argument("--env-file", help="Optional .env path; defaults to /.env") parser.add_argument("--out", help="Optional output Markdown file path") + parser.add_argument( + "--use-awesomegpt-prefs", + action="store_true", + help="Opt in to reading Zotero AwesomeGPT/ZoteroGPT preferences as a legacy config source.", + ) 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_awesomegpt_prefs() + if args.use_awesomegpt_prefs: + load_awesomegpt_prefs() keys = list(args.item_key) if args.item_keys: