feat: 支持自动获取并安全保存数据库密钥

This commit is contained in:
电摇小子
2026-06-30 10:12:25 +08:00
parent c00d581d9e
commit 10dc74e2a1
7 changed files with 482 additions and 9 deletions
+120 -7
View File
@@ -3,6 +3,8 @@ import { Sidebar } from './components/Sidebar'
import ChatWindow from './components/ChatWindow'
import { Contact, Message } from '../../shared/types'
const MAC_KEY_FAQ_URL = 'https://github.com/hicccc77/WeFlow/blob/main/docs/MAC-KEY-FAQ.md'
function App(): React.ReactElement {
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [dbKey, setDbKey] = useState(import.meta.env.VITE_DB_KEY || '')
@@ -12,6 +14,32 @@ function App(): React.ReactElement {
const [filteredContacts, setFilteredContacts] = useState<Contact[]>([])
const [dateRange, setDateRange] = useState('today') // 默认为今天
const [contentFilter, setContentFilter] = useState('')
const [isFetchingDbKey, setIsFetchingDbKey] = useState(false)
const [dbKeyStatus, setDbKeyStatus] = useState('')
const [dbKeyStatusKind, setDbKeyStatusKind] = useState<'normal' | 'success' | 'error'>('normal')
const [showDbKey, setShowDbKey] = useState(false)
const [showMacKeyFaq, setShowMacKeyFaq] = useState(false)
React.useEffect(() => {
let active = true
void window.api.getSavedDbKey().then((result) => {
if (!active) return
if (result.success && result.key) {
setDbKey(result.key)
setDbKeyStatus('已加载安全保存的密钥')
setDbKeyStatusKind('success')
}
})
const unsubscribe = window.api.onDbKeyStatus(({ message }) => {
if (!active) return
setDbKeyStatus(message)
setDbKeyStatusKind('normal')
})
return () => {
active = false
unsubscribe()
}
}, [])
// useEffect(() => {
// if (import.meta.env.VITE_DB_KEY) {
@@ -38,6 +66,55 @@ function App(): React.ReactElement {
}
}
const handleAutoGetDbKey = async (): Promise<void> => {
if (isFetchingDbKey) return
setIsFetchingDbKey(true)
setDbKeyStatus('正在准备获取密钥...')
setDbKeyStatusKind('normal')
setShowMacKeyFaq(false)
try {
const result = await window.api.autoGetDbKey()
if (!result.success || !result.key) {
setShowMacKeyFaq(result.code === 'SCAN_FAILED')
throw new Error(result.error || '获取密钥失败')
}
setDbKey(result.key)
setDbKeyStatus(result.saved ? '密钥已获取并安全保存' : result.warning || '密钥已获取')
setDbKeyStatusKind(result.saved ? 'success' : 'normal')
} catch (error) {
setDbKeyStatus(error instanceof Error ? error.message : String(error))
setDbKeyStatusKind('error')
} finally {
setIsFetchingDbKey(false)
}
}
const handlePasteAndSaveDbKey = async (): Promise<void> => {
setShowMacKeyFaq(false)
const result = await window.api.pasteAndSaveDbKey()
if (result.success && result.key) {
setDbKey(result.key)
setDbKeyStatus('已从剪贴板粘贴并安全保存')
setDbKeyStatusKind('success')
} else {
setDbKeyStatus(result.error || '粘贴并保存失败')
setDbKeyStatusKind('error')
}
}
const handleClearSavedDbKey = async (): Promise<void> => {
setShowMacKeyFaq(false)
const result = await window.api.clearSavedDbKey()
if (!result.success) {
setDbKeyStatus(result.error || '清除密钥失败')
setDbKeyStatusKind('error')
return
}
setDbKey('')
setDbKeyStatus('已清除保存的密钥')
setDbKeyStatusKind('normal')
}
const loadContacts = async (): Promise<void> => {
const list = await window.api.getContacts()
setContacts(list)
@@ -146,16 +223,52 @@ function App(): React.ReactElement {
<div className="login-modal">
<div className="login-box">
<h2>Enter WeChat DB Key</h2>
<input
type="password"
className="login-input"
value={dbKey}
onChange={(e) => setDbKey(e.target.value)}
placeholder="Key (e.g. 0x...)"
/>
<div className="login-input-wrapper">
<input
type={showDbKey ? 'text' : 'password'}
className="login-input"
value={dbKey}
onChange={(e) => setDbKey(e.target.value)}
placeholder="Key (e.g. 0x...)"
/>
<button
type="button"
className="login-input-toggle"
onClick={() => setShowDbKey(!showDbKey)}
title={showDbKey ? '隐藏密钥' : '显示密钥'}
>
{showDbKey ? '👁️' : '👁️‍🗨️'}
</button>
</div>
<button
className="login-btn login-btn-secondary"
onClick={handleAutoGetDbKey}
disabled={isFetchingDbKey}
>
{isFetchingDbKey ? '正在获取...' : '自动获取密钥'}
</button>
<button className="login-btn login-btn-secondary" onClick={handlePasteAndSaveDbKey}>
</button>
<button className="login-btn" onClick={() => handleLogin()}>
Connect
</button>
<button className="login-clear-btn" onClick={handleClearSavedDbKey}>
</button>
{dbKeyStatus && (
<div className={`login-key-status ${dbKeyStatusKind}`}>{dbKeyStatus}</div>
)}
{showMacKeyFaq && (
<a
className="login-key-help-link"
href={MAC_KEY_FAQ_URL}
target="_blank"
rel="noreferrer"
>
macOS
</a>
)}
</div>
</div>
)
+89 -1
View File
@@ -654,7 +654,7 @@ body {
background-color: #fff;
padding: 20px;
border-radius: 8px;
width: 300px;
width: 360px;
text-align: center;
}
@@ -666,6 +666,27 @@ body {
border-radius: 4px;
}
.login-input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.login-input-wrapper .login-input {
padding-right: 40px;
margin: 10px 0 10px 0;
}
.login-input-toggle {
position: absolute;
right: 8px;
background: none;
border: none;
cursor: pointer;
padding: 4px 8px;
font-size: 18px;
}
.login-btn {
background-color: #07c160;
color: #fff;
@@ -673,12 +694,79 @@ body {
padding: 8px 20px;
border-radius: 4px;
cursor: pointer;
width: 100%;
margin-top: 8px;
}
.login-btn:hover {
background-color: #06ad56;
}
.login-btn:disabled {
cursor: wait;
opacity: 0.65;
}
.login-btn-secondary {
border: 1px solid #cfd5d8;
background: #fff;
color: #30383d;
}
.login-btn-secondary:hover {
border-color: #07c160;
background: #f2fbf6;
color: #078f49;
}
.login-clear-btn {
margin-top: 10px;
border: 0;
background: transparent;
color: #8a949a;
cursor: pointer;
font-size: 12px;
}
.login-clear-btn:hover {
color: #d33b3b;
}
.login-key-status {
margin-top: 10px;
padding: 8px 10px;
border-radius: 6px;
background: #f3f5f6;
color: #59636a;
font-size: 12px;
line-height: 1.45;
text-align: left;
white-space: pre-wrap;
word-break: break-word;
}
.login-key-status.success {
background: #eefaf3;
color: #078f49;
}
.login-key-status.error {
background: #fff1f0;
color: #c73737;
}
.login-key-help-link {
display: inline-block;
margin-top: 9px;
color: #1677ff;
font-size: 12px;
text-decoration: none;
}
.login-key-help-link:hover {
text-decoration: underline;
}
.modal-overlay {
position: fixed;
top: 0;