# 2026-04-23：把 Scratch GUI、作品分享与本地资源库真正跑起来

## 写在前面

今天这篇不写日常简报，改写成一篇完整的项目记录。

这一天的核心工作，是把一套原本偏向在线运行的 `scratch-gui`，逐步改造成一套适合本地校园环境使用的图形化编程系统。最终目标很明确：

1. `scratch-gui` 可以在本地目录中编译并通过浏览器访问。
2. `scratch` 和现有本地 Web 根目录整合到一起。
3. 角色、背景、帮助页、素材等尽量本地化，不再依赖外网。
4. 增加一个学生上传 Scratch 源文件、老师登录管理的 `share` 页面。
5. 上传后的 `.sb` / `.sb2` / `.sb3` 文件可以直接在本地 Scratch 编辑器中打开。
6. 整个系统最终可以通过 `http://127.0.0.1/share/` 和 `http://127.0.0.1/scratch/build/` 这种朴素地址稳定访问。

这篇记录会尽量把做过的事情、关键文件、踩过的坑和最终修法都整理下来，方便以后自己维护，也方便后面继续扩展。

---

## 一、目录结构与整体思路

最终落地在本地 Web 根目录 `/Users/jarod/Sites` 下，核心结构如下：

```text
/Users/jarod/Sites
├── scratch/               # scratch-gui 工程与 build 产物
├── share/                 # 学生上传 / 老师管理 PHP 页面
├── briefings/             # 博客与简报记录
├── index.html
└── index.php
```

其中两块是主角：

- `scratch/`：负责编辑器本身
- `share/`：负责作品上传、列表展示、老师管理、点开作品进入编辑器

一开始的思路是“先让它能跑”，后面逐步演进为“让它稳定可用、资源尽量本地化、尽量少依赖外部服务”。

---

## 二、先把 scratch-gui 编译并接入本地站点

### 1. 编译与运行

最开始先把 `scratch-gui` 放进本地目录中，完成依赖安装和构建。早期阶段用过开发服务器，也做过静态 `build` 输出，后面为了更好地和本地站点整合，最终选择以 `scratch/build` 静态构建产物为主。

实际使用时访问入口统一落在：

- `http://127.0.0.1/scratch/`
- `http://127.0.0.1/scratch/build/`

### 2. 和本地 Web 服务整合

为了不让 Scratch 成为一个独立端口服务，后面把它整合进 `/Users/jarod/Sites` 的现有本地 Web 服务目录。这样目录更统一，后续 `share` 页面和 Scratch 编辑器之间的互相跳转也更简单。

这一步的价值很直接：

- 避免多个端口带来的跳转混乱
- 后续 `share/open.php` 可以直接重定向到 `/scratch/build/`
- 资源路径能稳定写成站内路径

---

## 三、把 Scratch 资源尽量本地化

### 1. 本地化素材库资源

Scratch 角色库、背景库、声音库，本来大量依赖线上资源。为了离线或内网环境可用，后面把素材镜像到了本地目录：

```text
scratch/static/local-assets/
scratch/build/static/local-assets/
```

并且修改了资源请求路径，让素材优先从本地读取。

关键改动涉及：

- [storage.js](/Users/jarod/Sites/scratch/src/lib/storage.js)
- [project-fetcher-hoc.jsx](/Users/jarod/Sites/scratch/src/lib/project-fetcher-hoc.jsx)
- [asset.php](/Users/jarod/Sites/scratch/asset.php)

### 2. 为什么后来加了 `asset.php`

一开始本地素材是直接仿照 Scratch 官方资产路径放到静态目录中，类似：

```text
/scratch/build/static/local-assets/internalapi/asset/.../get
```

但本地 PHP/Apache 站点对这类“无扩展名结尾”的素材返回 `Content-Type` 时不够稳定，导致角色库里明明是 `200`，浏览器却因为 MIME 或资源解释问题不显示图片。

后来为了彻底绕过这个坑，增加了：

- [asset.php](/Users/jarod/Sites/scratch/asset.php)

