# CYSOEditor 扩展开发教程

> **版本**: v1.0.0\
> **难度**: 🟢 初级 → 🔴 高级\
> **预计学习时间**: 30 分钟 - 2 小时\
> **更新日期**: 2026-05-30

***

## 📚 目录

1. [什么是扩展？](#什么是扩展)
2. [5分钟创建你的第一个扩展](#5分钟创建你的第一个扩展)
3. [理解扩展的基本结构](#理解扩展的基本结构)
4. [添加不同类型的积木](#添加不同类型的积木)
5. [使用参数和菜单](#使用参数和菜单)
6. [使用 CYSOCore 高级功能](#使用-cysocore-高级功能)
7. [权限系统完全指南](#权限系统完全指南)
8. [实战项目：系统信息查看器](#实战项目系统信息查看器)
9. [调试技巧](#调试技巧)
10. [打包与分享](#打包与分享)
11. [常见问题 FAQ](#常见问题-faq)
12. [进阶资源](#进阶资源)

***

## 什么是扩展？

### 简单来说

**扩展 = 一堆"积木"的集合**

每个积木都能做一件事，比如：

- 📖 读取文件内容
- ✏️ 写入数据到文件
- 💻 执行系统命令
- 🖥️ 显示屏幕覆盖窗口
- ⌨️ 注册全局快捷键
- 📊 获取 CPU/内存使用率

### 为什么需要扩展？

CYSOEditor 基于 Scratch，默认功能有限。通过扩展，你可以：

- ✅ 访问计算机文件系统
- ✅ 控制硬件设备
- ✅ 与操作系统交互
- ✅ 创建自定义功能

### 扩展 vs 插件 (Addon)

| 类型                 | 作用范围       | 能做什么            |
| ------------------ | ---------- | --------------- |
| **插件 (Addon)**     | 编辑器界面      | 改变编辑器外观、添加按钮等   |
| **扩展 (Extension)** | Scratch 项目 | 添加新的积木块、访问系统API |

**本教程教你开发的是：扩展 (Extension)**

***

## 5分钟创建你的第一个扩展

让我们用最简单的例子开始！

### 步骤 1: 创建文件

在任意位置创建一个新文件，命名为 `my-first-extension.js`

### 步骤 2: 编写代码

将以下代码复制到文件中：

```javascript
class MyFirstExtension {
    getInfo() {
        return {
            id: 'myFirstExtension',       // 扩展的唯一 ID
            name: '我的第一个扩展',          // 在积木面板显示的名称
            color1: '#4CAF50',             // 积木主颜色（深绿）
            color2: '#388E3C',             // 积木次颜色（中绿）
            color3: '#2E7D32',             // 积木第三色（浅绿）
            
            blocks: [
                {
                    opcode: 'sayHello',           // 积木的唯一标识符
                    blockType: Scratch.BlockType.COMMAND,  // 类型：命令积木
                    text: '说你好'                // 积木上显示的文字
                }
            ]
        };
    }

    // 当积木被点击时执行的方法
    sayHello() {
        console.log('🎉 你好，世界！');
    }
}

// 注册扩展（必须！）
if (typeof Scratch !== 'undefined') {
    Scratch.extensions.register(new MyFirstExtension());
}
```

### 步骤 3: 加载扩展

1. 打开 **CYSOEditor 桌面版**
2. 点击左下角的 **"扩展"** 按钮
3. 点击 **"添加扩展"**
4. 选择你刚才创建的 `my-first-extension.js` 文件

### 步骤 4: 测试

你应该能在扩展列表看到 **"我的第一个扩展"** ！

- 把它拖到脚本区
- 点击运行
- 按 `F12` 打开控制台，应该能看到 `🎉 你好，世界！`

🎉 **恭喜！你已经创建了第一个扩展！**

***

## 理解扩展的基本结构

每个扩展都是一个 JavaScript 类，必须包含以下部分：

### 核心结构模板

```javascript
class MyExtension {
    // ① 构造函数（可选但推荐）
    constructor() {
        this.isDesktop = typeof EditorPreload !== 'undefined';
    }

    // ② 必须实现！返回扩展的信息
    getInfo() {
        return {
            // 基本信息
            id: 'unique-id',
            name: '扩展名称',
            
            // 外观设置
            color1: '#FF5733',   // 主色（积木边缘）
            color2: '#E64A19',   // 次色（积木主体）
            color3: '#CC4400',   // 第三色（阴影等）
            iconURI: '...',      // 图标（可选，Base64编码的SVG）
            
            // 权限声明（如果使用高级功能）
            permissions: [],
            
            // 积木定义
            blocks: [...],
            
            // 菜单定义（如果使用下拉菜单）
            menus: {...}
        };
    }

    // ③ 积木方法（名称必须与 opcode 对应）
    myBlockMethod(args) {
        // args 是包含所有参数的对象
        return '返回值';  // 报告器积木需要返回值
    }
}

// ④ 必须注册！
if (typeof Scratch !== 'undefined') {
    Scratch.extensions.register(new MyExtension());
}
```

### 关键点说明

#### ① `id` - 扩展标识符

- **唯一性**: 不能与其他扩展重复
- **命名规则**: 使用小写字母、数字
- **示例**: `'filemanager'`, `'systemmonitor'`, `'gamecontrols'`

#### ② `color1/2/3` - 颜色方案

- **color1**: 最深的颜色（用于边框）
- **color2**: 中间色（用于积木主体）
- **color3**: 最浅的颜色（用于阴影和高亮）

**常用配色方案**:

| 风格     | color1    | color2    | color3    |
| ------ | --------- | --------- | --------- |
| 🟢 绿色系 | `#4CAF50` | `#388E3C` | `#2E7D32` |
| 🔵 蓝色系 | `#2196F3` | `#1976D2` | `#0D47A1` |
| 🔴 红色系 | `#F44336` | `#D32F2F` | `#B71C1C` |
| 🟡 黄色系 | `#FFC107` | `#FFA000` | `#FF6F00` |
| 🟣 紫色系 | `#9C27B0` | `#7B1FA2` | `#4A148C` |
| 🟠 橙色系 | `#FF9800` | `#F57C00` | `#E65100` |

#### ③ `opcode` vs 方法名

- **`opcode`**: 积木的内部标识符（用于 getInfo 中定义）
- **方法名**: 实际执行的函数名（两者通常相同，但不强制）

```javascript
// getInfo 中定义
{ opcode: 'calculateSum', ... }

// 实现方法（可以同名）
calculateSum(args) { ... }

// 或者不同名也行（不推荐）
// { opcode: 'calcSum', ... }
// calculateSum(args) { ... }  // Scratch 会自动映射
```

***

## 添加不同类型的积木

Scratch 支持多种积木类型，扩展开发中最常用的有 **4 种**（命令、报告器、布尔、帽子）：

### 1. 命令积木 (COMMAND)

**用途**: 执行操作，不返回值

```javascript
{
    opcode: 'moveSprite',
    blockType: Scratch.BlockType.COMMAND,
    text: '移动角色 [STEPS] 步'
}
```

**特点**:

- 形状：圆角矩形，像拼图块
- 使用方式：放在脚本底部，点击执行
- 返回值：无（或返回 undefined）

**示例**:

```javascript
async moveSprite(args) {
    const steps = args.STEPS;
    console.log(`移动 ${steps} 步`);
    // 这里可以调用其他 API...
}
```

***

### 2. 报告器积木 (REPORTER)

**用途**: 计算并返回一个值

```javascript
{
    opcode: 'addNumbers',
    blockType: Scratch.BlockType.REPORTER,
    text: '[A] + [B]'
}
```

**特点**:

- 形状：长圆角矩形
- 返回值：数字、字符串、布尔值
- 可以嵌套在其他积木中使用

**示例**:

```javascript
addNumbers(args) {
    const a = Number(args.A) || 0;
    const b = Number(args.B) || 0;
    return a + b;  // 必须返回值！
}
```

***

### 3. 布尔积木 (BOOLEAN)

**用途**: 返回 true 或 false

```javascript
{
    opcode: 'isGreaterThan',
    blockType: Scratch.BlockType.BOOLEAN,
    text: '[A] > [B] ?'
}
```

**特点**:

- 形状：六边形
- 返回值：必须是 `true` 或 `false`
- 用于条件判断（如果...那么）

**示例**:

```javascript
isGreaterThan(args) {
    const a = Number(args.A) || 0;
    const b = Number(args.B) || 0;
    return a > b;  // 返回布尔值
}
```

***

### 4. 帽子积木 (HAT)

**用途**: 作为脚本的起点，在“某个事件发生时”启动其下方的积木

```javascript
{
    opcode: 'whenKeyPressed',
    blockType: Scratch.BlockType.HAT,
    isEdgeActivated: false,            // ✅ 推荐显式写成 false：让编译器把它当“谓词帽子”（HAT_PREDICATE），函数返回 true 时帽子体执行一次。
                                       //    不写时在本运行时里也是谓词帽子（edgeActivated 取 undefined 视为 falsy），但显式写明最清晰稳妥。
                                       //    ⚠️ 无论哪种路径，帽子函数都必须返回 true 才会触发，返回 undefined/false 都会“永不触发”。
    shouldRestartExistingThreads: true,
    text: '当按下按键 [KEY]'
}
```

**特点**:

- 形状：圆弧顶（像帽子）
- 放在脚本最顶端，作为起点
- 用于事件监听（如全局快捷键触发）

> ⚠️ 自定义扩展帽子**不会“自动触发”**。必须由你的扩展代码在事件发生时，调用
> `runtime.startHats('<扩展ID>_<opcode>', {})` 显式启动它。而且运行时里帽子的 opcode 是
> **带扩展 ID 前缀**的（如扩展 `id` 为 `myExt`、积木 `opcode` 为 `whenKeyPressed`，则真实 opcode 是
> `myExt_whenKeyPressed`），传裸 `opcode` 会匹配不到任何帽子、安静失败。

**示例（配合全局快捷键触发帽子）**:

```javascript
// 1) 在 getInfo() 的 blocks 中定义帽子（注意 isEdgeActivated: false）
//    { opcode: 'whenShortcut', blockType: Scratch.BlockType.HAT,
//      isEdgeActivated: false, shouldRestartExistingThreads: true,
//      text: '当快捷键 [EVENT] 触发', arguments: { EVENT: { type: STRING, defaultValue: 'my-event' } } }

// 2) 帽子函数：VM 把它当“谓词”求值，返回 true 时帽子体执行一次
whenShortcut(args) {
    const event = ((args && args.EVENT) || '').toUpperCase();
    if (event && this._pending && this._pending[event]) {
        this._pending[event] = false;   // 消费，保证只触发一次
        return true;
    }
    return false;
}

// 3) 在快捷键回调里，用“带前缀的完整 opcode”触发帽子
setupShortcuts() {
    EditorPreload.onShortcutTriggered((data) => {
        const vm = window.Scratch?.vm || (typeof Scratch !== 'undefined' && Scratch.vm);
        if (!vm || !vm.runtime) return;
        const event = ((data && data.eventName) || '').toUpperCase();
        this._pending = this._pending || {};
        this._pending[event] = true;
        // 必须用扩展ID_原始opcode，例如 myExt_whenShortcut
        const hats = vm.runtime._hats || {};
        const hatKey = Object.keys(hats).find(k => k.endsWith('_whenShortcut')) || 'myExt_whenShortcut';
        vm.runtime.startHats(hatKey, {});
    });
}
```

更完整的“全局快捷键 → 帽子积木”范例，见 [EditorPreload API 参考](./EditorPreload-API参考.md) 的“通过自定义帽子积木触发”一节。

***

## 使用参数和菜单

### 参数类型

Scratch 支持多种参数类型：

| 类型      | 常量                             | 用途    | 示例              |
| ------- | ------------------------------ | ----- | --------------- |
| **数字**  | `Scratch.ArgumentType.NUMBER`  | 输入数字  | `10`, `3.14`    |
| **字符串** | `Scratch.ArgumentType.STRING`  | 输入文本  | `"Hello"`       |
| **布尔值** | `Scratch.ArgumentType.BOOLEAN` | 是/否选择 | `true/false`    |
| **角度**  | `Scratch.ArgumentType.ANGLE`   | 角度值   | `90`, `-45`     |
| **颜色**  | `Scratch.ArgumentType.COLOR`   | 颜色选择器 | `#FF0000`       |
| **矩阵**  | `Scratch.ArgumentType.MATRIX`  | 二维数组  | `[[1,2],[3,4]]` |

### 带参数的积木

```javascript
{
    opcode: 'greetUser',
    blockType: Scratch.BlockType.COMMAND,
    text: '问候 [NAME]，你的年龄是 [AGE] 岁',
    arguments: {
        NAME: {
            type: Scratch.ArgumentType.STRING,
            defaultValue: '小明'
        },
        AGE: {
            type: Scratch.ArgumentType.NUMBER,
            defaultValue: 10
        }
    }
}
```

**实现方法**:

```javascript
greetUser(args) {
    const name = args.NAME;     // "小明"
    const age = args.AGE;       // 10
    
    console.log(`你好，${name}！你 ${age} 岁了。`);
}
```

### 下拉菜单

当选项有限时，使用菜单比手动输入更好：

```javascript
{
    opcode: 'setVolume',
    blockType: Scratch.BlockType.COMMAND,
    text: '设置音量为 [LEVEL]',
    arguments: {
        LEVEL: {
            type: Scratch.ArgumentType.STRING,
            menu: 'volumeLevels',  // 引用菜单名
            defaultValue: 'medium'
        }
    }
},
// 在 getInfo() 的 menus 字段中定义
menus: {
    volumeLevels: {
        acceptReporters: false,  // 是否允许输入变量
        items: [
            { text: '静音', value: 'mute' },
            { text: '低', value: 'low' },
            { text: '中', value: 'medium' },
            { text: '高', value: 'high' },
            { text: '最大', value: 'max' }
        ]
    }
}
```

**实现方法**:

```javascript
setVolume(args) {
    const level = args.LEVEL;  // 'low', 'medium', 'high', etc.
    
    switch(level) {
        case 'mute': console.log('音量: 静音'); break;
        case 'low': console.log('音量: 低'); break;
        case 'medium': console.log('音量: 中'); break;
        case 'high': console.log('音量: 高'); break;
        case 'max': console.log('音量: 最大'); break;
    }
}
```

**💡 小技巧**: 设置 `acceptReporters: true` 允许用户在下拉菜单中输入变量或表达式。

***

## 使用 CYSOCore 高级功能

这是 CYSOEditor 最强大的部分！通过 **EditorPreload API**，你可以访问文件系统、硬件等系统功能。

### 准备工作

在使用任何 EditorPreload 功能之前：

**步骤 1**: 在构造函数中检查环境

```javascript
constructor() {
    this.isDesktop = typeof EditorPreload !== 'undefined';
}
```

**步骤 2**: 在 `getInfo()` 中声明所需权限

```javascript
getInfo() {
    return {
        permissions: ['file-read', 'file-write'],  // 声明权限
        blocks: [...]
    };
}
```

**步骤 3**: 在每个方法开头检查环境

```javascript
async myMethod(args) {
    if (!this.isDesktop) {
        return '❌ 此功能仅支持桌面版';
    }
    
    // 继续执行...
}
```

### EditorPreload API 完整列表

以下是所有可用的 EditorPreload API 方法：

#### 文件操作

| 方法                         | 说明         | 返回值                                         |
| -------------------------- | ---------- | ------------------------------------------- |
| `readFile(path)`           | 读取文件内容     | `{success, content}`                        |
| `writeFile(path, content)` | 写入文件       | `{success}`                                 |
| `deleteFile(path)`         | 删除文件       | `{success}`                                 |
| `fileExists(path)`         | 检查文件是否存在   | `{success, exists}`                         |
| `getFileStats(path)`       | 获取文件大小/时间等 | `{success, stats}`                          |
| `createFolder(path)`       | 创建文件夹      | `{success}`                                 |
| `readLocalFolder(path)`    | 列出文件夹内容    | `{success, files[{name,isDirectory,size}]}` |

#### 硬件状态

| 方法                                     | 说明                                                                   | 返回值               |
| -------------------------------------- | -------------------------------------------------------------------- | ----------------- |
| `getHardwareStatus(device)`            | 获取硬件状态（参数 `device` 取值：`'cpu'`/`'memory'`/`'network'`/`'disk'`；`'gpu'` 仅返回占位信息；**`'battery'` 暂未实现**） | `{success, data}` |

#### 窗口控制

| 方法                                    | 说明           | 返回值         |
| ------------------------------------- | ------------ | ----------- |
| `createOverlayWindow(id, x, y, w, h)` | 创建透明覆盖窗口     | `{success}` |
| `closeOverlayWindow(id)`              | 关闭覆盖窗口       | `{success}` |
| `setOverlayContent(id, html)`         | 设置窗口 HTML 内容 | `{success}` |
| `createAdvancedWindow(id, options)`   | 创建高级标准窗口     | `{success}` |
| `setWindowProperty(id, prop, value)`  | 设置窗口属性       | `{success}` |
| `closeAdvancedWindow(id)`             | 关闭高级窗口       | `{success}` |

#### 系统交互

| 方法                                       | 说明        | 返回值                         |
| ---------------------------------------- | --------- | --------------------------- |
| `executeCommand(command, options)`       | 执行系统命令    | `{success, stdout, stderr}` |
| `registerGlobalShortcut(key, eventName)` | 注册全局快捷键   | `{success}`                 |
| `unregisterGlobalShortcut(key)`          | 注销全局快捷键   | `{success}`                 |
| `onShortcutTriggered(callback)`          | 监听快捷键触发事件 | -                           |
| `captureScreen(target)`                  | 截取屏幕画面    | `{success, dataUrl}`        |
| `captureRegion(x, y, w, h)`              | 截取指定区域    | `{success, dataUrl}`        |
| `showNotification(options)`              | 显示系统通知    | `{success}`                 |

#### 网络

| 方法                           | 说明                 | 返回值               |
| ---------------------------- | ------------------ | ----------------- |

#### 权限管理

| 方法                                                       | 说明               | 返回值                             |
| -------------------------------------------------------- | ---------------- | ------------------------------- |
| `checkPermission(extensionId, permissionType)`           | 检查特定权限状态（只读）    | `'always'` / `'ask'` / `'deny'` |
| `getPermissions()`                                       | 获取当前所有权限设置（只读）  | `{permission: setting, ...}`    |
| `getDefaults()`                                          | 获取权限默认设置（只读）     | `{permission: setting, ...}`    |
| `getExtensionPermissions(extensionId)`                   | 获取某扩展的权限（只读）     | `{permission: setting, ...}`    |
| `getExtensionPermissionStatus(extensionId, permissionType)` | 获取某扩展某权限状态（只读） | `{action, ...}`                 |
| `getAllPermissionsStatus()`                              | 获取全部扩展权限状态（只读）  | `{...}`                         |
| `registerExtensionPermissions(extensionId, permissions)` | 注册扩展所需权限         | -                               |
| `getCYSOCoreEnabled()`                                   | 检查 CYSOCore 是否启用 | `boolean`                       |
| `setCYSOCoreEnabled(enabled)`                            | 启用/禁用 CYSOCore   | -                               |


#### 工具

| 方法              | 说明              | 返回值      |
| --------------- | --------------- | -------- |
| `getPath(name)` | 获取系统路径（如桌面、文档等） | `string` |

> **注意**: 所有方法都是**异步的**（返回 Promise），调用时需要使用 `await`。

### 示例 1: 文件读取器

创建一个能读取文本文件的扩展：

```javascript
class FileReaderExtension {
    constructor() {
        this.isDesktop = typeof EditorPreload !== 'undefined';
    }

    getInfo() {
        return {
            id: 'fileReader',
            name: '📖 文件读取器',
            color1: '#2196F3',
            color2: '#1976D2',
            color3: '#0D47A1',
            
            permissions: ['file-read', 'file-metadata'],
            
            blocks: [
                {
                    opcode: 'readFile',
                    blockType: Scratch.BlockType.REPORTER,
                    text: '读取文件 [PATH]',
                    arguments: {
                        PATH: {
                            type: Scratch.ArgumentType.STRING,
                            defaultValue: '%DESKTOP%\\notes.txt'
                        }
                    }
                },
                {
                    opcode: 'fileExists',
                    blockType: Scratch.BlockType.BOOLEAN,
                    text: '文件存在? [PATH]',
                    arguments: {
                        PATH: {
                            type: Scratch.ArgumentType.STRING,
                            defaultValue: '%DESKTOP%\\test.txt'
                        }
                    }
                },
                {
                    opcode: 'getFileSize',
                    blockType: Scratch.BlockType.REPORTER,
                    text: '获取文件大小 [PATH]',
                    arguments: {
                        PATH: {
                            type: Scratch.ArgumentType.STRING,
                            defaultValue: '%DESKTOP%\\data.json'
                        }
                    }
                }
            ]
        };
    }

    async readFile(args) {
        if (!this.isDesktop) return '错误: 需要桌面版';

        try {
            const result = await EditorPreload.readFile(args.PATH);
            
            if (result.success) {
                return result.content;  // 返回文件内容
            } else {
                return `❌ 读取失败: ${result.error}`;
            }
        } catch (error) {
            return `❌ 异常: ${error.message}`;
        }
    }

    async fileExists(args) {
        if (!this.isDesktop) return false;

        try {
            const result = await EditorPreload.fileExists(args.PATH);
            return result.success && result.exists;
        } catch (error) {
            return false;
        }
    }

    async getFileSize(args) {
        if (!this.isDesktop) return '错误: 需要桌面版';

        try {
            const result = await EditorPreload.getFileStats(args.PATH);
            
            if (result.success) {
                const bytes = result.stats.size;
                
                // 格式化文件大小
                if (bytes < 1024) return `${bytes} B`;
                if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
                return `${(bytes / 1048576).toFixed(2)} MB`;
            } else {
                return `❌ ${result.error}`;
            }
        } catch (error) {
            return `❌ ${error.message}`;
        }
    }
}

if (typeof Scratch !== 'undefined') {
    Scratch.extensions.register(new FileReaderExtension());
}
```

**关键点**:

- ✅ 所有 EditorPreload 方法都是 **异步的**（需要 `await`）
- ✅ 方法也要声明为 `async`
- ✅ 始终使用 `try-catch` 处理错误
- ✅ 使用路径变量 `%DESKTOP%` 而不是硬编码路径

***

### 示例 2: 系统监控器

获取并显示计算机硬件状态：

```javascript
class SystemMonitorExtension {
    constructor() {
        this.isDesktop = typeof EditorPreload !== 'undefined';
    }

    getInfo() {
        return {
            id: 'sysMonitor',
            name: '💻 系统监控器',
            color1: '#9C27B0',
            color2: '#7B1FA2',
            color3: '#4A148C',
            
            permissions: ['hardware-status'],
            
            blocks: [
                {
                    opcode: 'getCpuUsage',
                    blockType: Scratch.BlockType.REPORTER,
                    text: 'CPU 使用率 %'
                },
                {
                    opcode: 'getMemoryUsage',
                    blockType: Scratch.BlockType.REPORTER,
                    text: '内存使用率 %'
                },
                {
                    opcode: 'getMemoryTotal',
                    blockType: Scratch.BlockType.REPORTER,
                    text: '总内存 (GB)'
                },
            ]
        };
    }

    async getCpuUsage() {
        if (!this.isDesktop) return 0;

        try {
            const result = await EditorPreload.getHardwareStatus('cpu');
            return result.success ? result.data.usage : 0;
        } catch (error) {
            return 0;
        }
    }

    async getMemoryUsage() {
        if (!this.isDesktop) return 0;

        try {
            const result = await EditorPreload.getHardwareStatus('memory');
            return result.success ? result.data.usage : 0;
        } catch (error) {
            return 0;
        }
    }

    async getMemoryTotal() {
        if (!this.isDesktop) return 0;

        try {
            const result = await EditorPreload.getHardwareStatus('memory');
            if (result.success) {
                return (result.data.total / 1073741824).toFixed(1);  // 转换为 GB
            }
            return 0;
        } catch (error) {
            return 0;
        }
    }

}

if (typeof Scratch !== 'undefined') {
    Scratch.extensions.register(new SystemMonitorExtension());
}
```

***

### 示例 3: 文件写入器

创建和修改文件：

```javascript
class FileWriterExtension {
    constructor() {
        this.isDesktop = typeof EditorPreload !== 'undefined';
    }

    getInfo() {
        return {
            id: 'fileWriter',
            name: '✏️ 文件写入器',
            color1: '#FF9800',
            color2: '#F57C00',
            color3: '#E65100',
            
            permissions: ['file-write', 'file-delete'],
            
            blocks: [
                {
                    opcode: 'writeFile',
                    blockType: Scratch.BlockType.COMMAND,
                    text: '写入文件 [PATH] 内容 [CONTENT]',
                    arguments: {
                        PATH: {
                            type: Scratch.ArgumentType.STRING,
                            defaultValue: '%DESKTOP%\\output.txt'
                        },
                        CONTENT: {
                            type: Scratch.ArgumentType.STRING,
                            defaultValue: 'Hello World!'
                        }
                    }
                },
                {
                    opcode: 'appendToFile',
                    blockType: Scratch.BlockType.COMMAND,
                    text: '追加到文件 [PATH] 内容 [CONTENT]',
                    arguments: {
                        PATH: {
                            type: Scratch.ArgumentType.STRING,
                            defaultValue: '%DESKTOP%\\log.txt'
                        },
                        CONTENT: {
                            type: Scratch.ArgumentType.STRING,
                            defaultValue: 'New line'
                        }
                    }
                },
                {
                    opcode: 'deleteFile',
                    blockType: Scratch.BlockType.COMMAND,
                    text: '删除文件 [PATH]',
                    arguments: {
                        PATH: {
                            type: Scratch.ArgumentType.STRING,
                            defaultValue: '%DESKTOP%\\temp.txt'
                        }
                    }
                },
                {
                    opcode: 'createFolder',
                    blockType: Scratch.BlockType.COMMAND,
                    text: '创建文件夹 [PATH]',
                    arguments: {
                        PATH: {
                            type: Scratch.ArgumentType.STRING,
                            defaultValue: '%DESKTOP%\\新建文件夹'
                        }
                    }
                },
                {
                    opcode: 'saveJSON',
                    blockType: Scratch.BlockType.COMMAND,
                    text: '保存 JSON 到 [PATH] 数据 [DATA]',
                    arguments: {
                        PATH: {
                            type: Scratch.ArgumentType.STRING,
                            defaultValue: '%DESKTOP%\\data.json'
                        },
                        DATA: {
                            type: Scratch.ArgumentType.STRING,
                            defaultValue: '{"score": 100}'
                        }
                    }
                }
            ]
        };
    }

    async writeFile(args) {
        if (!this.isDesktop) return;

        try {
            const result = await EditorPreload.writeFile(args.PATH, args.CONTENT);
            
            if (result.success) {
                console.log('✅ 文件已保存:', args.PATH);
            } else {
                console.error('❌ 保存失败:', result.error);
            }
        } catch (error) {
            console.error('❌ 异常:', error.message);
        }
    }

    async appendToFile(args) {
        if (!this.isDesktop) return;

        try {
            // 先读取现有内容
            const readResult = await EditorPreload.readFile(args.PATH);
            let existingContent = '';
            
            if (readResult.success) {
                existingContent = readResult.content + '\n';
            }
            
            // 写入追加后的内容
            const writeResult = await EditorPreload.writeFile(
                args.PATH, 
                existingContent + args.CONTENT
            );
            
            if (writeResult.success) {
                console.log('✅ 已追加到:', args.PATH);
            }
        } catch (error) {
            console.error('❌ 追加失败:', error.message);
        }
    }

    async deleteFile(args) {
        if (!this.isDesktop) return;

        try {
            const result = await EditorPreload.deleteFile(args.PATH);
            
            if (result.success) {
                console.log('🗑️ 已删除:', args.PATH);
            } else {
                console.error('❌ 删除失败:', result.error);
            }
        } catch (error) {
            console.error('❌ 异常:', error.message);
        }
    }

    async createFolder(args) {
        if (!this.isDesktop) return;

        try {
            const result = await EditorPreload.createFolder(args.PATH);
            
            if (result.success) {
                console.log('📁 已创建:', args.PATH);
            } else {
                console.error('❌ 创建失败:', result.error);
            }
        } catch (error) {
            console.error('❌ 异常:', error.message);
        }
    }

    async saveJSON(args) {
        if (!this.isDesktop) return;

        try {
            // 尝试解析 JSON 并格式化
            let jsonData = args.DATA;
            try {
                const parsed = JSON.parse(jsonData);
                jsonData = JSON.stringify(parsed, null, 2);  // 格式化
            } catch (e) {
                // 如果不是有效 JSON，原样保存
            }
            
            const result = await EditorPreload.writeFile(args.PATH, jsonData);
            
            if (result.success) {
                console.log('✅ JSON 已保存:', args.PATH);
            }
        } catch (error) {
            console.error('❌ 保存失败:', error.message);
        }
    }
}

if (typeof Scratch !== 'undefined') {
    Scratch.extensions.register(new FileWriterExtension());
}
```

***

## 权限系统完全指南

### 为什么需要权限？

CYSOCore 让扩展能够访问敏感的系统功能。为了安全，采用了类似手机 App 的权限模型：

```
用户安装扩展 → 扩展声明需要的权限 → 用户授权 → 扩展可以使用功能
```

### 权限等级

| 等级         | 风险 | 说明            | 示例             |
| ---------- | -- | ------------- | -------------- |
| 🟢 **低风险** | 安全 | 只读操作，不会造成损害   | 读取文件、查看硬件状态    |
| 🟡 **中风险** | 注意 | 可能影响用户体验      | 写入文件、注册快捷键     |
| 🔴 **高风险** | 危险 | 可能造成数据丢失或安全问题 | 删除文件、执行命令、屏幕捕获 |

### 完整权限清单

#### 文件操作类

| 权限 ID           | 名称    | 风险 | 说明            |
| --------------- | ----- | -- | ------------- |
| `file-read`     | 读取文件  | 🟢 | 读取文件内容、检查文件存在 |
| `file-write`    | 写入文件  | 🟡 | 创建或修改文件内容     |
| `file-delete`   | 删除文件  | 🔴 | 永久删除文件        |
| `file-metadata` | 文件元数据 | 🟢 | 获取文件大小、时间等信息  |

#### 系统操作类

| 权限 ID             | 名称    | 风险 | 说明                     |
| ----------------- | ----- | -- | ---------------------- |
| `system-command`  | 执行命令  | 🔴 | 运行系统命令（如 dir、python 等）。**出厂默认：拒绝**，需在 CYSO Core 控制中心手动开启 |
| `global-shortcut` | 全局快捷键 | 🟡 | 注册系统级快捷键               |

#### 界面操作类

| 权限 ID             | 名称     | 风险 | 说明          |
| ----------------- | ------ | -- | ----------- |
| `draw-window`     | 屏幕绘制窗口 | 🟡 | 创建透明覆盖窗口    |
| `screen-capture`  | 屏幕捕获   | 🔴 | 截取屏幕或窗口画面   |
| `advanced-window` | 高级窗口   | 🟡 | 创建特殊属性的标准窗口 |

#### 硬件操作类

| 权限 ID             | 名称   | 风险 | 说明              |
| ----------------- | ---- | -- | --------------- |
| `hardware-status` | 硬件状态 | 🟢 | 读取 CPU、内存、磁盘等信息 |

### 如何声明权限？

在 `getInfo()` 中添加 `permissions` 数组：

```javascript
getInfo() {
    return {
        id: 'my-extension',
        name: '我的扩展',
        
        // 声明这个扩展需要的所有权限
        permissions: [
            'file-read',         // 需要读取文件
            'file-write',        // 需要写入文件
            'hardware-status'    // 需要查看硬件状态
            // 不要申请不需要的权限！
        ],
        
        blocks: [...]
    };
}
```

**⚠️ 最佳实践**:

- ✅ 只申请**真正需要**的权限
- ❌ 不要一次性申请所有权限（用户会警惕）
- ✅ 在文档中说明为什么需要这些权限

### 权限请求流程

当你调用需要权限的 API 时：

```
1. 扩展调用 EditorPreload.readFile()
         ↓
2. 系统检查该权限的设置
         ↓
3a. 如果是 "always" → 直接允许 ✅
3b. 如果是 "deny" → 直接拒绝 ❌
3c. 如果是 "ask" → 弹出对话框询问用户
         ↓
4. 用户选择 "允许" 或 "拒绝"
         ↓
5a. 允许 → 执行操作
5b. 拒绝 → 返回错误

> 📌 **注意**:
> - 如果扩展**没有在 `permissions` 里声明**该权限，系统会**直接拒绝**（不会弹窗）。
> - `system-command`（执行命令）的出厂默认是 **「拒绝」**，必须在 CYSO Core 控制中心手动开启后才能使用；其余权限默认是「询问」。
> - 扩展权限只能由用户手动设置。
```

### 如何减少权限弹窗？

**方法 1**: 引导用户预先设置权限

```javascript
class MyExtension {
    constructor() {
        this.isDesktop = typeof EditorPreload !== 'undefined';
    }

    async onEnable() {
        if (!this.isDesktop) return;

        // 在扩展启用时，建议用户设置权限
        console.log('建议：请在 CYSO Core 设置中将以下权限设为"始终允许":');
        console.log('- file-read (低风险)');
        console.log('- hardware-status (低风险)');
    }
}
```

**方法 2**: 引导用户在 CYSO Core 控制中心手动设置

> 请在扩展说明里提示用户：打开编辑器的 CYSO Core 控制中心，把需要的权限手动设为"始终允许"。
***

## 实战项目：系统信息查看器

让我们把学到的知识整合起来，做一个实用的项目！

### 项目目标

创建一个扩展，能够：

1. 📊 显示系统硬件信息
2. 📁 浏览文件夹内容
3. 📝 读写配置文件
4. 🖥️ 创建系统监控悬浮窗

### 完整代码

```javascript
class SystemToolkitExtension {
    constructor() {
        this.isDesktop = typeof EditorPreload !== 'undefined';
    }

    getInfo() {
        return {
            id: 'systemToolkit',
            name: '🛠️ 系统工具箱',
            color1: '#607D8B',
            color2: '#455A64',
            color3: '#263238',
            
            permissions: [
                'file-read',
                'file-write',
                'file-metadata',
                'hardware-status',
                'draw-window'
            ],
            
            blocks: [
                // ====== 硬件信息 ======
                {
                    opcode: 'getCpuInfo',
                    blockType: Scratch.BlockType.REPORTER,
                    text: 'CPU 信息'
                },
                {
                    opcode: 'getMemoryInfo',
                    blockType: Scratch.BlockType.REPORTER,
                    text: '内存信息'
                },
                {
                    opcode: 'getDiskInfo',
                    blockType: Scratch.BlockType.REPORTER,
                    text: '磁盘信息 [DRIVE]',
                    arguments: {
                        DRIVE: {
                            type: Scratch.ArgumentType.STRING,
                            menu: 'drives',
                            defaultValue: 'C:'
                        }
                    }
                },

                // ====== 文件操作 ======
                {
                    opcode: 'listFolder',
                    blockType: Scratch.BlockType.REPORTER,
                    text: '列出文件夹 [PATH] 的内容',
                    arguments: {
                        PATH: {
                            type: Scratch.ArgumentType.STRING,
                            defaultValue: '%DESKTOP%'
                        }
                    }
                },
                {
                    opcode: 'readTextFile',
                    blockType: Scratch.BlockType.REPORTER,
                    text: '读取文本 [PATH]',
                    arguments: {
                        PATH: {
                            type: Scratch.ArgumentType.STRING,
                            defaultValue: '%DESKTOP%\\notes.txt'
                        }
                    }
                },
                {
                    opcode: 'saveTextFile',
                    blockType: Scratch.BlockType.COMMAND,
                    text: '保存文本到 [PATH] 内容 [TEXT]',
                    arguments: {
                        PATH: {
                            type: Scratch.ArgumentType.STRING,
                            defaultValue: '%DESKTOP%\\output.txt'
                        },
                        TEXT: {
                            type: Scratch.ArgumentType.STRING,
                            defaultValue: 'Hello World'
                        }
                    }
                },

                // ====== 监控悬浮窗 ======
                {
                    opcode: 'showMonitor',
                    blockType: Scratch.BlockType.COMMAND,
                    text: '显示系统监控窗'
                },
                {
                    opcode: 'hideMonitor',
                    blockType: Scratch.BlockType.COMMAND,
                    text: '隐藏系统监控窗'
                },
                {
                    opcode: 'updateMonitor',
                    blockType: Scratch.BlockType.COMMAND,
                    text: '更新监控数据'
                }
            ],

            menus: {
                drives: {
                    acceptReporters: true,
                    items: [
                        { text: 'C: 盘', value: 'C:' },
                        { text: 'D: 盘', value: 'D:' },
                        { text: 'E: 盘', value: 'E:' }
                    ]
                }
            }
        };
    }

    // ========== 硬件信息方法 ==========

    async getCpuInfo() {
        if (!this.isDesktop) return '❌ 需要桌面版';

        try {
            const result = await EditorPreload.getHardwareStatus('cpu');
            if (result.success) {
                const cpu = result.data;
                return `CPU: ${cpu.model}\n核心数: ${cpu.cores}\n主频: ${cpu.speed} MHz\n使用率: ${cpu.usage}%`;
            }
            return `❌ ${result.error}`;
        } catch (error) {
            return `❌ ${error.message}`;
        }
    }

    async getMemoryInfo() {
        if (!this.isDesktop) return '❌ 需要桌面版';

        try {
            const result = await EditorPreload.getHardwareStatus('memory');
            if (result.success) {
                const mem = result.data;
                const totalGB = (mem.total / 1073741824).toFixed(1);
                const usedGB = (mem.used / 1073741824).toFixed(1);
                const freeGB = (mem.free / 1073741824).toFixed(1);
                
                return `总内存: ${totalGB} GB\n已使用: ${usedGB} GB (${mem.usage}%)\n可用: ${freeGB} GB`;
            }
            return `❌ ${result.error}`;
        } catch (error) {
            return `❌ ${error.message}`;
        }
    }

    async getDiskInfo(args) {
        if (!this.isDesktop) return '❌ 需要桌面版';

        try {
            const result = await EditorPreload.getHardwareStatus('disk');
            if (result.success) {
                const disk = result.data.disks.find(d => d.drive === args.DRIVE);
                
                if (disk) {
                    const totalGB = (disk.total / 1073741824).toFixed(1);
                    const freeGB = (disk.free / 1073741824).toFixed(1);
                    const usedGB = (disk.used / 1073741824).toFixed(1);
                    
                    return `${args.DRIVE}\n总容量: ${totalGB} GB\n已用: ${usedGB} GB (${disk.usage}%)\n剩余: ${freeGB} GB`;
                }
                return `❌ 未找到 ${args.DRIVE} 盘`;
            }
            return `❌ ${result.error}`;
        } catch (error) {
            return `❌ ${error.message}`;
        }
    }

    // ========== 文件操作方法 ==========

    async listFolder(args) {
        if (!this.isDesktop) return '❌ 需要桌面版';

        try {
            const result = await EditorPreload.readLocalFolder(args.PATH);
            
            if (result.success) {
                let output = `📂 ${args.PATH}\n${'━'.repeat(40)}\n`;
                
                for (const file of result.files.slice(0, 20)) {  // 限制显示前20个
                    const icon = file.isDirectory ? '📁' : '📄';
                    const size = file.isDirectory 
                        ? '<DIR>' 
                        : this.formatSize(file.size);
                    output += `${icon} ${file.name.padEnd(25)} ${size}\n`;
                }
                
                if (result.files.length > 20) {
                    output += `\n... 还有 ${result.files.length - 20} 个项目`;
                }
                
                return output;
            }
            return `❌ ${result.error}`;
        } catch (error) {
            return `❌ ${error.message}`;
        }
    }

    async readTextFile(args) {
        if (!this.isDesktop) return '❌ 需要桌面版';

        try {
            const result = await EditorPreload.readFile(args.PATH);
            
            if (result.success) {
                // 限制返回长度，避免卡顿
                const content = result.content.length > 5000 
                    ? result.content.substring(0, 5000) + '\n...(内容过长，已截断)'
                    : result.content;
                return content;
            }
            return `❌ ${result.error}`;
        } catch (error) {
            return `❌ ${error.message}`;
        }
    }

    async saveTextFile(args) {
        if (!this.isDesktop) return;

        try {
            const result = await EditorPreload.writeFile(args.PATH, args.TEXT);
            
            if (result.success) {
                console.log(`✅ 已保存到: ${args.PATH}`);
            } else {
                console.error(`❌ 保存失败: ${result.error}`);
            }
        } catch (error) {
            console.error(`❌ 异常: ${error.message}`);
        }
    }

    // ========== 监控悬浮窗方法 ==========

    async showMonitor() {
        if (!this.isDesktop) {
            alert('此功能需要桌面版！');
            return;
        }

        try {
            // 创建悬浮窗（右上角，350x220像素）
            await EditorPreload.createOverlayWindow('sys-toolkit-monitor', 1200, 10, 350, 220);
            
            // 立即更新一次显示
            await this.updateMonitorDisplay();
            
            console.log('✅ 系统监控已启动');
        } catch (error) {
            console.error('❌ 启动失败:', error.message);
        }
    }

    async hideMonitor() {
        if (!this.isDesktop) return;

        try {
            await EditorPreload.closeOverlayWindow('sys-toolkit-monitor');
            console.log('⏹️ 系统监控已关闭');
        } catch (error) {
            console.error('❌ 关闭失败:', error.message);
        }
    }

    async updateMonitor() {
        if (!this.isDesktop) return;
        await this.updateMonitorDisplay();
    }

    // 内部方法：更新悬浮窗内容
    async updateMonitorDisplay() {
        if (!this.isDesktop) return;

        try {
            // 并行获取所有硬件信息
            const [cpuResult, memResult] = await Promise.all([
                EditorPreload.getHardwareStatus('cpu'),
                EditorPreload.getHardwareStatus('memory')
            ]);

            const cpu = cpuResult.data;
            const mem = memResult.data;

            // 根据使用率选择颜色
            const cpuColor = cpu.usage > 80 ? '#f44336' : cpu.usage > 50 ? '#ff9800' : '#4caf50';
            const memColor = mem.usage > 80 ? '#f44336' : mem.usage > 50 ? '#ff9800' : '#2196f3';

            // 构建 HTML 内容
            const html = `
                <div style="
                    padding: 15px;
                    font-family: 'Segoe UI', Arial, sans-serif;
                    background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
                    color: white;
                    border-radius: 12px;
                    box-shadow: 0 8px 32px rgba(0,0,0,0.3);
                    font-size: 13px;
                ">
                    <div style="font-size: 16px; font-weight: bold; margin-bottom: 12px; text-align: center;">
                        🖥️ 系统工具箱 - 实时监控
                    </div>
                    
                    <div style="background: rgba(255,255,255,0.1); padding: 10px; border-radius: 8px; margin-bottom: 8px;">
                        <div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
                            <span>🔲 CPU</span>
                            <span style="color: ${cpuColor}; font-weight: bold;">${cpu.usage}%</span>
                        </div>
                        <div style="background: rgba(0,0,0,0.3); height: 8px; border-radius: 4px; overflow: hidden;">
                            <div style="width: ${cpu.usage}%; background: ${cpuColor}; height: 100%; transition: width 0.5s;"></div>
                        </div>
                    </div>

                    <div style="background: rgba(255,255,255,0.1); padding: 10px; border-radius: 8px; margin-bottom: 8px;">
                        <div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
                            <span>💾 内存</span>
                            <span style="color: ${memColor}; font-weight: bold;">
                                ${(mem.used / 1073741824).toFixed(1)} / ${(mem.total / 1073741824).toFixed(1)} GB
                            </span>
                        </div>
                        <div style="background: rgba(0,0,0,0.3); height: 8px; border-radius: 4px; overflow: hidden;">
                            <div style="width: ${mem.usage}%; background: ${memColor}; height: 100%; transition: width 0.5s;"></div>
                        </div>
                    </div>

                    <div style="text-align: center; margin-top: 10px; font-size: 11px; opacity: 0.7;">
                        ${new Date().toLocaleTimeString('zh-CN')}
                    </div>
                </div>
            `;

            await EditorPreload.setOverlayContent('sys-toolkit-monitor', html);

        } catch (error) {
            console.error('更新失败:', error.message);
        }
    }

    // 工具方法：格式化文件大小
    formatSize(bytes) {
        if (bytes < 1024) return bytes + ' B';
        if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
        return (bytes / 1048576).toFixed(1) + ' MB';
    }
}

// 注册扩展
if (typeof Scratch !== 'undefined') {
    Scratch.extensions.register(new SystemToolkitExtension());
}
```

### 如何测试这个扩展？

1. 将上面的代码保存为 `system-toolkit.js`
2. 在 CYSOEditor 中加载这个扩展
3. 尝试以下操作：

**测试硬件信息**:

- 拖出 "CPU 信息" 积木 → 应该显示 CPU 详情
- 拖出 "内存信息" 积木 → 应该显示内存使用情况

**测试文件操作**:

- 拖出 "列出文件夹内容" 积木 → 默认会列出桌面文件
- 拖出 "保存文本到..." 积木 → 在桌面创建一个测试文件
- 再用 "读取文本" 积木读取它

**测试监控悬浮窗**:

- 点击 "显示系统监控窗" → 右上角出现漂亮的监控窗口
- 点击 "更新监控数据" → 刷新数据
- 点击 "隐藏系统监控窗" → 关闭窗口

***

## 调试技巧

### 1. 使用浏览器开发者工具

1. 在 CYSOEditor 中按 **F12** 或 **Ctrl+Shift+I**
2. 切换到 **Console**（控制台）标签
3. 所有 `console.log()` 都会显示在这里

### 2. 常用的调试代码

```javascript
class DebuggableExtension {
    async myMethod(args) {
        // 🔍 1. 打印输入参数
        console.log('[DEBUG] myMethod 被调用');
        console.log('[DEBUG] 参数:', args);

        try {
            // 🔍 2. 打印中间结果
            const result = await EditorPreload.someAPI(args);
            console.log('[DEBUG] API 返回:', result);

            // 🔍 3. 打印最终结果
            const finalValue = this.processData(result);
            console.log('[DEBUG] 最终返回:', finalValue);

            return finalValue;

        } catch (error) {
            // 🔍 4. 错误追踪
            console.error('[ERROR] 出错了!', error);
            console.error('[ERROR] 错误栈:', error.stack);
            
            throw error;  // 重新抛出让上层处理
        }
    }
}
```

### 3. 常见错误及解决方法

#### 错误 1: "EditorPreload is not defined"

**原因**: 在网页版或非桌面环境中使用了 EditorPreload

**解决**:

```javascript
constructor() {
    this.isDesktop = typeof EditorPreload !== 'undefined';
}

async myMethod() {
    if (!this.isDesktop) {
        return '❌ 此功能仅在桌面版可用';
    }
    // ...
}
```

#### 错误 2: "Permission denied"

**原因**: 用户拒绝了权限请求

**解决**:

```javascript
const result = await EditorPreload.readFile(path);
if (!result.success) {
    if (result.error.includes('Permission')) {
        console.log('请授予 file-read 权限！');
        return '❌ 权限被拒绝';
    }
    return `❌ ${result.error}`;
}
```

#### 错误 3: "Cannot read property of undefined"

**原因**: 访问了不存在的结果字段

**解决**:

```javascript
// ❌ 危险写法
return result.data.stats.size;

// ✅ 安全写法
if (result?.success && result?.stats?.size) {
    return result.stats.size;
}
return 0;
```

#### 错误 4: 积木不显示

**检查清单**:

- [ ] 代码末尾有 `Scratch.extensions.register(...)` ?
- [ ] `getInfo()` 格式正确？
- [ ] 没有 JavaScript 语法错误？（看控制台红色报错）
- [ ] `id` 不重复？
- [ ] 至少有一个 block 定义？

***

## 打包与分享

### 单文件分享（最简单）

直接分享 `.js` 文件即可：

- 通过 QQ、微信发送文件
- 上传到网盘提供下载链接
- 发邮件附件

### 发布到 CYScrExt Hub
- CYSO Editor存在其官方扩展库[CYScrExtHub](https://cyscrexthub.cc.cd)
- 你可以将你的扩展发布到CYScrExtHub，等待审核成功，你的扩展就可以在扩展库官方网页和编辑器的内置扩展库查看了！
***

## 常见问题 FAQ

### Q1: 扩展可以在网页版使用吗？

**部分可以**：

- ✅ 普通积木（计算、逻辑等）可以在网页版使用
- ❌ EditorPreload 相关的功能只能在桌面版使用

**建议**: 始终检查环境并提供降级方案

### Q2: 如何让积木支持中文？

直接在 `text` 字段使用中文：

```javascript
{
    opcode: 'sayHello',
    blockType: Scratch.BlockType.COMMAND,
    text: '说你好'  // ✅ 中文完全支持！
}
```

### Q3: 一个扩展可以有多个积木吗？

**当然可以！** 在 `blocks` 数组中定义多个积木：

```javascript
blocks: [
    { opcode: 'method1', ... },
    { opcode: 'method2', ... },
    { opcode: 'method3', ... },  // 可以很多个！
]
```

然后在类中分别实现对应的方法。

### Q4: async/await 太难了，可以不用吗？

**不可以** 😅

因为所有 EditorPreload 操作都是异步的（需要等待完成），所以必须使用 async/await。

**好消息是**: 其实很简单！

```javascript
// 同步思维（伪代码）
1. 开始读文件
2. 等待读完 ← await 就是"等待"
3. 拿到结果继续
4. 返回结果
```

只要记住两条规则：

1. 使用 EditorPreload 的方法前加 `await`
2. 包含 `await` 的函数前加 `async`

### Q5: 如何制作好看的图标？

图标需要是 Base64 编码的 SVG 格式：

**简单方法**: 

1. 使用绘图软件手动绘制或使用AI生成图片，当然上网找也是可以的
2. 保存图片
3. 使用img to base64工具将图片转为base64编码格式
4. 在 `iconURI` 中使用

**或者使用纯文字图标**（不用图标也可以正常工作）：

```javascript
getInfo() {
    return {
        // iconURI: '...',  // 不写也行，会有默认图标
        ...
    };
}
```

### Q6: 扩展会影响性能吗？

**一般来说不会**，除非：

- ❌ 在死循环中频繁调用 EditorPreload
- ❌ 一次读取超大文件（>100MB）
- ❌ 创建太多窗口或快捷键

**优化建议**:

- 缓存结果，避免重复查询
- 使用 `setTimeout` 或 `setInterval` 控制频率
- 及时清理不再使用的资源

***

## 进阶资源

### 学习资料

📖 **必读文档**:

- [EditorPreload API 参考](./EditorPreload-API参考.md) - 所有 API 的详细说明

🌐 **外部资源**:

- [TurboWarp 扩展库](https://extensions.turbowarp.org/) - 标准 Scratch 扩展开发
- [MDN JavaScript 教程](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript) - JS 基础
- [async/await 教程](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function) - 异步编程

### 社区支持

- [CYScrExtHub](https://cyscrexthub.cc.cd)

***

## 总结

恭喜你完成了本教程！🎉

你现在应该已经掌握了：

✅ **基础**: 创建扩展、添加积木、定义参数\
✅ **进阶**: 使用 EditorPreload API、文件操作、硬件访问\
✅ **高级**: 权限管理、屏幕覆盖窗口、错误处理\
✅ **实战**: 完整的系统工具箱项目

### 下一步建议
 **阅读**: 仔细阅读 [EditorPreload API 参考](./EditorPreload-API参考.md)
 **分享**: 把你做的扩展分享给朋友或在[社区](https://cyscrexthub.cc.cd)
 **贡献**: 发现 Bug 或有好主意？欢迎在[CYScrExtHub](https://cyscrexthub.cc.cd)一起交流提意见！

### 快速参考卡片

```javascript
// 扩展模板
class Extension {
    constructor() { this.isDesktop = !!window.EditorPreload; }
    
    getInfo() {
        return {
            id: '',
            name: '',
            color1: '', color2: '', color3: '',
            permissions: [],  // 需要的权限
            blocks: [{ opcode: '', blockType: Scratch.BlockType.XXX, text: '' }],
            menus: {}
        };
    }
    
    async method(args) {
        if (!this.isDesktop) return '';
        try {
            const r = await EditorPreload.api(args);
            return r.success ? r.data : `Error: ${r.error}`;
        } catch(e) { return `Error: ${e.message}`; }
    }
}

Scratch.extensions.register(new Extension());
```

***

> **祝你开发愉快！** 🚀\
> 如果遇到问题，不要犹豫去查阅文档或寻求帮助。
>[编辑器官网](https://cysoeditor.pages.dev)
>[扩展库](https://cyscrexthub.cc.cd)
