background.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. let isPlayerActive = false;
  2. const ttsUrl = 'https://vs.aimanyi.top/generate';
  3. const grammarApiBase = 'https://gram.aimanyi.top';
  4. // ---------- Grammar API ----------
  5. async function fetchHighlights(text) {
  6. const response = await fetch(`${grammarApiBase}/analyze`, {
  7. method: 'POST',
  8. headers: { 'Content-Type': 'application/json' },
  9. body: JSON.stringify({ text })
  10. });
  11. if (!response.ok) {
  12. const detail = await response.text();
  13. throw new Error(detail || '后台服务调用失败。');
  14. }
  15. const data = await response.json();
  16. return data.highlighted_html;
  17. }
  18. async function fetchParagraphHighlights(paragraphs) {
  19. const results = [];
  20. for (const paragraph of paragraphs) {
  21. const highlighted = await fetchHighlights(paragraph);
  22. results.push(highlighted);
  23. }
  24. return results;
  25. }
  26. chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
  27. if (message?.type !== 'GRAMMAR_API_REQUEST') {
  28. return;
  29. }
  30. const { text, paragraphs } = message;
  31. (Array.isArray(paragraphs) && paragraphs.length > 0
  32. ? fetchParagraphHighlights(paragraphs).then((highlightedHtmls) => ({
  33. highlightedHtmls
  34. }))
  35. : fetchHighlights(text).then((highlightedHtml) => ({ highlightedHtml })))
  36. .then((payload) => sendResponse(payload))
  37. .catch((error) => {
  38. sendResponse({ error: error.message || '后台服务调用失败。' });
  39. });
  40. return true;
  41. });
  42. // ---------- TTS Stream ----------
  43. chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  44. if (request.action === "fetchTTS") {
  45. handleTTSRequest(request, sendResponse);
  46. return true;
  47. }
  48. });
  49. async function handleTTSRequest(request, sendResponse) {
  50. try {
  51. const response = await fetch(ttsUrl, {
  52. method: 'POST',
  53. headers: { 'Content-Type': 'application/json' },
  54. body: JSON.stringify({
  55. text: request.text,
  56. voice: request.voice,
  57. speed: request.speed
  58. })
  59. });
  60. if (!response.ok) {
  61. throw new Error(`Server error: ${response.status}`);
  62. }
  63. const reader = response.body.getReader();
  64. const decoder = new TextDecoder();
  65. let buffer = '';
  66. const audioChunks = [];
  67. while (true) {
  68. const { done, value } = await reader.read();
  69. if (done) break;
  70. buffer += decoder.decode(value, { stream: true });
  71. const lines = buffer.split('\n');
  72. buffer = lines.pop() || '';
  73. for (const line of lines) {
  74. if (!line.trim()) continue;
  75. try {
  76. const data = JSON.parse(line);
  77. if (data.audio) {
  78. audioChunks.push(data);
  79. }
  80. } catch (e) {
  81. console.error('Parse error:', e);
  82. }
  83. }
  84. }
  85. if (buffer.trim()) {
  86. try {
  87. const data = JSON.parse(buffer.trim());
  88. if (data.audio) {
  89. audioChunks.push(data);
  90. }
  91. } catch (e) {
  92. console.error('Parse error:', e);
  93. }
  94. }
  95. sendResponse({ audioChunks });
  96. } catch (error) {
  97. console.error('TTS request failed:', error);
  98. sendResponse({ error: error.message });
  99. }
  100. }
  101. chrome.runtime.onConnect.addListener((port) => {
  102. if (port.name !== "tts-stream") {
  103. return;
  104. }
  105. let isDisconnected = false;
  106. const closePort = () => {
  107. if (isDisconnected) return;
  108. isDisconnected = true;
  109. try {
  110. port.disconnect();
  111. } catch (err) {
  112. console.warn('Port disconnect failed:', err);
  113. }
  114. };
  115. const safePostMessage = (payload) => {
  116. if (isDisconnected) return;
  117. try {
  118. port.postMessage(payload);
  119. } catch (err) {
  120. console.warn('Port post failed:', err);
  121. closePort();
  122. }
  123. };
  124. port.onDisconnect.addListener(() => {
  125. isDisconnected = true;
  126. });
  127. port.onMessage.addListener(async (request) => {
  128. if (request.action !== "startStreamTTS" || isDisconnected) {
  129. return;
  130. }
  131. try {
  132. const response = await fetch(ttsUrl, {
  133. method: 'POST',
  134. headers: { 'Content-Type': 'application/json' },
  135. body: JSON.stringify({
  136. text: request.text,
  137. voice: request.voice,
  138. speed: request.speed
  139. })
  140. });
  141. if (!response.ok) {
  142. safePostMessage({ error: `Server error: ${response.status}` });
  143. closePort();
  144. return;
  145. }
  146. const reader = response.body.getReader();
  147. const decoder = new TextDecoder();
  148. let buffer = '';
  149. let index = 0;
  150. while (!isDisconnected) {
  151. const { done, value } = await reader.read();
  152. if (done) break;
  153. buffer += decoder.decode(value, { stream: true });
  154. const lines = buffer.split('\n');
  155. buffer = lines.pop() || '';
  156. for (const line of lines) {
  157. if (!line.trim() || isDisconnected) continue;
  158. try {
  159. const data = JSON.parse(line);
  160. if (data.audio) {
  161. safePostMessage({
  162. audio: data.audio,
  163. index: index++
  164. });
  165. }
  166. } catch (e) {
  167. console.error('Parse error:', e);
  168. }
  169. }
  170. }
  171. if (!isDisconnected && buffer.trim()) {
  172. try {
  173. const data = JSON.parse(buffer.trim());
  174. if (data.audio) {
  175. safePostMessage({
  176. audio: data.audio,
  177. index: index++
  178. });
  179. }
  180. } catch (e) {
  181. console.error('Parse error:', e);
  182. }
  183. }
  184. safePostMessage({ completed: true });
  185. closePort();
  186. } catch (error) {
  187. safePostMessage({ error: error.message });
  188. closePort();
  189. }
  190. });
  191. });
  192. // ---------- Player Toggle ----------
  193. chrome.action.onClicked.addListener(async (tab) => {
  194. const url = tab?.url ?? tab?.pendingUrl ?? '';
  195. if (!url || /^(chrome|chrome-extension|edge|about):/i.test(url)) {
  196. console.warn('受限制的页面,跳过播放器注入:', url);
  197. return;
  198. }
  199. if (!tab?.id) {
  200. console.warn('无法获取标签页 ID,跳过播放器注入');
  201. return;
  202. }
  203. try {
  204. if (!isPlayerActive) {
  205. await chrome.scripting.executeScript({
  206. target: { tabId: tab.id },
  207. files: ['content.js']
  208. });
  209. await chrome.scripting.insertCSS({
  210. target: { tabId: tab.id },
  211. files: ['styles.css']
  212. });
  213. }
  214. } catch (error) {
  215. const lastError = chrome.runtime.lastError?.message || error.message;
  216. console.warn('播放器注入失败:', lastError);
  217. return;
  218. }
  219. chrome.tabs.sendMessage(
  220. tab.id,
  221. { action: isPlayerActive ? "deactivatePlayer" : "activatePlayer" },
  222. () => {
  223. const message = chrome.runtime.lastError?.message;
  224. if (message) {
  225. console.warn('发送播放器指令失败:', message);
  226. return;
  227. }
  228. isPlayerActive = !isPlayerActive;
  229. }
  230. );
  231. });