fix: 修复ai模型页面loop循环问题 (#16)

修改获取密钥文案
新增模型跳转等问题
This commit is contained in:
电摇小子
2026-08-15 21:17:18 +08:00
parent 1fc39678ff
commit a52da455c3
7 changed files with 129 additions and 19 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "tracememo",
"version": "2.2.1",
"version": "2.2.2",
"packageManager": "pnpm@7.33.7",
"description": "TraceMemo(迹忆)是一款本地优先、可追溯的 AI 微信知识与分析工作台。 原名 WechatExplorer,支持聊天记录搜索、知识库、AI 总结和 Agent 助手。",
"keywords": [
+1 -1
View File
@@ -683,7 +683,7 @@ export class KeyService {
if (loginRequired) {
return {
success: false,
error: '微信已启动但尚未完成登录,请先在微信客户端完成登录后再重试自动获取密钥。',
error: '微信可能已经启动并登录,请先在微信客户端保持未登录状态,具体点击上方“查看5分钟上手教程” ',
logs
}
}
@@ -43,7 +43,7 @@ export function AIProviderEditor({
)
return (
<section className="settings-card ai-provider-editor">
<section id="ai-provider-editor" className="settings-card ai-provider-editor">
<header>
<div>
<h2>{editing ? '编辑供应商' : '新增供应商'}</h2>
@@ -66,7 +66,11 @@ export function AIProviderEditor({
<div className="ai-provider-form-grid">
<label>
<input value={provider.name} onChange={(event) => patch({ name: event.target.value })} />
<input
id="ai-provider-name"
value={provider.name}
onChange={(event) => patch({ name: event.target.value })}
/>
</label>
<label>
ID
@@ -148,16 +152,22 @@ export function AIProviderEditor({
<div className="ai-model-table">
{provider.models.map((model, index) => (
<div className="ai-model-row" key={`${index}-${model.id}`}>
<input
value={model.name}
onChange={(event) => patchModel(index, { name: event.target.value })}
placeholder="显示名称"
/>
<input
value={model.id}
onChange={(event) => patchModel(index, { id: event.target.value })}
placeholder="实际模型 ID"
/>
<label className="ai-model-identity-field">
<span></span>
<input
value={model.name}
onChange={(event) => patchModel(index, { name: event.target.value })}
placeholder="例如:DeepSeek Chat"
/>
</label>
<label className="ai-model-identity-field">
<span> ID</span>
<input
value={model.id}
onChange={(event) => patchModel(index, { id: event.target.value })}
placeholder="例如:deepseek-chat"
/>
</label>
<label>
<input
type="checkbox"
@@ -314,5 +324,9 @@ export function AIProviderEditor({
}
function emptyModel(): AIModelDefinition {
return { name: '', id: '', capabilities: { chat: true, vision: false, ocr: false, longContext: false } }
return {
name: '',
id: '',
capabilities: { chat: true, vision: false, ocr: false, longContext: false }
}
}
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useReducer } from 'react'
import { useCallback, useEffect, useReducer, useRef } from 'react'
import type {
AIProviderConfig,
AIProviderSummary,
@@ -16,6 +16,11 @@ export function useAIModelSettingsController({
onNotice: (message: string) => void
}): AIModelSettingsController {
const [state, dispatch] = useReducer(aiModelSettingsReducer, initialAIModelSettingsState)
const onRuntimeChangeRef = useRef(onRuntimeChange)
useEffect(() => {
onRuntimeChangeRef.current = onRuntimeChange
}, [onRuntimeChange])
const refresh = useCallback(async (): Promise<void> => {
const [list, runtime] = await Promise.all([
@@ -24,8 +29,8 @@ export function useAIModelSettingsController({
])
if (!list.success) return dispatch({ type: 'ERROR', error: list.error || '供应商配置读取失败' })
dispatch({ type: 'LOADED', providers: list.providers, runtime })
onRuntimeChange(runtime)
}, [onRuntimeChange])
onRuntimeChangeRef.current(runtime)
}, [])
useEffect(() => {
void refresh()
@@ -1,3 +1,4 @@
import { useEffect, useRef } from 'react'
import type { AIRuntimeModelConfig } from '../../../../../shared/ai-provider'
import { AIProviderCard } from '../ai-model/AIProviderCard'
import { AIProviderEditor } from '../ai-model/AIProviderEditor'
@@ -12,10 +13,25 @@ export function AIModelPage({
onNotice: (message: string) => void
}): React.ReactElement {
const controller = useAIModelSettingsController({ onRuntimeChange, onNotice })
const shouldRevealNewEditor = useRef(false)
const runtime = controller.state.runtime
const defaultProvider = controller.state.providers.find(
(provider) => provider.id === runtime?.providerId
)
useEffect(() => {
if (!controller.state.editor || !shouldRevealNewEditor.current) return
shouldRevealNewEditor.current = false
const editor = document.getElementById('ai-provider-editor')
editor?.scrollIntoView({ behavior: 'smooth', block: 'start' })
editor?.querySelector<HTMLInputElement>('#ai-provider-name')?.focus({ preventScroll: true })
}, [controller.state.editor])
const openNewProvider = (): void => {
shouldRevealNewEditor.current = true
controller.openNew()
}
return (
<div className="settings-page ai-model-page">
<header className="settings-page-header">
@@ -23,7 +39,7 @@ export function AIModelPage({
<h1>AI </h1>
<p></p>
</div>
<button className="database-key-primary" onClick={controller.openNew}>
<button className="database-key-primary" onClick={openNewProvider}>
</button>
</header>
+10
View File
@@ -1777,6 +1777,16 @@
white-space: nowrap;
font-weight: 400;
}
.ai-model-row .ai-model-identity-field {
display: grid;
min-width: 0;
gap: 5px;
justify-self: stretch;
color: #66706b;
font-family: inherit;
font-size: 11px;
font-weight: 600;
}
.ai-provider-advanced,
.ai-provider-preview {
border: 1px solid #e3e8e5;
@@ -0,0 +1,65 @@
import { act, fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { AIModelPage } from '../../src/renderer/src/features/settings/pages/AIModelPage'
import type { AIRuntimeModelConfig } from '../../src/shared/ai-provider'
const runtime: AIRuntimeModelConfig = {
providerName: 'Not configured',
model: '',
modelName: 'No model selected',
configured: false,
status: 'untested'
}
describe('AI model settings', () => {
const scrollIntoView = vi.fn()
beforeEach(() => {
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
configurable: true,
value: scrollIntoView
})
window.api = {
listAIProviders: vi.fn().mockResolvedValue({ success: true, providers: [] }),
getAIRuntimeConfig: vi.fn().mockResolvedValue(runtime)
} as typeof window.api
})
it('reveals the new provider editor and labels model identity fields', async () => {
render(<AIModelPage onRuntimeChange={vi.fn()} onNotice={vi.fn()} />)
await act(async () => {
await Promise.resolve()
})
fireEvent.click(screen.getByRole('button', { name: '添加供应商' }))
expect(scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth', block: 'start' })
expect(screen.getByLabelText('供应商名称')).toHaveFocus()
expect(screen.getByLabelText('模型名称')).toHaveAttribute('placeholder', '例如:DeepSeek Chat')
expect(screen.getByLabelText('模型 ID')).toHaveAttribute('placeholder', '例如:deepseek-chat')
})
it('does not reload providers when the parent callback identity changes', async () => {
const firstRuntimeChange = vi.fn()
const secondRuntimeChange = vi.fn()
const { rerender } = render(
<AIModelPage onRuntimeChange={firstRuntimeChange} onNotice={vi.fn()} />
)
await act(async () => {
await Promise.resolve()
})
expect(window.api.listAIProviders).toHaveBeenCalledOnce()
expect(window.api.getAIRuntimeConfig).toHaveBeenCalledOnce()
expect(firstRuntimeChange).toHaveBeenCalledWith(runtime)
await act(async () => {
rerender(<AIModelPage onRuntimeChange={secondRuntimeChange} onNotice={vi.fn()} />)
await Promise.resolve()
})
expect(window.api.listAIProviders).toHaveBeenCalledOnce()
expect(window.api.getAIRuntimeConfig).toHaveBeenCalledOnce()
expect(secondRuntimeChange).not.toHaveBeenCalled()
})
})