refactor: update release storage layout and fragment handling for improved clarity and consistency
This commit is contained in:
1 parent
4dbd750417
commit
d25a153488
15 files changed
+96
-71
No files matched your search
@@ -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
@@ -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。
|
||||
|
||||
### 本地保留与清理
|
||||
|
||||
|
||||
@@ -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),不是无限账单。
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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,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',
|
||||
|
||||
@@ -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,
|
||||
[
|
||||
|
||||
@@ -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/')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user