# Plaso SDK for Electron

## 目录

<!-- toc -->
- [环境支持](#环境支持)
- [安装](#安装)
- [electron-builder打包特别说明](#electron-builder打包特别说明)
- [使用](#使用)
  - [在主进程中使用](#在主进程中使用)
  - [在渲染进程中使用](#在渲染进程中使用)
    - [打开实时课堂](#打开实时课堂)
    - [打开备课课堂](#打开备课课堂)
    - [打开实时课堂/备课课堂通用参数说明](#打开实时课堂备课课堂通用参数说明)
    - [API参考](#api参考)
- [资料中心](#资料中心)
- [播放历史课堂](#播放历史课堂)
- [注意点](#注意点)
- [Q&A](#qa)
<!-- tocstop -->

## 环境支持

- MacOS：x86-64、arm64
- Windows：ia32、x64
- Electron：14.0.0~22.3.27

## 安装

**安装 @electron/remote**

```shell
npm install @electron/remote --global-style --legacy-peer-deps
```

**安装 plaso-electron-sdk**

```shell
npm install @plasosdk/plaso-electron-sdk --global-style
```

**<font color=red>注意: 不同平台需要单独安装，尤其是MacOS，请分别在Intel芯片和Apple芯片的电脑上安装</font>**

## electron-builder打包特别说明

### 文件拷贝相关配置

> ⚠️ 该配置在升级到 1.3.12 以后有所调整

agora-v4包在plaso-electron-sdk内部作为第三方依赖存在，由于electron-builder拷贝应用代码到app目录时对node_modules文件夹的特殊处理和多级符号链接拷贝上的bug，不能够走[files](https://www.electron.build/configuration#files)配置项去对agora-v4包进行拷贝，而应走[extraResources](https://www.electron.build/configuration#extraresources)配置项去拷贝。

同时，由于这个包是通过extraResources进行拷贝的，需要在签名脚本中额外配置这个包的路径。

<details open>
<summary><strong>版本 >= 1.3.12 的配置</strong></summary>

```json
{
    "files": [
        "!node_modules/@plasosdk/plaso-electron-sdk/lib/agora-v4/*"
    ],
    "extraResources": [
        {
            "from": "node_modules/@plasosdk/plaso-electron-sdk/lib/agora-v4",
            "to": "app/node_modules/@plasosdk/plaso-electron-sdk/lib/agora-v4",
            "filter": [
                "**/*"
            ]
        },
        {
            "from": "node_modules/@plasosdk/plaso-electron-sdk/lib/agora-v4/node_modules",
            "to": "app/node_modules/@plasosdk/plaso-electron-sdk/lib/agora-v4/node_modules",
            "filter": [
                "**/*"
            ]
        }
    ]
}
```
</details>

<details>
<summary><strong>版本 1.3.7 ~ 1.3.11 的配置</strong></summary>
该版本已知问题：给项目安装新依赖时，可能会导致agora-v4包被npm清理掉

```json
{
    "extraResources": [
        {
            "from": "node_modules/@plasosdk/plaso-electron-sdk/node_modules/agora-electron-sdk-v4",
            "to": "app/node_modules/@plasosdk/plaso-electron-sdk/node_modules/agora-electron-sdk-v4",
            "filter": [
                "**/*"
            ]
        },
        {
            "from": "node_modules/@plasosdk/plaso-electron-sdk/node_modules/agora-electron-sdk-v4/node_modules",
            "to": "app/node_modules/@plasosdk/plaso-electron-sdk/node_modules/agora-electron-sdk-v4/node_modules",
            "filter": [
                "**/*"
            ]
        }
    ]
}
```
</details>

<details>
<summary><strong>版本 <= 1.3.6 的配置</strong></summary>
该版本已知问题：给项目安装新依赖时，可能会导致agora-v4包被npm清理掉

```json
{
    "extraResources": [
        {
            "from": "node_modules/agora-electron-sdk-v4",
            "to": "app/node_modules/agora-electron-sdk-v4",
            "filter": ["**/*"]
        },
        {
            "from": "node_modules/agora-electron-sdk-v4/node_modules",
            "to": "app/node_modules/agora-electron-sdk-v4/node_modules",
            "filter": ["**/*"]
        }
    ]
}
```
</details>

### 关闭asar

需要关闭asar，否则SDK会无法加载到node_modules。关闭方式详见electron-buider[官方文档](https://www.electron.build/configuration#asar)。

```json
{
    "asar": false, // 关闭asar
}
```

### MacOS赋予文件执行权限

打MacOS的包时有几个需要额外进行授权的可执行文件。建议通过额外的脚本来执行。

下面的可执行文件是在安装@plasosdk/plaso-electron-sdk时动态下载的，因此授权的脚本需要保证是在文件下载之后。

```shell
// 截图会用到
chmod +x node_modules/@plasosdk/plaso-electron-sdk/lib/flameshot.app

// 桌面共享会用到
chmod +x node_modules/@plasosdk/plaso-electron-sdk/lib/PlasoALD/PlasoALD.scpt
```

### 验证

MacOS打包完毕后，建议安装打好的包，验证上述操作是否成功。核对清单：
- 上课测试截图功能是否正常，截图正常说明授权正确。
- 上课测试桌面共享功能，使用屏幕共享，并在浏览器里面播放一段视频，结束课堂后检查回放中能否听到桌面共享那一段播放的视频的声音，能听到说明授权正确。

## 使用

### 在主进程中使用

需要在主进程加载 `@plasosdk/plaso-electron-sdk` 依赖包

```ts
// electron 版本>=14.0.0 时：需要在主进程里 初始化、启动 remote
const remoteMain = require('@electron/remote/main');
remoteMain.initialize();
remoteMain.enable(mainWindow.webContents); // mainWindow: 主进程通过loadURL加载的那个渲染进程窗口

// plaso-electron-sdk中依赖@electron/remote，需要通过 initRemoteMain方法 传入 remoteMain
const { initRemoteMain } = require('@plasosdk/plaso-electron-sdk');
initRemoteMain(remoteMain);
```

### 在渲染进程中使用

<a id="open-sdk-window-params"></a>

**打开实时课堂/备课课堂方法入参类型定义**
```ts
interface CreateClassWindowPamras {
    /**
     * 属性说明详见下方classOptions，打开实时课堂时为ILiveClassOptions，备课课堂为IPrepareClassOptions
     */
    classOptions: ILiveClassOptions | IPrepareClassOptions;
    electronWinOptions?: Electron.BrowserWindowConstructorOptions;
    onClassWindowReadyFn?: (winId: number) => void
    onClassWindowLeaveFn?: (winId: number) => void
    onClassFinishedFn?: (meetingId: string) => void;
    onSaveBoardFn?: (
        params: {
            fileInfo: FileParams[];
            fileName?: string;
        },
        callback: (result: boolean) => void,
    ) => void;
    onOpenResourceCenterFn?: () => void;
    onGetExtFileNameFn?: (info: any[], ...args: any[]) => Promise<string>;
    onGetPreParseFileNameFn?: (info: any[], option: { suffix: string }) => Promise<string>;
    onReportIssuesFn?: (Issues: any) => void;
}
```

#### 打开实时课堂

SDK打开一个新的`BrowserWindow`来加载实时课堂UI界面，示例代码如下：

```ts
const PlasoElectronSdk = window.require('@plasosdk/plaso-electron-sdk');
const createLiveClassWindowParams: CreateClassWindowPamras = { classOptions: { query } };
PlasoElectronSdk.createLiveClassWindow(createLiveClassWindowParams);
```

##### createLiveClassWindow参数说明

###### classOptions

```ts
interface ILiveClassOptions {
    /** 进入课堂必须的签名字符串 */
    query: string;
    /** 当前进入课堂用户的头像，取值为https全地址 */
    displayAvatarUrl?: string;
    /** 课堂中的成员，会在成员列表中呈现 */
    classMembers?: UserInfo[];
    /** 是否启用降噪 */
    enableENC?: boolean;
    /** 是否启用3A */
    enableRTC3A?: boolean;
    /** 是否启用新桌面共享 */
    enableLiveNewShare?: boolean;
    /** 是否启用触屏设备上的新桌面共享 */
    enableLiveNewShareInTouch?: boolean;
    /** 是否启用部分屏幕区域共享 */
    enableLiveNewShareRegion?: boolean;
    /** 是否启用签到 */
    enableLiveSign?: boolean;
    /** 是否启用资料中心（云盘） */
    supportShowResourceCenter?: boolean;
    /** 是否支持保存板书 */
    supportSaveBoard?: boolean;
    /** 是否启用摄像头常驻 */
    residentCamera?: boolean;
    /** 是否启用日志上传。默认启用，进入和退出课堂时自动将日志上传至伯索服务器。 */
    enableUploadLog?: boolean;
}

interface UserInfo {
    /** 用户名，唯一标识 */
    loginName: string;
    /** 用户昵称 */
    name: string;
    /** 用户角色 */
    upimeRole: 'speaker' | 'assistant' | 'listener';
    /** 用户头像 */
    displayAvatarUrl?: string;
}
```

| <span style="white-space: nowrap;">参数名称</span> | <span style="white-space: nowrap;">是否必填</span> | <span style="white-space: nowrap;">类型</span> | <span style="white-space: nowrap;">参数描述</span> |
| --- | --- | --- | --- |
| query | 是 | string | 带签名的字符串，具体拼接逻辑见下文[query属性说明](#query) |
| displayAvatarUrl | 否 | string | 用户头像地址。不传默认使用用户昵称作为头像。 |
| classMembers | 否 | UserInfo[] | 成员列表中显示的人员，最多支持 2000 人。**人员超过2000人需要联系伯索平台进行额外申请**。 |
| enableRTC3A | 否 | boolean | 是否启用3A：回音消除、噪声抑制、自动增益控制。默认启用。 |
| enableENC | 否 | boolean | 是否启用降噪控制，开启后设置界面可以设置`基础降噪`或`增强降噪`。默认启用。 |
| enableLiveNewShare | 否 | boolean | 是否启用新桌面共享。启用后共享`屏幕`/`部分屏幕区域`时会对课堂窗口做透明化处理，需要选中工具栏上的`交互模式`工具来交互桌面应用，可能会触发`Electron`长时间透传鼠标事件导致的透传异常问题，谨慎使用。默认不启用。 |
| enableLiveNewShareInTouch | 否 | boolean | 是否在触屏设备上启用新桌面共享，启用后通过切换`演示模式`和`互动模式`来交互桌面应用和课堂中的工具。默认不启用。 |
| enableLiveNewShareRegion | 否 | boolean | 是否启用`部分屏幕区域`共享。 |
| enableLiveSign | 否 | boolean | 是否启用签到，启用后工具箱中显示`签到`按钮。默认不启用。 |
| supportShowResourceCenter | 否 | boolean | 是否启用`资料中心(云盘)`，启用后在工具栏显示`资料中心(云盘)`按钮，此按钮需要配合`onOpenResourceCenterFn`回调一起使用。默认不启用。 |
| supportSaveBoard | 否 | boolean | 是否启用保存板书，启用后工具箱中显示`保存当页板书`按钮，需要配合云盘使用。默认不启用。 |
| residentCamera | 否 | boolean | 是否启用常驻摄像头：只对学生或游客生效。启用后学生进入课堂后摄像头常驻开启，没有开启摄像头权限时也会开启。默认不启用。 |
| enableUploadLog | 否 | boolean | 是否启用日志自动上传。默认启用，退出课堂时 SDK 会自动将日志文件压缩并上传至伯索日志服务器，便于问题排查。传入 `false` 可关闭。 |

<a id="query"></a>
**query属性说明**

query本质上是根据`IQueryParams`对象中的字段生成带签名的字符串，query示例如下：

```ts
const query = 'appId=plaso&appType=liveclassSDK&d_dimension=1280x720&enableNewClassExam=1&loginName=t_1&mediaType=video&meetingId=test_1742442362&meetingType=public&signature=A226198904A392579B98987FB4CD5478AB3F5587&userName=%E8%80%81%E5%B8%881&userType=speaker&validBegin=1742442364&validTime=99999'
```

```ts
interface IQueryParams {
    appId: string;
    validBegin: number;
    validTime: number;
    mediaType: string;
    meetingType: string;
    meetingId: string;
    userType: string;
    loginName: string;
    userName: string;
    d_dimension: string;

    topic?: string;
    endTime?: number;
    onlineMode?: number;
    d_delayEndTimes?: number;
    d_delayEnd?: number;
    d_enableAvatarFreeScale?: number;
    d_enableObjectEraser?: number;
    d_vote?: number;
    d_sharpness?: number;
    isNewMT?: number;
    enableNewClassExam?: number;
    d_enableReRecording?: number;
    d_restrictAssistantPerm?: number;
}
```

| <span style="white-space: nowrap;">字段名称</span> | <span style="white-space: nowrap;">是否必填</span> | <span style="white-space: nowrap;">类型</span> | <span style="white-space: nowrap;">字段描述</span> |
| --- | --- | --- |--- |
| appId | 是 | string | 在申请接入时，伯索平台给予的 appId |
| validBegin | 是 | number  | 签名query生效的起始时间，Unix Epoch 时间戳，单位为秒 |
| validTime | 是 | number | 签名query的有效期，从`validBegin`开始计算，单位为秒 |
| mediaType | 是 | string  | 媒体类型，取值：<ul><li>audio: 音频课堂</li><li>video: 视频课堂</li></ul>
| meetingType | 是 | string | 课堂类型，固定值为`public` |
| meetingId | 是 | string  | 课堂ID，唯一标识该课堂；使用ASSIIC字符，不得包含/，\,空格等；长度在40字节以内的字符串。 |
| userType | 是 | string | 用户角色类型，取值：<ul><li>speaker: 课堂的主讲者，有控制其他listener是否可板书/发言的权限。课堂中只能有一个主讲</li><li>assistant：助教，辅助主讲授课的角色，在课堂中的权限与主讲基本一致。课堂中可以有多个助教</li><li>listener：听众，可以理解为学生</li></ul> |
| loginName | 是 | string | 唯一标识该用户的id，不能为空，相同的loginName进入课堂时，后面进入的会使前面进入的登出 |
| userName | 是 | string | 用户昵称，头像缺省时会显示 |
| d_dimension | 是 | string | 固定值为`1280x720`，定义界面尺寸为16:9界面 |
| topic | 否 | string | 课堂名称，在标题栏上显示 |
| endTime | 否 | number | 课堂结束时间，格式为 Unix Epoch 时间戳，单位为秒 |
| onlineMode | 否 | number | 当mediaType为`video`时生效，表示最大能开启的`listener`的摄像头的个数，取值：<ul><li>1</li><li>6</li><li>12</li></ul>默认值为6 |
| d_delayEndTimes | 否 | number | 单节课最大延时下课次数，取值：<ul><li>1</li><li>2</li><li>3</li><li>4</li></ul>默认没有延时 |
| d_delayEnd | 否 | number | 单次延时时间，单位为秒，取值：<ul><li>5 * 60</li><li>10 * 60</li><li>20 * 60</li><li>30 * 60</li></ul>默认20分钟 |
| d_enableAvatarFreeScale | 否 | number | 是否开启头像任意比例缩放，仅上课中各端同步，录制头像时历史课堂不支持课堂调整的任意比例，取值：<ul><li>0：关闭</li><li>1：开启</li></ul>默认关闭 |
| d_enableObjectEraser | 否 | number | 是否启用新版板书，新版板书的橡皮擦支持对象擦除，传入大于0的值启用新版板书。取值：<ul><li>0: 不启用，橡皮功能为点擦</li><li>1: 支持对象擦除手写</li><li>3: 支持对象擦除手写+文本</li><li>5: 支持对象擦除手写+图形</li><li>7: 支持对象擦除手写+文本+图形</li></ul>默认不启用 |
| d_vote | 否 | number | 是否启用投票工具。取值：<ul><li>0：关闭</li><li>1：开启</li></ul>默认关闭 |
| d_sharpness | 否 | number | 当mediaType为`video`时生效，表示摄像头画面的清晰度。取值：<ul><li>10: 360p</li><li>20: 720p</li><li>30: 1080p</li></ul>默认值为10 |
| isNewMT | 否 | number | 是否支持移动授课模式，建议传1 |
| recordAvator | 否 | string | 传入老师或助教的`loginName`表示录制对应人的头像，传入`recordScreen`时SDK会忽略此参数 |
| recordScreen | 否 | string | 传入`screen`表示录制屏幕（当前仅支持录制老师屏幕）|
| enableNewClassExam | 否 | number | 是否启用新版随堂测，取值：<ul><li>0: 不启用</li><li>1: 启用选择题</li><li>2: 启用填空题</li><li>3: 启用选择题+填空题</li></ul>默认不启用，建议传3 |
| d_enableReRecording | 否 | number | 是否允许老师/助教重新录制，取值：<ul><li>0: 不允许老师和助教重新录制</li><li>1: 仅允许老师重新录制</li><li>2: 仅允许助教重新录制</li><li>3: 允许老师和助教重新录制</li></ul>默认值为3 |
| d_restrictAssistantPerm | 否 | number | 是否启用限制助教权限，取值：<ul><li>0: 不启用</li><li>1: 启用</li></ul>默认不启用 |

**根据 queryParams 对象生成签名字符串**
> 注意：为了安全和各端签名统一，建议将签名的计算放在服务端，前端通过接口获取带签名的query

将queryParams传入签名函数生成签名，签名示例参考：[签名示例](https://open.plaso.cn/doc-6285173?nav=01HEQ5Y5RXKMCPBPF6S8T3VK56)

获取`signature`后将`signature`加到`queryParams`中作为一个字段。

```ts
queryParams.signature = signature;
```

获取完整的`queryParams`后，遍历`queryParams`生成`query`字符串，每个字段的值用`encodeURIComponent`编码。完整流程示例如下：

```ts
function genSignature(params) {
    return 'xxx';
}

function genQuery(params) {
    const keys = Object.keys(params).sort();
    const res = [];
    for (const key of keys) {
        res.push(key + '=' + encodeURIComponent(params[key]));
    }
    return res.join('&');
}

const queryParams = {
    appId: 'xxx';
    validBegin: 175645206;
    validTime: 3600;
    mediaType: 'video';
    meetingType: 'public';
    meetingId: 1234;
    userType: 'speaker';
    loginName: 'hello';
    userName: 'world';
    d_dimension: '1280x720';
};

const signature = genSignature(queryParams);
queryParams.signature = signature;

const query = genQuery(queryParams);
```

#### 打开备课课堂

SDK打开一个新的`BrowserWindow`来加载备课课堂UI界面，示例代码如下：

```ts
const PlasoElectronSdk = window.require('@plasosdk/plaso-electron-sdk');
const createPrepareClassWindowParams: CreateClassWindowPamras = { classOptions: { loginName: 'hello', userName: 'world' } };
PlasoElectronSdk.createPrepareClassWindow(createPrepareClassWindowParams);
```

##### createPrepareClassWindow参数说明

###### classOptions

```ts
interface IPrepareClassOptions {
    loginName: string;
    userName: string;

    displayAvatarUrl?: string;
    topic?: string;
    d_enableObjectEraser?: number;
}
```

| <span style="white-space: nowrap;">参数名称</span> | <span style="white-space: nowrap;">是否必填</span> | <span style="white-space: nowrap;">类型</span> | <span style="white-space: nowrap;">参数描述</span> |
| --- | --- | --- | --- |
| loginName | 是 | string | 唯一标识该用户的id，不能为空，相同的loginName进入课堂时，后面进入的会使前面进入的登出 |
| userName | 是 | string | 用户昵称，头像缺省时会显示 |
| displayAvatarUrl | 否 | string | 用户头像地址。不传默认使用用户昵称作为头像。 |
| d_enableObjectEraser | 否 | number | 是否启用新版板书，新版板书的橡皮擦支持对象擦除，传入大于0的值启用新版板书。取值：<ul><li>0: 不启用，橡皮功能为点擦</li><li>1: 支持对象擦除手写</li><li>3: 支持对象擦除手写+文本</li><li>5: 支持对象擦除手写+图形</li><li>7: 支持对象擦除手写+文本+图形</li></ul>默认不启用 |

#### 打开实时课堂/备课课堂通用参数说明

##### electronWinOptions 

> **即将弃用**: 不推荐传入，SDK内部默认设置了一些窗口参数，为了保证最佳体验，不要传入此参数。

Electron的窗口参数，详情参考 [Electron官方文档](https://www.electronjs.org/zh/docs/latest/api/browser-window#new-browserwindowoptions)。

```ts
type electronWinOptions = Electron.BrowserWindowConstructiorOptions;
```

##### onClassWindowReadyFn

```ts
// 课堂窗口打开渲染成功后的回调，回调参数为 窗口id
type onClassWindowReadyFn = (winId: number) => void;
```

##### onClassWindowLeaveFn

```ts
// 课堂窗口关闭后的回调，回调参数为 窗口id
type onClassWindowLeaveFn = (winId: number) => void;
```

##### onClassFinishedFn

```ts
// 课堂结束后的回调，回调参数为 课堂的meetingId
type onClassFinishedFn = (meetingId: string) => void;
```

##### onSaveBoardFn

**注意：**

1、**filePath 对应的文件资源，用户保存在自己的云端时，需要把 本次保存的备课文件的 相关资源放在同一特定目录下**

2、**每次保存生成的备课文件 都放在一个新的目录下，不同的备课文件不能共用一个目录**

3、**备课保存的资源文件名 不能更改，info.pb 是固定的文件名**

```ts
// 保存板书方法，具体的保存逻辑由外部实现，取消保存板书时，callback传false, 不然传true

type FileParams = {
    /** 备课相关资源文件的本地地址*/
    filePath: string[];
    /** 备课文件 类型*/
    fileType: 'png' | 'pb';
};

type onSaveBoardFn = (
    params: {
        fileInfo: FileParams[];
        fileName?: string;
    },
    callback: (result: boolean) => void,
) => void;
```

##### onOpenResourceCenterFn
详见[云盘接入](https://open.plaso.cn/doc-6285183#showresourcecenter)
```ts
// 通知外部用户打开自己的资料中心，资料中心的具体ui和逻辑由外部用户自己实现
// 推荐：通过onClassWindowReadyFn回调的winId拿到课堂窗口实例，用户的云盘通过一个BrowserWindow
// 和课堂窗口组成父子窗口，云盘是子窗口，并且云盘窗口打开时保持置顶，这样来保证云盘打开时始终可见并且跟随课堂窗口。
type onOpenResourceCenterFn = () => void;
```

##### onGetExtFileNameFn
详见[云盘接入](https://open.plaso.cn/doc-6285183#getextfilename)

```ts
type onGetExtFileNameFn = (info: any, ...args: any[]) => Promise<string>;
```

##### onGetPreParseFileNameFn
详见[云盘接入](https://open.plaso.cn/doc-6285183#getpreparsefilename)
```ts
type onGetPreParseFileNameFn = (info: any, option: { suffix: string }) => Promise<string>;
```

##### onReportIssuesFn
传入该函数后，设置面板将会显示“异常问题上报”入口。用户输入异常问题信息后，在点击上报按钮时触发该回调，返回异常信息。
```ts
interface Issues {
    /** 上报时间 */
    time: string;
    /** 用户 loginName */
    id: string;
    /** 用户 name */
    name: string;
    /** 异常信息 */
    bugDescription: string;
}

type onReportIssuesFn = (info: any) => void;
```

#### API参考

##### PlasoElectronSdk.initLogConfig

该方法用于设置SDK日志位置，SDK崩溃时会在同级目录下生成`reports`文件夹存储 dump，在调用`createLiveClassWindow`前设置，不调用SDK会在默认位置写入日志，默认位置如下：

```js
// 获取日志路径的代码如下
require('path').join(require('electron').app.getPath('userData'), 'P403FileTemp');

// Windows：C:\Users\${userName}\AppData\Roaming\${appName}\P403FileTemp
// Mac：/Users/${userName}/Application\ Support/${appName}/P403FileTemp
```

参考示例：

```ts
// 代码示例
const PlasoElectronSdk = window.require('@plasosdk/plaso-electron-sdk');
const logDir = 'C:/Users/userName/Desktop/electronDemo/electron12.0.18_x32/resources/app';
PlasoElectronSdk.initLogConfig(logDir);
```

> 重要：SDK 默认会在退出课堂后自动上传日志并清理过期文件（7天前），用户无需自行实现上传逻辑。如需关闭自动上传（设置 `enableUploadLog: false`），则需要对日志目录做定期清理和及时上传日志到自己的服务器或OSS。推荐在退出SDK后做一次清理和上传，建议清理创建时间超过7天的日志即可。
相关代码示例如下：
```ts
import fs from 'fs';
import path from 'path';

/**
 * 清理创建时间超过7天的日志
 * @param logDir 日志目录
 */
function clearExpireLogs(logDir: string) {
    try {
        const logFile = fs.readdirSync(logDir).filter((fileName) => /\.(log|dmp|ips|diag)/.test(path.extname(fileName)));
        logFile.forEach((fileName) => {
            const fileInfo = fs.statSync(path.join(logDir, fileName));
            const fileDate = new Date(fileInfo.birthtime).setHours(0, 0, 0, 0).valueOf();
            const currentDate = new Date().setHours(0, 0, 0, 0);
            if (currentDate - fileDate > 7 * 24 * 60 * 60 * 1000) {
                fs.unlinkSync(path.join(logDir, fileName));
            }
        });
    } catch (e) {
        console.error('clear expire log error', e);
    }
}

/**
 * 收集崩溃日志
 * @param logDir 日志目录
 * @param pendingUploadDir 待上传目录
 */
function collectCrashLogs(logDir: string, pendingUploadDir: string) {
    const platform = require('os').platform();
    switch (platform) {
        case 'darwin':
            const newDumpFolder = path.join(logDir, '/new/');
            const pendingDumpFolder = path.join(logDir, '/pending/');
            const completedDumpFolder = path.join(logDir, '/completed/');
            const diagnosticReports = path.normalize('/Library/Logs/DiagnosticReports');
            collectDumpFiles(newDumpFolder);
            collectDumpFiles(pendingDumpFolder);
            collectDumpFiles(completedDumpFolder);
            collectSystemLogs(diagnosticReports);
            break;
        case 'win32':
            const reportsDumpFolder = path.join(logDir, '/reports/');
            collectDumpFiles(reportsDumpFolder);
            break;
        default:
            break;
    }

    function collectSystemLogs(folder: string) {
        try {
            const files = fs.readdirSync(folder);
            const todayDate = new Date();
            const formatDate = `${todayDate.getFullYear()}-${(todayDate.getMonth() + 1).toString().padStart(2, '0')}-${todayDate
                .getDate()
                .toString()
                .padStart(2, '0')}`;
            files.forEach((name) => {
                const reg = new RegExp(`Electron Helper.*${formatDate}`);
                if (name && name.match(reg)) {
                    fs.copyFileSync(path.join(folder, name), path.join(pendingUploadDir, name));
                }
            });
        } catch (e) {
            console.error(e);
        }
    }

    function collectDumpFiles(folder: string) {
        try {
            const files = fs.readdirSync(folder);
            files.forEach((name) => {
                if (name) {
                    fs.copyFileSync(path.join(folder, name), path.join(pendingUploadDir, name));
                    fs.unlinkSync(path.join(folder, name));
                }
            });
        } catch (e) {
            console.error(e);
        }
    }
}

/**
 * 收集业务日志
 * @param logDir 日志目录
 * @param pendingUploadDir 待上传目录
 */
function collectBussinessLogs(logDir: string, pendingUploadDir: string) {
    const logFiles = fs.readdirSync(logDir).filter((fileName) => /\.(log|dmp|ips|diag)/.test(path.extname(fileName)));
    logFiles.forEach((fileName) => {
        fs.copyFileSync(path.join(logDir, fileName), path.join(pendingUploadDir, fileName));
    });
}

/**
 * 收集日志
 * @param logDir 日志目录
 * @param pendingUploadDir 待上传目录
 */
function collectLogs(logDir: string, pendingUploadDir: string) {
    collectCrashLogs(logDir, pendingUploadDir);
    collectBussinessLogs(logDir, pendingUploadDir);
}

/**
 * 压缩日志
 * @param pendingUploadDir 待上传目录
 */
function zipLogs(pendingUploadDir: string): Promise<string> {
    return new Promise((resolve) => {
        const zipLogPath = path.join(pendingUploadDir, 'xxx.zip'); // 这边的xxx.zip取名时建议将用户名以及上传时间拼接上去，方便查询
        // TODO: 这边做压缩，压缩完毕后将zip文件路径返回
        resolve(zipLogPath);
    });
}

/**
 * 上传日志到服务器
 * @param filePath zip文件路径
 */
function uploadToServer(filePath: string): Promise<boolean> {
    return new Promise((resolve) => {
        // TODO: 这边做上传
        resolve(true);
    });
}

/**
 * 上传日志
 * @param logDir 日志目录
 */
export function uploadLogs(logDir: string): Promise<boolean> {
    return new Promise(async (resolve) => {
        try {
            // 上传前先清理过期的日志，避免垃圾日志太多
            clearExpireLogs(logDir);

            // 创建待上传目录
            const pendingUploadDir = path.join(logDir, 'pendingUpload');
            if (fs.existsSync(pendingUploadDir)) {
                fs.rmdirSync(pendingUploadDir, { recursive: true });
            }
            fs.mkdirSync(pendingUploadDir);

            // 将日志复制到待上传目录
            collectLogs(logDir, pendingUploadDir);

            // 压缩待上传目录
            const zipName = await zipLogs(pendingUploadDir);

            // 将压缩后的日志上传到服务器
            await uploadToServer(zipName);

            // 上传完成后删除临时文件
            fs.unlinkSync(zipName);
            fs.rmdirSync(pendingUploadDir, { recursive: true });

            resolve(true);
        } catch (error) {
            resolve(false);
        }
    });
}
```

##### PlasoElectronSdk.getVersion

返回包的版本，格式：x.x.x

```ts
type getVersion = () => string;
```

##### PlasoElectronSdk.createLiveClassWindow

**创建实时课堂窗口**

```ts
function createLiveClassWindow(params: CreateClassWindowPamras): void;
```
参数详见[打开实时课堂/备课课堂方法入参类型定义](#open-sdk-window-params)

##### PlasoElectronSdk.createPrepareClassWindow

**创建备课课堂窗口**
```ts
function createLiveClassWindow(params: CreateClassWindowPamras): void;
```
参数详见[打开实时课堂/备课课堂方法入参类型定义](#open-sdk-window-params)

<a id="insert-object"></a>

##### PlasoElectronSdk.insertObject

用户从自己的资料中心往**实时课堂/备课课堂**插入WORD/EXCEL/PPT/PDF、音视频、备课文件等，详见[云盘接入](https://open.plaso.cn/doc-6285183#insertobject)

##### PlasoElectronSdk.updateProhibitedWords

更新严禁词

- 设置严禁词后，消息发送时会受到严禁词列表的限制。
- 严禁词功能只对upimeRole为listener的角色有效。
- 该函数需在课堂准备好后调用（onClassWindowReadyFn回调触发后，即代表课堂准备好）

```ts
type updateProhibitedWords = (words: string[]) => void;
```

## 资料中心

- 进课堂时，对象 `classOptions.supportShowResourceCenter` 需要是 true
- 资料中心的接入还涉及 `onOpenResourceCenterFn`、`PlasoElectronSdk.insertObject`、`onGetExtFileNameFn`、`onGetPreParseFileNameFn` 几个函数，具体用法详见[云盘接入](https://open.plaso.cn/doc-6285183)的 `showResourceCenter`、`insertObject`、`getExtFileName`、`getPreParseFileName` 章节

## 播放历史课堂

1、参考文档： **[播放器SDK-Web播放器](https://open.plaso.cn/doc-6285192)**

2、当课堂中insertObject使用了`info`属性时，SDK内部需要外部传入`getExtFileName`，因此历史课堂需要使用jssdk的接入方式， 详见：**[Web播放器-jssdk接入](https://open.plaso.cn/doc-6285192#jssdk%E6%8E%A5%E5%85%A5)**

3、当课堂中insertObject插入了预解析资源时，SDK内部需要外部传入`getPreParseFileName`，因此历史课堂需要使用jssdk的接入方式，同上。

## 注意点

（1）用户的课堂外主窗口销毁时需要销毁课堂窗口

（2）**基于 此包封装新包时**：注意 @electron/remote 这个包的位置需要 和新包处于同级目录，需要把 和该包同级的@electron/remote 移到新包的同级目录处

（3）确保 仅最后的 node_moudles 的顶层有 @electron/remote

## Q&A

### 进课堂遇到闪退/崩溃怎么办？
- 检查应用程序路径中是否存在中文，路径中不应出现中文</br>
可通过在主进程中，用`console.log(require.resolve('@plasosdk/plaso-electron-sdk'))`来检查路径
- 如果是Mac，进课堂会需要麦克风权限。打开系统设置->隐私与安全性->麦克风，查看列表里你的应用程序是否被授权。
