chore: prepare GoodBuddy 0.8.5

This commit is contained in:
lofyer
2026-08-07 10:45:01 +08:00
parent e20cb447af
commit 17e66a3369
54 changed files with 1306 additions and 1640 deletions
+20 -4
View File
@@ -122,7 +122,6 @@ const api: DesktopApi = {
continueMode: 'chat',
runtimeSandboxMode: 'auto',
subagentSmartRoutingEnabled: false,
intranetCompatibilityEnabled: true,
knowledgeEmbeddingEnabled: false,
knowledgeEmbeddingBaseUrl:
'http://127.0.0.1:11434/v1/embeddings',
@@ -175,8 +174,6 @@ const api: DesktopApi = {
runtimeSandboxMode: input.runtimeSandboxMode,
subagentSmartRoutingEnabled:
input.subagentSmartRoutingEnabled ?? false,
intranetCompatibilityEnabled:
input.intranetCompatibilityEnabled ?? true,
knowledgeEmbeddingEnabled: input.knowledgeEmbeddingEnabled,
knowledgeEmbeddingBaseUrl: input.knowledgeEmbeddingBaseUrl,
knowledgeEmbeddingModel: input.knowledgeEmbeddingModel,
@@ -806,11 +803,30 @@ describe('App', () => {
})
agentListener?.({
requestId: request.requestId,
type: 'done'
type: 'reasoning',
delta: '先检查项目结构'
})
})
expect(await screen.findByText('这是回答内容')).toBeInTheDocument()
const streamingReasoning = screen
.getByText('正在推理')
.closest('details')
expect(streamingReasoning).toHaveAttribute('open')
expect(screen.getByText('先检查项目结构')).toBeInTheDocument()
act(() => {
if (!request) {
throw new Error('Missing request')
}
agentListener?.({
requestId: request.requestId,
type: 'done'
})
})
const completedReasoning = await screen.findByText('推理过程')
expect(completedReasoning.closest('details')).not.toHaveAttribute('open')
expect(screen.getByText('项目:默认项目')).toHaveClass('scope-badge')
})
+27
View File
@@ -297,6 +297,7 @@ type Message = {
id: string
role: 'user' | 'assistant'
content: string
reasoning?: string
createdAt: number
state: 'streaming' | 'complete' | 'error'
status?: string
@@ -489,6 +490,8 @@ function isConversation(value: unknown): value is Conversation {
(entry.role === 'user' || entry.role === 'assistant') &&
typeof entry.content === 'string' &&
entry.content.length <= 1_000_000 &&
(entry.reasoning === undefined ||
typeof entry.reasoning === 'string') &&
typeof entry.createdAt === 'number' &&
(entry.state === 'streaming' ||
entry.state === 'complete' ||
@@ -521,6 +524,7 @@ function toConversationSnapshots(
id: message.id,
role: message.role,
content: message.content,
reasoning: message.reasoning,
createdAt: message.createdAt,
state: message.state,
status: message.status,
@@ -1677,6 +1681,11 @@ function App(): React.JSX.Element {
? '回答过长,已在本地截断显示'
: undefined
}))
} else if (event.type === 'reasoning') {
updateMessage(run.conversationId, run.messageId, (message) => ({
...message,
reasoning: `${message.reasoning ?? ''}${event.delta}`
}))
} else if (event.type === 'status') {
updateMessage(run.conversationId, run.messageId, (message) => ({
...message,
@@ -3892,6 +3901,24 @@ function App(): React.JSX.Element {
})}
</div>
)}
{message.reasoning && (
<details
className="message-reasoning"
key={`${message.id}-${message.state}`}
open={message.state === 'streaming'}
>
<summary>
{message.state === 'streaming'
? '正在推理'
: '推理过程'}
</summary>
<div className="markdown-content message-reasoning__content">
<MarkdownRenderer>
{message.reasoning}
</MarkdownRenderer>
</div>
</details>
)}
{message.content && (
<div className="markdown-content message__content">
<MarkdownRenderer>
+27 -40
View File
@@ -41,7 +41,6 @@ const runtimeSettings: RuntimeSettings = {
continueMode: 'chat',
runtimeSandboxMode: 'auto',
subagentSmartRoutingEnabled: false,
intranetCompatibilityEnabled: true,
knowledgeEmbeddingEnabled: false,
knowledgeEmbeddingBaseUrl:
'http://127.0.0.1:11434/v1/embeddings',
@@ -563,43 +562,6 @@ describe('SettingsPanel runtime files', () => {
)
})
it('shows and saves the global intranet compatibility mode', async () => {
render(
<SettingsPanel
{...heartbeatSettingsProps}
open
onClearLocalData={vi.fn(async () => {})}
onClose={vi.fn()}
onSaved={vi.fn()}
/>
)
fireEvent.click(screen.getByRole('tab', { name: '安全与数据' }))
const intranetCompatibility = await screen.findByRole('checkbox', {
name: '内网兼容模式'
})
expect(intranetCompatibility).toBeChecked()
const warning = screen.getByText(/HTTP 传输未加密/)
expect(warning).toHaveTextContent(
'无效、自签名或已过期的 HTTPS 证书'
)
expect(warning).toHaveTextContent('整个应用')
expect(warning).toHaveTextContent(
'关闭后恢复严格的地址与证书校验'
)
fireEvent.click(intranetCompatibility)
expect(intranetCompatibility).not.toBeChecked()
expect(warning).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '保存设置' }))
await waitFor(() =>
expect(updateRuntime).toHaveBeenCalledWith(
expect.objectContaining({
intranetCompatibilityEnabled: false
})
)
)
})
it('automatically detects runtimes and displays path, version, and detail', async () => {
render(
@@ -925,6 +887,31 @@ describe('SettingsPanel runtime files', () => {
)
})
it('orders model protocols by the preferred connection flow', async () => {
render(
<SettingsPanel
{...heartbeatSettingsProps}
open
onClearLocalData={vi.fn(async () => {})}
onClose={vi.fn()}
onSaved={vi.fn()}
/>
)
fireEvent.click(screen.getByRole('tab', { name: '模型连接' }))
const protocol = await screen.findByLabelText('接口协议 默认模型')
expect(
within(protocol)
.getAllByRole('option')
.map((option) => (option as HTMLOptionElement).value)
).toEqual([
'openai-chat-completions',
'openai-responses',
'anthropic-messages',
'openai-images-generations'
])
})
it('assigns an OpenAI Responses connection to both Agent Runtimes', async () => {
render(
<SettingsPanel
@@ -1196,7 +1183,7 @@ describe('SettingsPanel runtime files', () => {
it('shows the first settings validation issue without IPC wrappers', async () => {
updateRuntime.mockRejectedValueOnce(
new Error(
"Error invoking remote method 'settings:runtime:update': [ { \"code\": \"custom\", \"path\": [ \"modelProfiles\", 0, \"baseUrl\" ], \"message\": \"模型服务地址必须使用 HTTPS\" } ]"
"Error invoking remote method 'settings:runtime:update': [ { \"code\": \"custom\", \"path\": [ \"modelProfiles\", 0, \"baseUrl\" ], \"message\": \"模型服务地址必须使用 HTTP 或 HTTPS\" } ]"
)
)
render(
@@ -1213,7 +1200,7 @@ describe('SettingsPanel runtime files', () => {
fireEvent.click(screen.getByRole('button', { name: '保存设置' }))
expect(
await screen.findByText('模型服务地址必须使用 HTTPS')
await screen.findByText('模型服务地址必须使用 HTTP 或 HTTPS')
).toBeInTheDocument()
expect(screen.queryByText(/Error invoking remote method/u))
.not.toBeInTheDocument()
+5 -45
View File
@@ -313,12 +313,6 @@ export function SettingsPanel({
subagentSmartRoutingEnabled,
setSubagentSmartRoutingEnabled
] = useState(false)
const [
intranetCompatibilityEnabled,
setIntranetCompatibilityEnabled
] = useState<boolean>(
defaultRuntimeSettings.intranetCompatibilityEnabled
)
const [saving, setSaving] = useState(false)
const [testing, setTesting] = useState(false)
const [embeddingSnapshot, setEmbeddingSnapshot] =
@@ -419,9 +413,6 @@ export function SettingsPanel({
setSubagentSmartRoutingEnabled(
value.subagentSmartRoutingEnabled
)
setIntranetCompatibilityEnabled(
value.intranetCompatibilityEnabled
)
})
.catch((reason: unknown) => {
setError(settingsErrorMessage(reason, '读取设置失败'))
@@ -555,8 +546,7 @@ export function SettingsPanel({
opencodeModelSource,
continueModelSource,
toolApproval,
subagentSmartRoutingEnabled,
intranetCompatibilityEnabled
subagentSmartRoutingEnabled
})
setSettings(value)
setModelProfiles(toModelProfileDrafts(value))
@@ -586,9 +576,6 @@ export function SettingsPanel({
setSubagentSmartRoutingEnabled(
value.subagentSmartRoutingEnabled
)
setIntranetCompatibilityEnabled(
value.intranetCompatibilityEnabled
)
const embeddings = window.goodbuddy.embeddings
if (embeddings) {
try {
@@ -1850,14 +1837,14 @@ export function SettingsPanel({
}
value={profile.protocol}
>
<option value="anthropic-messages">
Anthropic Messages
<option value="openai-chat-completions">
OpenAI Chat Completions
</option>
<option value="openai-responses">
OpenAI Responses
</option>
<option value="openai-chat-completions">
OpenAI Chat Completions
<option value="anthropic-messages">
Anthropic Messages
</option>
<option value="openai-images-generations">
OpenAI Images Generations
@@ -2115,33 +2102,6 @@ export function SettingsPanel({
{activeTab === 'security' && (
<>
<div className="settings-section">
<div className="settings-section__title">
<div>
<strong></strong>
<small></small>
</div>
</div>
<label className="check-field">
<input
aria-describedby="intranet-compatibility-warning"
checked={intranetCompatibilityEnabled}
onChange={(event) =>
setIntranetCompatibilityEnabled(event.target.checked)
}
type="checkbox"
/>
<span></span>
</label>
<p
className="settings-warning"
id="intranet-compatibility-warning"
>
{
'HTTP 传输未加密。启用后,整个应用允许 HTTP 内网地址,并接受无效、自签名或已过期的 HTTPS 证书;关闭后恢复严格的地址与证书校验。'
}
</p>
</div>
<label className="field">
<span>Runtime OS </span>
<select
+40
View File
@@ -1813,6 +1813,46 @@ textarea:focus-visible {
background: #e6f4ff;
}
.message-reasoning {
max-width: 100%;
margin: 0 0 var(--space-3);
border: 1px solid var(--border-default);
border-radius: var(--radius-control);
background: var(--surface-subtle);
color: var(--text-secondary);
}
.message-reasoning > summary {
padding: var(--space-2) var(--space-3);
cursor: pointer;
font-size: var(--font-body);
font-weight: 600;
list-style-position: inside;
}
.message-reasoning > summary:hover {
background: var(--surface-muted);
}
.message-reasoning > summary:focus-visible {
border-radius: var(--radius-control);
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.message-reasoning__content {
max-height: 320px;
padding: 0 var(--space-3) var(--space-3);
overflow-y: auto;
color: var(--text-secondary);
font-size: var(--font-body);
}
.message-reasoning[open] > summary {
margin-bottom: var(--space-2);
border-bottom: 1px solid var(--border-subtle);
}
.markdown-content > :first-child {
margin-top: 0;
}