前端桌面应用图像处理【免费下载链接】jspaint Classic MS Paint, ✨Extras项目地址https://gitcode.com/gh_mirrors/js/jspaint点击查看免费下载导读本指南以 jspaint 仓库中 discord-activity 子目录自带的 Discord Embedded App Starter 为核心完整讲解如何把 jspaint 以 Discord Activity嵌入式应用的形式跑起来包括 vanilla JS 客户端与 Express TypeScript 服务端的架构分工、OAuth2 环境变量的配置与类型安全、本地联调的三步命令以及服务端/api/token、多用户房间等源码级实现细节。读完你将掌握从创建 Discord 应用到本地通过隧道预览完整 Activity 的全流程并能基于现有骨架扩展自己的嵌入式应用。说明discord-activity/README.md是上游 Discord 官方 starter 示例文档本仓库在引入时对其进行了深度改造无 Vite 构建、无独立 client 包、直接静态托管 jspaint 本体。本文以该文档为骨架结合仓库内实际源码补充实现细节。一、Starter 定位与整体架构该 starter 的目标是以最小成本让嵌入式应用在 Discord 中跑起来同时把客户端与服务端拆成易于替换的模块。文档明确了两端的技术选型客户端front-endvanilla JS原生 JavaScript不依赖框架服务端back-endExpress TypeScript。在本仓库中这一结构被进一步简化。查看 discord-activity 目录可以看到 pnpm workspace 布局discord-activity/ ├── package.json # workspace 根定义 dev / tunnel 脚本 ├── package-lock.json └── packages/ └── server/ ├── environment.d.ts # 服务端环境变量类型声明 ├── package.json # Express 服务端及 dev/build 脚本 ├── tsconfig.json └── src/ ├── app.ts # Express 应用入口 ├── utils.ts # fetchAndRetry429 限流重试 └── shared/ └── hello.ts # 共享目录示例值得注意的差异点原版 starter 拥有packages/client与packages/server两个包而本仓库服务端源码中的注释明确写道Im hacking this to work without Vite, without a build step, and without a client folder / monorepo structure.见 app.ts也就是说jspaint 的改编版去掉了独立 client 包和 Vite 构建步骤改用 Express 直接以静态文件方式托管 jspaint 的源码目录clientSourcePath向上回溯指向仓库根目录详见下文“服务端源码解析”。二、前置准备创建 Discord 应用并获取 OAuth2 凭据按照 Discord 官方文档Building an Activity的 Step 1 指引先在 Discord Developer Portal 创建应用。核心要点是在 Developer Portal 新建 Application将应用配置为支持 Activity / Embedded App SDK 所需的能力找到应用的OAuth2 Client ID与Client Secret这是后文.env中两个必需变量的来源。完成上述配置后才进入本仓库的代码配置环节。三、环境变量配置与类型安全原文档要求在discord-activity目录下创建.env文件并填入两个 OAuth2 变量VITE_CLIENT_ID123456789012345678 CLIENT_SECRETabcdefghijklmnopqrstuvwxyzabcdefVITE_CLIENT_IDDiscord 应用的 Client ID数字串CLIENT_SECRETDiscord 应用的 Client Secret密钥串仅服务端使用切勿暴露到客户端。服务端加载.env的位置见 app.tsdotenv.config({ path: ../../.env });该路径相对于packages/server目录向上两级恰好指向discord-activity/.env与文档要求的位置一致。添加新环境变量的标准三步流程原文档规定新增环境变量必须同步完成三步以保证消费侧的类型安全在.env中添加键值对原版在packages/client/src/vite-env.d.ts中添加键名在 discord-activity/packages/server/environment.d.ts 中添加键名。由于本仓库改编版没有 client 包客户端侧的类型声明文件在本仓库中并不存在服务端侧的类型声明位于packages/server/environment.d.ts。从源码看该文件为process.env声明了如下类型契约declare global { namespace NodeJS { interface ProcessEnv { VITE_CLIENT_ID: string; CLIENT_SECRET: string; NODE_ENV: development | production; PORT?: string; PWD: string; } } }其中PORT为可选变量对应服务端监听端口默认 1999见下文。服务端 tsconfig 通过include显式纳入了该类型声明文件见 tsconfig.json确保process.env.VITE_CLIENT_ID等访问在编译期即被类型检查覆盖。四、服务端源码解析Express TypeScript4.1 端口与启动app.ts 定义端口const port: number Number(process.env.PORT) || 1999;即默认监听1999端口可通过.env中的PORT覆盖。启动时打印App is listening on port 1999 !。4.2 客户端代码的 CLIENT_ID 注入monkey-patching服务端并不直接修改 jspaint 源码文件而是运行时替换占位符。源码关键逻辑const clientId process.env.VITE_CLIENT_ID; const clientIdNeedle $$$$$CLIENT_ID$$$$$; // same length as the client ID, just in case const urlPathForPatching /src/discord-activity-client.js; const fsPathForPatching path.join(clientSourcePath, urlPathForPatching);启动时校验 src/discord-activity-client.js 存在然后通过路由动态响应app.get(urlPathForPatching, (req, res) { fs.readFile(fsPathForPatching, utf8, (err, data) { ... res.send(data.replace(clientIdNeedle, clientId)); }); });即访问/src/discord-activity-client.js时将文件内的$$$$$CLIENT_ID$$$$$占位符实时替换为.env中的真实 Client ID不写缓存便于开发期热更新。对应地客户端文件 src/discord-activity-client.js 中声明const CLIENT_ID $$$$$CLIENT_ID$$$$$; // monkey-patched in by the server const APPLICATION_ID CLIENT_ID; // seems to be the same const { DiscordSDK } Discord; const discordSdk new DiscordSDK(CLIENT_ID); await discordSdk.ready();SDK 本体来自仓库内置的 lib/discord-embedded-app-sdk-v1.2.0-bundled-with-skypack.js。4.3 静态托管与安全过滤服务端通过express.static(clientSourcePath)直接托管整个 jspaint 仓库根目录app.ts从而免去构建步骤。由于会把.git、.env等敏感文件也暴露出来服务端在静态中间件之前加了拦截路由app.use((req, res, next) { // Must be case-insensitive for Windows FS! if (req.path.match(/\.(git|history|env)/i)) { res.status(403).send(Forbidden); return; } next(); });任何路径中包含.git、.history、.env大小写不敏感兼容 Windows 文件系统的请求都会返回 403。源码注释也坦诚地标记了 TODO更稳妥的方案是维护一份明确的“允许访问文件白名单”。4.4 OAuth2 Token 端点/api/token客户端拿到 Discord 返回的code后会向服务端/api/token发起 POST由服务端代为换取access_tokenapp.tsapp.post(/api/token, async (req: Request, res: Response) { const response await fetchAndRetry(https://discord.com/api/oauth2/token, { method: POST, headers: { Content-Type: application/x-www-form-urlencoded }, body: new URLSearchParams({ client_id: process.env.VITE_CLIENT_ID, client_secret: process.env.CLIENT_SECRET, grant_type: authorization_code, code: req.body.code, }), }); const { access_token } await response.json(); res.send({ access_token }); });将client_secret放在服务端、由服务端统一请求 Discord API是避免密钥泄漏到客户端的标准做法。这里还调用了自定义的fetchAndRetry工具。4.5 fetchAndRetry限流与失败重试utils.ts 实现了一个带重试语义的 fetch 封装核心逻辑为请求返回429Discord 限流时读取响应头retry_after秒sleep相应时长后重试最多重试 3 次其他请求异常网络失败等时等待 1 秒后重试同样最多 3 次重试次数耗尽后抛出异常。if (response.status 429 nRetries 0) { const retryAfter Number(response.headers.get(retry_after)); ... await sleep(retryAfter * 1000); return await fetchAndRetry(input, init, nRetries - 1); }这套逻辑直接对应 Discord 官方 Rate Limits 文档描述的retry_after语义源码注释也给出了对应出处说明。4.6 简单多用户房间Roomsapp.ts中还有一组用于“Simple multiplayer image editing”的实验性端点app.tsconst rooms: { [key: string]: string } {}; app.get(/api/rooms/:roomId/data, (req, res) { const { roomId } req.params; res.send(rooms[roomId]); }); app.put(/api/rooms/:roomId/data, bodyParser.text({ type: */* }), (req, res) { const { roomId } req.params; const image req.body; rooms[roomId] image; res.send({ success: true }); });它用内存对象保存每个 roomId 对应的图片数据以 data URI 字符串存储提供 GET/PUT 两个接口客户端侧由 sessions.js 中的RESTSession配合使用监听画布变更后 debounce 100ms 将main_canvas.toDataURL()PUT 到服务端同时每 1000ms 轮询 GET 拉取远端数据实现轻量级的多人协作注释注明这是单向同步、服务端内存态、无鉴权属实验性质。五、jspaint 客户端侧的 Discord 集成服务端之外jspaint 本体的集成逻辑也值得关注它验证了上述 starter 的端到端链路。5.1 环境识别helpers.js 中通过 URL 参数识别是否处于 Discord 嵌入式环境export const is_discord_embed query_params.get(frame_id) ! null;Discord 嵌入式应用 SDK 要求 URL 携带frame_id参数SDK 内置逻辑亦会校验该参数。同时 functions.js 在更新 URL 时特意保留 query string注释写明“The Discord Activity needs to preserve the query string, so its exempt from this”。5.2 会话引导与 OAuth 全流程在 sessions.js 中当is_discord_embed为真时动态导入客户端模块并执行const { discordSdk, newAuth, guildMember, handleExternalLinks, discordActivitySystemHooks } await import(./discord-activity-client.js); handleExternalLinks(); change_url_param(session, discord-activity-${discordSdk.instanceId}); Object.assign(window.systemHooks, discordActivitySystemHooks);即以 Discord Activity 实例 ID 作为 jspaint 的 session ID 启动会话并把 Discord 特定的 systemHooks 注入全局。客户端模块 discord-activity-client.js 内部完成标准 OAuth 授权链路discordSdk.commands.authorize(...)获取授权codescope 至少需要一个仓库默认使用identify并注释了guilds.members.read、messages.read等可选 scope 的用途将codePOST 到服务端/api/token换取access_token用access_token调用discordSdk.commands.authenticate(...)完成身份认证可选拉取/discord/api/users/me/guilds/${discordSdk.guildId}/member获取服务器内昵称与头像。5.3 systemHooks把保存转化为 Discord 分享discordActivitySystemHooks重写了 jspaint 的保存行为discord-activity-client.js。由于嵌入式环境无法使用showSaveFilePicker会抛SecurityError: Cross origin sub frames arent allowed to show a file picker、blob:URL 和数据 URI 也均无法触发下载实现改为弹出 jspaint 自带的“保存为”对话框选择格式 →getBlob()生成图片 → 调用shareImage()const body new FormData(); body.append(file, imageFile); const attachmentResponse await fetch(${DISCORD_API_BASE}/applications/${APPLICATION_ID}/attachment, { method: POST, headers: { Authorization: Bearer ${access_token} }, body, }); const mediaUrl attachmentJson.attachment.url; await discordSdk.commands.openShareMomentDialog({ mediaUrl });即通过 Discord 的 attachments API 上传图片获得临时 CDN URL再调用openShareMomentDialog弹出 Discord 内置分享对话框。源码注释记录了这段从blob:URL、data URI 一路试到 attachment API 的探索过程对理解嵌入式环境的限制很有价值。六、本地运行三步启动原文档要求在discord-activity目录下依次执行三条命令pnpm install # only need to run this the first time pnpm dev pnpm tunnel # from another terminal6.1 命令背后的脚本从根 package.json 可以看到dev: pnpm run --filter \./packages/**\ --parallel dev, tunnel: cloudflared tunnel --url http://localhost:3000pnpm dev通过 pnpm--filter匹配packages/**下所有包并--parallel并行执行各包的dev脚本服务端包 packages/server/package.json 的dev为nodemon --watch src -e ts,ejs --exec $npm_execpath start即监听src目录的.ts/.ejs变更自动重启start会先执行buildrimraf disttsc编译再运行node ./dist/app.js。6.2 隧道把本地服务暴露给 DiscordDiscord 的嵌入式应用要求 Discord 客户端能够访问你的应用 URL因此官方推荐配合隧道工具如 cloudflared做本地联调。pnpm tunnel即启动 cloudflared 将本地端口转发为公网 HTTPS 临时地址随后在 Discord 开发者后台的 Activity URL 配置中填入该地址。实战注意源码层面的坑tunnel脚本写死转发到http://localhost:3000而服务端默认监听的是1999端口Number(process.env.PORT) || 1999。二者不一致时隧道将无法连通建议二选一在.env中设置PORT3000或把 tunnel 命令的端口改为 1999。6.3 首次启动顺序建议创建并配置好 Discord 应用获取 Client ID / Secret在discord-activity/.env写入VITE_CLIENT_ID与CLIENT_SECRET首次运行pnpm install安装依赖终端 A 执行pnpm dev启动服务端终端 B 执行pnpm tunnel建立公网隧道将输出的 HTTPS 地址填入 Discord Activity URL在 Discord 客户端中打开该 Activity 验证联调。七、小结原文档的核心要点——vanilla JS 客户端 Express/TypeScript 服务端、OAuth2 环境变量、三步本地启动——在本仓库中均有对应实现且被 jspaint 的实际集成进一步验证环境变量经 environment.d.ts 获得类型安全服务端 app.ts 承担静态托管、CLIENT_ID 注入、OAuth2 Token 交换与多用户房间utils.ts 提供符合 Discord 限流语义的重试封装客户端 discord-activity-client.js 与 sessions.js 完成环境识别、授权认证与 systemHooks 注入。如果你要基于这个骨架开发自己的 Discord Activity建议按原文档的初衷“swap in pieces”保留.env三步配置与 tunnel 工作流替换服务端路由与客户端逻辑即可。若要落地到生产还需关注源码注释中坦承的已知边界——房间数据为内存态且无鉴权、静态托管采用黑名单式过滤而非白名单、fetchAndRetry仅实现简单退避。赞分享前端桌面应用图像处理【免费下载链接】jspaint Classic MS Paint, ✨Extras项目地址https://gitcode.com/gh_mirrors/js/jspaint点击查看免费下载相关推荐OpenCode Stats 统计服务解构app / core / server 三层架构与本地运行实操OpenCode Stats 统计服务解构app / core / server 三层架构与本地运行实操 Stats 是 opencode 仓库中一个独立于人工智能AI 应用AI Agent代码智能体CLI开发者工具Onlook 开源项目的 Supabase 后端栈架构解析与本地运行实战指南Onlook 开源项目的 Supabase 后端栈架构解析与本地运行实战指南 导读 Onlook 是一款开源的 AI First 可视化设计工具用于以可视化前端AI 应用开发工具Flower Agent 本地实战使用本地 SuperLink 在开发机上运行 AgentAppFlower Agent 本地实战使用本地 SuperLink 在开发机上运行 AgentApp 本文讲解 Flower 官方文档 Run an AgentA人工智能联邦学习机器学习深度学习上一篇FSPagerView自定义布局教程轻松实现瀑布流与网格滑动视图下一篇LLM-Graph-Builder终极指南5分钟从零构建AI知识图谱创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考