diff --git a/doc/prd/workbench.md b/doc/prd/workbench.md index ffe9381b..be7d02bc 100644 --- a/doc/prd/workbench.md +++ b/doc/prd/workbench.md @@ -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 实际调用知识库工具;新建干净智能对话、仅切换伙伴时不得凭空出现该过程文案。 diff --git a/doc/release.md b/doc/release.md index d2465f63..b59d2833 100644 --- a/doc/release.md +++ b/doc/release.md @@ -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。 ### 本地保留与清理 diff --git a/doc/tencent-cdn-billing.md b/doc/tencent-cdn-billing.md index f7d30a8b..371e0c2c 100644 --- a/doc/tencent-cdn-billing.md +++ b/doc/tencent-cdn-billing.md @@ -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),不是无限账单。 diff --git a/doc/test-plan/workbench-test.md b/doc/test-plan/workbench-test.md index 8ca91920..fed95f93 100644 --- a/doc/test-plan/workbench-test.md +++ b/doc/test-plan/workbench-test.md @@ -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` diff --git a/scripts/release/r2/finalize-macos.mjs b/scripts/release/r2/finalize-macos.mjs index eaadb55d..23ead318 100644 --- a/scripts/release/r2/finalize-macos.mjs +++ b/scripts/release/r2/finalize-macos.mjs @@ -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 { diff --git a/scripts/release/r2/finalize-windows.mjs b/scripts/release/r2/finalize-windows.mjs index 50b34475..79edb20f 100644 --- a/scripts/release/r2/finalize-windows.mjs +++ b/scripts/release/r2/finalize-windows.mjs @@ -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` diff --git a/scripts/release/r2/storage-layout.mjs b/scripts/release/r2/storage-layout.mjs index 2902e805..4ecd6ad0 100644 --- a/scripts/release/r2/storage-layout.mjs +++ b/scripts/release/r2/storage-layout.mjs @@ -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}/` diff --git a/scripts/release/r2/upload-fragment.mjs b/scripts/release/r2/upload-fragment.mjs index a33080ac..dd123eba 100644 --- a/scripts/release/r2/upload-fragment.mjs +++ b/scripts/release/r2/upload-fragment.mjs @@ -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', diff --git a/scripts/release/shared/windows.mjs b/scripts/release/shared/windows.mjs index 37a2e2ab..dadb1f1e 100644 --- a/scripts/release/shared/windows.mjs +++ b/scripts/release/shared/windows.mjs @@ -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, [ diff --git a/scripts/release/storage/plan.mjs b/scripts/release/storage/plan.mjs index 8728800f..3f4503cb 100644 --- a/scripts/release/storage/plan.mjs +++ b/scripts/release/storage/plan.mjs @@ -9,7 +9,7 @@ const plan = { publicBaseUrl: String(process.env.COS_PUBLIC_BASE_URL || DOMESTIC_DOWNLOAD_BASE_URL), writes: [ 'artifacts///*', - 'release-status//.json', + 'artifacts///fragment.json', 'updates/latest/latest*.yml', 'releases/latest/.json' ], diff --git a/src/renderer/src/components/NotificationCenter.tsx b/src/renderer/src/components/NotificationCenter.tsx index 99657853..6906f5b5 100644 --- a/src/renderer/src/components/NotificationCenter.tsx +++ b/src/renderer/src/components/NotificationCenter.tsx @@ -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', diff --git a/src/renderer/src/components/workbench/AssistantTurnCard.tsx b/src/renderer/src/components/workbench/AssistantTurnCard.tsx index 9d05e273..e9e6535f 100644 --- a/src/renderer/src/components/workbench/AssistantTurnCard.tsx +++ b/src/renderer/src/components/workbench/AssistantTurnCard.tsx @@ -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 if (iconKind === 'camera') return if (iconKind === 'scroll') return - if (iconKind === 'extract') return - if (iconKind === 'window') return - if (iconKind === 'fetch') return + if (iconKind === 'extract') return + if (iconKind === 'window') return + if (iconKind === 'fetch') return return } diff --git a/src/renderer/src/components/workbench/composer/constants.ts b/src/renderer/src/components/workbench/composer/constants.ts index 0ab7a3d2..4c113431 100644 --- a/src/renderer/src/components/workbench/composer/constants.ts +++ b/src/renderer/src/components/workbench/composer/constants.ts @@ -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'] diff --git a/tests/case/unit/release/app-update-publish-config.test.ts b/tests/case/unit/release/app-update-publish-config.test.ts index 4d73b84f..be76b412 100644 --- a/tests/case/unit/release/app-update-publish-config.test.ts +++ b/tests/case/unit/release/app-update-publish-config.test.ts @@ -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') diff --git a/tests/case/unit/release/release-storage-layout.test.ts b/tests/case/unit/release/release-storage-layout.test.ts index 68f80b77..9258d12e 100644 --- a/tests/case/unit/release/release-storage-layout.test.ts +++ b/tests/case/unit/release/release-storage-layout.test.ts @@ -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/') }) })