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',
|
||||
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({
|
||||
scope: 'project',
|
||||
scopeId: project.id,
|
||||
@@ -2205,6 +2192,99 @@ describe('AssistantDatabase', () => {
|
||||
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 () => {
|
||||
const database = await createDatabase()
|
||||
const artifact = database.createInlineArtifact({
|
||||
|
||||
@@ -3166,11 +3166,25 @@ export class AssistantDatabase {
|
||||
CASE WHEN kind = 'image' THEN NULL
|
||||
ELSE inline_content END AS inline_content,
|
||||
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
|
||||
? this.requireDatabase()
|
||||
.prepare(
|
||||
`SELECT ${columns} FROM artifacts
|
||||
WHERE project_id = ?
|
||||
WHERE project_id = ? AND ${visibleArtifact}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?`
|
||||
)
|
||||
@@ -3178,6 +3192,7 @@ export class AssistantDatabase {
|
||||
: this.requireDatabase()
|
||||
.prepare(
|
||||
`SELECT ${columns} FROM artifacts
|
||||
WHERE ${visibleArtifact}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?`
|
||||
)
|
||||
|
||||
@@ -3213,12 +3213,7 @@ describe('registerIpcHandlers agent terminal state', () => {
|
||||
).toBe(expectedOutput)
|
||||
expect(
|
||||
harness.assistantDatabase.createTextArtifact
|
||||
).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
taskId: requestId,
|
||||
content: expectedOutput
|
||||
})
|
||||
)
|
||||
).not.toHaveBeenCalled()
|
||||
await harness.dispose()
|
||||
})
|
||||
|
||||
@@ -4663,6 +4658,9 @@ describe('registerIpcHandlers agent terminal state', () => {
|
||||
'base64'
|
||||
).toString('utf8')
|
||||
).toBe('# 本周报告\n\n已完成。')
|
||||
expect(
|
||||
harness.assistantDatabase.createTextArtifact
|
||||
).not.toHaveBeenCalled()
|
||||
await harness.dispose()
|
||||
})
|
||||
|
||||
|
||||
+5
-23
@@ -1551,7 +1551,7 @@ export function registerIpcHandlers(
|
||||
})
|
||||
}
|
||||
}
|
||||
if (output.trim()) {
|
||||
if (origin !== 'channel' && output.trim()) {
|
||||
assistantDatabase.createTextArtifact({
|
||||
projectId: schedule.projectId,
|
||||
taskId: requestId,
|
||||
@@ -1568,7 +1568,9 @@ export function registerIpcHandlers(
|
||||
body:
|
||||
origin === 'channel'
|
||||
? '结果已回复,并保存到远程通道会话。'
|
||||
: '结果已保存到 GoodBuddy 成果工作栏。'
|
||||
: origin === 'schedule'
|
||||
? '结果已保存到 GoodBuddy 成果工作栏。'
|
||||
: '结果已保存到成果工作栏和委派记录。'
|
||||
})
|
||||
return {
|
||||
status: 'completed',
|
||||
@@ -2413,7 +2415,6 @@ export function registerIpcHandlers(
|
||||
activeRequests.set(request.requestId, controller)
|
||||
|
||||
const execution = (async () => {
|
||||
let outputText = ''
|
||||
let completed = false
|
||||
let runtimeErrorEvent:
|
||||
| Extract<AgentEvent, { type: 'error' }>
|
||||
@@ -2784,15 +2785,6 @@ export function registerIpcHandlers(
|
||||
.slice(0, 120)
|
||||
})
|
||||
: agentEvent
|
||||
if (
|
||||
publicEvent.type === 'text' &&
|
||||
outputText.length < 1_000_000
|
||||
) {
|
||||
outputText += publicEvent.delta.slice(
|
||||
0,
|
||||
1_000_000 - outputText.length
|
||||
)
|
||||
}
|
||||
if (publicEvent.type === 'tool') {
|
||||
toolStates.set(publicEvent.callId, publicEvent)
|
||||
}
|
||||
@@ -2831,23 +2823,13 @@ export function registerIpcHandlers(
|
||||
}
|
||||
if (publicEvent.type === 'done') {
|
||||
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(
|
||||
request.requestId,
|
||||
'completed'
|
||||
)
|
||||
showDesktopNotificationWhenUnfocused(window, {
|
||||
title: 'GoodBuddy 任务已完成',
|
||||
body: '任务结果已保存到成果工作栏。'
|
||||
body: '回复已生成,可返回会话查看。'
|
||||
})
|
||||
if (!window.isDestroyed()) {
|
||||
window.webContents.send(
|
||||
|
||||
@@ -6640,9 +6640,9 @@ describe('App', () => {
|
||||
screen.getByText(/Agent 打开网页后/)
|
||||
).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('tab', { name: '成果' }))
|
||||
expect(screen.getByText('对话与导入成果')).toBeInTheDocument()
|
||||
expect(screen.getByText('生成与导入成果')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText(/查看并预览由对话生成或手动导入/)
|
||||
screen.getByText(/查看并预览由任务、自动化生成或手动导入/)
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('tab', { name: '预览' })
|
||||
@@ -6651,6 +6651,46 @@ describe('App', () => {
|
||||
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 () => {
|
||||
render(<App />)
|
||||
|
||||
|
||||
@@ -2958,8 +2958,8 @@ function App(): React.JSX.Element {
|
||||
[conversations]
|
||||
)
|
||||
const sidebarArtifacts = useMemo<SidebarArtifact[]>(
|
||||
() => {
|
||||
const persisted = assistantArtifacts
|
||||
() =>
|
||||
assistantArtifacts
|
||||
.filter(
|
||||
(artifact) =>
|
||||
!activeProjectId || artifact.projectId === activeProjectId
|
||||
@@ -2970,33 +2970,8 @@ function App(): React.JSX.Element {
|
||||
content: artifact.content ?? '',
|
||||
createdAt: new Date(artifact.createdAt).getTime(),
|
||||
mimeType: artifact.mimeType
|
||||
}))
|
||||
if (persisted.length > 0) {
|
||||
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]
|
||||
})),
|
||||
[activeProjectId, assistantArtifacts]
|
||||
)
|
||||
const enabledSidebarLibraries = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -135,7 +135,6 @@ export const app = {
|
||||
},
|
||||
chat: {
|
||||
user: 'You',
|
||||
assistantResult: 'Assistant result {{index}}',
|
||||
welcome: {
|
||||
eyebrow: 'GOODBUDDY WORKSPACE',
|
||||
title: 'What would you like to accomplish today?',
|
||||
|
||||
@@ -87,7 +87,7 @@ export const workspace = {
|
||||
},
|
||||
results: {
|
||||
label: 'Results',
|
||||
description: 'View generated or imported content'
|
||||
description: 'View generated or imported standalone results'
|
||||
}
|
||||
},
|
||||
tasks: {
|
||||
@@ -151,10 +151,11 @@ export const workspace = {
|
||||
title: 'Results',
|
||||
loadingImage: 'Loading image…',
|
||||
description:
|
||||
'View and preview text, images, PDFs, and web content generated by conversations or imported manually.',
|
||||
sectionTitle: 'Conversation and imported results',
|
||||
'View and preview text, images, PDFs, and web results generated by tasks or automations, or imported manually.',
|
||||
sectionTitle: 'Generated and imported results',
|
||||
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: {
|
||||
title: 'Live browser',
|
||||
|
||||
@@ -132,7 +132,6 @@ export const app = {
|
||||
},
|
||||
chat: {
|
||||
user: '用户',
|
||||
assistantResult: '助手成果 {{index}}',
|
||||
welcome: {
|
||||
eyebrow: 'GOODBUDDY 工作台',
|
||||
title: '今天想一起完成什么?',
|
||||
|
||||
@@ -83,7 +83,7 @@ export const workspace = {
|
||||
},
|
||||
results: {
|
||||
label: '成果',
|
||||
description: '查看对话生成或手动导入的内容'
|
||||
description: '查看生成或手动导入的独立成果'
|
||||
}
|
||||
},
|
||||
tasks: {
|
||||
@@ -142,10 +142,10 @@ export const workspace = {
|
||||
title: '成果',
|
||||
loadingImage: '正在加载图片…',
|
||||
description:
|
||||
'查看并预览由对话生成或手动导入的文本、图片、PDF 与网页内容。',
|
||||
sectionTitle: '对话与导入成果',
|
||||
'查看并预览由任务、自动化生成或手动导入的文本、图片、PDF 与网页成果。',
|
||||
sectionTitle: '生成与导入成果',
|
||||
import: '导入 PDF、图片或网页',
|
||||
empty: '完成的回复会作为可预览成果显示在这里。'
|
||||
empty: '生成的文件、图片、报告和手动导入内容会显示在这里。'
|
||||
},
|
||||
browser: {
|
||||
title: '实时浏览器',
|
||||
|
||||
Reference in New Issue
Block a user