mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-04-18 13:12:39 +08:00
Compare commits
9 Commits
c65b5238cc
...
5adb1122d4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5adb1122d4 | ||
|
|
c869383193 | ||
|
|
53208de60e | ||
|
|
fa3282f625 | ||
|
|
a0155b5f68 | ||
|
|
4929b49135 | ||
|
|
7919929b94 | ||
|
|
b8a3823cef | ||
|
|
d49a094164 |
70
.github/workflows/release.yml
vendored
70
.github/workflows/release.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '24'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '24'
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -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: '24'
|
||||
|
||||
- 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
|
||||
|
||||
@@ -1,4 +1,32 @@
|
||||
[
|
||||
{
|
||||
"version": "0.14.3",
|
||||
"date": "2026-04-07",
|
||||
"summary": "大幅度优化搜索与查询性能,搜索工具支持自动携带上下文消息,并支持 Linux 平台。",
|
||||
"changes": [
|
||||
{
|
||||
"type": "feat",
|
||||
"items": [
|
||||
"实验室新增基础调试工具",
|
||||
"搜索工具支持自动携带上下文消息",
|
||||
"新增查询缓存以提升访问速度",
|
||||
"移除旧版提示词系统"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "fix",
|
||||
"items": ["补全工具调用显示名称的 i18n 翻译"]
|
||||
},
|
||||
{
|
||||
"type": "chore",
|
||||
"items": ["支持 Linux 平台打包"]
|
||||
},
|
||||
{
|
||||
"type": "docs",
|
||||
"items": ["更新项目文档内容"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "0.14.2",
|
||||
"date": "2026-04-07",
|
||||
|
||||
@@ -1,4 +1,32 @@
|
||||
[
|
||||
{
|
||||
"version": "0.14.3",
|
||||
"date": "2026-04-07",
|
||||
"summary": "This release adds core debugging tools for Lab, improves search context and query performance, fills missing i18n labels, removes legacy prompts, and adds Linux packaging support.",
|
||||
"changes": [
|
||||
{
|
||||
"type": "feat",
|
||||
"items": [
|
||||
"Add core debugging tools in Lab.",
|
||||
"Make the search tool carry conversation context automatically.",
|
||||
"Add query caching to speed up repeated access.",
|
||||
"Remove the legacy prompt system."
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "fix",
|
||||
"items": ["Fill in missing i18n labels for tool-call display names."]
|
||||
},
|
||||
{
|
||||
"type": "chore",
|
||||
"items": ["Add Linux packaging support."]
|
||||
},
|
||||
{
|
||||
"type": "docs",
|
||||
"items": ["Update project documentation."]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "0.14.2",
|
||||
"date": "2026-04-07",
|
||||
|
||||
@@ -1,4 +1,32 @@
|
||||
[
|
||||
{
|
||||
"version": "0.14.3",
|
||||
"date": "2026-04-07",
|
||||
"summary": "今回の更新では、Lab の基本デバッグツールを追加し、検索コンテキストとクエリ性能を改善。i18n 表示文言の不足を補い、旧プロンプトを整理し、Linux パッケージングにも対応しました。",
|
||||
"changes": [
|
||||
{
|
||||
"type": "feat",
|
||||
"items": [
|
||||
"Lab に基本的なデバッグツールを追加",
|
||||
"検索ツールが会話コンテキストを自動で引き継ぐよう改善",
|
||||
"クエリキャッシュを追加し、繰り返しアクセスを高速化",
|
||||
"旧プロンプトシステムを削除"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "fix",
|
||||
"items": ["ツール呼び出し表示名の i18n 翻訳不足を修正"]
|
||||
},
|
||||
{
|
||||
"type": "chore",
|
||||
"items": ["Linux 向けパッケージングに対応"]
|
||||
},
|
||||
{
|
||||
"type": "docs",
|
||||
"items": ["プロジェクトドキュメントを更新"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "0.14.2",
|
||||
"date": "2026-04-07",
|
||||
|
||||
@@ -1,4 +1,32 @@
|
||||
[
|
||||
{
|
||||
"version": "0.14.3",
|
||||
"date": "2026-04-07",
|
||||
"summary": "本次更新新增實驗室基礎除錯工具,優化搜尋上下文與查詢效能,補齊 i18n 顯示文案,並移除舊版提示詞與支援 Linux 打包。",
|
||||
"changes": [
|
||||
{
|
||||
"type": "feat",
|
||||
"items": [
|
||||
"實驗室新增基礎除錯工具",
|
||||
"搜尋工具可自動攜帶上下文訊息",
|
||||
"新增查詢快取以加速重複存取",
|
||||
"移除舊版提示詞系統"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "fix",
|
||||
"items": ["補齊工具呼叫顯示名稱的 i18n 翻譯"]
|
||||
},
|
||||
{
|
||||
"type": "chore",
|
||||
"items": ["新增 Linux 平台打包支援"]
|
||||
},
|
||||
{
|
||||
"type": "docs",
|
||||
"items": ["更新專案文件內容"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "0.14.2",
|
||||
"date": "2026-04-07",
|
||||
|
||||
@@ -70,7 +70,6 @@ linux:
|
||||
target:
|
||||
- AppImage
|
||||
- deb
|
||||
- rpm
|
||||
- tar.gz
|
||||
category: Utility
|
||||
|
||||
|
||||
@@ -33,11 +33,22 @@ 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,22 @@ 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)
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
} from '@mariozechner/pi-ai'
|
||||
import { t } from '../i18n'
|
||||
import type { ToolContext } from '../ai/tools/types'
|
||||
import { TOOL_REGISTRY } from '../ai/tools/definitions'
|
||||
import { getDefaultRulesForLocale, mergeRulesForLocale } from '../ai/preprocessor/builtin-rules'
|
||||
import type { IpcContext } from './types'
|
||||
|
||||
@@ -147,6 +148,29 @@ function formatAIError(error: unknown, provider?: llm.LLMProvider): string {
|
||||
return friendlyMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归剥离对象中的 avatar/senderAvatar 字段(base64 大数据)
|
||||
* 用于工具测试场景,避免传输和序列化大量无用头像数据
|
||||
*/
|
||||
function stripAvatarFields(obj: unknown): void {
|
||||
if (!obj || typeof obj !== 'object') return
|
||||
if (Array.isArray(obj)) {
|
||||
for (const item of obj) stripAvatarFields(item)
|
||||
return
|
||||
}
|
||||
const record = obj as Record<string, unknown>
|
||||
for (const key of Object.keys(record)) {
|
||||
if ((key === 'avatar' || key === 'senderAvatar') && typeof record[key] === 'string') {
|
||||
const val = record[key] as string
|
||||
if (val.length > 200) {
|
||||
record[key] = '[stripped]'
|
||||
}
|
||||
} else if (typeof record[key] === 'object' && record[key] !== null) {
|
||||
stripAvatarFields(record[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function registerAIHandlers({ win }: IpcContext): void {
|
||||
console.log('[IPC] Registering AI handlers...')
|
||||
|
||||
@@ -820,6 +844,94 @@ export function registerAIHandlers({ win }: IpcContext): void {
|
||||
}
|
||||
})
|
||||
|
||||
// ==================== 工具测试 API(实验室 - 基础工具) ====================
|
||||
|
||||
const activeToolTests = new Map<string, AbortController>()
|
||||
|
||||
ipcMain.handle('ai:getToolCatalog', async () => {
|
||||
try {
|
||||
return TOOL_REGISTRY.map((entry) => {
|
||||
const dummyContext: ToolContext = { sessionId: '__catalog__' }
|
||||
const tool = entry.factory(dummyContext)
|
||||
const descKey = `ai.tools.${entry.name}.desc`
|
||||
const translated = t(descKey)
|
||||
return {
|
||||
name: entry.name,
|
||||
category: entry.category,
|
||||
description: translated !== descKey ? translated : (tool.description ?? ''),
|
||||
parameters: tool.parameters ?? {},
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to get tool catalog:', error)
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
'ai:executeTool',
|
||||
async (_, testId: string, toolName: string, params: Record<string, unknown>, sessionId: string) => {
|
||||
const MAX_RESULT_CHARS = 500_000
|
||||
const abortController = new AbortController()
|
||||
activeToolTests.set(testId, abortController)
|
||||
|
||||
try {
|
||||
const entry = TOOL_REGISTRY.find((e) => e.name === toolName)
|
||||
if (!entry) {
|
||||
return { success: false, error: `Tool not found: ${toolName}` }
|
||||
}
|
||||
|
||||
const context: ToolContext = { sessionId }
|
||||
const tool = entry.factory(context)
|
||||
const startTime = Date.now()
|
||||
const result = await tool.execute(`test_${Date.now()}`, params)
|
||||
const elapsed = Date.now() - startTime
|
||||
|
||||
if (abortController.signal.aborted) {
|
||||
return { success: false, error: 'cancelled' }
|
||||
}
|
||||
|
||||
let details = result.details as Record<string, unknown> | undefined
|
||||
let truncated = false
|
||||
|
||||
if (details) {
|
||||
stripAvatarFields(details)
|
||||
const raw = JSON.stringify(details)
|
||||
if (raw.length > MAX_RESULT_CHARS) {
|
||||
truncated = true
|
||||
details = { _truncated: true, _originalSize: raw.length, _preview: raw.slice(0, MAX_RESULT_CHARS) }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
elapsed,
|
||||
content: result.content,
|
||||
details,
|
||||
truncated,
|
||||
}
|
||||
} catch (error) {
|
||||
if (abortController.signal.aborted) {
|
||||
return { success: false, error: 'cancelled' }
|
||||
}
|
||||
console.error(`Failed to execute tool ${toolName}:`, error)
|
||||
return { success: false, error: String(error) }
|
||||
} finally {
|
||||
activeToolTests.delete(testId)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle('ai:cancelToolTest', async (_, testId: string) => {
|
||||
const controller = activeToolTests.get(testId)
|
||||
if (controller) {
|
||||
controller.abort()
|
||||
activeToolTests.delete(testId)
|
||||
return { success: true }
|
||||
}
|
||||
return { success: false }
|
||||
})
|
||||
|
||||
// ==================== AI Agent API ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,7 @@ 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 +219,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 +262,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 })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近消息(用于概览性问题)
|
||||
*/
|
||||
|
||||
@@ -113,6 +113,24 @@ export interface DesensitizeRule {
|
||||
locales: string[]
|
||||
}
|
||||
|
||||
/** 工具目录条目(实验室 - 基础工具) */
|
||||
export interface ToolCatalogEntry {
|
||||
name: string
|
||||
category: 'core' | 'analysis'
|
||||
description: string
|
||||
parameters: Record<string, unknown>
|
||||
}
|
||||
|
||||
/** 工具执行结果 */
|
||||
export interface ToolExecuteResult {
|
||||
success: boolean
|
||||
elapsed?: number
|
||||
content?: Array<{ type: string; text: string }>
|
||||
details?: Record<string, unknown>
|
||||
error?: string
|
||||
truncated?: boolean
|
||||
}
|
||||
|
||||
/** 聊天记录预处理配置 */
|
||||
export interface PreprocessConfig {
|
||||
dataCleaning: boolean
|
||||
@@ -501,6 +519,23 @@ export const aiApi = {
|
||||
mergeDesensitizeRules: (existingRules: DesensitizeRule[], locale: string): Promise<DesensitizeRule[]> => {
|
||||
return ipcRenderer.invoke('ai:mergeDesensitizeRules', existingRules, locale)
|
||||
},
|
||||
|
||||
getToolCatalog: (): Promise<ToolCatalogEntry[]> => {
|
||||
return ipcRenderer.invoke('ai:getToolCatalog')
|
||||
},
|
||||
|
||||
executeTool: (
|
||||
testId: string,
|
||||
toolName: string,
|
||||
params: Record<string, unknown>,
|
||||
sessionId: string
|
||||
): Promise<ToolExecuteResult> => {
|
||||
return ipcRenderer.invoke('ai:executeTool', testId, toolName, params, sessionId)
|
||||
},
|
||||
|
||||
cancelToolTest: (testId: string): Promise<{ success: boolean }> => {
|
||||
return ipcRenderer.invoke('ai:cancelToolTest', testId)
|
||||
},
|
||||
}
|
||||
|
||||
// ==================== LLM API ====================
|
||||
|
||||
23
electron/preload/index.d.ts
vendored
23
electron/preload/index.d.ts
vendored
@@ -377,6 +377,9 @@ interface AiApi {
|
||||
showAiLogFile: () => Promise<{ success: boolean; path?: string; error?: string }>
|
||||
getDefaultDesensitizeRules: (locale: string) => Promise<DesensitizeRule[]>
|
||||
mergeDesensitizeRules: (existingRules: DesensitizeRule[], locale: string) => Promise<DesensitizeRule[]>
|
||||
getToolCatalog: () => Promise<ToolCatalogEntry[]>
|
||||
executeTool: (testId: string, toolName: string, params: Record<string, unknown>, sessionId: string) => Promise<ToolExecuteResult>
|
||||
cancelToolTest: (testId: string) => Promise<{ success: boolean }>
|
||||
// 自定义筛选(支持分页)
|
||||
filterMessagesWithContext: (
|
||||
sessionId: string,
|
||||
@@ -632,6 +635,24 @@ interface DesensitizeRule {
|
||||
locales: string[]
|
||||
}
|
||||
|
||||
/** 工具目录条目(实验室 - 基础工具) */
|
||||
interface ToolCatalogEntry {
|
||||
name: string
|
||||
category: 'core' | 'analysis'
|
||||
description: string
|
||||
parameters: Record<string, unknown>
|
||||
}
|
||||
|
||||
/** 工具执行结果 */
|
||||
interface ToolExecuteResult {
|
||||
success: boolean
|
||||
elapsed?: number
|
||||
content?: Array<{ type: string; text: string }>
|
||||
details?: Record<string, unknown>
|
||||
error?: string
|
||||
truncated?: boolean
|
||||
}
|
||||
|
||||
/** 聊天记录预处理配置 */
|
||||
interface PreprocessConfig {
|
||||
dataCleaning: boolean
|
||||
@@ -1053,6 +1074,8 @@ export {
|
||||
ToolContext,
|
||||
DesensitizeRule,
|
||||
PreprocessConfig,
|
||||
ToolCatalogEntry,
|
||||
ToolExecuteResult,
|
||||
TokenUsage,
|
||||
CacheDirectoryInfo,
|
||||
CacheInfo,
|
||||
|
||||
17
package.json
17
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ChatLab",
|
||||
"version": "0.14.2",
|
||||
"version": "0.14.3",
|
||||
"description": "本地化的聊天记录分析工具,通过 SQL 和 AI Agent 回顾你的社交记忆",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -8,6 +8,11 @@
|
||||
},
|
||||
"author": "",
|
||||
"main": "./out/main/index.js",
|
||||
"packageManager": "pnpm@9.15.9",
|
||||
"engines": {
|
||||
"node": ">=24 <25",
|
||||
"pnpm": ">=9 <10"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"better-sqlite3",
|
||||
@@ -20,11 +25,12 @@
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"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:mac": "pnpm run build && electron-builder --mac --config electron-builder.yml -p never",
|
||||
"build:win": "pnpm run build && electron-builder --win --config electron-builder.yml -p never",
|
||||
"build:linux": "pnpm 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",
|
||||
"type-check:all": "pnpm run type-check:web && pnpm run type-check:node",
|
||||
"type-check": "vue-tsc --noEmit -p tsconfig.web.json",
|
||||
"test:agent-context": "node --experimental-strip-types --test electron/main/ai/context/sessionLog.test.mjs",
|
||||
"test:e2e:launcher": "node --test tests/e2e/helpers/app-launcher.test.js",
|
||||
@@ -76,6 +82,7 @@
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^6.3.5",
|
||||
"vue": "^3.5.25",
|
||||
"vue-router": "^4.6.3"
|
||||
"vue-router": "^4.6.3",
|
||||
"vue-tsc": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
69
pnpm-lock.yaml
generated
69
pnpm-lock.yaml
generated
@@ -138,6 +138,9 @@ importers:
|
||||
vue-router:
|
||||
specifier: ^4.6.3
|
||||
version: 4.6.4(vue@3.5.27(typescript@5.9.3))
|
||||
vue-tsc:
|
||||
specifier: ^3.1.1
|
||||
version: 3.2.6(typescript@5.9.3)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -1845,6 +1848,15 @@ packages:
|
||||
vite: ^5.0.0 || ^6.0.0
|
||||
vue: ^3.2.25
|
||||
|
||||
'@volar/language-core@2.4.28':
|
||||
resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==}
|
||||
|
||||
'@volar/source-map@2.4.28':
|
||||
resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==}
|
||||
|
||||
'@volar/typescript@2.4.28':
|
||||
resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==}
|
||||
|
||||
'@vue/compiler-core@3.5.27':
|
||||
resolution: {integrity: sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==}
|
||||
|
||||
@@ -1886,6 +1898,9 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
'@vue/language-core@3.2.6':
|
||||
resolution: {integrity: sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==}
|
||||
|
||||
'@vue/reactivity@3.5.27':
|
||||
resolution: {integrity: sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==}
|
||||
|
||||
@@ -2036,6 +2051,9 @@ packages:
|
||||
ajv@8.18.0:
|
||||
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
|
||||
|
||||
alien-signals@3.1.2:
|
||||
resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3669,6 +3687,9 @@ packages:
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
muggle-string@0.4.1:
|
||||
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
|
||||
|
||||
nanoid@3.3.11:
|
||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
@@ -3848,6 +3869,9 @@ packages:
|
||||
partial-json@0.1.7:
|
||||
resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==}
|
||||
|
||||
path-browserify@1.0.1:
|
||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||
|
||||
path-exists@4.0.0:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -4738,6 +4762,9 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vscode-uri@3.1.0:
|
||||
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
||||
|
||||
vue-component-type-helpers@3.2.4:
|
||||
resolution: {integrity: sha512-05lR16HeZDcDpB23ku5b5f1fBOoHqFnMiKRr2CiEvbG5Ux4Yi0McmQBOET0dR0nxDXosxyVqv67q6CzS3AK8rw==}
|
||||
|
||||
@@ -4775,6 +4802,12 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^3.5.0
|
||||
|
||||
vue-tsc@3.2.6:
|
||||
resolution: {integrity: sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: '>=5.0.0'
|
||||
|
||||
vue@3.5.27:
|
||||
resolution: {integrity: sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==}
|
||||
peerDependencies:
|
||||
@@ -7252,6 +7285,18 @@ snapshots:
|
||||
vite: 6.4.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.8.2)
|
||||
vue: 3.5.27(typescript@5.9.3)
|
||||
|
||||
'@volar/language-core@2.4.28':
|
||||
dependencies:
|
||||
'@volar/source-map': 2.4.28
|
||||
|
||||
'@volar/source-map@2.4.28': {}
|
||||
|
||||
'@volar/typescript@2.4.28':
|
||||
dependencies:
|
||||
'@volar/language-core': 2.4.28
|
||||
path-browserify: 1.0.1
|
||||
vscode-uri: 3.1.0
|
||||
|
||||
'@vue/compiler-core@3.5.27':
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.0
|
||||
@@ -7324,6 +7369,16 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vue/language-core@3.2.6':
|
||||
dependencies:
|
||||
'@volar/language-core': 2.4.28
|
||||
'@vue/compiler-dom': 3.5.27
|
||||
'@vue/shared': 3.5.27
|
||||
alien-signals: 3.1.2
|
||||
muggle-string: 0.4.1
|
||||
path-browserify: 1.0.1
|
||||
picomatch: 4.0.3
|
||||
|
||||
'@vue/reactivity@3.5.27':
|
||||
dependencies:
|
||||
'@vue/shared': 3.5.27
|
||||
@@ -7457,6 +7512,8 @@ snapshots:
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
alien-signals@3.1.2: {}
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-regex@6.2.2: {}
|
||||
@@ -9262,6 +9319,8 @@ snapshots:
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
muggle-string@0.4.1: {}
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
napi-build-utils@2.0.0: {}
|
||||
@@ -9442,6 +9501,8 @@ snapshots:
|
||||
|
||||
partial-json@0.1.7: {}
|
||||
|
||||
path-browserify@1.0.1: {}
|
||||
|
||||
path-exists@4.0.0: {}
|
||||
|
||||
path-is-absolute@1.0.1: {}
|
||||
@@ -10348,6 +10409,8 @@ snapshots:
|
||||
lightningcss: 1.31.1
|
||||
yaml: 2.8.2
|
||||
|
||||
vscode-uri@3.1.0: {}
|
||||
|
||||
vue-component-type-helpers@3.2.4: {}
|
||||
|
||||
vue-demi@0.14.10(vue@3.5.27(typescript@5.9.3)):
|
||||
@@ -10391,6 +10454,12 @@ snapshots:
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.27(typescript@5.9.3)
|
||||
|
||||
vue-tsc@3.2.6(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@volar/typescript': 2.4.28
|
||||
'@vue/language-core': 3.2.6
|
||||
typescript: 5.9.3
|
||||
|
||||
vue@3.5.27(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.27
|
||||
|
||||
@@ -5,9 +5,12 @@ import { useRoute } from 'vue-router'
|
||||
import { SubTabs } from '@/components/UI'
|
||||
import { ChatExplorer } from '../AIChat'
|
||||
import SQLLabTab from './SQLLabTab.vue'
|
||||
import ToolTestTab from './ToolTestTab.vue'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
@@ -19,9 +22,14 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const subTabs = computed(() => {
|
||||
// 实验室模式下只保留 SQL 实验室子 Tab,一级导航由外层页面承载。
|
||||
if (props.mode === 'sql-only') {
|
||||
return [{ id: 'sql-lab', label: t('ai.tab.sqlLab'), icon: 'i-heroicons-command-line' }]
|
||||
const tabs = [
|
||||
{ id: 'sql-lab', label: t('ai.tab.sqlLab'), icon: 'i-heroicons-command-line' },
|
||||
]
|
||||
if (settingsStore.debugMode) {
|
||||
tabs.push({ id: 'tool-test', label: t('ai.lab.basicTools'), icon: 'i-heroicons-wrench-screwdriver' })
|
||||
}
|
||||
return tabs
|
||||
}
|
||||
|
||||
return [
|
||||
@@ -32,12 +40,13 @@ const subTabs = computed(() => {
|
||||
|
||||
const activeSubTab = ref(props.mode === 'sql-only' ? 'sql-lab' : (route.query.aiSubTab as string) || 'chat-explorer')
|
||||
|
||||
// 悬浮任务条返回时会通过 query 指定目标子页,这里同步一次,确保能直接回到对话流。
|
||||
watch(
|
||||
() => route.query.aiSubTab,
|
||||
(nextTab) => {
|
||||
if (props.mode === 'sql-only') {
|
||||
activeSubTab.value = 'sql-lab'
|
||||
if (nextTab === 'sql-lab' || (nextTab === 'tool-test' && settingsStore.debugMode)) {
|
||||
activeSubTab.value = nextTab
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -47,6 +56,15 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => settingsStore.debugMode,
|
||||
(enabled) => {
|
||||
if (!enabled && activeSubTab.value === 'tool-test') {
|
||||
activeSubTab.value = 'sql-lab'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// ChatExplorer 组件引用
|
||||
const chatExplorerRef = ref<InstanceType<typeof ChatExplorer> | null>(null)
|
||||
|
||||
@@ -79,6 +97,12 @@ defineExpose({
|
||||
:time-filter="timeFilter"
|
||||
:chat-type="chatType"
|
||||
/>
|
||||
<!-- 基础工具测试 -->
|
||||
<ToolTestTab
|
||||
v-else-if="activeSubTab === 'tool-test'"
|
||||
class="h-full"
|
||||
:session-id="props.sessionId"
|
||||
/>
|
||||
<!-- SQL 实验室 -->
|
||||
<SQLLabTab v-else class="h-full" :session-id="props.sessionId" :chat-type="props.chatType" />
|
||||
</Transition>
|
||||
|
||||
308
src/components/analysis/ToolTestTab.vue
Normal file
308
src/components/analysis/ToolTestTab.vue
Normal file
@@ -0,0 +1,308 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
sessionId: string
|
||||
}>()
|
||||
|
||||
interface ToolCatalogEntry {
|
||||
name: string
|
||||
category: 'core' | 'analysis'
|
||||
description: string
|
||||
parameters: Record<string, unknown>
|
||||
}
|
||||
|
||||
interface ParamField {
|
||||
name: string
|
||||
type: string
|
||||
description: string
|
||||
required: boolean
|
||||
defaultValue?: unknown
|
||||
enumValues?: string[]
|
||||
isArray?: boolean
|
||||
arrayItemType?: string
|
||||
}
|
||||
|
||||
const catalog = ref<ToolCatalogEntry[]>([])
|
||||
const selectedToolName = ref('')
|
||||
const paramValues = ref<Record<string, string>>({})
|
||||
const isExecuting = ref(false)
|
||||
const currentTestId = ref<string | null>(null)
|
||||
const resultJson = ref<string | null>(null)
|
||||
const resultError = ref<string | null>(null)
|
||||
const resultTruncated = ref(false)
|
||||
const elapsed = ref<number | null>(null)
|
||||
|
||||
function toolLabel(name: string): string {
|
||||
const key = `ai.chat.message.tools.${name}`
|
||||
const translated = t(key)
|
||||
return translated !== key ? translated : name
|
||||
}
|
||||
|
||||
const coreTools = computed(() => catalog.value.filter((t) => t.category === 'core'))
|
||||
const analysisTools = computed(() => catalog.value.filter((t) => t.category === 'analysis'))
|
||||
const selectedTool = computed(() => catalog.value.find((t) => t.name === selectedToolName.value))
|
||||
|
||||
const paramFields = computed<ParamField[]>(() => {
|
||||
const tool = selectedTool.value
|
||||
if (!tool?.parameters) return []
|
||||
|
||||
const params = tool.parameters as {
|
||||
properties?: Record<string, Record<string, unknown>>
|
||||
required?: string[]
|
||||
}
|
||||
if (!params.properties) return []
|
||||
|
||||
const requiredSet = new Set(params.required ?? [])
|
||||
return Object.entries(params.properties).map(([name, schema]) => {
|
||||
const field: ParamField = {
|
||||
name,
|
||||
type: (schema.type as string) ?? 'string',
|
||||
description: (schema.description as string) ?? '',
|
||||
required: requiredSet.has(name),
|
||||
defaultValue: schema.default,
|
||||
}
|
||||
if (schema.enum) {
|
||||
field.enumValues = schema.enum as string[]
|
||||
}
|
||||
if (schema.type === 'array') {
|
||||
field.isArray = true
|
||||
const items = schema.items as Record<string, unknown> | undefined
|
||||
field.arrayItemType = (items?.type as string) ?? 'string'
|
||||
}
|
||||
return field
|
||||
})
|
||||
})
|
||||
|
||||
watch(selectedToolName, () => {
|
||||
paramValues.value = {}
|
||||
resultJson.value = null
|
||||
resultError.value = null
|
||||
elapsed.value = null
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
catalog.value = await window.aiApi.getToolCatalog()
|
||||
if (catalog.value.length > 0) {
|
||||
selectedToolName.value = catalog.value[0].name
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load tool catalog:', e)
|
||||
}
|
||||
})
|
||||
|
||||
function buildParams(): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {}
|
||||
for (const field of paramFields.value) {
|
||||
const raw = paramValues.value[field.name]
|
||||
if (raw === undefined || raw === '') continue
|
||||
|
||||
if (field.type === 'number') {
|
||||
const num = Number(raw)
|
||||
if (!isNaN(num)) result[field.name] = num
|
||||
} else if (field.isArray) {
|
||||
result[field.name] = raw
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
} else {
|
||||
result[field.name] = raw
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
async function execute() {
|
||||
if (!selectedToolName.value || isExecuting.value) return
|
||||
isExecuting.value = true
|
||||
resultJson.value = null
|
||||
resultError.value = null
|
||||
resultTruncated.value = false
|
||||
elapsed.value = null
|
||||
|
||||
const testId = `tool_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||||
currentTestId.value = testId
|
||||
|
||||
try {
|
||||
const params = buildParams()
|
||||
const res = await window.aiApi.executeTool(testId, selectedToolName.value, params, props.sessionId)
|
||||
if (res.error === 'cancelled') return
|
||||
if (res.success) {
|
||||
elapsed.value = res.elapsed ?? null
|
||||
resultTruncated.value = !!res.truncated
|
||||
resultJson.value = res.truncated
|
||||
? (res.details as Record<string, unknown>)?._preview as string ?? ''
|
||||
: JSON.stringify(res.details ?? res.content, null, 2)
|
||||
} else {
|
||||
resultError.value = res.error ?? 'Unknown error'
|
||||
}
|
||||
} catch (e) {
|
||||
resultError.value = String(e)
|
||||
} finally {
|
||||
isExecuting.value = false
|
||||
currentTestId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function cancel() {
|
||||
if (currentTestId.value) {
|
||||
await window.aiApi.cancelToolTest(currentTestId.value)
|
||||
isExecuting.value = false
|
||||
currentTestId.value = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full">
|
||||
<!-- Left Sidebar: Tool List -->
|
||||
<div class="w-56 shrink-0 overflow-y-auto border-r border-gray-200 bg-gray-50/50 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<!-- Core Tools Group -->
|
||||
<div v-if="coreTools.length > 0" class="py-2">
|
||||
<div class="px-3 py-1.5 text-[11px] font-semibold tracking-wide text-gray-400 uppercase dark:text-gray-500">
|
||||
{{ t('ai.lab.toolTest.coreTools') }}
|
||||
</div>
|
||||
<button
|
||||
v-for="tool in coreTools"
|
||||
:key="tool.name"
|
||||
class="flex w-full flex-col px-3 py-1.5 text-left transition-colors"
|
||||
:class="[
|
||||
selectedToolName === tool.name
|
||||
? 'bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300'
|
||||
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700/50',
|
||||
]"
|
||||
:title="tool.description"
|
||||
@click="selectedToolName = tool.name"
|
||||
>
|
||||
<span class="truncate text-xs font-medium">{{ toolLabel(tool.name) }}</span>
|
||||
<span class="truncate text-[10px] opacity-50">{{ tool.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Analysis Tools Group -->
|
||||
<div v-if="analysisTools.length > 0" class="border-t border-gray-200 py-2 dark:border-gray-700">
|
||||
<div class="px-3 py-1.5 text-[11px] font-semibold tracking-wide text-gray-400 uppercase dark:text-gray-500">
|
||||
{{ t('ai.lab.toolTest.analysisTools') }}
|
||||
</div>
|
||||
<button
|
||||
v-for="tool in analysisTools"
|
||||
:key="tool.name"
|
||||
class="flex w-full flex-col px-3 py-1.5 text-left transition-colors"
|
||||
:class="[
|
||||
selectedToolName === tool.name
|
||||
? 'bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300'
|
||||
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700/50',
|
||||
]"
|
||||
:title="tool.description"
|
||||
@click="selectedToolName = tool.name"
|
||||
>
|
||||
<span class="truncate text-xs font-medium">{{ toolLabel(tool.name) }}</span>
|
||||
<span class="truncate text-[10px] opacity-50">{{ tool.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Content: Parameters + Result -->
|
||||
<div class="flex flex-1 flex-col gap-4 overflow-y-auto p-4">
|
||||
<!-- Tool Description -->
|
||||
<div v-if="selectedTool" class="rounded-lg border border-primary-200 bg-primary-50/50 px-4 py-3 dark:border-primary-800 dark:bg-primary-900/20">
|
||||
<div class="text-sm font-medium text-primary-700 dark:text-primary-300">
|
||||
{{ toolLabel(selectedTool.name) }}
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-primary-600/70 dark:text-primary-400/70">
|
||||
{{ selectedTool.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parameters Form -->
|
||||
<div v-if="paramFields.length > 0" class="flex flex-col gap-3">
|
||||
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('ai.lab.toolTest.parameters') }}
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div v-for="field in paramFields" :key="field.name" class="flex flex-col gap-1">
|
||||
<label class="text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||
{{ field.name }}
|
||||
<span v-if="field.required" class="text-red-500">*</span>
|
||||
<span v-if="field.type === 'number'" class="ml-1 text-gray-400">(number)</span>
|
||||
<span v-if="field.isArray" class="ml-1 text-gray-400">({{ field.arrayItemType }}[])</span>
|
||||
</label>
|
||||
<USelectMenu
|
||||
v-if="field.enumValues"
|
||||
v-model="paramValues[field.name]"
|
||||
:items="field.enumValues"
|
||||
:placeholder="field.description"
|
||||
size="sm"
|
||||
/>
|
||||
<UInput
|
||||
v-else
|
||||
v-model="paramValues[field.name]"
|
||||
:placeholder="
|
||||
field.isArray
|
||||
? t('ai.lab.toolTest.arrayPlaceholder')
|
||||
: field.defaultValue !== undefined
|
||||
? String(field.defaultValue)
|
||||
: field.description
|
||||
"
|
||||
:type="field.type === 'number' ? 'number' : 'text'"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Execute / Cancel Button -->
|
||||
<div class="flex items-center gap-3">
|
||||
<UButton
|
||||
v-if="!isExecuting"
|
||||
color="primary"
|
||||
:disabled="!selectedToolName"
|
||||
icon="i-heroicons-play"
|
||||
@click="execute"
|
||||
>
|
||||
{{ t('ai.lab.toolTest.execute') }}
|
||||
</UButton>
|
||||
<UButton
|
||||
v-else
|
||||
color="error"
|
||||
icon="i-heroicons-stop"
|
||||
@click="cancel"
|
||||
>
|
||||
{{ t('ai.lab.toolTest.cancel') }}
|
||||
</UButton>
|
||||
<UIcon v-if="isExecuting" name="i-heroicons-arrow-path" class="h-4 w-4 animate-spin text-gray-400" />
|
||||
<span v-if="elapsed !== null" class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('ai.lab.toolTest.elapsed', { ms: elapsed }) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Result -->
|
||||
<div v-if="resultJson || resultError" class="flex-1 min-h-0">
|
||||
<h3 class="mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('ai.lab.toolTest.result') }}
|
||||
</h3>
|
||||
<div
|
||||
v-if="resultError"
|
||||
class="rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-900/20 dark:text-red-400"
|
||||
>
|
||||
{{ resultError }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="resultTruncated"
|
||||
class="mb-2 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-700 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-400"
|
||||
>
|
||||
{{ t('ai.lab.toolTest.truncated') }}
|
||||
</div>
|
||||
<pre
|
||||
class="max-h-[60vh] overflow-auto rounded-lg border border-gray-200 bg-gray-50 p-3 text-xs leading-relaxed text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200"
|
||||
>{{ resultJson }}</pre>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,249 +0,0 @@
|
||||
/**
|
||||
* AI 提示词统一配置
|
||||
*
|
||||
* 本文件集中管理所有 AI 提示词相关的配置:
|
||||
* - 内置预设定义(统一版本,不再区分群聊/私聊)
|
||||
* - 默认系统提示词
|
||||
* - 锁定部分说明(用于前端预览)
|
||||
*
|
||||
* 注意:群聊/私聊的差异化内容(如成员查询策略)由后端 agent.ts 根据运行时 chatType 自动处理。
|
||||
*/
|
||||
|
||||
import type { PromptPreset } from '@/types/ai'
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
export type LocaleType = 'zh-CN' | 'en-US' | 'zh-TW' | 'ja-JP'
|
||||
|
||||
// ==================== 国际化内容配置 ====================
|
||||
|
||||
const i18nContent = {
|
||||
'zh-CN': {
|
||||
presetName: '默认分析助手',
|
||||
systemPrompt: `你是一个专业但风格轻松的聊天记录分析助手。
|
||||
你的任务是帮助用户理解和分析他们的聊天记录数据,同时可以适度使用 B 站/网络热梗和表情/颜文字活跃气氛,但不影响结论的准确性。
|
||||
|
||||
## 回答要求
|
||||
1. 基于工具返回的数据回答,不要编造信息
|
||||
2. 如果数据不足以回答问题,请说明
|
||||
3. 回答要简洁明了,使用 Markdown 格式
|
||||
4. 可以引用具体的发言作为证据
|
||||
5. 对于统计数据,可以适当总结趋势和特点
|
||||
6. 可以适度加入 B 站/网络热梗、表情/颜文字(强度适中)
|
||||
7. 玩梗不得影响事实准确与结论清晰,避免低俗或冒犯性表达`,
|
||||
lockedSection: {
|
||||
chatContext: {
|
||||
group: '群聊',
|
||||
private: '对话',
|
||||
},
|
||||
ownerNoteTemplate: (displayName: string, chatContext: string) =>
|
||||
`当前用户身份:
|
||||
- 用户在${chatContext}中的身份是「${displayName}」
|
||||
- 当用户提到"我"、"我的"时,指的就是「${displayName}」
|
||||
- 查询"我"的发言时,使用 sender_id 参数筛选该成员
|
||||
`,
|
||||
memberNote: {
|
||||
group: `成员查询策略:
|
||||
- 当用户提到特定群成员(如"张三说过什么"、"小明的发言"等)时,应先调用 get_members 获取成员列表
|
||||
- 群成员有三种名称:accountName(原始昵称)、groupNickname(群昵称)、aliases(用户自定义别名)
|
||||
- 通过 get_members 的 search 参数可以模糊搜索这三种名称
|
||||
- 找到成员后,使用其 id 字段作为 search_messages 的 sender_id 参数来获取该成员的发言`,
|
||||
private: `成员查询策略:
|
||||
- 私聊只有两个人,可以直接获取成员列表
|
||||
- 当用户提到"对方"、"他/她"时,通过 get_members 获取另一方信息`,
|
||||
},
|
||||
currentDatePrefix: '当前日期是',
|
||||
timeParamsTemplate: (year: number, prevYear: number) =>
|
||||
`时间参数:按用户提到的精度组合 year/month/day/hour
|
||||
- "10月" → year: ${year}, month: 10
|
||||
- "10月1号" → year: ${year}, month: 10, day: 1
|
||||
- "10月1号下午3点" → year: ${year}, month: 10, day: 1, hour: 15
|
||||
未指定年份默认${year}年,若该月份未到则用${prevYear}年`,
|
||||
conclusion: '根据用户的问题,选择合适的工具获取数据,然后基于数据给出回答。',
|
||||
},
|
||||
},
|
||||
'en-US': {
|
||||
presetName: 'Default Analysis Assistant',
|
||||
systemPrompt: `You are a professional chat analysis assistant.
|
||||
Your task is to help users understand and analyze their chat records.
|
||||
|
||||
## Response Requirements
|
||||
1. Answer based on data returned by tools, do not fabricate information
|
||||
2. If data is insufficient to answer the question, explain
|
||||
3. Keep answers concise and clear, use Markdown format
|
||||
4. Quote specific messages as evidence when possible
|
||||
5. For statistics, summarize trends and characteristics appropriately`,
|
||||
lockedSection: {
|
||||
chatContext: {
|
||||
group: 'group chat',
|
||||
private: 'conversation',
|
||||
},
|
||||
ownerNoteTemplate: (displayName: string, chatContext: string) =>
|
||||
`Current user identity:
|
||||
- The user's identity in the ${chatContext} is "${displayName}"
|
||||
- When the user mentions "I" or "my", it refers to "${displayName}"
|
||||
- When querying "my" messages, use sender_id parameter to filter by this member
|
||||
`,
|
||||
memberNote: {
|
||||
group: `Member query strategy:
|
||||
- When the user mentions a specific group member (e.g., "what did John say", "Mary's messages"), first call get_members to get the member list
|
||||
- Group members have three name types: accountName (original nickname), groupNickname (group nickname), aliases (user-defined aliases)
|
||||
- Use the search parameter of get_members to fuzzy search across all three name types
|
||||
- After finding the member, use their id field as the sender_id parameter for search_messages to get their messages`,
|
||||
private: `Member query strategy:
|
||||
- Private chats have only two people, you can directly get the member list
|
||||
- When the user mentions "the other person" or "he/she", use get_members to get the other party's information`,
|
||||
},
|
||||
currentDatePrefix: 'The current date is',
|
||||
timeParamsTemplate: (year: number, prevYear: number) =>
|
||||
`Time parameters: Combine year/month/day/hour based on user's specified precision
|
||||
- "October" → year: ${year}, month: 10
|
||||
- "October 1st" → year: ${year}, month: 10, day: 1
|
||||
- "October 1st 3pm" → year: ${year}, month: 10, day: 1, hour: 15
|
||||
Default to ${year} if year not specified, use ${prevYear} if the month hasn't arrived yet`,
|
||||
conclusion:
|
||||
"Based on the user's question, select appropriate tools to retrieve data, then provide an answer based on the data.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ==================== 预设 ID 常量 ====================
|
||||
|
||||
/** 默认预设ID */
|
||||
export const DEFAULT_PRESET_ID = 'builtin-default'
|
||||
|
||||
/** @deprecated 使用 DEFAULT_PRESET_ID 代替 */
|
||||
export const DEFAULT_GROUP_PRESET_ID = DEFAULT_PRESET_ID
|
||||
/** @deprecated 使用 DEFAULT_PRESET_ID 代替 */
|
||||
export const DEFAULT_PRIVATE_PRESET_ID = DEFAULT_PRESET_ID
|
||||
|
||||
// ==================== 默认提示词内容 ====================
|
||||
|
||||
/**
|
||||
* 获取默认系统提示词
|
||||
* @param locale 语言设置
|
||||
*/
|
||||
export function getDefaultSystemPrompt(locale: LocaleType = 'zh-CN'): string {
|
||||
const content = i18nContent[locale] || i18nContent['zh-CN']
|
||||
return content.systemPrompt
|
||||
}
|
||||
|
||||
/** @deprecated 使用 getDefaultSystemPrompt 代替 */
|
||||
export function getDefaultRoleDefinition(locale: LocaleType = 'zh-CN'): string {
|
||||
return getDefaultSystemPrompt(locale)
|
||||
}
|
||||
|
||||
/** @deprecated responseRules 已合并到 systemPrompt */
|
||||
export function getDefaultResponseRules(_locale: LocaleType = 'zh-CN'): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内置预设名称
|
||||
* @param locale 语言设置
|
||||
*/
|
||||
export function getBuiltinPresetName(locale: LocaleType = 'zh-CN'): string {
|
||||
const content = i18nContent[locale] || i18nContent['zh-CN']
|
||||
return content.presetName
|
||||
}
|
||||
|
||||
// ==================== 内置预设定义 ====================
|
||||
|
||||
/**
|
||||
* 获取内置预设列表
|
||||
* @param locale 语言设置
|
||||
*/
|
||||
export function getBuiltinPresets(locale: LocaleType = 'zh-CN'): PromptPreset[] {
|
||||
const now = Date.now()
|
||||
|
||||
const BUILTIN_DEFAULT: PromptPreset = {
|
||||
id: DEFAULT_PRESET_ID,
|
||||
name: getBuiltinPresetName(locale),
|
||||
systemPrompt: getDefaultSystemPrompt(locale),
|
||||
isBuiltIn: true,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
return [BUILTIN_DEFAULT]
|
||||
}
|
||||
|
||||
/** 所有内置预设(原始版本,用于重置)- 默认中文 */
|
||||
export const BUILTIN_PRESETS: PromptPreset[] = getBuiltinPresets('zh-CN')
|
||||
|
||||
/**
|
||||
* 获取内置预设的原始版本(用于重置)
|
||||
* @param presetId 预设ID
|
||||
* @param locale 语言设置
|
||||
*/
|
||||
export function getOriginalBuiltinPreset(presetId: string, locale: LocaleType = 'zh-CN'): PromptPreset | undefined {
|
||||
const presets = getBuiltinPresets(locale)
|
||||
return presets.find((p) => p.id === presetId)
|
||||
}
|
||||
|
||||
// ==================== 锁定部分预览(仅用于前端展示) ====================
|
||||
|
||||
/** Owner 信息(用于前端预览) */
|
||||
export interface OwnerInfoPreview {
|
||||
displayName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取锁定部分的提示词预览
|
||||
* 注意:实际执行时由主进程 agent.ts 生成,包含动态日期和差异化内容
|
||||
*
|
||||
* @param chatType 聊天类型(用于展示对应的成员策略)
|
||||
* @param ownerInfo Owner 信息(可选,用于预览时显示)
|
||||
* @param locale 语言设置
|
||||
*/
|
||||
export function getLockedPromptSectionPreview(
|
||||
chatType: 'group' | 'private' = 'group',
|
||||
ownerInfo?: OwnerInfoPreview,
|
||||
locale: LocaleType = 'zh-CN'
|
||||
): string {
|
||||
const contentKey = locale.startsWith('zh') ? 'zh-CN' : 'en-US'
|
||||
const content = i18nContent[contentKey] || i18nContent['zh-CN']
|
||||
const now = new Date()
|
||||
|
||||
const dateLocale = locale.startsWith('zh') ? 'zh-CN' : locale === 'ja-JP' ? 'ja-JP' : 'en-US'
|
||||
const currentDate = now.toLocaleDateString(dateLocale, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long',
|
||||
})
|
||||
|
||||
const chatContext = content.lockedSection.chatContext[chatType]
|
||||
const ownerNote = ownerInfo ? content.lockedSection.ownerNoteTemplate(ownerInfo.displayName, chatContext) : ''
|
||||
const memberNote = content.lockedSection.memberNote[chatType]
|
||||
const year = now.getFullYear()
|
||||
const prevYear = year - 1
|
||||
|
||||
return `${content.lockedSection.currentDatePrefix} ${currentDate}。
|
||||
${ownerNote}
|
||||
${memberNote}
|
||||
|
||||
${content.lockedSection.timeParamsTemplate(year, prevYear)}
|
||||
|
||||
${content.lockedSection.conclusion}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建完整提示词预览(用于前端展示)
|
||||
* @param systemPrompt 系统提示词
|
||||
* @param chatType 聊天类型(用于展示对应的锁定部分)
|
||||
* @param ownerInfo Owner 信息(可选)
|
||||
* @param locale 语言设置
|
||||
*/
|
||||
export function buildPromptPreview(
|
||||
systemPrompt: string,
|
||||
chatType: 'group' | 'private' = 'group',
|
||||
ownerInfo?: OwnerInfoPreview,
|
||||
locale: LocaleType = 'zh-CN'
|
||||
): string {
|
||||
const lockedSection = getLockedPromptSectionPreview(chatType, ownerInfo, locale)
|
||||
|
||||
return `${systemPrompt}
|
||||
|
||||
${lockedSection}`
|
||||
}
|
||||
@@ -61,7 +61,23 @@
|
||||
"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",
|
||||
"message_type_breakdown": "Message Type Breakdown",
|
||||
"peak_chat_hours_by_member": "Peak Hours by Member",
|
||||
"member_activity_trend": "Member Activity Trend",
|
||||
"silent_members": "Silent Members",
|
||||
"reply_interaction_ranking": "Reply Interaction Ranking",
|
||||
"mutual_interaction_pairs": "Mutual Interaction Pairs",
|
||||
"member_message_length_stats": "Message Length Stats",
|
||||
"daily_active_members": "Daily Active Members",
|
||||
"conversation_initiator_stats": "Conversation Initiators",
|
||||
"activity_heatmap": "Activity Heatmap",
|
||||
"unanswered_messages": "Unanswered Messages"
|
||||
},
|
||||
"generating": "Generating response...",
|
||||
"think": {
|
||||
@@ -408,5 +424,21 @@
|
||||
"deleteSuccess": "Skill deleted",
|
||||
"deleteFailed": "Failed to delete"
|
||||
}
|
||||
},
|
||||
"lab": {
|
||||
"basicTools": "Basic Tools",
|
||||
"toolTest": {
|
||||
"selectTool": "Select Tool",
|
||||
"selectToolPlaceholder": "Select a tool...",
|
||||
"coreTools": "Core Tools",
|
||||
"analysisTools": "Analysis Tools",
|
||||
"parameters": "Parameters",
|
||||
"arrayPlaceholder": "Separate values with commas",
|
||||
"execute": "Execute",
|
||||
"cancel": "Cancel",
|
||||
"elapsed": "Elapsed {ms}ms",
|
||||
"result": "Result",
|
||||
"truncated": "Result too large, truncated. Try reducing the limit parameter for complete data."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"aiConfig": "Chat Model",
|
||||
"aiRAG": "Vector Model",
|
||||
"aiPrompt": "Chat Config",
|
||||
"aiPreset": "Legacy Prompts",
|
||||
"aiPreprocess": "Preprocess",
|
||||
"dataManage": "Data Management",
|
||||
"storage": "Storage",
|
||||
@@ -210,6 +209,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",
|
||||
@@ -268,27 +273,6 @@
|
||||
"noDescription": "No description",
|
||||
"fetchingContent": "Loading content...",
|
||||
"fetchError": "Failed to load content"
|
||||
},
|
||||
"legacyPrompt": {
|
||||
"title": "Legacy Prompts",
|
||||
"description": "The old prompt system no longer affects AI conversations. This section is kept only for viewing and copying legacy configs during the transition.",
|
||||
"copyJson": "Copy JSON",
|
||||
"emptyTitle": "No legacy prompt data found",
|
||||
"emptyDescription": "There is no legacy prompt config available to view on this device.",
|
||||
"activePreset": "Previously Active Preset",
|
||||
"notConfigured": "Not recorded",
|
||||
"customPresetCount": "Custom Presets",
|
||||
"remotePresetCount": "Imported Remote Presets",
|
||||
"parseError": "Failed to parse legacy prompt data, but you can still copy the raw JSON first.",
|
||||
"customPresetList": "Custom Presets",
|
||||
"noPromptContent": "No prompt content recorded",
|
||||
"rawJsonTitle": "Raw Config JSON",
|
||||
"rawJsonHint": "View and copy only",
|
||||
"copySuccess": "JSON copied to clipboard",
|
||||
"copyFailed": "Copy failed",
|
||||
"groupOnly": "Group Only",
|
||||
"privateOnly": "Private Only",
|
||||
"common": "Common"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
|
||||
@@ -61,7 +61,23 @@
|
||||
"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": "キーワード頻度分析",
|
||||
"message_type_breakdown": "メッセージ種別分布",
|
||||
"peak_chat_hours_by_member": "メンバー活動時間帯",
|
||||
"member_activity_trend": "メンバー活動傾向",
|
||||
"silent_members": "休眠メンバー検出",
|
||||
"reply_interaction_ranking": "返信インタラクションランキング",
|
||||
"mutual_interaction_pairs": "頻繁なインタラクションペア",
|
||||
"member_message_length_stats": "メッセージ長統計",
|
||||
"daily_active_members": "日別アクティブ人数",
|
||||
"conversation_initiator_stats": "話題開始者統計",
|
||||
"activity_heatmap": "活動ヒートマップ",
|
||||
"unanswered_messages": "未返信メッセージ"
|
||||
},
|
||||
"generating": "回答を作成中...",
|
||||
"think": {
|
||||
@@ -372,5 +388,21 @@
|
||||
"active": {
|
||||
"label": "手動呼び出しスキル:{name}"
|
||||
}
|
||||
},
|
||||
"lab": {
|
||||
"basicTools": "基本ツール",
|
||||
"toolTest": {
|
||||
"selectTool": "ツールを選択",
|
||||
"selectToolPlaceholder": "ツールを選択してください...",
|
||||
"coreTools": "コアツール",
|
||||
"analysisTools": "分析ツール",
|
||||
"parameters": "パラメータ",
|
||||
"arrayPlaceholder": "カンマで区切って入力",
|
||||
"execute": "実行",
|
||||
"cancel": "キャンセル",
|
||||
"elapsed": "所要時間 {ms}ms",
|
||||
"result": "実行結果",
|
||||
"truncated": "結果が大きすぎるため、切り捨てて表示しています。limit パラメータを小さくしてください。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"aiConfig": "チャットモデル",
|
||||
"aiRAG": "埋め込みモデル",
|
||||
"aiPrompt": "チャット設定",
|
||||
"aiPreset": "旧版プロンプト",
|
||||
"aiPreprocess": "前処理",
|
||||
"dataManage": "データ管理",
|
||||
"storage": "ストレージ管理",
|
||||
@@ -210,6 +209,12 @@
|
||||
"title": "AI コンテキスト制限",
|
||||
"description": "会話ごとに保持する直近のやり取り数です(1 往復 = ユーザーの質問 + AI の回答)。文脈が長くなりすぎて Token を消費するのを防ぎます"
|
||||
},
|
||||
"searchContext": {
|
||||
"title": "検索コンテキストウィンドウ",
|
||||
"description": "検索ヒット時に前後の会話コンテキストを自動的に含めることで、AI がメッセージの背景を理解しやすくなります。0 に設定するとコンテキストなし",
|
||||
"before": "前",
|
||||
"after": "後"
|
||||
},
|
||||
"exportFormat": {
|
||||
"title": "会話エクスポート形式",
|
||||
"description": "AI チャットをエクスポートする際のファイル形式",
|
||||
@@ -268,27 +273,6 @@
|
||||
"noDescription": "説明がありません",
|
||||
"fetchingContent": "コンテンツを読み込み中...",
|
||||
"fetchError": "コンテンツの読み込みに失敗しました"
|
||||
},
|
||||
"legacyPrompt": {
|
||||
"title": "旧版プロンプト",
|
||||
"description": "旧プロンプトシステムはすでに AI 会話の実行には使われません。移行期間中に旧設定を確認・コピーするためだけの入口です。",
|
||||
"copyJson": "JSON をコピー",
|
||||
"emptyTitle": "旧版プロンプトデータが見つかりません",
|
||||
"emptyDescription": "この端末には確認できる旧版プロンプト設定がありません。",
|
||||
"activePreset": "以前の有効プリセット",
|
||||
"notConfigured": "記録なし",
|
||||
"customPresetCount": "カスタムプリセット数",
|
||||
"remotePresetCount": "リモート導入数",
|
||||
"parseError": "旧版プロンプトデータの解析に失敗しましたが、先に生の JSON をコピーできます。",
|
||||
"customPresetList": "カスタムプリセット",
|
||||
"noPromptContent": "プロンプト内容の記録がありません",
|
||||
"rawJsonTitle": "元の設定 JSON",
|
||||
"rawJsonHint": "閲覧とコピー専用",
|
||||
"copySuccess": "JSON をクリップボードにコピーしました",
|
||||
"copyFailed": "コピーに失敗しました",
|
||||
"groupOnly": "グループのみ",
|
||||
"privateOnly": "個人のみ",
|
||||
"common": "共通"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
|
||||
@@ -61,7 +61,23 @@
|
||||
"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": "关键词频率分析",
|
||||
"message_type_breakdown": "消息类型分布",
|
||||
"peak_chat_hours_by_member": "成员活跃时段",
|
||||
"member_activity_trend": "成员活跃趋势",
|
||||
"silent_members": "沉默成员检测",
|
||||
"reply_interaction_ranking": "回复互动排行",
|
||||
"mutual_interaction_pairs": "互动频繁成员对",
|
||||
"member_message_length_stats": "消息长度统计",
|
||||
"daily_active_members": "每日活跃人数",
|
||||
"conversation_initiator_stats": "话题发起者统计",
|
||||
"activity_heatmap": "活跃度热力图",
|
||||
"unanswered_messages": "未回复消息"
|
||||
},
|
||||
"generating": "正在生成回复...",
|
||||
"think": {
|
||||
@@ -408,5 +424,21 @@
|
||||
"deleteSuccess": "技能已删除",
|
||||
"deleteFailed": "删除失败"
|
||||
}
|
||||
},
|
||||
"lab": {
|
||||
"basicTools": "基础工具",
|
||||
"toolTest": {
|
||||
"selectTool": "选择工具",
|
||||
"selectToolPlaceholder": "请选择一个工具...",
|
||||
"coreTools": "核心工具",
|
||||
"analysisTools": "分析工具",
|
||||
"parameters": "参数",
|
||||
"arrayPlaceholder": "多个值用逗号分隔",
|
||||
"execute": "执行",
|
||||
"cancel": "取消",
|
||||
"elapsed": "耗时 {ms}ms",
|
||||
"result": "执行结果",
|
||||
"truncated": "结果过大,已截断显示。建议减小 limit 参数以获取完整数据。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"aiConfig": "对话模型",
|
||||
"aiRAG": "向量模型",
|
||||
"aiPrompt": "对话配置",
|
||||
"aiPreset": "旧版提示词",
|
||||
"aiPreprocess": "预处理",
|
||||
"dataManage": "数据管理",
|
||||
"storage": "存储管理",
|
||||
@@ -210,6 +209,12 @@
|
||||
"title": "AI上下文限制",
|
||||
"description": "每次对话保留最近的对话轮数(1轮 = 用户提问 + AI回复),防止上下文过长消耗 Token"
|
||||
},
|
||||
"searchContext": {
|
||||
"title": "搜索上下文窗口",
|
||||
"description": "搜索命中消息时自动携带前后的对话上下文,帮助 AI 理解消息背景。设为 0 则不携带上下文",
|
||||
"before": "前",
|
||||
"after": "后"
|
||||
},
|
||||
"exportFormat": {
|
||||
"title": "对话导出格式",
|
||||
"description": "导出 AI 对话时使用的文件格式",
|
||||
@@ -268,27 +273,6 @@
|
||||
"noDescription": "暂无描述",
|
||||
"fetchingContent": "正在加载内容...",
|
||||
"fetchError": "加载内容失败"
|
||||
},
|
||||
"legacyPrompt": {
|
||||
"title": "旧版提示词",
|
||||
"description": "提示词系统已不再参与 AI 对话运行。这里仅保留旧配置的查看与复制入口,方便你在过渡期手动迁移。",
|
||||
"copyJson": "复制 JSON",
|
||||
"emptyTitle": "未发现旧版提示词数据",
|
||||
"emptyDescription": "当前设备上没有可查看的旧版提示词配置。",
|
||||
"activePreset": "历史激活预设",
|
||||
"notConfigured": "未记录",
|
||||
"customPresetCount": "自定义预设数",
|
||||
"remotePresetCount": "远程导入数",
|
||||
"parseError": "旧版提示词数据解析失败,但你仍然可以先复制原始 JSON。",
|
||||
"customPresetList": "自定义预设",
|
||||
"noPromptContent": "未记录提示词内容",
|
||||
"rawJsonTitle": "原始配置 JSON",
|
||||
"rawJsonHint": "仅供查看与复制",
|
||||
"copySuccess": "JSON 已复制到剪贴板",
|
||||
"copyFailed": "复制失败",
|
||||
"groupOnly": "仅群聊",
|
||||
"privateOnly": "仅私聊",
|
||||
"common": "通用"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
|
||||
@@ -61,7 +61,23 @@
|
||||
"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": "關鍵詞頻率分析",
|
||||
"message_type_breakdown": "訊息類型分佈",
|
||||
"peak_chat_hours_by_member": "成員活躍時段",
|
||||
"member_activity_trend": "成員活躍趨勢",
|
||||
"silent_members": "沉默成員偵測",
|
||||
"reply_interaction_ranking": "回覆互動排行",
|
||||
"mutual_interaction_pairs": "互動頻繁成員對",
|
||||
"member_message_length_stats": "訊息長度統計",
|
||||
"daily_active_members": "每日活躍人數",
|
||||
"conversation_initiator_stats": "話題發起者統計",
|
||||
"activity_heatmap": "活躍度熱力圖",
|
||||
"unanswered_messages": "未回覆訊息"
|
||||
},
|
||||
"generating": "正在產生回覆...",
|
||||
"think": {
|
||||
@@ -372,5 +388,21 @@
|
||||
"active": {
|
||||
"label": "主動呼叫技能:{name}"
|
||||
}
|
||||
},
|
||||
"lab": {
|
||||
"basicTools": "基礎工具",
|
||||
"toolTest": {
|
||||
"selectTool": "選擇工具",
|
||||
"selectToolPlaceholder": "請選擇一個工具...",
|
||||
"coreTools": "核心工具",
|
||||
"analysisTools": "分析工具",
|
||||
"parameters": "參數",
|
||||
"arrayPlaceholder": "多個值用逗號分隔",
|
||||
"execute": "執行",
|
||||
"cancel": "取消",
|
||||
"elapsed": "耗時 {ms}ms",
|
||||
"result": "執行結果",
|
||||
"truncated": "結果過大,已截斷顯示。建議減小 limit 參數以獲取完整資料。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"aiConfig": "對話模型",
|
||||
"aiRAG": "向量模型",
|
||||
"aiPrompt": "聊天設定",
|
||||
"aiPreset": "舊版提示詞",
|
||||
"aiPreprocess": "前處理",
|
||||
"dataManage": "資料管理",
|
||||
"storage": "儲存管理",
|
||||
@@ -210,6 +209,12 @@
|
||||
"title": "AI 上下文限制",
|
||||
"description": "每次對話只保留最近幾輪內容(1 輪 = 使用者提問 + AI 回覆),避免上下文過長而消耗過多 Token"
|
||||
},
|
||||
"searchContext": {
|
||||
"title": "搜尋上下文視窗",
|
||||
"description": "搜尋命中訊息時自動攜帶前後的對話上下文,幫助 AI 理解訊息背景。設為 0 則不攜帶上下文",
|
||||
"before": "前",
|
||||
"after": "後"
|
||||
},
|
||||
"exportFormat": {
|
||||
"title": "對話匯出格式",
|
||||
"description": "匯出 AI 對話時使用的檔案格式",
|
||||
@@ -268,27 +273,6 @@
|
||||
"noDescription": "暫無描述",
|
||||
"fetchingContent": "正在載入內容...",
|
||||
"fetchError": "載入內容失敗"
|
||||
},
|
||||
"legacyPrompt": {
|
||||
"title": "舊版提示詞",
|
||||
"description": "提示詞系統已不再參與 AI 對話執行。這裡僅保留舊設定的查看與複製入口,方便你在過渡期手動遷移。",
|
||||
"copyJson": "複製 JSON",
|
||||
"emptyTitle": "未發現舊版提示詞資料",
|
||||
"emptyDescription": "目前這台裝置上沒有可查看的舊版提示詞設定。",
|
||||
"activePreset": "歷史啟用預設",
|
||||
"notConfigured": "未記錄",
|
||||
"customPresetCount": "自訂預設數",
|
||||
"remotePresetCount": "遠端匯入數",
|
||||
"parseError": "舊版提示詞資料解析失敗,但你仍可先複製原始 JSON。",
|
||||
"customPresetList": "自訂預設",
|
||||
"noPromptContent": "未記錄提示詞內容",
|
||||
"rawJsonTitle": "原始設定 JSON",
|
||||
"rawJsonHint": "僅供查看與複製",
|
||||
"copySuccess": "JSON 已複製到剪貼簿",
|
||||
"copyFailed": "複製失敗",
|
||||
"groupOnly": "僅群聊",
|
||||
"privateOnly": "僅私聊",
|
||||
"common": "通用"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
|
||||
interface LegacyPromptStoreData {
|
||||
customPromptPresets?: Array<{
|
||||
id?: string
|
||||
name?: string
|
||||
systemPrompt?: string
|
||||
applicableTo?: 'common' | 'group' | 'private'
|
||||
}>
|
||||
builtinPresetOverrides?: Record<string, { name?: string; systemPrompt?: string }>
|
||||
fetchedRemotePresetIds?: string[]
|
||||
aiPromptSettings?: { activePresetId?: string }
|
||||
activeGroupPresetId?: string
|
||||
activePrivatePresetId?: string
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const rawPromptStore = ref<LegacyPromptStoreData | null>(null)
|
||||
const rawPromptText = ref('')
|
||||
const parseError = ref('')
|
||||
|
||||
/**
|
||||
* 旧版提示词已经不再参与运行,这里只保留原始数据查看与复制能力。
|
||||
*/
|
||||
function loadLegacyPromptStore() {
|
||||
const raw = localStorage.getItem('prompt')
|
||||
rawPromptText.value = raw || ''
|
||||
parseError.value = ''
|
||||
rawPromptStore.value = null
|
||||
|
||||
if (!raw) return
|
||||
|
||||
try {
|
||||
rawPromptStore.value = JSON.parse(raw) as LegacyPromptStoreData
|
||||
} catch (error) {
|
||||
parseError.value = String(error)
|
||||
}
|
||||
}
|
||||
|
||||
const hasLegacyPromptStore = computed(() => rawPromptText.value.trim().length > 0)
|
||||
|
||||
const customPromptPresets = computed(() => {
|
||||
return Array.isArray(rawPromptStore.value?.customPromptPresets) ? rawPromptStore.value!.customPromptPresets : []
|
||||
})
|
||||
|
||||
const remotePresetIds = computed(() => {
|
||||
return Array.isArray(rawPromptStore.value?.fetchedRemotePresetIds) ? rawPromptStore.value!.fetchedRemotePresetIds : []
|
||||
})
|
||||
|
||||
const activePresetId = computed(() => {
|
||||
const settings = rawPromptStore.value?.aiPromptSettings
|
||||
return (
|
||||
settings?.activePresetId ||
|
||||
rawPromptStore.value?.activeGroupPresetId ||
|
||||
rawPromptStore.value?.activePrivatePresetId ||
|
||||
''
|
||||
)
|
||||
})
|
||||
|
||||
const formattedPromptStoreJson = computed(() => {
|
||||
if (!rawPromptText.value) return ''
|
||||
if (!rawPromptStore.value) return rawPromptText.value
|
||||
return JSON.stringify(rawPromptStore.value, null, 2)
|
||||
})
|
||||
|
||||
function getApplicableLabel(applicableTo?: 'common' | 'group' | 'private'): string {
|
||||
if (applicableTo === 'group') return t('settings.aiPrompt.legacyPrompt.groupOnly')
|
||||
if (applicableTo === 'private') return t('settings.aiPrompt.legacyPrompt.privateOnly')
|
||||
return t('settings.aiPrompt.legacyPrompt.common')
|
||||
}
|
||||
|
||||
async function handleCopyJson() {
|
||||
if (!formattedPromptStoreJson.value) return
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(formattedPromptStoreJson.value)
|
||||
toast.success(t('settings.aiPrompt.legacyPrompt.copySuccess'))
|
||||
} catch (error) {
|
||||
toast.fail(t('settings.aiPrompt.legacyPrompt.copyFailed'), { description: String(error) })
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadLegacyPromptStore()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<div class="rounded-xl border border-amber-200 bg-amber-50/80 p-4 dark:border-amber-900/60 dark:bg-amber-950/20">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-amber-100 text-amber-600 dark:bg-amber-900/40 dark:text-amber-300"
|
||||
>
|
||||
<UIcon name="i-heroicons-archive-box" class="h-4.5 w-4.5" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h4 class="text-sm font-semibold text-amber-900 dark:text-amber-100">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.title') }}
|
||||
</h4>
|
||||
<p class="mt-1 text-sm leading-6 text-amber-800 dark:text-amber-200">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.description') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<UButton color="primary" size="xs" :disabled="!hasLegacyPromptStore" @click="handleCopyJson">
|
||||
<UIcon name="i-heroicons-document-duplicate" class="mr-1 h-3.5 w-3.5" />
|
||||
{{ t('settings.aiPrompt.legacyPrompt.copyJson') }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!hasLegacyPromptStore"
|
||||
class="rounded-xl border border-gray-200 bg-gray-50 p-5 dark:border-gray-700 dark:bg-gray-800/50"
|
||||
>
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.emptyTitle') }}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.emptyDescription') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- 这里只保留最必要的旧数据摘要,避免遗留信息继续分散注意力。 -->
|
||||
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('settings.aiPrompt.legacyPrompt.activePreset') }}</p>
|
||||
<p class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ activePresetId || t('settings.aiPrompt.legacyPrompt.notConfigured') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.customPresetCount') }}
|
||||
</p>
|
||||
<p class="mt-2 text-sm font-medium text-gray-900 dark:text-white">{{ customPromptPresets.length }}</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.remotePresetCount') }}
|
||||
</p>
|
||||
<p class="mt-2 text-sm font-medium text-gray-900 dark:text-white">{{ remotePresetIds.length }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="parseError"
|
||||
class="rounded-xl border border-red-200 bg-red-50 p-4 dark:border-red-900/60 dark:bg-red-950/20"
|
||||
>
|
||||
<p class="text-sm font-medium text-red-700 dark:text-red-300">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.parseError') }}
|
||||
</p>
|
||||
<p class="mt-1 break-all text-xs text-red-600 dark:text-red-400">{{ parseError }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="customPromptPresets.length > 0" class="space-y-3">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.customPresetList') }}
|
||||
</h4>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="preset in customPromptPresets"
|
||||
:key="preset.id || preset.name || preset.systemPrompt"
|
||||
class="rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-900"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ preset.name || '-' }}</p>
|
||||
<UBadge color="gray" variant="soft" size="xs">
|
||||
{{ getApplicableLabel(preset.applicableTo) }}
|
||||
</UBadge>
|
||||
</div>
|
||||
<p class="mt-2 line-clamp-3 whitespace-pre-wrap text-xs leading-6 text-gray-500 dark:text-gray-400">
|
||||
{{ preset.systemPrompt || t('settings.aiPrompt.legacyPrompt.noPromptContent') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.rawJsonTitle') }}
|
||||
</h4>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">
|
||||
{{ t('settings.aiPrompt.legacyPrompt.rawJsonHint') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="rounded-xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/70">
|
||||
<pre
|
||||
class="max-h-[360px] overflow-auto whitespace-pre-wrap break-all text-xs leading-6 text-gray-600 dark:text-gray-300"
|
||||
>{{ formattedPromptStoreJson }}</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,7 +3,6 @@ import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import AIModelConfigTab from './AI/AIModelConfigTab.vue'
|
||||
import AIPromptConfigTab from './AI/AIPromptConfigTab.vue'
|
||||
import AIPromptPresetTab from './AI/AIPromptPresetTab.vue'
|
||||
import AIPreprocessTab from './AI/AIPreprocessTab.vue'
|
||||
// TODO: 向量模型暂时隐藏,待功能完善后恢复
|
||||
// import RAGConfigTab from './AI/RAGConfigTab.vue'
|
||||
@@ -24,7 +23,6 @@ const navItems = computed(() => [
|
||||
// { id: 'rag', label: t('settings.tabs.aiRAG') },
|
||||
{ id: 'chat', label: t('settings.tabs.aiPrompt') },
|
||||
{ id: 'preprocess', label: t('settings.tabs.aiPreprocess') },
|
||||
{ id: 'preset', label: t('settings.tabs.aiPreset') },
|
||||
])
|
||||
|
||||
// 使用二级导航滚动联动 composable
|
||||
@@ -91,14 +89,6 @@ void aiModelConfigRef.value
|
||||
<div :ref="(el) => setSectionRef('preprocess', el as HTMLElement)">
|
||||
<AIPreprocessTab />
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="border-t border-gray-200 dark:border-gray-700" />
|
||||
|
||||
<!-- 旧版提示词查看 -->
|
||||
<div :ref="(el) => setSectionRef('preset', el as HTMLElement)">
|
||||
<AIPromptPresetTab />
|
||||
</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(
|
||||
|
||||
@@ -1,44 +1,13 @@
|
||||
import { defineStore, storeToRefs } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { PromptPreset, AIPromptSettings } from '@/types/ai'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { KeywordTemplate } from '@/types/analysis'
|
||||
import { DEFAULT_PRESET_ID, getBuiltinPresets, getOriginalBuiltinPreset, type LocaleType } from '@/config/prompts'
|
||||
import { useSettingsStore } from './settings'
|
||||
|
||||
// 远程预设配置 URL 基础地址
|
||||
const REMOTE_PRESET_BASE_URL = 'https://chatlab.fun'
|
||||
|
||||
/**
|
||||
* 远程预设的原始数据结构(从 JSON 获取)
|
||||
*/
|
||||
export interface RemotePresetData {
|
||||
id: string
|
||||
name: string
|
||||
/** Markdown 文件绝对路径(如 /cn/system-prompt/xxx.md) */
|
||||
path: string
|
||||
/** 简短描述(索引中提供,用于列表展示) */
|
||||
description?: string
|
||||
/** 系统提示词(从 Markdown 文件解析后填充) */
|
||||
systemPrompt?: string
|
||||
/** 适用场景:common(通用)、group(仅群聊)、private(仅私聊) */
|
||||
chatType?: 'common' | 'group' | 'private'
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 配置、提示词和关键词模板相关的全局状态
|
||||
* AI 配置与关键词模板相关的全局状态
|
||||
*/
|
||||
export const usePromptStore = defineStore(
|
||||
'prompt',
|
||||
() => {
|
||||
// 获取当前语言设置
|
||||
const settingsStore = useSettingsStore()
|
||||
const { locale } = storeToRefs(settingsStore)
|
||||
|
||||
const customPromptPresets = ref<PromptPreset[]>([])
|
||||
const builtinPresetOverrides = ref<Record<string, { name?: string; systemPrompt?: string; updatedAt?: number }>>({})
|
||||
const aiPromptSettings = ref<AIPromptSettings>({
|
||||
activePresetId: DEFAULT_PRESET_ID,
|
||||
})
|
||||
const aiConfigVersion = ref(0)
|
||||
const aiGlobalSettings = ref({
|
||||
maxMessagesPerRequest: 1000,
|
||||
@@ -46,47 +15,11 @@ 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[]>([])
|
||||
/** 已同步的远程预设 ID 列表(避免重复添加) */
|
||||
const fetchedRemotePresetIds = ref<string[]>([])
|
||||
|
||||
/** 当前语言的内置预设列表(响应式) */
|
||||
const builtinPresets = computed(() => getBuiltinPresets(locale.value as LocaleType))
|
||||
|
||||
/** 获取所有提示词预设(内置 + 覆盖 + 自定义) */
|
||||
const allPromptPresets = computed(() => {
|
||||
const mergedBuiltins = builtinPresets.value.map((preset) => {
|
||||
const override = builtinPresetOverrides.value[preset.id]
|
||||
if (override) {
|
||||
return { ...preset, ...override }
|
||||
}
|
||||
return preset
|
||||
})
|
||||
return [...mergedBuiltins, ...customPromptPresets.value]
|
||||
})
|
||||
|
||||
/** 当前激活的预设 */
|
||||
const activePreset = computed(() => {
|
||||
const preset = allPromptPresets.value.find((p) => p.id === aiPromptSettings.value.activePresetId)
|
||||
return preset || builtinPresets.value.find((p) => p.id === DEFAULT_PRESET_ID)!
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取适用于指定聊天类型的预设列表
|
||||
* @param chatType 聊天类型
|
||||
*/
|
||||
function getPresetsForChatType(chatType: 'group' | 'private'): PromptPreset[] {
|
||||
return allPromptPresets.value.filter((preset) => {
|
||||
// 内置预设始终适用
|
||||
if (preset.isBuiltIn) return true
|
||||
// 未设置 applicableTo 或 common 适用于所有类型
|
||||
if (!preset.applicableTo || preset.applicableTo === 'common') return true
|
||||
// 检查是否匹配当前类型
|
||||
return preset.applicableTo === chatType
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知外部 AI 配置已经被修改
|
||||
@@ -105,6 +38,8 @@ export const usePromptStore = defineStore(
|
||||
exportFormat: 'markdown' | 'txt'
|
||||
sqlExportFormat: 'csv' | 'json'
|
||||
enableAutoSkill: boolean
|
||||
searchContextBefore: number
|
||||
searchContextAfter: number
|
||||
}>
|
||||
) {
|
||||
aiGlobalSettings.value = { ...aiGlobalSettings.value, ...settings }
|
||||
@@ -150,306 +85,12 @@ export const usePromptStore = defineStore(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新的提示词预设
|
||||
*/
|
||||
function addPromptPreset(preset: {
|
||||
name: string
|
||||
systemPrompt: string
|
||||
applicableTo?: 'common' | 'group' | 'private'
|
||||
}) {
|
||||
const newPreset: PromptPreset = {
|
||||
id: `custom-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
name: preset.name,
|
||||
systemPrompt: preset.systemPrompt,
|
||||
isBuiltIn: false,
|
||||
applicableTo: preset.applicableTo || 'common',
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
customPromptPresets.value.push(newPreset)
|
||||
return newPreset.id
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新提示词预设(含内置覆盖)
|
||||
*/
|
||||
function updatePromptPreset(
|
||||
presetId: string,
|
||||
updates: {
|
||||
name?: string
|
||||
systemPrompt?: string
|
||||
applicableTo?: 'common' | 'group' | 'private'
|
||||
}
|
||||
) {
|
||||
const isBuiltin = builtinPresets.value.some((p) => p.id === presetId)
|
||||
if (isBuiltin) {
|
||||
builtinPresetOverrides.value[presetId] = {
|
||||
...builtinPresetOverrides.value[presetId],
|
||||
name: updates.name,
|
||||
systemPrompt: updates.systemPrompt,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const index = customPromptPresets.value.findIndex((p) => p.id === presetId)
|
||||
if (index !== -1) {
|
||||
customPromptPresets.value[index] = {
|
||||
...customPromptPresets.value[index],
|
||||
...updates,
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置内置预设为初始状态
|
||||
*/
|
||||
function resetBuiltinPreset(presetId: string): boolean {
|
||||
const original = getOriginalBuiltinPreset(presetId, locale.value as LocaleType)
|
||||
if (!original) return false
|
||||
delete builtinPresetOverrides.value[presetId]
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断内置预设是否被自定义过
|
||||
*/
|
||||
function isBuiltinPresetModified(presetId: string): boolean {
|
||||
return !!builtinPresetOverrides.value[presetId]
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除提示词预设(自定义)
|
||||
*/
|
||||
function removePromptPreset(presetId: string) {
|
||||
const index = customPromptPresets.value.findIndex((p) => p.id === presetId)
|
||||
if (index !== -1) {
|
||||
customPromptPresets.value.splice(index, 1)
|
||||
// 如果删除的是当前激活的预设,切换回默认
|
||||
if (aiPromptSettings.value.activePresetId === presetId) {
|
||||
aiPromptSettings.value.activePresetId = DEFAULT_PRESET_ID
|
||||
}
|
||||
// 如果是从远程导入的预设,同时从已导入列表中移除,以便用户可以重新导入
|
||||
const remoteIndex = fetchedRemotePresetIds.value.indexOf(presetId)
|
||||
if (remoteIndex !== -1) {
|
||||
fetchedRemotePresetIds.value.splice(remoteIndex, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制指定提示词预设
|
||||
*/
|
||||
function duplicatePromptPreset(presetId: string) {
|
||||
const source = allPromptPresets.value.find((p) => p.id === presetId)
|
||||
if (source) {
|
||||
const copySuffix = locale.value.startsWith('zh') ? '(副本)' : '(Copy)'
|
||||
return addPromptPreset({
|
||||
name: `${source.name} ${copySuffix}`,
|
||||
systemPrompt: source.systemPrompt,
|
||||
})
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前激活的预设
|
||||
*/
|
||||
function setActivePreset(presetId: string) {
|
||||
const preset = allPromptPresets.value.find((p) => p.id === presetId)
|
||||
if (preset) {
|
||||
aiPromptSettings.value.activePresetId = presetId
|
||||
notifyAIConfigChanged()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前激活的预设
|
||||
* @param _chatType 已弃用,保留参数兼容旧代码
|
||||
*/
|
||||
function getActivePresetForChatType(_chatType?: 'group' | 'private'): PromptPreset {
|
||||
return activePreset.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Markdown 文件内容为完整的系统提示词
|
||||
* 旧格式使用 `---` 分隔角色定义和回答要求,现统一为单一字段
|
||||
*/
|
||||
function parseMarkdownContent(content: string): { systemPrompt: string } {
|
||||
return { systemPrompt: content.trim() }
|
||||
}
|
||||
|
||||
/**
|
||||
* 从远程获取预设索引列表(不下载 Markdown 内容,节省流量)
|
||||
* @param locale 当前语言设置 (如 'zh-CN', 'en-US')
|
||||
* @returns 远程预设索引列表,获取失败返回空数组
|
||||
*/
|
||||
async function fetchRemotePresets(locale: string): Promise<RemotePresetData[]> {
|
||||
const langPathMap: Record<string, string> = { 'zh-CN': 'cn', 'zh-TW': 'tw', 'en-US': 'en', 'ja-JP': 'ja' }
|
||||
const langPath = langPathMap[locale] ?? 'en'
|
||||
const indexUrl = `${REMOTE_PRESET_BASE_URL}/${langPath}/system-prompt.json`
|
||||
|
||||
try {
|
||||
const result = await window.api.app.fetchRemoteConfig(indexUrl)
|
||||
if (!result.success || !result.data) {
|
||||
return []
|
||||
}
|
||||
|
||||
const presetIndex = result.data as RemotePresetData[]
|
||||
if (!Array.isArray(presetIndex)) {
|
||||
return []
|
||||
}
|
||||
|
||||
// 过滤有效的索引项(必须有 id、name、path)
|
||||
return presetIndex.filter((p) => p.id && p.name && p.path)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按需下载单个预设的 Markdown 内容
|
||||
* @param preset 预设索引数据
|
||||
* @returns 包含完整内容的预设数据,失败返回 null
|
||||
*/
|
||||
async function fetchPresetContent(
|
||||
preset: RemotePresetData
|
||||
): Promise<(RemotePresetData & { systemPrompt: string }) | null> {
|
||||
if (preset.systemPrompt) {
|
||||
return preset as RemotePresetData & { systemPrompt: string }
|
||||
}
|
||||
|
||||
const mdUrl = `${REMOTE_PRESET_BASE_URL}${preset.path}`
|
||||
try {
|
||||
const mdResult = await window.api.app.fetchRemoteConfig(mdUrl)
|
||||
if (!mdResult.success || typeof mdResult.data !== 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
const { systemPrompt } = parseMarkdownContent(mdResult.data)
|
||||
if (!systemPrompt) {
|
||||
return null
|
||||
}
|
||||
|
||||
return { ...preset, systemPrompt }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加远程预设到自定义预设列表
|
||||
* @param preset 远程预设数据
|
||||
* @returns 是否添加成功
|
||||
*/
|
||||
function addRemotePreset(preset: RemotePresetData): boolean {
|
||||
if (fetchedRemotePresetIds.value.includes(preset.id)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const applicableTo = preset.chatType || 'common'
|
||||
|
||||
const newPreset: PromptPreset = {
|
||||
id: preset.id,
|
||||
name: preset.name,
|
||||
systemPrompt: preset.systemPrompt || '',
|
||||
isBuiltIn: false,
|
||||
applicableTo,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
customPromptPresets.value.push(newPreset)
|
||||
fetchedRemotePresetIds.value.push(preset.id)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断远程预设是否已添加
|
||||
* @param presetId 预设 ID
|
||||
*/
|
||||
function isRemotePresetAdded(presetId: string): boolean {
|
||||
return fetchedRemotePresetIds.value.includes(presetId)
|
||||
}
|
||||
|
||||
// ==================== 数据迁移(兼容旧版本) ====================
|
||||
|
||||
/**
|
||||
* 迁移旧版本的预设数据
|
||||
* 将群聊/私聊分离的预设合并为统一预设
|
||||
*/
|
||||
function migrateOldPresets() {
|
||||
// 检查是否存在旧版本数据结构
|
||||
const oldSettings = aiPromptSettings.value as unknown as {
|
||||
activeGroupPresetId?: string
|
||||
activePrivatePresetId?: string
|
||||
activePresetId?: string
|
||||
}
|
||||
|
||||
// 如果存在旧字段,进行迁移
|
||||
if (oldSettings.activeGroupPresetId && !oldSettings.activePresetId) {
|
||||
const oldGroupId = oldSettings.activeGroupPresetId
|
||||
if (oldGroupId === 'builtin-group-default' || oldGroupId === 'builtin-private-default') {
|
||||
aiPromptSettings.value.activePresetId = DEFAULT_PRESET_ID
|
||||
} else {
|
||||
aiPromptSettings.value.activePresetId = oldGroupId
|
||||
}
|
||||
delete (aiPromptSettings.value as Record<string, unknown>).activeGroupPresetId
|
||||
delete (aiPromptSettings.value as Record<string, unknown>).activePrivatePresetId
|
||||
}
|
||||
|
||||
for (const preset of customPromptPresets.value) {
|
||||
const oldPreset = preset as PromptPreset & { chatType?: string }
|
||||
if (oldPreset.chatType) {
|
||||
delete oldPreset.chatType
|
||||
}
|
||||
}
|
||||
|
||||
// 迁移旧 roleDefinition + responseRules → systemPrompt
|
||||
for (const preset of customPromptPresets.value) {
|
||||
const legacy = preset as unknown as { roleDefinition?: string; responseRules?: string; systemPrompt?: string }
|
||||
if (legacy.roleDefinition && !legacy.systemPrompt) {
|
||||
preset.systemPrompt = legacy.responseRules
|
||||
? `${legacy.roleDefinition}\n\n## 回答要求\n${legacy.responseRules}`
|
||||
: legacy.roleDefinition
|
||||
delete (preset as Record<string, unknown>).roleDefinition
|
||||
delete (preset as Record<string, unknown>).responseRules
|
||||
}
|
||||
}
|
||||
|
||||
// 迁移 builtinPresetOverrides 中的旧字段
|
||||
for (const [id, override] of Object.entries(builtinPresetOverrides.value)) {
|
||||
const legacy = override as unknown as { roleDefinition?: string; responseRules?: string; systemPrompt?: string }
|
||||
if (legacy.roleDefinition && !legacy.systemPrompt) {
|
||||
override.systemPrompt = legacy.responseRules
|
||||
? `${legacy.roleDefinition}\n\n## 回答要求\n${legacy.responseRules}`
|
||||
: legacy.roleDefinition
|
||||
delete (override as Record<string, unknown>).roleDefinition
|
||||
delete (override as Record<string, unknown>).responseRules
|
||||
builtinPresetOverrides.value[id] = override
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时执行迁移
|
||||
migrateOldPresets()
|
||||
|
||||
return {
|
||||
// state
|
||||
customPromptPresets,
|
||||
builtinPresetOverrides,
|
||||
aiPromptSettings,
|
||||
aiConfigVersion,
|
||||
aiGlobalSettings,
|
||||
customKeywordTemplates,
|
||||
deletedPresetTemplateIds,
|
||||
fetchedRemotePresetIds,
|
||||
// getters
|
||||
allPromptPresets,
|
||||
activePreset,
|
||||
// actions
|
||||
notifyAIConfigChanged,
|
||||
updateAIGlobalSettings,
|
||||
@@ -457,33 +98,12 @@ export const usePromptStore = defineStore(
|
||||
updateCustomKeywordTemplate,
|
||||
removeCustomKeywordTemplate,
|
||||
addDeletedPresetTemplateId,
|
||||
addPromptPreset,
|
||||
updatePromptPreset,
|
||||
resetBuiltinPreset,
|
||||
isBuiltinPresetModified,
|
||||
removePromptPreset,
|
||||
duplicatePromptPreset,
|
||||
setActivePreset,
|
||||
getActivePresetForChatType,
|
||||
getPresetsForChatType,
|
||||
fetchRemotePresets,
|
||||
fetchPresetContent,
|
||||
addRemotePreset,
|
||||
isRemotePresetAdded,
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: [
|
||||
{
|
||||
pick: [
|
||||
'customKeywordTemplates',
|
||||
'deletedPresetTemplateIds',
|
||||
'aiGlobalSettings',
|
||||
'customPromptPresets',
|
||||
'builtinPresetOverrides',
|
||||
'aiPromptSettings',
|
||||
'fetchedRemotePresetIds',
|
||||
],
|
||||
pick: ['customKeywordTemplates', 'deletedPresetTemplateIds', 'aiGlobalSettings'],
|
||||
storage: localStorage,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* ChatLab AI 相关类型定义
|
||||
* 包含:提示词预设、AI 配置
|
||||
*/
|
||||
|
||||
// ==================== AI 提示词预设 ====================
|
||||
|
||||
/**
|
||||
* 预设适用的聊天类型
|
||||
* - 'group': 仅群聊
|
||||
* - 'private': 仅私聊
|
||||
* - 'common': 通用(群聊和私聊都适用)
|
||||
*/
|
||||
export type PresetApplicableType = 'group' | 'private' | 'common'
|
||||
|
||||
/**
|
||||
* AI 提示词预设
|
||||
*
|
||||
* applicableTo 表示预设适用的场景:
|
||||
* - 'common' 表示群聊和私聊都适用(默认)
|
||||
* - 'group' 表示仅群聊
|
||||
* - 'private' 表示仅私聊
|
||||
*
|
||||
* 后端会根据运行时的 chatType 自动处理差异化内容(如成员查询策略)。
|
||||
*/
|
||||
export interface PromptPreset {
|
||||
id: string
|
||||
name: string // 预设名称
|
||||
systemPrompt: string // 系统提示词(角色定义 + 回答要求,统一为单一字段)
|
||||
isBuiltIn: boolean // 是否内置(内置不可删除)
|
||||
applicableTo?: PresetApplicableType // 适用场景,默认 'common'
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 提示词配置(激活的预设)
|
||||
*/
|
||||
export interface AIPromptSettings {
|
||||
activePresetId: string // 当前激活的预设ID
|
||||
}
|
||||
|
||||
// ==================== 兼容旧版本 ====================
|
||||
|
||||
/**
|
||||
* @deprecated 使用 PresetApplicableType 代替
|
||||
*/
|
||||
export type PromptPresetChatType = 'group' | 'private'
|
||||
@@ -10,6 +10,7 @@
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["electron-vite/node"]
|
||||
"types": ["electron-vite/node"],
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user