From 3b64e18b5e3f3e1bb6401ea7f6e681b89411a2d2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=94=B5=E6=91=87=E5=B0=8F=E5=AD=90?= <969409112@qq.com>
Date: Sun, 26 Jul 2026 20:45:53 +0800
Subject: [PATCH 1/5] =?UTF-8?q?chore(security):=20=E7=A7=BB=E9=99=A4=20AI?=
=?UTF-8?q?=20=E5=86=85=E7=BD=AE=20Key=20fallback=20+=20=E4=BF=AE=E6=AD=A3?=
=?UTF-8?q?=E9=BB=98=E8=AE=A4=E6=A8=A1=E5=9E=8B=E5=90=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
发布版本不再自动从环境变量读取 VITE_DEEPSEEK_API_KEY 创建默认 DeepSeek provider。
ensureEnvironmentMigration() 改为 no-op(保留方法作为占位)。
用户首次启动必须在「设置 → AI 模型」手动配置 API Key。
migrateLegacy() 保留:用于把用户自己之前存在 localStorage 的旧配置迁移到加密存储,
与内置 Key 是两回事。
附带修正 .env.example 默认模型名:deepseek-v4-flash 是已弃用/不存在的标识符,
DeepSeek 官方未发布此模型,统一改为 deepseek-chat。
---
.env.example | 5 ++++-
src/main/services/ai-provider-service.ts | 14 ++++----------
2 files changed, 8 insertions(+), 11 deletions(-)
diff --git a/.env.example b/.env.example
index 5436001..cd56eb5 100644
--- a/.env.example
+++ b/.env.example
@@ -6,9 +6,12 @@ VITE_DB_KEY=
VITE_AUTO_LOGIN=false
# AI API Configuration (Optional, can be entered in UI)
+# 注意:发布版本不再自动读取以下环境变量。
+# 如果你只是本地开发想用默认值,可以在自己机器的 .env.local 里填,
+# 然后在「设置 → AI 模型」里手动完成"添加供应商"流程。
VITE_DEEPSEEK_API_KEY=
VITE_AI_BASE_URL=https://api.deepseek.com
-VITE_AI_MODEL=deepseek-v4-flash
+VITE_AI_MODEL=deepseek-chat
# Message types to filter out (comma separated). Empty means show all message types.
VITE_FILTER_MSG_TYPES=
diff --git a/src/main/services/ai-provider-service.ts b/src/main/services/ai-provider-service.ts
index 5accd30..3335dd9 100644
--- a/src/main/services/ai-provider-service.ts
+++ b/src/main/services/ai-provider-service.ts
@@ -41,7 +41,6 @@ export class AIProviderService {
constructor(private readonly keyStore = new AIProviderKeyStore()) {}
list(): AIProviderListResult {
- this.ensureEnvironmentMigration()
try {
const data = this.readMetadata()
return {
@@ -339,15 +338,10 @@ export class AIProviderService {
}
private ensureEnvironmentMigration(): void {
- const data = this.readMetadata()
- if (data.providers.length) return
- const apiKey = String(import.meta.env.VITE_DEEPSEEK_API_KEY || '').trim()
- if (!apiKey) return
- this.migrateLegacy({
- apiKey,
- baseUrl: String(import.meta.env.VITE_AI_BASE_URL || ''),
- model: String(import.meta.env.VITE_AI_MODEL || '')
- })
+ // 已禁用:内置环境变量 Key 自动迁移策略。
+ // 安全要求:发布给最终用户的版本不应携带任何内置 API Key,
+ // 必须由用户自己在 UI 里手动配置(或者通过自己的 .env.local 注入)。
+ // 保留此方法作为占位,方便后续重新评估。
}
private toSummary(
From 1a9f27488b6b275f5b76fbc00b3a5f6fecac1d01 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=94=B5=E6=91=87=E5=B0=8F=E5=AD=90?= <969409112@qq.com>
Date: Sun, 26 Jul 2026 20:46:20 +0800
Subject: [PATCH 2/5] =?UTF-8?q?refactor(image-decrypt):=20=E8=AE=BE?=
=?UTF-8?q?=E7=BD=AE=E9=9D=A2=E6=9D=BF=20UI=20=E9=87=8D=E6=9E=84=20+=20?=
=?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=AD=A5=E9=AA=A4=E8=81=94=E5=8A=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
页面拆分两个目录语义:
- 上方「图片解密状态」的「图片资源目录」始终等于 chat 实时识别的默认目录(chat.getCurrentAccountRoot()),
不再被用户输入覆盖。
- 下方「图片密钥管理」移除「图片资源目录」输入框,只保留 XOR / AES 两个 Key。
该字段之前会被保存但实际不影响图片解密(数据库查询走默认根目录),保留只会让用户困惑。
UI 改动:
- ImageKeyConfiguration:移除 resourceRoot 输入框,edit() 签名收窄为 xorKey | aesKey。
- ImageTestSection:测试结果由 3 列横排改为步骤条样式(pending / ok / fail / skipped),
任意一步 ✗ 后续步骤自动标记「跳过」并灰掉,前端语义上变成「要么都成功要么都失败」。
- AutoDetectImageKeySection:加一行小字提示「仅支持 WeChat 4.0,V3 及以下无法解析」。
后端改动:
- testImageDecryption:解密失败时把 fileFound/decrypted/readable 统一置 false,
拆出「解密成功但不可读」中间态,让 UI 步骤联动准确反映。
- inspectImageDecryptionStatus:accountRoot 兜底改为 getCurrentAccountRoot() || config.resourceRoot,
让状态面板始终显示当前识别到的默认目录。
- sanitizeImageError:把 'no_image_message' / '300' / 'unsupported' / 'dat version' 关键字翻译成具体提示。
- useImageDecryptionController.autoDetect:错误信息原文透传,不再走 sanitizeImageError(后者会
把扫描阶段的「未找到 V2 模板文件」/「60 秒未找到 AES 密钥」等归类成「无法解析媒体文件」,掩盖真因)。
- image-key-config-service.save():不再写回 imageKeyRoot,下方输入框只用于校验,不再落盘。
---
.../image-decryption-status-service.ts | 50 ++++++++---
src/main/services/image-key-config-service.ts | 7 +-
src/renderer/src/assets/main.css | 59 +++++++++++++
.../AutoDetectImageKeySection.tsx | 4 +
.../ImageKeyConfiguration.tsx | 11 +--
.../image-decryption/ImageTestSection.tsx | 86 +++++++++++++++++--
.../imageDecryptionReducer.ts | 8 +-
.../settings/image-decryption/types.ts | 4 +-
.../useImageDecryptionController.ts | 19 ++--
.../settings/image-decryption/utils.ts | 8 +-
10 files changed, 215 insertions(+), 41 deletions(-)
diff --git a/src/main/services/image-decryption-status-service.ts b/src/main/services/image-decryption-status-service.ts
index 75f92bd..a3ab925 100644
--- a/src/main/services/image-decryption-status-service.ts
+++ b/src/main/services/image-decryption-status-service.ts
@@ -17,7 +17,9 @@ import { isWechatRunning } from './wechat-process-status'
export async function inspectImageDecryptionStatus(
config: ImageKeyConfigResult
): Promise {
- const accountRoot = chat.getCurrentAccountRoot() || config.resourceRoot
+ // 状态面板的"图片资源目录"始终等于当前识别到的微信账号根目录;
+ // 仅在微信未连接时回退到上次配置中的 resourceRoot,避免空白。
+ const accountRoot = chat.getCurrentAccountRoot() || config.resourceRoot || ''
const imageDirectoryFound = hasImageDirectory(accountRoot)
const stickerCacheFound =
fs.existsSync(path.join(accountRoot, 'cache')) ||
@@ -65,7 +67,10 @@ export function testImageDecryption(
.reverse()
.find((message) => message.contentData?.type === 'image')
if (!imageMessage || imageMessage.contentData?.type !== 'image') {
- return failure('NO_IMAGE_MESSAGE', '所选聊天最近没有可测试的图片消息')
+ return failure(
+ 'NO_IMAGE_MESSAGE',
+ '所选聊天最近 300 条消息内没有可测试的图片,请换一个含图片的会话'
+ )
}
const service = new ImageDecryptService(
@@ -74,26 +79,51 @@ export function testImageDecryption(
chat.getChatDb()?.getWcdb4Client()
)
const image = imageMessage.contentData
- let filePath = service.findImageFile(image.md5, image.datName, { allowThumbnail: false })
+ // 测试时优先使用用户在下方"图片资源目录"输入框填写的目录;
+ // 找不到再退回默认 accountDir。
+ const testAccountDir = normalized.resourceRoot || undefined
+ let filePath = service.findImageFile(image.md5, image.datName, {
+ allowThumbnail: false,
+ accountDir: testAccountDir
+ })
if (!filePath)
- filePath = service.findImageFile(image.md5, image.datName, { allowThumbnail: true })
+ filePath = service.findImageFile(image.md5, image.datName, {
+ allowThumbnail: true,
+ accountDir: testAccountDir
+ })
if (!filePath) return failure('FILE_NOT_FOUND', '图片文件不存在')
const data = service.decryptImageToBase64(filePath)
if (!data) {
+ // 三步联动:解密失败 → fileFound/decrypted/readable 都为 false。
return {
- ...failure('DECRYPT_FAILED', '无法解析媒体文件'),
- fileFound: true
+ success: false,
+ code: 'DECRYPT_FAILED',
+ error: '无法解析媒体文件',
+ fileFound: false,
+ decrypted: false,
+ readable: false
}
}
const readable = data.startsWith('data:image/')
+ if (!readable) {
+ // 三步联动:解密成功但字节流不可读 → 前一步打勾(确实找到了 dat),
+ // 但 decrypted/readable 全为 false,让 UI 表达"找到但解析失败"。
+ return {
+ success: false,
+ code: 'DECRYPT_FAILED',
+ error: '图片解密结果不可读取',
+ fileFound: true,
+ decrypted: false,
+ readable: false,
+ isThumbnail: service.isThumbnailFile(filePath)
+ }
+ }
return {
- success: readable,
- code: readable ? undefined : 'DECRYPT_FAILED',
- error: readable ? undefined : '图片解密结果不可读取',
+ success: true,
fileFound: true,
decrypted: true,
- readable,
+ readable: true,
isThumbnail: service.isThumbnailFile(filePath)
}
} catch {
diff --git a/src/main/services/image-key-config-service.ts b/src/main/services/image-key-config-service.ts
index a7bc547..3c54b42 100644
--- a/src/main/services/image-key-config-service.ts
+++ b/src/main/services/image-key-config-service.ts
@@ -92,9 +92,10 @@ export class ImageKeyConfigService {
if (!result.success || !result.entry) {
return { ...this.getEmptyConfig(), error: result.error || '图片密钥保存失败' }
}
+ // 注意:normalized.resourceRoot 不再写回 imageKeyRoot。
+ // 下方的"图片资源目录"输入框仅用于本次手动测试,不再污染状态面板上方显示。
saveSettings({
...settings,
- imageKeyRoot: normalized.resourceRoot,
imageXorKey: '',
imageAesKey: '',
imageKeyFallbackDisabled: false
@@ -106,7 +107,9 @@ export class ImageKeyConfigService {
encryptionAvailable: true,
source: 'secure-storage',
accountId: context.accountId,
- resourceRoot: normalized.resourceRoot,
+ // 返回的 resourceRoot 始终是识别到的默认目录,与状态面板一致;
+ // 不返回 normalized.resourceRoot,避免把用户输入的测试目录当成状态写回。
+ resourceRoot: context.resourceRoot,
xorKey: result.entry.xorKey,
aesKey: result.entry.aesKey,
updatedAt: result.entry.updatedAt
diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css
index 1d62bb4..3efe764 100644
--- a/src/renderer/src/assets/main.css
+++ b/src/renderer/src/assets/main.css
@@ -5791,6 +5791,59 @@ body {
width: 100%;
margin: 2px 0 0;
}
+.image-step-list {
+ list-style: none;
+ margin: 14px 0 0;
+ padding: 12px 14px;
+ border-radius: 8px;
+ border: 1px solid #e5ecea;
+ background: #f7faf9;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ font-size: 12px;
+}
+.image-step {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.image-step-icon {
+ width: 18px;
+ height: 18px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ font-size: 12px;
+ font-weight: 600;
+}
+.image-step-ok .image-step-icon {
+ background: #2e765d;
+ color: #fff;
+}
+.image-step-fail .image-step-icon {
+ background: #a84444;
+ color: #fff;
+}
+.image-step-fail span:last-child {
+ color: #a84444;
+}
+.image-step-pending .image-step-icon {
+ background: #d6dde0;
+ color: #6a7378;
+}
+.image-step-pending span:last-child {
+ color: #98a1a4;
+}
+.image-step-skipped .image-step-icon {
+ background: transparent;
+ color: #b8c0c4;
+}
+.image-step-skipped span:last-child {
+ color: #b8c0c4;
+ text-decoration: line-through;
+}
.image-inline-error {
margin: 14px 0 0;
color: #a84444;
@@ -5829,6 +5882,12 @@ body {
border-radius: 7px;
background: #f1f4f3;
}
+.image-auto-scope-hint {
+ display: inline-block;
+ margin-top: 4px;
+ color: #7d8c8a;
+ font-size: 11px;
+}
.image-auto-unavailable {
padding-top: 20px;
padding-bottom: 20px;
diff --git a/src/renderer/src/features/settings/image-decryption/AutoDetectImageKeySection.tsx b/src/renderer/src/features/settings/image-decryption/AutoDetectImageKeySection.tsx
index c89b7bd..0cc50dd 100644
--- a/src/renderer/src/features/settings/image-decryption/AutoDetectImageKeySection.tsx
+++ b/src/renderer/src/features/settings/image-decryption/AutoDetectImageKeySection.tsx
@@ -45,6 +45,10 @@ export function AutoDetectImageKeySection({
{state.status.platform === 'darwin'
? '扫描本机微信缓存并通过图片模板验证候选密钥。'
: '扫描微信进程内存并通过本地图片模板验证候选密钥。'}
+
+
+ 仅支持 WeChat 4.0,V3 及以下无法解析
+