From dd6e06665c28ff42fd4078bbd96aece697d370c2 Mon Sep 17 00:00:00 2001 From: qyh15 Date: Fri, 22 May 2026 16:19:11 +0800 Subject: [PATCH] Replace existing Zotero AI notes by default --- README.md | 4 +++- SKILL.md | 5 +++-- scripts/generate_zotero_ai_note.py | 35 +++++++++++++++++++++++++----- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1a4efcc..a0d745d 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ config/config.local.json ``` ```text -使用 MYwrite skill,为这个 Zotero 条目生成满血版深度阅读笔记:SXAIQUJT。不要跳过已有笔记,生成一条新的更详细 Zotero 子笔记。 +使用 MYwrite skill,为这个 Zotero 条目重新生成满血版深度阅读笔记:SXAIQUJT。覆盖已有 AI 子笔记,不要新建重复笔记。 ``` ```text @@ -71,6 +71,8 @@ config/config.local.json 如果需要和手动 AwesomeGPT 生成结果接近的详细笔记,请明确告诉 AI 使用“满血版”或“深度精读”模式。该模式会读取更多全文内容,并要求模型输出更完整的结构化笔记。 +重新生成笔记时,默认应覆盖已有 AI 子笔记,不要新建重复笔记。只有在明确需要对比两个版本时,才让 AI 另存为新的 Zotero 子笔记。 + ## 仓库内容 ```text diff --git a/SKILL.md b/SKILL.md index 3e1f557..69c09b3 100644 --- a/SKILL.md +++ b/SKILL.md @@ -50,10 +50,10 @@ py "$env:USERPROFILE\.codex\skills\MYwrite\scripts\generate_zotero_ai_note.py" - ### Deep full-detail item -Use this when the user asks for `满血版本`, `详细版`, `深度精读`, or says the batch note is not as detailed as manually generated AwesomeGPT notes. Do not use `--skip-existing` when the user wants to regenerate a fuller note; create a new Zotero child note unless the user explicitly asks to delete old notes. +Use this when the user asks for `满血版本`, `详细版`, `深度精读`, or says the batch note is not as detailed as manually generated AwesomeGPT notes. When regenerating, prefer `--replace-existing` so the existing AI child note is updated instead of creating duplicate Zotero child notes. ```powershell -py "$env:USERPROFILE\.codex\skills\MYwrite\scripts\generate_zotero_ai_note.py" --vault "C:\Users\qyh15\Documents\Obsidian Vault" --item-key SXAIQUJT --mode deep --fulltext-chars 80000 --max-tokens 12000 +py "$env:USERPROFILE\.codex\skills\MYwrite\scripts\generate_zotero_ai_note.py" --vault "C:\Users\qyh15\Documents\Obsidian Vault" --item-key SXAIQUJT --mode deep --fulltext-chars 80000 --max-tokens 12000 --replace-existing ``` ### Multiple items @@ -99,6 +99,7 @@ Default vault organization: ## Operating Rules - For live Zotero writes, state that Zotero child notes will be created. +- For regenerating notes, use `--replace-existing` by default; do not create duplicate child notes unless the user explicitly asks for a new comparison copy. - Use `--dry-run` for first-time validation or template changes. - Use `--fulltext-chars 4000` for cheap, lightweight notes; use `--mode deep --fulltext-chars 80000 --max-tokens 12000` when the user asks for a full-detail note. - Do not add visible machine markers to note bodies. Use the Zotero item link for duplicate detection. diff --git a/scripts/generate_zotero_ai_note.py b/scripts/generate_zotero_ai_note.py index 3c36978..d9db00a 100644 --- a/scripts/generate_zotero_ai_note.py +++ b/scripts/generate_zotero_ai_note.py @@ -275,8 +275,9 @@ def all_top_items(limit: int = 0) -> list[dict[str, Any]]: return items -def has_existing_ai_note(parent_key: str) -> bool: +def existing_ai_notes(parent_key: str) -> list[dict[str, Any]]: children = zotero_local(f"/items/{urllib.parse.quote(parent_key)}/children") + matches: list[dict[str, Any]] = [] for child in children or []: data = child.get("data") or {} if data.get("itemType") != "note": @@ -286,8 +287,12 @@ def has_existing_ai_note(parent_key: str) -> bool: full_note = zotero_local_optional(f"/items/{urllib.parse.quote(child['key'])}") note = ((full_note or {}).get("data") or {}).get("note") or "" if "AI文献笔记" in note or "AI Literature Note" in note or f"items/{parent_key}" in note: - return True - return False + matches.append(child) + return matches + + +def has_existing_ai_note(parent_key: str) -> bool: + return bool(existing_ai_notes(parent_key)) def export_bibtex(item_key: str) -> str: @@ -548,6 +553,17 @@ def create_child_note(user_id: str, parent_key: str, markdown: str, dry_run: boo return zotero_web(f"/users/{user_id}/items", method="POST", payload=payload) +def update_child_note(user_id: str, note_key: str, markdown: str, dry_run: bool) -> Any: + note = zotero_web(f"/users/{user_id}/items/{urllib.parse.quote(note_key)}") + data = note.get("data") if isinstance(note, dict) else None + if not isinstance(data, dict): + fail(f"could not load existing note {note_key}") + data["note"] = markdown_to_zotero_html(markdown) + if dry_run: + return {"dryRun": True, "itemKey": note_key, "payload": data} + return zotero_web(f"/users/{user_id}/items/{urllib.parse.quote(note_key)}", method="PUT", payload=data) + + def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--item-key", action="append", default=[], help="Zotero top-level item key; can be repeated") @@ -559,6 +575,7 @@ def main() -> None: parser.add_argument("--mode", choices=["quick", "deep"], default="quick", help="quick for batch notes; deep for full-detail notes") parser.add_argument("--max-tokens", type=int, default=0, help="Optional LLM output token limit; deep mode defaults to 12000") parser.add_argument("--skip-existing", action="store_true", help="Skip items that already have a generated AI child note") + parser.add_argument("--replace-existing", action="store_true", help="Update the first existing AI child note instead of creating a duplicate") 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") @@ -586,7 +603,8 @@ def main() -> None: if not key: results.append({"index": index, "status": "skipped", "reason": "missing key", "title": title}) continue - if args.skip_existing and has_existing_ai_note(key): + existing_notes = existing_ai_notes(key) + if args.skip_existing and existing_notes: results.append({"index": index, "itemKey": key, "title": title, "status": "skipped", "reason": "existing AI note"}) continue print(f"[{index}/{len(items)}] generating {key}: {title}", file=sys.stderr) @@ -600,8 +618,13 @@ def main() -> None: fulltext = local_fulltext(key, fulltext_chars) prompt = build_prompt(item, bibtex, fulltext, vault, args.mode) markdown = call_llm(prompt, max_tokens or None) - result = create_child_note(user_id, key, markdown, args.dry_run) - results.append({"index": index, "itemKey": key, "title": title, "status": "ok", "result": result}) + if args.replace_existing and existing_notes: + note_key = existing_notes[0].get("key") + result = update_child_note(user_id, str(note_key), markdown, args.dry_run) + results.append({"index": index, "itemKey": key, "title": title, "status": "ok", "action": "updated", "noteKey": note_key, "result": result}) + else: + result = create_child_note(user_id, key, markdown, args.dry_run) + results.append({"index": index, "itemKey": key, "title": title, "status": "ok", "action": "created", "result": result}) except SystemExit as exc: results.append({ "index": index,