打通一键发布与真机预览集成

需求:将登录续期、一键提交审核和精确 Release 真机预览合为可推进主分支的完整客户端链路。

实现:提交成功后保存安全版本映射,映射失败不误报上传失败;同步新会话测试契约和集成态项目文档。
This commit is contained in:
2026-08-08 18:06:46 +08:00
parent f02e99ea9b
commit 52626b332d
9 changed files with 144 additions and 52 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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`

View File

@@ -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 previewRenderer 不接收账号 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 files85 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 ESLintpassed
- `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 checkspassed。
## Follow-ups
- 获得对原主工作区未提交改动的明确处理方式后,将集成分支安全推进到 `main`
- 在真实生产 Works Square、Builder 与 Static Gateway 环境运行“客户端提交 → 自动构建/浏览器门禁 → 运营批准 → 公共访问”整链验收
## Promotion Candidates
- None recorded.
- 已将用户确认的单入口发布、Main-owned 凭据/打包和精确 Release 预览边界提升到 README、architecture、data-flow 与 business-rules。

View File

@@ -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

View File

@@ -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,

View File

@@ -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,
},
};
}

View File

@@ -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 () => {