feat(M3.7): 必应词典真实API (v0.2.7)
This commit is contained in:
parent
7bb557be01
commit
e279da0c8b
@ -7,7 +7,7 @@
|
|||||||
## 版本速查
|
## 版本速查
|
||||||
|
|
||||||
### 当前版本
|
### 当前版本
|
||||||
`0.2.6` → 下一目标 `0.2.7` ([M3.7](./M3.md))
|
`0.2.7` → 下一目标 `0.2.8` ([M3.8](./M3.md))
|
||||||
|
|
||||||
### 模块版本范围
|
### 模块版本范围
|
||||||
|
|
||||||
|
|||||||
@ -65,9 +65,9 @@ M11.10完成 → 1.0.0 (正式发布)
|
|||||||
|
|
||||||
## 当前状态
|
## 当前状态
|
||||||
|
|
||||||
**当前版本**: `0.2.6`
|
**当前版本**: `0.2.7`
|
||||||
**当前进度**: 20/97 (21%)
|
**当前进度**: 21/97 (22%)
|
||||||
**下一任务**: [M3.7 必应词典真实API](./M3.md#m37-必应词典真实api--目标版本-027)
|
**下一任务**: [M3.8 加载状态显示](./M3.md#m38-加载状态显示--目标版本-028)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -42,7 +42,7 @@
|
|||||||
| M3.4 | 0.2.4 | 后台查询接口 | ✅ | 2026-02-11 |
|
| M3.4 | 0.2.4 | 后台查询接口 | ✅ | 2026-02-11 |
|
||||||
| M3.5 | 0.2.5 | 结果展示组件 | ✅ | 2026-02-11 |
|
| M3.5 | 0.2.5 | 结果展示组件 | ✅ | 2026-02-11 |
|
||||||
| M3.6 | 0.2.6 | 点击图标查词(Mock) | ✅ | 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.8 | 0.2.8 | 加载状态显示 | ⬜ | - |
|
||||||
| M3.9 | 0.2.9 | 有道词典实现 | ⬜ | - |
|
| M3.9 | 0.2.9 | 有道词典实现 | ⬜ | - |
|
||||||
| M3.10 | 0.2.10 | 结果折叠/展开 | ⬜ | - |
|
| M3.10 | 0.2.10 | 结果折叠/展开 | ⬜ | - |
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "沙拉查词",
|
"name": "沙拉查词",
|
||||||
"version": "0.2.6",
|
"version": "0.2.7",
|
||||||
"description": "聚合词典划词翻译",
|
"description": "聚合词典划词翻译",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
"storage",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "salad-dict",
|
"name": "salad-dict",
|
||||||
"version": "0.2.6",
|
"version": "0.2.7",
|
||||||
"description": "聚合词典划词翻译",
|
"description": "聚合词典划词翻译",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@ -47,4 +47,34 @@ backgroundHandler.register('DICT.GET_LIST', async () => {
|
|||||||
return { dictionaries };
|
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');
|
console.log('[SaladDict] Message handlers registered');
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* @file 必应词典实现(Mock版)
|
* @file 必应词典实现
|
||||||
* @description 必应词典的 Mock 实现,返回模拟数据
|
* @description 必应词典的真实实现,通过 HTTP 请求获取数据
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { DictionaryBase, createResult, createMeaning, createExample } from './base.js';
|
import { DictionaryBase, createResult, createMeaning, createExample } from './base.js';
|
||||||
|
import { messaging } from '../messaging.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 必应词典 Mock 实现
|
* 必应词典实现
|
||||||
*/
|
*/
|
||||||
export class BingDictionary extends DictionaryBase {
|
export class BingDictionary extends DictionaryBase {
|
||||||
constructor(config = {}) {
|
constructor(config = {}) {
|
||||||
@ -19,124 +20,193 @@ export class BingDictionary extends DictionaryBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询单词(Mock 实现)
|
* 查询单词
|
||||||
* @param {string} word - 要查询的单词
|
* @param {string} word - 要查询的单词
|
||||||
* @returns {Promise<DictionaryResult>} 模拟查询结果
|
* @returns {Promise<DictionaryResult>} 查询结果
|
||||||
*/
|
*/
|
||||||
async search(word) {
|
async search(word) {
|
||||||
if (!word?.trim()) {
|
if (!word?.trim()) {
|
||||||
throw new Error('Word is empty');
|
throw new Error('查询单词不能为空');
|
||||||
}
|
}
|
||||||
|
|
||||||
const trimmedWord = word.trim();
|
const trimmedWord = word.trim();
|
||||||
|
const url = `https://cn.bing.com/dict/search?q=${encodeURIComponent(trimmedWord)}`;
|
||||||
|
|
||||||
// 模拟网络延迟
|
try {
|
||||||
await this._delay(100);
|
// 通过 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({
|
return createResult({
|
||||||
word: trimmedWord,
|
word,
|
||||||
phonetic: this._getMockPhonetic(trimmedWord),
|
phonetic,
|
||||||
meanings: this._getMockMeanings(trimmedWord),
|
meanings,
|
||||||
examples: this._getMockExamples(trimmedWord),
|
examples,
|
||||||
url: `https://cn.bing.com/dict/search?q=${encodeURIComponent(trimmedWord)}`
|
url
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 模拟延迟
|
* 提取音标
|
||||||
* @private
|
* @private
|
||||||
* @param {number} ms - 延迟毫秒数
|
* @param {Document} doc - HTML 文档
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
_delay(ms) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取模拟音标
|
|
||||||
* @private
|
|
||||||
* @param {string} word - 单词
|
|
||||||
* @returns {string} 音标
|
* @returns {string} 音标
|
||||||
*/
|
*/
|
||||||
_getMockPhonetic(word) {
|
_extractPhonetic(doc) {
|
||||||
const phonetics = {
|
// 尝试多个可能的选择器
|
||||||
'hello': '/həˈləʊ/',
|
const selectors = [
|
||||||
'world': '/wɜːld/',
|
'.hd_p1_1F_OWM', // 主要音标容器
|
||||||
'apple': '/ˈæpl/',
|
'.hd_tf_lh', // 音标文本
|
||||||
'computer': '/kəmˈpjuːtə(r)/',
|
'[class*="phonetic"]', // 包含 phonetic 的类
|
||||||
'dictionary': '/ˈdɪkʃənri/'
|
'.prons' // 发音区域
|
||||||
};
|
];
|
||||||
|
|
||||||
return phonetics[word.toLowerCase()] || `/${word.toLowerCase()}/`;
|
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
|
* @private
|
||||||
* @param {string} word - 单词
|
* @param {Document} doc - HTML 文档
|
||||||
* @returns {Array<Meaning>} 释义列表
|
* @returns {Array<Meaning>} 释义列表
|
||||||
*/
|
*/
|
||||||
_getMockMeanings(word) {
|
_extractMeanings(doc) {
|
||||||
const meaningsMap = {
|
const meanings = [];
|
||||||
'hello': [
|
|
||||||
createMeaning('int.', ['你好', '喂', '嘿']),
|
// 尝试多个可能的选择器
|
||||||
createMeaning('n.', ['问候', '招呼'])
|
const selectors = [
|
||||||
],
|
'.qdef ul li', // 主要释义列表
|
||||||
'world': [
|
'.def li', // 备选释义
|
||||||
createMeaning('n.', ['世界', '地球', '天下', '界'])
|
'[class*="meaning"] li', // 包含 meaning 的类
|
||||||
],
|
'.content ul li' // 通用内容列表
|
||||||
'apple': [
|
|
||||||
createMeaning('n.', ['苹果', '苹果公司'])
|
|
||||||
],
|
|
||||||
'computer': [
|
|
||||||
createMeaning('n.', ['计算机', '电脑'])
|
|
||||||
],
|
|
||||||
'dictionary': [
|
|
||||||
createMeaning('n.', ['词典', '字典', '辞典'])
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
return meaningsMap[word.toLowerCase()] || [
|
|
||||||
createMeaning('n.', [`${word} 的中文释义`]),
|
|
||||||
createMeaning('v.', [`${word} 的动词释义`])
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
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
|
* @private
|
||||||
* @param {string} word - 单词
|
* @param {Document} doc - HTML 文档
|
||||||
* @returns {Array<Example>} 例句列表
|
* @returns {Array<Example>} 例句列表
|
||||||
*/
|
*/
|
||||||
_getMockExamples(word) {
|
_extractExamples(doc) {
|
||||||
const examplesMap = {
|
const examples = [];
|
||||||
'hello': [
|
|
||||||
createExample('Hello, how are you?', '你好,你好吗?'),
|
// 尝试多个可能的选择器
|
||||||
createExample('She said hello to everyone.', '她向每个人问好。')
|
const selectors = [
|
||||||
],
|
'.sen_li', // 主要例句容器
|
||||||
'world': [
|
'.sentences li', // 备选例句
|
||||||
createExample('The world is getting smaller.', '世界变得越来越小。'),
|
'[class*="example"] li', // 包含 example 的类
|
||||||
createExample('He wants to travel around the world.', '他想环游世界。')
|
'.content .ex_li' // 通用例句
|
||||||
],
|
|
||||||
'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}"造句吗?`)
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user