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",
|
"name": "tracememo",
|
||||||
"version": "2.2.1",
|
"version": "2.2.2",
|
||||||
"packageManager": "pnpm@7.33.7",
|
"packageManager": "pnpm@7.33.7",
|
||||||
"description": "TraceMemo(迹忆)是一款本地优先、可追溯的 AI 微信知识与分析工作台。 原名 WechatExplorer,支持聊天记录搜索、知识库、AI 总结和 Agent 助手。",
|
"description": "TraceMemo(迹忆)是一款本地优先、可追溯的 AI 微信知识与分析工作台。 原名 WechatExplorer,支持聊天记录搜索、知识库、AI 总结和 Agent 助手。",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -683,7 +683,7 @@ export class KeyService {
|
|||||||
if (loginRequired) {
|
if (loginRequired) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: '微信已启动但尚未完成登录,请先在微信客户端完成登录后再重试自动获取密钥。',
|
error: '微信可能已经启动并登录,请先在微信客户端保持未登录状态,具体点击上方“查看5分钟上手教程” ',
|
||||||
logs
|
logs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function AIProviderEditor({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="settings-card ai-provider-editor">
|
<section id="ai-provider-editor" className="settings-card ai-provider-editor">
|
||||||
<header>
|
<header>
|
||||||
<div>
|
<div>
|
||||||
<h2>{editing ? '编辑供应商' : '新增供应商'}</h2>
|
<h2>{editing ? '编辑供应商' : '新增供应商'}</h2>
|
||||||
@@ -66,7 +66,11 @@ export function AIProviderEditor({
|
|||||||
<div className="ai-provider-form-grid">
|
<div className="ai-provider-form-grid">
|
||||||
<label>
|
<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>
|
||||||
<label>
|
<label>
|
||||||
供应商 ID
|
供应商 ID
|
||||||
@@ -148,16 +152,22 @@ export function AIProviderEditor({
|
|||||||
<div className="ai-model-table">
|
<div className="ai-model-table">
|
||||||
{provider.models.map((model, index) => (
|
{provider.models.map((model, index) => (
|
||||||
<div className="ai-model-row" key={`${index}-${model.id}`}>
|
<div className="ai-model-row" key={`${index}-${model.id}`}>
|
||||||
|
<label className="ai-model-identity-field">
|
||||||
|
<span>模型名称</span>
|
||||||
<input
|
<input
|
||||||
value={model.name}
|
value={model.name}
|
||||||
onChange={(event) => patchModel(index, { name: event.target.value })}
|
onChange={(event) => patchModel(index, { name: event.target.value })}
|
||||||
placeholder="显示名称"
|
placeholder="例如:DeepSeek Chat"
|
||||||
/>
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="ai-model-identity-field">
|
||||||
|
<span>模型 ID</span>
|
||||||
<input
|
<input
|
||||||
value={model.id}
|
value={model.id}
|
||||||
onChange={(event) => patchModel(index, { id: event.target.value })}
|
onChange={(event) => patchModel(index, { id: event.target.value })}
|
||||||
placeholder="实际模型 ID"
|
placeholder="例如:deepseek-chat"
|
||||||
/>
|
/>
|
||||||
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -314,5 +324,9 @@ export function AIProviderEditor({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function emptyModel(): AIModelDefinition {
|
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 {
|
import type {
|
||||||
AIProviderConfig,
|
AIProviderConfig,
|
||||||
AIProviderSummary,
|
AIProviderSummary,
|
||||||
@@ -16,6 +16,11 @@ export function useAIModelSettingsController({
|
|||||||
onNotice: (message: string) => void
|
onNotice: (message: string) => void
|
||||||
}): AIModelSettingsController {
|
}): AIModelSettingsController {
|
||||||
const [state, dispatch] = useReducer(aiModelSettingsReducer, initialAIModelSettingsState)
|
const [state, dispatch] = useReducer(aiModelSettingsReducer, initialAIModelSettingsState)
|
||||||
|
const onRuntimeChangeRef = useRef(onRuntimeChange)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onRuntimeChangeRef.current = onRuntimeChange
|
||||||
|
}, [onRuntimeChange])
|
||||||
|
|
||||||
const refresh = useCallback(async (): Promise<void> => {
|
const refresh = useCallback(async (): Promise<void> => {
|
||||||
const [list, runtime] = await Promise.all([
|
const [list, runtime] = await Promise.all([
|
||||||
@@ -24,8 +29,8 @@ export function useAIModelSettingsController({
|
|||||||
])
|
])
|
||||||
if (!list.success) return dispatch({ type: 'ERROR', error: list.error || '供应商配置读取失败' })
|
if (!list.success) return dispatch({ type: 'ERROR', error: list.error || '供应商配置读取失败' })
|
||||||
dispatch({ type: 'LOADED', providers: list.providers, runtime })
|
dispatch({ type: 'LOADED', providers: list.providers, runtime })
|
||||||
onRuntimeChange(runtime)
|
onRuntimeChangeRef.current(runtime)
|
||||||
}, [onRuntimeChange])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void refresh()
|
void refresh()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
import type { AIRuntimeModelConfig } from '../../../../../shared/ai-provider'
|
import type { AIRuntimeModelConfig } from '../../../../../shared/ai-provider'
|
||||||
import { AIProviderCard } from '../ai-model/AIProviderCard'
|
import { AIProviderCard } from '../ai-model/AIProviderCard'
|
||||||
import { AIProviderEditor } from '../ai-model/AIProviderEditor'
|
import { AIProviderEditor } from '../ai-model/AIProviderEditor'
|
||||||
@@ -12,10 +13,25 @@ export function AIModelPage({
|
|||||||
onNotice: (message: string) => void
|
onNotice: (message: string) => void
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const controller = useAIModelSettingsController({ onRuntimeChange, onNotice })
|
const controller = useAIModelSettingsController({ onRuntimeChange, onNotice })
|
||||||
|
const shouldRevealNewEditor = useRef(false)
|
||||||
const runtime = controller.state.runtime
|
const runtime = controller.state.runtime
|
||||||
const defaultProvider = controller.state.providers.find(
|
const defaultProvider = controller.state.providers.find(
|
||||||
(provider) => provider.id === runtime?.providerId
|
(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 (
|
return (
|
||||||
<div className="settings-page ai-model-page">
|
<div className="settings-page ai-model-page">
|
||||||
<header className="settings-page-header">
|
<header className="settings-page-header">
|
||||||
@@ -23,7 +39,7 @@ export function AIModelPage({
|
|||||||
<h1>AI 模型</h1>
|
<h1>AI 模型</h1>
|
||||||
<p>管理模型供应商、连接信息和默认模型</p>
|
<p>管理模型供应商、连接信息和默认模型</p>
|
||||||
</div>
|
</div>
|
||||||
<button className="database-key-primary" onClick={controller.openNew}>
|
<button className="database-key-primary" onClick={openNewProvider}>
|
||||||
添加供应商
|
添加供应商
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -1777,6 +1777,16 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-weight: 400;
|
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-advanced,
|
||||||
.ai-provider-preview {
|
.ai-provider-preview {
|
||||||
border: 1px solid #e3e8e5;
|
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