mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 03:27:00 +08:00
+1
-1
@@ -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": [
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user