再把资源读取统一改成：

```text
/scratch/asset.php?md5ext=xxxx.svg
```

这个改法的好处是：

- 所有素材请求都有明确 PHP 输出
- `Content-Type` 可以精确控制
- 不再依赖目录名、`.htaccess`、站点 MIME 猜测

### 3. 去掉外网帮助页和追踪依赖

还陆续把若干外部帮助链接、视频页、提示页改到本地说明页，避免编辑器运行时再去碰外网。

本地帮助页放在：

- [offline-help.html](/Users/jarod/Sites/scratch/static/local-pages/offline-help.html)

涉及过的文件包括：

- [update-peripheral-step.jsx](/Users/jarod/Sites/scratch/src/components/connection-modal/update-peripheral-step.jsx)
- [webgl-modal.jsx](/Users/jarod/Sites/scratch/src/components/webgl-modal/webgl-modal.jsx)
- [index.ejs](/Users/jarod/Sites/scratch/src/playground/index.ejs)

这一步做完后，Scratch 角色库、背景库、扩展说明、帮助跳转都更像一个完整的本地系统，而不是“本地壳子 + 线上资源拼接”。

---

## 四、做一个本地 `share` 页面：学生上传，老师管理

### 1. 功能目标

用户需求很明确：

- 学生上传任意版本 Scratch 源文件
- 老师密码登录后台管理
- 支持中文文件名
- 支持按学生姓名或班级搜索
- 文件名可直接点击，在本地 Scratch 编辑器中打开

最开始我做过 Python 版原型，后面因为本地目录本身就是 PHP Web 服务目录，所以最终改为纯 PHP 版本。

### 2. 最终 `share` 结构

关键文件如下：

- [index.php](/Users/jarod/Sites/share/index.php)
- [app.js](/Users/jarod/Sites/share/app.js)
- [styles.css](/Users/jarod/Sites/share/styles.css)
- [lib/share-lib.php](/Users/jarod/Sites/share/lib/share-lib.php)
- [open.php](/Users/jarod/Sites/share/open.php)
- [public-file.php](/Users/jarod/Sites/share/public-file.php)
- [files.php](/Users/jarod/Sites/share/files.php)
- [api/upload.php](/Users/jarod/Sites/share/api/upload.php)
- [api/uploads-public.php](/Users/jarod/Sites/share/api/uploads-public.php)
- [api/teacher-login.php](/Users/jarod/Sites/share/api/teacher-login.php)
- [api/teacher-uploads.php](/Users/jarod/Sites/share/api/teacher-uploads.php)
- [api/teacher-delete.php](/Users/jarod/Sites/share/api/teacher-delete.php)

### 3. 页面布局

后面又根据使用体验做了页面重排：

- 左侧：学生上传
- 右侧：作品列表
- 底部：老师登录 / 后台管理

这样比“上传区和列表堆在一起”清晰很多，尤其适合课堂现场快速操作。

---

## 五、中文文件名问题：为什么会变成 `______.sb3`

这是一个很典型、也很容易被忽视的细节。

最初为了保证文件系统安全，上传逻辑直接把原始文件名做了 ASCII 清洗。这样一来，中文名自然都被替换成下划线，于是出现了：

```text
校园作品.sb3  ->  ______.sb3
```

后来做了拆分：

1. 界面显示保留原始文件名
2. 磁盘实际存储使用安全文件名
3. 下载响应头同时写入普通 `filename` 和 UTF-8 `filename*`

相关代码主要在：

- [share-lib.php](/Users/jarod/Sites/share/lib/share-lib.php)
- [upload.php](/Users/jarod/Sites/share/api/upload.php)

这个修正之后，新上传文件的中文名在前台、后台和下载时都能正常显示。

---

## 六、让上传作品能直接在 Scratch 编辑器中打开

这是整套系统里最折腾、也最有代表性的一条链路。

### 1. 设计目标

在 `share` 页面中点击文件名时，不是下载文件，而是直接进入本地 Scratch 编辑器，并把该项目打开。

