- JavaScript 90.7%
- CSS 9.2%
| packages/cli | ||
| scripts | ||
| server | ||
| shared | ||
| test | ||
| web | ||
| .gitignore | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| vite.config.js | ||
rCodenow Gallery · rCodenow 画廊
rCodenow 社区:作品、扩展与插件的共享平台与交互社区。 前端用 GTK4 / libadwaita 的视觉语言(
@gtk-js/adwaita),后端是 Express + Node 内置的 SQLite。配套命令行工具rcodenow-gallery。
- 包名:
rcodenow-gallery - 需要 Node ≥ 22.5(
node:sqlite在 22 上是 experimental,会有一条 warning;24 起转正) - 许可:GPL-3.0
快速开始
npm install
npm run build # 构建前端到 dist/
GALLERY_SECURE_COOKIE=0 npm start
# 打开 http://localhost:8080/Gallery/
本地开发(前后端一起起,Vite 代理 /Gallery 与 WebSocket 到 8080):
npm run dev # 前端 5173,后端 8080
npm run seed # 另开一个终端,灌一批演示数据
GALLERY_SECURE_COOKIE=0 只用于本地 HTTP 调试 —— 生产必须走 HTTPS,让会话
Cookie 带上 Secure。
GALLERY_ALLOWED_ORIGINS=origin1,origin2 放行跨源访问(默认关闭)。编辑器与
画廊同源时不需要;桌面端或本地开发把编辑器跑在别的源上时用它放行,编辑器会
用登录时签发的 Bearer Token 调接口。
编辑器本地开发时,在 rCodenow-GUI 里用 GALLERY_PROXY_TARGET=http://127.0.0.1:8601 npm start 把 /Gallery 代理到本服务端,两边保持同源。
目录结构
extension-gallery/
├── shared/ # 唯一真源:清单、依赖分析、semver、类型元数据
├── server/ # Express + node:sqlite
│ ├── db.js # 连接与迁移
│ ├── migrations/ # SQL 迁移
│ ├── auth/ # 密码、会话、API Token、device code 授权
│ ├── routes/ # v2 接口 + 编辑器用的仓库列表 + 旧路径重定向
│ ├── services/ # 文件落盘、zip、sb3 解析、发布、序列化
│ └── realtime/ # WebSocket
├── web/src/ # 前端(React + gtk-js/adwaita)
│ ├── lib/ # 路由、接口客户端、全局状态、高亮、格式化
│ ├── components/ # 展示框、分栏外壳、通用控件、代码/文档查看器、评论
│ └── pages/ # 发现 / 全部 / 项目 / 消息 / 我的 / 创作 / 上传 / 关于
├── packages/cli/ # npm 包 rcodenow-gallery
├── test/ # 服务端与 shared 的 node:test
└── scripts/ # 端到端验证、演示数据、开发脚本
三类项目
| 类型 | 产物 | 项目页的「代码」 | 说明 |
|---|---|---|---|
| 作品(work) | .sb3 |
跳转到 rCodenow 创作页 | 概览上方嵌一个可运行的 rCodenow 窗口 |
| 扩展(extension) | 单文件 .js |
源码树 + Prism 高亮 | 装进编辑器的扩展库 |
| 插件(plugin) | 单文件 ESM .js |
源码树 + Prism 高亮 | 改造编辑器界面与工作流 |
元数据:唯一真源
rcodenow-gallery.json 是元数据的唯一来源。网页端上传表单生成它,命令行读它,
服务端只做形状校验后原样存进 project_versions.manifest_json。
必填:名称、类型、语义化版本、许可证(SPDX)、支持的 rCodenow 版本范围、简介 可选:详细描述、标签、仓库、主页、封面、展示色、示例、文档
依赖不需要填。发布时服务端用 shared/deps.js 分析出来 ——
CLI 与服务端 import 的是同一份代码,所以「上传前看到的」和「上传后看到的」必然一致:
- 作品:读 sb3 里的
modconfigs.json依赖列表,并扫描project.json里对画廊扩展的引用 - 扩展 / 插件:扫描产物中的画廊项目地址,加上清单里显式声明的
dependencies
依赖到一个还没发布的东西也不会被吞掉 —— 它会以「未解析」的形式列出来。
接口
全部在 /Gallery/api/v2/ 下。浏览器用会话 Cookie,命令行与编辑器用
Authorization: Bearer <token>。
| 分组 | 路径 |
|---|---|
| 账号 | POST /auth/register、/auth/login、/auth/logout、GET /auth/me、POST /auth/password、/auth/password/reset |
| Token | GET/POST /auth/tokens、DELETE /auth/tokens/:id |
| 浏览器授权 | POST /auth/device/code、GET /auth/device/:userCode、POST /auth/device/approve、POST /auth/device/token、POST /auth/authorize |
| 用户 | GET /users/:username、PATCH /users/:username、POST /users/:username/avatar、GET /users/:username/projects?scope=assets|stars、GET /users/search |
| 项目 | GET /projects、GET /projects/discover、GET /projects/:owner/:slug、POST /projects、PATCH、DELETE、POST /transfer、POST/DELETE /star、POST /share、DELETE /share/:token、POST /cover |
| 版本与文件 | GET/POST /projects/:owner/:slug/versions、GET .../files?kind=、GET .../raw?path=、GET .../download?path=、GET .../bundle、GET .../readme、GET .../project.sb3、GET .../samples/:name、GET .../cover、GET .../icon |
| 依赖 | GET /projects/:owner/:slug/dependencies |
| 评论 | GET/POST /projects/:owner/:slug/comments、PATCH/DELETE .../comments/:id、GET .../comments/count |
| 消息 | GET /chat/channels、POST /chat/channels、POST /chat/channels/direct、GET/POST /chat/channels/:id/messages、POST /chat/attachments |
| 编辑器 | GET /repos/extensions.json、GET /repos/mods.json |
| 实时 | WS /Gallery/api/v2/ws |
给 rCodenow 编辑器的两个文件
/Gallery/api/v2/repos/extensions.json 与 mods.json 是编辑器的输入契约,字段
形状与旧版一致(id / name / compatibility / extensionURL / modURL /
dependencies …),只是数据来源从 JSON 文件换成了数据库。颜色永远有值、封面可能
为 null,编辑器拿到 null 会用颜色生成占位图标。
旧路径
v1 的对外路径(/extensionList.json、/extensions/<id>.js、/samples/<x>.sb3、
/images/<id>.png、/mods/<id>.sb3)全部 302 重定向 到 v2 对应地址,而不是
404。理由很实际:已发布插件把 meta.namespace 硬编码成了
https://rc.0832.ink/Gallery/extensions/<id>.js——那是它们的部署契约,服务器
单方面换成 v2 会让这些插件的自动更新静默失效。
旧上传接口 /api/upload、/api/upload-mod 返回 410 并说明换成什么。
权限
- 密码:scrypt;会话与 API Token:sha256,库里没有明文
- 浏览器会话:HttpOnly Cookie,
SameSite=Lax(编辑器与画廊同源,弹窗授权不受第三方 Cookie 限制) - API Token:明文只在签发时返回一次,
packages/cli存在~/.config/rcodenow-gallery/config.json(0600) - 私有项目:只有所有者与持有未吊销分享链接的人能看到;对外一律表现为「不存在」
- 忘记密码:已登录 + 注册邮箱匹配即可重置(会同时吊销所有 Token)。没有邮件服务, 所以没登记邮箱的账号需要联系管理员
命令行工具
npm i -g rcodenow-gallery
rcodenow-gallery login
rcodenow-gallery init extension
npm install && npm run build
rcodenow-gallery upload
测试与验证
npm test # 服务端 + shared + CLI(node:test,39 项)
npm run test:e2e # 无头 Chrome 端到端(113 项,含截图)
npm run test:e2e:upload # 命令行上传端到端(不需要 Chrome)
npm test 跑的是真实链路:真的开 SQLite、真的收 multipart、真的解析 sb3,
不是在打桩的接口上数 200。CLI 那一组会真的构建模板工程、真的把产物传上去。
npm run test:e2e 会自己起一个服务端(临时数据目录)、灌演示数据、用无头 Chrome
把画廊点一遍:发现页 → 全部页 → 分类与标签筛选 → 项目页五个页签 → 评论区 → 收藏 →
登录注册 → 创作 → 上传 → 消息 → 我的三个分区 → 关于 → 未登录状态 → 404 → 窄屏布局,
共 113 项断言,截图落在 data/e2e-screenshots/。
其中有几项专门盯着「整页变空白 / 元素溢出视口」这类问题,每个页面都过一遍:
没有元素横向溢出视口:遍历 DOM,凡是有元素超出视口且祖先不可横向滚动就失败。 库的 reset 把整页设成content-box(贴近 GTK 的盒模型),而我们的布局大量用 「宽度 + 内边距」,很容易一个按钮比容器宽出一截、把内容面板顶到视口之外。未登录也能打开「…」页:/auth/me回来之前user还是null,页面如果在首帧就 解引用它(比如写在 hook 的依赖数组里),React 会卸载整棵树 —— 于是所有页面都 变成一片空白。逐页确认一次,顺便保证有ErrorBoundary兜底。- 侧栏的宽屏停靠 / 收起 / 展开、点侧栏入口不误收起,以及 320px 下的单列卡片。
npm run test:e2e:upload 只覆盖命令行上传那条链路,方便在没有浏览器的环境里验证。
已知限制
@gtk-js/adwaita0.1.5 没有AdwViewStack/AdwTabView/AdwTreeListView/ 文件选择器:标签页用受控AdwViewSwitcher+GtkStack,文件树自绘,目录上传用<input webkitdirectory>- 同版本的
AdwOverlaySplitView有个语义 bug:展开状态下show-sidebar=false会被 忽略(collapsed ? showSidebar : true),侧栏收不起来。应用外壳改用web/src/components/SplitView.jsx,DOM 结构与 类名保持一致,只是把这条语义补上并给内容面板加了min-width: 0 - 同版本的
GtkButton的iconName只渲染一个空的<span class="gtk-image"/>, 不查图标表。带图标的按钮因此走AdwButtonContent;busy属性库不支持,由components/common.jsx的Button自己接(转圈 + 禁用) - 库的 reset 把整页设成
content-box,而我们的布局几乎每处都是「宽度 + 内边距」。styles/base.css用一条「类名里没有gtk-/adw-前缀就改回border-box」的 规则统一处理 —— 新增组件自动被覆盖,不用回来改清单 - 写操作全部串行在单进程里(
node:sqlite同步 API)。要横向扩到多实例得换 Postgres - 忘记旧密码的自助重置需要已登录态
- 编辑器侧的对接(
embed-project.html播放入口、登录、上传作品、设封面)在 rCodenow-GUI 里实现;本仓库只提供接口与嵌入协议