mirror of
https://github.com/moeru-ai/airi.git
synced 2026-08-14 08:52:42 +00:00
refactor(airi-plugin-web-extension): use eventa for ipc
This commit is contained in:
@@ -2,9 +2,10 @@ import type {
|
||||
BackgroundToContentMessage,
|
||||
ContentToBackgroundMessage,
|
||||
ExtensionSettings,
|
||||
PopupToBackgroundMessage,
|
||||
} from '../src/shared/types'
|
||||
|
||||
import { defineInvokeHandler } from '@moeru/eventa'
|
||||
|
||||
import {
|
||||
createClientState,
|
||||
ensureClient,
|
||||
@@ -15,6 +16,15 @@ import {
|
||||
} from '../src/background/client'
|
||||
import { loadSettings, saveSettings } from '../src/background/storage'
|
||||
import { DEFAULT_SETTINGS, STORAGE_KEY } from '../src/shared/constants'
|
||||
import {
|
||||
backgroundStatusChanged,
|
||||
popupClearError,
|
||||
popupGetStatus,
|
||||
popupRequestVisionFrame,
|
||||
popupToggleEnabled,
|
||||
popupUpdateSettings,
|
||||
} from '../src/shared/eventa'
|
||||
import { createRuntimeEventaContext } from '../src/shared/eventa-runtime'
|
||||
import { detectSiteFromUrl } from '../src/shared/sites'
|
||||
|
||||
const state = createClientState()
|
||||
@@ -23,6 +33,7 @@ let settings: ExtensionSettings = { ...DEFAULT_SETTINGS }
|
||||
let lastVideoNotifyKey = ''
|
||||
let lastStatusSentAt = 0
|
||||
let connectionKey = ''
|
||||
let eventaContext: ReturnType<typeof createRuntimeEventaContext>['context'] | undefined
|
||||
|
||||
async function refreshClient() {
|
||||
const nextKey = `${settings.enabled}:${settings.wsUrl}:${settings.token}`
|
||||
@@ -54,7 +65,7 @@ function emitStatus() {
|
||||
return
|
||||
|
||||
lastStatusSentAt = now
|
||||
void browser.runtime.sendMessage({ type: 'background:status', payload: toStatus(state, settings) }).catch(() => {})
|
||||
eventaContext?.emit(backgroundStatusChanged, toStatus(state, settings))
|
||||
}
|
||||
|
||||
async function updateSettings(partial: Partial<ExtensionSettings>) {
|
||||
@@ -106,45 +117,47 @@ function handleContentMessage(message: ContentToBackgroundMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePopupMessage(message: PopupToBackgroundMessage) {
|
||||
switch (message.type) {
|
||||
case 'popup:get-status':
|
||||
return toStatus(state, settings)
|
||||
case 'popup:update-settings':
|
||||
await updateSettings(message.payload)
|
||||
return toStatus(state, settings)
|
||||
case 'popup:toggle-enabled':
|
||||
await updateSettings({ enabled: message.payload })
|
||||
return toStatus(state, settings)
|
||||
case 'popup:request-vision-frame': {
|
||||
const message: BackgroundToContentMessage = { type: 'background:request-vision-frame' }
|
||||
const tabs = await browser.tabs.query({ active: true, currentWindow: true })
|
||||
const tab = tabs[0]
|
||||
if (tab?.id != null) {
|
||||
await browser.tabs.sendMessage(tab.id, message).catch(() => {})
|
||||
}
|
||||
return toStatus(state, settings)
|
||||
}
|
||||
case 'popup:clear-error':
|
||||
state.lastError = undefined
|
||||
emitStatus()
|
||||
return toStatus(state, settings)
|
||||
}
|
||||
}
|
||||
|
||||
export default defineBackground(() => {
|
||||
const { context } = createRuntimeEventaContext()
|
||||
eventaContext = context
|
||||
|
||||
defineInvokeHandler(context, popupGetStatus, () => toStatus(state, settings))
|
||||
defineInvokeHandler(context, popupUpdateSettings, async (partial) => {
|
||||
await updateSettings(partial)
|
||||
return toStatus(state, settings)
|
||||
})
|
||||
|
||||
defineInvokeHandler(context, popupToggleEnabled, async (enabled) => {
|
||||
await updateSettings({ enabled })
|
||||
return toStatus(state, settings)
|
||||
})
|
||||
|
||||
defineInvokeHandler(context, popupRequestVisionFrame, async () => {
|
||||
const message: BackgroundToContentMessage = { type: 'background:request-vision-frame' }
|
||||
const tabs = await browser.tabs.query({ active: true, currentWindow: true })
|
||||
const tab = tabs[0]
|
||||
if (tab?.id != null) {
|
||||
await browser.tabs.sendMessage(tab.id, message).catch(() => {})
|
||||
}
|
||||
|
||||
return toStatus(state, settings)
|
||||
})
|
||||
|
||||
defineInvokeHandler(context, popupClearError, () => {
|
||||
state.lastError = undefined
|
||||
emitStatus()
|
||||
return toStatus(state, settings)
|
||||
})
|
||||
|
||||
void init()
|
||||
|
||||
browser.runtime.onMessage.addListener((message: ContentToBackgroundMessage | PopupToBackgroundMessage) => {
|
||||
if (message && typeof message === 'object' && 'type' in message) {
|
||||
if (message.type.startsWith('content:')) {
|
||||
handleContentMessage(message as ContentToBackgroundMessage)
|
||||
return
|
||||
}
|
||||
|
||||
if (message.type.startsWith('popup:')) {
|
||||
return handlePopupMessage(message as PopupToBackgroundMessage)
|
||||
}
|
||||
browser.runtime.onMessage.addListener((message: unknown) => {
|
||||
if (!message || typeof message !== 'object')
|
||||
return
|
||||
if ('__eventa' in message)
|
||||
return
|
||||
if ('type' in message && typeof message.type === 'string' && message.type.startsWith('content:')) {
|
||||
handleContentMessage(message as ContentToBackgroundMessage)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -2,10 +2,7 @@ import { startContentObserver } from '../src/content'
|
||||
|
||||
export default defineContentScript({
|
||||
matches: [
|
||||
'*://*.youtube.com/*',
|
||||
'*://*.youtu.be/*',
|
||||
'*://*.bilibili.com/*',
|
||||
'*://*.b23.tv/*',
|
||||
'*://*/*',
|
||||
],
|
||||
runAt: 'document_idle',
|
||||
main() {
|
||||
|
||||
@@ -17,7 +17,7 @@ body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 600px;
|
||||
min-width: 420px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,15 +14,14 @@
|
||||
"compile": "vue-tsc --noEmit",
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/solar": "^1.2.5",
|
||||
"@iconify-json/svg-spinners": "^1.2.4",
|
||||
"@moeru/eventa": "^1.0.0-alpha.10",
|
||||
"@proj-airi/server-sdk": "workspace:^",
|
||||
"@proj-airi/ui": "workspace:^",
|
||||
"@unocss/reset": "^66.5.11",
|
||||
"@vueuse/core": "catalog:",
|
||||
"@wxt-dev/module-vue": "^1.0.3",
|
||||
"nanoid": "^5.1.6",
|
||||
"vue": "catalog:",
|
||||
|
||||
@@ -235,6 +235,8 @@ function observeVideo(site: VideoSite) {
|
||||
safeSend({ type: 'content:page', payload: buildPageContext(site) })
|
||||
}
|
||||
|
||||
const onPlayback = () => sendVideo(true)
|
||||
|
||||
const attach = () => {
|
||||
const found = document.querySelector('video') as HTMLVideoElement | null
|
||||
if (!found || found === video)
|
||||
@@ -271,8 +273,6 @@ function observeVideo(site: VideoSite) {
|
||||
sendVideo(false)
|
||||
}, TITLE_POLL_INTERVAL)
|
||||
|
||||
const onPlayback = () => sendVideo(true)
|
||||
|
||||
const cleanup = () => {
|
||||
window.clearInterval(interval)
|
||||
window.clearInterval(progressInterval)
|
||||
|
||||
@@ -1,32 +1,22 @@
|
||||
import type { BackgroundToPopupMessage, ExtensionSettings, ExtensionStatus, PopupToBackgroundMessage } from '../shared/types'
|
||||
import type { ExtensionStatus } from '../shared/types'
|
||||
|
||||
export async function requestStatus(): Promise<ExtensionStatus> {
|
||||
return await browser.runtime.sendMessage({ type: 'popup:get-status' } satisfies PopupToBackgroundMessage)
|
||||
}
|
||||
import { defineInvoke } from '@moeru/eventa'
|
||||
|
||||
export async function updateSettings(partial: Partial<ExtensionSettings>): Promise<ExtensionStatus> {
|
||||
return await browser.runtime.sendMessage({ type: 'popup:update-settings', payload: partial } satisfies PopupToBackgroundMessage)
|
||||
}
|
||||
import { backgroundStatusChanged, popupClearError, popupGetStatus, popupRequestVisionFrame, popupToggleEnabled, popupUpdateSettings } from '../shared/eventa'
|
||||
import { createRuntimeEventaContext } from '../shared/eventa-runtime'
|
||||
|
||||
export async function toggleEnabled(enabled: boolean): Promise<ExtensionStatus> {
|
||||
return await browser.runtime.sendMessage({ type: 'popup:toggle-enabled', payload: enabled } satisfies PopupToBackgroundMessage)
|
||||
}
|
||||
const { context } = createRuntimeEventaContext()
|
||||
|
||||
export async function requestVisionFrame(): Promise<ExtensionStatus> {
|
||||
return await browser.runtime.sendMessage({ type: 'popup:request-vision-frame' } satisfies PopupToBackgroundMessage)
|
||||
}
|
||||
|
||||
export async function clearError(): Promise<ExtensionStatus> {
|
||||
return await browser.runtime.sendMessage({ type: 'popup:clear-error' } satisfies PopupToBackgroundMessage)
|
||||
}
|
||||
export const requestStatus = defineInvoke(context, popupGetStatus)
|
||||
export const updateSettings = defineInvoke(context, popupUpdateSettings)
|
||||
export const toggleEnabled = defineInvoke(context, popupToggleEnabled)
|
||||
export const requestVisionFrame = defineInvoke(context, popupRequestVisionFrame)
|
||||
export const clearError = defineInvoke(context, popupClearError)
|
||||
|
||||
export function onBackgroundStatus(callback: (status: ExtensionStatus) => void) {
|
||||
const listener = (message: BackgroundToPopupMessage) => {
|
||||
if (message?.type === 'background:status')
|
||||
callback(message.payload)
|
||||
}
|
||||
const off = context.on(backgroundStatusChanged, (event) => {
|
||||
callback(event.body!)
|
||||
})
|
||||
|
||||
browser.runtime.onMessage.addListener(listener)
|
||||
|
||||
return () => browser.runtime.onMessage.removeListener(listener)
|
||||
return () => off()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { createContext } from '@moeru/eventa/adapters/event-target'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
const EVENTA_MESSAGE_EVENT = 'eventa:message'
|
||||
const EVENTA_RUNTIME_CHANNEL = 'airi:web-extension:eventa'
|
||||
|
||||
const runtimeInstanceId = nanoid()
|
||||
|
||||
interface EventaRuntimeMessage {
|
||||
__eventa: true
|
||||
channel: string
|
||||
sourceId: string
|
||||
type?: string
|
||||
detail?: unknown
|
||||
}
|
||||
|
||||
type RuntimeEventListener = (event: Event) => void
|
||||
|
||||
class RuntimeEventTarget implements EventTarget {
|
||||
private listeners = new Map<string, Map<EventListenerOrEventListenerObject, RuntimeEventListener>>()
|
||||
|
||||
constructor(private readonly send: (message: EventaRuntimeMessage) => void) {}
|
||||
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject | null) {
|
||||
if (!listener)
|
||||
return
|
||||
|
||||
if (!this.listeners.has(type))
|
||||
this.listeners.set(type, new Map())
|
||||
|
||||
const handler: RuntimeEventListener = typeof listener === 'function'
|
||||
? listener
|
||||
: event => listener.handleEvent(event)
|
||||
|
||||
this.listeners.get(type)?.set(listener, handler)
|
||||
}
|
||||
|
||||
removeEventListener(type: string, listener: EventListenerOrEventListenerObject | null) {
|
||||
if (!listener)
|
||||
return
|
||||
|
||||
this.listeners.get(type)?.delete(listener)
|
||||
}
|
||||
|
||||
dispatchEvent(event: Event) {
|
||||
const detail = 'detail' in event ? (event as CustomEvent).detail : undefined
|
||||
this.send({
|
||||
__eventa: true,
|
||||
channel: EVENTA_RUNTIME_CHANNEL,
|
||||
sourceId: runtimeInstanceId,
|
||||
type: event.type,
|
||||
detail,
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
emit(type: string, detail?: unknown) {
|
||||
const event = { type, detail } as CustomEvent
|
||||
for (const listener of this.listeners.get(type)?.values() ?? [])
|
||||
listener(event)
|
||||
}
|
||||
}
|
||||
|
||||
export function createRuntimeEventaContext() {
|
||||
const eventTarget = new RuntimeEventTarget((message) => {
|
||||
void browser.runtime.sendMessage(message).catch(() => {})
|
||||
})
|
||||
|
||||
const { context, dispose } = createContext(eventTarget, {
|
||||
messageEventName: EVENTA_MESSAGE_EVENT,
|
||||
errorEventName: false,
|
||||
})
|
||||
|
||||
const runtimeListener = (message: unknown) => {
|
||||
if (!message || typeof message !== 'object')
|
||||
return
|
||||
|
||||
const runtimeMessage = message as EventaRuntimeMessage
|
||||
|
||||
if (!('__eventa' in runtimeMessage) || !runtimeMessage.__eventa)
|
||||
return
|
||||
|
||||
if (runtimeMessage.channel !== EVENTA_RUNTIME_CHANNEL)
|
||||
return
|
||||
|
||||
if (runtimeMessage.sourceId === runtimeInstanceId)
|
||||
return
|
||||
|
||||
eventTarget.emit(runtimeMessage.type ?? EVENTA_MESSAGE_EVENT, runtimeMessage.detail)
|
||||
}
|
||||
|
||||
browser.runtime.onMessage.addListener(runtimeListener)
|
||||
|
||||
return {
|
||||
context,
|
||||
dispose: () => {
|
||||
browser.runtime.onMessage.removeListener(runtimeListener)
|
||||
dispose()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { ExtensionSettings, ExtensionStatus } from './types'
|
||||
|
||||
import { defineEventa, defineInvokeEventa } from '@moeru/eventa'
|
||||
|
||||
export const popupGetStatus = defineInvokeEventa<ExtensionStatus>('eventa:invoke:web-extension:popup:get-status')
|
||||
export const popupUpdateSettings = defineInvokeEventa<ExtensionStatus, Partial<ExtensionSettings>>('eventa:invoke:web-extension:popup:update-settings')
|
||||
export const popupToggleEnabled = defineInvokeEventa<ExtensionStatus, boolean>('eventa:invoke:web-extension:popup:toggle-enabled')
|
||||
export const popupRequestVisionFrame = defineInvokeEventa<ExtensionStatus>('eventa:invoke:web-extension:popup:request-vision-frame')
|
||||
export const popupClearError = defineInvokeEventa<ExtensionStatus>('eventa:invoke:web-extension:popup:clear-error')
|
||||
|
||||
export const backgroundStatusChanged = defineEventa<ExtensionStatus>('eventa:event:web-extension:background:status')
|
||||
@@ -74,15 +74,5 @@ export interface ExtensionStatus {
|
||||
lastVisionFrameAt?: number
|
||||
}
|
||||
|
||||
export type PopupToBackgroundMessage
|
||||
= | { type: 'popup:get-status' }
|
||||
| { type: 'popup:update-settings', payload: Partial<ExtensionSettings> }
|
||||
| { type: 'popup:toggle-enabled', payload: boolean }
|
||||
| { type: 'popup:request-vision-frame' }
|
||||
| { type: 'popup:clear-error' }
|
||||
|
||||
export type BackgroundToPopupMessage
|
||||
= | { type: 'background:status', payload: ExtensionStatus }
|
||||
|
||||
export type BackgroundToContentMessage
|
||||
= | { type: 'background:request-vision-frame' }
|
||||
|
||||
@@ -13,11 +13,8 @@ export default defineConfig({
|
||||
name: 'AIRI Web Extension',
|
||||
description: 'Capture web context (videos, pages, subtitles) for Project AIRI.',
|
||||
permissions: ['storage', 'tabs'],
|
||||
host_permissions: [
|
||||
'*://*.youtube.com/*',
|
||||
'*://*.youtu.be/*',
|
||||
'*://*.bilibili.com/*',
|
||||
'*://*.b23.tv/*',
|
||||
optional_host_permissions: [
|
||||
'*://*/*',
|
||||
],
|
||||
action: {
|
||||
default_title: 'AIRI Web Extension',
|
||||
|
||||
Generated
+15
-7
@@ -3006,10 +3006,6 @@ importers:
|
||||
version: 1.5.1
|
||||
|
||||
plugins/airi-plugin-web-extension:
|
||||
dependencies:
|
||||
'@vueuse/core':
|
||||
specifier: 'catalog:'
|
||||
version: 14.1.0(vue@3.5.26(typescript@5.9.3))
|
||||
devDependencies:
|
||||
'@iconify-json/solar':
|
||||
specifier: ^1.2.5
|
||||
@@ -3017,6 +3013,9 @@ importers:
|
||||
'@iconify-json/svg-spinners':
|
||||
specifier: ^1.2.4
|
||||
version: 1.2.4
|
||||
'@moeru/eventa':
|
||||
specifier: ^1.0.0-alpha.10
|
||||
version: 1.0.0-alpha.10(electron@39.2.7)(h3@2.0.1-rc.5(crossws@0.4.1(srvx@0.9.8(patch_hash=f0151386fdcbcb6f53833cf8f66926ef2eb31b71a2782d6e0960dec832ef108d))))
|
||||
'@proj-airi/server-sdk':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/server-sdk
|
||||
@@ -3026,9 +3025,12 @@ importers:
|
||||
'@unocss/reset':
|
||||
specifier: ^66.5.11
|
||||
version: 66.5.11
|
||||
'@vueuse/core':
|
||||
specifier: 'catalog:'
|
||||
version: 14.1.0(vue@3.5.26(typescript@5.9.3))
|
||||
'@wxt-dev/module-vue':
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
version: 1.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
nanoid:
|
||||
specifier: ^5.1.6
|
||||
version: 5.1.6
|
||||
@@ -22249,6 +22251,12 @@ snapshots:
|
||||
vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vue: 3.5.26(typescript@5.9.3)
|
||||
|
||||
'@vitejs/plugin-vue@6.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-beta.53
|
||||
vite: 8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vue: 3.5.26(typescript@5.9.3)
|
||||
|
||||
'@vitest/browser-playwright@4.0.16(bufferutil@4.1.0)(playwright@1.57.0)(utf-8-validate@5.0.10)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)':
|
||||
dependencies:
|
||||
'@vitest/browser': 4.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)
|
||||
@@ -22948,9 +22956,9 @@ snapshots:
|
||||
'@types/filesystem': 0.0.36
|
||||
'@types/har-format': 1.2.16
|
||||
|
||||
'@wxt-dev/module-vue@1.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
'@wxt-dev/module-vue@1.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@vitejs/plugin-vue': 6.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))
|
||||
'@vitejs/plugin-vue': 6.0.3(vite@8.0.0-beta.5(@types/node@24.10.4)(esbuild@0.25.12)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))
|
||||
wxt: 0.20.13(@types/node@24.10.4)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- vite
|
||||
|
||||
Reference in New Issue
Block a user