所以有了这个跳转流程：

```text
/share/open.php?id=xxxx
  -> 302 跳转到
/scratch/build/?project_file=...&project_title=...
```

其中：

- `project_file` 指向真实项目文件
- `project_title` 用于在编辑器标题栏显示

### 2. 关键桥梁文件

- [open.php](/Users/jarod/Sites/share/open.php)
- [public-file.php](/Users/jarod/Sites/share/public-file.php)
- [query-parser-hoc.jsx](/Users/jarod/Sites/scratch/src/lib/query-parser-hoc.jsx)

### 3. 这条链路踩过的坑

#### 坑一：默认项目覆盖了上传项目

Scratch 在没有 hash 项目 ID 时，本来会自动加载默认项目。  
当 `project_file` 方案接进来后，这条默认加载流程和远程项目加载打架，结果就是：刚加载的上传项目又被默认项目覆盖。

修法：

- 在 [hash-parser-hoc.jsx](/Users/jarod/Sites/scratch/src/lib/hash-parser-hoc.jsx) 中判断：只要 URL 里带 `project_file`，就不要再走默认项目逻辑。

#### 坑二：项目文件下载成功，但被当成“线上项目”继续保存

项目虽然已经通过 `vm.loadProject()` 打开，但后续 Redux 状态还把它当成“可保存的线上项目”，继续往自动保存分支走，导致编辑器状态异常。

修法：

- 在 [query-parser-hoc.jsx](/Users/jarod/Sites/scratch/src/lib/query-parser-hoc.jsx) 中把 `project_file` 打开的项目视为“本地上传项目”，加载完成后不再触发自动保存流程。

#### 坑三：另一个本地上传流程把远程打开误判成空上传

这是今天最关键的一处。

`project_file` 打开项目时，本质上也借用了“上传文件”的加载状态；但 `SBFileUploaderHOC` 监听到这个状态后，以为是一次没有选择文件的本地上传，于是直接调用取消逻辑，把正在加载的项目打回空白。

修法：

- 在 [sb-file-uploader-hoc.jsx](/Users/jarod/Sites/scratch/src/lib/sb-file-uploader-hoc.jsx) 中加判断：
  如果当前 URL 带有 `project_file`，就不要让本地上传组件去取消这次加载。

这一步修完后，`/share` 中上传的 `.sb2/.sb3` 文件终于能稳定打开。

#### 坑四：绝对 URL 在不同访问方式下不稳定

最初 `project_file` 会生成带协议和主机的绝对地址，比如：

```text
http://127.0.0.1/share/public-file.php?id=...
```

但如果实际是通过其他主机名、局域网地址或 HTTPS 访问，很容易出现协议判断不一致。  
后面改成站内相对路径：

```text
/share/public-file.php?id=...
```

相关修改在：

- [share-lib.php](/Users/jarod/Sites/share/lib/share-lib.php)

这样无论是 `127.0.0.1`、局域网 IP，还是别的本地域名，都能沿当前站点环境去请求项目文件。

---

## 七、角色库与背景库本地导入：缩略图正常不代表能导入

这块是另一个典型问题：看起来图片能显示了，但点了以后资源并没有真正进入编辑器。

### 1. 缩略图修复

角色库、背景库缩略图依赖的图片资源，后来都统一从本地 `asset.php` 输出。  
这一步解决的是“能看到图”。

### 2. 角色导入修复

角色导入一开始尝试的是“把角色对象和 asset 数据直接喂给 `vm.addSprite()`”。  
这个方式虽然能在一部分情况下工作，但不够稳，后期又出现了“角色库中的图片能看见，却不能加入编辑器”的回归。

最后的修法是：

1. 先把角色和它依赖的 costumes / sounds 拉到本地
2. 动态打成一个标准 `.sprite3` 包
3. 再走 Scratch VM 原生的 `addSprite(spriteArchive)` 导入路径

关键文件：

