2026-10-18 17:39:54 +08:00
|
|
|
|
import type {
|
|
|
|
|
|
ReleaseProgressDto as ContractReleaseProgressDto,
|
|
|
|
|
|
CoursewareTransfer,
|
|
|
|
|
|
} from '@classhelper/shared';
|
2026-09-11 22:46:31 +08:00
|
|
|
|
import { contextBridge, ipcRenderer } from 'electron';
|
2026-10-11 11:49:32 +08:00
|
|
|
|
import { IPC_CHANNELS, UPDATE_AVAILABLE_CHANNEL } from '../main/channels.js';
|
|
|
|
|
|
import type { ClientAgentStatus } from '../main/client-agent.js';
|
2026-09-12 08:41:08 +08:00
|
|
|
|
import type {
|
|
|
|
|
|
DesktopAppInfo,
|
|
|
|
|
|
DesktopBridge,
|
|
|
|
|
|
DesktopStoredConfig,
|
|
|
|
|
|
IslandClassStatePayload,
|
|
|
|
|
|
IslandPushContext,
|
|
|
|
|
|
} from '../types/desktop.js';
|
2026-10-05 16:39:38 +08:00
|
|
|
|
import type { IslandNotification, UpdateInfo } from '@classhelper/shared';
|
2026-09-11 22:46:31 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 预加载脚本:在开启了 contextIsolation + sandbox 的前提下,
|
|
|
|
|
|
* 只向渲染进程暴露一组最小的、显式声明的能力(不暴露 ipcRenderer 本体)。
|
2026-09-27 10:43:57 +08:00
|
|
|
|
*
|
|
|
|
|
|
* 注意:contextBridge 在参数跨越"主世界 → 隔离世界"时就做结构化克隆,
|
|
|
|
|
|
* 因此**桥接层无法替调用方兜底**:传 Vue 响应式对象(Proxy)会在进入这里的函数体
|
|
|
|
|
|
* 之前就抛 `An object could not be cloned.`。调用方必须传纯数据,见 desktop.d.ts。
|
2026-09-11 22:46:31 +08:00
|
|
|
|
*/
|
|
|
|
|
|
const bridge: DesktopBridge = {
|
2026-10-25 16:41:29 +08:00
|
|
|
|
getIslandWeather: (coordinates) => ipcRenderer.invoke('island:weather', coordinates),
|
2026-10-18 11:53:37 +08:00
|
|
|
|
downloadCourseware: (id) => ipcRenderer.invoke('courseware:download', id),
|
|
|
|
|
|
revealCourseware: (id) => ipcRenderer.invoke('courseware:reveal', id),
|
2026-10-18 17:39:54 +08:00
|
|
|
|
listLocalCourseware: () => ipcRenderer.invoke('courseware:local-list'),
|
2026-10-18 11:53:37 +08:00
|
|
|
|
syncCourseware: () => ipcRenderer.invoke('courseware:sync'),
|
|
|
|
|
|
getCoursewareTransfer: () => ipcRenderer.invoke('courseware:get-progress'),
|
|
|
|
|
|
onCoursewareTransfer: (handler) => {
|
|
|
|
|
|
const listener = (_event: unknown, value: CoursewareTransfer | null) => handler(value);
|
|
|
|
|
|
ipcRenderer.on('courseware:progress', listener);
|
|
|
|
|
|
return () => ipcRenderer.removeListener('courseware:progress', listener);
|
|
|
|
|
|
},
|
2026-10-17 12:55:39 +08:00
|
|
|
|
islandEditLayout: () => ipcRenderer.invoke('island:edit-layout'),
|
2026-10-17 21:21:28 +08:00
|
|
|
|
onIslandEditorOpen: (handler) => {
|
|
|
|
|
|
const listener = () => handler();
|
|
|
|
|
|
ipcRenderer.on('island:open-editor', listener);
|
|
|
|
|
|
return () => ipcRenderer.removeListener('island:open-editor', listener);
|
|
|
|
|
|
},
|
2026-09-11 22:46:31 +08:00
|
|
|
|
smokeTest: process.env.ELECTRON_SMOKE_TEST === '1',
|
2026-10-02 00:25:10 +08:00
|
|
|
|
// 冒烟凭据:只在冒烟模式下透传,供渲染进程的冒烟钩子构造测试数据用。
|
|
|
|
|
|
// **不走代码字面量**——本文件与渲染产物都会进 app.asar,写死账号等于随安装包把教师口令发出去。
|
|
|
|
|
|
smokeCredentials:
|
|
|
|
|
|
process.env.ELECTRON_SMOKE_TEST === '1'
|
|
|
|
|
|
? {
|
|
|
|
|
|
username: process.env.ELECTRON_SMOKE_USER ?? '',
|
|
|
|
|
|
password: process.env.ELECTRON_SMOKE_PASSWORD ?? '',
|
|
|
|
|
|
classCode: process.env.ELECTRON_SMOKE_CLASS_CODE ?? '',
|
|
|
|
|
|
classPassword: process.env.ELECTRON_SMOKE_CLASS_PASSWORD ?? '',
|
|
|
|
|
|
}
|
|
|
|
|
|
: undefined,
|
2026-10-17 12:55:39 +08:00
|
|
|
|
getUpdateProgress: () => ipcRenderer.invoke('client:get-update-progress'),
|
|
|
|
|
|
onUpdateProgress: (handler: (progress: ContractReleaseProgressDto) => void) => {
|
|
|
|
|
|
const listener = (_event: unknown, value: ContractReleaseProgressDto) => handler(value);
|
|
|
|
|
|
ipcRenderer.on('client:update-progress', listener);
|
|
|
|
|
|
return () => ipcRenderer.removeListener('client:update-progress', listener);
|
|
|
|
|
|
},
|
2026-09-11 22:46:31 +08:00
|
|
|
|
getConfig: (): Promise<DesktopStoredConfig> => ipcRenderer.invoke(IPC_CHANNELS.getConfig),
|
|
|
|
|
|
saveConfig: (patch: Partial<DesktopStoredConfig>): Promise<DesktopStoredConfig> =>
|
|
|
|
|
|
ipcRenderer.invoke(IPC_CHANNELS.saveConfig, patch),
|
|
|
|
|
|
clearConfig: (): Promise<DesktopStoredConfig> => ipcRenderer.invoke(IPC_CHANNELS.clearConfig),
|
|
|
|
|
|
getAppInfo: (): Promise<DesktopAppInfo> => ipcRenderer.invoke(IPC_CHANNELS.appInfo),
|
|
|
|
|
|
openExternal: (url: string): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.openExternal, url),
|
2026-09-12 08:41:08 +08:00
|
|
|
|
|
2026-10-05 16:39:38 +08:00
|
|
|
|
/* 更新检查(主进程直连 GitHub,见 main/update.ts) */
|
|
|
|
|
|
checkForUpdates: (force = false): Promise<UpdateInfo> =>
|
|
|
|
|
|
ipcRenderer.invoke(IPC_CHANNELS.updateCheck, force),
|
|
|
|
|
|
ignoreUpdateVersion: (version: string): Promise<void> =>
|
|
|
|
|
|
ipcRenderer.invoke(IPC_CHANNELS.updateIgnore, version),
|
|
|
|
|
|
onUpdateAvailable: (handler: (info: UpdateInfo) => void): void => {
|
|
|
|
|
|
ipcRenderer.on(UPDATE_AVAILABLE_CHANNEL, (_event, info: UpdateInfo) => handler(info));
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-09-12 08:41:08 +08:00
|
|
|
|
islandPush: (payload: { notification: IslandNotification; context?: IslandPushContext }): void => {
|
|
|
|
|
|
ipcRenderer.send('island:push', payload);
|
|
|
|
|
|
},
|
2026-10-17 03:06:46 +08:00
|
|
|
|
islandRemoveNotification: (id: string): void => {
|
|
|
|
|
|
ipcRenderer.send('island:remove-notification', id);
|
|
|
|
|
|
},
|
2026-09-12 08:41:08 +08:00
|
|
|
|
islandSetClassState: (payload: IslandClassStatePayload): void => {
|
|
|
|
|
|
ipcRenderer.send('island:class-state', payload);
|
|
|
|
|
|
},
|
|
|
|
|
|
islandGetState: () => ipcRenderer.invoke('island:get-state'),
|
2026-09-12 15:19:13 +08:00
|
|
|
|
islandSetAppearance: (appearance): void => {
|
|
|
|
|
|
ipcRenderer.send('island:set-appearance', appearance);
|
|
|
|
|
|
},
|
|
|
|
|
|
islandGetAppearance: () => ipcRenderer.invoke('island:get-appearance'),
|
2026-09-12 13:57:46 +08:00
|
|
|
|
/** 灵动岛点了"标为已读":主进程转交渲染进程同步通知中心 */
|
|
|
|
|
|
onIslandMarkRead: (handler: (id: string) => void): void => {
|
|
|
|
|
|
ipcRenderer.on('island:mark-read', (_event, id: string) => handler(id));
|
|
|
|
|
|
},
|
2026-10-02 00:25:10 +08:00
|
|
|
|
/** 灵动岛点了"标为已读"(多条通知的整批):一次同步这一批 */
|
|
|
|
|
|
onIslandMarkAllRead: (handler: (ids: string[]) => void): void => {
|
|
|
|
|
|
ipcRenderer.on('island:mark-all-read', (_event, ids: string[]) => handler(ids ?? []));
|
|
|
|
|
|
},
|
2026-10-11 11:49:32 +08:00
|
|
|
|
|
|
|
|
|
|
/* 开机自启 / 日志 / 服务端联动 */
|
|
|
|
|
|
getAutoLaunch: (): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.getAutoLaunch),
|
|
|
|
|
|
setAutoLaunch: (enabled: boolean): Promise<boolean> =>
|
|
|
|
|
|
ipcRenderer.invoke(IPC_CHANNELS.setAutoLaunch, enabled === true),
|
|
|
|
|
|
openLogDir: (): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.openLogDir),
|
|
|
|
|
|
openDevTools: (): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.openDevTools),
|
|
|
|
|
|
getClientAgentStatus: (): Promise<ClientAgentStatus> => ipcRenderer.invoke(IPC_CHANNELS.clientStatus),
|
|
|
|
|
|
beatClientNow: (): Promise<ClientAgentStatus> => ipcRenderer.invoke(IPC_CHANNELS.clientBeatNow),
|
2026-09-11 22:46:31 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
contextBridge.exposeInMainWorld('desktop', bridge);
|