From 55a7ce7b6645d43cd86660ae0a6044eaf02adb07 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 28 Apr 2026 00:14:05 +0800 Subject: [PATCH 01/12] feat(insight): add moments context gating and prompt integration --- electron/services/config.ts | 9 +++ electron/services/insightService.ts | 61 ++++++++++++++++++ src/pages/SettingsPage.scss | 80 ++++++++++++++++++++++-- src/pages/SettingsPage.tsx | 95 ++++++++++++++++++++++++++++- src/services/config.ts | 53 ++++++++++++++++ 5 files changed, 293 insertions(+), 5 deletions(-) diff --git a/electron/services/config.ts b/electron/services/config.ts index ff06ccd..948fbf0 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -85,7 +85,13 @@ interface ConfigSchema { aiInsightApiModel: string aiInsightSilenceDays: number aiInsightAllowContext: boolean + aiInsightAllowMomentsContext: boolean + aiInsightMomentsContextCount: number + aiInsightMomentsBindings: Record aiInsightAllowSocialContext: boolean + aiInsightSocialContextCount: number + aiInsightWeiboCookie: string + aiInsightWeiboBindings: Record aiInsightFilterMode: 'whitelist' | 'blacklist' aiInsightFilterList: string[] aiInsightWhitelistEnabled: boolean @@ -205,6 +211,9 @@ export class ConfigService { aiInsightApiModel: 'gpt-4o-mini', aiInsightSilenceDays: 3, aiInsightAllowContext: false, + aiInsightAllowMomentsContext: false, + aiInsightMomentsContextCount: 5, + aiInsightMomentsBindings: {}, aiInsightAllowSocialContext: false, aiInsightFilterMode: 'whitelist', aiInsightFilterList: [], diff --git a/electron/services/insightService.ts b/electron/services/insightService.ts index 0566571..4bfdeba 100644 --- a/electron/services/insightService.ts +++ b/electron/services/insightService.ts @@ -21,6 +21,7 @@ import { URL } from 'url' import { app, Notification } from 'electron' import { ConfigService } from './config' import { chatService, ChatSession, Message } from './chatService' +import { snsService } from './snsService' import { weiboService } from './social/weiboService' // ─── 常量 ──────────────────────────────────────────────────────────────────── @@ -52,6 +53,9 @@ const INSIGHT_CONFIG_KEYS = new Set([ 'aiModelApiMaxTokens', 'aiInsightFilterMode', 'aiInsightFilterList', + 'aiInsightAllowMomentsContext', + 'aiInsightMomentsContextCount', + 'aiInsightMomentsBindings', 'aiInsightAllowSocialContext', 'aiInsightSocialContextCount', 'aiInsightWeiboCookie', @@ -853,6 +857,61 @@ ${topMentionText} return new Date(parsed).toLocaleString('zh-CN') } + private formatMomentsTimestamp(raw: unknown): string { + const numeric = Number(raw) + if (!Number.isFinite(numeric) || numeric <= 0) { + return '' + } + const ms = numeric > 1_000_000_000_000 ? numeric : numeric * 1000 + return new Date(ms).toLocaleString('zh-CN') + } + + private extractMomentReadableText(post: { contentDesc?: unknown; linkTitle?: unknown }): string { + const contentDesc = this.normalizeInsightText(String(post.contentDesc || '')).replace(/\s+/g, ' ').trim() + if (contentDesc) return contentDesc + + const linkTitle = this.normalizeInsightText(String(post.linkTitle || '')).replace(/\s+/g, ' ').trim() + if (linkTitle) return `[链接] ${linkTitle}` + + return '' + } + + private async getMomentsContextSection(sessionId: string): Promise { + const allowMomentsContext = this.config.get('aiInsightAllowMomentsContext') === true + if (!allowMomentsContext) return '' + + const bindings = + (this.config.get('aiInsightMomentsBindings') as Record | undefined) || {} + const isEnabledForSession = bindings[sessionId]?.enabled === true + if (!isEnabledForSession) return '' + + const countRaw = Number(this.config.get('aiInsightMomentsContextCount') || 5) + const momentsCount = Math.max(1, Math.min(20, Math.floor(countRaw) || 5)) + + try { + const result = await snsService.getTimeline(momentsCount, 0, [sessionId]) + const posts = result.success && Array.isArray(result.timeline) ? result.timeline : [] + if (posts.length === 0) return '' + + const lines = posts + .map((post) => { + const text = this.extractMomentReadableText(post as { contentDesc?: unknown; linkTitle?: unknown }) + if (!text) return '' + const shortText = text.length > 180 ? `${text.slice(0, 180)}...` : text + const time = this.formatMomentsTimestamp((post as { createTime?: unknown }).createTime) + return time ? `[朋友圈 ${time}] ${shortText}` : `[朋友圈] ${shortText}` + }) + .filter(Boolean) as string[] + + if (lines.length === 0) return '' + insightLog('INFO', `已加载 ${lines.length} 条朋友圈内容 (sessionId=${sessionId})`) + return `以下是该联系人的朋友圈内容(仅人类可读原文,最近 ${lines.length} 条):\n${lines.join('\n')}` + } catch (error) { + insightLog('WARN', `拉取朋友圈内容失败 (sessionId=${sessionId}): ${(error as Error).message}`) + return '' + } + } + private async getSocialContextSection(sessionId: string): Promise { const allowSocialContext = this.config.get('aiInsightAllowSocialContext') === true if (!allowSocialContext) return '' @@ -1136,6 +1195,7 @@ ${topMentionText} } } + const momentsContextSection = await this.getMomentsContextSection(sessionId) const socialContextSection = await this.getSocialContextSection(sessionId) // ── 默认 system prompt(稳定内容,有利于 provider 端 prompt cache 命中)──── @@ -1170,6 +1230,7 @@ ${topMentionText} `时间统计:${todayStatsDesc}`, `全局统计:${globalStatsDesc}`, contextSection, + momentsContextSection, socialContextSection, '请给出你的见解(≤80字):' ].filter(Boolean).join('\n\n') diff --git a/src/pages/SettingsPage.scss b/src/pages/SettingsPage.scss index 76188e1..b121249 100644 --- a/src/pages/SettingsPage.scss +++ b/src/pages/SettingsPage.scss @@ -3617,7 +3617,12 @@ &.insight-social-tab { .anti-revoke-list-header { - grid-template-columns: minmax(0, 1fr) minmax(300px, 420px) auto; + grid-template-columns: minmax(0, 1fr) 86px minmax(240px, 340px) auto; + + .insight-moments-column-title { + color: var(--text-tertiary); + text-align: center; + } .insight-social-column-title { color: var(--text-tertiary); @@ -3626,7 +3631,7 @@ .anti-revoke-row { display: grid; - grid-template-columns: minmax(0, 1fr) minmax(300px, 420px) auto; + grid-template-columns: minmax(0, 1fr) 86px minmax(240px, 340px) auto; align-items: center; gap: 14px; } @@ -3635,6 +3640,67 @@ min-width: 0; } + .insight-moments-cell { + min-width: 0; + display: flex; + align-items: center; + justify-content: center; + min-height: 30px; + } + + .insight-moments-toggle { + position: relative; + width: 18px; + height: 18px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + + input[type='checkbox'] { + position: absolute; + inset: 0; + margin: 0; + opacity: 0; + cursor: pointer; + } + + .check-indicator { + width: 100%; + height: 100%; + border-radius: 6px; + border: 1px solid color-mix(in srgb, var(--border-color) 78%, var(--primary) 22%); + background: color-mix(in srgb, var(--bg-primary) 86%, var(--bg-secondary) 14%); + color: var(--on-primary, #fff); + display: inline-flex; + align-items: center; + justify-content: center; + transition: all 0.16s ease; + + svg { + opacity: 0; + transform: scale(0.75); + transition: opacity 0.16s ease, transform 0.16s ease; + } + } + + input[type='checkbox']:checked + .check-indicator { + background: var(--primary); + border-color: var(--primary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent); + + svg { + opacity: 1; + transform: scale(1); + } + } + + input[type='checkbox']:focus-visible + .check-indicator { + outline: 2px solid color-mix(in srgb, var(--primary) 42%, transparent); + outline-offset: 1px; + } + } + .insight-social-binding-cell { min-width: 0; display: grid; @@ -3653,7 +3719,7 @@ .binding-platform-chip { flex-shrink: 0; border-radius: 999px; - padding: 2px 8px; + padding: 2px 7px; font-size: 11px; color: var(--text-secondary); border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); @@ -3663,7 +3729,7 @@ .insight-social-binding-input { width: 100%; min-width: 0; - height: 30px; + height: 28px; border-radius: 8px; border: 1px solid var(--border-color); background: color-mix(in srgb, var(--bg-primary) 92%, var(--bg-secondary) 8%); @@ -3752,6 +3818,7 @@ .anti-revoke-list-header { grid-template-columns: minmax(0, 1fr) auto; + .insight-moments-column-title, .insight-social-column-title { display: none; } @@ -3763,11 +3830,16 @@ flex-direction: column; } + .insight-moments-cell, .insight-social-binding-cell, .anti-revoke-row-status { width: 100%; } + .insight-moments-cell { + justify-content: flex-start; + } + .insight-social-binding-cell { grid-template-columns: 1fr; } diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 9025a73..f375f22 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -284,6 +284,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const [aiModelApiMaxTokens, setAiModelApiMaxTokens] = useState(200) const [aiInsightSilenceDays, setAiInsightSilenceDays] = useState(3) const [aiInsightAllowContext, setAiInsightAllowContext] = useState(false) + const [aiInsightAllowMomentsContext, setAiInsightAllowMomentsContext] = useState(false) + const [aiInsightMomentsContextCount, setAiInsightMomentsContextCount] = useState(5) + const [aiInsightMomentsBindings, setAiInsightMomentsBindings] = useState>({}) const [isTestingInsight, setIsTestingInsight] = useState(false) const [insightTestResult, setInsightTestResult] = useState<{ success: boolean; message: string } | null>(null) const [showInsightApiKey, setShowInsightApiKey] = useState(false) @@ -549,6 +552,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const savedAiModelApiMaxTokens = await configService.getAiModelApiMaxTokens() const savedAiInsightSilenceDays = await configService.getAiInsightSilenceDays() const savedAiInsightAllowContext = await configService.getAiInsightAllowContext() + const savedAiInsightAllowMomentsContext = await configService.getAiInsightAllowMomentsContext() + const savedAiInsightMomentsContextCount = await configService.getAiInsightMomentsContextCount() + const savedAiInsightMomentsBindings = await configService.getAiInsightMomentsBindings() const savedAiInsightFilterMode = await configService.getAiInsightFilterMode() const savedAiInsightFilterList = await configService.getAiInsightFilterList() const savedAiInsightCooldownMinutes = await configService.getAiInsightCooldownMinutes() @@ -573,6 +579,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { setAiModelApiMaxTokens(savedAiModelApiMaxTokens) setAiInsightSilenceDays(savedAiInsightSilenceDays) setAiInsightAllowContext(savedAiInsightAllowContext) + setAiInsightAllowMomentsContext(savedAiInsightAllowMomentsContext) + setAiInsightMomentsContextCount(savedAiInsightMomentsContextCount) + setAiInsightMomentsBindings(savedAiInsightMomentsBindings) setAiInsightFilterMode(savedAiInsightFilterMode) setAiInsightFilterList(new Set(savedAiInsightFilterList)) setAiInsightCooldownMinutes(savedAiInsightCooldownMinutes) @@ -3081,6 +3090,24 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { }) } + const isMomentsEnabledForSession = (sessionId: string): boolean => { + return aiInsightMomentsBindings[sessionId]?.enabled === true + } + + const handleToggleMomentsBinding = async (sessionId: string, enabled: boolean) => { + const nextBindings = { ...aiInsightMomentsBindings } + if (enabled) { + nextBindings[sessionId] = { + enabled: true, + updatedAt: Date.now() + } + } else { + delete nextBindings[sessionId] + } + setAiInsightMomentsBindings(nextBindings) + await configService.setAiInsightMomentsBindings(nextBindings) + } + const handleSaveWeiboBinding = async (sessionId: string, displayName: string) => { const draftUid = getWeiboBindingDraftValue(sessionId) setWeiboBindingLoadingSessionId(sessionId) @@ -3319,6 +3346,53 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
+
+ + + 仅对列表中勾选了「朋友圈」且会触发见解的私聊联系人生效。 + 程序只会在触发见解时按需读取最近朋友圈内容,不会做后台持续扫描。 + +
+ {aiInsightAllowMomentsContext ? '已开启' : '已关闭'} + +
+
+ + {aiInsightAllowMomentsContext && ( +
+ + + 仅提取人类可读文本原文,不会拼接朋友圈原始 XML 字段。 + + { + const val = Math.max(1, Math.min(20, parseInt(e.target.value, 10) || 5)) + setAiInsightMomentsContextCount(val) + scheduleConfigSave('aiInsightMomentsContextCount', () => configService.setAiInsightMomentsContextCount(val)) + }} + style={{ width: 100 }} + /> +
+ )} + +
+
@@ -3652,11 +3726,14 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { <>
对话({filteredSessions.length}) + 朋友圈 社交平台(微博) 状态
{filteredSessions.map((session) => { const isSelected = aiInsightFilterList.has(session.username) + const isPrivateSession = session.type === 'private' + const isMomentsEnabled = isMomentsEnabledForSession(session.username) const weiboBinding = aiInsightWeiboBindings[session.username] const weiboDraftValue = getWeiboBindingDraftValue(session.username) const isBindingLoading = weiboBindingLoadingSessionId === session.username @@ -3695,8 +3772,24 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { {getSessionFilterTypeLabel(session.type)}
+
+ {isPrivateSession ? ( + + ) : ( + - + )} +
- {session.type === 'private' ? ( + {isPrivateSession ? ( <>
微博 diff --git a/src/services/config.ts b/src/services/config.ts index 3b0863a..d85e611 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -97,6 +97,9 @@ export const CONFIG_KEYS = { AI_INSIGHT_API_MODEL: 'aiInsightApiModel', AI_INSIGHT_SILENCE_DAYS: 'aiInsightSilenceDays', AI_INSIGHT_ALLOW_CONTEXT: 'aiInsightAllowContext', + AI_INSIGHT_ALLOW_MOMENTS_CONTEXT: 'aiInsightAllowMomentsContext', + AI_INSIGHT_MOMENTS_CONTEXT_COUNT: 'aiInsightMomentsContextCount', + AI_INSIGHT_MOMENTS_BINDINGS: 'aiInsightMomentsBindings', AI_INSIGHT_ALLOW_SOCIAL_CONTEXT: 'aiInsightAllowSocialContext', AI_INSIGHT_FILTER_MODE: 'aiInsightFilterMode', AI_INSIGHT_FILTER_LIST: 'aiInsightFilterList', @@ -132,6 +135,11 @@ export interface AiInsightWeiboBinding { updatedAt: number } +export interface AiInsightMomentsBinding { + enabled: boolean + updatedAt: number +} + export interface ExportDefaultMediaConfig { images: boolean videos: boolean @@ -1922,6 +1930,24 @@ export async function setAiInsightAllowContext(allow: boolean): Promise { await config.set(CONFIG_KEYS.AI_INSIGHT_ALLOW_CONTEXT, allow) } +export async function getAiInsightAllowMomentsContext(): Promise { + const value = await config.get(CONFIG_KEYS.AI_INSIGHT_ALLOW_MOMENTS_CONTEXT) + return value === true +} + +export async function setAiInsightAllowMomentsContext(allow: boolean): Promise { + await config.set(CONFIG_KEYS.AI_INSIGHT_ALLOW_MOMENTS_CONTEXT, allow) +} + +export async function getAiInsightMomentsContextCount(): Promise { + const value = await config.get(CONFIG_KEYS.AI_INSIGHT_MOMENTS_CONTEXT_COUNT) + return typeof value === 'number' && value > 0 ? value : 5 +} + +export async function setAiInsightMomentsContextCount(count: number): Promise { + await config.set(CONFIG_KEYS.AI_INSIGHT_MOMENTS_CONTEXT_COUNT, count) +} + export async function getAiInsightAllowSocialContext(): Promise { const value = await config.get(CONFIG_KEYS.AI_INSIGHT_ALLOW_SOCIAL_CONTEXT) return value === true @@ -2067,6 +2093,33 @@ export async function setAiInsightWeiboBindings(bindings: Record => { + if (!value || typeof value !== 'object') return {} + const result: Record = {} + for (const [sessionIdRaw, bindingRaw] of Object.entries(value as Record)) { + const sessionId = String(sessionIdRaw || '').trim() + if (!sessionId) continue + if (!bindingRaw || typeof bindingRaw !== 'object') continue + const bindingObj = bindingRaw as { enabled?: unknown; updatedAt?: unknown } + if (bindingObj.enabled !== true) continue + const updatedAtRaw = Number(bindingObj.updatedAt) + result[sessionId] = { + enabled: true, + updatedAt: Number.isFinite(updatedAtRaw) && updatedAtRaw > 0 ? Math.floor(updatedAtRaw) : Date.now() + } + } + return result +} + +export async function getAiInsightMomentsBindings(): Promise> { + const value = await config.get(CONFIG_KEYS.AI_INSIGHT_MOMENTS_BINDINGS) + return normalizeAiInsightMomentsBindings(value) +} + +export async function setAiInsightMomentsBindings(bindings: Record): Promise { + await config.set(CONFIG_KEYS.AI_INSIGHT_MOMENTS_BINDINGS, normalizeAiInsightMomentsBindings(bindings)) +} + export async function getAiFootprintEnabled(): Promise { const value = await config.get(CONFIG_KEYS.AI_FOOTPRINT_ENABLED) return value === true From 60a4011539d463d5a6a6958ed6f99fb7f52317f4 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 28 Apr 2026 12:05:33 +0800 Subject: [PATCH 02/12] fix(release): upload assets after packaging --- .github/workflows/release.yml | 101 +++++++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fe14ef8..39f6ae8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,8 +13,42 @@ env: ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/ jobs: + prepare-release: + runs-on: ubuntu-latest + + steps: + - name: Check out git repository + uses: actions/checkout@v5 + with: + fetch-depth: 1 + + - name: Ensure GitHub release exists + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + set -euo pipefail + source .github/scripts/release-utils.sh + + TAG="$GITHUB_REF_NAME" + REPO="$GITHUB_REPOSITORY" + VERSION="${TAG#v}" + prerelease_args=() + if [[ "$VERSION" == *-* ]]; then + prerelease_args+=(--prerelease) + fi + + if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then + retry_cmd 5 3 gh release edit "$TAG" --repo "$REPO" --draft=false "${prerelease_args[@]}" + else + retry_cmd 5 3 gh release create "$TAG" --repo "$REPO" --title "$TAG" --notes "Release $TAG" --target "$GITHUB_SHA" "${prerelease_args[@]}" + fi + + wait_for_release_id "$REPO" "$TAG" 12 2 >/dev/null + release-mac-arm64: runs-on: macos-14 + needs: prepare-release steps: - name: Check out git repository @@ -43,19 +77,29 @@ jobs: npx tsc npx vite build - - name: Package and Publish macOS arm64 (unsigned DMG + ZIP) + - name: Package and Upload macOS arm64 (unsigned DMG + ZIP) env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CSC_IDENTITY_AUTO_DISCOVERY: "false" shell: bash run: | set -euo pipefail + source .github/scripts/release-utils.sh export ELECTRON_BUILDER_BINARIES_MIRROR="https://github.com/electron-userland/electron-builder-binaries/releases/download/" echo "Using ELECTRON_BUILDER_BINARIES_MIRROR=$ELECTRON_BUILDER_BINARIES_MIRROR" - if ! npx electron-builder --mac dmg zip --arm64 --publish always '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}'; then + if ! npx electron-builder --mac dmg zip --arm64 --publish never '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}'; then echo "::warning::DMG packaging failed (hdiutil instability on runner). Retrying with ZIP only." - npx electron-builder --mac zip --arm64 --publish always '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}' + npx electron-builder --mac zip --arm64 --publish never '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}' fi + assets=() + while IFS= read -r file; do + assets+=("$file") + done < <(find release -maxdepth 1 -type f | sort) + if [ "${#assets[@]}" -eq 0 ]; then + echo "No release files found in ./release" + exit 1 + fi + upload_release_assets_with_retry "$GITHUB_REPOSITORY" "$GITHUB_REF_NAME" "${assets[@]}" - name: Inject minimumVersion into latest yml env: @@ -81,6 +125,7 @@ jobs: release-linux: runs-on: ubuntu-latest + needs: prepare-release steps: - name: Check out git repository @@ -114,11 +159,23 @@ jobs: npx tsc npx vite build - - name: Package and Publish Linux + - name: Package and Upload Linux env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash run: | - npx electron-builder --linux --publish always '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}' + set -euo pipefail + source .github/scripts/release-utils.sh + npx electron-builder --linux --publish never '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}' + assets=() + while IFS= read -r file; do + assets+=("$file") + done < <(find release -maxdepth 1 -type f | sort) + if [ "${#assets[@]}" -eq 0 ]; then + echo "No release files found in ./release" + exit 1 + fi + upload_release_assets_with_retry "$GITHUB_REPOSITORY" "$GITHUB_REF_NAME" "${assets[@]}" - name: Inject minimumVersion into latest yml env: @@ -139,6 +196,7 @@ jobs: release: runs-on: windows-latest + needs: prepare-release steps: - name: Check out git repository @@ -167,11 +225,23 @@ jobs: npx tsc npx vite build - - name: Package and Publish + - name: Package and Upload Windows x64 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash run: | - npx electron-builder --win nsis --x64 --publish always '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}' '--config.artifactName=${productName}-${version}-x64-Setup.${ext}' + set -euo pipefail + source .github/scripts/release-utils.sh + npx electron-builder --win nsis --x64 --publish never '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}' '--config.artifactName=${productName}-${version}-x64-Setup.${ext}' + assets=() + while IFS= read -r file; do + assets+=("$file") + done < <(find release -maxdepth 1 -type f | sort) + if [ "${#assets[@]}" -eq 0 ]; then + echo "No release files found in ./release" + exit 1 + fi + upload_release_assets_with_retry "$GITHUB_REPOSITORY" "$GITHUB_REF_NAME" "${assets[@]}" - name: Inject minimumVersion into latest yml env: @@ -192,6 +262,7 @@ jobs: release-windows-arm64: runs-on: windows-latest + needs: prepare-release steps: - name: Check out git repository @@ -220,11 +291,23 @@ jobs: npx tsc npx vite build - - name: Package and Publish Windows arm64 + - name: Package and Upload Windows arm64 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash run: | - npx electron-builder --win nsis --arm64 --publish always '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}' '--config.publish.channel=latest-arm64' '--config.artifactName=${productName}-${version}-arm64-Setup.${ext}' + set -euo pipefail + source .github/scripts/release-utils.sh + npx electron-builder --win nsis --arm64 --publish never '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}' '--config.publish.channel=latest-arm64' '--config.artifactName=${productName}-${version}-arm64-Setup.${ext}' + assets=() + while IFS= read -r file; do + assets+=("$file") + done < <(find release -maxdepth 1 -type f | sort) + if [ "${#assets[@]}" -eq 0 ]; then + echo "No release files found in ./release" + exit 1 + fi + upload_release_assets_with_retry "$GITHUB_REPOSITORY" "$GITHUB_REF_NAME" "${assets[@]}" - name: Inject minimumVersion into latest yml env: From 13cede13f976d1b9afca95f59a98907f217f5b9f Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 28 Apr 2026 12:55:46 +0800 Subject: [PATCH 03/12] fix(settings): polish insight context controls --- src/pages/SettingsPage.scss | 45 ++++++++++-- src/pages/SettingsPage.tsx | 135 +++++++++++++++++++----------------- 2 files changed, 113 insertions(+), 67 deletions(-) diff --git a/src/pages/SettingsPage.scss b/src/pages/SettingsPage.scss index b121249..eccde56 100644 --- a/src/pages/SettingsPage.scss +++ b/src/pages/SettingsPage.scss @@ -915,6 +915,29 @@ color: var(--text-secondary); } +.insight-collapsible-setting { + display: grid; + grid-template-rows: 0fr; + opacity: 0; + transform: translateY(-4px); + transition: grid-template-rows 0.22s ease, opacity 0.18s ease, transform 0.2s ease; + + &.expanded { + grid-template-rows: 1fr; + opacity: 1; + transform: translateY(0); + } + + &.collapsed { + pointer-events: none; + } +} + +.insight-collapsible-setting-inner { + min-height: 0; + overflow: hidden; +} + /* Premium Switch Style */ .switch { position: relative; @@ -3616,22 +3639,35 @@ } &.insight-social-tab { + --insight-moments-column-width: 76px; + --insight-social-column-width: minmax(220px, 300px); + --insight-status-column-width: 82px; + --insight-social-list-grid: minmax(0, 1fr) var(--insight-moments-column-width) var(--insight-social-column-width) var(--insight-status-column-width); + .anti-revoke-list-header { - grid-template-columns: minmax(0, 1fr) 86px minmax(240px, 340px) auto; + grid-template-columns: var(--insight-social-list-grid); + gap: 14px; .insight-moments-column-title { + display: flex; + justify-content: center; color: var(--text-tertiary); - text-align: center; } .insight-social-column-title { + min-width: 0; + color: var(--text-tertiary); + } + + .anti-revoke-status-column-title { + justify-self: end; color: var(--text-tertiary); } } .anti-revoke-row { display: grid; - grid-template-columns: minmax(0, 1fr) 86px minmax(240px, 340px) auto; + grid-template-columns: var(--insight-social-list-grid); align-items: center; gap: 14px; } @@ -3772,9 +3808,10 @@ } .anti-revoke-row-status { - justify-self: flex-end; + justify-self: end; align-items: flex-end; max-width: none; + min-width: 0; } } diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index f375f22..0980328 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -3322,27 +3322,30 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
- {aiInsightAllowContext && ( -
- - - 发送给 AI 的聊天记录最大条数。条数越多分析越准确,token 消耗也越多。 - - { - const val = Math.max(1, Math.min(200, parseInt(e.target.value, 10) || 40)) - setAiInsightContextCount(val) - scheduleConfigSave('aiInsightContextCount', () => configService.setAiInsightContextCount(val)) - }} - style={{ width: 100 }} - /> +
+
+
+ + + 发送给 AI 的聊天记录最大条数。条数越多分析越准确,token 消耗也越多。 + + { + const val = Math.max(1, Math.min(200, parseInt(e.target.value, 10) || 40)) + setAiInsightContextCount(val) + scheduleConfigSave('aiInsightContextCount', () => configService.setAiInsightContextCount(val)) + }} + style={{ width: 100 }} + /> +
- )} +
@@ -3369,27 +3372,30 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
- {aiInsightAllowMomentsContext && ( -
- - - 仅提取人类可读文本原文,不会拼接朋友圈原始 XML 字段。 - - { - const val = Math.max(1, Math.min(20, parseInt(e.target.value, 10) || 5)) - setAiInsightMomentsContextCount(val) - scheduleConfigSave('aiInsightMomentsContextCount', () => configService.setAiInsightMomentsContextCount(val)) - }} - style={{ width: 100 }} - /> +
+
+
+ + + 仅提取人类可读文本原文,不会拼接朋友圈原始 XML 字段。 + + { + const val = Math.max(1, Math.min(20, parseInt(e.target.value, 10) || 5)) + setAiInsightMomentsContextCount(val) + scheduleConfigSave('aiInsightMomentsContextCount', () => configService.setAiInsightMomentsContextCount(val)) + }} + style={{ width: 100 }} + /> +
- )} +
@@ -3428,29 +3434,32 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { )}
- {aiInsightAllowSocialContext && ( -
- - - 当前仅支持微博最近发帖。 -
- 不建议超过 5,避免触发平台风控。 -
- { - const val = Math.max(1, Math.min(5, parseInt(e.target.value, 10) || 3)) - setAiInsightSocialContextCount(val) - scheduleConfigSave('aiInsightSocialContextCount', () => configService.setAiInsightSocialContextCount(val)) - }} - style={{ width: 100 }} - /> +
+
+
+ + + 当前仅支持微博最近发帖。 +
+ 不建议超过 5,避免触发平台风控。 +
+ { + const val = Math.max(1, Math.min(5, parseInt(e.target.value, 10) || 3)) + setAiInsightSocialContextCount(val) + scheduleConfigSave('aiInsightSocialContextCount', () => configService.setAiInsightSocialContextCount(val)) + }} + style={{ width: 100 }} + /> +
- )} +
{/* 自定义 System Prompt */} @@ -3728,7 +3737,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { 对话({filteredSessions.length}) 朋友圈 社交平台(微博) - 状态 + 状态
{filteredSessions.map((session) => { const isSelected = aiInsightFilterList.has(session.username) From 9f9ad337abbedf27083901ed72f8df714390fed6 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 28 Apr 2026 13:30:17 +0800 Subject: [PATCH 04/12] fix(insight): trim prompt noise and smooth settings animation --- electron/services/insightService.ts | 56 ++++++----------------------- src/pages/SettingsPage.scss | 18 +++++----- src/pages/SettingsPage.tsx | 11 +++--- 3 files changed, 26 insertions(+), 59 deletions(-) diff --git a/electron/services/insightService.ts b/electron/services/insightService.ts index 4bfdeba..5554a29 100644 --- a/electron/services/insightService.ts +++ b/electron/services/insightService.ts @@ -10,7 +10,7 @@ * 设计原则: * - 不引入任何额外 npm 依赖,使用 Node 原生 https 模块调用 OpenAI 兼容 API * - 所有失败静默处理,不影响主流程 - * - 当日触发记录(sessionId + 时间列表)随 prompt 一起发送,让模型自行判断是否克制 + * - 触发频率、冷却与名单过滤均在本地完成,不把调度统计塞进模型 prompt */ import https from 'https' @@ -449,7 +449,7 @@ class InsightService { try { const endpoint = buildApiUrl(apiBaseUrl, '/chat/completions') - const requestMessages = [{ role: 'user', content: appendPromptCurrentTime('请回复"连接成功"四个字。') }] + const requestMessages = [{ role: 'user', content: '请回复"连接成功"四个字。' }] insightDebugSection( 'INFO', 'AI 测试连接请求', @@ -827,26 +827,13 @@ ${topMentionText} } /** - * 记录触发并返回该会话今日所有触发时间(用于组装 prompt)。 + * 记录成功推送的见解,用于设置页展示今日触发统计。 */ - private recordTrigger(sessionId: string): string[] { + private recordTrigger(sessionId: string): void { this.resetIfNewDay() const existing = this.todayTriggers.get(sessionId) ?? { timestamps: [] } existing.timestamps.push(Date.now()) this.todayTriggers.set(sessionId, existing) - return existing.timestamps.map(formatTimestamp) - } - - /** - * 获取今日全局已触发次数(所有会话合计),用于 prompt 中告知模型全局上下文。 - */ - private getTodayTotalTriggerCount(): number { - this.resetIfNewDay() - let total = 0 - for (const record of this.todayTriggers.values()) { - total += record.timestamps.length - } - return total } private formatWeiboTimestamp(raw: string): string { @@ -905,7 +892,7 @@ ${topMentionText} if (lines.length === 0) return '' insightLog('INFO', `已加载 ${lines.length} 条朋友圈内容 (sessionId=${sessionId})`) - return `以下是该联系人的朋友圈内容(仅人类可读原文,最近 ${lines.length} 条):\n${lines.join('\n')}` + return `近期朋友圈内容(最近 ${lines.length} 条):\n${lines.join('\n')}` } catch (error) { insightLog('WARN', `拉取朋友圈内容失败 (sessionId=${sessionId}): ${(error as Error).message}`) return '' @@ -917,7 +904,6 @@ ${topMentionText} if (!allowSocialContext) return '' const rawCookie = String(this.config.get('aiInsightWeiboCookie') || '').trim() - const hasCookie = rawCookie.length > 0 const bindings = (this.config.get('aiInsightWeiboBindings') as Record | undefined) || {} @@ -938,10 +924,7 @@ ${topMentionText} return `[微博 ${time}] ${text}` }) insightLog('INFO', `已加载 ${lines.length} 条微博公开内容 (uid=${uid})`) - const riskHint = hasCookie - ? '' - : '\n提示:未配置微博 Cookie,使用移动端公开接口抓取,可能因平台风控导致获取失败或内容较少。' - return `近期公开社交平台内容(来源:微博,最近 ${lines.length} 条):\n${lines.join('\n')}${riskHint}` + return `近期公开社交平台内容(来源:微博,最近 ${lines.length} 条):\n${lines.join('\n')}` } catch (error) { insightLog('WARN', `拉取微博公开内容失败 (uid=${uid}): ${(error as Error).message}`) return '' @@ -1177,10 +1160,6 @@ ${topMentionText} // ── 构建 prompt ──────────────────────────────────────────────────────────── - // 今日触发统计(让模型具备时间与克制感) - const sessionTriggerTimes = this.recordTrigger(sessionId) - const totalTodayTriggers = this.getTodayTotalTriggerCount() - let contextSection = '' if (allowContext) { try { @@ -1211,24 +1190,10 @@ ${topMentionText} const customPrompt = (this.config.get('aiInsightSystemPrompt') as string) || '' const systemPrompt = customPrompt.trim() || DEFAULT_SYSTEM_PROMPT - // 可变的上下文统计信息放在 user message 里,保持 system prompt 稳定不变 - // 这样 provider 端(Anthropic/OpenAI)能最大化命中 prompt cache,降低费用 - const triggerDesc = - triggerReason === 'silence' - ? `你已经 ${silentDays} 天没有和「${resolvedDisplayName}」聊天了。` - : `你最近和「${resolvedDisplayName}」有新的聊天动态。` - - const todayStatsDesc = - sessionTriggerTimes.length > 1 - ? `今天你已经针对「${resolvedDisplayName}」收到过 ${sessionTriggerTimes.length - 1} 条见解(时间:${sessionTriggerTimes.slice(0, -1).join('、')}),请适当克制。` - : `今天你还没有针对「${resolvedDisplayName}」发出过见解。` - - const globalStatsDesc = `今天全部联系人合计已触发 ${totalTodayTriggers} 条见解。` - const userPromptBase = [ - `触发原因:${triggerDesc}`, - `时间统计:${todayStatsDesc}`, - `全局统计:${globalStatsDesc}`, + triggerReason === 'silence' && silentDays + ? `已 ${silentDays} 天未联系「${resolvedDisplayName}」。` + : '', contextSection, momentsContextSection, socialContextSection, @@ -1250,7 +1215,7 @@ ${topMentionText} `接口地址:${endpoint}`, `模型:${model}`, `Max Tokens:${maxTokens}`, - `触发原因:${triggerReason}`, + `触发类型:${triggerReason}`, `上下文开关:${allowContext ? '开启' : '关闭'}`, `上下文条数:${contextCount}`, '', @@ -1314,6 +1279,7 @@ ${topMentionText} } insightLog('INFO', `已为 ${resolvedDisplayName} 推送见解`) + this.recordTrigger(sessionId) } catch (e) { insightDebugSection( 'ERROR', diff --git a/src/pages/SettingsPage.scss b/src/pages/SettingsPage.scss index eccde56..ecf1d8d 100644 --- a/src/pages/SettingsPage.scss +++ b/src/pages/SettingsPage.scss @@ -916,16 +916,18 @@ } .insight-collapsible-setting { - display: grid; - grid-template-rows: 0fr; + max-height: 0; opacity: 0; - transform: translateY(-4px); - transition: grid-template-rows 0.22s ease, opacity 0.18s ease, transform 0.2s ease; + overflow: hidden; + transform: translate3d(0, -4px, 0); + contain: layout paint; + will-change: max-height, opacity, transform; + transition: max-height 0.2s ease, opacity 0.18s ease, transform 0.2s ease; &.expanded { - grid-template-rows: 1fr; + max-height: 128px; opacity: 1; - transform: translateY(0); + transform: translate3d(0, 0, 0); } &.collapsed { @@ -934,8 +936,8 @@ } .insight-collapsible-setting-inner { - min-height: 0; - overflow: hidden; + padding-top: 2px; + backface-visibility: hidden; } /* Premium Switch Style */ diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 0980328..0d508ed 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -3301,7 +3301,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { 开启后,触发见解时会将该联系人最近 N 条聊天记录发送给 AI,分析质量显著提升。
- 关闭时:AI 仅知道统计摘要(沉默天数等),输出质量较低。 + 关闭时:不会发送聊天原文,输出质量较低。
开启时:聊天文本内容(不含图片、语音)会通过你配置的 API 发送给模型提供商。请确认你信任该服务商。
@@ -3352,8 +3352,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
- 仅对列表中勾选了「朋友圈」且会触发见解的私聊联系人生效。 - 程序只会在触发见解时按需读取最近朋友圈内容,不会做后台持续扫描。 + 开启后,可在下方列表为私聊联系人单独允许朋友圈补充分析。程序只会在触发见解时按需读取,不会做后台持续扫描。
{aiInsightAllowMomentsContext ? '已开启' : '已关闭'} @@ -3377,7 +3376,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
- 仅提取人类可读文本原文,不会拼接朋友圈原始 XML 字段。 + 发送给 AI 的朋友圈最大条数。条数越多上下文越充分,token 消耗也越多。

- 触发方式一:活跃会话分析 — 每当微信数据库变化(即你收到新消息)时,经过 500ms 防抖后,对符合黑白名单规则的活跃会话进行分析。
+ 触发方式一:活跃会话分析 — 每当微信数据库变化(即你收到新消息)时,经过约 2 秒防抖后,对符合黑白名单规则的活跃会话进行分析。
触发方式二:沉默扫描 — 每 4 小时独立扫描一次,对超过阈值天数无消息的联系人发出提醒。
- 时间观念 — 每次调用时,AI 会收到今天已向该联系人和全局发出过多少次见解,由 AI 自行决定是否需要克制。
+ 频率控制 — 冷却期、沉默间隔、黑白名单均在本地判断,不额外发送给模型。
隐私 — 所有分析请求均直接从你的电脑发往你填写的 API 地址,不经过任何 WeFlow 服务器。

From dfe018626770316aa57918fd8aafb48cd8f48a45 Mon Sep 17 00:00:00 2001 From: Jason <159670257+Jasonzhu1207@users.noreply.github.com> Date: Tue, 28 Apr 2026 14:00:26 +0800 Subject: [PATCH 05/12] Add files via upload --- .github/workflows/release.yml | 101 +++------------------------------- 1 file changed, 9 insertions(+), 92 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 39f6ae8..fe14ef8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,42 +13,8 @@ env: ELECTRON_BUILDER_BINARIES_MIRROR: https://github.com/electron-userland/electron-builder-binaries/releases/download/ jobs: - prepare-release: - runs-on: ubuntu-latest - - steps: - - name: Check out git repository - uses: actions/checkout@v5 - with: - fetch-depth: 1 - - - name: Ensure GitHub release exists - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - shell: bash - run: | - set -euo pipefail - source .github/scripts/release-utils.sh - - TAG="$GITHUB_REF_NAME" - REPO="$GITHUB_REPOSITORY" - VERSION="${TAG#v}" - prerelease_args=() - if [[ "$VERSION" == *-* ]]; then - prerelease_args+=(--prerelease) - fi - - if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then - retry_cmd 5 3 gh release edit "$TAG" --repo "$REPO" --draft=false "${prerelease_args[@]}" - else - retry_cmd 5 3 gh release create "$TAG" --repo "$REPO" --title "$TAG" --notes "Release $TAG" --target "$GITHUB_SHA" "${prerelease_args[@]}" - fi - - wait_for_release_id "$REPO" "$TAG" 12 2 >/dev/null - release-mac-arm64: runs-on: macos-14 - needs: prepare-release steps: - name: Check out git repository @@ -77,29 +43,19 @@ jobs: npx tsc npx vite build - - name: Package and Upload macOS arm64 (unsigned DMG + ZIP) + - name: Package and Publish macOS arm64 (unsigned DMG + ZIP) env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CSC_IDENTITY_AUTO_DISCOVERY: "false" shell: bash run: | set -euo pipefail - source .github/scripts/release-utils.sh export ELECTRON_BUILDER_BINARIES_MIRROR="https://github.com/electron-userland/electron-builder-binaries/releases/download/" echo "Using ELECTRON_BUILDER_BINARIES_MIRROR=$ELECTRON_BUILDER_BINARIES_MIRROR" - if ! npx electron-builder --mac dmg zip --arm64 --publish never '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}'; then + if ! npx electron-builder --mac dmg zip --arm64 --publish always '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}'; then echo "::warning::DMG packaging failed (hdiutil instability on runner). Retrying with ZIP only." - npx electron-builder --mac zip --arm64 --publish never '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}' + npx electron-builder --mac zip --arm64 --publish always '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}' fi - assets=() - while IFS= read -r file; do - assets+=("$file") - done < <(find release -maxdepth 1 -type f | sort) - if [ "${#assets[@]}" -eq 0 ]; then - echo "No release files found in ./release" - exit 1 - fi - upload_release_assets_with_retry "$GITHUB_REPOSITORY" "$GITHUB_REF_NAME" "${assets[@]}" - name: Inject minimumVersion into latest yml env: @@ -125,7 +81,6 @@ jobs: release-linux: runs-on: ubuntu-latest - needs: prepare-release steps: - name: Check out git repository @@ -159,23 +114,11 @@ jobs: npx tsc npx vite build - - name: Package and Upload Linux + - name: Package and Publish Linux env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - shell: bash run: | - set -euo pipefail - source .github/scripts/release-utils.sh - npx electron-builder --linux --publish never '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}' - assets=() - while IFS= read -r file; do - assets+=("$file") - done < <(find release -maxdepth 1 -type f | sort) - if [ "${#assets[@]}" -eq 0 ]; then - echo "No release files found in ./release" - exit 1 - fi - upload_release_assets_with_retry "$GITHUB_REPOSITORY" "$GITHUB_REF_NAME" "${assets[@]}" + npx electron-builder --linux --publish always '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}' - name: Inject minimumVersion into latest yml env: @@ -196,7 +139,6 @@ jobs: release: runs-on: windows-latest - needs: prepare-release steps: - name: Check out git repository @@ -225,23 +167,11 @@ jobs: npx tsc npx vite build - - name: Package and Upload Windows x64 + - name: Package and Publish env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - shell: bash run: | - set -euo pipefail - source .github/scripts/release-utils.sh - npx electron-builder --win nsis --x64 --publish never '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}' '--config.artifactName=${productName}-${version}-x64-Setup.${ext}' - assets=() - while IFS= read -r file; do - assets+=("$file") - done < <(find release -maxdepth 1 -type f | sort) - if [ "${#assets[@]}" -eq 0 ]; then - echo "No release files found in ./release" - exit 1 - fi - upload_release_assets_with_retry "$GITHUB_REPOSITORY" "$GITHUB_REF_NAME" "${assets[@]}" + npx electron-builder --win nsis --x64 --publish always '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}' '--config.artifactName=${productName}-${version}-x64-Setup.${ext}' - name: Inject minimumVersion into latest yml env: @@ -262,7 +192,6 @@ jobs: release-windows-arm64: runs-on: windows-latest - needs: prepare-release steps: - name: Check out git repository @@ -291,23 +220,11 @@ jobs: npx tsc npx vite build - - name: Package and Upload Windows arm64 + - name: Package and Publish Windows arm64 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - shell: bash run: | - set -euo pipefail - source .github/scripts/release-utils.sh - npx electron-builder --win nsis --arm64 --publish never '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}' '--config.publish.channel=latest-arm64' '--config.artifactName=${productName}-${version}-arm64-Setup.${ext}' - assets=() - while IFS= read -r file; do - assets+=("$file") - done < <(find release -maxdepth 1 -type f | sort) - if [ "${#assets[@]}" -eq 0 ]; then - echo "No release files found in ./release" - exit 1 - fi - upload_release_assets_with_retry "$GITHUB_REPOSITORY" "$GITHUB_REF_NAME" "${assets[@]}" + npx electron-builder --win nsis --arm64 --publish always '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}' '--config.publish.channel=latest-arm64' '--config.artifactName=${productName}-${version}-arm64-Setup.${ext}' - name: Inject minimumVersion into latest yml env: From 9c7ed1729a0b190a33551559ff114b6dfd5b56dc Mon Sep 17 00:00:00 2001 From: H3CoF6 <1707889225@qq.com> Date: Wed, 29 Apr 2026 02:44:01 +0800 Subject: [PATCH 06/12] chore: add win32 dll + readme --- electron/services/imageDownloadService.ts | 0 resources/image/README.md | 1 + resources/image/win32/x64/img_helper.dll | Bin 0 -> 22528 bytes 3 files changed, 1 insertion(+) create mode 100644 electron/services/imageDownloadService.ts create mode 100644 resources/image/README.md create mode 100644 resources/image/win32/x64/img_helper.dll diff --git a/electron/services/imageDownloadService.ts b/electron/services/imageDownloadService.ts new file mode 100644 index 0000000..e69de29 diff --git a/resources/image/README.md b/resources/image/README.md new file mode 100644 index 0000000..a964638 --- /dev/null +++ b/resources/image/README.md @@ -0,0 +1 @@ +> 目前只适配了x64 win32平台,其它平台同样原理,但是代码还没写( \ No newline at end of file diff --git a/resources/image/win32/x64/img_helper.dll b/resources/image/win32/x64/img_helper.dll new file mode 100644 index 0000000000000000000000000000000000000000..ad5112577c6ad7b12cee6658ad43462120034356 GIT binary patch literal 22528 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 Y1;z!**lm0)_pyq{fd2jZ|HK0S8yH`L6#xJL literal 0 HcmV?d00001 From 1f0b2613bfa7cfcccb6fd40ddefd9d7847680f8e Mon Sep 17 00:00:00 2001 From: H3CoF6 <1707889225@qq.com> Date: Wed, 29 Apr 2026 04:05:48 +0800 Subject: [PATCH 07/12] =?UTF-8?q?feat(image):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E4=B8=8B=E8=BD=BD=E5=A4=A7=E5=9B=BE=E9=80=89?= =?UTF-8?q?=E9=A1=B9=EF=BC=88win32=20x64=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: NineBird --- electron/main.ts | 20 +++ electron/preload.ts | 10 +- electron/services/config.ts | 4 +- electron/services/imageDownloadService.ts | 174 ++++++++++++++++++++++ src/pages/SettingsPage.tsx | 53 ++++++- src/services/config.ts | 12 +- 6 files changed, 269 insertions(+), 4 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index b57b76b..5f54f94 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -34,6 +34,7 @@ import { insightService } from './services/insightService' import { normalizeWeiboCookieInput, weiboService } from './services/social/weiboService' import { bizService } from './services/bizService' import { backupService } from './services/backupService' +import { imageDownloadService } from './services/imageDownloadService' // 配置自动更新 autoUpdater.autoDownload = false @@ -3954,6 +3955,20 @@ function registerIpcHandlers() { } }) + // 自动下载原图 + ipcMain.handle('image:startAutoDownload', async () => { + await imageDownloadService.startAutoDownload() + return { success: true } + }) + + ipcMain.handle('image:stopAutoDownload', async () => { + await imageDownloadService.stopAutoDownload() + return { success: true } + }) + + ipcMain.handle('image:getAutoDownloadStatus', async () => { + return await imageDownloadService.getStatus() + }) } // 主窗口引用 @@ -4081,6 +4096,9 @@ app.whenReady().then(async () => { // 注册 IPC 处理器 updateSplashProgress(28, '正在初始化...') registerIpcHandlers() + if (configService.get('autoDownloadHighRes')) { + imageDownloadService.startAutoDownload() + } chatService.addDbMonitorListener((type, json) => { messagePushService.handleDbMonitorChange(type, json) insightService.handleDbMonitorChange(type, json) @@ -4252,6 +4270,8 @@ const shutdownAppServices = async (): Promise => { }, 5000) forceExitTimer.unref() try { await cloudControlService.stop() } catch {} + // 停止自动下载服务 + try { await imageDownloadService.stopAutoDownload() } catch {} // 停止 chatService(内部会关闭 cursor 与 DB),避免退出阶段仍触发监控回调 try { chatService.close() } catch {} // 停止 HTTP 服务器,释放 TCP 端口占用,避免进程无法退出 diff --git a/electron/preload.ts b/electron/preload.ts index c7ba7c2..84ed45f 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -365,7 +365,10 @@ contextBridge.exposeInMainWorld('electronAPI', { }) => callback(payload) ipcRenderer.on('image:decryptProgress', listener) return () => ipcRenderer.removeListener('image:decryptProgress', listener) - } + }, + startAutoDownload: () => ipcRenderer.invoke('image:startAutoDownload'), + stopAutoDownload: () => ipcRenderer.invoke('image:stopAutoDownload'), + getAutoDownloadStatus: () => ipcRenderer.invoke('image:getAutoDownloadStatus') }, // 视频 @@ -374,6 +377,11 @@ contextBridge.exposeInMainWorld('electronAPI', { parseVideoMd5: (content: string) => ipcRenderer.invoke('video:parseVideoMd5', content) }, + process: { + platform: process.platform, + arch: process.arch + }, + // 数据分析 analytics: { getOverallStatistics: (force?: boolean) => ipcRenderer.invoke('analytics:getOverallStatistics', force), diff --git a/electron/services/config.ts b/electron/services/config.ts index 9e38931..27c216c 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -117,6 +117,7 @@ interface ConfigSchema { aiFootprintSystemPrompt: string /** 是否将 AI 见解调试日志输出到桌面 */ aiInsightDebugLogEnabled: boolean + autoDownloadHighRes: boolean } interface ConfigStoreLike> { @@ -294,7 +295,8 @@ export class ConfigService { aiInsightWeiboBindings: {}, aiFootprintEnabled: false, aiFootprintSystemPrompt: '', - aiInsightDebugLogEnabled: false + aiInsightDebugLogEnabled: false, + autoDownloadHighRes: false } const cwd = String(process.env.WEFLOW_CONFIG_CWD || process.env.WEFLOW_USER_DATA_PATH || '').trim() diff --git a/electron/services/imageDownloadService.ts b/electron/services/imageDownloadService.ts index e69de29..3d978f6 100644 --- a/electron/services/imageDownloadService.ts +++ b/electron/services/imageDownloadService.ts @@ -0,0 +1,174 @@ +import { app } from 'electron' +import { join } from 'path' +import { existsSync } from 'fs' +import { execFile } from 'child_process' +import { promisify } from 'util' +// import { ConfigService } from './config' + +const execFileAsync = promisify(execFile) + +export class ImageDownloadService { + private static instance: ImageDownloadService + private koffi: any = null + private lib: any = null + private initialized = false + + private initImgHelper: any = null + private uninstallImgHelper: any = null + private getImgHelperError: any = null + + private currentPid: number | null = null + private pollTimer: NodeJS.Timeout | null = null + private isHooked = false + + static getInstance(): ImageDownloadService { + if (!ImageDownloadService.instance) { + ImageDownloadService.instance = new ImageDownloadService() + } + return ImageDownloadService.instance + } + + private constructor() { + } + + private async ensureInitialized(): Promise { + if (this.initialized) return true + if (process.platform !== 'win32' || process.arch !== 'x64') return false + + try { + this.koffi = require('koffi') + const dllPath = this.getDllPath() + if (!existsSync(dllPath)) { + console.error(`[ImageDownloadService] dll not found: ${dllPath}`) + return false + } + + this.lib = this.koffi.load(dllPath) + this.initImgHelper = this.lib.func('bool InitImgHelper(uint32)') + this.uninstallImgHelper = this.lib.func('void UninstallImgHelper()') + this.getImgHelperError = this.lib.func('const char* GetImgHelperError()') + + this.initialized = true + return true + } catch (error) { + console.error('[ImageDownloadService] failed to initialize:', error) + return false + } + } + + private getDllPath(): string { + const isPackaged = app.isPackaged + const candidates: string[] = [] + + if (isPackaged) { + candidates.push(join(process.resourcesPath, 'resources', 'image', 'win32', 'x64', 'img_helper.dll')) + } else { + candidates.push(join(process.cwd(), 'resources', 'image', 'win32', 'x64', 'img_helper.dll')) + } + + for (const path of candidates) { + if (existsSync(path)) return path + } + return candidates[0] + } + + private async findMainWeChatPid(): Promise { + try { + const script = ` + Get-CimInstance Win32_Process -Filter "Name = 'Weixin.exe'" | + Select-Object ProcessId, CommandLine | + ConvertTo-Json -Compress + `; + + const { stdout } = await execFileAsync('powershell', ['-NoProfile', '-Command', script]) + if (!stdout || !stdout.trim()) return null + + let processes = JSON.parse(stdout.trim()) + if (!Array.isArray(processes)) processes = [processes] + + const target = processes + .filter((p: any) => p.CommandLine && p.CommandLine.toLowerCase().includes('weixin.exe')) + .sort((a: any, b: any) => a.CommandLine.length - b.CommandLine.length)[0] + + return target ? target.ProcessId : null; + } catch (e) { + return null + } + } + + async startAutoDownload() { + if (!await this.ensureInitialized()) return + + if (this.pollTimer) return + + this.pollTimer = setInterval(() => this.checkAndHook(), 30000) + // Initial check + await this.checkAndHook() + } + + async stopAutoDownload() { + if (this.pollTimer) { + clearInterval(this.pollTimer) + this.pollTimer = null + } + await this.unhook() + } + + private async checkAndHook() { + const pid = await this.findMainWeChatPid() + + if (!pid) { + if (this.isHooked) { + console.log('[ImageDownloadService] WeChat exited, unhooking') + await this.unhook() + } + return + } + + if (this.isHooked && this.currentPid === pid) { + return + } + + if (this.isHooked && this.currentPid !== pid) { + console.log('[ImageDownloadService] WeChat PID changed, re-hooking') + await this.unhook() + } + + console.log(`[ImageDownloadService] attempting to hook PID: ${pid}`) + try { + const success = this.initImgHelper(pid) + if (success) { + this.isHooked = true + this.currentPid = pid + console.log('[ImageDownloadService] hook successful') + } else { + const err = this.getImgHelperError() + console.error(`[ImageDownloadService] hook failed: ${err}`) + } + } catch (e) { + console.error('[ImageDownloadService] InitImgHelper call crashed:', e) + } + } + + private async unhook() { + if (this.isHooked && this.uninstallImgHelper) { + try { + this.uninstallImgHelper() + } catch (e) { + console.error('[ImageDownloadService] uninstall failed:', e) + } + } + this.isHooked = false + this.currentPid = null + } + + async getStatus() { + return { + isHooked: this.isHooked, + pid: this.currentPid, + supported: process.platform === 'win32' && process.arch === 'x64' + } + } +} + +export const imageDownloadService = ImageDownloadService.getInstance() diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 0d508ed..a5bf75b 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -32,6 +32,7 @@ type SettingsTab = | 'aiCommon' | 'insight' | 'aiFootprint' + | 'autoDownload' const tabs: { id: Exclude; label: string; icon: React.ElementType }[] = [ { id: 'appearance', label: '外观', icon: Palette }, @@ -39,6 +40,7 @@ const tabs: { id: Exclude; label: string { id: 'antiRevoke', label: '防撤回', icon: RotateCcw }, { id: 'database', label: '数据库连接', icon: Database }, { id: 'models', label: '模型管理', icon: Mic }, + { id: 'autoDownload', label: '自动下载', icon: Download }, { id: 'cache', label: '缓存', icon: HardDrive }, { id: 'api', label: 'API 服务', icon: Globe }, { id: 'analytics', label: '分析', icon: BarChart2 }, @@ -47,6 +49,13 @@ const tabs: { id: Exclude; label: string { id: 'about', label: '关于', icon: Info } ] +const filteredTabs = tabs.filter(tab => { + if (tab.id === 'autoDownload') { + return (window as any).electronAPI.process.platform === 'win32' && (window as any).electronAPI.process.arch === 'x64' + } + return true +}) + const aiTabs: Array<{ id: Extract; label: string }> = [ { id: 'aiCommon', label: '基础配置' }, { id: 'insight', label: 'AI 见解' }, @@ -149,6 +158,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const [imageKeyPercent, setImageKeyPercent] = useState(null) const [logEnabled, setLogEnabled] = useState(false) + const [autoDownloadHighRes, setAutoDownloadHighRes] = useState(false) const [whisperModelName, setWhisperModelName] = useState('base') const [whisperModelDir, setWhisperModelDir] = useState('') const [isWhisperDownloading, setIsWhisperDownloading] = useState(false) @@ -529,8 +539,10 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { setWordCloudExcludeWords(savedExcludeWords) setExcludeWordsInput(savedExcludeWords.join('\n')) + const savedAutoDownloadHighRes = await configService.getAutoDownloadHighRes() const savedAnalyticsConsent = await configService.getAnalyticsConsent() setAnalyticsConsent(savedAnalyticsConsent ?? false) + setAutoDownloadHighRes(savedAutoDownloadHighRes) @@ -4658,6 +4670,44 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
) + const renderAutoDownloadTab = () => ( +
+
+ + + 开启后,WeFlow 会通过远程 Hook 技术强制微信在接收图片时下载高清原图(而非默认的缩略图)。 +
+ 风险提示:Hook 涉及修改微信进程内存,虽不注入 DLL 但仍有被检测风险,请谨慎开启。 +
+
+ {autoDownloadHighRes ? '已开启' : '已关闭'} + +
+
+
+ ) + + const handleToggleAutoDownload = async () => { + const newVal = !autoDownloadHighRes + setAutoDownloadHighRes(newVal) + await configService.setAutoDownloadHighRes(newVal) + if (newVal) { + await (window as any).electronAPI.image.startAutoDownload() + } else { + await (window as any).electronAPI.image.stopAutoDownload() + } + showMessage(newVal ? '自动下载已开启' : '自动下载已关闭', true) + } + const renderUpdatesTab = () => { const downloadPercent = Math.max(0, Math.min(100, Number(downloadProgress?.percent || 0))) const channelCards: { id: configService.UpdateChannel; title: string; desc: string }[] = [ @@ -4792,7 +4842,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
- {tabs.flatMap((tab) => { + {filteredTabs.flatMap((tab) => { const row: React.ReactNode[] = [ + +
+
+ + + {autoDownloadHighRes ? '已开启' : '已关闭'} + +
+
+
+ +
+
+ 已选 {selectedCount} 个目标会话 + (若不选则默认对所有聊天生效) +
+
-
- {!autoDownloadHighRes ? ( -
服务未启动
- ) : !autoDownloadStatus ? ( -
正在检测状态...
- ) : !autoDownloadStatus.supported ? ( -
⚠️ 当前系统架构不支持此功能(仅支持 Win32 x64)
- ) : autoDownloadStatus.isHooked ? ( -
- ✓ 运行中 - 已成功挂载到微信进程 (PID: {autoDownloadStatus.pid}) -
+ +
+
+ 会话({filteredSessions.length}) + 选择 +
+ {filteredSessions.length === 0 ? ( +
{autoDownloadSearchKeyword ? '没有匹配的会话' : '暂无会话'}
) : ( -
- ⏳ 等待中 - 未检测到微信主进程 (Weixin.exe) 运行,请启动微信 -
+ filteredSessions.map((session) => ( +
toggleSelection(session.username)} + > +
+ +
+ {session.displayName || session.username} + {session.username} +
+
+
+ + + +
+
+ )) )}
-
-
-
- -

风险提示

-
-
-
-
- - 此功能涉及hook修改微信进程内存 -
-
- - 虽然当前方案不直接注入 DLL,但仍存在被微信安全机制检测的风险 -
-
- - 建议先少量测试使用,确认有无被检测的风险 -
+ {/* 风险提示 */} +
+
+ +

风险警告

+
+
+ 此功能通过内存 Hook 修改微信行为,具有一定的风险。请尽量仅在白名单模式下针对必要会话开启。
-
- ) - const handleToggleAutoDownload = async () => { + ) + } + const handleToggleAutoDownload = async (whitelist?: string[] | string) => { const newVal = !autoDownloadHighRes setAutoDownloadHighRes(newVal) try { if (newVal) { - const result = await (window as any).electronAPI.image.startAutoDownload() + let currentWhitelist: string[] | string = whitelist || Array.from(autoDownloadSelectedIds) + if (Array.isArray(currentWhitelist)) { + currentWhitelist = currentWhitelist.length > 0 ? (currentWhitelist.join('\0') + '\0\0') : '' + } + const result = await (window as any).electronAPI.image.startAutoDownload(currentWhitelist) if (result && !result.success) { // 如果底层明确返回了失败 throw new Error(result.error || '启动自动下载服务失败') diff --git a/src/services/config.ts b/src/services/config.ts index c06c6f4..0d6588e 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -120,7 +120,8 @@ export const CONFIG_KEYS = { AI_FOOTPRINT_ENABLED: 'aiFootprintEnabled', AI_FOOTPRINT_SYSTEM_PROMPT: 'aiFootprintSystemPrompt', AI_INSIGHT_DEBUG_LOG_ENABLED: 'aiInsightDebugLogEnabled', - AUTO_DOWNLOAD_HIGH_RES: 'autoDownloadHighRes' + AUTO_DOWNLOAD_HIGH_RES: 'autoDownloadHighRes', + AUTO_DOWNLOAD_WHITELIST: 'autoDownloadWhitelist' } as const export interface WxidConfig { @@ -2157,3 +2158,13 @@ export async function setAutoDownloadHighRes(enabled: boolean): Promise { await config.set(CONFIG_KEYS.AUTO_DOWNLOAD_HIGH_RES, enabled) } +export async function getAutoDownloadWhitelist(): Promise { + const value = await config.get(CONFIG_KEYS.AUTO_DOWNLOAD_WHITELIST) + return Array.isArray(value) ? value : [] +} + +export async function setAutoDownloadWhitelist(list: string[]): Promise { + const normalized = Array.from(new Set((list || []).map(item => String(item || '').trim()).filter(Boolean))) + await config.set(CONFIG_KEYS.AUTO_DOWNLOAD_WHITELIST, normalized) +} + From bdf285062fb5ae5798ac75517a3e4296a78bb363 Mon Sep 17 00:00:00 2001 From: H3CoF6 <1707889225@qq.com> Date: Wed, 29 Apr 2026 08:25:45 +0800 Subject: [PATCH 12/12] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E9=80=89=E6=8B=A9=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/SettingsPage.tsx | 79 ++++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 09bca0a..fb53979 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -1055,11 +1055,11 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { } useEffect(() => { - if (activeTab !== 'antiRevoke' && activeTab !== 'insight') return + if (activeTab !== 'antiRevoke' && activeTab !== 'insight' && activeTab !== 'autoDownload') return let canceled = false ;(async () => { try { - if (activeTab === 'antiRevoke') { + if (activeTab === 'antiRevoke' || activeTab === 'autoDownload') { await ensureAntiRevokeSessionsLoaded() } else { await ensureChatSessionsLoaded() @@ -4701,12 +4701,13 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { ) const renderAutoDownloadTab = () => { - const sortedSessions = [...chatSessions].sort((a, b) => (b.sortTimestamp || 0) - (a.sortTimestamp || 0)) + const sortedSessions = [...antiRevokeSessions].sort((a, b) => (b.sortTimestamp || 0) - (a.sortTimestamp || 0)) const keyword = autoDownloadSearchKeyword.trim().toLowerCase() const filteredSessions = sortedSessions.filter((session) => { if (!keyword) return true - return (session.displayName || '').toLowerCase().includes(keyword) || - session.username.toLowerCase().includes(keyword) + const displayName = String(session.displayName || '').toLowerCase() + const username = String(session.username || '').toLowerCase() + return displayName.includes(keyword) || username.includes(keyword) }) const filteredSessionIds = filteredSessions.map((session) => session.username) const selectedCount = autoDownloadSelectedIds.size @@ -4718,7 +4719,6 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const whitelistArr = Array.from(ids) configService.setAutoDownloadWhitelist(whitelistArr) if (autoDownloadHighRes) { - // 转换为 wxid\0wxid\0wxid\0\0 格式 const whitelistStr = whitelistArr.length > 0 ? (whitelistArr.join('\0') + '\0\0') : ''; (window as any).electronAPI.image.startAutoDownload(whitelistStr) } @@ -4747,10 +4747,10 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { return (
- {/* 顶部 Hero 区域 */} + {/* 顶部 Hero 区域保持不变 */}
- 测试功能 (Beta) + 测试功能 (Test)

自动下载原图

强制微信在接收图片时下载高清原图。建议仅在必要会话中开启以节省流量和空间。

@@ -4758,8 +4758,8 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
服务状态 - {isHooked ? '正在监控' : autoDownloadHighRes ? '等待连接' : '未启用'} - + {isHooked ? '正在监控' : autoDownloadHighRes ? '等待连接' : '未启用'} +
已选会话 @@ -4782,14 +4782,14 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
-
@@ -4815,35 +4815,44 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
会话({filteredSessions.length}) - 选择 + 状态
{filteredSessions.length === 0 ? (
{autoDownloadSearchKeyword ? '没有匹配的会话' : '暂无会话'}
) : ( - filteredSessions.map((session) => ( -
toggleSelection(session.username)} - > -
- -
- {session.displayName || session.username} - {session.username} + filteredSessions.map((session) => { + const isSelected = autoDownloadSelectedIds.has(session.username) + return ( +
+ +
+ +