diff --git a/AGENTS.md b/AGENTS.md index 42653d6..6d12382 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,7 @@ - `make lint`:使用 `markdownlint-cli` 校验全仓库 Markdown。 - `make build` / `make test` / `make clean`:目前为占位。 - 提示词转换:`cd libs/external/prompts-library && python main.py`。 +- JSONL 批处理(Gemini):`python libs/external/prompts-library/scripts/gemini_jsonl_batch.py --input 2 --output 2/prompts.jsonl`。 - 备份:`bash backups/一键备份.sh` 或 `python backups/快速备份.py`。 ## Coding Style & Naming Conventions diff --git a/i18n/zh/documents/02-方法论/GEMINI-HEADLESS.md b/i18n/zh/documents/02-方法论/GEMINI-HEADLESS.md index 0e63f77..b966875 100644 --- a/i18n/zh/documents/02-方法论/GEMINI-HEADLESS.md +++ b/i18n/zh/documents/02-方法论/GEMINI-HEADLESS.md @@ -1,42 +1,60 @@ -# Gemini 无头模式翻译指引 +# Gemini 无头模式 JSONL 规范化指引 -目标:在本地使用 Gemini CLI(gemini-2.5-flash)完成无交互批量翻译,避免工具调用与权限弹窗,适用于 prompts/skills/文档的快速机翻初稿。 +目标:在本地使用 Gemini CLI(gemini-2.5-flash)批量将提示词内容转换为标准 JSONL(`{"title": "...", "content": "..."}`),全程无交互、禁止工具调用,输出可直接落盘。 -## 原理概述 -- CLI 通过本地缓存的 Google 凭证直连 Gemini API,模型推理在云端完成。 -- 使用 `--allowed-tools ''` 关闭工具调用,确保只返回纯文本,不触发 shell/浏览器等动作。 -- 通过标准输入传入待翻译文本,标准输出获取结果,便于脚本流水线处理。 -- 可设置代理(http/https)让请求走本地代理节点,提升成功率与稳定性。 +## 工作原理 +- Gemini CLI 无独立 system slot,使用“位置参数 prompt”承载系统提示词;待处理文本通过 stdin 传入。 +- 通过 `--allowed-tools ''` 关闭工具调用,确保只返回模型文本。 +- 选用 `--output-format text`,配合系统提示词的“纯 JSONL 输出”约束,得到一行一个 JSON 对象。 +- 代理变量可选(`http_proxy/https_proxy`),按实际网络需要设置。 -## 基本命令 +## 系统提示词(请原样传入) +```text +{"category_id": 1, "category": "JSONL规范化", "row": 2, "col": 1, "title": "# JSONL 提示词转换器 - 系统提示词", "content": "# JSONL 提示词转换器 - 系统提示词\n\n你是一个专业 的提示词格式转换器。将用户提供的提示词内容转换为标准 JSONL 格式。\n\n## 输出格式\n\n```json\n{\"title\": \"<标题>\", \"content\": \"<完整内容>\"}\n```\n\n### 字段说明\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `title` | string | 提示词标题,取内容的第一行或前 50 字符 |\n| `content` | string | 完整的提示词内容 |\n\n## 转换规则\n\n1. **标题提取**:\n - 若内容以 `#` 开头,取第一个标题作为 title\n - 否则取前 50 字符(去除换行)\n2. **内容转义**:\n - 换行符 转为 `\\n`\n - 双引号转为 `\\\"`\n - 反斜杠转为 `\\\\`\n\n## 输出要求\n\n- 每行一个完整的 JSON 对象\n- 不要添加任何解释、注释或额外文字\n- 不要用 ```json 代码块包裹\n- 直接输出纯 JSONL 内容\n\n## 示例\n\n### 输入\n```\n# Role:智能文档助手\n\n## Background\n用户需要一个能够处理文档的 AI 助手。\n\n## Skills\n- 文档解析\n- 格式转换\n```\n\n### 输出\n```\n{\"title\": \"# Role:智能文档助手\", \"content\": \"# Role:智能文档助手\\n\\n## Background\\n用户需要一个能够处 理文档的 AI 助手。\\n\\n## Skills\\n- 文档解析\\n- 格式转换\"}\n```\n\n---\n\n现在,请将用户提供的内容转换为标准 JSONL 格式。"} +``` + +## 单文件示例 ```bash -# 代理(如需) -export http_proxy=http://127.0.0.1:9910 -export https_proxy=http://127.0.0.1:9910 +SYS_PROMPT_JSONL=$(cat <<'EOF' +{"category_id": 1, "category": "JSONL规范化", "row": 2, "col": 1, "title": "# JSONL 提示词转换器 - 系统提示词", "content": "# JSONL 提示词转换器 - 系统提示词\n\n你是一个专业 的提示词格式转换器。将用户提供的提示词内容转换为标准 JSONL 格式。\n\n## 输出格式\n\n```json\n{\"title\": \"<标题>\", \"content\": \"<完整内容>\"}\n```\n\n### 字段说明\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `title` | string | 提示词标题,取内容的第一行或前 50 字符 |\n| `content` | string | 完整的提示词内容 |\n\n## 转换规则\n\n1. **标题提取**:\n - 若内容以 `#` 开头,取第一个标题作为 title\n - 否则取前 50 字符(去除换行)\n2. **内容转义**:\n - 换行符 转为 `\\n`\n - 双引号转为 `\\\"`\n - 反斜杠转为 `\\\\`\n\n## 输出要求\n\n- 每行一个完整的 JSON 对象\n- 不要添加任何解释、注释或额外文字\n- 不要用 ```json 代码块包裹\n- 直接输出纯 JSONL 内容\n\n## 示例\n\n### 输入\n```\n# Role:智能文档助手\n\n## Background\n用户需要一个能够处理文档的 AI 助手。\n\n## Skills\n- 文档解析\n- 格式转换\n```\n\n### 输出\n```\n{\"title\": \"# Role:智能文档助手\", \"content\": \"# Role:智能文档助手\\n\\n## Background\\n用户需要一个能够处 理文档的 AI 助手。\\n\\n## Skills\\n- 文档解析\\n- 格式转换\"}\n```\n\n---\n\n现在,请将用户提供的内容转换为标准 JSONL 格式。"} +EOF +) -# 单条示例:中文 -> 英文 -printf '你好,翻译成英文。' | gemini -m gemini-2.5-flash \ +# 单条转换(stdin 输入提示词内容,stdout 得到单行 JSON) +cat 2/ASCII图生成.md | gemini -m gemini-2.5-flash \ --output-format text \ --allowed-tools '' \ - "Translate this to English." + "$SYS_PROMPT_JSONL" ``` -- 提示语放在位置参数即可(`-p/--prompt` 已被标记弃用)。 -- 输出为纯文本,可重定向保存。 +- CLI 会把系统提示词当作主提示,stdin 内容被模型视为“用户提供的待转换文本”。 +- 若需要代理,可在命令前设置 `http_proxy/https_proxy`。 -## 批量翻译文件示例(stdin → stdout) +## 批量处理目录 `2/` → 生成 `2/prompts.jsonl` ```bash -src=i18n/zh/prompts/README.md -dst=i18n/en/prompts/README.md -cat "$src" | gemini -m gemini-2.5-flash --output-format text --allowed-tools '' \ - "Translate to English; keep code fences unchanged." > "$dst" +SYS_PROMPT_JSONL=... # 同上 +out=2/prompts.jsonl +: > "$out" +for f in 2/*.md; do + [ -f "$f" ] || continue + cat "$f" | gemini -m gemini-2.5-flash \ + --output-format text \ + --allowed-tools '' \ + "$SYS_PROMPT_JSONL" >> "$out" +done ``` -- 可在脚本中循环多个文件;失败时检查退出码与输出。 +- 确保输出文件不被再次作为输入(可在循环中过滤 `*.jsonl`)。 +- 完成后可用 `wc -l 2/prompts.jsonl` 验证行数应等于处理的文件数。 -## 与现有 l10n-tool 的搭配 -- l10n-tool(deep-translator)用于全量机翻;若质量或连通性不稳,可改为逐文件走 Gemini CLI。 -- 流程:`cat 源文件 | gemini ... > 目标文件`;必要时在其他语种目录放跳转说明或手动校对。 +## 质量校验清单 +- 每行必须是合法 JSON,对象包含 `title` 与 `content` 两个字段。 +- 不得出现额外解释、空行或代码块定界符。 +- `title` 取首个 `#` 标题或去除换行后的前 50 字符;`content` 保留原文并正确转义。 +- 建议随机抽查 2–3 行,确认换行、引号、反斜杠均被转义为 `\\n` / `\\\"` / `\\\\`。 -## 注意事项 -- 确保 `gemini` 命令在 PATH 且已完成身份认证(首次运行会引导登录)。 -- 长文本建议分段,避免超时;代码块保持原样可在提示语中声明 “keep code fences unchanged”。 -- 代理端口依实际环境调整;如不需要代理,省略相关环境变量。 +## 常见故障 +- **输出混入 CLI 提示或日志**:确保命令中未开启 `--debug`,并避免在循环内打印 stdout。 +- **代理导致失败**:移除 `http_proxy/https_proxy` 或改用本地直连后重试。 +- **行数不匹配**:检查是否循环中包含 `*.jsonl` 自身或隐藏文件,必要时改为 `for f in 2/*.md; do ...; done`。 + +## 已生成的基线文件 +- 运行批量脚本后,本仓库已生成 `2/prompts.jsonl`,涵盖 `2/` 目录下所有 `.md` 提示词,可直接复用或作为后续增量基线。 diff --git a/libs/external/prompts-library/scripts/gemini_jsonl_batch.py b/libs/external/prompts-library/scripts/gemini_jsonl_batch.py new file mode 100644 index 0000000..46a68d8 --- /dev/null +++ b/libs/external/prompts-library/scripts/gemini_jsonl_batch.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +""" +使用 Gemini CLI 按固定系统提示词,将指定目录下的 .md 提示词批量转换为 JSONL。 + +特点: +- 内置系统提示词,与《Gemini 无头模式 JSONL 规范化指引》一致 +- 禁用工具调用 (--allowed-tools ''), 输出纯文本,每个文件生成一行 JSON +- 默认输入目录为仓库根下的 `2/`,输出为 `2/prompts.jsonl` + +用法示例: + python3 gemini_jsonl_batch.py + python3 gemini_jsonl_batch.py --input 2 --output 2/prompts.jsonl --model gemini-2.5-flash +""" +from __future__ import annotations + +import argparse +import subprocess +import sys +from pathlib import Path + +# ==================== 固定系统提示词 ==================== +SYS_PROMPT = """{"category_id": 1, "category": "JSONL规范化", "row": 2, "col": 1, "title": "# JSONL 提示词转换器 - 系统提示词", "content": "# JSONL 提示词转换器 - 系统提示词\n\n你是一个专业 的提示词格式转换器。将用户提供的提示词内容转换为标准 JSONL 格式。\n\n## 输出格式\n\n```json\n{\\"title\\\": \\"<标题>\\", \\"content\\\": \\"<完整内容>\\"}\n```\n\n### 字段说明\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `title` | string | 提示词标题,取内容的第一行或前 50 字符 |\n| `content` | string | 完整的提示词内容 |\n\n## 转换规则\n\n1. **标题提取**:\n - 若内容以 `#` 开头,取第一个标题作为 title\n - 否则取前 50 字符(去除换行)\n2. **内容转义**:\n - 换行符 转为 `\\\\n`\n - 双引号转为 `\\\\\"`\n - 反斜杠转为 `\\\\\\\\`\n\n## 输出要求\n\n- 每行一个完整的 JSON 对象\n- 不要添加任何解释、注释或额外文字\n- 不要用 ```json 代码块包裹\n- 直接输出纯 JSONL 内容\n\n## 示例\n\n### 输入\n```\n# Role:智能文档助手\n\n## Background\n用户需要一个能够处理文档的 AI 助手。\n\n## Skills\n- 文档解析\n- 格式转换\n```\n\n### 输出\n```\n{\\"title\\\": \\"# Role:智能文档助手\\", \\"content\\\": \\"# Role:智能文档助手\\\\n\\\\n## Background\\\\n用户需要一个能够处 理文档的 AI 助手。\\\\n\\\\n## Skills\\\\n- 文档解析\\\\n- 格式转换\\"}\n```\n\n---\n\n现在,请将用户提供的内容转换为标准 JSONL 格式。"}""" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="使用 Gemini CLI 批量将 .md 提示词转换为 JSONL(固定系统提示词)。", + ) + parser.add_argument( + "-i", + "--input", + type=Path, + default=Path("2"), + help="输入目录,遍历其中的 .md 文件(默认:仓库根目录下的 2/)", + ) + parser.add_argument( + "-o", + "--output", + type=Path, + default=None, + help="输出 JSONL 文件路径,默认写入 /prompts.jsonl", + ) + parser.add_argument( + "-m", + "--model", + default="gemini-2.5-flash", + help="Gemini 模型名称(默认:gemini-2.5-flash)", + ) + parser.add_argument( + "--gemini-cmd", + default="gemini", + help="Gemini CLI 可执行文件名或路径(默认:gemini)", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="输出处理中的详细信息", + ) + return parser.parse_args() + + +def run_gemini(content: str, model: str, cmd: str) -> str: + """调用 Gemini CLI,将单个文本转换为一行 JSON。""" + proc = subprocess.run( + [ + cmd, + "-m", + model, + "--output-format", + "text", + "--allowed-tools", + "", + SYS_PROMPT, + ], + input=content.encode("utf-8"), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + stdout = proc.stdout.decode("utf-8", errors="replace").strip() + stderr = proc.stderr.decode("utf-8", errors="replace").strip() + + if proc.returncode != 0: + raise RuntimeError(f"Gemini 调用失败 (code={proc.returncode}): {stderr or '无错误输出'}") + + if stderr: + # 某些 CLI 可能在 stderr 打印警告,保留但不中断 + print(f"⚠️ Gemini 警告: {stderr}", file=sys.stderr) + + if not stdout: + raise RuntimeError("Gemini 未返回内容") + + # 去除多余行,只保留非空行并合并 + lines = [ln for ln in stdout.splitlines() if ln.strip()] + return " ".join(lines).strip() + + +def main() -> None: + args = parse_args() + input_dir = args.input.resolve() + if not input_dir.exists() or not input_dir.is_dir(): + print(f"❌ 输入目录不存在: {input_dir}") + sys.exit(1) + + output_path = args.output or (input_dir / "prompts.jsonl") + output_path = output_path.resolve() + output_path.parent.mkdir(parents=True, exist_ok=True) + + md_files = sorted(f for f in input_dir.iterdir() if f.suffix == ".md") + if not md_files: + print(f"⚠️ 未找到任何 .md 文件: {input_dir}") + sys.exit(0) + + results = [] + for md in md_files: + content = md.read_text(encoding="utf-8") + if args.verbose: + print(f"→ 处理 {md.name}") + try: + json_line = run_gemini(content, args.model, args.gemini_cmd) + results.append(json_line) + except Exception as exc: # noqa: BLE001 + print(f"❌ 处理失败 {md.name}: {exc}", file=sys.stderr) + + with output_path.open("w", encoding="utf-8") as f: + for line in results: + f.write(line + "\n") + + print(f"✅ 完成:{len(results)} 条 → {output_path}") + + +if __name__ == "__main__": + main()