mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-10-04 03:01:42 +08:00
fix: 修复样式, 调用接口方法
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { app } from 'electron'
|
import { app, net } from 'electron'
|
||||||
import fs from 'fs-extra'
|
import fs from 'fs-extra'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
@@ -119,7 +119,7 @@ export class TextToSpeechSettingsService {
|
|||||||
if (tag.trim()) url.searchParams.append('tag', tag.trim())
|
if (tag.trim()) url.searchParams.append('tag', tag.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fishAudioFetch(url.toString(), {
|
||||||
headers: { Authorization: `Bearer ${resolved.key}` },
|
headers: { Authorization: `Bearer ${resolved.key}` },
|
||||||
signal: AbortSignal.timeout(30_000)
|
signal: AbortSignal.timeout(30_000)
|
||||||
})
|
})
|
||||||
@@ -179,7 +179,7 @@ export class TextToSpeechSettingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${FISH_AUDIO_BASE_URL}/v1/tts`, {
|
const response = await fishAudioFetch(`${FISH_AUDIO_BASE_URL}/v1/tts`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${resolved.key}`,
|
Authorization: `Bearer ${resolved.key}`,
|
||||||
@@ -255,10 +255,13 @@ export class TextToSpeechSettingsService {
|
|||||||
|
|
||||||
private async getVoice(id: string, key: string): Promise<TextToSpeechVoice | null> {
|
private async getVoice(id: string, key: string): Promise<TextToSpeechVoice | null> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${FISH_AUDIO_BASE_URL}/model/${encodeURIComponent(id)}`, {
|
const response = await fishAudioFetch(
|
||||||
headers: { Authorization: `Bearer ${key}` },
|
`${FISH_AUDIO_BASE_URL}/model/${encodeURIComponent(id)}`,
|
||||||
signal: AbortSignal.timeout(15_000)
|
{
|
||||||
})
|
headers: { Authorization: `Bearer ${key}` },
|
||||||
|
signal: AbortSignal.timeout(15_000)
|
||||||
|
}
|
||||||
|
)
|
||||||
if (!response.ok) return null
|
if (!response.ok) return null
|
||||||
const model = (await response.json()) as FishAudioModelEntity
|
const model = (await response.json()) as FishAudioModelEntity
|
||||||
return model.type === 'tts' && model.state === 'trained' ? toVoice(model) : null
|
return model.type === 'tts' && model.state === 'trained' ? toVoice(model) : null
|
||||||
@@ -351,6 +354,25 @@ async function fishAudioError(response: Response): Promise<Error> {
|
|||||||
return new Error(detail || `语音服务请求失败(HTTP ${response.status})`)
|
return new Error(detail || `语音服务请求失败(HTTP ${response.status})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fishAudioFetch(input: string, init: RequestInit): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return await net.fetch(input, init)
|
||||||
|
} catch (electronError) {
|
||||||
|
try {
|
||||||
|
return await fetch(input, init)
|
||||||
|
} catch (nodeError) {
|
||||||
|
throw new Error(
|
||||||
|
`Fish Audio 网络请求失败(Electron: ${networkErrorMessage(electronError)};Node: ${networkErrorMessage(nodeError)})`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function networkErrorMessage(error: unknown): string {
|
||||||
|
if (error instanceof Error && error.message.trim()) return error.message.trim()
|
||||||
|
return String(error || '未知错误')
|
||||||
|
}
|
||||||
|
|
||||||
function safeFishAudioError(error: unknown): string {
|
function safeFishAudioError(error: unknown): string {
|
||||||
if (error instanceof DOMException && error.name === 'TimeoutError') return '语音服务请求超时'
|
if (error instanceof DOMException && error.name === 'TimeoutError') return '语音服务请求超时'
|
||||||
if (error instanceof Error) return error.message
|
if (error instanceof Error) return error.message
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function FirstUseWelcome({
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="mt-5 h-auto w-full justify-start whitespace-normal p-4 text-left"
|
className="mt-5 !h-auto w-full justify-start whitespace-normal p-4 text-left"
|
||||||
onClick={onOpenReport}
|
onClick={onOpenReport}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function ExportPreviewPanel({
|
|||||||
: ''
|
: ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="flex min-h-0 min-w-0 flex-col overflow-hidden border-l border-border bg-surface max-[1100px]:hidden">
|
<aside className="export-preview-panel flex min-h-0 min-w-0 flex-col overflow-hidden border-l border-border max-[1100px]:hidden">
|
||||||
{status === 'idle' && (
|
{status === 'idle' && (
|
||||||
<>
|
<>
|
||||||
<div className="flex h-[52px] shrink-0 items-center justify-between border-b border-border px-[18px] text-xs text-foreground">
|
<div className="flex h-[52px] shrink-0 items-center justify-between border-b border-border px-[18px] text-xs text-foreground">
|
||||||
|
|||||||
@@ -194,14 +194,32 @@ export function AgentHubWorkspace(): React.ReactElement {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<aside className="agent-hub-card agent-hub-capability-card">
|
<aside className="agent-hub-card agent-hub-capability-card">
|
||||||
<span className="agent-hub-card-kicker">本机能力</span>
|
<span className="agent-hub-card-kicker">已启用能力</span>
|
||||||
<h2>能力与状态</h2>
|
<h2>微信数据助手</h2>
|
||||||
<p>机器人通过本机 Agent Hub 调用 TraceMemo,不向公网暴露数据库。</p>
|
<p>机器人通过本机 Agent Hub 调用 TraceMemo,不向公网暴露数据库。</p>
|
||||||
|
<div className="agent-hub-example">
|
||||||
|
<span>支持自然语言,可以这样问</span>
|
||||||
|
<strong>“最近 5 条消息是谁?”</strong>
|
||||||
|
<strong>“帮我看看最近跟xx聊了些什么”</strong>
|
||||||
|
<strong>“生成产品交流群今天的群聊总结图片”</strong>
|
||||||
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li>本机 HTTP 通信</li>
|
<li>
|
||||||
<li>入站请求鉴权</li>
|
<i />
|
||||||
<li>消息重复保护</li>
|
本机 HTTP 通信
|
||||||
<li>使用已配置 AI 理解自然语言</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<i />
|
||||||
|
入站请求鉴权
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i />
|
||||||
|
消息重复保护
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i />
|
||||||
|
使用已配置 AI 理解自然语言
|
||||||
|
</li>
|
||||||
<li className="agent-hub-capability-status">
|
<li className="agent-hub-capability-status">
|
||||||
<i className={status.dataApi === 'online' ? '' : 'offline'} />
|
<i className={status.dataApi === 'online' ? '' : 'offline'} />
|
||||||
本地数据 API:{status.dataApi === 'online' ? '已连接' : '未连接'}
|
本地数据 API:{status.dataApi === 'online' ? '已连接' : '未连接'}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ const RUNTIME_STATUS_LABELS: Record<PersonalWechatRuntimeStatus['state'], string
|
|||||||
}
|
}
|
||||||
|
|
||||||
const VOICE_FILTERS = [
|
const VOICE_FILTERS = [
|
||||||
|
{ value: 'Chinese', label: '中文' },
|
||||||
{ value: 'male', label: '男性' },
|
{ value: 'male', label: '男性' },
|
||||||
{ value: 'female', label: '女性' },
|
{ value: 'female', label: '女性' },
|
||||||
{ value: 'neutral', label: '中性' },
|
{ value: 'neutral', label: '中性' },
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
background: var(--wxex-bg-main);
|
background: var(--wxex-bg-main);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.export-preview-panel {
|
||||||
|
background: var(--wxex-bg-main);
|
||||||
|
}
|
||||||
|
|
||||||
.export-config-panel {
|
.export-config-panel {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|||||||
@@ -1124,21 +1124,44 @@
|
|||||||
.agent-hub-capability-card > p {
|
.agent-hub-capability-card > p {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
.agent-hub-example {
|
||||||
|
margin: 18px 0 14px;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: var(--wxex-radius-md);
|
||||||
|
background: var(--wxex-brand-soft);
|
||||||
|
}
|
||||||
|
.agent-hub-example span {
|
||||||
|
display: block;
|
||||||
|
color: var(--wxex-text-muted);
|
||||||
|
font: 600 11px/16px var(--wxex-font);
|
||||||
|
}
|
||||||
|
.agent-hub-example strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: var(--wxex-brand);
|
||||||
|
font: 700 14px/21px var(--wxex-font);
|
||||||
|
}
|
||||||
|
.agent-hub-example strong + strong {
|
||||||
|
margin-top: 9px;
|
||||||
|
padding-top: 9px;
|
||||||
|
border-top: 1px solid rgba(36, 122, 99, 0.15);
|
||||||
|
}
|
||||||
.agent-hub-capability-card ul {
|
.agent-hub-capability-card ul {
|
||||||
margin: 18px 0 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
.agent-hub-capability-card li {
|
.agent-hub-capability-card li {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 9px;
|
||||||
padding: 7px 0;
|
padding: 7px 0;
|
||||||
|
border-bottom: 1px solid var(--wxex-border);
|
||||||
color: var(--wxex-text-secondary);
|
color: var(--wxex-text-secondary);
|
||||||
font: 13px/19px var(--wxex-font);
|
font: 13px/19px var(--wxex-font);
|
||||||
}
|
}
|
||||||
.agent-hub-capability-card li + li {
|
.agent-hub-capability-card li:last-child {
|
||||||
margin-top: 2px;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
.agent-hub-capability-card li i {
|
.agent-hub-capability-card li i {
|
||||||
width: 7px;
|
width: 7px;
|
||||||
@@ -1146,9 +1169,6 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--wxex-brand);
|
background: var(--wxex-brand);
|
||||||
}
|
}
|
||||||
.agent-hub-capability-card li:not(.agent-hub-capability-status) {
|
|
||||||
padding-left: 15px;
|
|
||||||
}
|
|
||||||
.agent-hub-capability-card li i.offline {
|
.agent-hub-capability-card li i.offline {
|
||||||
background: var(--wxex-danger);
|
background: var(--wxex-danger);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,23 @@ describe('Agent Hub controls', () => {
|
|||||||
expect(screen.getByRole('button', { name: '复制日志' })).toBeDisabled()
|
expect(screen.getByRole('button', { name: '复制日志' })).toBeDisabled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps the enabled capability examples visible', async () => {
|
||||||
|
render(<AgentHubWorkspace />)
|
||||||
|
await screen.findByText('系统就绪')
|
||||||
|
|
||||||
|
expect(screen.getByText('已启用能力')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('微信数据助手')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('支持自然语言,可以这样问')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('“最近 5 条消息是谁?”')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('“帮我看看最近跟xx聊了些什么”')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('“生成产品交流群今天的群聊总结图片”')).toBeInTheDocument()
|
||||||
|
expect(
|
||||||
|
document.querySelectorAll(
|
||||||
|
'.agent-hub-capability-card li:not(.agent-hub-capability-status) > i'
|
||||||
|
)
|
||||||
|
).toHaveLength(4)
|
||||||
|
})
|
||||||
|
|
||||||
it('keeps reconnect and destructive disconnect actions separate', async () => {
|
it('keeps reconnect and destructive disconnect actions separate', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
render(<AgentHubWorkspace />)
|
render(<AgentHubWorkspace />)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const baseProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('export progress panel', () => {
|
describe('export progress panel', () => {
|
||||||
it('uses the semantic accent surface for outgoing preview messages', () => {
|
it('keeps the preview canvas distinct from the bubble surface', () => {
|
||||||
render(
|
render(
|
||||||
<ExportPreviewPanel
|
<ExportPreviewPanel
|
||||||
{...baseProps}
|
{...baseProps}
|
||||||
@@ -40,6 +40,7 @@ describe('export progress panel', () => {
|
|||||||
expect(screen.getByText('已发送的消息').closest('.export-preview-bubble')).toHaveClass(
|
expect(screen.getByText('已发送的消息').closest('.export-preview-bubble')).toHaveClass(
|
||||||
'bg-accent'
|
'bg-accent'
|
||||||
)
|
)
|
||||||
|
expect(screen.getByRole('complementary')).toHaveClass('export-preview-panel')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows an indeterminate bar while the first message scan is still at zero', () => {
|
it('shows an indeterminate bar while the first message scan is still at zero', () => {
|
||||||
|
|||||||
@@ -8,6 +8,17 @@ const mocks = vi.hoisted(() => ({
|
|||||||
updateSettings: vi.fn()
|
updateSettings: vi.fn()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const electronNetFetch = vi.hoisted(() => vi.fn())
|
||||||
|
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
app: {
|
||||||
|
getPath: vi.fn(() => '/tmp/tracememo-test')
|
||||||
|
},
|
||||||
|
net: {
|
||||||
|
fetch: electronNetFetch
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('../../src/main/ai-provider-key-store', () => ({
|
vi.mock('../../src/main/ai-provider-key-store', () => ({
|
||||||
AIProviderKeyStore: class {
|
AIProviderKeyStore: class {
|
||||||
get = mocks.keyGet
|
get = mocks.keyGet
|
||||||
@@ -66,4 +77,41 @@ describe('TextToSpeechSettingsService', () => {
|
|||||||
ttsSelectedVoiceId: 'fish-model-id'
|
ttsSelectedVoiceId: 'fish-model-id'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('uses Electron net.fetch for Fish Audio requests', async () => {
|
||||||
|
mocks.keyGet.mockReturnValue({ success: true, available: true, key: 'stored' })
|
||||||
|
electronNetFetch.mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: vi.fn().mockResolvedValue({ total: 0, items: [], has_more: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await new TextToSpeechSettingsService().listVoices({ pageSize: 1 })
|
||||||
|
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
expect(electronNetFetch).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('https://api.fish.audio/model'),
|
||||||
|
expect.objectContaining({
|
||||||
|
headers: { Authorization: 'Bearer stored' }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to Node fetch when Electron transport is unavailable', async () => {
|
||||||
|
mocks.keyGet.mockReturnValue({ success: true, available: true, key: 'stored' })
|
||||||
|
electronNetFetch.mockRejectedValue(new TypeError('Electron transport unavailable'))
|
||||||
|
const nodeFetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: vi.fn().mockResolvedValue({ total: 0, items: [], has_more: false })
|
||||||
|
})
|
||||||
|
vi.stubGlobal('fetch', nodeFetch)
|
||||||
|
|
||||||
|
const result = await new TextToSpeechSettingsService().listVoices({ pageSize: 1 })
|
||||||
|
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
expect(nodeFetch).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('https://api.fish.audio/model'),
|
||||||
|
expect.objectContaining({ headers: { Authorization: 'Bearer stored' } })
|
||||||
|
)
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user