fix: update attachment paths to use runtime directory
This commit is contained in:
1 parent
9c8c4ff090
commit
749ee4f05e
7 files changed
+96
-18
No files matched your search
@@ -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 的附件上下文只包含顺序、名称和路径,不包含发送前预解析摘要。
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in new issue
Block a user