feat(desktop): ✨ 优化一些逻辑
1. 优化通知配置 2. 优化命名规范 3. 优化代码逻辑
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
appId: com.electron.app
|
||||
appId: com.reading_book
|
||||
productName: read_books
|
||||
directories:
|
||||
buildResources: build
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
{
|
||||
"name": "read_books",
|
||||
"version": "1.0.0",
|
||||
"description": "An Electron application with Vue and TypeScript",
|
||||
"description": "让每一篇阅读者享受阅读与思考的乐趣",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "example.com",
|
||||
"homepage": "https://electron-vite.org",
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint --cache .",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { app, BrowserWindow, ipcMain, shell } from 'electron'
|
||||
import { app, BrowserWindow, shell } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
|
||||
import icon from '../../resources/icon.png?asset'
|
||||
@@ -8,11 +8,11 @@ import { createIPCHandler } from 'electron-trpc/main'
|
||||
import { initDB } from '@main/db/data-source'
|
||||
|
||||
function createWindow(): void {
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
show: false,
|
||||
title: '读书心得助手',
|
||||
resizable: false,
|
||||
autoHideMenuBar: true,
|
||||
...(process.platform === 'linux' ? { icon } : {}),
|
||||
@@ -38,8 +38,6 @@ function createWindow(): void {
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||
} else {
|
||||
@@ -47,41 +45,22 @@ function createWindow(): void {
|
||||
}
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(async () => {
|
||||
await initDB()
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId('com.electron')
|
||||
electronApp.setAppUserModelId('com.reading_book')
|
||||
|
||||
// Default open or close DevTools by F12 in development
|
||||
// and ignore CommandOrControl + R in production.
|
||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window)
|
||||
})
|
||||
|
||||
// IPC test
|
||||
ipcMain.on('ping', () => console.log('pong'))
|
||||
|
||||
createWindow()
|
||||
|
||||
app.on('activate', function () {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
// for applications and their menu bar to stay active until the user quits
|
||||
// explicitly with Cmd + Q.
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
// In this file you can include the rest of your app's specific main process
|
||||
// code. You can also put them in separate files and require them here.
|
||||
|
||||
@@ -4,9 +4,16 @@ import { readingReflectionGraph } from '@main/services/ai/graph/readingReflectio
|
||||
import { AppDataSource } from '@main/db/data-source'
|
||||
import { ReadingReflectionTaskBatch } from '@main/db/entities/ReadingReflectionTaskBatch'
|
||||
import { ReadingReflectionTaskItem } from '@main/db/entities/ReadingReflectionTaskItem'
|
||||
import Store from 'electron-store'
|
||||
import { CONFIG_STORE_KEY } from '@rpc/constants/store_key'
|
||||
import { Notification } from 'electron'
|
||||
|
||||
export const readingReflectionTaskEvent = new EventEmitter()
|
||||
|
||||
// 兼容性处理获取 Store 构造函数
|
||||
const StoreClass = (Store as any).default || Store
|
||||
const store = new StoreClass({ encryptionKey: CONFIG_STORE_KEY })
|
||||
|
||||
class TaskManager {
|
||||
private limit = pLimit(2)
|
||||
private batchRepo = AppDataSource.getRepository(ReadingReflectionTaskBatch)
|
||||
@@ -113,6 +120,7 @@ class TaskManager {
|
||||
result?: any
|
||||
) {
|
||||
const displayId = total === 1 ? taskId : `${taskId}-${index}`
|
||||
//发送 tRPC 实时事件(驱动前端 UI 进度条)
|
||||
readingReflectionTaskEvent.emit('readingReflectionTaskProgress', {
|
||||
taskId: displayId,
|
||||
progress,
|
||||
@@ -120,6 +128,44 @@ class TaskManager {
|
||||
statusText: `[任务${index + 1}/${total}] ${status}`, // 传描述文字
|
||||
result
|
||||
})
|
||||
// 2. 添加任务状态通知判断
|
||||
this.handleNotification(status, progress, total, index)
|
||||
}
|
||||
/**
|
||||
* 内部私有方法:处理通知逻辑
|
||||
*/
|
||||
private handleNotification(status: string, progress: number, total: number, index: number) {
|
||||
// 从 electron-store 获取用户偏好
|
||||
const config = store.get('notification') || {
|
||||
masterSwitch: true,
|
||||
taskCompleted: true,
|
||||
taskFailed: true
|
||||
}
|
||||
|
||||
// 如果总开关关闭,直接拦截
|
||||
if (!config.masterSwitch) return
|
||||
|
||||
// 场景 A: 任务全部完成 (100%)
|
||||
if (progress === 100 && config.taskCompleted) {
|
||||
// 只有当所有子任务都完成,或者当前是单任务时才弹出
|
||||
// 如果是批量任务,你可以选择在最后一个子任务完成时通知
|
||||
if (index + 1 === total) {
|
||||
new Notification({
|
||||
title: '🎉 读书心得已生成',
|
||||
body: total > 1 ? `共 ${total} 篇心得已全部处理完成。` : '您的书籍心得已准备就绪。',
|
||||
silent: config.silentMode
|
||||
}).show()
|
||||
}
|
||||
}
|
||||
|
||||
// 场景 B: 任务失败 (假设你传入的 status 是 'FAILED')
|
||||
if (status === 'FAILED' && config.taskFailed) {
|
||||
new Notification({
|
||||
title: '❌ 任务生成失败',
|
||||
body: `第 ${index + 1} 项任务执行异常,请检查网络或 API 余额。`,
|
||||
silent: config.silentMode
|
||||
}).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Annotation } from '@langchain/langgraph'
|
||||
import { Occupation } from '@shared/types/reflections'
|
||||
import { Occupation } from '@shared/types/IReadingReflectionTask'
|
||||
|
||||
export const ReadingReflectionState = Annotation.Root({
|
||||
// 输入任务
|
||||
|
||||
2
src/renderer/components.d.ts
vendored
2
src/renderer/components.d.ts
vendored
@@ -12,6 +12,7 @@ export {}
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AButton: typeof import('@arco-design/web-vue')['Button']
|
||||
ACheckbox: typeof import('@arco-design/web-vue')['Checkbox']
|
||||
ACollapse: typeof import('@arco-design/web-vue')['Collapse']
|
||||
ACollapseItem: typeof import('@arco-design/web-vue')['CollapseItem']
|
||||
ActiveMenu: typeof import('./src/components/ActiveMenu.vue')['default']
|
||||
@@ -23,6 +24,7 @@ declare module 'vue' {
|
||||
AOption: typeof import('@arco-design/web-vue')['Option']
|
||||
ASelect: typeof import('@arco-design/web-vue')['Select']
|
||||
ASlider: typeof import('@arco-design/web-vue')['Slider']
|
||||
ASwitch: typeof import('@arco-design/web-vue')['Switch']
|
||||
ATabPane: typeof import('@arco-design/web-vue')['TabPane']
|
||||
ATabs: typeof import('@arco-design/web-vue')['Tabs']
|
||||
ATag: typeof import('@arco-design/web-vue')['Tag']
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Electron</title>
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
|
||||
/>
|
||||
</head>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>读书心得助手 | 让每一篇阅读者享受阅读与思考的乐趣</title>
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
BIN
src/renderer/src/assets/application.png
Normal file
BIN
src/renderer/src/assets/application.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 161 KiB |
@@ -16,7 +16,6 @@ const navButtons = [
|
||||
]
|
||||
|
||||
const active = (key: string) => {
|
||||
console.log(key)
|
||||
go('/' + key)
|
||||
}
|
||||
</script>
|
||||
@@ -40,7 +39,7 @@ const active = (key: string) => {
|
||||
:fill="activeBtn === btn.key ? '#7816ff' : '#64748b'"
|
||||
/>
|
||||
|
||||
<span class="text-[10px] ml-1.5 hidden group-hover:inline-block text-slate-500 font-medium">
|
||||
<span class="text-[10px] ml-1.5 group-hover:inline-block text-slate-500 font-medium">
|
||||
{{ btn.title }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouterHook } from '@renderer/hooks/useRouterHook'
|
||||
import {
|
||||
AdProduct,
|
||||
ArrowRight,
|
||||
Github,
|
||||
HoldInterface,
|
||||
InternalData,
|
||||
Mail,
|
||||
Twitter
|
||||
} from '@icon-park/vue-next'
|
||||
import { AdProduct, Github, HoldInterface, InternalData, Mail, Twitter } from '@icon-park/vue-next'
|
||||
import applicationImg from '@renderer/assets/application.png'
|
||||
|
||||
const { goBack } = useRouterHook()
|
||||
const { go } = useRouterHook()
|
||||
|
||||
const coreValues = [
|
||||
{
|
||||
icon: InternalData,
|
||||
title: '数据驱动',
|
||||
desc: '基于自研深度学习模型,精准提取每一本书籍的灵魂。'
|
||||
desc: '基于大模型,精准提取每一本书籍的灵魂。'
|
||||
},
|
||||
{
|
||||
icon: AdProduct,
|
||||
@@ -34,15 +27,9 @@ const coreValues = [
|
||||
<template>
|
||||
<div class="flex-1 h-full overflow-y-auto bg-[#FAFAFB] custom-scroll">
|
||||
<section class="pt-20 pb-16 px-6 text-center bg-white border-b border-slate-50">
|
||||
<div class="inline-flex items-center space-x-2 bg-purple-50 px-3 py-1 rounded-full mb-6">
|
||||
<span class="w-2 h-2 bg-[#7816ff] rounded-full animate-pulse"></span>
|
||||
<span class="text-[10px] font-bold text-[#7816ff] uppercase tracking-widest"
|
||||
>Version 2.0.0 Now Live</span
|
||||
>
|
||||
</div>
|
||||
<h1 class="text-4xl font-black text-slate-800 mb-4 tracking-tight">
|
||||
让每一页文字<br />
|
||||
<span class="text-[#7816ff]">都有迹可循</span>
|
||||
<h1 class="text-4xl font-black text-slate-800 mb-4 tracking-tight flex flex-col gap-4">
|
||||
<span>让每一篇阅读者</span>
|
||||
<span class="text-[#7816ff]">享受阅读与思考的乐趣</span>
|
||||
</h1>
|
||||
<p class="max-w-xl mx-auto text-slate-400 text-sm leading-relaxed mb-8">
|
||||
我们致力于通过最前沿的 AI 技术,帮助深度阅读者高效消化知识。
|
||||
@@ -53,12 +40,10 @@ const coreValues = [
|
||||
type="primary"
|
||||
shape="round"
|
||||
class="bg-[#7816ff] border-none px-8 h-10 shadow-lg shadow-purple-100"
|
||||
@click="go('/task/create')"
|
||||
>
|
||||
开始体验
|
||||
</a-button>
|
||||
<a-button shape="round" class="px-8 h-10 border-slate-200" @click="goBack()">
|
||||
返回探索
|
||||
</a-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -90,10 +75,7 @@ const coreValues = [
|
||||
class="bg-white rounded-[32px] border border-slate-100 shadow-sm overflow-hidden flex flex-col md:flex-row items-center"
|
||||
>
|
||||
<div class="flex-1 p-12 md:p-16">
|
||||
<div class="text-[11px] font-bold text-[#7816ff] uppercase tracking-[0.2em] mb-4">
|
||||
Our Mission
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-slate-800 mb-6">为了那些真正热爱文字的人</h2>
|
||||
<h2 class="text-2xl font-bold text-slate-800 mb-6">让那些真正热爱文字的人</h2>
|
||||
<p class="text-sm text-slate-500 leading-relaxed mb-8">
|
||||
在一个信息碎片化的时代,深度阅读正变得前所未有的奢侈。我们不希望 AI
|
||||
替代阅读,而是希望它能作为你的“数字笔友”,帮你梳理逻辑、捕捉灵感,让你从繁琐的摘要工作中解放,回归思考本身。
|
||||
@@ -101,16 +83,16 @@ const coreValues = [
|
||||
<div class="flex items-center space-x-6">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-xl font-black text-slate-800">12,400+</span>
|
||||
<span class="text-[10px] text-slate-400 font-bold uppercase">Active Users</span>
|
||||
<span class="text-[10px] text-slate-400 font-bold uppercase">使用用户</span>
|
||||
</div>
|
||||
<div class="w-[1px] h-8 bg-slate-100"></div>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-xl font-black text-slate-800">8.5M</span>
|
||||
<span class="text-[10px] text-slate-400 font-bold uppercase">Words Generated</span>
|
||||
<span class="text-[10px] text-slate-400 font-bold uppercase">阅读量</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 w-full h-[400px] bg-slate-50 flex items-center justify-center relative">
|
||||
<div class="flex-1 w-full h-[400px] flex items-center justify-center relative">
|
||||
<div
|
||||
class="absolute inset-0 opacity-20"
|
||||
style="
|
||||
@@ -119,19 +101,10 @@ const coreValues = [
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
class="relative z-10 w-64 h-64 bg-white rounded-3xl shadow-xl border border-slate-100 flex flex-col p-6 animate-bounce-slow"
|
||||
class="relative z-10 w-90 bg-white rounded-3xl shadow-xl border border-slate-100 flex flex-col p-6 animate-bounce-slow"
|
||||
>
|
||||
<div class="w-8 h-8 bg-purple-50 rounded-lg mb-4"></div>
|
||||
<div class="h-2 w-3/4 bg-slate-100 rounded-full mb-3"></div>
|
||||
<div class="h-2 w-full bg-slate-50 rounded-full mb-3"></div>
|
||||
<div class="h-2 w-2/3 bg-slate-50 rounded-full"></div>
|
||||
<div class="mt-auto flex justify-end">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full bg-[#7816ff] flex items-center justify-center text-white"
|
||||
>
|
||||
<arrow-right size="14" />
|
||||
</div>
|
||||
</div>
|
||||
<img :src="applicationImg" alt="" />
|
||||
<div class="mt-auto flex justify-end"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
45
src/renderer/src/pages/faq/data/faqData.ts
Normal file
45
src/renderer/src/pages/faq/data/faqData.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export const faqList = [
|
||||
{
|
||||
id: 1,
|
||||
category: 'general',
|
||||
q: '这个 AI 阅读工具是如何工作的?',
|
||||
a: '我们利用深度学习模型对书籍文本进行语义分析。它不仅能总结全文,还能根据你选择的“职业背景”提取特定的知识点。'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category: 'usage',
|
||||
q: '生成的字数可以超过 5000 字吗?',
|
||||
a: '目前单次生成上限为 5000 字,以确保内容的逻辑连贯性。如果需要更长篇幅,建议分章节创建任务。'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
category: 'billing',
|
||||
q: '当前有订阅计划吗?',
|
||||
a: '当前工具为免费使用,不需要任何费用,但是需要自己使用大模型KEY'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
category: 'privacy',
|
||||
q: '如何查看我的数据?',
|
||||
a: '你可以在设置页面查看你的数据。'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
category: 'usage',
|
||||
q: '如何设置大模型?',
|
||||
a: `
|
||||
您可以在<span style="color: red">『设置中心』-『大模型配置』中分别设置读书心得与摘要模型</span>。
|
||||
<br/>推荐使用【阿里云-心流平台 (iflow.cn)】提供的免费 API,其单次生成支持长文本处理。若生成内容接近 5000 字上限,建议分章节创建任务以获得最佳逻辑效果。
|
||||
<br/><br/>可以参考以下操作指南:
|
||||
<br/>
|
||||
<ul>
|
||||
<li>获取 API Key:访问 阿里云心流平台 (iflow.cn)。</li>
|
||||
注册并登录后,在后台创建一个新的 API Key。心流平台目前提供稳定的免费额度,非常适合个人阅读助手使用。
|
||||
<li>配置接口地址:</li>
|
||||
在应用设置的 Base URL 处填入:https://apis.iflow.cn/v1。
|
||||
<li>模型选择:</li>
|
||||
平台提供的最新模型 ID。
|
||||
</ul>
|
||||
`
|
||||
}
|
||||
]
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Security,
|
||||
Wallet
|
||||
} from '@icon-park/vue-next'
|
||||
import { faqList } from './data/faqData'
|
||||
|
||||
// 分类配置
|
||||
const categories = [
|
||||
@@ -21,34 +22,6 @@ const categories = [
|
||||
const activeCategory = ref('general')
|
||||
const searchQuery = ref('')
|
||||
|
||||
// 问题数据
|
||||
const faqList = [
|
||||
{
|
||||
id: 1,
|
||||
category: 'general',
|
||||
q: '这个 AI 阅读工具是如何工作的?',
|
||||
a: '我们利用深度学习模型对书籍文本进行语义分析。它不仅能总结全文,还能根据你选择的“职业背景”提取特定的知识点。'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category: 'usage',
|
||||
q: '生成的字数可以超过 5000 字吗?',
|
||||
a: '目前单次生成上限为 5000 字,以确保内容的逻辑连贯性。如果需要更长篇幅,建议分章节创建任务。'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
category: 'billing',
|
||||
q: '订阅后可以申请退款吗?',
|
||||
a: '在订阅后的 24 小时内且未使用过算力资源的情况下,您可以联系客服申请全额退款。'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
category: 'general',
|
||||
q: '支持哪些格式的书籍?',
|
||||
a: '目前支持 PDF, EPUB, TXT 格式。为了获得最佳效果,建议上传排版清晰的文档。'
|
||||
}
|
||||
]
|
||||
|
||||
// 搜索过滤逻辑
|
||||
const filteredFaqs = computed(() => {
|
||||
return faqList.filter((item) => {
|
||||
@@ -110,7 +83,7 @@ const filteredFaqs = computed(() => {
|
||||
<span class="text-[14px] font-bold text-slate-700">{{ faq.q }}</span>
|
||||
</template>
|
||||
<div class="text-[13px] leading-relaxed text-slate-500 py-2">
|
||||
{{ faq.a }}
|
||||
<span v-html="faq.a"></span>
|
||||
</div>
|
||||
</a-collapse-item>
|
||||
</a-collapse>
|
||||
|
||||
@@ -1,62 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onMounted, reactive, ref, toRaw } from 'vue'
|
||||
import { SettingConfig } from '@icon-park/vue-next'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { trpc } from '@renderer/lib/trpc'
|
||||
import { modelOptions } from '../data/ModelData'
|
||||
import { ModelForm } from '@renderer/pages/setting/types/IModel'
|
||||
|
||||
interface ModelOption {
|
||||
id: string // 这里的 id 即 modelName
|
||||
name: string
|
||||
desc: string
|
||||
tag?: string
|
||||
const activeTab = ref<'reading' | 'summary'>('reading') // 初始设为 reading
|
||||
|
||||
const modelForm = reactive<ModelForm>({
|
||||
reading: {
|
||||
apiKey: 'sk-172309b16482e6ad4264b1cd89f010d8',
|
||||
baseURL: 'https://apis.iflow.cn/v1',
|
||||
modelName: 'deepseek-v3.2',
|
||||
temperature: 0.7
|
||||
},
|
||||
summary: {
|
||||
apiKey: 'sk-172309b16482e6ad4264b1cd89f010d8',
|
||||
baseURL: 'https://apis.iflow.cn/v1',
|
||||
modelName: 'deepseek-v3.2',
|
||||
temperature: 0.3
|
||||
}
|
||||
})
|
||||
|
||||
// --- 逻辑方法 ---
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const savedConfig = (await trpc.config.getChatConfigs.query()) as any
|
||||
if (savedConfig) {
|
||||
// 这里的 Key 必须与后端存储的 chatModels.reading 对应
|
||||
if (savedConfig.reading) Object.assign(modelForm.reading, savedConfig.reading)
|
||||
if (savedConfig.summary) Object.assign(modelForm.summary, savedConfig.summary)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
interface ModelConfig {
|
||||
apiKey: string
|
||||
baseURL: string
|
||||
modelName: string
|
||||
temperature: number
|
||||
const handleSave = async (type: 'reading' | 'summary') => {
|
||||
try {
|
||||
// 关键点:使用 toRaw 转换,剥离 Vue 的响应式代理
|
||||
const configToSave = toRaw(modelForm[type])
|
||||
|
||||
// 打印检查一下,确保是普通对象而非 Proxy
|
||||
console.log('正在保存纯对象数据:', configToSave)
|
||||
|
||||
await trpc.config.saveChatConfig.mutate({
|
||||
type,
|
||||
config: configToSave
|
||||
})
|
||||
|
||||
Message.success({
|
||||
content: `${type === 'reading' ? '读书心得' : '摘要总结'}配置已安全保存`,
|
||||
showIcon: true
|
||||
})
|
||||
} catch (error) {
|
||||
Message.error('保存失败,无法克隆对象')
|
||||
console.error('tRPC 错误详情:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 包含两种类型的配置
|
||||
interface ModelForm {
|
||||
reflection: ModelConfig
|
||||
summary: ModelConfig
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: ModelForm // 必填,父组件传 reactive 对象
|
||||
options?: ModelOption[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'save'])
|
||||
|
||||
// 当前激活的标签页
|
||||
const activeTab = ref<'reflection' | 'summary'>('reflection')
|
||||
|
||||
// 处理模型卡片点击:更新当前 Tab 下的 modelName
|
||||
const selectModel = (modelId: string) => {
|
||||
const newData = { ...props.modelValue }
|
||||
newData[activeTab.value].modelName = modelId
|
||||
emit('update:modelValue', newData)
|
||||
modelForm[activeTab.value].modelName = modelId
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="modelValue" class="space-y-6 animate-in">
|
||||
<div class="space-y-6 animate-in pb-10">
|
||||
<a-tabs v-model:active-key="activeTab" type="capsule" class="custom-tabs">
|
||||
<a-tab-pane key="reflection" title="读书心得模型" />
|
||||
<a-tab-pane key="reading" title="读书心得模型" />
|
||||
<a-tab-pane key="summary" title="摘要总结模型" />
|
||||
</a-tabs>
|
||||
|
||||
<div class="bg-white rounded-2xl border border-slate-100 shadow-sm p-8">
|
||||
<div class="flex items-center space-x-2 mb-8">
|
||||
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
|
||||
<h3 class="text-sm font-bold text-slate-800">接口配置</h3>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<label class="setting-label">Base URL (API 代理地址)</label>
|
||||
<a-input
|
||||
v-model="modelValue[activeTab].baseURL"
|
||||
v-model="modelForm[activeTab].baseURL"
|
||||
placeholder="https://api.openai.com/v1"
|
||||
class="custom-input"
|
||||
/>
|
||||
@@ -64,7 +93,7 @@ const selectModel = (modelId: string) => {
|
||||
<div class="space-y-2">
|
||||
<label class="setting-label">API Key</label>
|
||||
<a-input-password
|
||||
v-model="modelValue[activeTab].apiKey"
|
||||
v-model="modelForm[activeTab].apiKey"
|
||||
placeholder="sk-..."
|
||||
class="custom-input"
|
||||
/>
|
||||
@@ -77,37 +106,34 @@ const selectModel = (modelId: string) => {
|
||||
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
|
||||
<h3 class="text-sm font-bold text-slate-800">模型名称 (modelName)</h3>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<a-input
|
||||
v-model="modelValue[activeTab].modelName"
|
||||
placeholder="或者手动输入模型 ID,如 gpt-4o"
|
||||
v-model="modelForm[activeTab].modelName"
|
||||
placeholder="手动输入模型 ID,如 gpt-4o"
|
||||
class="custom-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div
|
||||
v-for="m in options"
|
||||
v-for="m in modelOptions"
|
||||
:key="m.id"
|
||||
@click="selectModel(m.id)"
|
||||
:class="[
|
||||
'p-4 rounded-xl border-2 cursor-pointer transition-all relative overflow-hidden',
|
||||
modelValue[activeTab].modelName === m.id
|
||||
modelForm[activeTab].modelName === m.id
|
||||
? 'border-[#7816ff] bg-purple-50/30'
|
||||
: 'border-slate-50 bg-slate-50/50 hover:border-slate-200'
|
||||
]"
|
||||
>
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<span class="text-xs font-bold text-slate-800">{{ m.name }}</span>
|
||||
<span v-if="m.tag" class="text-[9px] bg-[#7816ff] text-white px-1.5 py-0.5 rounded">
|
||||
{{ m.tag }}
|
||||
</span>
|
||||
<span v-if="m.tag" class="text-[9px] bg-[#7816ff] text-white px-1.5 py-0.5 rounded">{{
|
||||
m.tag
|
||||
}}</span>
|
||||
</div>
|
||||
<p class="text-[10px] text-slate-400 leading-relaxed">{{ m.desc }}</p>
|
||||
|
||||
<div
|
||||
v-if="modelValue[activeTab].modelName === m.id"
|
||||
v-if="modelForm[activeTab].modelName === m.id"
|
||||
class="absolute -right-2 -bottom-2 text-[#7816ff]/10"
|
||||
>
|
||||
<SettingConfig theme="outline" size="48" />
|
||||
@@ -120,33 +146,31 @@ const selectModel = (modelId: string) => {
|
||||
<div class="flex items-center space-x-2 mb-8">
|
||||
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
|
||||
<h3 class="text-sm font-bold text-slate-800">运行参数</h3>
|
||||
<p class="text-[11px] text-[#7816ff] leading-relaxed">
|
||||
<p class="text-[11px] text-[#7816ff] leading-relaxed ml-4 italic">
|
||||
正在配置
|
||||
<span class="font-bold underline">{{
|
||||
activeTab === 'reflection' ? '心得生成' : '文本总结'
|
||||
activeTab === 'reading' ? '心得生成' : '文本总结'
|
||||
}}</span>
|
||||
专用模型。 建议为总结模型设置较低的 Temperature 以保证稳定性。
|
||||
专用模型。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-8">
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<label class="setting-label"
|
||||
>随机性 (Temperature): {{ modelValue[activeTab].temperature }}</label
|
||||
>随机性 (Temperature): {{ modelForm[activeTab].temperature }}</label
|
||||
>
|
||||
<span class="text-[10px] text-slate-400">建议心得 0.7-0.8,总结 0.2-0.3</span>
|
||||
</div>
|
||||
<a-slider v-model="modelValue[activeTab].temperature" :min="0" :max="1.2" :step="0.1" />
|
||||
<a-slider v-model="modelForm[activeTab].temperature" :min="0" :max="1.2" :step="0.1" />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-4 border-t border-slate-50">
|
||||
<a-button
|
||||
type="primary"
|
||||
class="rounded-lg bg-[#7816ff] border-none px-8 font-bold"
|
||||
@click="emit('save', activeTab)"
|
||||
@click="handleSave(activeTab)"
|
||||
>
|
||||
保存 {{ activeTab === 'reflection' ? '心得' : '总结' }} 配置
|
||||
保存 {{ activeTab === 'reading' ? '心得' : '总结' }} 配置
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -155,7 +179,7 @@ const selectModel = (modelId: string) => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 继承原有样式 */
|
||||
/* 样式部分保持不变... */
|
||||
.setting-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
@@ -165,17 +189,24 @@ const selectModel = (modelId: string) => {
|
||||
letter-spacing: 0.05em;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
/* Tabs 风格微调适配你的紫色主题 */
|
||||
:deep(.custom-tabs .arco-tabs-nav-capsule) {
|
||||
background-color: #f1f5f9;
|
||||
border-radius: 12px;
|
||||
padding: 4px;
|
||||
:deep(.custom-input) {
|
||||
background-color: #fcfcfd !important;
|
||||
border: 1px solid #f1f5f9 !important;
|
||||
border-radius: 12px !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
:deep(.custom-tabs .arco-tabs-nav-capsule-light) {
|
||||
background-color: #fff;
|
||||
color: #7816ff;
|
||||
font-weight: bold;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.animate-in {
|
||||
animation: fadeIn 0.4s ease-out forwards;
|
||||
}
|
||||
</style>
|
||||
87
src/renderer/src/pages/setting/components/NotFound.vue
Normal file
87
src/renderer/src/pages/setting/components/NotFound.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { Hourglass } from '@icon-park/vue-next'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-[600px] flex flex-col items-center justify-center animate-in">
|
||||
<div
|
||||
class="relative bg-white rounded-3xl border border-slate-100 shadow-xl shadow-slate-200/50 p-12 max-w-md w-full text-center overflow-hidden"
|
||||
>
|
||||
<div class="space-y-3 relative z-10">
|
||||
<h2 class="text-xl font-bold text-slate-800 tracking-tight">
|
||||
<span class="text-[#7816ff]">正在构建中</span>
|
||||
</h2>
|
||||
<p class="text-sm text-slate-400 leading-relaxed px-3">
|
||||
我们正在为该模块编写最后一行代码,以确保为您提供最极致的体验。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 space-y-4">
|
||||
<div class="h-2 w-full bg-slate-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full bg-gradient-to-r from-[#7816ff] to-[#a855f7] w-[50%] rounded-full relative"
|
||||
>
|
||||
<div
|
||||
class="absolute top-0 right-0 h-full w-2 bg-white/30 skew-x-12 animate-shimmer"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-10 pt-8 border-t border-slate-50 flex items-center justify-center gap-6">
|
||||
<div class="flex items-center gap-2 text-[11px] text-slate-400 font-medium">
|
||||
<hourglass theme="outline" size="14" fill="#94a3b8" />
|
||||
预计下个版本上线
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
from {
|
||||
transform: translateX(-100%) skewX(-12deg);
|
||||
}
|
||||
to {
|
||||
transform: translateX(200%) skewX(-12deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-shimmer {
|
||||
animation: shimmer 2s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.animate-in {
|
||||
animation: fadeIn 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,159 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted, toRaw } from 'vue'
|
||||
import { Remind, Tips, SettingTwo, MessageSuccess } from '@icon-park/vue-next'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { trpc } from '@renderer/lib/trpc'
|
||||
import { INoticeConfig } from '@shared/types/IConfig'
|
||||
|
||||
// 1. 数据结构定义
|
||||
const notificationForm = reactive<INoticeConfig>({
|
||||
masterSwitch: true, // 总开关
|
||||
taskCompleted: true, // 任务完成时提醒
|
||||
taskFailed: true, // 任务失败时提醒
|
||||
silentMode: false // 静默模式(仅弹窗无声音)
|
||||
})
|
||||
|
||||
// 2. 加载与保存逻辑
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
const saved = await trpc.config.getNoticeConfigs.query()
|
||||
if (saved) Object.assign(notificationForm, saved)
|
||||
} catch (e) {
|
||||
console.error('加载通知设置失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggle = async () => {
|
||||
try {
|
||||
await trpc.config.saveNoticeConfigs.mutate(toRaw(notificationForm))
|
||||
Message.success({ content: '通知偏好已更新', showIcon: true })
|
||||
} catch (e) {
|
||||
Message.error('保存设置失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadSettings)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6 animate-in">
|
||||
<div
|
||||
class="bg-gradient-to-br from-[#7816ff] to-[#a855f7] rounded-2xl p-8 text-white shadow-lg shadow-purple-200/50"
|
||||
>
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="space-y-2">
|
||||
<h2 class="text-xl font-bold flex items-center gap-2">
|
||||
<Remind theme="filled" size="24" fill="#fff" /> 通知管理中心
|
||||
</h2>
|
||||
<p class="text-purple-100 text-xs opacity-80">
|
||||
实时掌握读书心得生成进度,确保每一份灵感都能及时送达。
|
||||
</p>
|
||||
</div>
|
||||
<a-switch
|
||||
v-model="notificationForm.masterSwitch"
|
||||
type="line"
|
||||
@change="handleToggle"
|
||||
class="custom-white-switch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-white rounded-2xl border border-slate-100 shadow-sm p-8"
|
||||
:class="{ 'opacity-60 grayscale-[0.5] pointer-events-none': !notificationForm.masterSwitch }"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
|
||||
<h3 class="text-sm font-bold text-slate-800">任务状态通知</h3>
|
||||
</div>
|
||||
<a-tag size="small" color="arcoblue" class="rounded-md font-bold text-[10px]"
|
||||
>实时推送</a-tag
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div
|
||||
class="flex items-center justify-between p-4 rounded-xl bg-slate-50/50 border border-transparent hover:border-slate-100 transition-all"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 rounded-full bg-green-50 flex items-center justify-center">
|
||||
<message-success theme="outline" size="20" fill="#00b42a" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-bold text-slate-700">生成成功提醒</div>
|
||||
<div class="text-[11px] text-slate-400">
|
||||
当读书心得、摘要生成完毕时,发送桌面通知。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-checkbox v-model="notificationForm.taskCompleted" @change="handleToggle" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between p-4 rounded-xl bg-slate-50/50 border border-transparent hover:border-slate-100 transition-all"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center">
|
||||
<tips theme="outline" size="20" fill="#f53f3f" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-bold text-slate-700">异常中断提醒</div>
|
||||
<div class="text-[11px] text-slate-400">
|
||||
若因网络波动或 API 余额不足导致生成失败时提醒。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-checkbox v-model="notificationForm.taskFailed" @change="handleToggle" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between p-4 rounded-xl bg-slate-50/50 border border-transparent hover:border-slate-100 transition-all"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 rounded-full bg-purple-50 flex items-center justify-center">
|
||||
<setting-two theme="outline" size="20" fill="#7816ff" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-bold text-slate-700">静默模式</div>
|
||||
<div class="text-[11px] text-slate-400">通知弹出时不再播放系统提示音。</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-switch v-model="notificationForm.silentMode" size="small" @change="handleToggle" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-3 p-4 bg-blue-50/50 rounded-xl border border-blue-100">
|
||||
<remind theme="outline" size="16" fill="#0066ff" class="mt-0.5" />
|
||||
<p class="text-[11px] text-blue-500 leading-relaxed">
|
||||
提示:通知效果受操作系统“专注模式”或“勿扰模式”影响。如果在设置开启后仍未收到通知,请检查系统的通知管理权限。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.custom-white-switch :deep(.arco-switch-dot) {
|
||||
background-color: #7816ff;
|
||||
}
|
||||
.custom-white-switch :deep(.arco-switch-checked) {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* 继承你之前的动画 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.animate-in {
|
||||
animation: fadeIn 0.4s ease-out forwards;
|
||||
}
|
||||
</style>
|
||||
8
src/renderer/src/pages/setting/data/ModelData.ts
Normal file
8
src/renderer/src/pages/setting/data/ModelData.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { IModelOption } from '../types/IModel'
|
||||
|
||||
export const modelOptions: IModelOption[] = [
|
||||
{ id: 'gpt-4o', name: 'GPT-4o', desc: '能力最强,适合复杂文学分析', tag: '推荐' },
|
||||
{ id: 'gpt-3.5-turbo', name: 'GPT-3.5', desc: '响应速度极快', tag: '超快' },
|
||||
{ id: 'claude-3-5-sonnet-20240620', name: 'Claude 3.5', desc: '文笔细腻', tag: '文笔佳' },
|
||||
{ id: 'deepseek-v3.2', name: 'deepseekV3.2', desc: '适合快速生成内容', tag: '推荐' }
|
||||
]
|
||||
@@ -1,112 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref, toRaw } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { Remind, Right, SettingConfig, ShieldAdd, Wallet } from '@icon-park/vue-next'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { trpc } from '@renderer/lib/trpc'
|
||||
|
||||
// --- 类型定义 ---
|
||||
interface ModelOption {
|
||||
id: string
|
||||
name: string
|
||||
desc: string
|
||||
tag?: string
|
||||
}
|
||||
|
||||
interface ModelConfig {
|
||||
apiKey: string
|
||||
baseURL: string
|
||||
modelName: string
|
||||
temperature: number
|
||||
}
|
||||
|
||||
interface ModelForm {
|
||||
reading: ModelConfig // 统一使用 reading
|
||||
summary: ModelConfig
|
||||
}
|
||||
import ModelSetting from '@renderer/pages/setting/components/ModelSetting.vue'
|
||||
import NotFound from '@renderer/pages/setting/components/NotFound.vue'
|
||||
import NotificationSetting from '@renderer/pages/setting/components/NotificationSetting.vue'
|
||||
|
||||
// --- 状态管理 ---
|
||||
const activeMenu = ref('model')
|
||||
const activeTab = ref<'reading' | 'summary'>('reading') // 初始设为 reading
|
||||
const activeMenuItem = computed(() => {
|
||||
return menuItems.find((item) => item.key === activeMenu.value)?.component
|
||||
})
|
||||
|
||||
const menuItems = [
|
||||
{ key: 'model', name: '大模型配置', icon: SettingConfig },
|
||||
{ key: 'model', name: '大模型配置', icon: SettingConfig, component: ModelSetting },
|
||||
{ key: 'account', name: '账号安全', icon: ShieldAdd },
|
||||
{ key: 'billing', name: '订阅与账单', icon: Wallet },
|
||||
{ key: 'notification', name: '通知设置', icon: Remind }
|
||||
{ key: 'notification', name: '通知设置', icon: Remind, component: NotificationSetting }
|
||||
]
|
||||
|
||||
const modelForm = reactive<ModelForm>({
|
||||
reading: {
|
||||
apiKey: 'sk-172309b16482e6ad4264b1cd89f010d8',
|
||||
baseURL: 'https://apis.iflow.cn/v1',
|
||||
modelName: 'deepseek-v3.2',
|
||||
temperature: 0.7
|
||||
},
|
||||
summary: {
|
||||
apiKey: 'sk-172309b16482e6ad4264b1cd89f010d8',
|
||||
baseURL: 'https://apis.iflow.cn/v1',
|
||||
modelName: 'deepseek-v3.2',
|
||||
temperature: 0.3
|
||||
}
|
||||
})
|
||||
|
||||
const modelOptions: ModelOption[] = [
|
||||
{ id: 'gpt-4o', name: 'GPT-4o', desc: '能力最强,适合复杂文学分析', tag: '推荐' },
|
||||
{ id: 'gpt-3.5-turbo', name: 'GPT-3.5', desc: '响应速度极快', tag: '超快' },
|
||||
{ id: 'claude-3-5-sonnet-20240620', name: 'Claude 3.5', desc: '文笔细腻', tag: '文笔佳' },
|
||||
{ id: 'deepseek-v3.2', name: 'deepseekV3.2', desc: '适合快速生成内容', tag: '推荐' }
|
||||
]
|
||||
|
||||
// --- 逻辑方法 ---
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const savedConfig = (await trpc.config.getChatConfigs.query()) as any
|
||||
if (savedConfig) {
|
||||
// 这里的 Key 必须与后端存储的 chatModels.reading 对应
|
||||
if (savedConfig.reading) Object.assign(modelForm.reading, savedConfig.reading)
|
||||
if (savedConfig.summary) Object.assign(modelForm.summary, savedConfig.summary)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async (type: 'reading' | 'summary') => {
|
||||
try {
|
||||
// 关键点:使用 toRaw 转换,剥离 Vue 的响应式代理
|
||||
const configToSave = toRaw(modelForm[type])
|
||||
|
||||
// 打印检查一下,确保是普通对象而非 Proxy
|
||||
console.log('正在保存纯对象数据:', configToSave)
|
||||
|
||||
await trpc.config.saveChatConfig.mutate({
|
||||
type,
|
||||
config: configToSave
|
||||
})
|
||||
|
||||
Message.success({
|
||||
content: `${type === 'reading' ? '读书心得' : '摘要总结'}配置已安全保存`,
|
||||
showIcon: true
|
||||
})
|
||||
} catch (error) {
|
||||
Message.error('保存失败,无法克隆对象')
|
||||
console.error('tRPC 错误详情:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const selectModel = (modelId: string) => {
|
||||
modelForm[activeTab.value].modelName = modelId
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConfig()
|
||||
})
|
||||
onMounted(() => {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 h-full overflow-y-auto bg-[#FAFAFB] p-8 custom-scroll">
|
||||
<div class="max-w-5xl mx-auto flex gap-8">
|
||||
<div class="flex-1 h-full bg-[#FAFAFB] p-8 custom-scroll">
|
||||
<div class="max-w-5xl mx-auto h-full flex gap-8">
|
||||
<aside class="w-64 shrink-0">
|
||||
<div class="flex items-center space-x-2 mb-8 px-2">
|
||||
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
|
||||
@@ -137,171 +53,16 @@ onMounted(() => {
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="flex-1">
|
||||
<div v-if="activeMenu === 'model'" class="space-y-6 animate-in">
|
||||
<a-tabs v-model:active-key="activeTab" type="capsule" class="custom-tabs">
|
||||
<a-tab-pane key="reading" title="读书心得模型" />
|
||||
<a-tab-pane key="summary" title="摘要总结模型" />
|
||||
</a-tabs>
|
||||
|
||||
<div class="bg-white rounded-2xl border border-slate-100 shadow-sm p-8">
|
||||
<div class="flex items-center space-x-2 mb-8">
|
||||
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
|
||||
<h3 class="text-sm font-bold text-slate-800">接口配置</h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<label class="setting-label">Base URL (API 代理地址)</label>
|
||||
<a-input
|
||||
v-model="modelForm[activeTab].baseURL"
|
||||
placeholder="https://api.openai.com/v1"
|
||||
class="custom-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="setting-label">API Key</label>
|
||||
<a-input-password
|
||||
v-model="modelForm[activeTab].apiKey"
|
||||
placeholder="sk-..."
|
||||
class="custom-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl border border-slate-100 shadow-sm p-8">
|
||||
<div class="flex items-center space-x-2 mb-6">
|
||||
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
|
||||
<h3 class="text-sm font-bold text-slate-800">模型名称 (modelName)</h3>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<a-input
|
||||
v-model="modelForm[activeTab].modelName"
|
||||
placeholder="手动输入模型 ID,如 gpt-4o"
|
||||
class="custom-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div
|
||||
v-for="m in modelOptions"
|
||||
:key="m.id"
|
||||
@click="selectModel(m.id)"
|
||||
:class="[
|
||||
'p-4 rounded-xl border-2 cursor-pointer transition-all relative overflow-hidden',
|
||||
modelForm[activeTab].modelName === m.id
|
||||
? 'border-[#7816ff] bg-purple-50/30'
|
||||
: 'border-slate-50 bg-slate-50/50 hover:border-slate-200'
|
||||
]"
|
||||
>
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<span class="text-xs font-bold text-slate-800">{{ m.name }}</span>
|
||||
<span
|
||||
v-if="m.tag"
|
||||
class="text-[9px] bg-[#7816ff] text-white px-1.5 py-0.5 rounded"
|
||||
>{{ m.tag }}</span
|
||||
>
|
||||
</div>
|
||||
<p class="text-[10px] text-slate-400 leading-relaxed">{{ m.desc }}</p>
|
||||
<div
|
||||
v-if="modelForm[activeTab].modelName === m.id"
|
||||
class="absolute -right-2 -bottom-2 text-[#7816ff]/10"
|
||||
>
|
||||
<SettingConfig theme="outline" size="48" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl border border-slate-100 shadow-sm p-8">
|
||||
<div class="flex items-center space-x-2 mb-8">
|
||||
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
|
||||
<h3 class="text-sm font-bold text-slate-800">运行参数</h3>
|
||||
<p class="text-[11px] text-[#7816ff] leading-relaxed ml-4 italic">
|
||||
正在配置
|
||||
<span class="font-bold underline">{{
|
||||
activeTab === 'reading' ? '心得生成' : '文本总结'
|
||||
}}</span>
|
||||
专用模型。
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-8">
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<label class="setting-label"
|
||||
>随机性 (Temperature): {{ modelForm[activeTab].temperature }}</label
|
||||
>
|
||||
<span class="text-[10px] text-slate-400">建议心得 0.7-0.8,总结 0.2-0.3</span>
|
||||
</div>
|
||||
<a-slider
|
||||
v-model="modelForm[activeTab].temperature"
|
||||
:min="0"
|
||||
:max="1.2"
|
||||
:step="0.1"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-end pt-4 border-t border-slate-50">
|
||||
<a-button
|
||||
type="primary"
|
||||
class="rounded-lg bg-[#7816ff] border-none px-8 font-bold"
|
||||
@click="handleSave(activeTab)"
|
||||
>
|
||||
保存 {{ activeTab === 'reading' ? '心得' : '总结' }} 配置
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<main class="flex-1 overflow-y-auto">
|
||||
<template v-if="activeMenuItem">
|
||||
<component :is="activeMenuItem"></component>
|
||||
</template>
|
||||
<template v-else>
|
||||
<NotFound />
|
||||
</template>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 样式部分保持不变... */
|
||||
.setting-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 0 4px;
|
||||
}
|
||||
:deep(.custom-input) {
|
||||
background-color: #fcfcfd !important;
|
||||
border: 1px solid #f1f5f9 !important;
|
||||
border-radius: 12px !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
:deep(.custom-tabs .arco-tabs-nav-capsule) {
|
||||
background-color: #f1f5f9;
|
||||
border-radius: 12px;
|
||||
padding: 4px;
|
||||
}
|
||||
:deep(.custom-tabs .arco-tabs-nav-capsule-light) {
|
||||
background-color: #fff;
|
||||
color: #7816ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
.custom-scroll::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.custom-scroll::-webkit-scrollbar-thumb {
|
||||
background: #e5e7eb;
|
||||
border-radius: 10px;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.animate-in {
|
||||
animation: fadeIn 0.4s ease-out forwards;
|
||||
}
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { Right } from '@icon-park/vue-next'
|
||||
|
||||
defineProps<{
|
||||
menuItems: Array<{ key: string; name: string; icon: any }>
|
||||
activeMenu: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:activeMenu'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 h-full overflow-y-auto bg-[#FAFAFB] p-8 custom-scroll">
|
||||
<div class="max-w-5xl mx-auto flex gap-8">
|
||||
<aside class="w-64 shrink-0">
|
||||
<div class="flex items-center space-x-2 mb-8 px-2">
|
||||
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
|
||||
<h2 class="text-base font-bold text-slate-800">设置中心</h2>
|
||||
</div>
|
||||
<nav class="space-y-1">
|
||||
<div
|
||||
v-for="item in menuItems"
|
||||
:key="item.key"
|
||||
@click="emit('update:activeMenu', item.key)"
|
||||
:class="[
|
||||
'flex items-center justify-between px-4 py-3 rounded-xl cursor-pointer transition-all duration-300',
|
||||
activeMenu === item.key
|
||||
? 'bg-white shadow-sm text-[#7816ff] font-bold border border-slate-100'
|
||||
: 'text-slate-500 hover:bg-white/60 hover:text-slate-700'
|
||||
]"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<component
|
||||
:is="item.icon"
|
||||
theme="outline"
|
||||
size="18"
|
||||
:fill="activeMenu === item.key ? '#7816ff' : '#94a3b8'"
|
||||
/>
|
||||
<span class="text-[13px]">{{ item.name }}</span>
|
||||
</div>
|
||||
<right v-if="activeMenu === item.key" theme="outline" size="14" />
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="flex-1">
|
||||
<slot></slot>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
19
src/renderer/src/pages/setting/types/IModel.ts
Normal file
19
src/renderer/src/pages/setting/types/IModel.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ILLMConfig } from '@shared/types/IConfig'
|
||||
|
||||
/**
|
||||
* 模型类型定义
|
||||
* */
|
||||
export interface IModelOption {
|
||||
id: string
|
||||
name: string
|
||||
desc: string
|
||||
tag?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义模型表单类型
|
||||
* */
|
||||
export interface ModelForm {
|
||||
reading: ILLMConfig['config']
|
||||
summary: ILLMConfig['config']
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { Message } from '@arco-design/web-vue'
|
||||
import { useRouterHook } from '@renderer/hooks/useRouterHook'
|
||||
import { Refresh, Send } from '@icon-park/vue-next'
|
||||
import { trpc } from '@renderer/lib/trpc'
|
||||
import { ReadingReflectionsTask } from '@shared/types/reflections'
|
||||
import { IReadingReflectionsTask } from '@shared/types/IReadingReflectionTask'
|
||||
import { eventBus } from '@renderer/lib/eventBus'
|
||||
|
||||
const { go } = useRouterHook()
|
||||
@@ -17,13 +17,14 @@ const form = reactive({
|
||||
quantity: 1,
|
||||
prompt: '',
|
||||
wordCount: 500
|
||||
} as ReadingReflectionsTask)
|
||||
} as IReadingReflectionsTask)
|
||||
|
||||
const occupationOptions = [
|
||||
{ label: '学生', value: 'student' },
|
||||
{ label: '职场白领', value: 'professional' },
|
||||
{ label: '学者/研究员', value: 'scholar' },
|
||||
{ label: '自由职业者', value: 'freelancer' }
|
||||
{ label: '自由职业者', value: 'freelancer' },
|
||||
{ label: '教师', value: 'teacher' }
|
||||
]
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@@ -60,18 +61,12 @@ const handleReset = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-1 overflow-y-auto bg-[#FAFAFB] p-6 custom-scroll">
|
||||
<div class="flex items-center justify-between mb-6 mx-auto">
|
||||
<div class="flex-1 p-6">
|
||||
<div class="flex-1 bg-white rounded-xl border border-slate-100 shadow-sm p-8 custom-scroll">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-1 h-5 bg-[#7816ff] rounded-full"></div>
|
||||
<h2 class="text-base font-bold text-slate-800">创建读书心得任务</h2>
|
||||
</div>
|
||||
<a-button type="text" size="small" class="text-slate-400" @click="go('/task/list')">
|
||||
返回列表
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="x-auto bg-white rounded-xl border border-slate-100 shadow-sm p-8">
|
||||
<a-form :model="form" layout="vertical" @submit="handleSubmit">
|
||||
<a-form-item field="bookName" label="书籍名称" required>
|
||||
<template #label><span class="text-xs font-bold text-slate-700">书籍名称</span></template>
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
export const CONFIG_STORE_KEY = 'reading-helper-secret-key'
|
||||
|
||||
export const NOTIFICATION_KEY = 'notification'
|
||||
|
||||
export const CHAT_MODELS_KEY = 'chatModels'
|
||||
|
||||
@@ -19,20 +19,19 @@ const loggerMiddleware = t.middleware(async ({ path, type, next, input, ctx }) =
|
||||
// 成功日志:包含路径、耗时、参数、简短响应
|
||||
const output = JSON.stringify(result.data) || ''
|
||||
const safeOutput = output.length > 100 ? `${output.substring(0, 100)}...` : output
|
||||
console.log('--- 编码测试:中文内容 ---')
|
||||
logger.info(
|
||||
`[前后台通讯成功:${ctx.window ? ctx.window.getTitle() : ''}] 路径:${path} | 类型:${type} | 耗时:${duration}ms | 参数:${safeInput} | 响应:${safeOutput}`
|
||||
`[rpc:${ctx.window ? ctx.window.getTitle() : ''}] 路径:${path} | 类型:${type} | 耗时:${duration}ms | 参数:${safeInput} | 响应:${safeOutput}`
|
||||
)
|
||||
} else {
|
||||
// 失败日志:包含路径、耗时、参数、错误码及原因
|
||||
// 错误堆栈通常较长,建议在单行中只记录 Message,详细堆栈可另行记录或放在最后
|
||||
logger.error(
|
||||
`[前后台通讯失败:${ctx.window ? ctx.window.getTitle() : ''}] 路径:${path} | 类型:${type} | 耗时:${duration}ms | 参数:${safeInput} | 错误:${result.error.code}(${result.error.message})`
|
||||
`[rpc:${ctx.window ? ctx.window.getTitle() : ''}] 路径:${path} | 类型:${type} | 耗时:${duration}ms | 参数:${safeInput} | 错误:${result.error.code}(${result.error.message})`
|
||||
)
|
||||
|
||||
// 如果需要排查极其严重的崩溃,堆栈信息依然建议保留(这部分无法缩减到一行,否则无法阅读)
|
||||
if (result.error.code === 'INTERNAL_SERVER_ERROR' && result.error.stack) {
|
||||
logger.debug(`详细堆栈: ${result.error.stack.replace(/\n/g, ' ')}`)
|
||||
logger.debug(`rpc详细堆栈: ${result.error.stack.replace(/\n/g, ' ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { router } from './init'
|
||||
import { router } from './index'
|
||||
import { taskRouter } from '@rpc/router/task.router'
|
||||
import { configRouter } from '@rpc/router/config.router'
|
||||
import { noticeRouter } from '@rpc/router/notice.router'
|
||||
|
||||
export const appRouter = router({
|
||||
task: taskRouter,
|
||||
config: configRouter
|
||||
config: configRouter,
|
||||
notice: noticeRouter
|
||||
})
|
||||
|
||||
export type AppRouter = typeof appRouter
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
import { z } from 'zod'
|
||||
import { publicProcedure, router } from '@rpc/init'
|
||||
import { publicProcedure, router } from '@rpc/index'
|
||||
import Store from 'electron-store'
|
||||
import { CONFIG_STORE_KEY } from '@rpc/constants/store_key'
|
||||
import { CHAT_MODELS_KEY, CONFIG_STORE_KEY, NOTIFICATION_KEY } from '@rpc/constants/store_key'
|
||||
import { ILLMConfigSchema, INoticeConfigSchema } from '@shared/types/IConfig'
|
||||
|
||||
// 兼容性处理获取 Store 构造函数
|
||||
const StoreClass = (Store as any).default || Store
|
||||
const store = new StoreClass({ encryptionKey: CONFIG_STORE_KEY })
|
||||
|
||||
export const configRouter = router({
|
||||
// 获取配置
|
||||
// src/main/rpc/routers/configRouter.ts
|
||||
// 获取大模型配置
|
||||
getChatConfigs: publicProcedure.query(() => {
|
||||
const data = store.get('chatModels')
|
||||
const data = store.get(CHAT_MODELS_KEY)
|
||||
|
||||
// 检查是否包含必要的嵌套 Key,如果没有,说明是旧版本数据
|
||||
if (data && !data.reading && !data.summary) {
|
||||
console.log('检测到旧版本配置,正在重置...')
|
||||
store.delete('chatModels') // 删除旧的根键
|
||||
store.delete(CHAT_MODELS_KEY) // 删除旧的根键
|
||||
return null
|
||||
}
|
||||
|
||||
return data || null
|
||||
}),
|
||||
|
||||
// 分类保存配置
|
||||
saveChatConfig: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
type: z.enum(['reading', 'summary']), // 明确支持 reading
|
||||
config: z.object({
|
||||
apiKey: z.string(),
|
||||
baseURL: z.string(),
|
||||
modelName: z.string(),
|
||||
temperature: z.number()
|
||||
})
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
// 存储到 chatModels.reading 或 chatModels.summary
|
||||
store.set(`chatModels.${input.type}`, input.config)
|
||||
return { success: true }
|
||||
})
|
||||
// 保存大模型配置
|
||||
saveChatConfig: publicProcedure.input(ILLMConfigSchema).mutation(async ({ input }) => {
|
||||
const { type, config } = input
|
||||
store.set(`${CHAT_MODELS_KEY}.${type}`, config)
|
||||
return { success: true }
|
||||
}),
|
||||
|
||||
// 获取通知配置
|
||||
getNoticeConfigs: publicProcedure.query(() => {
|
||||
const data = store.get(NOTIFICATION_KEY)
|
||||
return data || null
|
||||
}),
|
||||
|
||||
// 保存通知配置
|
||||
saveNoticeConfigs: publicProcedure.input(INoticeConfigSchema).mutation(async ({ input }) => {
|
||||
const {} = input
|
||||
store.set(NOTIFICATION_KEY, input)
|
||||
return { success: true }
|
||||
})
|
||||
})
|
||||
|
||||
36
src/rpc/router/notice.router.ts
Normal file
36
src/rpc/router/notice.router.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { z } from 'zod'
|
||||
import { router, publicProcedure } from '@rpc/index'
|
||||
import { Notification } from 'electron'
|
||||
|
||||
export const noticeRouter = router({
|
||||
// 发送系统通知
|
||||
send: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
title: z.string(),
|
||||
body: z.string(),
|
||||
silent: z.boolean().optional().default(false)
|
||||
})
|
||||
)
|
||||
.mutation(({ input }) => {
|
||||
// 检查系统是否支持通知
|
||||
if (!Notification.isSupported()) {
|
||||
throw new Error('当前系统不支持桌面通知')
|
||||
}
|
||||
|
||||
const notification = new Notification({
|
||||
title: input.title,
|
||||
body: input.body,
|
||||
silent: input.silent
|
||||
})
|
||||
|
||||
notification.show()
|
||||
|
||||
// 监听点击事件(可选)
|
||||
notification.on('click', () => {
|
||||
console.log('用户点击了通知')
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,4 @@
|
||||
import { publicProcedure, router } from '@rpc/init'
|
||||
import { ReadingReflectionsTaskSchema } from '@shared/types/reflections'
|
||||
import { publicProcedure, router } from '@rpc/index'
|
||||
import { observable } from '@trpc/server/observable'
|
||||
import {
|
||||
readingReflectionsTaskManager,
|
||||
@@ -13,7 +12,8 @@ import { ReadingReflectionTaskBatch } from '@main/db/entities/ReadingReflectionT
|
||||
import { ReadingReflectionTaskItem } from '@main/db/entities/ReadingReflectionTaskItem'
|
||||
import {
|
||||
IReadingReflectionTaskBatch,
|
||||
IReadingReflectionTaskItem
|
||||
IReadingReflectionTaskItem,
|
||||
IReadingReflectionsTaskSchema
|
||||
} from '@shared/types/IReadingReflectionTask'
|
||||
|
||||
/**
|
||||
@@ -23,10 +23,10 @@ import {
|
||||
export const taskRouter = router({
|
||||
/**
|
||||
* 创建读书心得生成任务
|
||||
* @param input {ReadingReflectionsTaskSchema}
|
||||
* @param input {IReadingReflectionsTaskSchema}
|
||||
*/
|
||||
createReadingReflectionsTask: publicProcedure
|
||||
.input(ReadingReflectionsTaskSchema)
|
||||
.input(IReadingReflectionsTaskSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const taskId = uuidv4()
|
||||
// 启动后台异步任务
|
||||
|
||||
32
src/shared/types/IConfig.ts
Normal file
32
src/shared/types/IConfig.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
/**
|
||||
* 大模型类型枚举
|
||||
* */
|
||||
export type LLMType = 'reading' | 'summary'
|
||||
|
||||
/**
|
||||
* 定义大模型配置
|
||||
* */
|
||||
export const ILLMConfigSchema = z.object({
|
||||
type: z.enum(['reading', 'summary']), // 明确支持 reading
|
||||
config: z.object({
|
||||
apiKey: z.string(), // api key
|
||||
baseURL: z.string(), // base url
|
||||
modelName: z.string(), // 模型名称
|
||||
temperature: z.number() // 模型温度
|
||||
})
|
||||
})
|
||||
export type ILLMConfig = z.infer<typeof ILLMConfigSchema>
|
||||
|
||||
/**
|
||||
* 定义通知配置
|
||||
* */
|
||||
export const INoticeConfigSchema = z.object({
|
||||
masterSwitch: z.boolean().default(true), // 总开关
|
||||
taskCompleted: z.boolean().default(true), // 任务完成时提醒
|
||||
taskFailed: z.boolean().default(true), // 任务失败时提醒
|
||||
silentMode: z.boolean().default(false) // 静默模式(仅弹窗无声音)
|
||||
})
|
||||
|
||||
export type INoticeConfig = z.infer<typeof INoticeConfigSchema>
|
||||
@@ -1,3 +1,40 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
/**
|
||||
* 读者职业枚举
|
||||
*/
|
||||
export type Occupation = 'student' | 'teacher' | 'professional' | 'researcher' | 'other'
|
||||
|
||||
/**
|
||||
* 读书心得生成请求任务模型
|
||||
*/
|
||||
export const IReadingReflectionsTaskSchema = z.object({
|
||||
bookName: z.string().min(1, '书名不能为空'),
|
||||
author: z.string().optional(),
|
||||
description: z.string(),
|
||||
occupation: z.enum(['student', 'teacher', 'professional', 'researcher', 'other']),
|
||||
prompt: z.string(),
|
||||
wordCount: z.number().default(1000),
|
||||
quantity: z.number().min(1).max(5).default(1),
|
||||
language: z.enum(['zh', 'en']).optional(),
|
||||
tone: z.string().optional()
|
||||
})
|
||||
export type IReadingReflectionsTask = z.infer<typeof IReadingReflectionsTaskSchema>
|
||||
|
||||
/**
|
||||
* 任务响应结果模型
|
||||
*/
|
||||
export interface IReadingReflectionsResponse {
|
||||
taskId: string
|
||||
reflections: Array<{
|
||||
title: string // 心得标题
|
||||
content: string // 心得正文
|
||||
keywords: string[] // 提取的关键词
|
||||
summary: string // 内容摘要
|
||||
}>
|
||||
status: 'pending' | 'completed' | 'failed'
|
||||
}
|
||||
|
||||
/**
|
||||
* 对应数据库中的批次记录 (主任务)
|
||||
*/
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
/**
|
||||
* 读者职业枚举
|
||||
*/
|
||||
export type Occupation = 'student' | 'teacher' | 'professional' | 'researcher' | 'other'
|
||||
|
||||
/**
|
||||
* 读书心得生成请求任务模型
|
||||
*/
|
||||
export const ReadingReflectionsTaskSchema = z.object({
|
||||
bookName: z.string().min(1, '书名不能为空'),
|
||||
author: z.string().optional(),
|
||||
description: z.string(),
|
||||
occupation: z.enum(['student', 'teacher', 'professional', 'researcher', 'other']),
|
||||
prompt: z.string(),
|
||||
wordCount: z.number().default(1000),
|
||||
quantity: z.number().min(1).max(5).default(1),
|
||||
language: z.enum(['zh', 'en']).optional(),
|
||||
tone: z.string().optional()
|
||||
})
|
||||
export type ReadingReflectionsTask = z.infer<typeof ReadingReflectionsTaskSchema>
|
||||
|
||||
/**
|
||||
* 任务响应结果模型
|
||||
*/
|
||||
export interface ReadingReflectionsResponse {
|
||||
taskId: string
|
||||
reflections: Array<{
|
||||
title: string // 心得标题
|
||||
content: string // 心得正文
|
||||
keywords: string[] // 提取的关键词
|
||||
summary: string // 内容摘要
|
||||
}>
|
||||
status: 'pending' | 'completed' | 'failed'
|
||||
}
|
||||
Reference in New Issue
Block a user