{"owner":"chatboxai","repo":"chatbox","hasSkills":true,"totalSkillsCount":6,"totalTokensCount":5129,"categories":["anthropic-skill","claude-rule"],"hasMcp":false,"mcpConfig":null,"found":[".claude/skills/i18n-translate/references/glossary.json",".claude/skills/i18n-translate/scripts/check-chatbox-i18n.mjs","scripts/session-rag-eval/SKILL.md",".claude/skills/i18n-translate/agents/openai.yaml",".claude/skills/i18n-translate/SKILL.md",".claude/skills/cherry-pick-pro/SKILL.md"],"skills":{".claude/skills/i18n-translate/references/glossary.json":"{\n  \"keepVerbatim\": [\n    \"Chatbox\",\n    \"AI\",\n    \"MCP\",\n    \"Deep Link\",\n    \"ID\",\n    \"API\",\n    \"URL\",\n    \"OCR\",\n    \"LaTeX\",\n    \"Token\",\n    \"Embedding\",\n    \"Rerank\",\n    \"Copilot\"\n  ],\n  \"translateAs\": {\n    \"ar\": {\n      \"Agent\": \"الوكيل\"\n    },\n    \"es\": {\n      \"Agent\": \"Agente\"\n    },\n    \"it-IT\": {\n      \"Agent\": \"Agente\"\n    },\n    \"ja\": {\n      \"Agent\": \"エージェント\"\n    },\n    \"ko\": {\n      \"Agent\": \"에이전트\"\n    },\n    \"pt-PT\": {\n      \"Agent\": \"Agente\"\n    },\n    \"ru\": {\n      \"Agent\": \"агент\"\n    },\n    \"zh-Hans\": {\n      \"Quota\": \"点数\",\n      \"Agent\": \"智能体\",\n      \"License Key\": \"许可证\",\n      \"License\": \"许可证\"\n    },\n    \"zh-Hant\": {\n      \"Quota\": \"點數\",\n      \"Agent\": \"智能體\",\n      \"License Key\": \"許可證\",\n      \"License\": \"許可證\"\n    }\n  },\n  \"notes\": [\n    \"keepVerbatim terms should remain in English in every locale unless a term is too short or ambiguous for deterministic checking.\",\n    \"translateAs terms are mandatory when the English source contains the listed term as a whole word.\",\n    \"License and License Key intentionally map to the same Chinese term without adding key/密钥/金鑰.\"\n  ]\n}\n",".claude/skills/i18n-translate/scripts/check-chatbox-i18n.mjs":"#!/usr/bin/env node\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst repoRoot = process.cwd()\nconst localeDir = path.join(repoRoot, 'src/renderer/i18n/locales')\nconst skillDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)))\nconst glossary = JSON.parse(fs.readFileSync(path.join(skillDir, 'references/glossary.json'), 'utf8'))\n\nconst localeArgIndex = process.argv.indexOf('--locales')\nconst requestedLocales =\n  localeArgIndex >= 0 && process.argv[localeArgIndex + 1]\n    ? new Set(process.argv[localeArgIndex + 1].split(',').map((s) => s.trim()).filter(Boolean))\n    : null\nconst keyFileIndex = process.argv.indexOf('--keys-file')\nconst strictKeys =\n  keyFileIndex >= 0 && process.argv[keyFileIndex + 1]\n    ? new Set(\n        fs\n          .readFileSync(process.argv[keyFileIndex + 1], 'utf8')\n          .split(/\\r?\\n/)\n          .map((line) => line.trim())\n          .filter(Boolean)\n      )\n    : null\nconst strictAll = process.argv.includes('--strict')\n\nfunction readJson(file) {\n  return JSON.parse(fs.readFileSync(file, 'utf8'))\n}\n\nfunction protectedTokens(text) {\n  return (String(text || '').match(/{{[^}]+}}|<\\/?[A-Za-z0-9]+>/g) || []).sort()\n}\n\nfunction sameMultiset(a, b) {\n  return a.length === b.length && a.every((value, index) => value === b[index])\n}\n\nfunction escapeRe(text) {\n  return text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\nfunction hasWholeWord(source, term) {\n  return new RegExp(`\\\\b${escapeRe(term)}\\\\b`, 'i').test(source)\n}\n\nfunction includesForcedTerm(translation, forced) {\n  return translation.toLowerCase().includes(forced.toLowerCase())\n}\n\nconst verbatimGuardTerms = glossary.keepVerbatim.filter((term) => /^[A-Za-z][A-Za-z0-9]{4,}$/.test(term))\n\nfunction hasStem(text, term) {\n  return new RegExp(`\\\\b${escapeRe(term)}`).test(text)\n}\n\nfunction entryIssues(source, translation, locale) {\n  const issues = []\n  if (!sameMultiset(protectedTokens(source), protectedTokens(translation))) {\n    issues.push('占位符/标签不一致')\n  }\n\n  const forcedTerms = glossary.translateAs[locale] || {}\n  for (const [term, forced] of Object.entries(forcedTerms)) {\n    if (hasWholeWord(source, term) && !includesForcedTerm(translation, forced)) {\n      issues.push(`缺少强制译法 ${term}=>${forced}`)\n    }\n  }\n\n  for (const term of verbatimGuardTerms) {\n    if (hasStem(source, term) && !hasStem(translation, term)) {\n      issues.push(`丢失应保留术语 ${term}`)\n    }\n  }\n\n  return issues\n}\n\nif (!fs.existsSync(localeDir)) {\n  console.error(`未找到 locale 目录：${localeDir}`)\n  process.exit(2)\n}\n\nconst enFile = path.join(localeDir, 'en/translation.json')\nconst en = readJson(enFile)\nconst enKeys = Object.keys(en).sort()\nconst localeNames = fs\n  .readdirSync(localeDir)\n  .filter((name) => fs.existsSync(path.join(localeDir, name, 'translation.json')))\n  .filter((name) => !requestedLocales || requestedLocales.has(name))\n  .sort()\n\nlet problemCount = 0\nlet warningCount = 0\n\nfor (const locale of localeNames) {\n  const file = path.join(localeDir, locale, 'translation.json')\n  const json = readJson(file)\n  const keys = Object.keys(json).sort()\n  const problems = []\n  const warnings = []\n\n  for (const key of enKeys) {\n    if (!(key in json)) {\n      problems.push({ key, issues: ['缺少 key'] })\n      continue\n    }\n\n    const value = json[key]\n    if (!value) {\n      problems.push({ key, issues: ['value 为空'] })\n      continue\n    }\n\n    const source = en[key] || key\n    if (locale === 'en') {\n      if (!source) problems.push({ key, issues: ['英文源文为空'] })\n      continue\n    }\n\n    const issues = entryIssues(source, value, locale)\n    if (issues.length) {\n      const strictForKey = strictAll || strictKeys?.has(key)\n      ;(strictForKey ? problems : warnings).push({ key, issues })\n    }\n  }\n\n  for (const key of keys) {\n    if (!(key in en)) problems.push({ key, issues: ['多余 key：en 中不存在'] })\n  }\n\n  if (problems.length) {\n    problemCount += problems.length\n    console.log(`✗ ${locale}: ${problems.length} 个问题`)\n    for (const { key, issues } of problems.slice(0, 80)) {\n      console.log(`    ${JSON.stringify(key.slice(0, 100))}: ${issues.join('; ')}`)\n    }\n    if (problems.length > 80) console.log(`    ... 还有 ${problems.length - 80} 个`)\n  } else {\n    console.log(`✓ ${locale}: 通过（${keys.length} 个 key）`)\n  }\n  if (warnings.length) {\n    warningCount += warnings.length\n    console.log(`  警告 ${locale}: ${warnings.length} 个`)\n    for (const { key, issues } of warnings.slice(0, 30)) {\n      console.log(`    ${JSON.stringify(key.slice(0, 100))}: ${issues.join('; ')}`)\n    }\n    if (warnings.length > 30) console.log(`    ... 还有 ${warnings.length - 30} 个`)\n  }\n}\n\nif (problemCount) {\n  console.error(`\\n发现 ${problemCount} 个 i18n 问题。`)\n  process.exit(1)\n}\n\nconsole.log(\n  warningCount\n    ? `\\n已检查的 locale 文件 key 都完整。另有 ${warningCount} 个占位符/glossary 警告。`\n    : '\\n已检查的 locale 文件 key 完整，且通过 glossary/占位符校验。'\n)\n","scripts/session-rag-eval/SKILL.md":"---\nname: session-rag-eval\ndescription: Run and debug Chatbox session attachment RAG model evaluation with synthetic and real long-file fixtures.\n---\n\n# Session Attachment RAG Eval\n\nUse this skill when validating file question-answering behavior for large chat attachments.\n\n## Goal\n\nVerify both sides of model behavior:\n\n- The model calls `query_session_attachment` when the answer depends on an uploaded large file.\n- The model avoids retrieval for clearly unrelated user requests.\n\nPrefer the Chatbox conversation-flow harness for product validation because it exercises the real renderer, config,\nlicense, local API, file upload, indexing, tool registration, and persisted messages.\n\n## Fixtures\n\nDefault fixture repo:\n\n```bash\n../../chatbox-session-rag-eval-fixtures\n```\n\nFixture types:\n\n- synthetic long documents with hidden needle facts;\n- Wikipedia-derived real long documents with source attribution;\n- direct positive cases;\n- implicit positive cases where the user does not explicitly ask to search the file;\n- unrelated negative cases.\n\nRegenerate fixtures in the fixture repo:\n\n```bash\nnode scripts/generate-fixtures.mjs\nnode scripts/fetch-real-fixtures.mjs\n```\n\n## Chatbox Flow\n\nStart the local API before running the harness. Then build with `USE_LOCAL_API=true`; the renderer API origin is compiled\ninto the bundle.\n\n```bash\nUSE_LOCAL_API=true node ./node_modules/electron-vite/bin/electron-vite.js build --mode development\npnpm eval:session-rag:chatbox -- --case long-citrine-threshold --keep-user-data\n```\n\nThe harness copies the real `config.json` into an isolated temporary userDataDir, injects a temporary default chat model\nif missing, and stores the session RAG sqlite DB at a separate temporary path.\n\n## Known Failure Modes\n\n- \"This attachment is too large...\" means `session_attachment_embedding` capability is false or unavailable for the\n  active local API/license path.\n- If requests go to production, rebuild with `USE_LOCAL_API=true`.\n- If Electron hangs at startup, check stale CDP/Electron processes. The harness now allocates a free CDP port.\n- If the assistant says the file is still indexing after DB says ready, inspect the persisted user message. The message\n  file must have `sessionAttachmentIndexStatus: \"ready\"` before context building.\n- Do not trust a log line that says the message was submitted unless the textarea cleared and the session contains the\n  user message.\n\n## Useful Cases\n\n```bash\npnpm eval:session-rag:chatbox -- --case long-citrine-threshold\npnpm eval:session-rag:chatbox -- --case implicit-citrine-current-policy\npnpm eval:session-rag:chatbox -- --case real-wiki-apollo-implicit-landing-site\npnpm eval:session-rag:chatbox -- --case multi-turn-real-wiki-apollo-followup\npnpm eval:session-rag:chatbox -- --case unrelated-simple-math\npnpm eval:session-rag:chatbox -- --case real-wiki-unrelated-capital\n```\n\nFor fast model-behavior iteration without Electron:\n\n```bash\npnpm eval:session-rag -- --dry-run\npnpm eval:session-rag -- --case implicit-citrine-current-policy\n```\n",".claude/skills/i18n-translate/agents/openai.yaml":"interface:\n  display_name: \"i18n Translate\"\n  short_description: \"翻译 Chatbox 改动中的 UI 文案\"\n  default_prompt: \"使用 $i18n-translate 翻译我当前改动引入的 i18n key。\"\n\npolicy:\n  allow_implicit_invocation: true\n",".claude/skills/i18n-translate/SKILL.md":"---\nname: i18n-translate\ndescription: 翻译 Chatbox Pro 当前未提交改动、暂存改动或用户指定 commit/range 中新增或受影响的 i18n key。当用户要求翻译、补全与改动 UI 文案相关的翻译时使用。Agent 需要阅读 diff 和组件上下文，遵守 skill 内置 glossary，并直接写 locale JSON；脚本只用于最后校验。\nmetadata:\n  short-description: 翻译 Chatbox 改动中的 i18n key\n---\n\n# i18n-translate\n\n你一名UI翻译者。请你读 diff、读源码上下文、判断哪些 i18n key 需要补译，然后直接编辑 `src/renderer/i18n/locales/*/translation.json`。注意不要运行 `pnpm translate`，也不要依赖抽取脚本来决定翻译内容。你(LLM)来检查哪些 key 需要翻译，并且直接翻译成目标语言。\n\n## 必读\n\n- 翻译前读取 `references/glossary.json`，这是本 skill 的术语表来源。\n- 默认处理工作区中所有未提交改动（包括已暂存和未暂存）；如果用户提供单个 commit SHA 或 Git range，则仅处理该范围内新增或修改的 key。\n- 如果用户提供的 commit 或 range 无法解析，请停止并向用户报告该输入无效，而不是猜测要处理的文件。\n\n## 怎么找 key\n\n从 diff/commit 中找新增或受影响的文案，重点看：\n\n- `t('...')`、`i18n.t('...')`\n- `<Trans i18nKey=\"...\">`\n- `i18nKey: '...'`，尤其是错误对象或 `src/shared` 中的 key\n- `src/renderer/i18n/for-key-scan.ts` 等动态 key 辅助文件\n- `src/renderer/i18n/locales/en/translation.json` 中被改动的英文 value\n\n\n## 翻译原则\n\n- 先理解 key 所在文件、组件、UI 角色和周围文案，再翻译。\n- UI 角色包括按钮、菜单、tooltip、aria label、placeholder、错误提示、toast、modal、设置项、状态文案、聊天消息和产品文案。\n- 参考同一组件中目标语言已有译文，保持语气和术语一致。\n- 以 `en[key] || key` 作为源文；大多数情况下 `en[key]` 应等于 key。先补全 `en/translation.json` 中缺失或空的 key，再翻译其他语言。\n- 如果 `en` 文案发生变化，更新所有已请求 locale 中对应 key 的翻译值；如果某个已有翻译需要更新而不是新增，请直接覆盖旧译文。\n- 当 `en` value 和源码 key 不一致时，按 3 步判断：\n  1. 先判断 `en` value 是否是有意的 slug key。\n  2. 如果不是，再检查是否由 `<Trans>` children 或动态 key 扫描造成的源文错误。\n  3. 只有在确认是源文错误时才修改源码，否则只更新 JSON。\n- 保留所有 `{{placeholder}}`、`<NamedTag>`、`<0>` 标签和源文前后空格。\n- 遵守 glossary 的保留词和强制译法。不要机械直译，写自然、符合目标语言 UI 习惯的译文。\n- 按语言逐个翻译：一种语言翻译完就立即写入该语言的 `translation.json`，再开始下一种语言。不要把所有语言的译文都攒到最后一次性写入。\n- 如果同一个英文 key 在不同 UI 角色中需要不同译法，先不要直接给出不同译文；停止并询问用户是否应将 key 改为更具体的名称，除非仓库已经有明确的命名约定。\n\n## 完成前\n\n可运行 checker 做确定性检查：\n\n```bash\nnode .codex/skills/i18n-translate/scripts/check-chatbox-i18n.mjs\n```\n\n如果你维护了本次改动 key 列表，可以做严格检查：\n\n```bash\nnode .codex/skills/i18n-translate/scripts/check-chatbox-i18n.mjs --keys-file /tmp/changed-i18n-keys.txt\n```\n\nchecker 默认只因缺 key、空 value、多余 key 失败；历史 glossary/placeholder 问题会作为 warning。不要提交 commit，把改动留给用户 review。\n",".claude/skills/cherry-pick-pro/SKILL.md":"---\nname: cherry-pick-pro\ndescription: Cherry-pick commits from chatbox-pro repo to the open chatbox repo, handling mobile-only files, package name differences, and other repo-specific exclusions.\nuser_invocable: true\nread_when:\n  - Cherry-picking from pro to open repo\n  - Syncing pro commits to open source repo\n  - Releasing open source version\n---\n\n# Cherry Pick Pro\n\nCherry-pick commits from the `chatbox-pro` repo (`pro` remote) to the open `chatbox` repo, filtering out mobile-only changes and adjusting for repo differences.\n\n## Usage\n\n```\n/cherry-pick-pro [from_commit] [to_commit]\n```\n\n- `from_commit`: (optional) A **`pro/main` commit hash** to start from (exclusive). Must be a commit on `pro/main`, not an open-repo commit — see step 2. Defaults to auto-detecting the last synced pro commit.\n- `to_commit`: (optional) The commit hash to end at (inclusive). Defaults to `pro/main` HEAD.\n\n## Workflow\n\n### 1. Sync remotes first\n```bash\ngit fetch pro\ngit fetch origin\n```\n\n⚠️ **Tag pollution**: `git fetch pro` also fetches pro's tags (e.g. `vX.Y.Z` created on pro's release branch). These local tags point at **pro commits**, not open repo commits. Never push them as-is — see step 8.\n\n### 2. Identify commits to cherry-pick\n\n⚠️ The range bound **must be a `pro/main` commit**. The open repo and `pro/main` share no history (cherry-picks create new SHAs), so an open-repo commit hash as the lower bound makes `A..pro/main` return *all* of pro's history, not the increment.\n\nFind the last synced pro commit by reading the `cherry picked from pro commit <hash>` footer that step 5 writes into each open-repo commit, then list pro commits after it:\n```bash\n# last synced pro commit (the lower bound)\nlast=$(git log --grep=\"cherry picked from pro commit\" -1 --pretty=%B HEAD \\\n  | grep -oE 'pro commit [0-9a-f]+' | awk '{print $3}')\ngit log \"$last\"..pro/main --oneline --reverse  # commits to pick\n```\nIf no footer is found (first-ever sync), pass `from_commit` explicitly.\n\n### 3. Analyze each commit\nFor each commit, check the files it touches:\n```bash\ngit diff-tree --no-commit-id --name-only -r <commit>\n```\n\n### 4. Cherry-pick rules\n\n#### Files/directories to SKIP (delete if conflict):\n- `android/` - mobile-only, not in open repo\n- `ios/` - mobile-only, not in open repo\n- `capacitor.config.ts` - mobile config\n- `.github/` - open repo has no CI; workflow/config differs (see step 8)\n- `CLAUDE.md`, `AGENTS.md` - pro-specific AI config\n- `.cursorrules` - pro-specific\n- `.claude/` - pro-specific skills/config (including this skill itself), never sync to open repo\n\n#### Package name differences:\n- Pro uses: `xyz.chatboxapp.app`\n- Open uses: `xyz.chatboxapp.ce`\n- When resolving conflicts in `package.json` or `release/app/package.json`, always keep the open repo name (`xyz.chatboxapp.ce`)\n\n#### Pro-only dependencies to remove if added:\n- `@playwright/test` - pro testing only\n- `playwright` - pro testing only\n- `@ruguoapp/jk-analytics` - pro analytics only\n\n#### `pnpm-lock.yaml`:\n- Will almost always conflict. Accept either side, then run `pnpm install --no-frozen-lockfile` at the end to regenerate.\n\n### 5. Cherry-pick process\nFor each commit:\n1. Try `git cherry-pick <hash>` directly first\n2. If conflicts occur, use `--no-commit` and resolve:\n   - Remove mobile-only files with `git rm`\n   - Fix package name conflicts (keep `ce`)\n   - Remove pro-only dependencies\n   - Accept either side for `pnpm-lock.yaml`\n3. Commit with original message + `(cherry picked from pro commit <hash>)` note\n\n### 6. Finalize\nAfter all cherry-picks:\n```bash\npnpm install --no-frozen-lockfile  # regenerate lockfile\ngit add pnpm-lock.yaml\ngit commit -m \"chore: regenerate pnpm-lock.yaml for open repo\"\n```\n\n### 7. Verify\n```bash\ngit log --oneline <start>..HEAD  # review all new commits\npnpm install  # ensure deps resolve\n```\n\n### 8. Release (if syncing a version release)\n\nThe open repo has **no CI workflows** — releases are manual:\n\n1. Push the sync branch to `origin/main`\n2. Re-create the version tag on the **open repo's** commit. The local tag fetched from pro points at a pro commit — pushing it would leak pro's commit history into the open repo:\n   ```bash\n   git tag -d vX.Y.Z                 # delete pro's tag fetched in step 1\n   git tag vX.Y.Z <open-repo-head>   # re-create on open repo commit\n   git push origin vX.Y.Z\n   ```\n3. Create the GitHub Release manually (no binary assets; notes = changelog + install links, copy format from the previous release):\n   ```bash\n   gh release view vX.Y.(Z-1) --json body --jq '.body'  # get template\n   gh release create vX.Y.Z --title \"vX.Y.Z\" --latest --notes \"...\"\n   ```\n\nVersion bump convention: only `release/app/package.json` gets the new version; root `package.json` stays `0.0.1` in the open repo.\n\n## Example\n\n```\n/cherry-pick-pro\n```\nThis will auto-detect the last release on the current branch and cherry-pick all newer commits from `pro/main`.\n"},"files":{".claude/skills/i18n-translate/references/glossary.json":"{\n  \"keepVerbatim\": [\n    \"Chatbox\",\n    \"AI\",\n    \"MCP\",\n    \"Deep Link\",\n    \"ID\",\n    \"API\",\n    \"URL\",\n    \"OCR\",\n    \"LaTeX\",\n    \"Token\",\n    \"Embedding\",\n    \"Rerank\",\n    \"Copilot\"\n  ],\n  \"translateAs\": {\n    \"ar\": {\n      \"Agent\": \"الوكيل\"\n    },\n    \"es\": {\n      \"Agent\": \"Agente\"\n    },\n    \"it-IT\": {\n      \"Agent\": \"Agente\"\n    },\n    \"ja\": {\n      \"Agent\": \"エージェント\"\n    },\n    \"ko\": {\n      \"Agent\": \"에이전트\"\n    },\n    \"pt-PT\": {\n      \"Agent\": \"Agente\"\n    },\n    \"ru\": {\n      \"Agent\": \"агент\"\n    },\n    \"zh-Hans\": {\n      \"Quota\": \"点数\",\n      \"Agent\": \"智能体\",\n      \"License Key\": \"许可证\",\n      \"License\": \"许可证\"\n    },\n    \"zh-Hant\": {\n      \"Quota\": \"點數\",\n      \"Agent\": \"智能體\",\n      \"License Key\": \"許可證\",\n      \"License\": \"許可證\"\n    }\n  },\n  \"notes\": [\n    \"keepVerbatim terms should remain in English in every locale unless a term is too short or ambiguous for deterministic checking.\",\n    \"translateAs terms are mandatory when the English source contains the listed term as a whole word.\",\n    \"License and License Key intentionally map to the same Chinese term without adding key/密钥/金鑰.\"\n  ]\n}\n",".claude/skills/i18n-translate/scripts/check-chatbox-i18n.mjs":"#!/usr/bin/env node\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst repoRoot = process.cwd()\nconst localeDir = path.join(repoRoot, 'src/renderer/i18n/locales')\nconst skillDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)))\nconst glossary = JSON.parse(fs.readFileSync(path.join(skillDir, 'references/glossary.json'), 'utf8'))\n\nconst localeArgIndex = process.argv.indexOf('--locales')\nconst requestedLocales =\n  localeArgIndex >= 0 && process.argv[localeArgIndex + 1]\n    ? new Set(process.argv[localeArgIndex + 1].split(',').map((s) => s.trim()).filter(Boolean))\n    : null\nconst keyFileIndex = process.argv.indexOf('--keys-file')\nconst strictKeys =\n  keyFileIndex >= 0 && process.argv[keyFileIndex + 1]\n    ? new Set(\n        fs\n          .readFileSync(process.argv[keyFileIndex + 1], 'utf8')\n          .split(/\\r?\\n/)\n          .map((line) => line.trim())\n          .filter(Boolean)\n      )\n    : null\nconst strictAll = process.argv.includes('--strict')\n\nfunction readJson(file) {\n  return JSON.parse(fs.readFileSync(file, 'utf8'))\n}\n\nfunction protectedTokens(text) {\n  return (String(text || '').match(/{{[^}]+}}|<\\/?[A-Za-z0-9]+>/g) || []).sort()\n}\n\nfunction sameMultiset(a, b) {\n  return a.length === b.length && a.every((value, index) => value === b[index])\n}\n\nfunction escapeRe(text) {\n  return text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\nfunction hasWholeWord(source, term) {\n  return new RegExp(`\\\\b${escapeRe(term)}\\\\b`, 'i').test(source)\n}\n\nfunction includesForcedTerm(translation, forced) {\n  return translation.toLowerCase().includes(forced.toLowerCase())\n}\n\nconst verbatimGuardTerms = glossary.keepVerbatim.filter((term) => /^[A-Za-z][A-Za-z0-9]{4,}$/.test(term))\n\nfunction hasStem(text, term) {\n  return new RegExp(`\\\\b${escapeRe(term)}`).test(text)\n}\n\nfunction entryIssues(source, translation, locale) {\n  const issues = []\n  if (!sameMultiset(protectedTokens(source), protectedTokens(translation))) {\n    issues.push('占位符/标签不一致')\n  }\n\n  const forcedTerms = glossary.translateAs[locale] || {}\n  for (const [term, forced] of Object.entries(forcedTerms)) {\n    if (hasWholeWord(source, term) && !includesForcedTerm(translation, forced)) {\n      issues.push(`缺少强制译法 ${term}=>${forced}`)\n    }\n  }\n\n  for (const term of verbatimGuardTerms) {\n    if (hasStem(source, term) && !hasStem(translation, term)) {\n      issues.push(`丢失应保留术语 ${term}`)\n    }\n  }\n\n  return issues\n}\n\nif (!fs.existsSync(localeDir)) {\n  console.error(`未找到 locale 目录：${localeDir}`)\n  process.exit(2)\n}\n\nconst enFile = path.join(localeDir, 'en/translation.json')\nconst en = readJson(enFile)\nconst enKeys = Object.keys(en).sort()\nconst localeNames = fs\n  .readdirSync(localeDir)\n  .filter((name) => fs.existsSync(path.join(localeDir, name, 'translation.json')))\n  .filter((name) => !requestedLocales || requestedLocales.has(name))\n  .sort()\n\nlet problemCount = 0\nlet warningCount = 0\n\nfor (const locale of localeNames) {\n  const file = path.join(localeDir, locale, 'translation.json')\n  const json = readJson(file)\n  const keys = Object.keys(json).sort()\n  const problems = []\n  const warnings = []\n\n  for (const key of enKeys) {\n    if (!(key in json)) {\n      problems.push({ key, issues: ['缺少 key'] })\n      continue\n    }\n\n    const value = json[key]\n    if (!value) {\n      problems.push({ key, issues: ['value 为空'] })\n      continue\n    }\n\n    const source = en[key] || key\n    if (locale === 'en') {\n      if (!source) problems.push({ key, issues: ['英文源文为空'] })\n      continue\n    }\n\n    const issues = entryIssues(source, value, locale)\n    if (issues.length) {\n      const strictForKey = strictAll || strictKeys?.has(key)\n      ;(strictForKey ? problems : warnings).push({ key, issues })\n    }\n  }\n\n  for (const key of keys) {\n    if (!(key in en)) problems.push({ key, issues: ['多余 key：en 中不存在'] })\n  }\n\n  if (problems.length) {\n    problemCount += problems.length\n    console.log(`✗ ${locale}: ${problems.length} 个问题`)\n    for (const { key, issues } of problems.slice(0, 80)) {\n      console.log(`    ${JSON.stringify(key.slice(0, 100))}: ${issues.join('; ')}`)\n    }\n    if (problems.length > 80) console.log(`    ... 还有 ${problems.length - 80} 个`)\n  } else {\n    console.log(`✓ ${locale}: 通过（${keys.length} 个 key）`)\n  }\n  if (warnings.length) {\n    warningCount += warnings.length\n    console.log(`  警告 ${locale}: ${warnings.length} 个`)\n    for (const { key, issues } of warnings.slice(0, 30)) {\n      console.log(`    ${JSON.stringify(key.slice(0, 100))}: ${issues.join('; ')}`)\n    }\n    if (warnings.length > 30) console.log(`    ... 还有 ${warnings.length - 30} 个`)\n  }\n}\n\nif (problemCount) {\n  console.error(`\\n发现 ${problemCount} 个 i18n 问题。`)\n  process.exit(1)\n}\n\nconsole.log(\n  warningCount\n    ? `\\n已检查的 locale 文件 key 都完整。另有 ${warningCount} 个占位符/glossary 警告。`\n    : '\\n已检查的 locale 文件 key 完整，且通过 glossary/占位符校验。'\n)\n","scripts/session-rag-eval/SKILL.md":"---\nname: session-rag-eval\ndescription: Run and debug Chatbox session attachment RAG model evaluation with synthetic and real long-file fixtures.\n---\n\n# Session Attachment RAG Eval\n\nUse this skill when validating file question-answering behavior for large chat attachments.\n\n## Goal\n\nVerify both sides of model behavior:\n\n- The model calls `query_session_attachment` when the answer depends on an uploaded large file.\n- The model avoids retrieval for clearly unrelated user requests.\n\nPrefer the Chatbox conversation-flow harness for product validation because it exercises the real renderer, config,\nlicense, local API, file upload, indexing, tool registration, and persisted messages.\n\n## Fixtures\n\nDefault fixture repo:\n\n```bash\n../../chatbox-session-rag-eval-fixtures\n```\n\nFixture types:\n\n- synthetic long documents with hidden needle facts;\n- Wikipedia-derived real long documents with source attribution;\n- direct positive cases;\n- implicit positive cases where the user does not explicitly ask to search the file;\n- unrelated negative cases.\n\nRegenerate fixtures in the fixture repo:\n\n```bash\nnode scripts/generate-fixtures.mjs\nnode scripts/fetch-real-fixtures.mjs\n```\n\n## Chatbox Flow\n\nStart the local API before running the harness. Then build with `USE_LOCAL_API=true`; the renderer API origin is compiled\ninto the bundle.\n\n```bash\nUSE_LOCAL_API=true node ./node_modules/electron-vite/bin/electron-vite.js build --mode development\npnpm eval:session-rag:chatbox -- --case long-citrine-threshold --keep-user-data\n```\n\nThe harness copies the real `config.json` into an isolated temporary userDataDir, injects a temporary default chat model\nif missing, and stores the session RAG sqlite DB at a separate temporary path.\n\n## Known Failure Modes\n\n- \"This attachment is too large...\" means `session_attachment_embedding` capability is false or unavailable for the\n  active local API/license path.\n- If requests go to production, rebuild with `USE_LOCAL_API=true`.\n- If Electron hangs at startup, check stale CDP/Electron processes. The harness now allocates a free CDP port.\n- If the assistant says the file is still indexing after DB says ready, inspect the persisted user message. The message\n  file must have `sessionAttachmentIndexStatus: \"ready\"` before context building.\n- Do not trust a log line that says the message was submitted unless the textarea cleared and the session contains the\n  user message.\n\n## Useful Cases\n\n```bash\npnpm eval:session-rag:chatbox -- --case long-citrine-threshold\npnpm eval:session-rag:chatbox -- --case implicit-citrine-current-policy\npnpm eval:session-rag:chatbox -- --case real-wiki-apollo-implicit-landing-site\npnpm eval:session-rag:chatbox -- --case multi-turn-real-wiki-apollo-followup\npnpm eval:session-rag:chatbox -- --case unrelated-simple-math\npnpm eval:session-rag:chatbox -- --case real-wiki-unrelated-capital\n```\n\nFor fast model-behavior iteration without Electron:\n\n```bash\npnpm eval:session-rag -- --dry-run\npnpm eval:session-rag -- --case implicit-citrine-current-policy\n```\n",".claude/skills/i18n-translate/agents/openai.yaml":"interface:\n  display_name: \"i18n Translate\"\n  short_description: \"翻译 Chatbox 改动中的 UI 文案\"\n  default_prompt: \"使用 $i18n-translate 翻译我当前改动引入的 i18n key。\"\n\npolicy:\n  allow_implicit_invocation: true\n",".claude/skills/i18n-translate/SKILL.md":"---\nname: i18n-translate\ndescription: 翻译 Chatbox Pro 当前未提交改动、暂存改动或用户指定 commit/range 中新增或受影响的 i18n key。当用户要求翻译、补全与改动 UI 文案相关的翻译时使用。Agent 需要阅读 diff 和组件上下文，遵守 skill 内置 glossary，并直接写 locale JSON；脚本只用于最后校验。\nmetadata:\n  short-description: 翻译 Chatbox 改动中的 i18n key\n---\n\n# i18n-translate\n\n你一名UI翻译者。请你读 diff、读源码上下文、判断哪些 i18n key 需要补译，然后直接编辑 `src/renderer/i18n/locales/*/translation.json`。注意不要运行 `pnpm translate`，也不要依赖抽取脚本来决定翻译内容。你(LLM)来检查哪些 key 需要翻译，并且直接翻译成目标语言。\n\n## 必读\n\n- 翻译前读取 `references/glossary.json`，这是本 skill 的术语表来源。\n- 默认处理工作区中所有未提交改动（包括已暂存和未暂存）；如果用户提供单个 commit SHA 或 Git range，则仅处理该范围内新增或修改的 key。\n- 如果用户提供的 commit 或 range 无法解析，请停止并向用户报告该输入无效，而不是猜测要处理的文件。\n\n## 怎么找 key\n\n从 diff/commit 中找新增或受影响的文案，重点看：\n\n- `t('...')`、`i18n.t('...')`\n- `<Trans i18nKey=\"...\">`\n- `i18nKey: '...'`，尤其是错误对象或 `src/shared` 中的 key\n- `src/renderer/i18n/for-key-scan.ts` 等动态 key 辅助文件\n- `src/renderer/i18n/locales/en/translation.json` 中被改动的英文 value\n\n\n## 翻译原则\n\n- 先理解 key 所在文件、组件、UI 角色和周围文案，再翻译。\n- UI 角色包括按钮、菜单、tooltip、aria label、placeholder、错误提示、toast、modal、设置项、状态文案、聊天消息和产品文案。\n- 参考同一组件中目标语言已有译文，保持语气和术语一致。\n- 以 `en[key] || key` 作为源文；大多数情况下 `en[key]` 应等于 key。先补全 `en/translation.json` 中缺失或空的 key，再翻译其他语言。\n- 如果 `en` 文案发生变化，更新所有已请求 locale 中对应 key 的翻译值；如果某个已有翻译需要更新而不是新增，请直接覆盖旧译文。\n- 当 `en` value 和源码 key 不一致时，按 3 步判断：\n  1. 先判断 `en` value 是否是有意的 slug key。\n  2. 如果不是，再检查是否由 `<Trans>` children 或动态 key 扫描造成的源文错误。\n  3. 只有在确认是源文错误时才修改源码，否则只更新 JSON。\n- 保留所有 `{{placeholder}}`、`<NamedTag>`、`<0>` 标签和源文前后空格。\n- 遵守 glossary 的保留词和强制译法。不要机械直译，写自然、符合目标语言 UI 习惯的译文。\n- 按语言逐个翻译：一种语言翻译完就立即写入该语言的 `translation.json`，再开始下一种语言。不要把所有语言的译文都攒到最后一次性写入。\n- 如果同一个英文 key 在不同 UI 角色中需要不同译法，先不要直接给出不同译文；停止并询问用户是否应将 key 改为更具体的名称，除非仓库已经有明确的命名约定。\n\n## 完成前\n\n可运行 checker 做确定性检查：\n\n```bash\nnode .codex/skills/i18n-translate/scripts/check-chatbox-i18n.mjs\n```\n\n如果你维护了本次改动 key 列表，可以做严格检查：\n\n```bash\nnode .codex/skills/i18n-translate/scripts/check-chatbox-i18n.mjs --keys-file /tmp/changed-i18n-keys.txt\n```\n\nchecker 默认只因缺 key、空 value、多余 key 失败；历史 glossary/placeholder 问题会作为 warning。不要提交 commit，把改动留给用户 review。\n",".claude/skills/cherry-pick-pro/SKILL.md":"---\nname: cherry-pick-pro\ndescription: Cherry-pick commits from chatbox-pro repo to the open chatbox repo, handling mobile-only files, package name differences, and other repo-specific exclusions.\nuser_invocable: true\nread_when:\n  - Cherry-picking from pro to open repo\n  - Syncing pro commits to open source repo\n  - Releasing open source version\n---\n\n# Cherry Pick Pro\n\nCherry-pick commits from the `chatbox-pro` repo (`pro` remote) to the open `chatbox` repo, filtering out mobile-only changes and adjusting for repo differences.\n\n## Usage\n\n```\n/cherry-pick-pro [from_commit] [to_commit]\n```\n\n- `from_commit`: (optional) A **`pro/main` commit hash** to start from (exclusive). Must be a commit on `pro/main`, not an open-repo commit — see step 2. Defaults to auto-detecting the last synced pro commit.\n- `to_commit`: (optional) The commit hash to end at (inclusive). Defaults to `pro/main` HEAD.\n\n## Workflow\n\n### 1. Sync remotes first\n```bash\ngit fetch pro\ngit fetch origin\n```\n\n⚠️ **Tag pollution**: `git fetch pro` also fetches pro's tags (e.g. `vX.Y.Z` created on pro's release branch). These local tags point at **pro commits**, not open repo commits. Never push them as-is — see step 8.\n\n### 2. Identify commits to cherry-pick\n\n⚠️ The range bound **must be a `pro/main` commit**. The open repo and `pro/main` share no history (cherry-picks create new SHAs), so an open-repo commit hash as the lower bound makes `A..pro/main` return *all* of pro's history, not the increment.\n\nFind the last synced pro commit by reading the `cherry picked from pro commit <hash>` footer that step 5 writes into each open-repo commit, then list pro commits after it:\n```bash\n# last synced pro commit (the lower bound)\nlast=$(git log --grep=\"cherry picked from pro commit\" -1 --pretty=%B HEAD \\\n  | grep -oE 'pro commit [0-9a-f]+' | awk '{print $3}')\ngit log \"$last\"..pro/main --oneline --reverse  # commits to pick\n```\nIf no footer is found (first-ever sync), pass `from_commit` explicitly.\n\n### 3. Analyze each commit\nFor each commit, check the files it touches:\n```bash\ngit diff-tree --no-commit-id --name-only -r <commit>\n```\n\n### 4. Cherry-pick rules\n\n#### Files/directories to SKIP (delete if conflict):\n- `android/` - mobile-only, not in open repo\n- `ios/` - mobile-only, not in open repo\n- `capacitor.config.ts` - mobile config\n- `.github/` - open repo has no CI; workflow/config differs (see step 8)\n- `CLAUDE.md`, `AGENTS.md` - pro-specific AI config\n- `.cursorrules` - pro-specific\n- `.claude/` - pro-specific skills/config (including this skill itself), never sync to open repo\n\n#### Package name differences:\n- Pro uses: `xyz.chatboxapp.app`\n- Open uses: `xyz.chatboxapp.ce`\n- When resolving conflicts in `package.json` or `release/app/package.json`, always keep the open repo name (`xyz.chatboxapp.ce`)\n\n#### Pro-only dependencies to remove if added:\n- `@playwright/test` - pro testing only\n- `playwright` - pro testing only\n- `@ruguoapp/jk-analytics` - pro analytics only\n\n#### `pnpm-lock.yaml`:\n- Will almost always conflict. Accept either side, then run `pnpm install --no-frozen-lockfile` at the end to regenerate.\n\n### 5. Cherry-pick process\nFor each commit:\n1. Try `git cherry-pick <hash>` directly first\n2. If conflicts occur, use `--no-commit` and resolve:\n   - Remove mobile-only files with `git rm`\n   - Fix package name conflicts (keep `ce`)\n   - Remove pro-only dependencies\n   - Accept either side for `pnpm-lock.yaml`\n3. Commit with original message + `(cherry picked from pro commit <hash>)` note\n\n### 6. Finalize\nAfter all cherry-picks:\n```bash\npnpm install --no-frozen-lockfile  # regenerate lockfile\ngit add pnpm-lock.yaml\ngit commit -m \"chore: regenerate pnpm-lock.yaml for open repo\"\n```\n\n### 7. Verify\n```bash\ngit log --oneline <start>..HEAD  # review all new commits\npnpm install  # ensure deps resolve\n```\n\n### 8. Release (if syncing a version release)\n\nThe open repo has **no CI workflows** — releases are manual:\n\n1. Push the sync branch to `origin/main`\n2. Re-create the version tag on the **open repo's** commit. The local tag fetched from pro points at a pro commit — pushing it would leak pro's commit history into the open repo:\n   ```bash\n   git tag -d vX.Y.Z                 # delete pro's tag fetched in step 1\n   git tag vX.Y.Z <open-repo-head>   # re-create on open repo commit\n   git push origin vX.Y.Z\n   ```\n3. Create the GitHub Release manually (no binary assets; notes = changelog + install links, copy format from the previous release):\n   ```bash\n   gh release view vX.Y.(Z-1) --json body --jq '.body'  # get template\n   gh release create vX.Y.Z --title \"vX.Y.Z\" --latest --notes \"...\"\n   ```\n\nVersion bump convention: only `release/app/package.json` gets the new version; root `package.json` stays `0.0.1` in the open repo.\n\n## Example\n\n```\n/cherry-pick-pro\n```\nThis will auto-detect the last release on the current branch and cherry-pick all newer commits from `pro/main`.\n"},"items":[{"name":"SKILL.md","path":".claude/skills/cherry-pick-pro/SKILL.md","rawUrl":"https://raw.githubusercontent.com/chatboxai/chatbox/HEAD/.claude/skills/cherry-pick-pro/SKILL.md","title":"cherry-pick-pro","category":"anthropic-skill","format":"markdown","content":"---\nname: cherry-pick-pro\ndescription: Cherry-pick commits from chatbox-pro repo to the open chatbox repo, handling mobile-only files, package name differences, and other repo-specific exclusions.\nuser_invocable: true\nread_when:\n  - Cherry-picking from pro to open repo\n  - Syncing pro commits to open source repo\n  - Releasing open source version\n---\n\n# Cherry Pick Pro\n\nCherry-pick commits from the `chatbox-pro` repo (`pro` remote) to the open `chatbox` repo, filtering out mobile-only changes and adjusting for repo differences.\n\n## Usage\n\n```\n/cherry-pick-pro [from_commit] [to_commit]\n```\n\n- `from_commit`: (optional) A **`pro/main` commit hash** to start from (exclusive). Must be a commit on `pro/main`, not an open-repo commit — see step 2. Defaults to auto-detecting the last synced pro commit.\n- `to_commit`: (optional) The commit hash to end at (inclusive). Defaults to `pro/main` HEAD.\n\n## Workflow\n\n### 1. Sync remotes first\n```bash\ngit fetch pro\ngit fetch origin\n```\n\n⚠️ **Tag pollution**: `git fetch pro` also fetches pro's tags (e.g. `vX.Y.Z` created on pro's release branch). These local tags point at **pro commits**, not open repo commits. Never push them as-is — see step 8.\n\n### 2. Identify commits to cherry-pick\n\n⚠️ The range bound **must be a `pro/main` commit**. The open repo and `pro/main` share no history (cherry-picks create new SHAs), so an open-repo commit hash as the lower bound makes `A..pro/main` return *all* of pro's history, not the increment.\n\nFind the last synced pro commit by reading the `cherry picked from pro commit <hash>` footer that step 5 writes into each open-repo commit, then list pro commits after it:\n```bash\n# last synced pro commit (the lower bound)\nlast=$(git log --grep=\"cherry picked from pro commit\" -1 --pretty=%B HEAD \\\n  | grep -oE 'pro commit [0-9a-f]+' | awk '{print $3}')\ngit log \"$last\"..pro/main --oneline --reverse  # commits to pick\n```\nIf no footer is found (first-ever sync), pass `from_commit` explicitly.\n\n### 3. Analyze each commit\nFor each commit, check the files it touches:\n```bash\ngit diff-tree --no-commit-id --name-only -r <commit>\n```\n\n### 4. Cherry-pick rules\n\n#### Files/directories to SKIP (delete if conflict):\n- `android/` - mobile-only, not in open repo\n- `ios/` - mobile-only, not in open repo\n- `capacitor.config.ts` - mobile config\n- `.github/` - open repo has no CI; workflow/config differs (see step 8)\n- `CLAUDE.md`, `AGENTS.md` - pro-specific AI config\n- `.cursorrules` - pro-specific\n- `.claude/` - pro-specific skills/config (including this skill itself), never sync to open repo\n\n#### Package name differences:\n- Pro uses: `xyz.chatboxapp.app`\n- Open uses: `xyz.chatboxapp.ce`\n- When resolving conflicts in `package.json` or `release/app/package.json`, always keep the open repo name (`xyz.chatboxapp.ce`)\n\n#### Pro-only dependencies to remove if added:\n- `@playwright/test` - pro testing only\n- `playwright` - pro testing only\n- `@ruguoapp/jk-analytics` - pro analytics only\n\n#### `pnpm-lock.yaml`:\n- Will almost always conflict. Accept either side, then run `pnpm install --no-frozen-lockfile` at the end to regenerate.\n\n### 5. Cherry-pick process\nFor each commit:\n1. Try `git cherry-pick <hash>` directly first\n2. If conflicts occur, use `--no-commit` and resolve:\n   - Remove mobile-only files with `git rm`\n   - Fix package name conflicts (keep `ce`)\n   - Remove pro-only dependencies\n   - Accept either side for `pnpm-lock.yaml`\n3. Commit with original message + `(cherry picked from pro commit <hash>)` note\n\n### 6. Finalize\nAfter all cherry-picks:\n```bash\npnpm install --no-frozen-lockfile  # regenerate lockfile\ngit add pnpm-lock.yaml\ngit commit -m \"chore: regenerate pnpm-lock.yaml for open repo\"\n```\n\n### 7. Verify\n```bash\ngit log --oneline <start>..HEAD  # review all new commits\npnpm install  # ensure deps resolve\n```\n\n### 8. Release (if syncing a version release)\n\nThe open repo has **no CI workflows** — releases are manual:\n\n1. Push the sync branch to `origin/main`\n2. Re-create the version tag on the **open repo's** commit. The local tag fetched from pro points at a pro commit — pushing it would leak pro's commit history into the open repo:\n   ```bash\n   git tag -d vX.Y.Z                 # delete pro's tag fetched in step 1\n   git tag vX.Y.Z <open-repo-head>   # re-create on open repo commit\n   git push origin vX.Y.Z\n   ```\n3. Create the GitHub Release manually (no binary assets; notes = changelog + install links, copy format from the previous release):\n   ```bash\n   gh release view vX.Y.(Z-1) --json body --jq '.body'  # get template\n   gh release create vX.Y.Z --title \"vX.Y.Z\" --latest --notes \"...\"\n   ```\n\nVersion bump convention: only `release/app/package.json` gets the new version; root `package.json` stays `0.0.1` in the open repo.\n\n## Example\n\n```\n/cherry-pick-pro\n```\nThis will auto-detect the last release on the current branch and cherry-pick all newer commits from `pro/main`.\n","frontmatter":{"name":"cherry-pick-pro","description":"Cherry-pick commits from chatbox-pro repo to the open chatbox repo, handling mobile-only files, package name differences, and other repo-specific exclusions.","user_invocable":true,"read_when":["Cherry-picking from pro to open repo","Syncing pro commits to open source repo","Releasing open source version"]},"isInternal":false,"tokens":1319,"sizeBytes":4936},{"name":"openai.yaml","path":".claude/skills/i18n-translate/agents/openai.yaml","rawUrl":"https://raw.githubusercontent.com/chatboxai/chatbox/HEAD/.claude/skills/i18n-translate/agents/openai.yaml","title":"Agents Skill","category":"anthropic-skill","format":"yaml","content":"interface:\n  display_name: \"i18n Translate\"\n  short_description: \"翻译 Chatbox 改动中的 UI 文案\"\n  default_prompt: \"使用 $i18n-translate 翻译我当前改动引入的 i18n key。\"\n\npolicy:\n  allow_implicit_invocation: true\n","isInternal":false,"tokens":73,"sizeBytes":234},{"name":"glossary.json","path":".claude/skills/i18n-translate/references/glossary.json","rawUrl":"https://raw.githubusercontent.com/chatboxai/chatbox/HEAD/.claude/skills/i18n-translate/references/glossary.json","title":"References Skill","category":"anthropic-skill","format":"json","content":"{\n  \"keepVerbatim\": [\n    \"Chatbox\",\n    \"AI\",\n    \"MCP\",\n    \"Deep Link\",\n    \"ID\",\n    \"API\",\n    \"URL\",\n    \"OCR\",\n    \"LaTeX\",\n    \"Token\",\n    \"Embedding\",\n    \"Rerank\",\n    \"Copilot\"\n  ],\n  \"translateAs\": {\n    \"ar\": {\n      \"Agent\": \"الوكيل\"\n    },\n    \"es\": {\n      \"Agent\": \"Agente\"\n    },\n    \"it-IT\": {\n      \"Agent\": \"Agente\"\n    },\n    \"ja\": {\n      \"Agent\": \"エージェント\"\n    },\n    \"ko\": {\n      \"Agent\": \"에이전트\"\n    },\n    \"pt-PT\": {\n      \"Agent\": \"Agente\"\n    },\n    \"ru\": {\n      \"Agent\": \"агент\"\n    },\n    \"zh-Hans\": {\n      \"Quota\": \"点数\",\n      \"Agent\": \"智能体\",\n      \"License Key\": \"许可证\",\n      \"License\": \"许可证\"\n    },\n    \"zh-Hant\": {\n      \"Quota\": \"點數\",\n      \"Agent\": \"智能體\",\n      \"License Key\": \"許可證\",\n      \"License\": \"許可證\"\n    }\n  },\n  \"notes\": [\n    \"keepVerbatim terms should remain in English in every locale unless a term is too short or ambiguous for deterministic checking.\",\n    \"translateAs terms are mandatory when the English source contains the listed term as a whole word.\",\n    \"License and License Key intentionally map to the same Chinese term without adding key/密钥/金鑰.\"\n  ]\n}\n","frontmatter":{"keepVerbatim":["Chatbox","AI","MCP","Deep Link","ID","API","URL","OCR","LaTeX","Token","Embedding","Rerank","Copilot"],"translateAs":{"ar":{"Agent":"الوكيل"},"es":{"Agent":"Agente"},"it-IT":{"Agent":"Agente"},"ja":{"Agent":"エージェント"},"ko":{"Agent":"에이전트"},"pt-PT":{"Agent":"Agente"},"ru":{"Agent":"агент"},"zh-Hans":{"Quota":"点数","Agent":"智能体","License Key":"许可证","License":"许可证"},"zh-Hant":{"Quota":"點數","Agent":"智能體","License Key":"許可證","License":"許可證"}},"notes":["keepVerbatim terms should remain in English in every locale unless a term is too short or ambiguous for deterministic checking.","translateAs terms are mandatory when the English source contains the listed term as a whole word.","License and License Key intentionally map to the same Chinese term without adding key/密钥/金鑰."]},"isInternal":false,"tokens":376,"sizeBytes":1198},{"name":"SKILL.md","path":".claude/skills/i18n-translate/SKILL.md","rawUrl":"https://raw.githubusercontent.com/chatboxai/chatbox/HEAD/.claude/skills/i18n-translate/SKILL.md","title":"i18n-translate","category":"anthropic-skill","format":"markdown","content":"---\nname: i18n-translate\ndescription: 翻译 Chatbox Pro 当前未提交改动、暂存改动或用户指定 commit/range 中新增或受影响的 i18n key。当用户要求翻译、补全与改动 UI 文案相关的翻译时使用。Agent 需要阅读 diff 和组件上下文，遵守 skill 内置 glossary，并直接写 locale JSON；脚本只用于最后校验。\nmetadata:\n  short-description: 翻译 Chatbox 改动中的 i18n key\n---\n\n# i18n-translate\n\n你一名UI翻译者。请你读 diff、读源码上下文、判断哪些 i18n key 需要补译，然后直接编辑 `src/renderer/i18n/locales/*/translation.json`。注意不要运行 `pnpm translate`，也不要依赖抽取脚本来决定翻译内容。你(LLM)来检查哪些 key 需要翻译，并且直接翻译成目标语言。\n\n## 必读\n\n- 翻译前读取 `references/glossary.json`，这是本 skill 的术语表来源。\n- 默认处理工作区中所有未提交改动（包括已暂存和未暂存）；如果用户提供单个 commit SHA 或 Git range，则仅处理该范围内新增或修改的 key。\n- 如果用户提供的 commit 或 range 无法解析，请停止并向用户报告该输入无效，而不是猜测要处理的文件。\n\n## 怎么找 key\n\n从 diff/commit 中找新增或受影响的文案，重点看：\n\n- `t('...')`、`i18n.t('...')`\n- `<Trans i18nKey=\"...\">`\n- `i18nKey: '...'`，尤其是错误对象或 `src/shared` 中的 key\n- `src/renderer/i18n/for-key-scan.ts` 等动态 key 辅助文件\n- `src/renderer/i18n/locales/en/translation.json` 中被改动的英文 value\n\n\n## 翻译原则\n\n- 先理解 key 所在文件、组件、UI 角色和周围文案，再翻译。\n- UI 角色包括按钮、菜单、tooltip、aria label、placeholder、错误提示、toast、modal、设置项、状态文案、聊天消息和产品文案。\n- 参考同一组件中目标语言已有译文，保持语气和术语一致。\n- 以 `en[key] || key` 作为源文；大多数情况下 `en[key]` 应等于 key。先补全 `en/translation.json` 中缺失或空的 key，再翻译其他语言。\n- 如果 `en` 文案发生变化，更新所有已请求 locale 中对应 key 的翻译值；如果某个已有翻译需要更新而不是新增，请直接覆盖旧译文。\n- 当 `en` value 和源码 key 不一致时，按 3 步判断：\n  1. 先判断 `en` value 是否是有意的 slug key。\n  2. 如果不是，再检查是否由 `<Trans>` children 或动态 key 扫描造成的源文错误。\n  3. 只有在确认是源文错误时才修改源码，否则只更新 JSON。\n- 保留所有 `{{placeholder}}`、`<NamedTag>`、`<0>` 标签和源文前后空格。\n- 遵守 glossary 的保留词和强制译法。不要机械直译，写自然、符合目标语言 UI 习惯的译文。\n- 按语言逐个翻译：一种语言翻译完就立即写入该语言的 `translation.json`，再开始下一种语言。不要把所有语言的译文都攒到最后一次性写入。\n- 如果同一个英文 key 在不同 UI 角色中需要不同译法，先不要直接给出不同译文；停止并询问用户是否应将 key 改为更具体的名称，除非仓库已经有明确的命名约定。\n\n## 完成前\n\n可运行 checker 做确定性检查：\n\n```bash\nnode .codex/skills/i18n-translate/scripts/check-chatbox-i18n.mjs\n```\n\n如果你维护了本次改动 key 列表，可以做严格检查：\n\n```bash\nnode .codex/skills/i18n-translate/scripts/check-chatbox-i18n.mjs --keys-file /tmp/changed-i18n-keys.txt\n```\n\nchecker 默认只因缺 key、空 value、多余 key 失败；历史 glossary/placeholder 问题会作为 warning。不要提交 commit，把改动留给用户 review。\n","frontmatter":{"name":"i18n-translate","description":"翻译 Chatbox Pro 当前未提交改动、暂存改动或用户指定 commit/range 中新增或受影响的 i18n key。当用户要求翻译、补全与改动 UI 文案相关的翻译时使用。Agent 需要阅读 diff 和组件上下文，遵守 skill 内置 glossary，并直接写 locale JSON；脚本只用于最后校验。","metadata":{"short-description":"翻译 Chatbox 改动中的 i18n key"}},"isInternal":false,"tokens":1265,"sizeBytes":3660},{"name":"SKILL.md","path":"scripts/session-rag-eval/SKILL.md","rawUrl":"https://raw.githubusercontent.com/chatboxai/chatbox/HEAD/scripts/session-rag-eval/SKILL.md","title":"session-rag-eval","category":"anthropic-skill","format":"markdown","content":"---\nname: session-rag-eval\ndescription: Run and debug Chatbox session attachment RAG model evaluation with synthetic and real long-file fixtures.\n---\n\n# Session Attachment RAG Eval\n\nUse this skill when validating file question-answering behavior for large chat attachments.\n\n## Goal\n\nVerify both sides of model behavior:\n\n- The model calls `query_session_attachment` when the answer depends on an uploaded large file.\n- The model avoids retrieval for clearly unrelated user requests.\n\nPrefer the Chatbox conversation-flow harness for product validation because it exercises the real renderer, config,\nlicense, local API, file upload, indexing, tool registration, and persisted messages.\n\n## Fixtures\n\nDefault fixture repo:\n\n```bash\n../../chatbox-session-rag-eval-fixtures\n```\n\nFixture types:\n\n- synthetic long documents with hidden needle facts;\n- Wikipedia-derived real long documents with source attribution;\n- direct positive cases;\n- implicit positive cases where the user does not explicitly ask to search the file;\n- unrelated negative cases.\n\nRegenerate fixtures in the fixture repo:\n\n```bash\nnode scripts/generate-fixtures.mjs\nnode scripts/fetch-real-fixtures.mjs\n```\n\n## Chatbox Flow\n\nStart the local API before running the harness. Then build with `USE_LOCAL_API=true`; the renderer API origin is compiled\ninto the bundle.\n\n```bash\nUSE_LOCAL_API=true node ./node_modules/electron-vite/bin/electron-vite.js build --mode development\npnpm eval:session-rag:chatbox -- --case long-citrine-threshold --keep-user-data\n```\n\nThe harness copies the real `config.json` into an isolated temporary userDataDir, injects a temporary default chat model\nif missing, and stores the session RAG sqlite DB at a separate temporary path.\n\n## Known Failure Modes\n\n- \"This attachment is too large...\" means `session_attachment_embedding` capability is false or unavailable for the\n  active local API/license path.\n- If requests go to production, rebuild with `USE_LOCAL_API=true`.\n- If Electron hangs at startup, check stale CDP/Electron processes. The harness now allocates a free CDP port.\n- If the assistant says the file is still indexing after DB says ready, inspect the persisted user message. The message\n  file must have `sessionAttachmentIndexStatus: \"ready\"` before context building.\n- Do not trust a log line that says the message was submitted unless the textarea cleared and the session contains the\n  user message.\n\n## Useful Cases\n\n```bash\npnpm eval:session-rag:chatbox -- --case long-citrine-threshold\npnpm eval:session-rag:chatbox -- --case implicit-citrine-current-policy\npnpm eval:session-rag:chatbox -- --case real-wiki-apollo-implicit-landing-site\npnpm eval:session-rag:chatbox -- --case multi-turn-real-wiki-apollo-followup\npnpm eval:session-rag:chatbox -- --case unrelated-simple-math\npnpm eval:session-rag:chatbox -- --case real-wiki-unrelated-capital\n```\n\nFor fast model-behavior iteration without Electron:\n\n```bash\npnpm eval:session-rag -- --dry-run\npnpm eval:session-rag -- --case implicit-citrine-current-policy\n```\n","frontmatter":{"name":"session-rag-eval","description":"Run and debug Chatbox session attachment RAG model evaluation with synthetic and real long-file fixtures."},"isInternal":false,"tokens":684,"sizeBytes":3031},{"name":"check-chatbox-i18n.mjs","path":".claude/skills/i18n-translate/scripts/check-chatbox-i18n.mjs","rawUrl":"https://raw.githubusercontent.com/chatboxai/chatbox/HEAD/.claude/skills/i18n-translate/scripts/check-chatbox-i18n.mjs","title":"Claude Directive: check-chatbox-i18n.mjs","category":"claude-rule","format":"markdown","content":"#!/usr/bin/env node\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst repoRoot = process.cwd()\nconst localeDir = path.join(repoRoot, 'src/renderer/i18n/locales')\nconst skillDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)))\nconst glossary = JSON.parse(fs.readFileSync(path.join(skillDir, 'references/glossary.json'), 'utf8'))\n\nconst localeArgIndex = process.argv.indexOf('--locales')\nconst requestedLocales =\n  localeArgIndex >= 0 && process.argv[localeArgIndex + 1]\n    ? new Set(process.argv[localeArgIndex + 1].split(',').map((s) => s.trim()).filter(Boolean))\n    : null\nconst keyFileIndex = process.argv.indexOf('--keys-file')\nconst strictKeys =\n  keyFileIndex >= 0 && process.argv[keyFileIndex + 1]\n    ? new Set(\n        fs\n          .readFileSync(process.argv[keyFileIndex + 1], 'utf8')\n          .split(/\\r?\\n/)\n          .map((line) => line.trim())\n          .filter(Boolean)\n      )\n    : null\nconst strictAll = process.argv.includes('--strict')\n\nfunction readJson(file) {\n  return JSON.parse(fs.readFileSync(file, 'utf8'))\n}\n\nfunction protectedTokens(text) {\n  return (String(text || '').match(/{{[^}]+}}|<\\/?[A-Za-z0-9]+>/g) || []).sort()\n}\n\nfunction sameMultiset(a, b) {\n  return a.length === b.length && a.every((value, index) => value === b[index])\n}\n\nfunction escapeRe(text) {\n  return text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\nfunction hasWholeWord(source, term) {\n  return new RegExp(`\\\\b${escapeRe(term)}\\\\b`, 'i').test(source)\n}\n\nfunction includesForcedTerm(translation, forced) {\n  return translation.toLowerCase().includes(forced.toLowerCase())\n}\n\nconst verbatimGuardTerms = glossary.keepVerbatim.filter((term) => /^[A-Za-z][A-Za-z0-9]{4,}$/.test(term))\n\nfunction hasStem(text, term) {\n  return new RegExp(`\\\\b${escapeRe(term)}`).test(text)\n}\n\nfunction entryIssues(source, translation, locale) {\n  const issues = []\n  if (!sameMultiset(protectedTokens(source), protectedTokens(translation))) {\n    issues.push('占位符/标签不一致')\n  }\n\n  const forcedTerms = glossary.translateAs[locale] || {}\n  for (const [term, forced] of Object.entries(forcedTerms)) {\n    if (hasWholeWord(source, term) && !includesForcedTerm(translation, forced)) {\n      issues.push(`缺少强制译法 ${term}=>${forced}`)\n    }\n  }\n\n  for (const term of verbatimGuardTerms) {\n    if (hasStem(source, term) && !hasStem(translation, term)) {\n      issues.push(`丢失应保留术语 ${term}`)\n    }\n  }\n\n  return issues\n}\n\nif (!fs.existsSync(localeDir)) {\n  console.error(`未找到 locale 目录：${localeDir}`)\n  process.exit(2)\n}\n\nconst enFile = path.join(localeDir, 'en/translation.json')\nconst en = readJson(enFile)\nconst enKeys = Object.keys(en).sort()\nconst localeNames = fs\n  .readdirSync(localeDir)\n  .filter((name) => fs.existsSync(path.join(localeDir, name, 'translation.json')))\n  .filter((name) => !requestedLocales || requestedLocales.has(name))\n  .sort()\n\nlet problemCount = 0\nlet warningCount = 0\n\nfor (const locale of localeNames) {\n  const file = path.join(localeDir, locale, 'translation.json')\n  const json = readJson(file)\n  const keys = Object.keys(json).sort()\n  const problems = []\n  const warnings = []\n\n  for (const key of enKeys) {\n    if (!(key in json)) {\n      problems.push({ key, issues: ['缺少 key'] })\n      continue\n    }\n\n    const value = json[key]\n    if (!value) {\n      problems.push({ key, issues: ['value 为空'] })\n      continue\n    }\n\n    const source = en[key] || key\n    if (locale === 'en') {\n      if (!source) problems.push({ key, issues: ['英文源文为空'] })\n      continue\n    }\n\n    const issues = entryIssues(source, value, locale)\n    if (issues.length) {\n      const strictForKey = strictAll || strictKeys?.has(key)\n      ;(strictForKey ? problems : warnings).push({ key, issues })\n    }\n  }\n\n  for (const key of keys) {\n    if (!(key in en)) problems.push({ key, issues: ['多余 key：en 中不存在'] })\n  }\n\n  if (problems.length) {\n    problemCount += problems.length\n    console.log(`✗ ${locale}: ${problems.length} 个问题`)\n    for (const { key, issues } of problems.slice(0, 80)) {\n      console.log(`    ${JSON.stringify(key.slice(0, 100))}: ${issues.join('; ')}`)\n    }\n    if (problems.length > 80) console.log(`    ... 还有 ${problems.length - 80} 个`)\n  } else {\n    console.log(`✓ ${locale}: 通过（${keys.length} 个 key）`)\n  }\n  if (warnings.length) {\n    warningCount += warnings.length\n    console.log(`  警告 ${locale}: ${warnings.length} 个`)\n    for (const { key, issues } of warnings.slice(0, 30)) {\n      console.log(`    ${JSON.stringify(key.slice(0, 100))}: ${issues.join('; ')}`)\n    }\n    if (warnings.length > 30) console.log(`    ... 还有 ${warnings.length - 30} 个`)\n  }\n}\n\nif (problemCount) {\n  console.error(`\\n发现 ${problemCount} 个 i18n 问题。`)\n  process.exit(1)\n}\n\nconsole.log(\n  warningCount\n    ? `\\n已检查的 locale 文件 key 都完整。另有 ${warningCount} 个占位符/glossary 警告。`\n    : '\\n已检查的 locale 文件 key 完整，且通过 glossary/占位符校验。'\n)\n","isInternal":false,"tokens":1412,"sizeBytes":5093}],"systemPromptSnippet":"<yakaai_skills repo=\"chatboxai/chatbox\">\n<!-- File: .claude/skills/cherry-pick-pro/SKILL.md (Tokens: ~1319 | Category: anthropic-skill) -->\n---\nname: cherry-pick-pro\ndescription: Cherry-pick commits from chatbox-pro repo to the open chatbox repo, handling mobile-only files, package name differences, and other repo-specific exclusions.\nuser_invocable: true\nread_when:\n  - Cherry-picking from pro to open repo\n  - Syncing pro commits to open source repo\n  - Releasing open source version\n---\n\n# Cherry Pick Pro\n\nCherry-pick commits from the `chatbox-pro` repo (`pro` remote) to the open `chatbox` repo, filtering out mobile-only changes and adjusting for repo differences.\n\n## Usage\n\n```\n/cherry-pick-pro [from_commit] [to_commit]\n```\n\n- `from_commit`: (optional) A **`pro/main` commit hash** to start from (exclusive). Must be a commit on `pro/main`, not an open-repo commit — see step 2. Defaults to auto-detecting the last synced pro commit.\n- `to_commit`: (optional) The commit hash to end at (inclusive). Defaults to `pro/main` HEAD.\n\n## Workflow\n\n### 1. Sync remotes first\n```bash\ngit fetch pro\ngit fetch origin\n```\n\n⚠️ **Tag pollution**: `git fetch pro` also fetches pro's tags (e.g. `vX.Y.Z` created on pro's release branch). These local tags point at **pro commits**, not open repo commits. Never push them as-is — see step 8.\n\n### 2. Identify commits to cherry-pick\n\n⚠️ The range bound **must be a `pro/main` commit**. The open repo and `pro/main` share no history (cherry-picks create new SHAs), so an open-repo commit hash as the lower bound makes `A..pro/main` return *all* of pro's history, not the increment.\n\nFind the last synced pro commit by reading the `cherry picked from pro commit <hash>` footer that step 5 writes into each open-repo commit, then list pro commits after it:\n```bash\n# last synced pro commit (the lower bound)\nlast=$(git log --grep=\"cherry picked from pro commit\" -1 --pretty=%B HEAD \\\n  | grep -oE 'pro commit [0-9a-f]+' | awk '{print $3}')\ngit log \"$last\"..pro/main --oneline --reverse  # commits to pick\n```\nIf no footer is found (first-ever sync), pass `from_commit` explicitly.\n\n### 3. Analyze each commit\nFor each commit, check the files it touches:\n```bash\ngit diff-tree --no-commit-id --name-only -r <commit>\n```\n\n### 4. Cherry-pick rules\n\n#### Files/directories to SKIP (delete if conflict):\n- `android/` - mobile-only, not in open repo\n- `ios/` - mobile-only, not in open repo\n- `capacitor.config.ts` - mobile config\n- `.github/` - open repo has no CI; workflow/config differs (see step 8)\n- `CLAUDE.md`, `AGENTS.md` - pro-specific AI config\n- `.cursorrules` - pro-specific\n- `.claude/` - pro-specific skills/config (including this skill itself), never sync to open repo\n\n#### Package name differences:\n- Pro uses: `xyz.chatboxapp.app`\n- Open uses: `xyz.chatboxapp.ce`\n- When resolving conflicts in `package.json` or `release/app/package.json`, always keep the open repo name (`xyz.chatboxapp.ce`)\n\n#### Pro-only dependencies to remove if added:\n- `@playwright/test` - pro testing only\n- `playwright` - pro testing only\n- `@ruguoapp/jk-analytics` - pro analytics only\n\n#### `pnpm-lock.yaml`:\n- Will almost always conflict. Accept either side, then run `pnpm install --no-frozen-lockfile` at the end to regenerate.\n\n### 5. Cherry-pick process\nFor each commit:\n1. Try `git cherry-pick <hash>` directly first\n2. If conflicts occur, use `--no-commit` and resolve:\n   - Remove mobile-only files with `git rm`\n   - Fix package name conflicts (keep `ce`)\n   - Remove pro-only dependencies\n   - Accept either side for `pnpm-lock.yaml`\n3. Commit with original message + `(cherry picked from pro commit <hash>)` note\n\n### 6. Finalize\nAfter all cherry-picks:\n```bash\npnpm install --no-frozen-lockfile  # regenerate lockfile\ngit add pnpm-lock.yaml\ngit commit -m \"chore: regenerate pnpm-lock.yaml for open repo\"\n```\n\n### 7. Verify\n```bash\ngit log --oneline <start>..HEAD  # review all new commits\npnpm install  # ensure deps resolve\n```\n\n### 8. Release (if syncing a version release)\n\nThe open repo has **no CI workflows** — releases are manual:\n\n1. Push the sync branch to `origin/main`\n2. Re-create the version tag on the **open repo's** commit. The local tag fetched from pro points at a pro commit — pushing it would leak pro's commit history into the open repo:\n   ```bash\n   git tag -d vX.Y.Z                 # delete pro's tag fetched in step 1\n   git tag vX.Y.Z <open-repo-head>   # re-create on open repo commit\n   git push origin vX.Y.Z\n   ```\n3. Create the GitHub Release manually (no binary assets; notes = changelog + install links, copy format from the previous release):\n   ```bash\n   gh release view vX.Y.(Z-1) --json body --jq '.body'  # get template\n   gh release create vX.Y.Z --title \"vX.Y.Z\" --latest --notes \"...\"\n   ```\n\nVersion bump convention: only `release/app/package.json` gets the new version; root `package.json` stays `0.0.1` in the open repo.\n\n## Example\n\n```\n/cherry-pick-pro\n```\nThis will auto-detect the last release on the current branch and cherry-pick all newer commits from `pro/main`.\n\n<!-- File: .claude/skills/i18n-translate/agents/openai.yaml (Tokens: ~73 | Category: anthropic-skill) -->\ninterface:\n  display_name: \"i18n Translate\"\n  short_description: \"翻译 Chatbox 改动中的 UI 文案\"\n  default_prompt: \"使用 $i18n-translate 翻译我当前改动引入的 i18n key。\"\n\npolicy:\n  allow_implicit_invocation: true\n\n<!-- File: .claude/skills/i18n-translate/references/glossary.json (Tokens: ~376 | Category: anthropic-skill) -->\n{\n  \"keepVerbatim\": [\n    \"Chatbox\",\n    \"AI\",\n    \"MCP\",\n    \"Deep Link\",\n    \"ID\",\n    \"API\",\n    \"URL\",\n    \"OCR\",\n    \"LaTeX\",\n    \"Token\",\n    \"Embedding\",\n    \"Rerank\",\n    \"Copilot\"\n  ],\n  \"translateAs\": {\n    \"ar\": {\n      \"Agent\": \"الوكيل\"\n    },\n    \"es\": {\n      \"Agent\": \"Agente\"\n    },\n    \"it-IT\": {\n      \"Agent\": \"Agente\"\n    },\n    \"ja\": {\n      \"Agent\": \"エージェント\"\n    },\n    \"ko\": {\n      \"Agent\": \"에이전트\"\n    },\n    \"pt-PT\": {\n      \"Agent\": \"Agente\"\n    },\n    \"ru\": {\n      \"Agent\": \"агент\"\n    },\n    \"zh-Hans\": {\n      \"Quota\": \"点数\",\n      \"Agent\": \"智能体\",\n      \"License Key\": \"许可证\",\n      \"License\": \"许可证\"\n    },\n    \"zh-Hant\": {\n      \"Quota\": \"點數\",\n      \"Agent\": \"智能體\",\n      \"License Key\": \"許可證\",\n      \"License\": \"許可證\"\n    }\n  },\n  \"notes\": [\n    \"keepVerbatim terms should remain in English in every locale unless a term is too short or ambiguous for deterministic checking.\",\n    \"translateAs terms are mandatory when the English source contains the listed term as a whole word.\",\n    \"License and License Key intentionally map to the same Chinese term without adding key/密钥/金鑰.\"\n  ]\n}\n\n<!-- File: .claude/skills/i18n-translate/SKILL.md (Tokens: ~1265 | Category: anthropic-skill) -->\n---\nname: i18n-translate\ndescription: 翻译 Chatbox Pro 当前未提交改动、暂存改动或用户指定 commit/range 中新增或受影响的 i18n key。当用户要求翻译、补全与改动 UI 文案相关的翻译时使用。Agent 需要阅读 diff 和组件上下文，遵守 skill 内置 glossary，并直接写 locale JSON；脚本只用于最后校验。\nmetadata:\n  short-description: 翻译 Chatbox 改动中的 i18n key\n---\n\n# i18n-translate\n\n你一名UI翻译者。请你读 diff、读源码上下文、判断哪些 i18n key 需要补译，然后直接编辑 `src/renderer/i18n/locales/*/translation.json`。注意不要运行 `pnpm translate`，也不要依赖抽取脚本来决定翻译内容。你(LLM)来检查哪些 key 需要翻译，并且直接翻译成目标语言。\n\n## 必读\n\n- 翻译前读取 `references/glossary.json`，这是本 skill 的术语表来源。\n- 默认处理工作区中所有未提交改动（包括已暂存和未暂存）；如果用户提供单个 commit SHA 或 Git range，则仅处理该范围内新增或修改的 key。\n- 如果用户提供的 commit 或 range 无法解析，请停止并向用户报告该输入无效，而不是猜测要处理的文件。\n\n## 怎么找 key\n\n从 diff/commit 中找新增或受影响的文案，重点看：\n\n- `t('...')`、`i18n.t('...')`\n- `<Trans i18nKey=\"...\">`\n- `i18nKey: '...'`，尤其是错误对象或 `src/shared` 中的 key\n- `src/renderer/i18n/for-key-scan.ts` 等动态 key 辅助文件\n- `src/renderer/i18n/locales/en/translation.json` 中被改动的英文 value\n\n\n## 翻译原则\n\n- 先理解 key 所在文件、组件、UI 角色和周围文案，再翻译。\n- UI 角色包括按钮、菜单、tooltip、aria label、placeholder、错误提示、toast、modal、设置项、状态文案、聊天消息和产品文案。\n- 参考同一组件中目标语言已有译文，保持语气和术语一致。\n- 以 `en[key] || key` 作为源文；大多数情况下 `en[key]` 应等于 key。先补全 `en/translation.json` 中缺失或空的 key，再翻译其他语言。\n- 如果 `en` 文案发生变化，更新所有已请求 locale 中对应 key 的翻译值；如果某个已有翻译需要更新而不是新增，请直接覆盖旧译文。\n- 当 `en` value 和源码 key 不一致时，按 3 步判断：\n  1. 先判断 `en` value 是否是有意的 slug key。\n  2. 如果不是，再检查是否由 `<Trans>` children 或动态 key 扫描造成的源文错误。\n  3. 只有在确认是源文错误时才修改源码，否则只更新 JSON。\n- 保留所有 `{{placeholder}}`、`<NamedTag>`、`<0>` 标签和源文前后空格。\n- 遵守 glossary 的保留词和强制译法。不要机械直译，写自然、符合目标语言 UI 习惯的译文。\n- 按语言逐个翻译：一种语言翻译完就立即写入该语言的 `translation.json`，再开始下一种语言。不要把所有语言的译文都攒到最后一次性写入。\n- 如果同一个英文 key 在不同 UI 角色中需要不同译法，先不要直接给出不同译文；停止并询问用户是否应将 key 改为更具体的名称，除非仓库已经有明确的命名约定。\n\n## 完成前\n\n可运行 checker 做确定性检查：\n\n```bash\nnode .codex/skills/i18n-translate/scripts/check-chatbox-i18n.mjs\n```\n\n如果你维护了本次改动 key 列表，可以做严格检查：\n\n```bash\nnode .codex/skills/i18n-translate/scripts/check-chatbox-i18n.mjs --keys-file /tmp/changed-i18n-keys.txt\n```\n\nchecker 默认只因缺 key、空 value、多余 key 失败；历史 glossary/placeholder 问题会作为 warning。不要提交 commit，把改动留给用户 review。\n\n<!-- File: scripts/session-rag-eval/SKILL.md (Tokens: ~684 | Category: anthropic-skill) -->\n---\nname: session-rag-eval\ndescription: Run and debug Chatbox session attachment RAG model evaluation with synthetic and real long-file fixtures.\n---\n\n# Session Attachment RAG Eval\n\nUse this skill when validating file question-answering behavior for large chat attachments.\n\n## Goal\n\nVerify both sides of model behavior:\n\n- The model calls `query_session_attachment` when the answer depends on an uploaded large file.\n- The model avoids retrieval for clearly unrelated user requests.\n\nPrefer the Chatbox conversation-flow harness for product validation because it exercises the real renderer, config,\nlicense, local API, file upload, indexing, tool registration, and persisted messages.\n\n## Fixtures\n\nDefault fixture repo:\n\n```bash\n../../chatbox-session-rag-eval-fixtures\n```\n\nFixture types:\n\n- synthetic long documents with hidden needle facts;\n- Wikipedia-derived real long documents with source attribution;\n- direct positive cases;\n- implicit positive cases where the user does not explicitly ask to search the file;\n- unrelated negative cases.\n\nRegenerate fixtures in the fixture repo:\n\n```bash\nnode scripts/generate-fixtures.mjs\nnode scripts/fetch-real-fixtures.mjs\n```\n\n## Chatbox Flow\n\nStart the local API before running the harness. Then build with `USE_LOCAL_API=true`; the renderer API origin is compiled\ninto the bundle.\n\n```bash\nUSE_LOCAL_API=true node ./node_modules/electron-vite/bin/electron-vite.js build --mode development\npnpm eval:session-rag:chatbox -- --case long-citrine-threshold --keep-user-data\n```\n\nThe harness copies the real `config.json` into an isolated temporary userDataDir, injects a temporary default chat model\nif missing, and stores the session RAG sqlite DB at a separate temporary path.\n\n## Known Failure Modes\n\n- \"This attachment is too large...\" means `session_attachment_embedding` capability is false or unavailable for the\n  active local API/license path.\n- If requests go to production, rebuild with `USE_LOCAL_API=true`.\n- If Electron hangs at startup, check stale CDP/Electron processes. The harness now allocates a free CDP port.\n- If the assistant says the file is still indexing after DB says ready, inspect the persisted user message. The message\n  file must have `sessionAttachmentIndexStatus: \"ready\"` before context building.\n- Do not trust a log line that says the message was submitted unless the textarea cleared and the session contains the\n  user message.\n\n## Useful Cases\n\n```bash\npnpm eval:session-rag:chatbox -- --case long-citrine-threshold\npnpm eval:session-rag:chatbox -- --case implicit-citrine-current-policy\npnpm eval:session-rag:chatbox -- --case real-wiki-apollo-implicit-landing-site\npnpm eval:session-rag:chatbox -- --case multi-turn-real-wiki-apollo-followup\npnpm eval:session-rag:chatbox -- --case unrelated-simple-math\npnpm eval:session-rag:chatbox -- --case real-wiki-unrelated-capital\n```\n\nFor fast model-behavior iteration without Electron:\n\n```bash\npnpm eval:session-rag -- --dry-run\npnpm eval:session-rag -- --case implicit-citrine-current-policy\n```\n\n<!-- File: .claude/skills/i18n-translate/scripts/check-chatbox-i18n.mjs (Tokens: ~1412 | Category: claude-rule) -->\n#!/usr/bin/env node\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst repoRoot = process.cwd()\nconst localeDir = path.join(repoRoot, 'src/renderer/i18n/locales')\nconst skillDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)))\nconst glossary = JSON.parse(fs.readFileSync(path.join(skillDir, 'references/glossary.json'), 'utf8'))\n\nconst localeArgIndex = process.argv.indexOf('--locales')\nconst requestedLocales =\n  localeArgIndex >= 0 && process.argv[localeArgIndex + 1]\n    ? new Set(process.argv[localeArgIndex + 1].split(',').map((s) => s.trim()).filter(Boolean))\n    : null\nconst keyFileIndex = process.argv.indexOf('--keys-file')\nconst strictKeys =\n  keyFileIndex >= 0 && process.argv[keyFileIndex + 1]\n    ? new Set(\n        fs\n          .readFileSync(process.argv[keyFileIndex + 1], 'utf8')\n          .split(/\\r?\\n/)\n          .map((line) => line.trim())\n          .filter(Boolean)\n      )\n    : null\nconst strictAll = process.argv.includes('--strict')\n\nfunction readJson(file) {\n  return JSON.parse(fs.readFileSync(file, 'utf8'))\n}\n\nfunction protectedTokens(text) {\n  return (String(text || '').match(/{{[^}]+}}|<\\/?[A-Za-z0-9]+>/g) || []).sort()\n}\n\nfunction sameMultiset(a, b) {\n  return a.length === b.length && a.every((value, index) => value === b[index])\n}\n\nfunction escapeRe(text) {\n  return text.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\nfunction hasWholeWord(source, term) {\n  return new RegExp(`\\\\b${escapeRe(term)}\\\\b`, 'i').test(source)\n}\n\nfunction includesForcedTerm(translation, forced) {\n  return translation.toLowerCase().includes(forced.toLowerCase())\n}\n\nconst verbatimGuardTerms = glossary.keepVerbatim.filter((term) => /^[A-Za-z][A-Za-z0-9]{4,}$/.test(term))\n\nfunction hasStem(text, term) {\n  return new RegExp(`\\\\b${escapeRe(term)}`).test(text)\n}\n\nfunction entryIssues(source, translation, locale) {\n  const issues = []\n  if (!sameMultiset(protectedTokens(source), protectedTokens(translation))) {\n    issues.push('占位符/标签不一致')\n  }\n\n  const forcedTerms = glossary.translateAs[locale] || {}\n  for (const [term, forced] of Object.entries(forcedTerms)) {\n    if (hasWholeWord(source, term) && !includesForcedTerm(translation, forced)) {\n      issues.push(`缺少强制译法 ${term}=>${forced}`)\n    }\n  }\n\n  for (const term of verbatimGuardTerms) {\n    if (hasStem(source, term) && !hasStem(translation, term)) {\n      issues.push(`丢失应保留术语 ${term}`)\n    }\n  }\n\n  return issues\n}\n\nif (!fs.existsSync(localeDir)) {\n  console.error(`未找到 locale 目录：${localeDir}`)\n  process.exit(2)\n}\n\nconst enFile = path.join(localeDir, 'en/translation.json')\nconst en = readJson(enFile)\nconst enKeys = Object.keys(en).sort()\nconst localeNames = fs\n  .readdirSync(localeDir)\n  .filter((name) => fs.existsSync(path.join(localeDir, name, 'translation.json')))\n  .filter((name) => !requestedLocales || requestedLocales.has(name))\n  .sort()\n\nlet problemCount = 0\nlet warningCount = 0\n\nfor (const locale of localeNames) {\n  const file = path.join(localeDir, locale, 'translation.json')\n  const json = readJson(file)\n  const keys = Object.keys(json).sort()\n  const problems = []\n  const warnings = []\n\n  for (const key of enKeys) {\n    if (!(key in json)) {\n      problems.push({ key, issues: ['缺少 key'] })\n      continue\n    }\n\n    const value = json[key]\n    if (!value) {\n      problems.push({ key, issues: ['value 为空'] })\n      continue\n    }\n\n    const source = en[key] || key\n    if (locale === 'en') {\n      if (!source) problems.push({ key, issues: ['英文源文为空'] })\n      continue\n    }\n\n    const issues = entryIssues(source, value, locale)\n    if (issues.length) {\n      const strictForKey = strictAll || strictKeys?.has(key)\n      ;(strictForKey ? problems : warnings).push({ key, issues })\n    }\n  }\n\n  for (const key of keys) {\n    if (!(key in en)) problems.push({ key, issues: ['多余 key：en 中不存在'] })\n  }\n\n  if (problems.length) {\n    problemCount += problems.length\n    console.log(`✗ ${locale}: ${problems.length} 个问题`)\n    for (const { key, issues } of problems.slice(0, 80)) {\n      console.log(`    ${JSON.stringify(key.slice(0, 100))}: ${issues.join('; ')}`)\n    }\n    if (problems.length > 80) console.log(`    ... 还有 ${problems.length - 80} 个`)\n  } else {\n    console.log(`✓ ${locale}: 通过（${keys.length} 个 key）`)\n  }\n  if (warnings.length) {\n    warningCount += warnings.length\n    console.log(`  警告 ${locale}: ${warnings.length} 个`)\n    for (const { key, issues } of warnings.slice(0, 30)) {\n      console.log(`    ${JSON.stringify(key.slice(0, 100))}: ${issues.join('; ')}`)\n    }\n    if (warnings.length > 30) console.log(`    ... 还有 ${warnings.length - 30} 个`)\n  }\n}\n\nif (problemCount) {\n  console.error(`\\n发现 ${problemCount} 个 i18n 问题。`)\n  process.exit(1)\n}\n\nconsole.log(\n  warningCount\n    ? `\\n已检查的 locale 文件 key 都完整。另有 ${warningCount} 个占位符/glossary 警告。`\n    : '\\n已检查的 locale 文件 key 完整，且通过 glossary/占位符校验。'\n)\n</yakaai_skills>"}