From 52626b332da5e0f3848223848432cff74caa5273 Mon Sep 17 00:00:00 2001 From: brother7 <7brother7@gmail.com> Date: Sat, 8 Aug 2026 18:06:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=93=E9=80=9A=E4=B8=80=E9=94=AE=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E4=B8=8E=E7=9C=9F=E6=9C=BA=E9=A2=84=E8=A7=88=E9=9B=86?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 需求:将登录续期、一键提交审核和精确 Release 真机预览合为可推进主分支的完整客户端链路。 实现:提交成功后保存安全版本映射,映射失败不误报上传失败;同步新会话测试契约和集成态项目文档。 --- .project-docs/20-architecture/data-flow.md | 12 +++-- .../20-architecture/system-overview.md | 15 ++++-- .project-docs/30-worklog/current-state.md | 25 +++++---- .project-docs/30-worklog/task-history.md | 5 +- .../20260807-integrate-login-client-a4f8.md | 40 +++++++------- .project-docs/40-domain/business-rules.md | 10 ++-- electron/api/routes/works.ts | 14 +++++ tests/unit/opencode-chat-panel.test.tsx | 22 ++++---- tests/unit/works-routes.test.ts | 53 ++++++++++++++++++- 9 files changed, 144 insertions(+), 52 deletions(-) diff --git a/.project-docs/20-architecture/data-flow.md b/.project-docs/20-architecture/data-flow.md index e108ed4..8c2490b 100644 --- a/.project-docs/20-architecture/data-flow.md +++ b/.project-docs/20-architecture/data-flow.md @@ -4,15 +4,21 @@ | Flow | Source | Destination | Notes | |---|---|---|---| +| 登录续期 | Renderer 活动信号 | Main Works Session | 连续 7 天未使用才清除会话 | +| 一键提交 | 项目配置 | Main → Works Square | Main 打包、自动版本、幂等重试和脱敏 | +| 运营发布 | Works Square 审核 | 公共 Runtime | 审核通过后服务端直接发布 | +| 真机预览 | 项目空间 | Main → Owner preview / Runtime | 核对 app、version、release 和同源 HTTPS | ## State Ownership -- {state owner or persistence rule} +- Main 持有刷新凭据、发布 Token、临时 ZIP、幂等键和本地 app/version/review 映射。 +- Renderer 仅持有短效公开会话状态、提交展示状态和短时预览 URL。 ## External Interfaces -- {API, file, service, or user-facing boundary} +- Works Square 项目创建、版本上传、Owner 状态与 Release preview API。 +- 本机 Host API 的发布与真机预览路由。 ## Last Updated -{YYYY-MM-DD} +2026-08-08 diff --git a/.project-docs/20-architecture/system-overview.md b/.project-docs/20-architecture/system-overview.md index 894d903..c4c3f34 100644 --- a/.project-docs/20-architecture/system-overview.md +++ b/.project-docs/20-architecture/system-overview.md @@ -2,21 +2,28 @@ ## Current Architecture -{short description of the current system shape} +Makelore 是 Electron 桌面客户端。Renderer 负责项目操作与状态展示;Electron Main 持有认证、文件系统、安全打包、Works Square 网络访问和预览 URL 校验。 ## Main Components | Component | Responsibility | Notes | |---|---|---| +| Renderer | 项目配置、一键提交状态、真机二维码 | 不接触账号 Token、ZIP、幂等键或本地绝对路径 | +| Host API | 校验本地项目请求并投影安全响应 | 只接受回环来源和 JSON mutation | +| Project Packager | 确定性扫描、敏感文件排除与 ZIP 生成 | 限制文件数、包体和目录替换 | +| Works Session | 七天真实活动滑动续期 | 刷新凭据由 Main 安全持有 | +| Device Preview | 核对本地映射、远端版本和 Release | 待审使用短时 Owner preview;已发布使用精确 Runtime | ## Important Boundaries -- {boundary that future work should respect} +- 发布只有现有项目配置底部的一个入口,不新增发布工作台、侧栏或资源卡。 +- 云端确认上传成功后,本地映射失败不能把请求改判为失败;否则会诱导重复提交。 +- Renderer 只能获得安全状态字段和可展示的短时预览 URL。 ## Related Decisions -- {ADR reference} +- 当前长期边界记录于 README 与集成任务 `20260807-integrate-login-client-a4f8`;后续如改变入口或凭据所有权,应新增 ADR。 ## Last Updated -{YYYY-MM-DD} +2026-08-08 diff --git a/.project-docs/30-worklog/current-state.md b/.project-docs/30-worklog/current-state.md index 310df3e..1146de1 100644 --- a/.project-docs/30-worklog/current-state.md +++ b/.project-docs/30-worklog/current-state.md @@ -4,35 +4,38 @@ This file is the integrated default-branch snapshot. Feature tasks record progre ## Integrated Through -- `86ece3a`:客户端登录七天滑动续期来源提交。 -- `4dde8f32678baa81fd3ae3f6e7bd0d34b17885d6`:集成合并提交。 +- `86ece3a` / `4dde8f3`:客户端登录七天滑动续期及集成提交。 +- `724290e` / `dcc92fc`:Main-owned 一键打包提交审核及集成提交。 +- `1a19ad9` / `f02e99e`:项目真机预览、待审 Release Owner preview 及集成提交。 ## Current Focus -客户端认证由 Electron Main 统一管理刷新凭据,并按真实用户活动维护七天闲置窗口。 +客户端面向非专业用户提供“项目配置中一键提交 → 运营审核通过即发布”的单入口链路,并以项目级真机预览完成待审版本扫码验收。 ## Recently Completed -- 2026-08-07:集成客户端登录滑动续期、刷新令牌轮换持久化和终态清理保护。 +- 2026-08-08:合并登录续期、一键发布和真机预览;发布成功后保存精确 app/version/review 映射,Renderer 不接触 Token、ZIP 或本地路径。 +- 2026-08-08:补齐跨平台 Electron E2E fixture、Windows ZIP 预检和异常成功响应安全投影。 ## In Progress -- 协调认证服务端七天刷新令牌配置及共享 `app` OAuth 客户端发布。 +- 使用真实 Works Square、Builder 与 Static Gateway 环境执行客户端提交到运营发布的生产整链验收。 ## Next Recommended Steps -1. 确认所有共享 `app` 客户端支持原子保存轮换后的刷新令牌。 -2. 发布兼容客户端与认证服务后执行数据库迁移,并刷新 RegisteredClient Redis 缓存。 +1. 配置真实生产环境,执行“客户端提交 → 构建与浏览器门禁 → 运营批准 → 公共访问”。 +2. 核对待审 Owner preview 的到期刷新、生产监控和告警证据。 ## Open Questions / Blockers -- 存量刷新令牌不会被数据库迁移追溯缩短为七天。 +- 本地与模拟上游回归已完成;生产账号和环境变量仍由部署环境提供。 ## Risky Areas -- 刷新响应返回新令牌后、客户端安全落盘前的进程崩溃窗口会要求重新授权。 -- Linux 缺少受保护系统 keyring 时不会跨重启保存刷新凭据。 +- 一键提交已成功但本地预览映射落盘失败时必须保持提交成功,避免用户重复提交;真机预览会明确显示尚不可用。 +- 预览 URL 必须绑定当前项目、精确版本和 Release,并保持 Works Square 同源 HTTPS。 +- 刷新凭据、发布 Token、ZIP、幂等键和重试只能由 Electron Main 持有。 ## Last Updated -2026-08-07 +2026-08-08 diff --git a/.project-docs/30-worklog/task-history.md b/.project-docs/30-worklog/task-history.md index 0484e89..a1439a0 100644 --- a/.project-docs/30-worklog/task-history.md +++ b/.project-docs/30-worklog/task-history.md @@ -4,7 +4,10 @@ | Date | Task | Outcome | Docs Updated | |---|---|---|---| +| 2026-08-07 | 客户端登录七天滑动续期 | Main-owned 刷新凭据与真实活动续期 | README、current-state | +| 2026-08-08 | Makelore 一键提交审核 | 项目配置单入口,Main 安全打包与幂等提交 | README、architecture、domain | +| 2026-08-08 | 项目真机预览 | 待审 Owner preview 与精确已发布 Runtime | README、architecture、domain | ## Notes -This is legacy integrated history. Feature tasks must not append here. Record new work in `30-worklog/tasks/{task_id}.md`; an integration workflow may render or summarize accepted history later. +详细实现、约束与验证证据保存在对应的 `30-worklog/tasks/{task_id}.md`。 diff --git a/.project-docs/30-worklog/tasks/20260807-integrate-login-client-a4f8.md b/.project-docs/30-worklog/tasks/20260807-integrate-login-client-a4f8.md index 3e2c423..ee17524 100644 --- a/.project-docs/30-worklog/tasks/20260807-integrate-login-client-a4f8.md +++ b/.project-docs/30-worklog/tasks/20260807-integrate-login-client-a4f8.md @@ -1,4 +1,4 @@ -# Task: 合并客户端登录滑动续期到主分支 +# Task: 合并客户端登录、发布与真机预览到主分支 ## Identity @@ -8,38 +8,42 @@ - Worktree: D:\Datas\OthersProjects\makelore-login-integration-a4f8 - Base commit: 7b23cce67a1b9e61a48d89dedbd088d37b10ecd4 - Owner: codex -- Status: In Progress +- Status: Done ## Scope -- 将已完成的客户端登录滑动续期提交 `86ece3a` 合并到主分支候选。 -- 复核来源提交、合并树、认证聚焦测试和类型检查。 -- 更新集成态项目记录,并在不覆盖原主工作区未提交改动的前提下推进 `main`。 +- 合并客户端登录滑动续期 `5792b07`、一键发布 `724290e` 与真机预览 `1a19ad9`。 +- 按用户确认移除旧登录 WIP,保留设备预览,维持版本 `2.0.0`。 +- 将一键提交成功结果写入真机预览所需的本地精确版本映射,并安全推进 `main`。 ## Intent And Constraints -- 保留原主工作区中的设备预览及其他未提交改动,不自动 stash、提交或覆盖。 -- 仅集成登录滑动续期来源提交,不顺带合并其他 ready-for-integration 任务。 -- 合并后的行为以来源任务记录的七天闲置、真实活动续期和 Main-owned 凭据边界为准。 +- 登录实现以已验证的 `5792b07` 为准。 +- 发布只有现有项目配置底部一个入口;Main 持有文件、Token、ZIP、版本、幂等与重试。 +- 待审预览必须绑定精确 Release 的短时 Owner preview;Renderer 不接收账号 Token。 +- 云端上传已成功但本地映射失败时仍返回提交成功,避免用户重复提交。 ## Outcome -- 来源提交已通过非快进合并进入集成分支,合并提交为 - `4dde8f32678baa81fd3ae3f6e7bd0d34b17885d6`。 -- 集成树中的业务代码与来源提交一致,无内容漂移。 -- 原 `main` 工作区存在与本次认证文件重叠的未提交改动,尚未推进主分支引用。 +- 登录、一键发布与真机预览均已进入同一集成树;README 冲突同时保留登录和发布安全边界。 +- 发布成功后写入 app/version/review/sha256 映射,真机预览随后核对远端最新版本与 Release。 +- 补齐旧语音重试测试对 Main-owned 新会话契约的适配,以及 Windows ZIP 预检兜底。 +- canonical project docs 已同步当前产品链路和长期边界。 ## Verification -- `git merge-base --is-ancestor 86ece3a HEAD`:通过。 -- 来源提交认证聚焦测试:6 files,85 tests passed。 -- `pnpm run typecheck`:通过。 -- `git diff --exit-code 86ece3a HEAD -- README.md electron shared src tests`:通过。 +- 登录与发布聚焦回归:11 files / 136 tests passed。 +- 发布、映射、真机预览和聊天回归:7 files / 165 tests passed。 +- `pnpm run typecheck`:passed。 +- scoped ESLint:passed。 +- `pnpm run build:vite`:Renderer、Main、Preload 均构建通过;仅保留既有 chunk 提示。 +- `pnpm exec playwright test --list --reporter=line`:13 files / 21 tests collected。 +- `git diff --check`、project docs schema 与 ownership drift checks:passed。 ## Follow-ups -- 获得对原主工作区未提交改动的明确处理方式后,将集成分支安全推进到 `main`。 +- 在真实生产 Works Square、Builder 与 Static Gateway 环境运行“客户端提交 → 自动构建/浏览器门禁 → 运营批准 → 公共访问”整链验收。 ## Promotion Candidates -- None recorded. +- 已将用户确认的单入口发布、Main-owned 凭据/打包和精确 Release 预览边界提升到 README、architecture、data-flow 与 business-rules。 diff --git a/.project-docs/40-domain/business-rules.md b/.project-docs/40-domain/business-rules.md index 7bef29f..202fd9d 100644 --- a/.project-docs/40-domain/business-rules.md +++ b/.project-docs/40-domain/business-rules.md @@ -2,12 +2,16 @@ ## Durable Rules -- {business or product rule} +- 非专业用户只执行一次“提交审核”;构建通过后由运营审核,审核通过即直接发布。 +- 不恢复独立发布上传页或云部署工作台;入口只位于现有项目配置操作区。 +- 待审版本只能通过绑定精确 Release 的短时 Owner preview 验收;旧版本、构建中或失败状态不能生成可扫码假象。 +- 发布和预览安全边界由 Electron Main 持有,Renderer 不接触账号 Token、ZIP、幂等键和本地绝对路径。 +- Works Square 会话按真实键盘、鼠标或触摸活动滑动续期,连续 7 天未使用才要求重新授权。 ## Open Questions -- {rule that needs human confirmation} +- 生产整链需要真实账号、Builder、Static Gateway 与监控环境完成最终验收。 ## Last Reviewed -{YYYY-MM-DD} +2026-08-08 diff --git a/electron/api/routes/works.ts b/electron/api/routes/works.ts index e3fddac..42f2773 100644 --- a/electron/api/routes/works.ts +++ b/electron/api/routes/works.ts @@ -14,6 +14,7 @@ import { ProjectPackageError, } from '../../services/project-packager'; import { getValidWorksSquareAccessToken } from '../../services/works-square-session'; +import { logger } from '../../utils/logger'; type CreateProjectInput = { accessToken?: unknown; @@ -923,6 +924,19 @@ async function handlePublishProjectSource( ); return; } + if (ctx.worksCloudDeployment) { + try { + await ctx.worksCloudDeployment.recordSubmitted(projectId, { + appId, + versionId: uploadPayload.version_id, + versionName, + reviewStatus: uploadPayload.review_status, + zipSha256: packageSummary.sha256, + }); + } catch { + logger.warn('[works] One-click submission succeeded, but local preview mapping could not be saved'); + } + } const { archivePath: _archivePath, ...rendererPackageSummary } = packageSummary; sendJson(res, uploadResponse.status, { success: true, diff --git a/tests/unit/opencode-chat-panel.test.tsx b/tests/unit/opencode-chat-panel.test.tsx index 9ec0731..7ed049d 100644 --- a/tests/unit/opencode-chat-panel.test.tsx +++ b/tests/unit/opencode-chat-panel.test.tsx @@ -2159,9 +2159,11 @@ describe('OpencodeChatPanel', () => { loading: false, error: null, accessToken: 'stale-token', - refreshToken: 'refresh-token', tokenType: 'Bearer', expiresAt: Date.now() + 60_000, + lastActiveAt: Date.now(), + canRefresh: true, + legacyRefreshToken: null, user: { username: 'student', userId: '42', @@ -2200,18 +2202,16 @@ describe('OpencodeChatPanel', () => { }, }; } - if (path === '/api/auth/refresh' && init?.method === 'POST') { - expect(init.body).toBe(JSON.stringify({ refreshToken: 'refresh-token' })); + if (path === '/api/auth/session/refresh' && init?.method === 'POST') { + expect(init.body).toBe(JSON.stringify({ forceRefresh: true })); return { success: true, - token: { - access_token: 'fresh-token', - refresh_token: 'refresh-token', - token_type: 'Bearer', - expires_in: 3600, - username: 'student', - user_id: '42', - authorities: [], + session: { + accessToken: 'fresh-token', + tokenType: 'Bearer', + expiresAt: Date.now() + 3_600_000, + lastActiveAt: Date.now(), + canRefresh: true, }, }; } diff --git a/tests/unit/works-routes.test.ts b/tests/unit/works-routes.test.ts index bf67715..94f3326 100644 --- a/tests/unit/works-routes.test.ts +++ b/tests/unit/works-routes.test.ts @@ -1039,12 +1039,16 @@ describe('works square host api routes', () => { }), { status: 201 })); vi.stubGlobal('fetch', fetchMock); const response = createResponse(); + const recordSubmitted = vi.fn(async () => undefined); const handled = await handleWorksRoutes( createRequest('POST', { projectId: project.id, project: projectMetadata }), response.res, new URL('http://127.0.0.1/api/works/projects/publish-source'), - { opencodeProjectStore: { listProjects: vi.fn(async () => [project]) } } as never, + { + opencodeProjectStore: { listProjects: vi.fn(async () => [project]) }, + worksCloudDeployment: { recordSubmitted }, + } as never, ); expect(handled).toBe(true); @@ -1100,6 +1104,53 @@ describe('works square host api routes', () => { expect(archive).toBeInstanceOf(File); expect((archive as File).name).toBe('project.zip'); expect((archive as File).size).toBeGreaterThan(0); + expect(recordSubmitted).toHaveBeenCalledWith(project.id, { + appId: 'space-cleaner', + versionId: 'version-1', + versionName: 'v2.3.4', + reviewStatus: 'building', + zipSha256: expect.stringMatching(/^[a-f0-9]{64}$/), + }); + }); + + it('keeps a confirmed submission successful when the local preview mapping cannot be saved', async () => { + tempDir = await mkdtemp(join(tmpdir(), 'makelore-source-mapping-failure-')); + await writePublishableProject(tempDir); + const project = { id: 'project-1', path: tempDir, name: 'space-cleaner' }; + const fetchMock = vi.fn() + .mockResolvedValueOnce(new Response('{}', { status: 201 })) + .mockResolvedValueOnce(new Response(JSON.stringify({ + version_id: 'version-1', + review_status: 'building', + }), { status: 201 })); + vi.stubGlobal('fetch', fetchMock); + const response = createResponse(); + + await handleWorksRoutes( + createRequest('POST', { + projectId: project.id, + project: { + app_id: 'space-cleaner', + title: '太空清洁队', + summary: '收集漂浮垃圾的小游戏。', + }, + }), + response.res, + new URL('http://127.0.0.1/api/works/projects/publish-source'), + { + opencodeProjectStore: { listProjects: vi.fn(async () => [project]) }, + worksCloudDeployment: { + recordSubmitted: vi.fn(async () => { throw new Error('disk unavailable'); }), + }, + } as never, + ); + + expect(response.statusCode).toBe(201); + expect(response.json()).toMatchObject({ + success: true, + upload: { version_id: 'version-1', review_status: 'building' }, + }); + expect(fetchMock).toHaveBeenCalledTimes(2); }); it('uses a bounded timestamp version when VERSION.md is oversized', async () => {