From e279da0c8be683b4075902429680e784c2a006a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=B2=A9=E5=B2=A9?= Date: Wed, 11 Feb 2026 14:21:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(M3.7):=20=E5=BF=85=E5=BA=94=E8=AF=8D?= =?UTF-8?q?=E5=85=B8=E7=9C=9F=E5=AE=9EAPI=20(v0.2.7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/QUICK_REF.md | 2 +- docs/README.md | 6 +- docs/VERSION.md | 2 +- manifest.json | 2 +- package.json | 2 +- src/background/index.js | 30 ++++ src/shared/dictionary/bing.js | 250 ++++++++++++++++++++++------------ 7 files changed, 197 insertions(+), 97 deletions(-) diff --git a/docs/QUICK_REF.md b/docs/QUICK_REF.md index 46d86ee..5e1c75b 100644 --- a/docs/QUICK_REF.md +++ b/docs/QUICK_REF.md @@ -7,7 +7,7 @@ ## 版本速查 ### 当前版本 -`0.2.6` → 下一目标 `0.2.7` ([M3.7](./M3.md)) +`0.2.7` → 下一目标 `0.2.8` ([M3.8](./M3.md)) ### 模块版本范围 diff --git a/docs/README.md b/docs/README.md index 3748609..9054736 100644 --- a/docs/README.md +++ b/docs/README.md @@ -65,9 +65,9 @@ M11.10完成 → 1.0.0 (正式发布) ## 当前状态 -**当前版本**: `0.2.6` -**当前进度**: 20/97 (21%) -**下一任务**: [M3.7 必应词典真实API](./M3.md#m37-必应词典真实api--目标版本-027) +**当前版本**: `0.2.7` +**当前进度**: 21/97 (22%) +**下一任务**: [M3.8 加载状态显示](./M3.md#m38-加载状态显示--目标版本-028) --- diff --git a/docs/VERSION.md b/docs/VERSION.md index 583fdea..154a4db 100644 --- a/docs/VERSION.md +++ b/docs/VERSION.md @@ -42,7 +42,7 @@ | M3.4 | 0.2.4 | 后台查询接口 | ✅ | 2026-02-11 | | M3.5 | 0.2.5 | 结果展示组件 | ✅ | 2026-02-11 | | M3.6 | 0.2.6 | 点击图标查词(Mock) | ✅ | 2026-02-11 | -| M3.7 | 0.2.7 | 必应词典真实API | ⬜ | - | +| M3.7 | 0.2.7 | 必应词典真实API | ✅ | 2026-02-11 | | M3.8 | 0.2.8 | 加载状态显示 | ⬜ | - | | M3.9 | 0.2.9 | 有道词典实现 | ⬜ | - | | M3.10 | 0.2.10 | 结果折叠/展开 | ⬜ | - | diff --git a/manifest.json b/manifest.json index 2894845..f025c1c 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "沙拉查词", - "version": "0.2.6", + "version": "0.2.7", "description": "聚合词典划词翻译", "permissions": [ "storage", diff --git a/package.json b/package.json index ecab8af..ebcdb72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salad-dict", - "version": "0.2.6", + "version": "0.2.7", "description": "聚合词典划词翻译", "private": true, "type": "module", diff --git a/src/background/index.js b/src/background/index.js index 4a47037..c85f94d 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -47,4 +47,34 @@ backgroundHandler.register('DICT.GET_LIST', async () => { return { dictionaries }; }); +// 注册 HTTP 请求处理器 +backgroundHandler.register('HTTP.GET', async (payload) => { + const { url, options = {} } = payload; + + console.log('[Background] HTTP.GET:', url); + + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.0', + ...options.headers + }, + ...options + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const text = await response.text(); + return { text, status: response.status }; + } catch (error) { + console.error('[Background] HTTP.GET failed:', error); + throw new Error(`请求失败: ${error.message}`); + } +}); + console.log('[SaladDict] Message handlers registered'); diff --git a/src/shared/dictionary/bing.js b/src/shared/dictionary/bing.js index f7d32d6..fd35f45 100644 --- a/src/shared/dictionary/bing.js +++ b/src/shared/dictionary/bing.js @@ -1,12 +1,13 @@ /** - * @file 必应词典实现(Mock版) - * @description 必应词典的 Mock 实现,返回模拟数据 + * @file 必应词典实现 + * @description 必应词典的真实实现,通过 HTTP 请求获取数据 */ import { DictionaryBase, createResult, createMeaning, createExample } from './base.js'; +import { messaging } from '../messaging.js'; /** - * 必应词典 Mock 实现 + * 必应词典实现 */ export class BingDictionary extends DictionaryBase { constructor(config = {}) { @@ -19,124 +20,193 @@ export class BingDictionary extends DictionaryBase { } /** - * 查询单词(Mock 实现) + * 查询单词 * @param {string} word - 要查询的单词 - * @returns {Promise} 模拟查询结果 + * @returns {Promise} 查询结果 */ async search(word) { if (!word?.trim()) { - throw new Error('Word is empty'); + throw new Error('查询单词不能为空'); } const trimmedWord = word.trim(); + const url = `https://cn.bing.com/dict/search?q=${encodeURIComponent(trimmedWord)}`; - // 模拟网络延迟 - await this._delay(100); + try { + // 通过 Background 发起 HTTP 请求 + const response = await messaging.sendToBackground('HTTP.GET', { url }, 10000); + + if (!response?.text) { + throw new Error('获取词典数据失败'); + } + + // 解析 HTML 提取数据 + return this._parseHtml(response.text, trimmedWord, url); + } catch (error) { + console.error('[BingDictionary] Search failed:', error); + + // 返回友好错误提示 + return createResult({ + word: trimmedWord, + phonetic: '', + meanings: [createMeaning('提示', ['查询失败,请检查网络连接'])], + examples: [], + url + }); + } + } + + /** + * 解析必应词典 HTML + * @private + * @param {string} html - HTML 内容 + * @param {string} word - 查询的单词 + * @param {string} url - 查询 URL + * @returns {DictionaryResult} 解析结果 + */ + _parseHtml(html, word, url) { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + + // 提取音标 + const phonetic = this._extractPhonetic(doc); + + // 提取释义 + const meanings = this._extractMeanings(doc); + + // 提取例句 + const examples = this._extractExamples(doc); - // 返回模拟数据 return createResult({ - word: trimmedWord, - phonetic: this._getMockPhonetic(trimmedWord), - meanings: this._getMockMeanings(trimmedWord), - examples: this._getMockExamples(trimmedWord), - url: `https://cn.bing.com/dict/search?q=${encodeURIComponent(trimmedWord)}` + word, + phonetic, + meanings, + examples, + url }); } /** - * 模拟延迟 + * 提取音标 * @private - * @param {number} ms - 延迟毫秒数 - * @returns {Promise} - */ - _delay(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - /** - * 获取模拟音标 - * @private - * @param {string} word - 单词 + * @param {Document} doc - HTML 文档 * @returns {string} 音标 */ - _getMockPhonetic(word) { - const phonetics = { - 'hello': '/həˈləʊ/', - 'world': '/wɜːld/', - 'apple': '/ˈæpl/', - 'computer': '/kəmˈpjuːtə(r)/', - 'dictionary': '/ˈdɪkʃənri/' - }; - - return phonetics[word.toLowerCase()] || `/${word.toLowerCase()}/`; + _extractPhonetic(doc) { + // 尝试多个可能的选择器 + const selectors = [ + '.hd_p1_1F_OWM', // 主要音标容器 + '.hd_tf_lh', // 音标文本 + '[class*="phonetic"]', // 包含 phonetic 的类 + '.prons' // 发音区域 + ]; + + for (const selector of selectors) { + const elements = doc.querySelectorAll(selector); + for (const el of elements) { + const text = el.textContent?.trim(); + if (text && text.includes('/')) { + return text; + } + } + } + + // 正则提取 fallback + const bodyText = doc.body?.textContent || ''; + const match = bodyText.match(/\[[\u0250-\u02AEˈˌ]+\]/); + if (match) { + return match[0]; + } + + return ''; } /** - * 获取模拟释义 + * 提取释义 * @private - * @param {string} word - 单词 + * @param {Document} doc - HTML 文档 * @returns {Array} 释义列表 */ - _getMockMeanings(word) { - const meaningsMap = { - 'hello': [ - createMeaning('int.', ['你好', '喂', '嘿']), - createMeaning('n.', ['问候', '招呼']) - ], - 'world': [ - createMeaning('n.', ['世界', '地球', '天下', '界']) - ], - 'apple': [ - createMeaning('n.', ['苹果', '苹果公司']) - ], - 'computer': [ - createMeaning('n.', ['计算机', '电脑']) - ], - 'dictionary': [ - createMeaning('n.', ['词典', '字典', '辞典']) - ] - }; - - return meaningsMap[word.toLowerCase()] || [ - createMeaning('n.', [`${word} 的中文释义`]), - createMeaning('v.', [`${word} 的动词释义`]) + _extractMeanings(doc) { + const meanings = []; + + // 尝试多个可能的选择器 + const selectors = [ + '.qdef ul li', // 主要释义列表 + '.def li', // 备选释义 + '[class*="meaning"] li', // 包含 meaning 的类 + '.content ul li' // 通用内容列表 ]; + + for (const selector of selectors) { + const items = doc.querySelectorAll(selector); + + for (const item of items) { + const text = item.textContent?.trim(); + if (!text) continue; + + // 尝试匹配词性和释义 + const match = text.match(/^([a-zA-Z]+\.?)\s*(.+)$/); + if (match) { + const partOfSpeech = match[1]; + const defsText = match[2]; + + // 分割多个释义 + const definitions = defsText + .split(/[;;]/) + .map(d => d.trim()) + .filter(d => d.length > 0); + + if (definitions.length > 0) { + meanings.push(createMeaning(partOfSpeech, definitions)); + } + } + } + + if (meanings.length > 0) break; + } + + return meanings.length > 0 ? meanings : [createMeaning('n.', ['暂无释义'])]; } /** - * 获取模拟例句 + * 提取例句 * @private - * @param {string} word - 单词 + * @param {Document} doc - HTML 文档 * @returns {Array} 例句列表 */ - _getMockExamples(word) { - const examplesMap = { - 'hello': [ - createExample('Hello, how are you?', '你好,你好吗?'), - createExample('She said hello to everyone.', '她向每个人问好。') - ], - 'world': [ - createExample('The world is getting smaller.', '世界变得越来越小。'), - createExample('He wants to travel around the world.', '他想环游世界。') - ], - 'apple': [ - createExample('An apple a day keeps the doctor away.', '一天一苹果,医生远离我。'), - createExample('She took a bite of the apple.', '她咬了一口苹果。') - ], - 'computer': [ - createExample('I use a computer for work.', '我用电脑工作。'), - createExample('The computer is running slowly.', '电脑运行很慢。') - ], - 'dictionary': [ - createExample('I looked it up in the dictionary.', '我在词典里查了一下。'), - createExample('This is an English-Chinese dictionary.', '这是一本英汉词典。') - ] - }; - - return examplesMap[word.toLowerCase()] || [ - createExample(`This is a sentence with "${word}".`, `这是一个包含"${word}"的句子。`), - createExample(`Can you use "${word}" in a sentence?`, `你能用"${word}"造句吗?`) + _extractExamples(doc) { + const examples = []; + + // 尝试多个可能的选择器 + const selectors = [ + '.sen_li', // 主要例句容器 + '.sentences li', // 备选例句 + '[class*="example"] li', // 包含 example 的类 + '.content .ex_li' // 通用例句 ]; + + for (const selector of selectors) { + const items = doc.querySelectorAll(selector); + + for (const item of items) { + const enEl = item.querySelector('.sen_en, .en_sent, [class*="english"]'); + const cnEl = item.querySelector('.sen_cn, .cn_sent, [class*="chinese"]'); + + const sentence = enEl?.textContent?.trim() || item.textContent?.trim(); + const translation = cnEl?.textContent?.trim() || ''; + + if (sentence) { + examples.push(createExample(sentence, translation)); + } + + if (examples.length >= 2) break; + } + + if (examples.length > 0) break; + } + + return examples; } }