mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-04-23 01:39:37 +08:00
Compare commits
5 Commits
5adb1122d4
...
c65b5238cc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c65b5238cc | ||
|
|
e3d72bb5da | ||
|
|
2306b3dfd7 | ||
|
|
9b8595925d | ||
|
|
fcca5773ca |
66
.github/workflows/release.yml
vendored
66
.github/workflows/release.yml
vendored
@@ -143,8 +143,61 @@ jobs:
|
||||
if-no-files-found: warn
|
||||
retention-days: 1 # 只保留1天,Release后会上传到GitHub Releases
|
||||
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
echo "node-linker=hoisted" >> .npmrc
|
||||
pnpm install
|
||||
|
||||
- name: Build Electron app for Linux
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
APTABASE_APP_KEY: ${{ secrets.APTABASE_APP_KEY }}
|
||||
run: pnpm build:linux
|
||||
|
||||
- name: Upload Linux artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ChatLab-linux
|
||||
path: |
|
||||
dist/*.AppImage
|
||||
dist/*.deb
|
||||
dist/*.tar.gz
|
||||
dist/*.yml
|
||||
dist/*.json
|
||||
if-no-files-found: warn
|
||||
retention-days: 1
|
||||
|
||||
release:
|
||||
needs: [build-mac, build-win]
|
||||
needs: [build-mac, build-win, build-linux]
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
|
||||
|
||||
@@ -170,6 +223,12 @@ jobs:
|
||||
name: ChatLab-win
|
||||
path: dist
|
||||
|
||||
- name: Download Linux artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ChatLab-linux
|
||||
path: dist
|
||||
|
||||
- name: List files
|
||||
run: ls -la dist/
|
||||
|
||||
@@ -216,6 +275,8 @@ jobs:
|
||||
echo "| Mac (Apple Silicon) | [ChatLab-${VERSION_NUMBER}-arm64.dmg](https://github.com/hellodigua/ChatLab/releases/download/v${VERSION_NUMBER}/ChatLab-${VERSION_NUMBER}-arm64.dmg) |"
|
||||
echo "| Mac (Intel) | [ChatLab-${VERSION_NUMBER}-x64.dmg](https://github.com/hellodigua/ChatLab/releases/download/v${VERSION_NUMBER}/ChatLab-${VERSION_NUMBER}-x64.dmg) |"
|
||||
echo "| Windows | [ChatLab-${VERSION_NUMBER}-setup.exe](https://github.com/hellodigua/ChatLab/releases/download/v${VERSION_NUMBER}/ChatLab-${VERSION_NUMBER}-setup.exe) |"
|
||||
echo "| Linux (AppImage) | [ChatLab-${VERSION_NUMBER}.AppImage](https://github.com/hellodigua/ChatLab/releases/download/v${VERSION_NUMBER}/ChatLab-${VERSION_NUMBER}.AppImage) |"
|
||||
echo "| Linux (deb) | [chatlab_${VERSION_NUMBER}_amd64.deb](https://github.com/hellodigua/ChatLab/releases/download/v${VERSION_NUMBER}/chatlab_${VERSION_NUMBER}_amd64.deb) |"
|
||||
} > release_notes.md
|
||||
|
||||
echo "Generated release notes:"
|
||||
@@ -231,6 +292,9 @@ jobs:
|
||||
dist/*.exe
|
||||
dist/*.dmg
|
||||
dist/*.zip
|
||||
dist/*.AppImage
|
||||
dist/*.deb
|
||||
dist/*.tar.gz
|
||||
dist/*.yml
|
||||
dist/*.blockmap
|
||||
env:
|
||||
|
||||
14
README.md
14
README.md
@@ -4,7 +4,7 @@
|
||||
|
||||
Rediscover your social memories with private, AI-powered analysis.
|
||||
|
||||
English | [简体中文](./README.zh-CN.md) | [繁體中文](./README.zh-TW.md) | [日本語](./README.ja-JP.md)
|
||||
English | [简体中文](./docs/README.zh-CN.md) | [繁體中文](./docs/README.zh-TW.md) | [日本語](./docs/README.ja-JP.md)
|
||||
|
||||
[Official Website](https://chatlab.fun/) · [Download](https://chatlab.fun/?type=download) · [Documentation](https://chatlab.fun/usage/) · [Roadmap](https://chatlabfun.featurebase.app/roadmap) · [Issue Submission](https://github.com/hellodigua/ChatLab/issues)
|
||||
|
||||
@@ -92,7 +92,11 @@ electron-fix start
|
||||
|
||||
```
|
||||
|
||||
## Contributing
|
||||
## Privacy Policy & User Agreement
|
||||
|
||||
Before using this software, please read the [Privacy Policy & User Agreement](./src/assets/docs/agreement_en.md).
|
||||
|
||||
## Community
|
||||
|
||||
Please follow these principles before submitting a Pull Request:
|
||||
|
||||
@@ -100,9 +104,11 @@ Please follow these principles before submitting a Pull Request:
|
||||
- For new features, please submit an Issue for discussion first; **PRs submitted without prior discussion will be closed**.
|
||||
- Keep one PR focused on one task; if changes are extensive, consider splitting them into multiple independent PRs.
|
||||
|
||||
## Privacy Policy & User Agreement
|
||||
Thanks to all contributors:
|
||||
|
||||
Before using this software, please read the [Privacy Policy & User Agreement](./src/assets/docs/agreement_en.md).
|
||||
<a href="https://github.com/hellodigua/ChatLab/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=hellodigua/ChatLab" />
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="./public/images/chatlab.svg" alt="ChatLab" title="ChatLab" width="300" />
|
||||
<img src="../public/images/chatlab.svg" alt="ChatLab" title="ChatLab" width="300" />
|
||||
|
||||
AI Agent でチャット履歴をローカル分析し、あなたのソーシャルな記憶を掘り起こす
|
||||
|
||||
[English](./README.md) | [简体中文](./README.zh-CN.md) | [繁體中文](./README.zh-TW.md) | 日本語
|
||||
[English](../README.md) | [简体中文](./README.zh-CN.md) | [繁體中文](./README.zh-TW.md) | 日本語
|
||||
|
||||
[公式サイト](https://chatlab.fun/ja/) · [ダウンロードガイド](https://chatlab.fun/ja/?type=download) · [ドキュメント](https://chatlab.fun/ja/usage/) · [Roadmap](https://chatlabfun.featurebase.app/roadmap) · [Issue](https://github.com/hellodigua/ChatLab/issues)
|
||||
|
||||
@@ -33,7 +33,7 @@ ChatLab は、チャット履歴を深く理解するためのローカル完結
|
||||
|
||||
その他の画面は公式サイト [chatlab.fun](https://chatlab.fun/ja/) を参照してください。
|
||||
|
||||

|
||||

|
||||
|
||||
## システムアーキテクチャ
|
||||
|
||||
@@ -82,7 +82,11 @@ npm install electron-fix -g
|
||||
electron-fix start
|
||||
```
|
||||
|
||||
## コントリビューション
|
||||
## プライバシーポリシーと利用規約
|
||||
|
||||
利用前に [プライバシーポリシーと利用規約](../src/assets/docs/agreement_ja.md) を確認してください。
|
||||
|
||||
## コミュニティ
|
||||
|
||||
Pull Request を送る前に、次の方針を確認してください。
|
||||
|
||||
@@ -90,9 +94,11 @@ Pull Request を送る前に、次の方針を確認してください。
|
||||
- 新機能は先に Issue で相談してください。**事前の議論がない PR はクローズされます**
|
||||
- 1 つの PR は 1 つの目的に絞り、変更が大きい場合は分割を検討してください
|
||||
|
||||
## プライバシーポリシーと利用規約
|
||||
ChatLab に貢献してくださったすべての方に感謝します!
|
||||
|
||||
利用前に [プライバシーポリシーと利用規約](./src/assets/docs/agreement_ja.md) を確認してください。
|
||||
<a href="https://github.com/hellodigua/ChatLab/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=hellodigua/ChatLab" />
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="./public/images/chatlab.svg" alt="ChatLab" title="ChatLab" width="300" />
|
||||
<img src="../public/images/chatlab.svg" alt="ChatLab" title="ChatLab" width="300" />
|
||||
|
||||
本地化的聊天记录分析工具,通过 AI Agent 回顾你的社交记忆
|
||||
|
||||
[English](./README.md) | 简体中文 | [繁體中文](./README.zh-TW.md) | [日本語](./README.ja-JP.md)
|
||||
[English](../README.md) | 简体中文 | [繁體中文](./README.zh-TW.md) | [日本語](./README.ja-JP.md)
|
||||
|
||||
[官网](https://chatlab.fun/cn/) · [下载指南](https://chatlab.fun/cn/?type=download) · [项目文档](https://chatlab.fun/cn/usage/) · [路线图](https://chatlabfun.featurebase.app/roadmap) · [问题提交](https://github.com/hellodigua/ChatLab/issues)
|
||||
|
||||
@@ -33,7 +33,7 @@ ChatLab 是一个专注于社交记录分析的本地化应用。通过 AI Agent
|
||||
|
||||
预览更多请前往官网 [chatlab.fun](https://chatlab.fun/cn/)
|
||||
|
||||

|
||||

|
||||
|
||||
## 系统架构
|
||||
|
||||
@@ -82,7 +82,11 @@ npm install electron-fix -g
|
||||
electron-fix start
|
||||
```
|
||||
|
||||
## 贡献指南
|
||||
## 隐私政策与用户协议
|
||||
|
||||
使用本软件前,请阅读 [隐私政策与用户协议](../src/assets/docs/agreement_zh.md)
|
||||
|
||||
## 社区
|
||||
|
||||
提交 Pull Request 前请遵循以下原则:
|
||||
|
||||
@@ -90,9 +94,11 @@ electron-fix start
|
||||
- 对于新功能,请先提交 Issue 进行讨论,**未经讨论直接提交的 PR 会被关闭**
|
||||
- 一个 PR 尽量只做一件事,若改动较大,请考虑拆分为多个独立的 PR
|
||||
|
||||
## 隐私政策与用户协议
|
||||
感谢所有为 ChatLab 做出贡献的人!
|
||||
|
||||
使用本软件前,请阅读 [隐私政策与用户协议](./src/assets/docs/agreement_zh.md)
|
||||
<a href="https://github.com/hellodigua/ChatLab/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=hellodigua/ChatLab" />
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="./public/images/chatlab.svg" alt="ChatLab" title="ChatLab" width="300" />
|
||||
<img src="../public/images/chatlab.svg" alt="ChatLab" title="ChatLab" width="300" />
|
||||
|
||||
在本機分析聊天記錄,透過 AI Agent 重新看見你的社交記憶
|
||||
|
||||
[English](./README.md) | [简体中文](./README.zh-CN.md) | 繁體中文 | [日本語](./README.ja-JP.md)
|
||||
[English](../README.md) | [简体中文](./README.zh-CN.md) | 繁體中文 | [日本語](./README.ja-JP.md)
|
||||
|
||||
[官網](https://chatlab.fun/tw/) · [下載指南](https://chatlab.fun/tw/?type=download) · [使用文件](https://chatlab.fun/tw/usage/) · [Roadmap](https://chatlabfun.featurebase.app/roadmap) · [問題回報](https://github.com/hellodigua/ChatLab/issues)
|
||||
|
||||
@@ -33,7 +33,7 @@ ChatLab 是一款專注於社交記錄分析的本機應用。結合 AI Agent
|
||||
|
||||
更多畫面請前往官網 [chatlab.fun](https://chatlab.fun/tw/)
|
||||
|
||||

|
||||

|
||||
|
||||
## 系統架構
|
||||
|
||||
@@ -82,7 +82,11 @@ npm install electron-fix -g
|
||||
electron-fix start
|
||||
```
|
||||
|
||||
## 貢獻指南
|
||||
## 隱私權政策與使用者協議
|
||||
|
||||
使用本軟體前,請先閱讀 [隱私權政策與使用者協議](../src/assets/docs/agreement_zh_tw.md)
|
||||
|
||||
## 社群
|
||||
|
||||
提交 Pull Request 前請遵循以下原則:
|
||||
|
||||
@@ -90,9 +94,11 @@ electron-fix start
|
||||
- 新功能請先提交 Issue 討論,**未經討論直接提交的 PR 會被關閉**
|
||||
- 一個 PR 盡量只處理一件事;若改動較大,建議拆分成多個獨立 PR
|
||||
|
||||
## 隱私權政策與使用者協議
|
||||
感謝所有為 ChatLab 做出貢獻的人!
|
||||
|
||||
使用本軟體前,請先閱讀 [隱私權政策與使用者協議](./src/assets/docs/agreement_zh_tw.md)
|
||||
<a href="https://github.com/hellodigua/ChatLab/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=hellodigua/ChatLab" />
|
||||
</a>
|
||||
|
||||
## License
|
||||
|
||||
@@ -70,7 +70,6 @@ linux:
|
||||
target:
|
||||
- AppImage
|
||||
- deb
|
||||
- rpm
|
||||
- tar.gz
|
||||
category: Utility
|
||||
|
||||
|
||||
@@ -33,11 +33,27 @@ export function createTool(context: ToolContext): AgentTool<typeof schema> {
|
||||
params.sender_id
|
||||
)
|
||||
|
||||
const contextBefore = context.searchContextBefore ?? 2
|
||||
const contextAfter = context.searchContextAfter ?? 2
|
||||
let finalMessages = result.messages
|
||||
|
||||
if ((contextBefore > 0 || contextAfter > 0) && result.messages.length > 0) {
|
||||
const hitIds = result.messages.map((m) => m.id).filter((id): id is number => id != null)
|
||||
if (hitIds.length > 0) {
|
||||
finalMessages = await workerManager.getSearchMessageContext(
|
||||
sessionId,
|
||||
hitIds,
|
||||
contextBefore,
|
||||
contextAfter
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const data = {
|
||||
total: result.total,
|
||||
returned: result.messages.length,
|
||||
returned: finalMessages.length,
|
||||
timeRange: formatTimeRange(effectiveTimeFilter, locale),
|
||||
rawMessages: result.messages,
|
||||
rawMessages: finalMessages,
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -34,11 +34,27 @@ export function createTool(context: ToolContext): AgentTool<typeof schema> {
|
||||
params.sender_id
|
||||
)
|
||||
|
||||
const contextBefore = context.searchContextBefore ?? 2
|
||||
const contextAfter = context.searchContextAfter ?? 2
|
||||
let finalMessages = result.messages
|
||||
|
||||
if ((contextBefore > 0 || contextAfter > 0) && result.messages.length > 0) {
|
||||
const hitIds = result.messages.map((m) => m.id).filter((id): id is number => id != null)
|
||||
if (hitIds.length > 0) {
|
||||
finalMessages = await workerManager.getSearchMessageContext(
|
||||
sessionId,
|
||||
hitIds,
|
||||
contextBefore,
|
||||
contextAfter
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const data = {
|
||||
total: result.total,
|
||||
returned: result.messages.length,
|
||||
returned: finalMessages.length,
|
||||
timeRange: formatTimeRange(effectiveTimeFilter, locale),
|
||||
rawMessages: result.messages,
|
||||
rawMessages: finalMessages,
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -53,4 +53,8 @@ export interface ToolContext {
|
||||
locale?: string
|
||||
/** 聊天记录预处理配置(全局) */
|
||||
preprocessConfig?: PreprocessConfig
|
||||
/** 搜索结果上下文:向前取多少条(默认 3) */
|
||||
searchContextBefore?: number
|
||||
/** 搜索结果上下文:向后取多少条(默认 3) */
|
||||
searchContextAfter?: number
|
||||
}
|
||||
|
||||
@@ -350,7 +350,9 @@ export function deleteSession(sessionId: string): boolean {
|
||||
if (fs.existsSync(shmPath)) {
|
||||
fs.unlinkSync(shmPath)
|
||||
}
|
||||
deleteSessionCache(sessionId, getCacheDir())
|
||||
const cacheDir = getCacheDir()
|
||||
deleteSessionCache(sessionId, cacheDir)
|
||||
deleteSessionCache(sessionId, path.join(cacheDir, 'query'))
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('[Database] Failed to delete session:', error)
|
||||
|
||||
@@ -636,7 +636,9 @@ export function registerChatHandlers(ctx: IpcContext): void {
|
||||
*/
|
||||
ipcMain.handle('chat:updateMemberAliases', async (_, sessionId: string, memberId: number, aliases: string[]) => {
|
||||
try {
|
||||
return await worker.updateMemberAliases(sessionId, memberId, aliases)
|
||||
const result = await worker.updateMemberAliases(sessionId, memberId, aliases)
|
||||
if (result) worker.invalidateAnalysisCache(sessionId).catch(() => {})
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Failed to update member alias:', error)
|
||||
return false
|
||||
@@ -651,7 +653,9 @@ export function registerChatHandlers(ctx: IpcContext): void {
|
||||
// 先关闭数据库连接
|
||||
await worker.closeDatabase(sessionId)
|
||||
// 执行删除
|
||||
return await worker.deleteMember(sessionId, memberId)
|
||||
const result = await worker.deleteMember(sessionId, memberId)
|
||||
if (result) worker.invalidateAnalysisCache(sessionId).catch(() => {})
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Failed to delete member:', error)
|
||||
return false
|
||||
@@ -1029,6 +1033,8 @@ export function registerChatHandlers(ctx: IpcContext): void {
|
||||
} catch (e) {
|
||||
console.error('[IpcMain] Failed to incrementally generate session index:', e)
|
||||
}
|
||||
// 数据变更后清除分析缓存
|
||||
worker.invalidateAnalysisCache(sessionId).catch(() => {})
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
* 4. 返回结果
|
||||
*/
|
||||
|
||||
import * as path from 'path'
|
||||
import { parentPort, workerData } from 'worker_threads'
|
||||
import { initDbDir, closeDatabase, closeAllDatabases } from './core'
|
||||
import { initDbDir, closeDatabase, closeAllDatabases, getCacheDir } from './core'
|
||||
import { getCache, setCache, deleteSessionCache } from '../database/sessionCache'
|
||||
import {
|
||||
getAvailableYears,
|
||||
getMemberActivity,
|
||||
@@ -34,6 +36,7 @@ import {
|
||||
searchMessages,
|
||||
deepSearchMessages,
|
||||
getMessageContext,
|
||||
getSearchMessageContext,
|
||||
getRecentMessages,
|
||||
getAllRecentMessages,
|
||||
getConversationBetween,
|
||||
@@ -72,6 +75,57 @@ import { streamImport, streamParseFileInfo, analyzeIncrementalImport, incrementa
|
||||
// 初始化数据库目录
|
||||
initDbDir(workerData.dbDir, workerData.cacheDir)
|
||||
|
||||
// ==================== 分析结果缓存 ====================
|
||||
|
||||
const ANALYSIS_CACHE_PREFIX = 'analysis:'
|
||||
|
||||
function getQueryCacheDir(): string {
|
||||
const cacheDir = getCacheDir()
|
||||
return cacheDir ? path.join(cacheDir, 'query') : ''
|
||||
}
|
||||
|
||||
const CACHEABLE_QUERIES = new Set([
|
||||
'getAvailableYears',
|
||||
'getMemberActivity',
|
||||
'getHourlyActivity',
|
||||
'getDailyActivity',
|
||||
'getWeekdayActivity',
|
||||
'getMonthlyActivity',
|
||||
'getYearlyActivity',
|
||||
'getMessageLengthDistribution',
|
||||
'getMessageTypeDistribution',
|
||||
'getTimeRange',
|
||||
'getCatchphraseAnalysis',
|
||||
'getMentionAnalysis',
|
||||
'getMentionGraph',
|
||||
'getLaughAnalysis',
|
||||
'getClusterGraph',
|
||||
'getWordFrequency',
|
||||
])
|
||||
|
||||
function buildAnalysisCacheKey(type: string, payload: any): string {
|
||||
const parts = [ANALYSIS_CACHE_PREFIX + type]
|
||||
// 标准 filter 对象(大多数分析查询)
|
||||
const filter = payload.filter || payload.timeFilter
|
||||
if (filter) {
|
||||
if (filter.startTs !== undefined) parts.push(`s${filter.startTs}`)
|
||||
if (filter.endTs !== undefined) parts.push(`e${filter.endTs}`)
|
||||
if (filter.memberId !== undefined && filter.memberId !== null) {
|
||||
parts.push(`m${filter.memberId}`)
|
||||
}
|
||||
}
|
||||
// 顶层 memberId(如 getWordFrequency 直接传 memberId)
|
||||
if (payload.memberId !== undefined && payload.memberId !== null) parts.push(`m${payload.memberId}`)
|
||||
if (payload.keywords) parts.push(`k${JSON.stringify(payload.keywords)}`)
|
||||
if (payload.options) parts.push(`o${JSON.stringify(payload.options)}`)
|
||||
// getWordFrequency 特有参数
|
||||
if (payload.locale) parts.push(`l${payload.locale}`)
|
||||
if (payload.topN) parts.push(`n${payload.topN}`)
|
||||
if (payload.minLength) parts.push(`ml${payload.minLength}`)
|
||||
if (payload.posTags) parts.push(`pt${JSON.stringify(payload.posTags)}`)
|
||||
return parts.join(':')
|
||||
}
|
||||
|
||||
// ==================== 消息处理 ====================
|
||||
|
||||
interface WorkerMessage {
|
||||
@@ -124,6 +178,8 @@ const syncHandlers: Record<string, (payload: any) => any> = {
|
||||
// AI 查询
|
||||
searchMessages: (p) => searchMessages(p.sessionId, p.keywords, p.filter, p.limit, p.offset, p.senderId),
|
||||
getMessageContext: (p) => getMessageContext(p.sessionId, p.messageIds, p.contextSize),
|
||||
getSearchMessageContext: (p) =>
|
||||
getSearchMessageContext(p.sessionId, p.messageIds, p.contextBefore, p.contextAfter),
|
||||
getRecentMessages: (p) => getRecentMessages(p.sessionId, p.filter, p.limit),
|
||||
getAllRecentMessages: (p) => getAllRecentMessages(p.sessionId, p.filter, p.limit),
|
||||
getConversationBetween: (p) => getConversationBetween(p.sessionId, p.memberId1, p.memberId2, p.filter, p.limit),
|
||||
@@ -164,6 +220,15 @@ const syncHandlers: Record<string, (payload: any) => any> = {
|
||||
|
||||
// 深度搜索(LIKE 子串匹配)
|
||||
deepSearchMessages: (p) => deepSearchMessages(p.sessionId, p.keywords, p.filter, p.limit, p.offset, p.senderId),
|
||||
|
||||
// 缓存管理
|
||||
invalidateAnalysisCache: (p) => {
|
||||
const queryCacheDir = getQueryCacheDir()
|
||||
if (queryCacheDir && p.sessionId) {
|
||||
deleteSessionCache(p.sessionId, queryCacheDir)
|
||||
}
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// 异步消息处理器(流式操作)
|
||||
@@ -198,6 +263,21 @@ parentPort?.on('message', async (message: WorkerMessage) => {
|
||||
throw new Error(`Unknown message type: ${type}`)
|
||||
}
|
||||
|
||||
// 可缓存查询:先查缓存,miss 后执行并写回
|
||||
const queryCacheDir = getQueryCacheDir()
|
||||
if (queryCacheDir && CACHEABLE_QUERIES.has(type) && payload.sessionId) {
|
||||
const cacheKey = buildAnalysisCacheKey(type, payload)
|
||||
const cached = getCache(payload.sessionId, cacheKey, queryCacheDir)
|
||||
if (cached !== null) {
|
||||
parentPort?.postMessage({ id, success: true, result: cached })
|
||||
return
|
||||
}
|
||||
const result = syncHandler(payload)
|
||||
setCache(payload.sessionId, cacheKey, result, queryCacheDir)
|
||||
parentPort?.postMessage({ id, success: true, result })
|
||||
return
|
||||
}
|
||||
|
||||
const result = syncHandler(payload)
|
||||
parentPort?.postMessage({ id, success: true, result })
|
||||
} catch (error) {
|
||||
|
||||
@@ -46,6 +46,7 @@ export {
|
||||
searchMessages,
|
||||
deepSearchMessages,
|
||||
getMessageContext,
|
||||
getSearchMessageContext,
|
||||
getRecentMessages,
|
||||
getAllRecentMessages,
|
||||
getConversationBetween,
|
||||
|
||||
@@ -526,6 +526,115 @@ export function getMessageContext(
|
||||
return rows.map(sanitizeMessageRow)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取搜索结果的上下文消息(会话感知 + 区间合并去重)
|
||||
* 用于 search_messages / deep_search_messages 自动扩展上下文。
|
||||
* 当存在会话索引时,上下文不跨会话边界;否则按 message.id 顺序取前后 N 条。
|
||||
*
|
||||
* @param sessionId 数据库会话 ID
|
||||
* @param messageIds 搜索命中的消息 ID 列表
|
||||
* @param contextBefore 每条命中消息向前取多少条上下文
|
||||
* @param contextAfter 每条命中消息向后取多少条上下文
|
||||
*/
|
||||
export function getSearchMessageContext(
|
||||
sessionId: string,
|
||||
messageIds: number[],
|
||||
contextBefore: number = 2,
|
||||
contextAfter: number = 2
|
||||
): MessageResult[] {
|
||||
ensureAvatarColumn(sessionId)
|
||||
const db = openDatabase(sessionId)
|
||||
if (!db) return []
|
||||
if (messageIds.length === 0) return []
|
||||
|
||||
const contextIds = new Set<number>()
|
||||
|
||||
const hasSessionData =
|
||||
(db.prepare('SELECT 1 FROM message_context LIMIT 1').get() as { 1: number } | undefined) !== undefined
|
||||
|
||||
for (const messageId of messageIds) {
|
||||
contextIds.add(messageId)
|
||||
|
||||
if (hasSessionData) {
|
||||
const sessionRow = db
|
||||
.prepare('SELECT session_id FROM message_context WHERE message_id = ?')
|
||||
.get(messageId) as { session_id: number } | undefined
|
||||
|
||||
if (sessionRow) {
|
||||
if (contextBefore > 0) {
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT mc.message_id as id
|
||||
FROM message_context mc
|
||||
WHERE mc.session_id = ? AND mc.message_id < ?
|
||||
ORDER BY mc.message_id DESC
|
||||
LIMIT ?`
|
||||
)
|
||||
.all(sessionRow.session_id, messageId, contextBefore) as { id: number }[]
|
||||
rows.forEach((r) => contextIds.add(r.id))
|
||||
}
|
||||
if (contextAfter > 0) {
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT mc.message_id as id
|
||||
FROM message_context mc
|
||||
WHERE mc.session_id = ? AND mc.message_id > ?
|
||||
ORDER BY mc.message_id ASC
|
||||
LIMIT ?`
|
||||
)
|
||||
.all(sessionRow.session_id, messageId, contextAfter) as { id: number }[]
|
||||
rows.forEach((r) => contextIds.add(r.id))
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: no session data or message not indexed — use simple id-based context
|
||||
if (contextBefore > 0) {
|
||||
const rows = db
|
||||
.prepare('SELECT id FROM message WHERE id < ? ORDER BY id DESC LIMIT ?')
|
||||
.all(messageId, contextBefore) as { id: number }[]
|
||||
rows.forEach((r) => contextIds.add(r.id))
|
||||
}
|
||||
if (contextAfter > 0) {
|
||||
const rows = db
|
||||
.prepare('SELECT id FROM message WHERE id > ? ORDER BY id ASC LIMIT ?')
|
||||
.all(messageId, contextAfter) as { id: number }[]
|
||||
rows.forEach((r) => contextIds.add(r.id))
|
||||
}
|
||||
}
|
||||
|
||||
if (contextIds.size === 0) return []
|
||||
|
||||
const idList = Array.from(contextIds)
|
||||
const placeholders = idList.map(() => '?').join(', ')
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
msg.id,
|
||||
m.id as senderId,
|
||||
COALESCE(m.group_nickname, m.account_name, m.platform_id) as senderName,
|
||||
m.platform_id as senderPlatformId,
|
||||
m.aliases,
|
||||
m.avatar,
|
||||
msg.content,
|
||||
msg.ts as timestamp,
|
||||
msg.type,
|
||||
msg.reply_to_message_id,
|
||||
reply_msg.content as replyToContent,
|
||||
COALESCE(reply_m.group_nickname, reply_m.account_name, reply_m.platform_id) as replyToSenderName
|
||||
FROM message msg
|
||||
JOIN member m ON msg.sender_id = m.id
|
||||
LEFT JOIN message reply_msg ON msg.reply_to_message_id = reply_msg.platform_message_id
|
||||
LEFT JOIN member reply_m ON reply_msg.sender_id = reply_m.id
|
||||
WHERE msg.id IN (${placeholders})
|
||||
ORDER BY msg.ts ASC, msg.id ASC
|
||||
`
|
||||
|
||||
const rows = db.prepare(sql).all(...idList) as DbMessageRow[]
|
||||
return rows.map(sanitizeMessageRow)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定消息之前的 N 条消息(用于向上无限滚动)
|
||||
* @param sessionId 会话 ID
|
||||
|
||||
@@ -253,6 +253,16 @@ export async function pluginCompute<TOutput = any>(fnString: string, input: any)
|
||||
return sendToWorker('pluginCompute', { fnString, input }, 120000)
|
||||
}
|
||||
|
||||
// ==================== 缓存管理 ====================
|
||||
|
||||
/**
|
||||
* 清除指定 session 的所有分析结果缓存
|
||||
* 在数据变更(增量导入、成员删除/别名更新)后调用
|
||||
*/
|
||||
export async function invalidateAnalysisCache(sessionId: string): Promise<boolean> {
|
||||
return sendToWorker('invalidateAnalysisCache', { sessionId })
|
||||
}
|
||||
|
||||
// ==================== 导出的异步 API ====================
|
||||
|
||||
export async function getAvailableYears(sessionId: string): Promise<number[]> {
|
||||
@@ -494,6 +504,18 @@ export async function getMessageContext(
|
||||
return sendToWorker('getMessageContext', { sessionId, messageIds, contextSize })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取搜索结果的上下文消息(会话感知 + 区间合并去重)
|
||||
*/
|
||||
export async function getSearchMessageContext(
|
||||
sessionId: string,
|
||||
messageIds: number[],
|
||||
contextBefore?: number,
|
||||
contextAfter?: number
|
||||
): Promise<SearchMessageResult[]> {
|
||||
return sendToWorker('getSearchMessageContext', { sessionId, messageIds, contextBefore, contextAfter })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近消息(用于概览性问题)
|
||||
*/
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"build": "electron-vite build",
|
||||
"build:mac": "npm run build && electron-builder --mac --config electron-builder.yml -p never",
|
||||
"build:win": "npm run build && electron-builder --win --config electron-builder.yml -p never",
|
||||
"build:linux": "npm run build && electron-builder --linux --config electron-builder.yml -p never",
|
||||
"type-check:web": "vue-tsc --noEmit -p tsconfig.web.json",
|
||||
"type-check:node": "tsc --noEmit -p tsconfig.node.json",
|
||||
"type-check:all": "npm run type-check:web && npm run type-check:node",
|
||||
|
||||
@@ -61,7 +61,12 @@
|
||||
"get_members": "Get Members",
|
||||
"get_member_name_history": "Get Nickname History",
|
||||
"get_conversation_between": "Get Conversation",
|
||||
"get_message_context": "Get Message Context"
|
||||
"get_message_context": "Get Message Context",
|
||||
"search_sessions": "Search Sessions",
|
||||
"get_session_messages": "Get Session Messages",
|
||||
"get_session_summaries": "Get Session Summaries",
|
||||
"response_time_analysis": "Response Time Analysis",
|
||||
"keyword_frequency": "Keyword Frequency"
|
||||
},
|
||||
"generating": "Generating response...",
|
||||
"think": {
|
||||
|
||||
@@ -210,6 +210,12 @@
|
||||
"title": "Context Limit",
|
||||
"description": "Number of recent conversation rounds to keep (1 round = User + AI). Prevents excessive token usage."
|
||||
},
|
||||
"searchContext": {
|
||||
"title": "Search Context Window",
|
||||
"description": "Automatically include surrounding messages for each search hit to help AI understand the context. Set to 0 to disable",
|
||||
"before": "Before",
|
||||
"after": "After"
|
||||
},
|
||||
"exportFormat": {
|
||||
"title": "Export Format",
|
||||
"description": "File format for exporting AI conversations",
|
||||
|
||||
@@ -61,7 +61,12 @@
|
||||
"get_members": "メンバー一覧を取得",
|
||||
"get_member_name_history": "ニックネーム履歴を取得",
|
||||
"get_conversation_between": "やり取り履歴を取得",
|
||||
"get_message_context": "コンテキストを取得"
|
||||
"get_message_context": "コンテキストを取得",
|
||||
"search_sessions": "セッションを検索",
|
||||
"get_session_messages": "セッションメッセージを取得",
|
||||
"get_session_summaries": "セッション要約を取得",
|
||||
"response_time_analysis": "応答時間分析",
|
||||
"keyword_frequency": "キーワード頻度分析"
|
||||
},
|
||||
"generating": "回答を作成中...",
|
||||
"think": {
|
||||
|
||||
@@ -210,6 +210,12 @@
|
||||
"title": "AI コンテキスト制限",
|
||||
"description": "会話ごとに保持する直近のやり取り数です(1 往復 = ユーザーの質問 + AI の回答)。文脈が長くなりすぎて Token を消費するのを防ぎます"
|
||||
},
|
||||
"searchContext": {
|
||||
"title": "検索コンテキストウィンドウ",
|
||||
"description": "検索ヒット時に前後の会話コンテキストを自動的に含めることで、AI がメッセージの背景を理解しやすくなります。0 に設定するとコンテキストなし",
|
||||
"before": "前",
|
||||
"after": "後"
|
||||
},
|
||||
"exportFormat": {
|
||||
"title": "会話エクスポート形式",
|
||||
"description": "AI チャットをエクスポートする際のファイル形式",
|
||||
|
||||
@@ -61,7 +61,12 @@
|
||||
"get_members": "获取成员列表",
|
||||
"get_member_name_history": "获取昵称历史",
|
||||
"get_conversation_between": "获取对话记录",
|
||||
"get_message_context": "获取上下文"
|
||||
"get_message_context": "获取上下文",
|
||||
"search_sessions": "搜索会话",
|
||||
"get_session_messages": "获取会话消息",
|
||||
"get_session_summaries": "获取会话摘要",
|
||||
"response_time_analysis": "回复时间分析",
|
||||
"keyword_frequency": "关键词频率分析"
|
||||
},
|
||||
"generating": "正在生成回复...",
|
||||
"think": {
|
||||
|
||||
@@ -210,6 +210,12 @@
|
||||
"title": "AI上下文限制",
|
||||
"description": "每次对话保留最近的对话轮数(1轮 = 用户提问 + AI回复),防止上下文过长消耗 Token"
|
||||
},
|
||||
"searchContext": {
|
||||
"title": "搜索上下文窗口",
|
||||
"description": "搜索命中消息时自动携带前后的对话上下文,帮助 AI 理解消息背景。设为 0 则不携带上下文",
|
||||
"before": "前",
|
||||
"after": "后"
|
||||
},
|
||||
"exportFormat": {
|
||||
"title": "对话导出格式",
|
||||
"description": "导出 AI 对话时使用的文件格式",
|
||||
|
||||
@@ -61,7 +61,12 @@
|
||||
"get_members": "取得成員列表",
|
||||
"get_member_name_history": "取得暱稱歷史",
|
||||
"get_conversation_between": "取得對話紀錄",
|
||||
"get_message_context": "取得上下文"
|
||||
"get_message_context": "取得上下文",
|
||||
"search_sessions": "搜尋會話",
|
||||
"get_session_messages": "取得會話訊息",
|
||||
"get_session_summaries": "取得會話摘要",
|
||||
"response_time_analysis": "回覆時間分析",
|
||||
"keyword_frequency": "關鍵詞頻率分析"
|
||||
},
|
||||
"generating": "正在產生回覆...",
|
||||
"think": {
|
||||
|
||||
@@ -210,6 +210,12 @@
|
||||
"title": "AI 上下文限制",
|
||||
"description": "每次對話只保留最近幾輪內容(1 輪 = 使用者提問 + AI 回覆),避免上下文過長而消耗過多 Token"
|
||||
},
|
||||
"searchContext": {
|
||||
"title": "搜尋上下文視窗",
|
||||
"description": "搜尋命中訊息時自動攜帶前後的對話上下文,幫助 AI 理解訊息背景。設為 0 則不攜帶上下文",
|
||||
"before": "前",
|
||||
"after": "後"
|
||||
},
|
||||
"exportFormat": {
|
||||
"title": "對話匯出格式",
|
||||
"description": "匯出 AI 對話時使用的檔案格式",
|
||||
|
||||
@@ -71,6 +71,24 @@ const enableAutoSkill = computed({
|
||||
emit('config-changed')
|
||||
},
|
||||
})
|
||||
|
||||
const searchContextBefore = computed({
|
||||
get: () => aiGlobalSettings.value.searchContextBefore ?? 3,
|
||||
set: (val: number) => {
|
||||
const clampedVal = Math.max(0, Math.min(20, val ?? 3))
|
||||
promptStore.updateAIGlobalSettings({ searchContextBefore: clampedVal })
|
||||
emit('config-changed')
|
||||
},
|
||||
})
|
||||
|
||||
const searchContextAfter = computed({
|
||||
get: () => aiGlobalSettings.value.searchContextAfter ?? 3,
|
||||
set: (val: number) => {
|
||||
const clampedVal = Math.max(0, Math.min(20, val ?? 3))
|
||||
promptStore.updateAIGlobalSettings({ searchContextAfter: clampedVal })
|
||||
emit('config-changed')
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -107,6 +125,32 @@ const enableAutoSkill = computed({
|
||||
</div>
|
||||
<UInputNumber v-model="globalMaxHistoryRounds" :min="1" :max="50" class="w-30" />
|
||||
</div>
|
||||
|
||||
<!-- 搜索上下文窗口 -->
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.aiPrompt.searchContext.title') }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.aiPrompt.searchContext.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.aiPrompt.searchContext.before') }}
|
||||
</span>
|
||||
<UInputNumber v-model="searchContextBefore" :min="0" :max="20" class="w-24" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.aiPrompt.searchContext.after') }}
|
||||
</span>
|
||||
<UInputNumber v-model="searchContextAfter" :min="0" :max="20" class="w-24" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -804,6 +804,8 @@ export const useAIChatStore = defineStore('aiChatRuntime', () => {
|
||||
: undefined,
|
||||
mentionedMembers: currentMentionedMembers.length > 0 ? currentMentionedMembers : undefined,
|
||||
preprocessConfig: serializablePreprocessConfig,
|
||||
searchContextBefore: aiGlobalSettings.value.searchContextBefore,
|
||||
searchContextAfter: aiGlobalSettings.value.searchContextAfter,
|
||||
}
|
||||
|
||||
const { requestId: agentReqId, promise: agentPromise } = window.agentApi.runStream(
|
||||
|
||||
@@ -46,6 +46,8 @@ export const usePromptStore = defineStore(
|
||||
exportFormat: 'markdown' as 'markdown' | 'txt',
|
||||
sqlExportFormat: 'csv' as 'csv' | 'json',
|
||||
enableAutoSkill: true,
|
||||
searchContextBefore: 2,
|
||||
searchContextAfter: 2,
|
||||
})
|
||||
const customKeywordTemplates = ref<KeywordTemplate[]>([])
|
||||
const deletedPresetTemplateIds = ref<string[]>([])
|
||||
@@ -105,6 +107,8 @@ export const usePromptStore = defineStore(
|
||||
exportFormat: 'markdown' | 'txt'
|
||||
sqlExportFormat: 'csv' | 'json'
|
||||
enableAutoSkill: boolean
|
||||
searchContextBefore: number
|
||||
searchContextAfter: number
|
||||
}>
|
||||
) {
|
||||
aiGlobalSettings.value = { ...aiGlobalSettings.value, ...settings }
|
||||
|
||||
Reference in New Issue
Block a user