From eaec7d0dd952add3e2c8b107cdeee476b75a8b08 Mon Sep 17 00:00:00 2001 From: qyh15 Date: Sat, 23 May 2026 14:29:30 +0800 Subject: [PATCH] Promote QQsci config for QQnote calls --- SKILL.md | 6 ++ config/config.template.json | 16 ++++- references/configuration.md | 96 +++++++++++++++++++++++++ references/zotero-obsidian-subskill.md | 14 ++-- scripts/run_qqnote_with_qqsci_config.py | 96 +++++++++++++++++++++++++ 5 files changed, 220 insertions(+), 8 deletions(-) create mode 100644 references/configuration.md create mode 100644 scripts/run_qqnote_with_qqsci_config.py diff --git a/SKILL.md b/SKILL.md index d20b46e..624065a 100644 --- a/SKILL.md +++ b/SKILL.md @@ -47,6 +47,7 @@ positioning, introduction logic, comparison, novelty framing, and citations. | [references/literature-routing.md](references/literature-routing.md) | User wants Zotero/Obsidian/DeepSeek note search, strong/weak related-paper screening, keyword extraction, or literature-grounded rewriting | | [references/word-citation-comments.md](references/word-citation-comments.md) | User wants citation insertion guidance in Word, reference placement, DOI-only comments, or annotated `.docx` citation suggestions | | [references/zotero-obsidian-subskill.md](references/zotero-obsidian-subskill.md) | User asks QQsci to obtain or refresh literature-note inputs through QQnote, including Zotero child-note summaries or paper comparison tables | +| [references/configuration.md](references/configuration.md) | You need DeepSeek, Zotero, Obsidian, QQnote inheritance, config precedence, secret handling, or wrapper-script usage | | [references/journal-positioning.md](references/journal-positioning.md) | Target journal is missing, uncertain, or needs recommendation and writing-position adjustment | ## Intake @@ -187,6 +188,11 @@ summaries or paper comparison tables with QQnote, use Important defaults: - QQnote is only the note/table generation tool. +- QQsci owns the top-level DeepSeek, Zotero, Obsidian, and QQnote routing + configuration for manuscript workflows. Open `references/configuration.md` + before calling QQnote from QQsci. +- QQnote must remain independently usable through its own environment variables, + vault `.env`, or AwesomeGPT/Zotero preferences when run outside QQsci. - QQsci owns keyword extraction, strong/weak related-paper screening, target journal positioning, comparative review, submission-package audit, and DOI-only Word citation comments. diff --git a/config/config.template.json b/config/config.template.json index 723c0f0..859e28b 100644 --- a/config/config.template.json +++ b/config/config.template.json @@ -2,18 +2,28 @@ "deepseek": { "api_key": "", "base_url": "https://api.deepseek.com", - "model": "deepseek-chat" + "model": "deepseek-chat", + "env_api_key": "AWESOMEGPT_API_KEY", + "env_base_url": "AWESOMEGPT_BASE_URL", + "env_model": "AWESOMEGPT_MODEL" }, "zotero": { "local_api": "http://127.0.0.1:23119/api/users/0", "web_api_key": "", "user_id": "", - "library_type": "user" + "library_type": "user", + "env_web_api_key": "ZOTERO_API_KEY", + "env_user_id": "ZOTERO_USER_ID" }, "obsidian": { - "vault_path": "", + "vault_path": "C:\\Users\\qyh15\\Documents\\Obsidian Vault", "zotero_integration_note_folder": "" }, + "qqnote": { + "skill_path": "C:\\Users\\qyh15\\.codex\\skills\\QQnote-skill", + "config_mode": "inherit_from_qqsci_when_called_by_qqsci", + "standalone_mode": "use_QQnote_environment_vault_env_or_AwesomeGPT_preferences" + }, "workflow": { "manifest_path": "state/qqsci-literature-manifest.jsonl", "skip_existing_notes": true, diff --git a/references/configuration.md b/references/configuration.md new file mode 100644 index 0000000..77c2f65 --- /dev/null +++ b/references/configuration.md @@ -0,0 +1,96 @@ +# QQsci Configuration + +Use this file when QQsci needs DeepSeek, Zotero, Obsidian, or QQnote access. + +## Ownership model + +QQsci is the top-level configuration owner for manuscript work. Store the +primary DeepSeek, Zotero, Obsidian, and QQnote routing settings in: + +```text +config/config.local.json +``` + +Never commit `config/config.local.json`, print secrets, or ask the user to paste +API keys into chat. + +QQnote remains independently usable. When QQnote is run by itself, it may use +its own environment variables, the vault `.env`, or AwesomeGPT/Zotero +preferences. When QQnote is called from QQsci, QQsci should inject its own +configuration into QQnote's expected environment variables. + +## Config precedence + +For QQsci-controlled manuscript workflows: + +1. Existing process environment variables override everything. +2. QQsci `config/config.local.json`. +3. QQsci `config/config.template.json` only for defaults, never secrets. +4. QQnote standalone fallbacks only when QQsci config is absent or the user asks + to run QQnote independently. + +For QQnote standalone workflows: + +1. Existing process environment variables. +2. Vault `.env`. +3. AwesomeGPT/Zotero profile preferences. +4. Any QQnote-specific local config if QQnote later adds one. + +## Environment mapping + +QQsci config maps to QQnote's existing environment variable interface: + +| QQsci config | QQnote env | +|---|---| +| `deepseek.api_key` | `AWESOMEGPT_API_KEY` | +| `deepseek.base_url` | `AWESOMEGPT_BASE_URL` | +| `deepseek.model` | `AWESOMEGPT_MODEL` | +| `zotero.web_api_key` | `ZOTERO_API_KEY` | +| `zotero.user_id` | `ZOTERO_USER_ID` | + +The Zotero local API remains: + +```text +http://127.0.0.1:23119/api/users/0 +``` + +unless the user explicitly changes it. + +## Calling QQnote from QQsci + +Prefer the wrapper: + +```powershell +py "$env:USERPROFILE\.codex\skills\qqsci\scripts\run_qqnote_with_qqsci_config.py" --script summarize_zotero_table -- --vault "C:\Users\qyh15\Documents\Obsidian Vault" --item-keys "SXAIQUJT X7GJZ627" +``` + +The wrapper loads QQsci local config, sets QQnote-compatible environment +variables for the child process, and does not print secrets. + +Use `--script generate_zotero_ai_note` when QQnote should create Zotero child +notes. + +Use `--script summarize_zotero_table` when QQsci needs a compact paper table. + +Use `--script audit_zotero_ai_notes` only for QQnote note-state maintenance, not +for QQsci's manuscript reasoning. + +## What belongs in QQsci + +QQsci owns: + +- manuscript keyword extraction +- strong/weak related-paper screening +- target-journal positioning +- comparative review against strong papers +- submission-package audit +- figure, format, spelling, and terminology checks +- DOI-only Word citation comments + +QQnote owns: + +- generating Zotero child-note summaries +- generating paper comparison tables from Zotero items +- optional QQnote note-state maintenance when explicitly requested + +QQnote output is an input to QQsci, not the decision engine. diff --git a/references/zotero-obsidian-subskill.md b/references/zotero-obsidian-subskill.md index b342daf..6396837 100644 --- a/references/zotero-obsidian-subskill.md +++ b/references/zotero-obsidian-subskill.md @@ -3,7 +3,8 @@ Use this file when QQsci needs literature-note inputs from QQnote. QQnote is only the tool for summarizing papers into Zotero child notes or -compact Markdown comparison tables. QQsci owns all manuscript-facing reasoning: +compact Markdown comparison tables. QQsci owns the top-level DeepSeek/Zotero +configuration for manuscript workflows and all manuscript-facing reasoning: keyword extraction, strong/weak related-paper screening, target-journal positioning, comparative review, submission-package audit, figure/format checks, terminology checks, and DOI-only Word citation comments. @@ -31,8 +32,8 @@ Do not default to creating standalone Obsidian markdown notes when the user says Use the installed `QQnote-skill` scripts only for note/table generation: ```powershell -py "$env:USERPROFILE\.codex\skills\QQnote-skill\scripts\generate_zotero_ai_note.py" --vault "C:\Users\qyh15\Documents\Obsidian Vault" --item-key SXAIQUJT --skip-existing -py "$env:USERPROFILE\.codex\skills\QQnote-skill\scripts\summarize_zotero_table.py" --vault "C:\Users\qyh15\Documents\Obsidian Vault" --item-keys "SXAIQUJT X7GJZ627" --batch-size 3 --out "C:\Users\qyh15\Documents\Obsidian Vault\99 misc\literature-comparison-table.md" +py "$env:USERPROFILE\.codex\skills\qqsci\scripts\run_qqnote_with_qqsci_config.py" --script generate_zotero_ai_note -- --vault "C:\Users\qyh15\Documents\Obsidian Vault" --item-key SXAIQUJT --skip-existing +py "$env:USERPROFILE\.codex\skills\qqsci\scripts\run_qqnote_with_qqsci_config.py" --script summarize_zotero_table -- --vault "C:\Users\qyh15\Documents\Obsidian Vault" --item-keys "SXAIQUJT X7GJZ627" --batch-size 3 --out "C:\Users\qyh15\Documents\Obsidian Vault\99 misc\literature-comparison-table.md" ``` Use `generate_zotero_ai_note.py` when new or selected papers need Zotero child @@ -47,8 +48,11 @@ logic unless the user explicitly asks for QQnote maintenance. ## Safety and config - Do not ask the user to paste API keys into chat. -- Follow QQnote's current config rules for DeepSeek/AwesomeGPT and Zotero - credentials when generating notes or tables. +- For QQsci manuscript workflows, load DeepSeek/AwesomeGPT and Zotero + credentials from QQsci `config/config.local.json` and inject them into QQnote + with `scripts/run_qqnote_with_qqsci_config.py`. +- QQnote must still be able to run standalone using its own environment + variables, vault `.env`, or AwesomeGPT/Zotero preferences. - Keep private config ignored and local-only. - Do not print secret-bearing config or preference lines. - Treat Zotero local API at `127.0.0.1:23119` as read-oriented. diff --git a/scripts/run_qqnote_with_qqsci_config.py b/scripts/run_qqnote_with_qqsci_config.py new file mode 100644 index 0000000..19b4e35 --- /dev/null +++ b/scripts/run_qqnote_with_qqsci_config.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Run QQnote scripts with QQsci config injected as environment variables.""" + +from __future__ import annotations + +import argparse +import json +import os +import subprocess +import sys +from pathlib import Path + + +SCRIPT_MAP = { + "generate_zotero_ai_note": "generate_zotero_ai_note.py", + "summarize_zotero_table": "summarize_zotero_table.py", + "audit_zotero_ai_notes": "audit_zotero_ai_notes.py", +} + + +def skill_root() -> Path: + return Path(__file__).resolve().parents[1] + + +def load_json(path: Path) -> dict: + if not path.exists(): + return {} + return json.loads(path.read_text(encoding="utf-8")) + + +def merged_config(root: Path) -> dict: + template = load_json(root / "config" / "config.template.json") + local = load_json(root / "config" / "config.local.json") + + def merge(base: dict, override: dict) -> dict: + out = dict(base) + for key, value in override.items(): + if isinstance(value, dict) and isinstance(out.get(key), dict): + out[key] = merge(out[key], value) + else: + out[key] = value + return out + + return merge(template, local) + + +def set_if_present(env: dict[str, str], key: str, value: object) -> None: + if key in env and env[key]: + return + if value is None: + return + text = str(value) + if text: + env[key] = text + + +def build_env(config: dict) -> dict[str, str]: + env = os.environ.copy() + deepseek = config.get("deepseek") or {} + zotero = config.get("zotero") or {} + + set_if_present(env, deepseek.get("env_api_key", "AWESOMEGPT_API_KEY"), deepseek.get("api_key")) + set_if_present(env, deepseek.get("env_base_url", "AWESOMEGPT_BASE_URL"), deepseek.get("base_url")) + set_if_present(env, deepseek.get("env_model", "AWESOMEGPT_MODEL"), deepseek.get("model")) + set_if_present(env, zotero.get("env_web_api_key", "ZOTERO_API_KEY"), zotero.get("web_api_key")) + set_if_present(env, zotero.get("env_user_id", "ZOTERO_USER_ID"), zotero.get("user_id")) + return env + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--script", required=True, choices=sorted(SCRIPT_MAP)) + parser.add_argument("--qqnote-path", default="") + parser.add_argument("qqnote_args", nargs=argparse.REMAINDER) + args = parser.parse_args() + + root = skill_root() + config = merged_config(root) + qqnote = Path(args.qqnote_path or (config.get("qqnote") or {}).get("skill_path") or "") + if not qqnote: + qqnote = Path.home() / ".codex" / "skills" / "QQnote-skill" + script = qqnote / "scripts" / SCRIPT_MAP[args.script] + if not script.exists(): + print(f"QQnote script not found: {script}", file=sys.stderr) + return 2 + + extra_args = list(args.qqnote_args) + if extra_args and extra_args[0] == "--": + extra_args = extra_args[1:] + + command = [sys.executable, str(script), *extra_args] + return subprocess.call(command, env=build_env(config)) + + +if __name__ == "__main__": + raise SystemExit(main())