fix: keep chat replies out of results
Completed local and channel replies were duplicated into Results as Markdown artifacts, making the panel a second conversation history. Replies now stay in their conversations, legacy duplicates are filtered without deleting data, and standalone images, scheduled jobs, headless delegations, heartbeats, and imports remain visible. Release note: 普通本地与消息通道回复不再重复出现在成果栏;已有重复内容仅从列表隐藏,不会删除历史数据。
This commit is contained in:
@@ -974,19 +974,6 @@ describe('AssistantDatabase', () => {
|
|||||||
status: 'completed',
|
status: 'completed',
|
||||||
completedAt: expect.any(String)
|
completedAt: expect.any(String)
|
||||||
})
|
})
|
||||||
const artifact = database.createTextArtifact({
|
|
||||||
projectId: project.id,
|
|
||||||
taskId,
|
|
||||||
title: '发布说明',
|
|
||||||
content: '# 发布说明\n\n内容'
|
|
||||||
})
|
|
||||||
expect(database.listArtifacts(project.id)).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: artifact.id,
|
|
||||||
kind: 'markdown',
|
|
||||||
content: '# 发布说明\n\n内容'
|
|
||||||
})
|
|
||||||
])
|
|
||||||
const memory = database.createMemory({
|
const memory = database.createMemory({
|
||||||
scope: 'project',
|
scope: 'project',
|
||||||
scopeId: project.id,
|
scopeId: project.id,
|
||||||
@@ -2205,6 +2192,99 @@ describe('AssistantDatabase', () => {
|
|||||||
durable.close()
|
durable.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps duplicate chat replies out of artifact listings', async () => {
|
||||||
|
const database = await createDatabase()
|
||||||
|
const project = database.listProjects()[0]!
|
||||||
|
const chatTaskId = '00000000-0000-4000-8000-000000000231'
|
||||||
|
const channelTaskId = '00000000-0000-4000-8000-000000000232'
|
||||||
|
const scheduleTaskId = '00000000-0000-4000-8000-000000000233'
|
||||||
|
const delegationTaskId = '00000000-0000-4000-8000-000000000234'
|
||||||
|
database.createTask({
|
||||||
|
id: chatTaskId,
|
||||||
|
projectId: project.id,
|
||||||
|
conversationId: 'conversation-chat',
|
||||||
|
title: '普通对话',
|
||||||
|
instructions: '回答问题',
|
||||||
|
workMode: 'ask'
|
||||||
|
})
|
||||||
|
database.createTask({
|
||||||
|
id: channelTaskId,
|
||||||
|
projectId: project.id,
|
||||||
|
conversationId: 'conversation-channel',
|
||||||
|
title: '远程对话',
|
||||||
|
instructions: '回答远程消息',
|
||||||
|
workMode: 'ask',
|
||||||
|
origin: 'delegation'
|
||||||
|
})
|
||||||
|
database.createTask({
|
||||||
|
id: scheduleTaskId,
|
||||||
|
projectId: project.id,
|
||||||
|
conversationId: 'schedule:daily',
|
||||||
|
title: '每日报告',
|
||||||
|
instructions: '生成报告',
|
||||||
|
workMode: 'ask',
|
||||||
|
origin: 'schedule'
|
||||||
|
})
|
||||||
|
database.createTask({
|
||||||
|
id: delegationTaskId,
|
||||||
|
projectId: project.id,
|
||||||
|
conversationId: 'delegation:weekly-report',
|
||||||
|
title: '委派报告',
|
||||||
|
instructions: '生成远程委派报告',
|
||||||
|
workMode: 'ask',
|
||||||
|
origin: 'delegation'
|
||||||
|
})
|
||||||
|
const chatReply = database.createTextArtifact({
|
||||||
|
projectId: project.id,
|
||||||
|
taskId: chatTaskId,
|
||||||
|
title: '普通对话',
|
||||||
|
content: '普通回复'
|
||||||
|
})
|
||||||
|
const channelReply = database.createTextArtifact({
|
||||||
|
projectId: project.id,
|
||||||
|
taskId: channelTaskId,
|
||||||
|
title: '远程对话',
|
||||||
|
content: '远程回复'
|
||||||
|
})
|
||||||
|
const scheduledReport = database.createTextArtifact({
|
||||||
|
projectId: project.id,
|
||||||
|
taskId: scheduleTaskId,
|
||||||
|
title: '每日报告',
|
||||||
|
content: '# 每日报告'
|
||||||
|
})
|
||||||
|
const delegatedReport = database.createTextArtifact({
|
||||||
|
projectId: project.id,
|
||||||
|
taskId: delegationTaskId,
|
||||||
|
title: '委派报告',
|
||||||
|
content: '# 委派报告'
|
||||||
|
})
|
||||||
|
const generatedImage = database.createImageArtifact({
|
||||||
|
projectId: project.id,
|
||||||
|
taskId: chatTaskId,
|
||||||
|
title: '生成图片',
|
||||||
|
mimeType: 'image/png',
|
||||||
|
base64: 'iVBORw0KGgo='
|
||||||
|
})
|
||||||
|
|
||||||
|
const visibleArtifactIds = database
|
||||||
|
.listArtifacts(project.id)
|
||||||
|
.map((artifact) => artifact.id)
|
||||||
|
expect(visibleArtifactIds).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
scheduledReport.id,
|
||||||
|
delegatedReport.id,
|
||||||
|
generatedImage.id
|
||||||
|
])
|
||||||
|
)
|
||||||
|
expect(visibleArtifactIds).not.toContain(chatReply.id)
|
||||||
|
expect(visibleArtifactIds).not.toContain(channelReply.id)
|
||||||
|
expect(database.getArtifact(chatReply.id)).toMatchObject({
|
||||||
|
id: chatReply.id,
|
||||||
|
content: '普通回复'
|
||||||
|
})
|
||||||
|
database.close()
|
||||||
|
})
|
||||||
|
|
||||||
it('loads image artifact content only when requested by id', async () => {
|
it('loads image artifact content only when requested by id', async () => {
|
||||||
const database = await createDatabase()
|
const database = await createDatabase()
|
||||||
const artifact = database.createInlineArtifact({
|
const artifact = database.createInlineArtifact({
|
||||||
|
|||||||
@@ -3166,11 +3166,25 @@ export class AssistantDatabase {
|
|||||||
CASE WHEN kind = 'image' THEN NULL
|
CASE WHEN kind = 'image' THEN NULL
|
||||||
ELSE inline_content END AS inline_content,
|
ELSE inline_content END AS inline_content,
|
||||||
byte_size, created_at, updated_at`
|
byte_size, created_at, updated_at`
|
||||||
|
const visibleArtifact = `NOT (
|
||||||
|
kind = 'markdown' AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM tasks
|
||||||
|
WHERE tasks.id = artifacts.task_id
|
||||||
|
AND tasks.conversation_id IS NOT NULL
|
||||||
|
AND (
|
||||||
|
tasks.origin = 'user' OR (
|
||||||
|
tasks.origin = 'delegation'
|
||||||
|
AND tasks.conversation_id NOT LIKE 'delegation:%'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)`
|
||||||
const rows = projectId
|
const rows = projectId
|
||||||
? this.requireDatabase()
|
? this.requireDatabase()
|
||||||
.prepare(
|
.prepare(
|
||||||
`SELECT ${columns} FROM artifacts
|
`SELECT ${columns} FROM artifacts
|
||||||
WHERE project_id = ?
|
WHERE project_id = ? AND ${visibleArtifact}
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT ?`
|
LIMIT ?`
|
||||||
)
|
)
|
||||||
@@ -3178,6 +3192,7 @@ export class AssistantDatabase {
|
|||||||
: this.requireDatabase()
|
: this.requireDatabase()
|
||||||
.prepare(
|
.prepare(
|
||||||
`SELECT ${columns} FROM artifacts
|
`SELECT ${columns} FROM artifacts
|
||||||
|
WHERE ${visibleArtifact}
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT ?`
|
LIMIT ?`
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3213,12 +3213,7 @@ describe('registerIpcHandlers agent terminal state', () => {
|
|||||||
).toBe(expectedOutput)
|
).toBe(expectedOutput)
|
||||||
expect(
|
expect(
|
||||||
harness.assistantDatabase.createTextArtifact
|
harness.assistantDatabase.createTextArtifact
|
||||||
).toHaveBeenCalledWith(
|
).not.toHaveBeenCalled()
|
||||||
expect.objectContaining({
|
|
||||||
taskId: requestId,
|
|
||||||
content: expectedOutput
|
|
||||||
})
|
|
||||||
)
|
|
||||||
await harness.dispose()
|
await harness.dispose()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -4663,6 +4658,9 @@ describe('registerIpcHandlers agent terminal state', () => {
|
|||||||
'base64'
|
'base64'
|
||||||
).toString('utf8')
|
).toString('utf8')
|
||||||
).toBe('# 本周报告\n\n已完成。')
|
).toBe('# 本周报告\n\n已完成。')
|
||||||
|
expect(
|
||||||
|
harness.assistantDatabase.createTextArtifact
|
||||||
|
).not.toHaveBeenCalled()
|
||||||
await harness.dispose()
|
await harness.dispose()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+5
-23
@@ -1551,7 +1551,7 @@ export function registerIpcHandlers(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (output.trim()) {
|
if (origin !== 'channel' && output.trim()) {
|
||||||
assistantDatabase.createTextArtifact({
|
assistantDatabase.createTextArtifact({
|
||||||
projectId: schedule.projectId,
|
projectId: schedule.projectId,
|
||||||
taskId: requestId,
|
taskId: requestId,
|
||||||
@@ -1568,7 +1568,9 @@ export function registerIpcHandlers(
|
|||||||
body:
|
body:
|
||||||
origin === 'channel'
|
origin === 'channel'
|
||||||
? '结果已回复,并保存到远程通道会话。'
|
? '结果已回复,并保存到远程通道会话。'
|
||||||
: '结果已保存到 GoodBuddy 成果工作栏。'
|
: origin === 'schedule'
|
||||||
|
? '结果已保存到 GoodBuddy 成果工作栏。'
|
||||||
|
: '结果已保存到成果工作栏和委派记录。'
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
status: 'completed',
|
status: 'completed',
|
||||||
@@ -2413,7 +2415,6 @@ export function registerIpcHandlers(
|
|||||||
activeRequests.set(request.requestId, controller)
|
activeRequests.set(request.requestId, controller)
|
||||||
|
|
||||||
const execution = (async () => {
|
const execution = (async () => {
|
||||||
let outputText = ''
|
|
||||||
let completed = false
|
let completed = false
|
||||||
let runtimeErrorEvent:
|
let runtimeErrorEvent:
|
||||||
| Extract<AgentEvent, { type: 'error' }>
|
| Extract<AgentEvent, { type: 'error' }>
|
||||||
@@ -2784,15 +2785,6 @@ export function registerIpcHandlers(
|
|||||||
.slice(0, 120)
|
.slice(0, 120)
|
||||||
})
|
})
|
||||||
: agentEvent
|
: agentEvent
|
||||||
if (
|
|
||||||
publicEvent.type === 'text' &&
|
|
||||||
outputText.length < 1_000_000
|
|
||||||
) {
|
|
||||||
outputText += publicEvent.delta.slice(
|
|
||||||
0,
|
|
||||||
1_000_000 - outputText.length
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (publicEvent.type === 'tool') {
|
if (publicEvent.type === 'tool') {
|
||||||
toolStates.set(publicEvent.callId, publicEvent)
|
toolStates.set(publicEvent.callId, publicEvent)
|
||||||
}
|
}
|
||||||
@@ -2831,23 +2823,13 @@ export function registerIpcHandlers(
|
|||||||
}
|
}
|
||||||
if (publicEvent.type === 'done') {
|
if (publicEvent.type === 'done') {
|
||||||
completed = true
|
completed = true
|
||||||
if (outputText.trim()) {
|
|
||||||
assistantDatabase.createTextArtifact({
|
|
||||||
projectId: request.projectId,
|
|
||||||
taskId: request.requestId,
|
|
||||||
title: parsedRequest.prompt
|
|
||||||
.split(/\r?\n/, 1)[0]!
|
|
||||||
.slice(0, 120),
|
|
||||||
content: outputText
|
|
||||||
})
|
|
||||||
}
|
|
||||||
assistantDatabase.updateTaskStatus(
|
assistantDatabase.updateTaskStatus(
|
||||||
request.requestId,
|
request.requestId,
|
||||||
'completed'
|
'completed'
|
||||||
)
|
)
|
||||||
showDesktopNotificationWhenUnfocused(window, {
|
showDesktopNotificationWhenUnfocused(window, {
|
||||||
title: 'GoodBuddy 任务已完成',
|
title: 'GoodBuddy 任务已完成',
|
||||||
body: '任务结果已保存到成果工作栏。'
|
body: '回复已生成,可返回会话查看。'
|
||||||
})
|
})
|
||||||
if (!window.isDestroyed()) {
|
if (!window.isDestroyed()) {
|
||||||
window.webContents.send(
|
window.webContents.send(
|
||||||
|
|||||||
@@ -6640,9 +6640,9 @@ describe('App', () => {
|
|||||||
screen.getByText(/Agent 打开网页后/)
|
screen.getByText(/Agent 打开网页后/)
|
||||||
).toBeInTheDocument()
|
).toBeInTheDocument()
|
||||||
fireEvent.click(screen.getByRole('tab', { name: '成果' }))
|
fireEvent.click(screen.getByRole('tab', { name: '成果' }))
|
||||||
expect(screen.getByText('对话与导入成果')).toBeInTheDocument()
|
expect(screen.getByText('生成与导入成果')).toBeInTheDocument()
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(/查看并预览由对话生成或手动导入/)
|
screen.getByText(/查看并预览由任务、自动化生成或手动导入/)
|
||||||
).toBeInTheDocument()
|
).toBeInTheDocument()
|
||||||
expect(
|
expect(
|
||||||
screen.queryByRole('tab', { name: '预览' })
|
screen.queryByRole('tab', { name: '预览' })
|
||||||
@@ -6651,6 +6651,46 @@ describe('App', () => {
|
|||||||
expect(sidebar).not.toHaveClass('assistant-sidebar--open')
|
expect(sidebar).not.toHaveClass('assistant-sidebar--open')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps completed chat replies out of the results sidebar', async () => {
|
||||||
|
render(<App />)
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
|
||||||
|
target: { value: '普通聊天问题' }
|
||||||
|
})
|
||||||
|
fireEvent.click(await screen.findByLabelText('发送'))
|
||||||
|
await waitFor(() => expect(run).toHaveBeenCalledOnce())
|
||||||
|
const request = run.mock.calls[0]?.[0]
|
||||||
|
if (!request) {
|
||||||
|
throw new Error('Missing request')
|
||||||
|
}
|
||||||
|
act(() => {
|
||||||
|
agentListener?.({
|
||||||
|
requestId: request.requestId,
|
||||||
|
type: 'text',
|
||||||
|
delta: '这是一条普通聊天回复'
|
||||||
|
})
|
||||||
|
agentListener?.({
|
||||||
|
requestId: request.requestId,
|
||||||
|
type: 'done'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
expect(
|
||||||
|
await screen.findByText('这是一条普通聊天回复')
|
||||||
|
).toBeInTheDocument()
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByLabelText('切换助手工作栏'))
|
||||||
|
fireEvent.click(screen.getByRole('tab', { name: '成果' }))
|
||||||
|
const sidebar = screen.getByLabelText('助手工作栏')
|
||||||
|
expect(
|
||||||
|
within(sidebar).queryByText('这是一条普通聊天回复')
|
||||||
|
).not.toBeInTheDocument()
|
||||||
|
expect(
|
||||||
|
within(sidebar).getByText(
|
||||||
|
'生成的文件、图片、报告和手动导入内容会显示在这里。'
|
||||||
|
)
|
||||||
|
).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('opens the live browser tab for the active conversation and can stop it', async () => {
|
it('opens the live browser tab for the active conversation and can stop it', async () => {
|
||||||
render(<App />)
|
render(<App />)
|
||||||
|
|
||||||
|
|||||||
@@ -2958,8 +2958,8 @@ function App(): React.JSX.Element {
|
|||||||
[conversations]
|
[conversations]
|
||||||
)
|
)
|
||||||
const sidebarArtifacts = useMemo<SidebarArtifact[]>(
|
const sidebarArtifacts = useMemo<SidebarArtifact[]>(
|
||||||
() => {
|
() =>
|
||||||
const persisted = assistantArtifacts
|
assistantArtifacts
|
||||||
.filter(
|
.filter(
|
||||||
(artifact) =>
|
(artifact) =>
|
||||||
!activeProjectId || artifact.projectId === activeProjectId
|
!activeProjectId || artifact.projectId === activeProjectId
|
||||||
@@ -2970,33 +2970,8 @@ function App(): React.JSX.Element {
|
|||||||
content: artifact.content ?? '',
|
content: artifact.content ?? '',
|
||||||
createdAt: new Date(artifact.createdAt).getTime(),
|
createdAt: new Date(artifact.createdAt).getTime(),
|
||||||
mimeType: artifact.mimeType
|
mimeType: artifact.mimeType
|
||||||
}))
|
})),
|
||||||
if (persisted.length > 0) {
|
[activeProjectId, assistantArtifacts]
|
||||||
return persisted
|
|
||||||
}
|
|
||||||
return (activeConversation?.messages ?? [])
|
|
||||||
.filter(
|
|
||||||
(message) =>
|
|
||||||
message.role === 'assistant' &&
|
|
||||||
message.state === 'complete' &&
|
|
||||||
message.content.trim()
|
|
||||||
)
|
|
||||||
.slice(-20)
|
|
||||||
.reverse()
|
|
||||||
.map((message, index) => ({
|
|
||||||
id: message.id,
|
|
||||||
title:
|
|
||||||
message.content
|
|
||||||
.split(/\r?\n/, 1)[0]
|
|
||||||
?.replace(/^#+\s*/, '')
|
|
||||||
.slice(0, 48) ||
|
|
||||||
t('chat.assistantResult', { index: index + 1 }),
|
|
||||||
content: message.content,
|
|
||||||
createdAt: message.createdAt,
|
|
||||||
mimeType: 'text/markdown'
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
[activeConversation, activeProjectId, assistantArtifacts, t]
|
|
||||||
)
|
)
|
||||||
const enabledSidebarLibraries = useMemo(
|
const enabledSidebarLibraries = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ export const app = {
|
|||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
user: 'You',
|
user: 'You',
|
||||||
assistantResult: 'Assistant result {{index}}',
|
|
||||||
welcome: {
|
welcome: {
|
||||||
eyebrow: 'GOODBUDDY WORKSPACE',
|
eyebrow: 'GOODBUDDY WORKSPACE',
|
||||||
title: 'What would you like to accomplish today?',
|
title: 'What would you like to accomplish today?',
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const workspace = {
|
|||||||
},
|
},
|
||||||
results: {
|
results: {
|
||||||
label: 'Results',
|
label: 'Results',
|
||||||
description: 'View generated or imported content'
|
description: 'View generated or imported standalone results'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tasks: {
|
tasks: {
|
||||||
@@ -151,10 +151,11 @@ export const workspace = {
|
|||||||
title: 'Results',
|
title: 'Results',
|
||||||
loadingImage: 'Loading image…',
|
loadingImage: 'Loading image…',
|
||||||
description:
|
description:
|
||||||
'View and preview text, images, PDFs, and web content generated by conversations or imported manually.',
|
'View and preview text, images, PDFs, and web results generated by tasks or automations, or imported manually.',
|
||||||
sectionTitle: 'Conversation and imported results',
|
sectionTitle: 'Generated and imported results',
|
||||||
import: 'Import PDF, image, or web page',
|
import: 'Import PDF, image, or web page',
|
||||||
empty: 'Completed responses will appear here as previewable results.'
|
empty:
|
||||||
|
'Generated files, images, reports, and manually imported content will appear here.'
|
||||||
},
|
},
|
||||||
browser: {
|
browser: {
|
||||||
title: 'Live browser',
|
title: 'Live browser',
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ export const app = {
|
|||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
user: '用户',
|
user: '用户',
|
||||||
assistantResult: '助手成果 {{index}}',
|
|
||||||
welcome: {
|
welcome: {
|
||||||
eyebrow: 'GOODBUDDY 工作台',
|
eyebrow: 'GOODBUDDY 工作台',
|
||||||
title: '今天想一起完成什么?',
|
title: '今天想一起完成什么?',
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export const workspace = {
|
|||||||
},
|
},
|
||||||
results: {
|
results: {
|
||||||
label: '成果',
|
label: '成果',
|
||||||
description: '查看对话生成或手动导入的内容'
|
description: '查看生成或手动导入的独立成果'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tasks: {
|
tasks: {
|
||||||
@@ -142,10 +142,10 @@ export const workspace = {
|
|||||||
title: '成果',
|
title: '成果',
|
||||||
loadingImage: '正在加载图片…',
|
loadingImage: '正在加载图片…',
|
||||||
description:
|
description:
|
||||||
'查看并预览由对话生成或手动导入的文本、图片、PDF 与网页内容。',
|
'查看并预览由任务、自动化生成或手动导入的文本、图片、PDF 与网页成果。',
|
||||||
sectionTitle: '对话与导入成果',
|
sectionTitle: '生成与导入成果',
|
||||||
import: '导入 PDF、图片或网页',
|
import: '导入 PDF、图片或网页',
|
||||||
empty: '完成的回复会作为可预览成果显示在这里。'
|
empty: '生成的文件、图片、报告和手动导入内容会显示在这里。'
|
||||||
},
|
},
|
||||||
browser: {
|
browser: {
|
||||||
title: '实时浏览器',
|
title: '实时浏览器',
|
||||||
|
|||||||
Reference in New Issue
Block a user