mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 19:47:08 +08:00
feat: 集成 Agent Hub 微信机器人能力
This commit is contained in:
+20
-16
@@ -4,6 +4,7 @@ import ChatWindow from './components/ChatWindow'
|
||||
import { AppShell } from './components/layout/AppShell'
|
||||
import { ApiWorkspace } from './features/api-center/ApiWorkspace'
|
||||
import { SettingsWorkspace } from './features/settings/SettingsWorkspace'
|
||||
import { AgentHubWorkspace } from './features/agent-hub/AgentHubWorkspace'
|
||||
import type { SettingsCategoryId } from './features/settings/model/types'
|
||||
import type { AIRuntimeModelConfig } from '../../shared/ai-provider'
|
||||
import { AppPage } from './components/layout/navigation'
|
||||
@@ -57,12 +58,6 @@ interface SelfInfo {
|
||||
const MAC_KEY_FAQ_URL = 'https://github.com/hicccc77/WeFlow/blob/main/docs/MAC-KEY-FAQ.md'
|
||||
const MESSAGE_MONITOR_DEBOUNCE_MS = 8000
|
||||
const VIEW_MESSAGE_LIMIT = 600
|
||||
const AUTO_LOGIN_ENABLED = ['1', 'true', 'yes', 'on'].includes(
|
||||
String(import.meta.env.VITE_AUTO_LOGIN || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
)
|
||||
|
||||
const getMessageIdentity = (message: Message): string => {
|
||||
if (message.localId) return `local:${message.localId}`
|
||||
if (message.id) return `id:${message.id}`
|
||||
@@ -369,13 +364,12 @@ function App(): React.ReactElement {
|
||||
React.useEffect(() => {
|
||||
let active = true
|
||||
const attemptAutoConnect = async (): Promise<void> => {
|
||||
const settingsResult = await window.api.getSettings()
|
||||
// 预填已保存的微信聊天文件路径
|
||||
try {
|
||||
const settings = await window.api.getSettings()
|
||||
if (active && settings?.settings?.dbRoot) setDbRootInput(settings.settings.dbRoot)
|
||||
} catch {
|
||||
// 忽略读取设置失败,继续走密钥流程
|
||||
if (active && settingsResult.settings.dbRoot) {
|
||||
setDbRootInput(settingsResult.settings.dbRoot)
|
||||
}
|
||||
const autoLoginEnabled = settingsResult.settings.autoLogin
|
||||
// 优先级 1: 构建期环境变量 VITE_DB_KEY(本地开发用)
|
||||
const envKey = String(import.meta.env.VITE_DB_KEY || '').trim()
|
||||
// 优先级 2: 上一次保存到 safeStorage 的密钥
|
||||
@@ -393,7 +387,7 @@ function App(): React.ReactElement {
|
||||
setDbKey(key)
|
||||
setAutoConnectSource(envKey ? 'env' : 'saved')
|
||||
setDbKeyStatus(
|
||||
AUTO_LOGIN_ENABLED
|
||||
autoLoginEnabled
|
||||
? envKey
|
||||
? '检测到环境变量中的密钥,正在自动连接...'
|
||||
: '已加载安全保存的密钥,正在自动连接...'
|
||||
@@ -402,14 +396,17 @@ function App(): React.ReactElement {
|
||||
: '已加载安全保存的密钥,请手动点击 Connect'
|
||||
)
|
||||
setDbKeyStatusKind('normal')
|
||||
setBootState(AUTO_LOGIN_ENABLED ? 'connecting' : 'login')
|
||||
setBootState(autoLoginEnabled ? 'connecting' : 'login')
|
||||
}
|
||||
if (!AUTO_LOGIN_ENABLED) return
|
||||
if (!autoLoginEnabled) return
|
||||
try {
|
||||
const result = await window.api.initDb(key)
|
||||
if (!active) return
|
||||
const success = typeof result === 'boolean' ? result : result.success
|
||||
if (success) {
|
||||
if (!settingsResult.settings.autoLoginPreferenceSet) {
|
||||
void window.api.setSettings({ autoLogin: true })
|
||||
}
|
||||
setIsNativeMonitorActive(typeof result !== 'boolean' && result.monitoring === true)
|
||||
setIsAuthenticated(true)
|
||||
setIsDatabaseConnected(true)
|
||||
@@ -489,6 +486,11 @@ function App(): React.ReactElement {
|
||||
const hasBootstrapCache = await loadBootstrapCache()
|
||||
// 持久化手动输入的密钥,供下次启动继续使用
|
||||
void window.api.saveDbKey(keyToUse).catch(() => undefined)
|
||||
void window.api.getSettings().then((current) => {
|
||||
if (!current.settings.autoLoginPreferenceSet) {
|
||||
void window.api.setSettings({ autoLogin: true })
|
||||
}
|
||||
})
|
||||
setStartupProgress({
|
||||
title: '正在加载账号信息...',
|
||||
subtitle: '即将进入 WechatExplorer',
|
||||
@@ -1036,9 +1038,9 @@ function App(): React.ReactElement {
|
||||
}
|
||||
|
||||
const renderPlaceholderPage = (
|
||||
page: Exclude<AppPage, 'archive' | 'report'>
|
||||
page: Exclude<AppPage, 'archive' | 'report' | 'agent-hub'>
|
||||
): React.ReactElement => {
|
||||
const labels: Record<Exclude<AppPage, 'archive' | 'report'>, string> = {
|
||||
const labels: Record<Exclude<AppPage, 'archive' | 'report' | 'agent-hub'>, string> = {
|
||||
search: '检索',
|
||||
export: '导出',
|
||||
api: 'API',
|
||||
@@ -1168,6 +1170,8 @@ function App(): React.ReactElement {
|
||||
return renderArchiveWorkspace()
|
||||
case 'report':
|
||||
return renderReportWorkspace()
|
||||
case 'agent-hub':
|
||||
return <AgentHubWorkspace />
|
||||
case 'api':
|
||||
return (
|
||||
<ApiWorkspace
|
||||
|
||||
+2800
-142
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,16 @@ function NavIcon({ page }: NavIconProps): React.ReactElement {
|
||||
<path d="M5.5 15.5v3h13v-3" />
|
||||
</svg>
|
||||
)
|
||||
case 'agent-hub':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<rect x="5" y="7" width="14" height="11" rx="3" />
|
||||
<path d="M12 4.5V7" />
|
||||
<circle cx="9.5" cy="12" r="1" />
|
||||
<circle cx="14.5" cy="12" r="1" />
|
||||
<path d="M9.5 15h5" />
|
||||
</svg>
|
||||
)
|
||||
case 'api':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type AppPage = 'archive' | 'search' | 'report' | 'export' | 'api' | 'settings'
|
||||
export type AppPage = 'archive' | 'search' | 'report' | 'agent-hub' | 'export' | 'api' | 'settings'
|
||||
|
||||
export interface NavigationItem {
|
||||
id: AppPage
|
||||
@@ -9,6 +9,7 @@ export const PRIMARY_NAV_ITEMS: NavigationItem[] = [
|
||||
{ id: 'archive', label: '档案' },
|
||||
{ id: 'search', label: '检索' },
|
||||
{ id: 'report', label: '日报' },
|
||||
{ id: 'agent-hub', label: 'Agent' },
|
||||
{ id: 'export', label: '导出' },
|
||||
{ id: 'api', label: 'API' },
|
||||
{ id: 'settings', label: '设置' }
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
import React from 'react'
|
||||
import type {
|
||||
AgentHubLogEntry,
|
||||
AgentHubLogSource,
|
||||
AgentHubStatus,
|
||||
WechatConnectorStatus
|
||||
} from '../../../../shared/agent-hub'
|
||||
|
||||
const STATUS_LABELS: Record<WechatConnectorStatus, string> = {
|
||||
checking: '正在检查',
|
||||
disconnected: '未连接',
|
||||
starting: '正在连接',
|
||||
waiting_scan: '等待扫码',
|
||||
scanned: '已扫码,等待手机确认',
|
||||
online: '在线',
|
||||
error: '连接异常'
|
||||
}
|
||||
|
||||
const LOG_SOURCE_LABELS: Record<AgentHubLogSource, string> = {
|
||||
system: '系统',
|
||||
'agent-hub': 'Agent Hub',
|
||||
'wechat-connector': '微信连接器'
|
||||
}
|
||||
|
||||
export function AgentHubWorkspace(): React.ReactElement {
|
||||
const [status, setStatus] = React.useState<AgentHubStatus>({
|
||||
hub: 'offline',
|
||||
connector: 'checking',
|
||||
updatedAt: Date.now()
|
||||
})
|
||||
const [busy, setBusy] = React.useState(false)
|
||||
const [logs, setLogs] = React.useState<AgentHubLogEntry[]>([])
|
||||
const [logSource, setLogSource] = React.useState<'all' | AgentHubLogSource>('all')
|
||||
const logBodyRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
let mounted = true
|
||||
void window.api.getAgentHubStatus().then((next) => {
|
||||
if (mounted) setStatus(next)
|
||||
})
|
||||
void window.api.getAgentHubLogs().then((entries) => {
|
||||
if (mounted) setLogs(entries)
|
||||
})
|
||||
const unsubscribe = window.api.onAgentHubStatus((next) => {
|
||||
if (mounted) setStatus(next)
|
||||
})
|
||||
const unsubscribeLog = window.api.onAgentHubLog((entry) => {
|
||||
if (mounted) setLogs((current) => [...current.slice(-799), entry])
|
||||
})
|
||||
return () => {
|
||||
mounted = false
|
||||
unsubscribe()
|
||||
unsubscribeLog()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const visibleLogs = logs.filter((entry) => logSource === 'all' || entry.source === logSource)
|
||||
|
||||
React.useEffect(() => {
|
||||
const body = logBodyRef.current
|
||||
if (body) body.scrollTop = body.scrollHeight
|
||||
}, [visibleLogs.length])
|
||||
|
||||
const copyLogs = async (): Promise<void> => {
|
||||
const text = visibleLogs
|
||||
.map(
|
||||
(entry) =>
|
||||
`${new Date(entry.timestamp).toLocaleTimeString()} [${LOG_SOURCE_LABELS[entry.source]}] [${entry.level}] ${entry.message}`
|
||||
)
|
||||
.join('\n')
|
||||
await window.api.copyText(text)
|
||||
}
|
||||
|
||||
const clearLogs = async (): Promise<void> => {
|
||||
await window.api.clearAgentHubLogs()
|
||||
setLogs([])
|
||||
}
|
||||
|
||||
const runAction = async (
|
||||
action: () => Promise<{ status: AgentHubStatus; error?: string }>
|
||||
): Promise<void> => {
|
||||
setBusy(true)
|
||||
try {
|
||||
const result = await action()
|
||||
setStatus(result.status)
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const isLoginFlow = ['starting', 'waiting_scan', 'scanned'].includes(status.connector)
|
||||
const showQRCode = Boolean(status.qrCodeDataUrl) && status.connector !== 'online'
|
||||
|
||||
return (
|
||||
<div className="agent-hub-workspace">
|
||||
<header className="agent-hub-header">
|
||||
<div>
|
||||
<div className="agent-hub-eyebrow">WechatExplorer</div>
|
||||
<h1>Agent Hub</h1>
|
||||
<p>让微信机器人安全调用聊天数据与 AI 能力。</p>
|
||||
</div>
|
||||
<span className={`agent-hub-runtime ${status.hub}`}>
|
||||
Agent Hub {status.hub === 'online' ? '运行中' : '未运行'}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div className="agent-hub-grid">
|
||||
<section className="agent-hub-card agent-hub-login-card">
|
||||
<div className="agent-hub-card-heading">
|
||||
<div>
|
||||
<span className="agent-hub-card-kicker">微信机器人</span>
|
||||
<h2>连接微信</h2>
|
||||
</div>
|
||||
<span className={`agent-hub-status ${status.connector}`}>
|
||||
<i aria-hidden />
|
||||
{STATUS_LABELS[status.connector]}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{showQRCode ? (
|
||||
<div className="agent-hub-qr-panel">
|
||||
<div className="agent-hub-qr-frame">
|
||||
<img src={status.qrCodeDataUrl} alt="微信机器人登录二维码" />
|
||||
</div>
|
||||
<div className="agent-hub-qr-copy">
|
||||
<h3>
|
||||
{status.connector === 'scanned' ? '请在手机上确认登录' : '使用微信扫描二维码'}
|
||||
</h3>
|
||||
<p>二维码仅用于机器人账号登录,不会读取你的微信密码。</p>
|
||||
<button
|
||||
type="button"
|
||||
className="agent-hub-button secondary"
|
||||
disabled={busy}
|
||||
onClick={() => void runAction(() => window.api.cancelAgentHubLogin())}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : status.connector === 'online' ? (
|
||||
<div className="agent-hub-connected">
|
||||
<div className="agent-hub-connected-icon" aria-hidden>
|
||||
✓
|
||||
</div>
|
||||
<div>
|
||||
<h3>微信机器人已连接</h3>
|
||||
<p>{status.accountId || status.wechatUserId || '登录凭据已就绪'}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="agent-hub-empty-login">
|
||||
<div className="agent-hub-phone" aria-hidden>
|
||||
<span />
|
||||
</div>
|
||||
<h3>{status.connector === 'error' ? '连接遇到问题' : '尚未连接微信机器人'}</h3>
|
||||
<p>{status.error || '扫码登录后,即可从微信向 Agent Hub 提问。'}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="agent-hub-actions">
|
||||
{status.connector === 'online' ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="agent-hub-button secondary"
|
||||
disabled={busy}
|
||||
onClick={() => void runAction(() => window.api.startAgentHubLogin())}
|
||||
>
|
||||
重新扫码登录
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="agent-hub-button danger"
|
||||
disabled={busy}
|
||||
onClick={() => void runAction(() => window.api.disconnectAgentHub())}
|
||||
>
|
||||
断开连接
|
||||
</button>
|
||||
</>
|
||||
) : !isLoginFlow ? (
|
||||
<button
|
||||
type="button"
|
||||
className="agent-hub-button primary"
|
||||
disabled={busy || status.hub !== 'online'}
|
||||
onClick={() => void runAction(() => window.api.startAgentHubLogin())}
|
||||
>
|
||||
{busy ? '正在获取二维码…' : '扫码登录微信机器人'}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside className="agent-hub-card agent-hub-capability-card">
|
||||
<span className="agent-hub-card-kicker">已启用能力</span>
|
||||
<h2>微信数据助手</h2>
|
||||
<p>机器人通过本机 Agent Hub 调用 WechatExplorer,不向公网暴露数据库。</p>
|
||||
<div className="agent-hub-example">
|
||||
<span>可以这样问</span>
|
||||
<strong>“最近 5 条消息是谁?”</strong>
|
||||
<strong>“生成产品交流群今天的群聊总结图片”</strong>
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<i />
|
||||
本机 HTTP 通信
|
||||
</li>
|
||||
<li>
|
||||
<i />
|
||||
入站请求鉴权
|
||||
</li>
|
||||
<li>
|
||||
<i />
|
||||
消息重复保护
|
||||
</li>
|
||||
<li>
|
||||
<i className={status.dataApi === 'online' ? '' : 'offline'} />
|
||||
本地数据 API:{status.dataApi === 'online' ? '已连接' : '未连接'}
|
||||
</li>
|
||||
<li>
|
||||
<i className={status.databaseReady ? '' : 'offline'} />
|
||||
微信数据库:{status.databaseReady ? '可查询' : '未就绪'}
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<section className="agent-hub-card agent-hub-log-card">
|
||||
<div className="agent-hub-log-heading">
|
||||
<div>
|
||||
<span className="agent-hub-card-kicker">故障诊断</span>
|
||||
<h2>运行日志</h2>
|
||||
</div>
|
||||
<div className="agent-hub-log-actions">
|
||||
<select
|
||||
aria-label="筛选日志来源"
|
||||
value={logSource}
|
||||
onChange={(event) => setLogSource(event.target.value as 'all' | AgentHubLogSource)}
|
||||
>
|
||||
<option value="all">全部来源</option>
|
||||
<option value="system">系统</option>
|
||||
<option value="agent-hub">Agent Hub</option>
|
||||
<option value="wechat-connector">微信连接器</option>
|
||||
</select>
|
||||
<button type="button" onClick={() => void copyLogs()} disabled={visibleLogs.length === 0}>
|
||||
复制日志
|
||||
</button>
|
||||
<button type="button" onClick={() => void clearLogs()}>
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="agent-hub-log-body" ref={logBodyRef}>
|
||||
{visibleLogs.length === 0 ? (
|
||||
<div className="agent-hub-log-empty">暂无运行日志。收到消息后,这里会显示处理到哪一步。</div>
|
||||
) : (
|
||||
visibleLogs.map((entry) => (
|
||||
<div className={`agent-hub-log-line ${entry.level}`} key={entry.id}>
|
||||
<time>{new Date(entry.timestamp).toLocaleTimeString()}</time>
|
||||
<span className={`source ${entry.source}`}>{LOG_SOURCE_LABELS[entry.source]}</span>
|
||||
<code>{entry.message}</code>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<p className="agent-hub-log-note">日志会隐藏 Token 和二维码数据,不记录你的微信密码。</p>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -40,6 +40,17 @@ export function ApiRequestTester({
|
||||
void onCopyCurl(command)
|
||||
}
|
||||
const update = (key: string, value: string): void => onParams({ ...params, [key]: value })
|
||||
const selectTestImage = async (): Promise<void> => {
|
||||
const result = await window.api.selectAgentHubTestImage()
|
||||
if (result.canceled || !result.path) return
|
||||
let payload: Record<string, unknown> = {}
|
||||
try {
|
||||
payload = JSON.parse(body) as Record<string, unknown>
|
||||
} catch {
|
||||
// Replace an invalid draft with a valid send-test request.
|
||||
}
|
||||
onBody(JSON.stringify({ ...payload, media_url: result.path }, null, 2))
|
||||
}
|
||||
return (
|
||||
<section className="api-request-tester" id="api-request-tester">
|
||||
<div className="api-section-heading">
|
||||
@@ -77,6 +88,14 @@ export function ApiRequestTester({
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
{endpoint.id === 'agent-send' && (
|
||||
<div className="api-upload-test-row">
|
||||
<button type="button" onClick={() => void selectTestImage()}>
|
||||
选择测试图片
|
||||
</button>
|
||||
<span>选择后只会填入本地路径;点击“发送请求”才会真正发送。</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="api-tester-actions">
|
||||
<button type="button" onClick={onClear}>
|
||||
清空
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useCallback, useEffect, useReducer } from 'react'
|
||||
import type { Contact } from '../../../../../shared/types'
|
||||
import { findEndpoint } from '../model/apiEndpoints'
|
||||
import { REPORT_REQUEST_PRESET } from '../model/requestPresets'
|
||||
import {
|
||||
AGENT_GROUP_REPORT_PRESET,
|
||||
AGENT_SEND_PRESET,
|
||||
REPORT_REQUEST_PRESET
|
||||
} from '../model/requestPresets'
|
||||
import { type AgentInstallTarget, type SkillInstallSource } from '../model/skillDistribution'
|
||||
import type {
|
||||
ApiResponse,
|
||||
@@ -66,14 +70,23 @@ function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
case 'loaded':
|
||||
return { ...state, settings: action.settings, service: action.service, skill: action.skill }
|
||||
case 'endpoint':
|
||||
case 'endpoint': {
|
||||
const preset =
|
||||
action.endpointId === 'report'
|
||||
? REPORT_REQUEST_PRESET
|
||||
: action.endpointId === 'agent-group-report'
|
||||
? AGENT_GROUP_REPORT_PRESET
|
||||
: action.endpointId === 'agent-send'
|
||||
? AGENT_SEND_PRESET
|
||||
: state.body
|
||||
return {
|
||||
...state,
|
||||
endpointId: action.endpointId,
|
||||
params: action.talker && action.endpointId === 'chatlog' ? { talker: action.talker } : {},
|
||||
body: action.endpointId === 'report' ? state.body || REPORT_REQUEST_PRESET : state.body,
|
||||
body: preset,
|
||||
error: ''
|
||||
}
|
||||
}
|
||||
case 'params':
|
||||
return { ...state, params: action.params }
|
||||
case 'body':
|
||||
|
||||
@@ -62,6 +62,20 @@ export const API_ENDPOINTS: ApiEndpoint[] = [
|
||||
name: '群聊日报导出',
|
||||
description: '通过内置模板导出群聊日报 HTML 与 PNG。',
|
||||
body: true
|
||||
}),
|
||||
endpoint('agent-status', {
|
||||
name: 'Agent Hub 状态',
|
||||
description: '检查 Agent Hub、微信连接器、本地数据 API 和数据库状态。'
|
||||
}),
|
||||
endpoint('agent-group-report', {
|
||||
name: '生成群聊总结图片',
|
||||
description: '读取指定群聊并生成今天、昨天或近 7 天的总结长图。',
|
||||
body: true
|
||||
}),
|
||||
endpoint('agent-send', {
|
||||
name: '微信发送测试',
|
||||
description: '测试文字或本地图片发送,并区分凭证失效、连接器离线和发送成功。',
|
||||
body: true
|
||||
})
|
||||
]
|
||||
|
||||
|
||||
@@ -29,3 +29,15 @@ export const REPORT_REQUEST_PRESET = JSON.stringify(
|
||||
null,
|
||||
2
|
||||
)
|
||||
|
||||
export const AGENT_GROUP_REPORT_PRESET = JSON.stringify(
|
||||
{ group: '技术交流', range: 'today' },
|
||||
null,
|
||||
2
|
||||
)
|
||||
|
||||
export const AGENT_SEND_PRESET = JSON.stringify(
|
||||
{ to: '', text: 'WechatExplorer Agent Hub 发送测试' },
|
||||
null,
|
||||
2
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AccountOverview } from '../account-database/AccountOverview'
|
||||
import { ConnectionHealthSection } from '../account-database/ConnectionHealthSection'
|
||||
import { LocalPrivacyNotice } from '../account-database/LocalPrivacyNotice'
|
||||
@@ -25,6 +26,26 @@ export function AccountDatabasePage({
|
||||
onNotice: (message: string) => void
|
||||
}): React.ReactElement {
|
||||
const controller = useAccountDatabaseController({ dbKey, dbReady, selfInfo, onNotice })
|
||||
const [autoLogin, setAutoLogin] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
void window.api.getSettings().then((result) => {
|
||||
if (active) setAutoLogin(result.settings.autoLogin)
|
||||
})
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const changeAutoLogin = async (checked: boolean): Promise<void> => {
|
||||
const result = await window.api.setSettings({
|
||||
autoLogin: checked,
|
||||
autoLoginPreferenceSet: true
|
||||
})
|
||||
setAutoLogin(result.settings.autoLogin)
|
||||
onNotice(checked ? '已开启启动时自动连接' : '已关闭启动时自动连接')
|
||||
}
|
||||
return (
|
||||
<div className="settings-page">
|
||||
<header className="settings-page-header">
|
||||
@@ -58,6 +79,20 @@ export function AccountDatabasePage({
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<h2 className="settings-section-heading">启动行为</h2>
|
||||
<section className="settings-card settings-auto-login-card">
|
||||
<label>
|
||||
<span>
|
||||
<b>启动时自动连接数据库</b>
|
||||
<small>使用安全存储中已保存的数据库密钥;可随时关闭。</small>
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoLogin}
|
||||
onChange={(event) => void changeAutoLogin(event.target.checked)}
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,14 @@ import {
|
||||
ReportVoiceLeaderboardItem
|
||||
} from '../../../shared/group-report'
|
||||
|
||||
declare const window: {
|
||||
api: {
|
||||
imageListCandidates: (...args: unknown[]) => Promise<any>
|
||||
imageAnalyze: (...args: unknown[]) => Promise<any>
|
||||
getImage: (...args: unknown[]) => Promise<any>
|
||||
}
|
||||
}
|
||||
|
||||
export interface GroupReportTranscriptRow {
|
||||
id: string
|
||||
datetime: string
|
||||
@@ -184,6 +192,7 @@ const buildMediaSection = async (
|
||||
warnings: string[]
|
||||
}> => {
|
||||
const warnings: string[] = []
|
||||
const rendererApi = typeof window === 'undefined' ? null : window.api
|
||||
const rawImageCandidates = messages
|
||||
.map((message, index) => {
|
||||
if (message.contentData?.type !== 'image') return null
|
||||
@@ -214,6 +223,7 @@ const buildMediaSection = async (
|
||||
// ============================================================
|
||||
let visionGallery: ReportVisionGalleryItem[] = []
|
||||
try {
|
||||
if (!rendererApi) throw new Error('后台模式不读取 Renderer 图片')
|
||||
const sessionId = messages.find((m) => m.sessionId)?.sessionId || (contact?.md5 ?? '')
|
||||
const startTime = messages.length ? parseTimestamp(messages[0]) : 0
|
||||
const endTime = messages.length ? parseTimestamp(messages[messages.length - 1]) : 0
|
||||
@@ -233,7 +243,7 @@ const buildMediaSection = async (
|
||||
}
|
||||
})
|
||||
|
||||
const candidatesResp = await window.api.imageListCandidates({
|
||||
const candidatesResp = await rendererApi.imageListCandidates({
|
||||
sessionId,
|
||||
startTime,
|
||||
endTime,
|
||||
@@ -249,7 +259,7 @@ const buildMediaSection = async (
|
||||
if (candidate.insight) return candidate.insight
|
||||
// 未命中:解密图片拿 base64 → 调 AI
|
||||
try {
|
||||
const img = await window.api.getImage(
|
||||
const img = await rendererApi.getImage(
|
||||
candidate.md5,
|
||||
candidate.datName,
|
||||
candidate.sessionId
|
||||
@@ -260,7 +270,7 @@ const buildMediaSection = async (
|
||||
)
|
||||
return null
|
||||
}
|
||||
const analyzeResp = await window.api.imageAnalyze({
|
||||
const analyzeResp = await rendererApi.imageAnalyze({
|
||||
imageHash: candidate.imageHash,
|
||||
imageDataUrl: img.data,
|
||||
messageId: candidate.messageId,
|
||||
@@ -306,7 +316,7 @@ const buildMediaSection = async (
|
||||
const orig = rawImageCandidates.find((c) => c.sourceMessageIds[0] === item.messageId)
|
||||
if (!orig) return item
|
||||
try {
|
||||
const img = await window.api.getImage(orig.md5, orig.datName, orig.sessionId)
|
||||
const img = await rendererApi.getImage(orig.md5, orig.datName, orig.sessionId)
|
||||
if (img.success && img.data?.startsWith('data:image/')) {
|
||||
return { ...item, imageUrl: img.data }
|
||||
}
|
||||
@@ -323,9 +333,10 @@ const buildMediaSection = async (
|
||||
visionGallery = []
|
||||
}
|
||||
|
||||
const imageCandidates = await Promise.all(
|
||||
rawImageCandidates.map(async (item) => {
|
||||
const result = await window.api.getImage(item.md5, item.datName, item.sessionId)
|
||||
const imageCandidates = rendererApi
|
||||
? await Promise.all(
|
||||
rawImageCandidates.map(async (item) => {
|
||||
const result = await rendererApi.getImage(item.md5, item.datName, item.sessionId)
|
||||
if (!result.success || !result.data?.startsWith('data:image/')) return null
|
||||
return {
|
||||
sender: item.sender,
|
||||
@@ -337,9 +348,10 @@ const buildMediaSection = async (
|
||||
sourceMessageIds: item.sourceMessageIds,
|
||||
replyCount: item.replyCount,
|
||||
score: item.score
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
: []
|
||||
|
||||
const gallery: ReportMediaGalleryItem[] = imageCandidates
|
||||
.filter((item): item is NonNullable<typeof item> => Boolean(item))
|
||||
|
||||
Reference in New Issue
Block a user