- [library-assets.js](/Users/jarod/Sites/scratch/src/lib/library-assets.js)
- [sprite-library.jsx](/Users/jarod/Sites/scratch/src/containers/sprite-library.jsx)

这条路径的优点很明显：

- 更贴近 Scratch 自己“上传角色”的原生流程
- 避免对象兼容细节导致的导入不稳定
- 后续维护成本更低

### 3. 背景导入修复

背景和角色看似类似，实际不是同一条 VM API。

`vm.addBackdrop(...)` 对传入 asset 的要求更严格，所以后面单独给背景做了 runtime asset 构造。

关键文件：

- [backdrop-library.jsx](/Users/jarod/Sites/scratch/src/containers/backdrop-library.jsx)
- [library-assets.js](/Users/jarod/Sites/scratch/src/lib/library-assets.js)

这之后，背景库点击后就能把正确背景加入舞台，而不是只出现一个不完整的图片对象。

---

## 八、顶部导航与视觉小完善

在功能基本跑通后，还做了两个收尾动作：

### 1. 顶部“查看作品页面”按钮

将 Scratch 顶部的“查看作品页面”按钮改为直接跳到 `/share/`，并在按钮前面加入校徽图标。

相关文件：

- [community-button.jsx](/Users/jarod/Sites/scratch/src/components/menu-bar/community-button.jsx)
- [community-button.css](/Users/jarod/Sites/scratch/src/components/menu-bar/community-button.css)
- [school-badge.png](/Users/jarod/Sites/scratch/src/components/menu-bar/school-badge.png)

这里还做过一次误改：一开始把左边“分享”按钮改掉了，后面已恢复原始分享逻辑，只保留右侧“查看作品页面”按钮改造。

### 2. 老师后台密码更新

老师登录密码后来调整为：

```text
ghtxx.cn
```

对应哈希保存在：

- [share-lib.php](/Users/jarod/Sites/share/lib/share-lib.php)

---

## 九、这次用到的关键代码与资源清单

为了以后复查方便，这里把最关键的文件做一个归档。

### Scratch 侧

- [storage.js](/Users/jarod/Sites/scratch/src/lib/storage.js)
- [project-fetcher-hoc.jsx](/Users/jarod/Sites/scratch/src/lib/project-fetcher-hoc.jsx)
- [query-parser-hoc.jsx](/Users/jarod/Sites/scratch/src/lib/query-parser-hoc.jsx)
- [hash-parser-hoc.jsx](/Users/jarod/Sites/scratch/src/lib/hash-parser-hoc.jsx)
- [sb-file-uploader-hoc.jsx](/Users/jarod/Sites/scratch/src/lib/sb-file-uploader-hoc.jsx)
- [library-assets.js](/Users/jarod/Sites/scratch/src/lib/library-assets.js)
- [sprite-library.jsx](/Users/jarod/Sites/scratch/src/containers/sprite-library.jsx)
- [backdrop-library.jsx](/Users/jarod/Sites/scratch/src/containers/backdrop-library.jsx)
- [menu-bar.jsx](/Users/jarod/Sites/scratch/src/components/menu-bar/menu-bar.jsx)
- [community-button.jsx](/Users/jarod/Sites/scratch/src/components/menu-bar/community-button.jsx)
- [asset.php](/Users/jarod/Sites/scratch/asset.php)
- [offline-help.html](/Users/jarod/Sites/scratch/static/local-pages/offline-help.html)

### Share 侧

- [index.php](/Users/jarod/Sites/share/index.php)
- [app.js](/Users/jarod/Sites/share/app.js)
- [styles.css](/Users/jarod/Sites/share/styles.css)
- [share-lib.php](/Users/jarod/Sites/share/lib/share-lib.php)
- [open.php](/Users/jarod/Sites/share/open.php)
- [public-file.php](/Users/jarod/Sites/share/public-file.php)
- [files.php](/Users/jarod/Sites/share/files.php)
- [upload.php](/Users/jarod/Sites/share/api/upload.php)
- [teacher-login.php](/Users/jarod/Sites/share/api/teacher-login.php)
- [teacher-uploads.php](/Users/jarod/Sites/share/api/teacher-uploads.php)
- [teacher-delete.php](/Users/jarod/Sites/share/api/teacher-delete.php)

