refactor: update release storage layout and fragment handling for improved clarity and consistency

This commit is contained in:
hy committed 2026-09-14 11:07:49 +08:00
1 parent 4dbd750417
commit d25a153488
15 files changed
+96 -71

No files matched your search

+1 -1
View File
@@ -379,7 +379,7 @@
- `WB-COMPOSER-009-AC6` 展开后的工具句子使用“动作 + 对象”的完整自然语言,不重复运行状态,不使用冒号。示例:`docx_read_outline` 展示“查看 Word 文档 xx 的结构”;`docx_read_nodes` 展示“读取 Word 文档 xx 范围”;`learn_skill` 展示“正在学习 / 已学会 xx”;`text_file_read` 展示“读取文件 xx”;`terminal_run` 展示“执行命令 xxxx”;`web_search search / fetch` 分别展示“搜索网页 query”“抓取网页 url”;`browser_use navigate / list_tabs / observe / extract / act / act_instruction / screenshot / scroll` 分别补充“打开网页 / 列出浏览器标签 / 查看页面并 / 从页面中 / 在页面上执行 / 在页面上执行 / 截取页面 / 滚动页面”的动作语义(半截连词仅用于展开行接对象,不用于 header)。
- `WB-COMPOSER-009-AC7` 文件名、query、命令、URL 和技能名不使用粗体或冒号强调,保持正常字重;可以通过文字颜色表达层级。
- `WB-COMPOSER-009-AC8` `learn_skill` 作为普通工具行展示,使用技能通用名称,例如 `docx 阅读器`、`docx 编辑器`、`资料查阅器`、`命令执行器`;运行中展示“正在学习 xx”,完成后展示“已学会 xx”,不再作为独立学习层。
- `WB-COMPOSER-009-AC9` Activity header、压缩层和重试层展示对应图标;浏览器各动作(打开网页使用地球图标、查看页面使用眼睛图标、操作页面使用指针图标、提取内容使用清单图标、截取页面使用相机图标、滚动页面使用上下箭头图标、列出标签使用窗口图标)、网页抓取使用下载图标、网页搜索使用放大镜图标、读取图片与生成图片使用照片相框图标(截取页面并读取图片的视觉成对动作统一使用照片图标,不退化为闪电)、知识库、文件、终端和学习使用独立语义图标;其他混合动作或混合工具时使用工作流闪电图标。展开后的每条工具句子不展示图标,也不渲染前置小圆点或“AI 思路”类图标。
- `WB-COMPOSER-009-AC9` Activity header、压缩层和重试层展示对应图标;浏览器各动作(打开网页使用地球图标、查看页面使用眼睛图标、操作页面使用指针图标、提取内容使用双眼图标、截取页面使用相机图标、滚动页面使用上下箭头图标、列出标签使用浏览器标签图标)、网页抓取使用蜘蛛图标、网页搜索使用放大镜图标、读取图片与生成图片使用照片相框图标(截取页面并读取图片的视觉成对动作统一使用照片图标,不退化为闪电)、知识库、文件、终端和学习使用独立语义图标;其他混合动作或混合工具时使用工作流闪电图标。展开后的每条工具句子不展示图标,也不渲染前置小圆点或“AI 思路”类图标。
- `WB-COMPOSER-009-AC10` 决策层、Activity、压缩层、重试层按事件时间顺序累计展示;决策层采用浅灰色文本,未超出单行时不显示折叠图标,超出单行时单行截断省略,折叠状态展开图标在 hover 时显示且垂直居中于单行文本,展开时常驻浅灰卡片背景、折叠按钮居顶对齐第一行文本并在最大高度内纵向滚动,Activity 默认折叠;不存在独立学习层或最终收尾等待层;Brain 请求期间在消息流最下方展示“让俺想想”。
- `WB-COMPOSER-009-AC11` 无消息时输入区保持居中;存在消息后消息区为唯一纵向滚动容器,输入区固定在主区底部且不被消息内容向下推出窗口。
- `WB-COMPOSER-009-AC12` “正在 / 已检索知识库”只来自 AI 实际调用知识库工具;新建干净智能对话、仅切换伙伴时不得凭空出现该过程文案。
+6 -4
View File
@@ -111,16 +111,18 @@ electron-builder 只从 `release-stage/app` 打包。安装包不得含源码、
```text
artifacts/windows/windows-vX.Y.Z/*
artifacts/windows/windows-vX.Y.Z/fragment.json
artifacts/macos/mac-vX.Y.Z/*
release-status/windows/windows-vX.Y.Z.json
release-status/macos/mac-vX.Y.Z/macos-arm64.json
artifacts/macos/mac-vX.Y.Z/fragment.json
updates/latest/latest.yml
updates/latest/latest-mac.yml
releases/latest/windows.json
releases/latest/macos.json
```
`artifacts` 不可变长缓存;`updates/latest` 与 `releases/latest` 为 `no-store`。`latest.yml` / `latest-mac.yml` 供 electron-updater;`releases/latest/*.json` 供官网 `fetch` 后导航到 artifacts。
`fragment.json` 是该 Tag 的发版快照(文件清单、sha512、更新 YAML、审计),给 upload → finalize 和 `release:status` 用,不是给官网或客户端的 live 指针。旧的顶层 `release-status/` 前缀在下一次该平台 finalize 时删除。
CDN 节点缓存遵循源站 `Cache-Control`:安装包为 `public, max-age=31536000, immutable`;`fragment.json`、`updates/latest` 与 `releases/latest` 为 `no-store`。控制台另保留 `/updates/latest`、`/releases/latest` 目录不缓存,作为源站头写错时的保险。`latest.yml` / `latest-mac.yml` 供 electron-updater;`releases/latest/*.json` 供官网 `fetch` 后导航到 artifacts。
公开对象键使用用户可见文件名(如 `馆长-X.Y.Z-macos-arm64.dmg` / `馆长-X.Y.Z-Windows-x64.exe`),官网 `/api/download/*` 仍 307 到该直链;浏览器保存名取自 URL 末段。本地构建产物与 fragment 的 `storageName` 可仍为 ASCII(如 `Ncurator-mac-arm64.dmg`),仅用于解析磁盘文件。
@@ -130,7 +132,7 @@ releases/latest/macos.json
仅当有意发布未来海外版时才切 R2(非当前常规循环):`RELEASE_STORAGE_TARGET=r2`,并设置 `R2_ACCESS_KEY_ID`、`R2_SECRET_ACCESS_KEY`、`R2_ACCOUNT_ID`、`R2_BUCKET_NAME`、`R2_PUBLIC_BASE_URL=https://download.ncurator.com`。
publish 上传候选与 fragment,更新该平台自动更新 metadata 与官网 manifest,验证公开 yml / 清单 / artifacts 的大小、sha512 与 manifest URL(安装包 URL 须以用户可见文件名结尾)。验证成功后标记 `published`,清理本地 `tests/logs/**`,并清理该平台更早 artifacts / release-status。失败、prepare-only、test-only 不清理 logs。
publish 上传候选与 fragment,更新该平台自动更新 metadata 与官网 manifest,验证公开 yml / 清单 / artifacts 的大小、sha512 与 manifest URL(安装包 URL 须以用户可见文件名结尾)。验证成功后标记 `published`,清理本地 `tests/logs/**`,并清理该平台更早 artifacts(含旧 `release-status/`)。失败、prepare-only、test-only 不清理 logs。
### 本地保留与清理
+3 -1
View File
@@ -29,7 +29,7 @@
| HTTPS 请求 | HTTPS 访问次数 | 每月前 300 万次免费 | 更新检查通常够用,不买请求包 |
| QUIC | 开启 QUIC 后按请求 | — | **不要开** |
对象布局(与资费相关):每平台只保留最新 `artifacts/...` 大文件并长缓存;`updates/latest`、`releases/latest` 指针不缓存。官网与自动更新共用同一 artifacts URL,减少回源。
对象布局(与资费相关):每平台只保留最新 `artifacts/...` 大文件;CDN 节点缓存遵循源站 `Cache-Control`(安装包一年 `immutable`,`fragment.json` 与 `updates/latest`、`releases/latest` 指针 `no-store`)。官网与自动更新共用同一 artifacts URL,减少回源。
## 3. 已落地的防刷 / 控费配置
@@ -42,6 +42,8 @@
| 余额预警 | **低于 50 元** | 费用中心订阅;连续提醒最多约 5 天 |
| 自动充值 | **不开** | 避免异常划卡 |
| COS 默认域名 | 标「高风险」可忽略 | 不对用户暴露;对外只用自定义 CDN 域名 |
| HTTP 头部缓存 | 已开启 | 透传源站 `Cache-Control` 等头部 |
| 节点缓存 | 全部文件遵循源站;`/updates/latest`、`/releases/latest` 不缓存 | 安装包按源站一年 `immutable` 缓存;指针 `no-store`。浏览器缓存不另配,跟随源站头 |
粗算:100GB/日封顶量级约对应「最多几十元级 CDN 日费」的上限(刊例约 0.21 元/GB),不是无限账单。
+2 -2
View File
@@ -189,10 +189,10 @@ pnpm test:update:e2e
#### `send-guard-and-preferences`
- 测试类型:`e2e`
- What:缺少模型时的配置提示、发送前置条件(已有伙伴 + 输入非空),以及模型 / AI 推进偏好 / 执行权限按伙伴恢复;创建伙伴后真实发送一次并完成 AI 运行。
- What:缺少模型时的配置提示(本机 localStorage 仅首次出现一次)、发送前置条件(已有伙伴 + 输入非空),以及模型 / AI 推进偏好 / 执行权限按伙伴恢复;创建伙伴后真实发送一次并完成 AI 运行。
- Why:Workbench 的任务入口依赖这些基础交互;任一状态失真都会导致用户无法发起任务或把任务发到错误伙伴。原独立 case `partner-required-to-send` 与本 case 等价,已并入此处,避免双倍 live。
- How:先在无模型配置状态下检查提示,再通过 UI 配置真实模型并创建伙伴;覆盖无伙伴空态、输入保留、模型选择、AI 推进偏好、执行权限菜单的两条说明与完全访问高危风险弹窗确认/取消门禁、切换伙伴后的独立恢复和真实发送(Composer 提交,含 Enter 发送路径)。Composer 不再有 AI / 搜索切换,也不再有工作区选择器。
- Oracle:无模型时快速显示配置提示;无伙伴时 Composer 空态且不出现发送按钮,对齐 `WB-CORE-001-AC2`、`WB-COMPOSER-001`(无伙伴空态 **只在本 case 硬断言**);有伙伴、未插 `@` 且输入非空时可发送,对齐 `WB-CORE-001-AC1`;发送走 Composer 提交并完成真实 AI 运行,对齐 `WB-COMPOSER-002-AC1`、`WB-CORE-006-AC1`;权限菜单同时展示“审核 / 涉及到风险操作需要你审核”和“完全访问 / 不受限制的操作你的电脑”;切换“完全访问”触发风险二次确认弹窗(取消维持审核、确认切换成功,切回审核免二次确认),对齐 `WB-COMPOSER-007-AC13`;模型、推进偏好和执行权限按伙伴恢复,对齐 `WB-MODE-001-AC1`、`WB-COMPOSER-007-AC5`、`WB-COMPOSER-007-AC9`、`WB-COMPOSER-007-AC12`、`WB-SESSION-008`。废弃的 `WB-COMPOSER-005`、`WB-MODE-001-AC2`、`WB-MODE-002`、`WB-MODE-003` 不测。
- Oracle:无模型且本机从未展示过该提示时快速显示配置提示(展示后写入 localStorage,之后即使仍无模型也不再弹出);无伙伴时 Composer 空态且不出现发送按钮,对齐 `WB-CORE-001-AC2`、`WB-COMPOSER-001`(无伙伴空态 **只在本 case 硬断言**);有伙伴、未插 `@` 且输入非空时可发送,对齐 `WB-CORE-001-AC1`;发送走 Composer 提交并完成真实 AI 运行,对齐 `WB-COMPOSER-002-AC1`、`WB-CORE-006-AC1`;权限菜单同时展示“审核 / 涉及到风险操作需要你审核”和“完全访问 / 不受限制的操作你的电脑”;切换“完全访问”触发风险二次确认弹窗(取消维持审核、确认切换成功,切回审核免二次确认),对齐 `WB-COMPOSER-007-AC13`;模型、推进偏好和执行权限按伙伴恢复,对齐 `WB-MODE-001-AC1`、`WB-COMPOSER-007-AC5`、`WB-COMPOSER-007-AC9`、`WB-COMPOSER-007-AC12`、`WB-SESSION-008`。废弃的 `WB-COMPOSER-005`、`WB-MODE-001-AC2`、`WB-MODE-002`、`WB-MODE-003` 不测。
- Not:不验证真实模型回答质量,只验证用户路径和发送前置条件。Composer 已删除搜索模式,不再测智能 / 搜索互斥。`WB-COMPOSER-001-AC3`(有伙伴时 hover 禁用原因)、`WB-COMPOSER-007-AC6`(失效模型回退)、`WB-COMPOSER-007-AC11`(偏好写入 run 上下文)标为不测。`WB-COMPOSER-002-AC2`、`WB-COMPOSER-002-AC3`(Shift+Enter 换行、中文输入法组词)标为不测。`WB-CORE-006-AC2/AC3/AC4` 由 AI live 承担。
#### `partner-ui-state-isolation`
+11 -19
View File
@@ -7,7 +7,7 @@ import { promisify } from 'node:util'
import { assertReleaseFragment } from './fragment-contract.mjs'
import { fetchWithRetry } from '../shared/http.mjs'
import { createReleaseStorageClient } from '../storage/client.mjs'
import { artifactKey, statusKey } from './storage-layout.mjs'
import { artifactKey, artifactPrefix, fragmentKey, legacyStatusPrefix } from './storage-layout.mjs'
import { parseReleaseTag } from '../tag.mjs'
const execFileAsync = promisify(execFile)
@@ -31,7 +31,7 @@ const downloadAndVerifyFragment = async (platform) => {
await aws([
's3',
'cp',
`s3://${bucket}/${statusKey(releaseTag, platform)}`,
`s3://${bucket}/${fragmentKey(releaseTag, platform)}`,
target,
'--no-progress'
])
@@ -164,28 +164,20 @@ try {
throw new Error('public macOS release manifest verification failed')
}
const removable = (await listKeys('artifacts/macos/')).filter(
(key) => !key.startsWith(`artifacts/macos/${releaseTag}/`)
)
const activeArtifactKeys = new Set(
arm64.files.map((file) => artifactKey(arm64, file.name))
const uploadedFragmentKey = fragmentKey(releaseTag, arm64.platform)
const removable = (await listKeys(artifactPrefix('macos'))).filter(
(key) => !key.startsWith(artifactPrefix('macos', releaseTag))
)
const activeArtifactKeys = new Set([
uploadedFragmentKey,
...arm64.files.map((file) => artifactKey(arm64, file.name))
])
removable.push(
...(await listKeys(`artifacts/macos/${releaseTag}/`)).filter(
...(await listKeys(artifactPrefix('macos', releaseTag))).filter(
(key) => !activeArtifactKeys.has(key)
)
)
removable.push(
...(await listKeys('release-status/macos/')).filter(
(key) => !key.startsWith(`release-status/macos/${releaseTag}/`)
)
)
const activeStatusKey = statusKey(releaseTag, arm64.platform)
removable.push(
...(await listKeys(`release-status/macos/${releaseTag}/`)).filter(
(key) => key !== activeStatusKey
)
)
removable.push(...(await listKeys(legacyStatusPrefix('macos'))))
await deleteKeys(removable)
process.stdout.write(`[release] macOS arm64 ${releaseTag} is live at ${installerUrl}\n`)
} finally {
+5 -9
View File
@@ -3,7 +3,7 @@ import os from 'node:os'
import path from 'node:path'
import process from 'node:process'
import { assertReleaseFragment } from './fragment-contract.mjs'
import { artifactKey, statusKey } from './storage-layout.mjs'
import { artifactKey, artifactPrefix, fragmentKey, legacyStatusPrefix } from './storage-layout.mjs'
import { createReleaseStorageClient } from '../storage/client.mjs'
import { fetchWithRetry } from '../shared/http.mjs'
import { parseReleaseTag } from '../tag.mjs'
@@ -27,7 +27,7 @@ try {
await aws([
's3',
'cp',
`s3://${bucket}/${statusKey(releaseTag, 'windows-x64')}`,
`s3://${bucket}/${fragmentKey(releaseTag, 'windows-x64')}`,
fragmentPath,
'--no-progress'
])
@@ -146,14 +146,10 @@ try {
throw new Error('public Windows release manifest verification failed')
}
const removable = (await listKeys('artifacts/windows/')).filter(
(key) => !key.startsWith(`artifacts/windows/${releaseTag}/`)
)
removable.push(
...(await listKeys('release-status/windows/')).filter(
(key) => key !== statusKey(releaseTag, 'windows-x64')
)
const removable = (await listKeys(artifactPrefix('windows'))).filter(
(key) => !key.startsWith(artifactPrefix('windows', releaseTag))
)
removable.push(...(await listKeys(legacyStatusPrefix('windows'))))
await deleteKeys(removable)
process.stdout.write(
`[release] Windows ${releaseTag} is live at ${installerUrl}\n`
+4 -10
View File
@@ -8,16 +8,10 @@ export const platformFamily = (platform) => {
export const artifactKey = (fragment, fileName) =>
`artifacts/${platformFamily(fragment.platform)}/${fragment.tag}/${fileName}`
export const statusKey = (tag, platform) => {
const family = platformFamily(platform)
return family === 'windows'
? `release-status/windows/${tag}.json`
: `release-status/macos/${tag}/${platform}.json`
}
export const fragmentKey = (tag, platform) =>
`${artifactPrefix(platformFamily(platform), tag)}fragment.json`
export const artifactPrefix = (family, tag = '') => `artifacts/${family}/${tag ? `${tag}/` : ''}`
export const statusPrefix = (family, tag = '') =>
family === 'windows'
? `release-status/windows/${tag}`
: `release-status/macos/${tag ? `${tag}/` : ''}`
/** Legacy prefix from the old top-level release-status tree; next finalize deletes it. */
export const legacyStatusPrefix = (family) => `release-status/${family}/`
+3 -3
View File
@@ -3,7 +3,7 @@ import path from 'node:path'
import process from 'node:process'
import { assertReleaseFragment, resolveReleaseAssetPath } from './fragment-contract.mjs'
import { fetchWithRetry } from '../shared/http.mjs'
import { artifactKey, statusKey } from './storage-layout.mjs'
import { artifactKey, fragmentKey } from './storage-layout.mjs'
import { createReleaseStorageClient } from '../storage/client.mjs'
const [inputDirectory = '', fragmentPath = ''] = process.argv.slice(2)
@@ -80,12 +80,12 @@ for (const file of fragment.files) {
}
}
const fragmentStatusKey = statusKey(fragment.tag, fragment.platform)
const uploadedFragmentKey = fragmentKey(fragment.tag, fragment.platform)
await aws([
's3',
'cp',
fragmentPath,
`s3://${bucket}/${fragmentStatusKey}`,
`s3://${bucket}/${uploadedFragmentKey}`,
'--content-type',
'application/json',
'--cache-control',
+4 -4
View File
@@ -7,7 +7,7 @@ import { promisify } from 'node:util'
import { loadReleaseEnvironment } from './environment.mjs'
import { assertReleaseFragment } from '../r2/fragment-contract.mjs'
import { fetchWithRetry } from './http.mjs'
import { artifactKey, statusKey } from '../r2/storage-layout.mjs'
import { artifactKey, fragmentKey } from '../r2/storage-layout.mjs'
import { DOMESTIC_DOWNLOAD_BASE_URL, resolveReleaseStorageConfig } from '../storage/config.mjs'
const execFileAsync = promisify(execFile)
@@ -162,7 +162,7 @@ export const readPublicReleaseFragment = async (
publicBaseUrl = PUBLIC_RELEASE_BASE_URL
) => {
const response = await fetch(
`${publicBaseUrl}/${statusKey(releaseTag, platform)}?check=${Date.now()}`,
`${publicBaseUrl}/${fragmentKey(releaseTag, platform)}?check=${Date.now()}`,
{ cache: 'no-store' }
)
if (response.status === 404) return undefined
@@ -184,7 +184,7 @@ export const verifyPublicReleaseFragment = async (
) => {
for (const file of fragment.files) {
const response = await fetchWithRetry(
`${publicBaseUrl}/${artifactKey(fragment, file.storageName)}`,
`${publicBaseUrl}/${artifactKey(fragment, file.name)}`,
{
method: 'HEAD',
cache: 'no-store'
@@ -202,7 +202,7 @@ export const verifyReleaseStorageFragment = async (fragment, awsExecutable) => {
const { createAwsCliRuntime } = await import('../storage/client.mjs')
const runtime = await createAwsCliRuntime(config)
for (const file of fragment.files) {
const key = artifactKey(fragment, file.storageName)
const key = artifactKey(fragment, file.name)
const result = await commandResult(
awsExecutable,
[
+1 -1
View File
@@ -9,7 +9,7 @@ const plan = {
publicBaseUrl: String(process.env.COS_PUBLIC_BASE_URL || DOMESTIC_DOWNLOAD_BASE_URL),
writes: [
'artifacts/<platform>/<tag>/*',
'release-status/<tag>/<platform>.json',
'artifacts/<platform>/<tag>/fragment.json',
'updates/latest/latest*.yml',
'releases/latest/<platform>.json'
],
@@ -7,6 +7,8 @@ import { workbenchSelectors } from '../testing/selectors'
const { Text } = Typography
const LLM_CONFIG_MISSING_SHOWN_STORAGE_KEY = 'workbench.llm-config-missing.shown'
export interface NotificationItem {
id: string
title: string
@@ -18,6 +20,22 @@ export interface NotificationItem {
}
}
const hasShownLlmConfigMissing = (): boolean => {
try {
return window.localStorage.getItem(LLM_CONFIG_MISSING_SHOWN_STORAGE_KEY) === '1'
} catch {
return false
}
}
const markLlmConfigMissingShown = (): void => {
try {
window.localStorage.setItem(LLM_CONFIG_MISSING_SHOWN_STORAGE_KEY, '1')
} catch {
// ignore quota / private-mode failures; in-memory dismiss still applies
}
}
const NotificationCenter: React.FC = () => {
const navigate = useNavigate()
const location = useLocation()
@@ -48,7 +66,8 @@ const NotificationCenter: React.FC = () => {
const config = await getActiveConfig()
if (disposed) return
if (!config) {
if (isWorkbenchPage) {
if (isWorkbenchPage && !hasShownLlmConfigMissing()) {
markLlmConfigMissingShown()
updateNotification('llm-config-missing', {
id: 'llm-config-missing',
title: '未配置大模型 API',
@@ -2,6 +2,8 @@
import { FiCheckCircle } from 'react-icons/fi'
import { MdOutlineSmsFailed } from 'react-icons/md'
import { RiForbidLine } from 'react-icons/ri'
import { FaSpider } from 'react-icons/fa'
import { PiBrowsers, PiEyes } from 'react-icons/pi'
import {
HiChevronDown,
HiChevronRight,
@@ -9,7 +11,6 @@ import {
HiOutlineAcademicCap,
HiOutlineArchiveBox,
HiOutlineArrowsUpDown,
HiOutlineArrowDownTray,
HiOutlineBolt,
HiOutlineBookOpen,
HiOutlineCamera,
@@ -20,9 +21,7 @@ import {
HiOutlineGlobeAlt,
HiOutlineMagnifyingGlass,
HiOutlinePhoto,
HiOutlinePencilSquare,
HiOutlineQueueList,
HiOutlineWindow
HiOutlinePencilSquare
} from 'react-icons/hi2'
import MarkdownRenderer from '../MarkdownRenderer'
import type {
@@ -96,9 +95,9 @@ const LayerIcon: React.FC<{ iconKind: AssistantProcessIconKind; className?: stri
if (iconKind === 'eye') return <HiOutlineEye {...iconProps} />
if (iconKind === 'camera') return <HiOutlineCamera {...iconProps} />
if (iconKind === 'scroll') return <HiOutlineArrowsUpDown {...iconProps} />
if (iconKind === 'extract') return <HiOutlineQueueList {...iconProps} />
if (iconKind === 'window') return <HiOutlineWindow {...iconProps} />
if (iconKind === 'fetch') return <HiOutlineArrowDownTray {...iconProps} />
if (iconKind === 'extract') return <PiEyes {...iconProps} />
if (iconKind === 'window') return <PiBrowsers {...iconProps} />
if (iconKind === 'fetch') return <FaSpider {...iconProps} />
return <HiOutlineDocumentText {...iconProps} />
}
@@ -1,5 +1,5 @@
import type { ReferenceKind } from '../../../../../shared/types'
export const COMPOSER_PLACEHOLDER = '输入问题,或 @ 资料'
export const COMPOSER_PLACEHOLDER = '输入你想让馆长做的事情或者@文件'
export const COMPOSER_REFERENCE_KINDS: ReferenceKind[] = ['directory', 'file', 'web']
@@ -118,14 +118,16 @@ describe('app update publish config', () => {
})
it('finalizes Windows and macOS release channels independently with a live-publish lock', async () => {
const [windowsRelease, macRelease, windowsFinalize, macFinalize, tagContract, storageConfig] =
const [windowsRelease, macRelease, windowsFinalize, macFinalize, tagContract, storageConfig, windowsShared, uploadFragment] =
await Promise.all([
readFile(path.resolve('scripts/release/windows.mjs'), 'utf8'),
readFile(path.resolve('scripts/release/macos.mjs'), 'utf8'),
readFile(path.resolve('scripts/release/r2/finalize-windows.mjs'), 'utf8'),
readFile(path.resolve('scripts/release/r2/finalize-macos.mjs'), 'utf8'),
readFile(path.resolve('scripts/release/tag.mjs'), 'utf8'),
readFile(path.resolve('scripts/release/storage/config.mjs'), 'utf8')
readFile(path.resolve('scripts/release/storage/config.mjs'), 'utf8'),
readFile(path.resolve('scripts/release/shared/windows.mjs'), 'utf8'),
readFile(path.resolve('scripts/release/r2/upload-fragment.mjs'), 'utf8')
])
expect(windowsRelease).toContain("'r2', 'finalize-windows.mjs'")
@@ -140,6 +142,17 @@ describe('app update publish config', () => {
expect(macFinalize).toContain('installerUrl')
expect(windowsFinalize).not.toContain('--keep-old')
expect(macFinalize).not.toContain('--keep-old')
expect(windowsFinalize).toContain('fragmentKey')
expect(macFinalize).toContain('fragmentKey')
expect(windowsFinalize).toContain('legacyStatusPrefix')
expect(macFinalize).toContain('legacyStatusPrefix')
expect(windowsFinalize).not.toContain('release-status/windows/')
expect(macFinalize).not.toContain('release-status/macos/')
expect(uploadFragment).toContain('fragmentKey')
expect(uploadFragment).toContain("'no-store'")
expect(windowsShared).toContain('fragmentKey')
expect(windowsShared).toContain('artifactKey(fragment, file.name)')
expect(windowsShared).not.toContain('file.storageName')
expect(tagContract).toContain('/^(mac|windows)-v')
expect(windowsRelease).toContain('assertLivePublishAllowed')
expect(macRelease).toContain('assertLivePublishAllowed')
@@ -1,19 +1,27 @@
import { describe, expect, it } from 'vitest'
import { artifactKey, statusKey } from '../../../../scripts/release/r2/storage-layout.mjs'
import {
artifactKey,
artifactPrefix,
fragmentKey,
legacyStatusPrefix
} from '../../../../scripts/release/r2/storage-layout.mjs'
describe('release storage layout', () => {
it('separates immutable assets and status by platform family', () => {
it('keeps immutable assets and the tag fragment under the same artifacts prefix', () => {
expect(
artifactKey({ platform: 'windows-x64', tag: 'windows-v0.2.0' }, 'Ncurator-windows-x64.exe')
).toBe('artifacts/windows/windows-v0.2.0/Ncurator-windows-x64.exe')
expect(
artifactKey({ platform: 'macos-arm64', tag: 'mac-v0.1.6' }, 'Ncurator-mac-arm64.dmg')
).toBe('artifacts/macos/mac-v0.1.6/Ncurator-mac-arm64.dmg')
expect(statusKey('windows-v0.2.0', 'windows-x64')).toBe(
'release-status/windows/windows-v0.2.0.json'
expect(fragmentKey('windows-v0.2.0', 'windows-x64')).toBe(
'artifacts/windows/windows-v0.2.0/fragment.json'
)
expect(statusKey('mac-v0.1.6', 'macos-arm64')).toBe(
'release-status/macos/mac-v0.1.6/macos-arm64.json'
expect(fragmentKey('mac-v0.1.6', 'macos-arm64')).toBe(
'artifacts/macos/mac-v0.1.6/fragment.json'
)
expect(artifactPrefix('windows', 'windows-v0.2.0')).toBe('artifacts/windows/windows-v0.2.0/')
expect(legacyStatusPrefix('windows')).toBe('release-status/windows/')
expect(legacyStatusPrefix('macos')).toBe('release-status/macos/')
})
})