chore: prepare GoodBuddy 0.8.5
This commit is contained in:
@@ -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')
|
||||
})
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user