### 资源与数据

- [school-badge.png](/Users/jarod/Sites/scratch/src/components/menu-bar/school-badge.png)
- [metadata.json](/Users/jarod/Sites/share/data/metadata.json)
- [/Users/jarod/Sites/share/data/uploads](/Users/jarod/Sites/share/data/uploads)
- [/Users/jarod/Sites/scratch/static/local-assets](/Users/jarod/Sites/scratch/static/local-assets)

---

## 十、今天最有代表性的三个经验

### 1. “能显示”不等于“能导入”

角色库、背景库最开始就是这样：  
缩略图看着已经正常，但点击后并不能真正加入编辑器。

前端里很多问题不是“有没有显示”，而是“完整的数据链有没有通”。  
只修视觉层，往往会留下更难追的假象。

### 2. 状态机打架比单点错误更难查

上传作品打开失败，不是某个接口直接 500，也不是文件坏了，而是两套“项目加载流程”在同时响应同一个状态。  
这种问题的难点在于：每一层单看都像“差不多是对的”，但放在一起就会互相抵消。

### 3. 尽量走原生路径，少走“看起来聪明”的兼容路径

角色库导入最终选择打包成 `.sprite3` 再交给 `vm.addSprite()`，本质上就是回到 Scratch 原生能力，而不是继续维护一个自定义对象注入方案。  
这类选择通常更稳、更容易维护，也更接近未来可持续的代码形态。

---

## 十一、当前结果

到今天为止，这套系统已经基本具备完整可用形态：

- 本地 Scratch 编辑器可访问
- 角色库、背景库、本地资源库基本可用
- `share` 页面支持学生上传与老师管理
- `.sb` / `.sb2` / `.sb3` 可直接从作品列表点开进入编辑器
- 顶部可直接跳转到作品页
- 老师后台密码已更新为 `ghtxx.cn`

从“能编译出来”到“校园环境里能顺手使用”，中间其实差了很多看不见的小修补。  
今天这篇博客，主要就是把这些“看不见但重要”的修补过程留下来。

---

## 后续可继续做的方向

如果后面继续完善，我觉得优先级可以是：

1. 给 `share` 增加项目缩略图或封面预览
2. 给老师后台增加批量下载与批量归档
3. 把作品列表按班级做分组视图
4. 增加项目打开失败时更友好的诊断页
5. 进一步清理 `scratch` 中仍然残留但不影响运行的旧兼容代码

---

**记录时间**：2026年4月23日  
**记录位置**：`/Users/jarod/Sites/briefings/2026-04-23.md`

---

## 2026-05-15 后续维护

5 月 15 日继续做了一轮面向真实课堂使用的收尾维护，重点包括：

1. 修复声音库缩略图错误，避免把 `.wav` / `.mp3` 当图片渲染。
2. 增加 [tts.php](/Users/jarod/Sites/scratch/tts.php) 和 [tts-test.php](/Users/jarod/Sites/scratch/tts-test.php)，让局域网电脑通过服务器代理使用文字朗读。
3. 让 `tts.php` 显式识别 Shadowrocket 系统代理 `127.0.0.1:1082`，并缓存语音合成结果。
4. 修正文字朗读语言代码，例如 `zh-cn` / `zh-tw` 统一转为 `cmn-CN`。
5. 修正 Scratch 扩展真实请求路径，避免编辑器访问 `/scratch/tts.php/synth?...` 导致 `404`。
6. 修复随机角色、随机背景、随机造型按钮，让它们复用已经稳定的本地素材导入流程。

完整记录见：

- [2026-05-15.md](/Users/jarod/Sites/briefings/2026-05-15.md)
- [2026-05-15.html](/Users/jarod/Sites/briefings/2026-05-15.html)
