Use direct LLM config for qqzot

This commit is contained in:
qyh15 2026-07-05 17:04:08 +08:00
parent 2d15c23975
commit f14378c0b4
4 changed files with 105 additions and 19 deletions

View File

@ -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 的旧配置。

View File

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

View File

@ -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 <vault>/.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:

View File

@ -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 <vault>/.env or AwesomeGPT preferences in the default Zotero profile.
Load from <vault>/.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 <vault>/.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: