打通一键发布与真机预览集成
需求:将登录续期、一键提交审核和精确 Release 真机预览合为可推进主分支的完整客户端链路。 实现:提交成功后保存安全版本映射,映射失败不误报上传失败;同步新会话测试契约和集成态项目文档。
This commit is contained in:
@@ -4,15 +4,21 @@
|
|||||||
|
|
||||||
| Flow | Source | Destination | Notes |
|
| 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 Ownership
|
||||||
|
|
||||||
- {state owner or persistence rule}
|
- Main 持有刷新凭据、发布 Token、临时 ZIP、幂等键和本地 app/version/review 映射。
|
||||||
|
- Renderer 仅持有短效公开会话状态、提交展示状态和短时预览 URL。
|
||||||
|
|
||||||
## External Interfaces
|
## External Interfaces
|
||||||
|
|
||||||
- {API, file, service, or user-facing boundary}
|
- Works Square 项目创建、版本上传、Owner 状态与 Release preview API。
|
||||||
|
- 本机 Host API 的发布与真机预览路由。
|
||||||
|
|
||||||
## Last Updated
|
## Last Updated
|
||||||
|
|
||||||
{YYYY-MM-DD}
|
2026-08-08
|
||||||
|
|||||||
@@ -2,21 +2,28 @@
|
|||||||
|
|
||||||
## Current Architecture
|
## Current Architecture
|
||||||
|
|
||||||
{short description of the current system shape}
|
Makelore 是 Electron 桌面客户端。Renderer 负责项目操作与状态展示;Electron Main 持有认证、文件系统、安全打包、Works Square 网络访问和预览 URL 校验。
|
||||||
|
|
||||||
## Main Components
|
## Main Components
|
||||||
|
|
||||||
| Component | Responsibility | Notes |
|
| Component | Responsibility | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| Renderer | 项目配置、一键提交状态、真机二维码 | 不接触账号 Token、ZIP、幂等键或本地绝对路径 |
|
||||||
|
| Host API | 校验本地项目请求并投影安全响应 | 只接受回环来源和 JSON mutation |
|
||||||
|
| Project Packager | 确定性扫描、敏感文件排除与 ZIP 生成 | 限制文件数、包体和目录替换 |
|
||||||
|
| Works Session | 七天真实活动滑动续期 | 刷新凭据由 Main 安全持有 |
|
||||||
|
| Device Preview | 核对本地映射、远端版本和 Release | 待审使用短时 Owner preview;已发布使用精确 Runtime |
|
||||||
|
|
||||||
## Important Boundaries
|
## Important Boundaries
|
||||||
|
|
||||||
- {boundary that future work should respect}
|
- 发布只有现有项目配置底部的一个入口,不新增发布工作台、侧栏或资源卡。
|
||||||
|
- 云端确认上传成功后,本地映射失败不能把请求改判为失败;否则会诱导重复提交。
|
||||||
|
- Renderer 只能获得安全状态字段和可展示的短时预览 URL。
|
||||||
|
|
||||||
## Related Decisions
|
## Related Decisions
|
||||||
|
|
||||||
- {ADR reference}
|
- 当前长期边界记录于 README 与集成任务 `20260807-integrate-login-client-a4f8`;后续如改变入口或凭据所有权,应新增 ADR。
|
||||||
|
|
||||||
## Last Updated
|
## Last Updated
|
||||||
|
|
||||||
{YYYY-MM-DD}
|
2026-08-08
|
||||||
|
|||||||
@@ -4,35 +4,38 @@ This file is the integrated default-branch snapshot. Feature tasks record progre
|
|||||||
|
|
||||||
## Integrated Through
|
## Integrated Through
|
||||||
|
|
||||||
- `86ece3a`:客户端登录七天滑动续期来源提交。
|
- `86ece3a` / `4dde8f3`:客户端登录七天滑动续期及集成提交。
|
||||||
- `4dde8f32678baa81fd3ae3f6e7bd0d34b17885d6`:集成合并提交。
|
- `724290e` / `dcc92fc`:Main-owned 一键打包提交审核及集成提交。
|
||||||
|
- `1a19ad9` / `f02e99e`:项目真机预览、待审 Release Owner preview 及集成提交。
|
||||||
|
|
||||||
## Current Focus
|
## Current Focus
|
||||||
|
|
||||||
客户端认证由 Electron Main 统一管理刷新凭据,并按真实用户活动维护七天闲置窗口。
|
客户端面向非专业用户提供“项目配置中一键提交 → 运营审核通过即发布”的单入口链路,并以项目级真机预览完成待审版本扫码验收。
|
||||||
|
|
||||||
## Recently Completed
|
## Recently Completed
|
||||||
|
|
||||||
- 2026-08-07:集成客户端登录滑动续期、刷新令牌轮换持久化和终态清理保护。
|
- 2026-08-08:合并登录续期、一键发布和真机预览;发布成功后保存精确 app/version/review 映射,Renderer 不接触 Token、ZIP 或本地路径。
|
||||||
|
- 2026-08-08:补齐跨平台 Electron E2E fixture、Windows ZIP 预检和异常成功响应安全投影。
|
||||||
|
|
||||||
## In Progress
|
## In Progress
|
||||||
|
|
||||||
- 协调认证服务端七天刷新令牌配置及共享 `app` OAuth 客户端发布。
|
- 使用真实 Works Square、Builder 与 Static Gateway 环境执行客户端提交到运营发布的生产整链验收。
|
||||||
|
|
||||||
## Next Recommended Steps
|
## Next Recommended Steps
|
||||||
|
|
||||||
1. 确认所有共享 `app` 客户端支持原子保存轮换后的刷新令牌。
|
1. 配置真实生产环境,执行“客户端提交 → 构建与浏览器门禁 → 运营批准 → 公共访问”。
|
||||||
2. 发布兼容客户端与认证服务后执行数据库迁移,并刷新 RegisteredClient Redis 缓存。
|
2. 核对待审 Owner preview 的到期刷新、生产监控和告警证据。
|
||||||
|
|
||||||
## Open Questions / Blockers
|
## Open Questions / Blockers
|
||||||
|
|
||||||
- 存量刷新令牌不会被数据库迁移追溯缩短为七天。
|
- 本地与模拟上游回归已完成;生产账号和环境变量仍由部署环境提供。
|
||||||
|
|
||||||
## Risky Areas
|
## Risky Areas
|
||||||
|
|
||||||
- 刷新响应返回新令牌后、客户端安全落盘前的进程崩溃窗口会要求重新授权。
|
- 一键提交已成功但本地预览映射落盘失败时必须保持提交成功,避免用户重复提交;真机预览会明确显示尚不可用。
|
||||||
- Linux 缺少受保护系统 keyring 时不会跨重启保存刷新凭据。
|
- 预览 URL 必须绑定当前项目、精确版本和 Release,并保持 Works Square 同源 HTTPS。
|
||||||
|
- 刷新凭据、发布 Token、ZIP、幂等键和重试只能由 Electron Main 持有。
|
||||||
|
|
||||||
## Last Updated
|
## Last Updated
|
||||||
|
|
||||||
2026-08-07
|
2026-08-08
|
||||||
|
|||||||
@@ -4,7 +4,10 @@
|
|||||||
|
|
||||||
| Date | Task | Outcome | Docs Updated |
|
| 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
|
## 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`。
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Task: 合并客户端登录滑动续期到主分支
|
# Task: 合并客户端登录、发布与真机预览到主分支
|
||||||
|
|
||||||
## Identity
|
## Identity
|
||||||
|
|
||||||
@@ -8,38 +8,42 @@
|
|||||||
- Worktree: D:\Datas\OthersProjects\makelore-login-integration-a4f8
|
- Worktree: D:\Datas\OthersProjects\makelore-login-integration-a4f8
|
||||||
- Base commit: 7b23cce67a1b9e61a48d89dedbd088d37b10ecd4
|
- Base commit: 7b23cce67a1b9e61a48d89dedbd088d37b10ecd4
|
||||||
- Owner: codex
|
- Owner: codex
|
||||||
- Status: In Progress
|
- Status: Done
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
- 将已完成的客户端登录滑动续期提交 `86ece3a` 合并到主分支候选。
|
- 合并客户端登录滑动续期 `5792b07`、一键发布 `724290e` 与真机预览 `1a19ad9`。
|
||||||
- 复核来源提交、合并树、认证聚焦测试和类型检查。
|
- 按用户确认移除旧登录 WIP,保留设备预览,维持版本 `2.0.0`。
|
||||||
- 更新集成态项目记录,并在不覆盖原主工作区未提交改动的前提下推进 `main`。
|
- 将一键提交成功结果写入真机预览所需的本地精确版本映射,并安全推进 `main`。
|
||||||
|
|
||||||
## Intent And Constraints
|
## Intent And Constraints
|
||||||
|
|
||||||
- 保留原主工作区中的设备预览及其他未提交改动,不自动 stash、提交或覆盖。
|
- 登录实现以已验证的 `5792b07` 为准。
|
||||||
- 仅集成登录滑动续期来源提交,不顺带合并其他 ready-for-integration 任务。
|
- 发布只有现有项目配置底部一个入口;Main 持有文件、Token、ZIP、版本、幂等与重试。
|
||||||
- 合并后的行为以来源任务记录的七天闲置、真实活动续期和 Main-owned 凭据边界为准。
|
- 待审预览必须绑定精确 Release 的短时 Owner preview;Renderer 不接收账号 Token。
|
||||||
|
- 云端上传已成功但本地映射失败时仍返回提交成功,避免用户重复提交。
|
||||||
|
|
||||||
## Outcome
|
## Outcome
|
||||||
|
|
||||||
- 来源提交已通过非快进合并进入集成分支,合并提交为
|
- 登录、一键发布与真机预览均已进入同一集成树;README 冲突同时保留登录和发布安全边界。
|
||||||
`4dde8f32678baa81fd3ae3f6e7bd0d34b17885d6`。
|
- 发布成功后写入 app/version/review/sha256 映射,真机预览随后核对远端最新版本与 Release。
|
||||||
- 集成树中的业务代码与来源提交一致,无内容漂移。
|
- 补齐旧语音重试测试对 Main-owned 新会话契约的适配,以及 Windows ZIP 预检兜底。
|
||||||
- 原 `main` 工作区存在与本次认证文件重叠的未提交改动,尚未推进主分支引用。
|
- canonical project docs 已同步当前产品链路和长期边界。
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
- `git merge-base --is-ancestor 86ece3a HEAD`:通过。
|
- 登录与发布聚焦回归:11 files / 136 tests passed。
|
||||||
- 来源提交认证聚焦测试:6 files,85 tests passed。
|
- 发布、映射、真机预览和聊天回归:7 files / 165 tests passed。
|
||||||
- `pnpm run typecheck`:通过。
|
- `pnpm run typecheck`:passed。
|
||||||
- `git diff --exit-code 86ece3a HEAD -- README.md electron shared src tests`:通过。
|
- 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
|
## Follow-ups
|
||||||
|
|
||||||
- 获得对原主工作区未提交改动的明确处理方式后,将集成分支安全推进到 `main`。
|
- 在真实生产 Works Square、Builder 与 Static Gateway 环境运行“客户端提交 → 自动构建/浏览器门禁 → 运营批准 → 公共访问”整链验收。
|
||||||
|
|
||||||
## Promotion Candidates
|
## Promotion Candidates
|
||||||
|
|
||||||
- None recorded.
|
- 已将用户确认的单入口发布、Main-owned 凭据/打包和精确 Release 预览边界提升到 README、architecture、data-flow 与 business-rules。
|
||||||
|
|||||||
@@ -2,12 +2,16 @@
|
|||||||
|
|
||||||
## Durable Rules
|
## Durable Rules
|
||||||
|
|
||||||
- {business or product rule}
|
- 非专业用户只执行一次“提交审核”;构建通过后由运营审核,审核通过即直接发布。
|
||||||
|
- 不恢复独立发布上传页或云部署工作台;入口只位于现有项目配置操作区。
|
||||||
|
- 待审版本只能通过绑定精确 Release 的短时 Owner preview 验收;旧版本、构建中或失败状态不能生成可扫码假象。
|
||||||
|
- 发布和预览安全边界由 Electron Main 持有,Renderer 不接触账号 Token、ZIP、幂等键和本地绝对路径。
|
||||||
|
- Works Square 会话按真实键盘、鼠标或触摸活动滑动续期,连续 7 天未使用才要求重新授权。
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
- {rule that needs human confirmation}
|
- 生产整链需要真实账号、Builder、Static Gateway 与监控环境完成最终验收。
|
||||||
|
|
||||||
## Last Reviewed
|
## Last Reviewed
|
||||||
|
|
||||||
{YYYY-MM-DD}
|
2026-08-08
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
ProjectPackageError,
|
ProjectPackageError,
|
||||||
} from '../../services/project-packager';
|
} from '../../services/project-packager';
|
||||||
import { getValidWorksSquareAccessToken } from '../../services/works-square-session';
|
import { getValidWorksSquareAccessToken } from '../../services/works-square-session';
|
||||||
|
import { logger } from '../../utils/logger';
|
||||||
|
|
||||||
type CreateProjectInput = {
|
type CreateProjectInput = {
|
||||||
accessToken?: unknown;
|
accessToken?: unknown;
|
||||||
@@ -923,6 +924,19 @@ async function handlePublishProjectSource(
|
|||||||
);
|
);
|
||||||
return;
|
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;
|
const { archivePath: _archivePath, ...rendererPackageSummary } = packageSummary;
|
||||||
sendJson(res, uploadResponse.status, {
|
sendJson(res, uploadResponse.status, {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -2159,9 +2159,11 @@ describe('OpencodeChatPanel', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
accessToken: 'stale-token',
|
accessToken: 'stale-token',
|
||||||
refreshToken: 'refresh-token',
|
|
||||||
tokenType: 'Bearer',
|
tokenType: 'Bearer',
|
||||||
expiresAt: Date.now() + 60_000,
|
expiresAt: Date.now() + 60_000,
|
||||||
|
lastActiveAt: Date.now(),
|
||||||
|
canRefresh: true,
|
||||||
|
legacyRefreshToken: null,
|
||||||
user: {
|
user: {
|
||||||
username: 'student',
|
username: 'student',
|
||||||
userId: '42',
|
userId: '42',
|
||||||
@@ -2200,18 +2202,16 @@ describe('OpencodeChatPanel', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (path === '/api/auth/refresh' && init?.method === 'POST') {
|
if (path === '/api/auth/session/refresh' && init?.method === 'POST') {
|
||||||
expect(init.body).toBe(JSON.stringify({ refreshToken: 'refresh-token' }));
|
expect(init.body).toBe(JSON.stringify({ forceRefresh: true }));
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
token: {
|
session: {
|
||||||
access_token: 'fresh-token',
|
accessToken: 'fresh-token',
|
||||||
refresh_token: 'refresh-token',
|
tokenType: 'Bearer',
|
||||||
token_type: 'Bearer',
|
expiresAt: Date.now() + 3_600_000,
|
||||||
expires_in: 3600,
|
lastActiveAt: Date.now(),
|
||||||
username: 'student',
|
canRefresh: true,
|
||||||
user_id: '42',
|
|
||||||
authorities: [],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1039,12 +1039,16 @@ describe('works square host api routes', () => {
|
|||||||
}), { status: 201 }));
|
}), { status: 201 }));
|
||||||
vi.stubGlobal('fetch', fetchMock);
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
const response = createResponse();
|
const response = createResponse();
|
||||||
|
const recordSubmitted = vi.fn(async () => undefined);
|
||||||
|
|
||||||
const handled = await handleWorksRoutes(
|
const handled = await handleWorksRoutes(
|
||||||
createRequest('POST', { projectId: project.id, project: projectMetadata }),
|
createRequest('POST', { projectId: project.id, project: projectMetadata }),
|
||||||
response.res,
|
response.res,
|
||||||
new URL('http://127.0.0.1/api/works/projects/publish-source'),
|
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);
|
expect(handled).toBe(true);
|
||||||
@@ -1100,6 +1104,53 @@ describe('works square host api routes', () => {
|
|||||||
expect(archive).toBeInstanceOf(File);
|
expect(archive).toBeInstanceOf(File);
|
||||||
expect((archive as File).name).toBe('project.zip');
|
expect((archive as File).name).toBe('project.zip');
|
||||||
expect((archive as File).size).toBeGreaterThan(0);
|
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 () => {
|
it('uses a bounded timestamp version when VERSION.md is oversized', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user