diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..90b9955 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,237 @@ +# GEMINI.md + +本文件提供在本仓库内进行开发/重构时的约定与定位入口(面向代码助手与维护者)。 + +## Project Overview + +Sapling 是一个 Manifest V3 浏览器扩展,用于沉浸式语言学习:在网页中将部分词汇替换为翻译,并提供 tooltip/发音/词汇管理等能力。 + +基于「可理解性输入」理论,在用户浏览母语内容时将部分词汇智能替换为学习语言,反之亦然。 + +## Build Commands + +```bash +# 安装依赖(会触发 postinstall: wxt prepare) +npm install + +# 开发(Chrome) +npm run dev + +# 开发(Firefox) +npm run dev:firefox + +# 构建(Chrome) +npm run build + +# 构建(Firefox) +npm run build:firefox + +# 打包发布 zip +npm run zip +npm run zip:firefox +``` + +本项目已迁移到 **WXT** 框架(Vite 构建 + TS),不再使用 legacy 的 `js/` + 手工 `manifest.json` + `vendor/` 打包流程。 + +## Dev API 配置(仅 dev) + +开发时可用 `.env.development.local` 强制覆盖 content script 运行时使用的 API 配置,避免反复去设置页修改。 + +```bash +cp .env.development.local.example .env.development.local +``` + +变量: +- `VITE_SAPLING_API_ENDPOINT`(非空才覆盖) +- `VITE_SAPLING_MODEL_NAME`(非空才覆盖) +- `VITE_SAPLING_API_KEY`(允许为空,若定义则覆盖) + +优先级:`?sapling-mock=1` 测试模式 > `.env.development.local` > storage/默认值 + +## Testing + +```bash +# E2E 测试(Playwright) +npm run test:e2e + +# Mock 服务器(用于本地调试,端口 3456) +npm run mock +``` + +**测试资源**: +- `tests/e2e/` - Playwright E2E 测试 +- `test/mock-server.js` - 模拟 OpenAI 兼容 API 的响应 +- `test/*.html` - 手工测试用的 HTML 页面 + +**手工验证**:加载 `.output/*/manifest.json` 到浏览器扩展管理页。 + +## Architecture + +``` +WXT 结构(`~` 导入别名指向 `src/`) + +入口(src/entrypoints/) + background.ts - 后台 service worker:安装初始化、右键菜单、消息代理 + content.ts - content script:DOM 分段/翻译/替换/交互 + popup/* - popup 页面(HTML + TS) + options/* - options 页面(HTML + TS) + vocab-test/* - 词汇量测试页(HTML + TS) + +核心模块(src/) + core/* - 配置与 storage 抽象 + services/* - API/缓存/分段/替换/音频 等服务 + prompts/* - AI 提示词模板(CEFR/翻译策略) + ui/* - tooltip/modal/toast 等 UI + utils/* - 语言检测/文本处理/过滤/TOON 等工具 + types/* - TypeScript 类型定义 + constants.ts - 全局常量(强度/跳过规则/停用词等) + +静态资源(public/) + _locales/* - i18n 资源 + css/* - options/popup 等页面样式 + icons/* - 图标 + wordlist/* - CEFR 词表 + +根目录类型(types/) + env.d.ts - Vite 环境变量类型 + globals.d.ts - 全局类型声明(Chrome API 扩展) +``` + +## Dependencies + +| Package | 用途 | +|---------|------| +| `@toon-format/toon` | TOON 格式解析(结构化翻译响应,避免 JSON 解析失败) | +| `segmentit` | 中日韩文本分词 | + +## Key Technical Details + +- **WXT + Vite**:构建与入口发现由 WXT 管理;`wxt.config.ts` 生成 manifest。 +- **TypeScript**:以"能跑"为目标(未开启严格模式),逐步补类型。 +- **导入别名**:统一使用 `~/...`,对应 `src/`(见 `wxt.config.ts` 的 `srcDir`)。 +- **存储分层**:`IStorageAdapter -> ChromeStorageAdapter -> StorageNamespace -> StorageService`;区分 sync/local。 +- **消息通信**:background ↔ content 通过 `chrome.runtime.sendMessage` / `tabs.sendMessage`。 +- **发音**:通过 background 代理 fetch + Web Audio 解码播放,绕过页面 CSP。 + +## Message Types + +消息类型定义在 `src/types/messages.ts`,分为两类: + +### Background Actions(background 处理) + +| Action | 说明 | 请求类型 | 响应类型 | +|--------|------|----------|----------| +| `fetchAudioData` | 代理 fetch 音频(绕过 CSP) | `{ url }` | `{ data, contentType }` | +| `togglePageProcessing` | 切换页面处理状态 | `{ tabId }` | `{ processedBefore, processedAfter }` | +| `refreshTogglePageMenuTitle` | 刷新右键菜单标题 | `{ tabId }` | `{ success }` | +| `testApi` | 测试 API 连接 | `{ endpoint, apiKey, model }` | `{ success, message }` | +| `getStats` | 获取学习统计 | `{}` | `{ totalWords, todayWords, ... }` | +| `getCacheStats` | 获取缓存统计 | `{}` | `{ size, maxSize }` | +| `clearCache` | 清空翻译缓存 | `{}` | `{ success }` | +| `clearLearnedWords` | 清空已学会词汇 | `{}` | `{ success }` | +| `clearMemorizeList` | 清空需记忆列表 | `{}` | `{ success }` | + +### Content Actions(content script 处理) + +| Action | 说明 | 请求类型 | 响应类型 | +|--------|------|----------|----------| +| `processPage` | 处理当前页面 | `{}` | `{ processed, skipped?, ... }` | +| `restorePage` | 还原页面 | `{}` | `{ success }` | +| `processSpecificWords` | 处理特定单词 | `{ words[] }` | `{ count }` | +| `getStatus` | 获取页面状态 | `{}` | `{ processed, hasTranslations, ... }` | +| `resetAllData` | 重置所有数据 | `{}` | `{ success }` | + +## Core Algorithms + +**难度过滤**:CEFR 6 级(A1 → C2),只选择/显示难度 >= 用户设置的词。 + +**替换强度**:低/中/高(每段落 4/8/14)。 + +**内容处理**:分段(50-2000 字符)、指纹去重、视口优先、并发批处理。 + +**LRU 缓存**:默认 2000 容量,按最近使用淘汰,跨会话持久化。 + +**翻译展示样式**: +- `translation-only`: 仅显示翻译 +- `original-translation`: 原文(翻译) +- `translation-original`: 翻译(原文) + +## Supported Languages + +- **母语**:中文(简/繁)、英语、日语、韩语 +- **目标语言**:英语、中文、日语、韩语、法语、德语、西语 +- **AI Provider**:任意 OpenAI 兼容接口(OpenAI/Gemini/DeepSeek/Moonshot/Groq/Ollama) + +## Localization + +使用扩展 i18n:`public/_locales/{locale}/messages.json`,默认 `zh_CN`。 + +## Storage Architecture + +存储系统为 4 层结构,便于替换后端与保持兼容: + +### Layer 1: IStorageAdapter (Interface) +- 定义 `get/set/remove/onChanged` 契约 +- 允许未来替换后端(Chrome Storage/WebDAV/其他) + +### Layer 2: ChromeStorageAdapter (Implementation) +- 包装 `chrome.storage.sync` / `chrome.storage.local` +- 过滤 area 的 change 事件 +- `onChanged()` 返回 unsubscribe + +### Layer 3: StorageNamespace (Low-level API) +- 同时提供 callback / Promise 风格(`get/getAsync`、`set/setAsync`) +- remote(sync)读取时合并 `DEFAULT_CONFIG`(默认值在代码中) +- `storage.remote`(sync)与 `storage.local`(local)分离 + +### Layer 4: StorageService (High-level Facade) +- 提供领域方法:统计/词表/列表等 +- 保持兼容:`get/set/getLocal/setLocal/removeLocal` +- 单例导出:`export const storage = new StorageService()` + +**存储区域**: +- **storage.remote**(sync):配置数据,支持跨设备同步 +- **storage.local**(local):大容量数据,如词汇缓存、已学会词汇 + +## Debugging + +### Content Script 日志 +打开目标页面的 DevTools Console,日志以 `[Sapling]` 前缀标识。 + +### Background 日志 +1. 打开 `chrome://extensions` +2. 找到 Sapling 扩展 +3. 点击"服务工作进程"链接打开 DevTools + +### 存储查看 +DevTools → Application → Storage → Extension Storage(sync / local) + +### 常见调试场景 + +| 场景 | 方法 | +|------|------| +| API 调用失败 | 检查 Network 面板,查看请求/响应内容 | +| 翻译未生效 | Console 查看 `[Sapling]` 日志,确认分段/替换流程 | +| 缓存问题 | 使用 options 页面清空缓存,或检查 `Sapling_word_cache` | +| 消息通信问题 | 在 background 和 content 的 Console 分别查看日志 | + +## Error Handling + +- **API 错误**:捕获并显示友好提示,不中断页面处理 +- **网络超时**:使用 AbortController,默认超时后自动重试或跳过 +- **缓存失败**:静默降级,不影响核心翻译功能 +- **DOM 变化**:使用 MutationObserver 监听,增量处理新内容 + +## Key Files for Common Tasks + +| Task | Files | +|------|-------| +| 修改翻译逻辑 | `src/services/api-service.ts`, `src/prompts/ai-prompts.ts` | +| 修改 DOM 处理 | `src/services/content-segmenter.ts`, `src/services/text-replacer.ts` | +| 修改 tooltip 行为 | `src/ui/tooltip.ts`, `src/ui/content.css` | +| 修改单词过滤规则 | `src/utils/word-filters.ts`, `src/constants.ts` | +| 修改存储行为 | `src/core/storage/StorageService.ts`, `src/core/storage/ChromeStorageAdapter.ts` | +| 添加新存储后端 | 实现 `src/core/storage/IStorageAdapter.ts` | +| 修改 popup/options UI | `src/entrypoints/popup/*`, `src/entrypoints/options/*` | +| 修改 AI 提示词 | `src/prompts/ai-prompts.ts` | +| 添加新消息类型 | `src/types/messages.ts`, 然后在 `background.ts` / `content.ts` 添加处理 | diff --git a/package-lock.json b/package-lock.json index 4b88ef9..892c97e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "segmentit": "^2.0.3" }, "devDependencies": { + "@playwright/test": "^1.49.1", "@types/chrome": "^0.1.32", "esbuild": "^0.24.0", "vite": "^5.4.11", @@ -782,6 +783,22 @@ "node": ">= 8" } }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -4105,6 +4122,53 @@ "pathe": "^2.0.3" } }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -5859,6 +5923,24 @@ } } }, + "node_modules/vite-node/node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/vite/node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", diff --git a/src/core/config.ts b/src/core/config.ts index f52e167..0aabdd6 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -35,6 +35,11 @@ export const API_PRESETS = { name: 'OpenAI', endpoint: 'https://api.openai.com/v1/chat/completions', model: 'gpt-4o-mini' + }, + gemini: { + name: 'Gemini', + endpoint: 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions', + model: 'gemini-2.5-flash' } }; @@ -55,13 +60,13 @@ export const DEFAULT_CONFIG: SaplingConfig = { modelName: API_PRESETS.openai.model, apiProfiles: [], activeApiProfileId: null, - + // 学习偏好 nativeLanguage: 'zh-CN', targetLanguage: 'en', difficultyLevel: 'B1', intensity: 'medium', - + // 行为设置 autoProcess: false, showPhonetic: true, @@ -70,16 +75,16 @@ export const DEFAULT_CONFIG: SaplingConfig = { pronunciationProvider: 'google', youdaoPronunciationType: 2, enabled: true, - + // 站点规则 blacklist: [], whitelist: [], - + // 统计数据 totalWords: 0, todayWords: 0, lastResetDate: new Date().toISOString().split('T')[0], - + // 缓存设置 cacheMaxSize: 2048, diff --git a/src/entrypoints/options/index.html b/src/entrypoints/options/index.html index 6344e81..8504d9e 100644 --- a/src/entrypoints/options/index.html +++ b/src/entrypoints/options/index.html @@ -101,6 +101,15 @@
点击上方 + 保存当前填写的 API 配置为自定义模板,可保存多个并随时切换。
+点击预设自动填充 API 端点和模型名称,仅需补充 API Key
+