fix: update attachment paths to use runtime directory

This commit is contained in:
hy committed 2026-07-15 10:45:01 +08:00
1 parent 9c8c4ff090
commit 749ee4f05e
7 files changed
+96 -18

No files matched your search

+2 -2
View File
@@ -256,10 +256,10 @@
#### WB-COMPOSER-010 附件保存与传递规则
- 系统行为:用户通过 `+` 或粘贴添加附件时,附件统一保存到当前工作区的 `<workspace>/.ncurator/assets/<assetBucketId>/`;`assetBucketId` 是当前输入草稿生成的 UUID,和会话 ID 解耦,添加附件不会提前创建新会话。发送给 AI 的附件上下文只保留顺序、名称和路径,不再自动做图片识别、文件解析或内容摘要注入。
- 系统行为:用户通过 `+` 或粘贴添加附件时,附件统一保存到当前工作区的 `<workspace>/.ncurator/runtime/assets/<assetBucketId>/`;`assetBucketId` 是当前输入草稿生成的 UUID,和会话 ID 解耦,添加附件不会提前创建新会话。发送给 AI 的附件上下文只保留顺序、名称和路径,不再自动做图片识别、文件解析或内容摘要注入。
- 验收:
- `WB-COMPOSER-010-AC1` 未绑定可用工作区时,不允许添加附件,并提示先选择工作区或完成目录绑定。
- `WB-COMPOSER-010-AC2` 已绑定工作区后,`+` 和粘贴添加的附件都会保存到 `<workspace>/.ncurator/assets/<assetBucketId>/`,且不会提前创建真实会话。
- `WB-COMPOSER-010-AC2` 已绑定工作区后,`+` 和粘贴添加的附件都会保存到 `<workspace>/.ncurator/runtime/assets/<assetBucketId>/`,且不会提前创建真实会话。
- `WB-COMPOSER-010-AC3` 当前附件支持范围只有图片、`docx`、`md`、`json`;其他格式提示不支持且不加入附件列表。
- `WB-COMPOSER-010-AC4` 发送给 AI 的附件上下文只包含顺序、名称和路径,不包含发送前预解析摘要。
+2 -2
View File
@@ -44,7 +44,7 @@
- docx apply case 需要真实 docx 输入、真实 AI 编辑、真实写回记录、真实预览和真实回退。
- `browser-use-task-panel` 需要真实 AI run 触发 `browser_use` 并驱动内置浏览器完成本地网页任务;当前已使用 DashScope / openai-compatible / `deepseek-v4-flash` 覆盖 Stagehand 官方 provider 与 `StagehandLangChainClient` 路径,不能用 fake browser 替代。
- `web_search` 与 `web_crawl` 需要分开覆盖:搜索 case 只验证候选网页返回;读取 case 必须验证 AI 继续读取网页正文后再回答。
- Workbench 附件链路当前固定保存到 `<workspace>/.ncurator/assets/<assetBucketId>/`,`assetBucketId` 为输入草稿 UUID,发送给 AI 时只传附件名称与路径,不再做发送前识图或文件解析。
- Workbench 附件链路当前固定保存到 `<workspace>/.ncurator/runtime/assets/<assetBucketId>/`,`assetBucketId` 为输入草稿 UUID,发送给 AI 时只传附件名称与路径,不再做发送前识图或文件解析。
## 3. Suite 设计
@@ -147,7 +147,7 @@ tests/case/workbench/
#### `composer-attachments-context`
- 测试类型:`e2e`
- What:验证 `+` 与粘贴添加附件时,附件会保存到工作区 `.ncurator/assets/<assetBucketId>/`,不会提前创建真实会话,并保持图片预览、文件打开、删除、普通文本粘贴和格式提示行为。
- What:验证 `+` 与粘贴添加附件时,附件会保存到工作区 `.ncurator/runtime/assets/<assetBucketId>/`,不会提前创建真实会话,并保持图片预览、文件打开、删除、普通文本粘贴和格式提示行为。
- Why:附件链路已改为“统一落盘到工作区 + 只把路径交给 AI”,必须用回归测试守住真实保存路径和交互行为。
- How:创建真实工作区,通过真实粘贴图片和 `md` 文件触发产品附件保存逻辑;另外通过粘贴覆盖不支持格式和大文件 warning。
- Oracle:真实粘贴会触发附件保存;添加附件后没有真实会话;图片预览可打开;文件副标题显示 `MD`;不支持格式只提示不加入;大文件只提示不拦截;普通文本粘贴仍正常,对齐 `WB-COMPOSER-010-AC2`、`WB-COMPOSER-010-AC3`。
@@ -35,7 +35,7 @@ export const LLM_RECOGNIZE_TOOL_SPEC: HumanToolSpec<
{
goal: '识别截图里主要展示了什么',
args: {
path: '/workspace/.ncurator/assets/session-1/screen.png',
path: '/workspace/.ncurator/runtime/assets/session-1/screen.png',
instruction: '请概括这张截图的主要内容,并指出用户接下来最可能要做的操作。'
}
}
@@ -3,7 +3,8 @@ import path from 'path'
import { v4 as uuidv4 } from 'uuid'
import type { ChatAttachment } from '../../../shared/types'
const WORKSPACE_RUNTIME_DIR = '.ncurator'
const WORKSPACE_NCURATOR_DIR = '.ncurator'
const WORKSPACE_RUNTIME_DIR = 'runtime'
const WORKSPACE_ASSETS_DIR = 'assets'
const WORKSPACE_SCRIPTS_DIR = 'scripts'
@@ -84,10 +85,16 @@ const isSupportedAttachment = (input: {
return SUPPORTED_FILE_EXTENSIONS.has(ext)
}
const getWorkspaceAssetsDir = (workspaceRootPath: string, assetBucketId: string): string =>
const getWorkspaceRuntimeDir = (workspaceRootPath: string): string =>
path.join(
path.resolve(String(workspaceRootPath || '').trim()),
WORKSPACE_RUNTIME_DIR,
WORKSPACE_NCURATOR_DIR,
WORKSPACE_RUNTIME_DIR
)
const getWorkspaceAssetsDir = (workspaceRootPath: string, assetBucketId: string): string =>
path.join(
getWorkspaceRuntimeDir(workspaceRootPath),
WORKSPACE_ASSETS_DIR,
String(assetBucketId || '').trim()
)
@@ -103,7 +110,7 @@ const ensureWorkspaceAttachmentDirs = async (
const assetDir = getWorkspaceAssetsDir(rootPath, assetBucketId)
await Promise.all([
fs.promises.mkdir(assetDir, { recursive: true }),
fs.promises.mkdir(path.join(rootPath, WORKSPACE_RUNTIME_DIR, WORKSPACE_SCRIPTS_DIR), {
fs.promises.mkdir(path.join(getWorkspaceRuntimeDir(rootPath), WORKSPACE_SCRIPTS_DIR), {
recursive: true
})
])
@@ -34,7 +34,7 @@ describe('ai unit ncurator ref codec', () => {
{
name: 'windows path with dot-prefixed hidden dir (the reported bug)',
label: '交互式探究学习案例:产业转移.docx',
path: 'D:\\1-work-space\\test\\workspace\\.ncurator\\assets\\d6fd02d9-b641-4a73-b047-0f286e2e5072\\交互式探究学习案例:产业转移.docx',
path: 'D:\\1-work-space\\test\\workspace\\.ncurator\\runtime\\assets\\d6fd02d9-b641-4a73-b047-0f286e2e5072\\交互式探究学习案例:产业转移.docx',
resourceType: 'file'
},
{
@@ -146,7 +146,7 @@ describe('ai unit ncurator ref codec', () => {
it('parses raw windows paths emitted without canonical encoding', () => {
const rawPath =
'D:\\1-work-space\\test\\workspace\\.ncurator\\assets\\d6fd02d9-b641-4a73-b047-0f286e2e5072\\交互式探究学习案例:产业转移.docx'
'D:\\1-work-space\\test\\workspace\\.ncurator\\runtime\\assets\\d6fd02d9-b641-4a73-b047-0f286e2e5072\\交互式探究学习案例:产业转移.docx'
const href = `ncurator://ref?kind=reference&resourceType=file&label=交互式探究学习案例:产业转移.docx&path=${rawPath}`
expect(parseNcuratorRefHref(href)).toEqual({
@@ -160,12 +160,12 @@ describe('ai unit ncurator ref codec', () => {
it('normalizes raw markdown links before CommonMark can consume path backslashes', () => {
const rawPath =
'D:\\1-work-space\\test\\workspace\\.ncurator\\assets\\d6fd02d9-b641-4a73-b047-0f286e2e5072\\交互式探究学习案例:产业转移.docx'
'D:\\1-work-space\\test\\workspace\\.ncurator\\runtime\\assets\\d6fd02d9-b641-4a73-b047-0f286e2e5072\\交互式探究学习案例:产业转移.docx'
const markdown = `[交互式探究学习案例:产业转移.docx](ncurator://ref?kind=reference&resourceType=file&label=交互式探究学习案例:产业转移.docx&path=${rawPath})`
const normalized = normalizeNcuratorRefMarkdownLinks(markdown)
const href = extractHref(normalized)
expect(href).toContain('%5C.ncurator%5Cassets')
expect(href).toContain('%5C.ncurator%5Cruntime%5Cassets')
expect(commonmarkUnescapeDestination(href)).toBe(href)
expect(parseNcuratorRefHref(href)).toEqual({
type: 'reference',
@@ -0,0 +1,71 @@
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { AttachmentService } from '../../../../src/main/services/attachments/AttachmentService'
describe('attachment service runtime directories', () => {
let workspaceRootPath = ''
let sourceRootPath = ''
let service: AttachmentService
beforeEach(async () => {
workspaceRootPath = await fs.mkdtemp(path.join(os.tmpdir(), 'ncurator-attachment-workspace-'))
sourceRootPath = await fs.mkdtemp(path.join(os.tmpdir(), 'ncurator-attachment-source-'))
service = new AttachmentService()
})
afterEach(async () => {
await Promise.all(
[workspaceRootPath, sourceRootPath]
.filter(Boolean)
.map((directoryPath) => fs.rm(directoryPath, { recursive: true, force: true }))
)
})
it('saves attachments under .ncurator/runtime and creates the scripts directory', async () => {
const attachment = await service.saveAttachment({
workspaceRootPath,
assetBucketId: 'bucket-1',
name: 'notes.md',
bytes: Buffer.from('# runtime attachment')
})
const expectedPath = path.join(
workspaceRootPath,
'.ncurator',
'runtime',
'assets',
'bucket-1',
'notes.md'
)
expect(attachment.path).toBe(expectedPath)
await expect(fs.readFile(expectedPath, 'utf8')).resolves.toBe('# runtime attachment')
expect(
(await fs.stat(path.join(workspaceRootPath, '.ncurator', 'runtime', 'scripts'))).isDirectory()
).toBe(true)
await expect(fs.stat(path.join(workspaceRootPath, '.ncurator', 'assets'))).rejects.toThrow()
await expect(fs.stat(path.join(workspaceRootPath, '.ncurator', 'scripts'))).rejects.toThrow()
})
it('imports attachments into the runtime bucket and keeps names unique', async () => {
const sourcePath = path.join(sourceRootPath, 'reference.json')
await fs.writeFile(sourcePath, '{"ready":true}')
const first = await service.importAttachmentFromPath({
workspaceRootPath,
assetBucketId: 'bucket-2',
sourcePath
})
const second = await service.importAttachmentFromPath({
workspaceRootPath,
assetBucketId: 'bucket-2',
sourcePath
})
const bucketPath = path.join(workspaceRootPath, '.ncurator', 'runtime', 'assets', 'bucket-2')
expect(first.path).toBe(path.join(bucketPath, 'reference.json'))
expect(second.path).toBe(path.join(bucketPath, 'reference-1.json'))
await expect(fs.readFile(second.path, 'utf8')).resolves.toBe('{"ready":true}')
})
})
@@ -35,12 +35,12 @@ describe('workbench markdown renderer reference links', () => {
})
it('opens raw windows ncurator file references with the original path', async () => {
const { default: MarkdownRenderer } = await import(
'../../../../src/renderer/src/components/MarkdownRenderer'
)
const { default: MarkdownRenderer } =
await import('../../../../src/renderer/src/components/MarkdownRenderer')
const onOpenReference = vi.fn()
const fileName = '\u4ea4\u4e92\u5f0f\u63a2\u7a76\u5b66\u4e60\u6848\u4f8b\uff1a\u4ea7\u4e1a\u8f6c\u79fb.docx'
const filePath = `D:\\1-work-space\\test\\workspace\\.ncurator\\assets\\d6fd02d9-b641-4a73-b047-0f286e2e5072\\${fileName}`
const fileName =
'\u4ea4\u4e92\u5f0f\u63a2\u7a76\u5b66\u4e60\u6848\u4f8b\uff1a\u4ea7\u4e1a\u8f6c\u79fb.docx'
const filePath = `D:\\1-work-space\\test\\workspace\\.ncurator\\runtime\\assets\\d6fd02d9-b641-4a73-b047-0f286e2e5072\\${fileName}`
const content = `[${fileName}](ncurator://ref?kind=reference&resourceType=file&label=${fileName}&path=${filePath})`
await act(async () => {