最高优先级协作约束:F:\Project\flutter\playmesh 仅允许只读检查与审计,禁止写入、修改、格式化、生成文件、覆盖构建产物或提交;所有实现改动只能发生在当前 playmesh-web 项目中。
本项目是独立的新项目,但 SDK 兼容以固定 Playmesh 版本的生成版 .js、.d.ts、源实现和测试共同作为语义事实源。生产 SDK 由逐字节校验的生成版 App/Main SDK 与本项目独立的 Web 传输/宿主适配层确定性组装;除已批准的好友频道替代 LAN 发现来源,以及浏览器无法等价提供的原生能力稳定返回 platform_unsupported 外,不允许自行重写、近似或静默改变 SDK 语义。任何无法精确同步的差异必须停止兼容声明并交由开发者决定。登录验证码的生成/答案校验逻辑与对应浏览器组件则直接复制自附带的 go-server;外围适配当前 HTTP、登录、资源注册,并把原模块的进程内短期状态替换为 Session Redis 中带 TTL、可原子消费的共享状态。复制来源与许可证保存在 licenses/。
核心访问边界:公网未登录访问仅限登录和有效分享链接的游客加入流程。分享链接是游客唯一入口,并在当前 Authority 房间会话内可重复使用;单次消费的是每次加入的 intent、Launch Grant 和信令票据。账号 Player 的 SDK 标识由后端认证会话投影为 u_<user_id>;游客邀请 Shell 在 Controller localStorage 生成并保存公开 UUID,后端把它与独立 HttpOnly 游客会话绑定后投影为 p_<uuid>。浏览器已确认游客身份后可在同一房间恢复同一 Guest/Player 而不重复显示身份选择;同一窗口打开该应用的另一条有效邀请时会轮换 Runtime Cookie/Context、撤销旧成员绑定,并在新房间沿用相同公开 player.id 字符串但创建全新的秘密 Guest/Runtime 身份。此前采用账号身份但登录已失效时必须重新选择。Guest Runtime Session 只能以 Player 身份访问邀请固定 app_id 的固定 Runtime Host 根资源及 /api/app/{app_id}/** 允许列表,并继续受单个房间、制品修订和接口允许列表约束。player.id 不具备授权能力,该 Session 对其他路径无效。所有应用 API 与资源逐请求鉴权。
阶段 2 已建立开放式内置系统伪应用机制,而不是硬编码两个应用分支。所有 system_facade 通过统一注册表声明稳定 ID、路由/模板入口和 entitlement 分配策略;它们都不是上传包,没有普通 Runtime,不进入市场,不能上传、覆盖、删除或导出。保留 ID app_manager 的“应用管理”、family_library 的“家庭库”、friends 的“好友”和 settings 的“设置”都是“所有账号必有”的系统应用,包括管理员在内的每个账号都取得不可撤销的 entitlement。应用管理提供可搜索的“我的应用 / 应用市场”,仅通过右键菜单获取、打开、管理桌面快捷方式或移除普通应用;家庭库管理创建/加入的家庭、短时邀请、成员、共享应用和独立家庭桌面;好友应用复用通用窗口与任务栏运行图标,提供好友及家庭固定联系人、服务端同步的最近会话、私聊、在线状态和公开游戏加入,关闭界面不影响桌面 SSE 接收消息。聊天首版支持文本、引用、三分钟撤回、受保护图片、随程序发布的第三方中文表情选择器、10 条游标分页和当前双方历史查询;最近入口可单独移除而不删除关系或消息。明确移除普通应用会撤销本人使用权;若仍有活动家庭共享则保留 Authority Bucket、SQLite 和文件,否则清理该用户/应用的服务端数据。保留 ID admin 的平台管理应用采用“按 entitlement 授予”策略,使用权本身就是完整平台管理授权,并承载审计化的家庭创建者转移恢复入口。后续内置应用可以按同一机制注册,无需给市场、导出、删除或鉴权增加按 ID 分支。初始 MySQL 建库 SQL直接写入 admin/admin 内置账号的密码哈希及其 app_manager、family_library、friends、settings、admin entitlement,不增加强制改密登录分支。
家庭共享不会复制应用包或应用数据。家庭 Runtime 保留实际成员作为操作者,并把 Authority Bucket、SQLite 和 Main Bucket 文件投影到共享者原有的 data_owner_user_id + app_id 目录;playmesh.app.storage 仍按实际成员隔离。当前文件型共享存储的锁和 SQLite 写门只在单个 Go 进程内有效,因此家庭共享首版只支持单 Go 服务实例;多实例部署必须先实现按 data_owner_user_id + app_id 的稳定归属或集中式存储。
Web 桌面框架的服务端主动通知只使用一套 SSE 总线,桌面控制面不另建 WebSocket。每个已登录 Web 桌面顶层窗口只建立一条桌面 SSE 连接;同一账号可以打开多个桌面窗口,所有桌面用户级事件必须扇出到该用户当前全部活动桌面窗口,桌面模块通过事件类型和可选 echo_id 在统一客户端分发器中监听。应用 iframe、Playmesh SDK Web 适配、Authority/Player、应用存储、应用 WebSocket 和 WebRTC 信令不连接或复用桌面 SSE,而是在隔离 Runtime 路由树中使用独立的应用运行时处理链;控制 API 使用 /api/app/{app_id}/**,源 SDK 的文件 Bucket 保持 /bucket/**。Redis 是登录状态以及多人房间、成员、好友房间发布、分享邀请、邀请意图和游客 Runtime Session 等临时运行态的唯一可信来源,并保存图形验证码挑战/一次性验证凭证、邮箱换绑验证码与流程状态、平台共享缓存及桌面 SSE 的多实例发布/订阅和有界短期重放;固定 go-core 的 Binary/RPC 审核、队列和请求关联状态则保留在服务实例内。同一房间的 Core、Binary 和 RPC Stream 使用固定路径,并统一携带单个 room_id 查询参数供入口代理做一致性哈希;HTTP Bucket 上传则由适配层自动携带 X-Playmesh-Room-ID 请求头。后端仍以 HttpOnly Runtime Context 鉴权并严格比对路由提示。不能保证粘性路由的部署必须使用单实例。这些运行态不写入 MySQL,Redis 或服务进程丢失后由用户重新登录或打开应用。MySQL 8 只保存账号、应用、entitlement、好友关系和审计等持久业务事实。
WebRTC 的控制面和数据面都由 Playmesh Web 提供:应用使用本服务签发的一次性 WSS 信令端点,Go 进程同时内置 IPv4/IPv6 STUN/TURN UDP/TCP 服务并签发按会话隔离的短期 TURN REST 凭据。开发环境默认枚举并绑定验证所有可用的本地单播地址;生产环境可用稳定域名作为浏览器侧 ICE Host,但仍须显式配置非回环的公网中继 IP 与同地址族本机绑定 IP,并开放 TURN 监听端口及完整中继端口范围。只开放 Controller 的 HTTP/HTTPS 端口无法承载 TURN。
所有上传应用均视为不可信代码。.env 的 PLAYMESH_RUNTIME_ORIGIN 是普通应用唯一的 Runtime Origin 模板,不由管理应用、MySQL、应用清单或请求参数配置;整个值必须有且只有一个独占最左侧 DNS 标签的 *,进程启动时完成模板语法校验,然后用内部 app_id 替换该标签得到固定应用 Origin。程序不配置或推导全局控制面 Origin,启动时不做 Runtime/控制面冲突校验、DNS 探测或可达性探测;只有用户实际启动普通应用时,浏览器才比较 runtimeURL.hostname 与 window.location.hostname,服务端同时比较生成的 Runtime hostname 与受保护 Launch POST Origin 的 hostname。只要 hostname 相同就拒绝启动,即使协议或端口不同;Runtime 必须使用与当前桌面不同的 hostname。
导入后的不可变制品使用 .env 的 PLAYMESH_ARTIFACT_ROOT 作为部署存储根。数据库只保存制品修订自己的相对 extracted_package_path,应用目录记录只保存当前制品指针,不保存服务器绝对路径。平台内部 applications.id 是主键且全局唯一的 app_id,上传应用必须是小写 DNS 单标签;包身份另存为独立、全局唯一的 applications.playmesh_id 并与 manifest.id 匹配。管理页选择 ZIP 后只在浏览器读取根 main.json,取 manifest.id 最后一个 . 分段预填 app_id;该值只是可编辑建议,用户手动修改后不再被同一文件的自动建议覆盖,服务端仍对最终提交值独立校验。每个应用固定使用 Runtime Origin 模板把最左侧 * 替换为 <app_id> 后的 Host;例如 http://*.game.localhost:18080 生成 http://demo.game.localhost:18080。物理 app/index.html 映射为该 Origin 的 /index.html。Controller 是唯一用户导航入口,Runtime 只在受控 iframe 内运行;服务端一旦识别 Runtime Host 就必须在独立路由树内终止处理,不得回落到 Controller 的登录、桌面、用户资料、平台 API 或静态资源。同一浏览器账号 Session 对同一应用只允许一个窗口:受信任 Shell 先取得按 app_id 命名的浏览器排他 Web Lock,Redis 再维持唯一活动 Runtime Context;异常关闭会自动释放浏览器锁,同浏览器可用既有 Runtime Cookie 原子接管其遗留 Context。完整决策见 ADR 0001。上传、校验、安全解压、不可变发布和原子切换均由阶段 2 导入器实现。
账号访问 Token、账号刷新 Token、Runtime Token 和 Launch Grant 都禁止写入 localStorage、sessionStorage 或 IndexedDB。Controller localStorage 中的游客 UUID 是唯一例外:它只是 SDK 公开标识输入,不是凭证,不能单独恢复或授权 Guest Runtime。Controller 使用 Redis 权威的两枚 host-only、Secure、HttpOnly Cookie:短期访问令牌逐请求授权,长期刷新令牌只由统一认证层在访问失效时签发新访问令牌,业务 Handler 不接受刷新令牌。两者固定期限由 PLAYMESH_SESSION_ACCESS_TOKEN_TTL 和 PLAYMESH_SESSION_REFRESH_TOKEN_TTL 配置,默认 15m 与 720h。每个固定应用 Runtime Host 使用独立的 __Host-pm_runtime、host-only、Secure、HttpOnly、SameSite=None、Partitioned Runtime Cookie,禁止不可信应用用父域或其他 Path 的同名 Cookie 覆盖,并结合 Redis Runtime Context 授权具体应用、房间和角色。仅当非生产环境显式设置 PLAYMESH_COOKIE_SECURE=false 时,本地 HTTP Runtime 在严格 Cookie 之外再签发同值的 pm_runtime_dev、host-only、HttpOnly、SameSite=Lax 回退 Cookie,由服务端接受其中任一有效值,以兼容浏览器对 localhost 子域 Secure/Partitioned Cookie 的不同处理;Controller 与 Runtime 必须保持同一 schemeful site(如 control.localhost 与 runtime.control.localhost)。回退项不声明 Secure/Partitioned,也不具备 __Host- 的浏览器强制防遮蔽保证,只能用于本地联调。生产只签发并接受严格 Cookie。应用 iframe 使用 allow-scripts allow-same-origin allow-modals,因此只能访问本应用固定 Origin 的 localStorage/IndexedDB/Cache;它仍与 Controller 和其他应用跨 Origin,且不能读取 HttpOnly 凭证。原生 Web Storage 属于当前浏览器配置文件和 app_id,不按登录账号隔离,只能作为设备级应用缓存;用户私有权威数据必须使用后续按 user_id + app_id 隔离的 SDK/服务端存储。Service Worker 明确禁止,普通 Worker 保留。iframe 允许原生模态但不允许 popup 或顶层导航;系统伪应用只能经桌面 Shell 同源校验后打开本站新标签页。桌面窗口、永久应用入口和独立界面只能通过统一 URL 构造器使用进程加载的 Runtime Origin 模板;修改 .env 后必须重启程序才生效。
- 功能与架构要求
- 技术可行性与架构边界
- 实施架构基线
- 三阶段开发流程与 v1 归档
- MySQL 手工部署与开发期 SQL 管理
- Redis 登录状态、缓存与桌面 SSE 分发
- 开发环境与 Go 工具链
- 当前直接依赖与许可证
- 使用 MySQL 8.x 创建空数据库,并由开发者手工执行
sql/init.sql。应用进程不会执行建表、增量或初始数据补种。 - 准备 Redis。验证码挑战和一次性验证凭证与账号 Session 一起使用不可静默淘汰的 Session Redis;生产环境应把它与可淘汰缓存/桌面事件实例分开。开发环境可以显式复用一个实例并接受同一故障域。
- 复制
.env.example为不会提交的.env,填写两个独立的 32 字节以上随机密钥、MySQL/Redis 地址、恰含一个最左侧*DNS 标签的PLAYMESH_RUNTIME_ORIGIN模板和PLAYMESH_ARTIFACT_ROOT。生产必须设置PLAYMESH_COOKIE_SECURE=true且不能启用桌面测试应用。该布尔值同时声明浏览器面对的 Controller 使用 HTTPS,使 TLS 在反向代理终止时的同源校验仍能得到正确 scheme;代理必须保留原始Host,后端不会信任X-Forwarded-Proto。只有真实代理网段才能写入PLAYMESH_TRUSTED_PROXY_CIDRS,应用只在直连方可信时从右向左解析其追加的X-Forwarded-For;空值会忽略所有转发来源头。项目不读取全局控制面 Origin;Runtime 模板语法在启动期校验,生成 Host 与当前请求来源的 hostname 冲突只在实际 Launch 时比较。 - 使用固定 Go
1.26.2运行:
& 'D:\KaiFaTool\runtime\go\go-1.26.2\bin\go.exe' run ./cmd/playmesh-web --env-file .env部署时只配置一个 Runtime Origin 模板;每个内部 app_id 替换最左侧唯一 * 标签得到固定应用 Host,实际桌面 Origin 取当前受保护启动请求。Runtime 与桌面必须使用不同 hostname,同 hostname 仅改变端口或协议仍拒绝启动。生产 DNS、TLS 与代理必须覆盖模板同形的最左侧通配域、保留浏览器请求的原始 Host,并只把流量转发到不对外暴露的后端监听;PLAYMESH_COOKIE_SECURE=true 表示浏览器侧 scheme 为 HTTPS,不能靠代理头临时切换。内部健康检查监听独立地址,不能通过公网控制面访问。验证码短期状态通过 Session Redis 跨实例共享,因此验证码挑战、校验和 POST /login 不要求粘性路由;Redis 异常统一失败关闭。空库初始化后,admin/admin 必须先完成图形验证码,再按普通账号流程登录;系统没有首次登录强制改密旁路。
控制台与文件都使用 [时间][模块(应用)][级别] 正文 [辅助字段] 行式格式;控制台时间为 HH:mm:ss,文件时间为 YYYY-MM-DD HH:mm:ss。日志等级在构造队列项前统一过滤;运行期日志随后进入容量为 10000 的有界异步队列,队列满时丢弃最新日志并由后台合并记录丢弃数量。后台按 100 条或 1 秒批量刷新文件,退出时排空队列并强制刷新。PLAYMESH_LOG_LEVEL 支持 debug、info、warn、error;未配置时生产默认 info,开发/测试默认 debug。PLAYMESH_LOG_DIR 默认 logs,PLAYMESH_LOG_RETENTION_DAYS 默认 30。
当天达到所设等级的全部日志追加到 logs/info.log,错误同时追加到 logs/error.log。本地时间到达午夜时,两份当前文件分别归档到 logs/info/YYYY-MM-DD.log 与 logs/error/YYYY-MM-DD.log;进程跨日后首次写日志以及跨日重启也会补做轮换,超过保留天数的日期文件会被清理。生产部署应把整个日志目录放在持久化卷上;多实例部署必须为每个实例配置独立目录,不能让多个进程并发写同一组文件。
常规验证不要求本机 MySQL/Redis 可用,因为进程启动不得探测它们:
& 'D:\KaiFaTool\runtime\go\go-1.26.2\bin\go.exe' test ./...
& 'D:\KaiFaTool\runtime\go\go-1.26.2\bin\go.exe' vet ./...
& 'D:\KaiFaTool\runtime\go\go-1.26.2\bin\go.exe' build ./...真实 MySQL 8/Redis 集成验证由外部测试编排先执行 SQL,再通过 PLAYMESH_INTEGRATION_MYSQL_* 与 PLAYMESH_INTEGRATION_REDIS_ADDR 环境变量显式启用。完整初始化验证还可把一个由部署者预先创建、确认为空且名称以 playmesh_stage1_init_ 开头的数据库填入 PLAYMESH_INTEGRATION_MYSQL_FRESH_DB;测试只执行 sql/init.sql 和回滚式业务校验,不创建或删除数据库。测试夹具使用随机且经过校验的 Redis 前缀清理自身数据,不执行全库清空。
头像和自定义桌面壁纸由当前 Controller 的 /public/** 与 /protected/** 路径提供,不需要额外文件域名;生产环境只需为 PLAYMESH_UPLOAD_ROOT 配置持久化卷。数据库和业务记录保存同源相对 URL,不保存部署 Host。内置应用图标随程序静态资源发布并由注册表固定绑定,管理员不能上传或替换。
普通用户可以上传自己的头像和任意尺寸/时长的图片或视频壁纸,单文件默认上限为 64 MiB;家庭壁纸仍只有家庭创建者可保存。普通应用包根目录的可选 icon.png 只在导入时做严格 PNG、完整解码、大小和像素上限校验,不改写原始文件;缺失或失败时回退文字图标,通过时才原样显示包内图标。
数据库升级使用 sql/dev-updates/D0013__platform_file_storage.sql。部署与恢复细节见 docs/database-deployment.md,安全边界见 docs/implementation-architecture.md。
