fix:基本实现相关功能
This commit is contained in:
30
src/main/db/data-source.ts
Normal file
30
src/main/db/data-source.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'reflect-metadata'
|
||||
import { DataSource } from 'typeorm'
|
||||
import { app } from 'electron'
|
||||
import path from 'path'
|
||||
import { ReadingReflectionTaskBatch } from '@main/db/entities/ReadingReflectionTaskBatch'
|
||||
import { ReadingReflectionTaskItem } from '@main/db/entities/ReadingReflectionTaskItem'
|
||||
|
||||
const dbPath = app.isPackaged
|
||||
? path.join(app.getPath('userData'), 'reflections.db')
|
||||
: path.join(process.cwd(), 'db.sqlite')
|
||||
console.log('--- 数据库存储绝对路径 ---')
|
||||
console.log(dbPath)
|
||||
console.log('-----------------------')
|
||||
export const AppDataSource = new DataSource({
|
||||
type: 'better-sqlite3',
|
||||
database: dbPath,
|
||||
synchronize: true, // 开发环境下自动同步表结构
|
||||
logging: true,
|
||||
entities: [ReadingReflectionTaskBatch, ReadingReflectionTaskItem],
|
||||
migrations: [],
|
||||
subscribers: []
|
||||
})
|
||||
|
||||
// 初始化方法,在 Electron app.whenReady() 中调用
|
||||
export const initDB = async () => {
|
||||
if (!AppDataSource.isInitialized) {
|
||||
await AppDataSource.initialize()
|
||||
console.log('TypeORM SQLite Data Source has been initialized!')
|
||||
}
|
||||
}
|
||||
38
src/main/db/entities/ReadingReflectionTaskBatch.ts
Normal file
38
src/main/db/entities/ReadingReflectionTaskBatch.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Column, CreateDateColumn, Entity, OneToMany, PrimaryColumn } from 'typeorm'
|
||||
import { ReadingReflectionTaskItem } from './ReadingReflectionTaskItem'
|
||||
import { IReadingReflectionTaskBatch } from '@shared/types/IReadingReflectionTask'
|
||||
|
||||
@Entity('reading_reflection_task_batches')
|
||||
export class ReadingReflectionTaskBatch implements IReadingReflectionTaskBatch {
|
||||
@PrimaryColumn({ type: 'varchar' })
|
||||
id!: string
|
||||
|
||||
@Column({ type: 'varchar' })
|
||||
bookName!: string
|
||||
|
||||
@Column({ type: 'int', default: 1 })
|
||||
totalCount!: number
|
||||
|
||||
/**
|
||||
* 批次总体状态
|
||||
* PENDING: 队列中, PROCESSING: 正在生成(部分完成), COMPLETED: 全部完成, FAILED: 彻底失败
|
||||
*/
|
||||
@Column({ type: 'varchar', default: 'PENDING' })
|
||||
status!: string
|
||||
|
||||
/**
|
||||
* 总体进度 (0-100)
|
||||
* 公式: (已完成子任务数 / 总任务数) * 100
|
||||
*/
|
||||
@Column({ type: 'int', default: 0 })
|
||||
progress!: number
|
||||
|
||||
@CreateDateColumn({ type: 'datetime' })
|
||||
createdAt!: Date
|
||||
|
||||
@OneToMany(() => ReadingReflectionTaskItem, (item) => item.batch, {
|
||||
cascade: true, // 级联操作:删除 Batch 时自动删除所有 Item
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
items: ReadingReflectionTaskItem[]
|
||||
}
|
||||
31
src/main/db/entities/ReadingReflectionTaskItem.ts
Normal file
31
src/main/db/entities/ReadingReflectionTaskItem.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm'
|
||||
import { ReadingReflectionTaskBatch } from './ReadingReflectionTaskBatch'
|
||||
import { IReadingReflectionTaskItem } from '@shared/types/IReadingReflectionTask'
|
||||
|
||||
@Entity('reading_reflection_task_items')
|
||||
export class ReadingReflectionTaskItem implements IReadingReflectionTaskItem {
|
||||
@PrimaryColumn({ type: 'varchar' })
|
||||
id: string
|
||||
|
||||
@Column({ type: 'varchar' })
|
||||
status: 'PENDING' | 'WRITING' | 'COMPLETED' | 'FAILED'
|
||||
|
||||
@Column({ type: 'int', default: 0 })
|
||||
progress: number
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
content?: string
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
title?: string // 存储标题、摘要、关键词等
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
summary?: string
|
||||
|
||||
@Column({ type: 'simple-json', nullable: true })
|
||||
keywords?: string[]
|
||||
// 多对一关联
|
||||
@ManyToOne(() => ReadingReflectionTaskBatch, (batch) => batch.items)
|
||||
batch!: ReadingReflectionTaskBatch
|
||||
resultData: any
|
||||
}
|
||||
87
src/main/index.ts
Normal file
87
src/main/index.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { app, BrowserWindow, ipcMain, shell } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
|
||||
import icon from '../../resources/icon.png?asset'
|
||||
import { createContext } from '@rpc/context'
|
||||
import { appRouter } from '@rpc/router'
|
||||
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,
|
||||
resizable: false,
|
||||
autoHideMenuBar: true,
|
||||
...(process.platform === 'linux' ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false
|
||||
}
|
||||
})
|
||||
|
||||
// 核心绑定:使用 exposeElectronTRPC 适配当前窗口
|
||||
createIPCHandler({
|
||||
router: appRouter,
|
||||
windows: [mainWindow], // 将当前新创建的窗口放入数组
|
||||
createContext: createContext
|
||||
})
|
||||
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow.show()
|
||||
})
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
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 {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
}
|
||||
|
||||
// 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')
|
||||
|
||||
// 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.
|
||||
126
src/main/manager/readingReflectionsTaskManager.ts
Normal file
126
src/main/manager/readingReflectionsTaskManager.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { EventEmitter } from 'events'
|
||||
import pLimit from 'p-limit' // 建议使用 v2.2.0 以兼容 CJS
|
||||
import { readingReflectionGraph } from '@main/services/ai/graph/readingReflectionGraph'
|
||||
import { AppDataSource } from '@main/db/data-source'
|
||||
import { ReadingReflectionTaskBatch } from '@main/db/entities/ReadingReflectionTaskBatch'
|
||||
import { ReadingReflectionTaskItem } from '@main/db/entities/ReadingReflectionTaskItem'
|
||||
|
||||
export const readingReflectionTaskEvent = new EventEmitter()
|
||||
|
||||
class TaskManager {
|
||||
private limit = pLimit(2)
|
||||
private batchRepo = AppDataSource.getRepository(ReadingReflectionTaskBatch)
|
||||
private itemRepo = AppDataSource.getRepository(ReadingReflectionTaskItem)
|
||||
|
||||
/**
|
||||
* 更新主任务汇总进度
|
||||
*/
|
||||
private async updateBatchStatus(batchId: string) {
|
||||
const items = await this.itemRepo.find({ where: { batch: { id: batchId } } })
|
||||
if (items.length === 0) return
|
||||
|
||||
const avgProgress = Math.round(items.reduce((acc, i) => acc + i.progress, 0) / items.length)
|
||||
let status = 'PROCESSING'
|
||||
if (avgProgress === 100) status = 'COMPLETED'
|
||||
if (items.every((i) => i.status === 'FAILED')) status = 'FAILED'
|
||||
|
||||
await this.batchRepo.update(batchId, { progress: avgProgress, status })
|
||||
|
||||
// 发送给左侧列表订阅者
|
||||
readingReflectionTaskEvent.emit('batchProgressUpdate', {
|
||||
batchId,
|
||||
progress: avgProgress,
|
||||
status
|
||||
})
|
||||
}
|
||||
|
||||
async startBatchTask(taskId: string, task: any) {
|
||||
const total = task.quantity || 1
|
||||
|
||||
// 1. 初始化主任务
|
||||
const batch = this.batchRepo.create({ id: taskId, bookName: task.bookName, totalCount: total })
|
||||
await this.batchRepo.save(batch)
|
||||
// 发送给左侧列表订阅者
|
||||
readingReflectionTaskEvent.emit('batchProgressUpdate', {
|
||||
batchId: taskId,
|
||||
progress: 0,
|
||||
status: 'PROCESSING'
|
||||
})
|
||||
|
||||
const promises = Array.from({ length: total }).map((_, index) => {
|
||||
const subTaskId = total === 1 ? taskId : `${taskId}-${index}`
|
||||
|
||||
return this.limit(async () => {
|
||||
try {
|
||||
const item = this.itemRepo.create({ id: subTaskId, batch: batch, status: 'PENDING' })
|
||||
await this.itemRepo.save(item)
|
||||
|
||||
const stream = await readingReflectionGraph.stream(
|
||||
{ ...task },
|
||||
{ configurable: { thread_id: subTaskId } }
|
||||
)
|
||||
|
||||
let finalResult: any = {}
|
||||
|
||||
for await (const chunk of stream) {
|
||||
// 处理生成正文节点
|
||||
if (chunk.generateReadingReflectionContent) {
|
||||
const contentData = chunk.generateReadingReflectionContent
|
||||
await this.itemRepo.update(subTaskId, {
|
||||
status: 'WRITING',
|
||||
progress: 50,
|
||||
content: contentData.content,
|
||||
title: contentData.title
|
||||
})
|
||||
finalResult = { ...finalResult, ...contentData }
|
||||
await this.updateBatchStatus(taskId)
|
||||
this.emitProgress(taskId, index, total, 60, '正文已生成...')
|
||||
}
|
||||
|
||||
// 处理生成摘要节点
|
||||
if (chunk.generateReadingReflectionSummary) {
|
||||
const summaryData = chunk.generateReadingReflectionSummary
|
||||
finalResult = { ...finalResult, ...summaryData }
|
||||
await this.itemRepo.update(subTaskId, {
|
||||
status: 'COMPLETED',
|
||||
progress: 100,
|
||||
summary: summaryData.summary,
|
||||
title: finalResult.title,
|
||||
keywords: summaryData.keywords
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await this.updateBatchStatus(taskId)
|
||||
this.emitProgress(taskId, index, total, 100, '生成成功', finalResult)
|
||||
} catch (error) {
|
||||
await this.itemRepo.update(subTaskId, { status: 'FAILED', progress: 0 })
|
||||
await this.updateBatchStatus(taskId)
|
||||
this.emitProgress(taskId, index, total, 0, '生成失败')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
private emitProgress(
|
||||
taskId: string,
|
||||
index: number,
|
||||
total: number,
|
||||
progress: number,
|
||||
status: string,
|
||||
result?: any
|
||||
) {
|
||||
const displayId = total === 1 ? taskId : `${taskId}-${index}`
|
||||
readingReflectionTaskEvent.emit('readingReflectionTaskProgress', {
|
||||
taskId: displayId,
|
||||
progress,
|
||||
status: status, // 传枚举 Key
|
||||
statusText: `[任务${index + 1}/${total}] ${status}`, // 传描述文字
|
||||
result
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const readingReflectionsTaskManager = new TaskManager()
|
||||
13
src/main/services/ai/graph/readingReflectionGraph.ts
Normal file
13
src/main/services/ai/graph/readingReflectionGraph.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { END, START, StateGraph } from '@langchain/langgraph'
|
||||
import { ReadingReflectionState } from '../state/readingReflectionState'
|
||||
import { generateReadingReflectionContentNode } from '../nodes/readingReflectionContent'
|
||||
import { generateReadingReflectionSummaryNode } from '../nodes/readingReflectionSummary'
|
||||
|
||||
const workflow = new StateGraph(ReadingReflectionState)
|
||||
.addNode('generateReadingReflectionContent', generateReadingReflectionContentNode)
|
||||
.addNode('generateReadingReflectionSummary', generateReadingReflectionSummaryNode)
|
||||
.addEdge(START, 'generateReadingReflectionContent') // 开始 -> 生成正文
|
||||
.addEdge('generateReadingReflectionContent', 'generateReadingReflectionSummary') // 正文生成后 -> 生成摘要
|
||||
.addEdge('generateReadingReflectionSummary', END) // 摘要生成后 -> 结束
|
||||
|
||||
export const readingReflectionGraph = workflow.compile()
|
||||
24
src/main/services/ai/llmService.ts
Normal file
24
src/main/services/ai/llmService.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ChatOpenAI } from '@langchain/openai'
|
||||
import Store from 'electron-store'
|
||||
import { CONFIG_STORE_KEY } from '@rpc/constants/store_key'
|
||||
|
||||
const StoreClass = (Store as any).default || Store
|
||||
const store = new StoreClass({ encryptionKey: CONFIG_STORE_KEY })
|
||||
|
||||
export const createChatModel = (type: 'reading' | 'summary', schema: any) => {
|
||||
const config = store.get(`chatModels.${type}`) as any
|
||||
console.log('chatModels', config)
|
||||
|
||||
if (!config || !config.apiKey) {
|
||||
throw new Error(`请先在设置中配置 ${type === 'reading' ? '心得' : '总结'} 模型的 API Key`)
|
||||
}
|
||||
|
||||
return new ChatOpenAI({
|
||||
apiKey: config.apiKey,
|
||||
configuration: {
|
||||
baseURL: config.baseURL || 'https://api.openai.com/v1'
|
||||
},
|
||||
modelName: config.modelName,
|
||||
temperature: config.temperature
|
||||
}).withStructuredOutput(schema)
|
||||
}
|
||||
55
src/main/services/ai/nodes/readingReflectionContent.ts
Normal file
55
src/main/services/ai/nodes/readingReflectionContent.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { PromptTemplate } from '@langchain/core/prompts'
|
||||
import { ReadingReflectionState } from '../state/readingReflectionState'
|
||||
import { REFLECTION_CONTENT_PROMPT } from '@main/services/ai/prompts/readingReflactionPrompts'
|
||||
import { z } from 'zod'
|
||||
import { AppDataSource } from '@main/db/data-source'
|
||||
import { ReadingReflectionTaskItem } from '@main/db/entities/ReadingReflectionTaskItem'
|
||||
import { createChatModel } from '@main/services/ai/llmService'
|
||||
|
||||
export const generateReadingReflectionContentNode = async (
|
||||
state: typeof ReadingReflectionState.State,
|
||||
config?: any
|
||||
) => {
|
||||
const taskId = config?.configurable?.thread_id
|
||||
const repo = AppDataSource.getRepository(ReadingReflectionTaskItem)
|
||||
const prompt = PromptTemplate.fromTemplate(REFLECTION_CONTENT_PROMPT)
|
||||
|
||||
const ReadingReflectionContentSchema = z.object({
|
||||
title: z.string().describe('标题'),
|
||||
content: z.string().describe('正文')
|
||||
})
|
||||
|
||||
const contentModel = createChatModel('reading', ReadingReflectionContentSchema)
|
||||
|
||||
const chain = prompt.pipe(contentModel)
|
||||
|
||||
// 针对不同职业的微调逻辑(可选)
|
||||
const occupationLabel =
|
||||
{
|
||||
student: '学生',
|
||||
teacher: '教师',
|
||||
professional: '职场人士',
|
||||
researcher: '科研工作者',
|
||||
other: '专业人士'
|
||||
}[state.occupation] || state.occupation
|
||||
|
||||
const res = (await chain.invoke({
|
||||
occupation: occupationLabel,
|
||||
bookName: state.bookName,
|
||||
author: state.author || '佚名',
|
||||
description: state.description,
|
||||
tone: state.tone || '温暖且理性',
|
||||
wordCount: state.wordCount || 1000
|
||||
})) as { title: string; content: string }
|
||||
|
||||
// 节点内部直接更新数据库状态
|
||||
if (taskId) {
|
||||
await repo.update(taskId, {
|
||||
status: 'WRITING',
|
||||
title: res.title,
|
||||
content: res.content,
|
||||
progress: 60
|
||||
})
|
||||
}
|
||||
return { title: res.title, content: res.content }
|
||||
}
|
||||
46
src/main/services/ai/nodes/readingReflectionSummary.ts
Normal file
46
src/main/services/ai/nodes/readingReflectionSummary.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { PromptTemplate } from '@langchain/core/prompts'
|
||||
import { ReadingReflectionState } from '../state/readingReflectionState'
|
||||
import { REFLECTION_SUMMARY_PROMPT } from '@main/services/ai/prompts/readingReflactionPrompts'
|
||||
import { z } from 'zod'
|
||||
import { createChatModel } from '@main/services/ai/llmService'
|
||||
|
||||
/**
|
||||
* 步骤 3:生成摘要和关键词
|
||||
* 该节点会接收上一个节点生成的 title 和 content
|
||||
*/
|
||||
export const generateReadingReflectionSummaryNode = async (
|
||||
state: typeof ReadingReflectionState.State
|
||||
) => {
|
||||
const prompt = PromptTemplate.fromTemplate(REFLECTION_SUMMARY_PROMPT)
|
||||
|
||||
// 定义输出的结构
|
||||
const summarySchema = z.object({
|
||||
summary: z.string().describe('100字以内的摘要'),
|
||||
keywords: z.array(z.string()).describe('3-5个关键词')
|
||||
})
|
||||
// const summaryModel = new ChatOpenAI({
|
||||
// apiKey: 'sk-172309b16482e6ad4264b1cd89f010d8',
|
||||
// configuration: {
|
||||
// baseURL: 'https://apis.iflow.cn/v1'
|
||||
// },
|
||||
// modelName: 'deepseek-v3.2',
|
||||
// temperature: 0.3
|
||||
// }).withStructuredOutput(summarySchema)
|
||||
const summaryModel = createChatModel('summary', summarySchema)
|
||||
|
||||
const chain = prompt.pipe(summaryModel)
|
||||
|
||||
const res = (await chain.invoke({
|
||||
title: state.title,
|
||||
content: state.content
|
||||
})) as {
|
||||
summary: string
|
||||
keywords: string[]
|
||||
}
|
||||
|
||||
// 返回的结果会自动合并到 ReflectionState 中
|
||||
return {
|
||||
summary: res.summary,
|
||||
keywords: res.keywords
|
||||
}
|
||||
}
|
||||
61
src/main/services/ai/prompts/readingReflactionPrompts.ts
Normal file
61
src/main/services/ai/prompts/readingReflactionPrompts.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
export const REFLECTION_CONTENT_PROMPT = `
|
||||
# Role
|
||||
你是一位在 {occupation} 领域拥有20年深厚资历的专家。你文笔{tone},擅长将理论书籍的核心观点与真实的行业场景深度结合。
|
||||
|
||||
# Goal
|
||||
请根据提供的书籍信息,以专业视角撰写一份高质量的读书心得。
|
||||
|
||||
# Constraints & Rules
|
||||
1. **禁止身份陈述 (No Self-Introduction)**:
|
||||
- **严禁**在开头或正文任何地方出现“作为一名...”、“我有...经验”、“从教/从业二十年”等自报家门的句子。
|
||||
- 你的专业性应通过文字深度、对行业痛点的理解和专业术语自然流露,而不是通过宣称身份。
|
||||
2. **场景化切入 (Direct Opening)**:
|
||||
- 开篇请直接从书籍的某个核心观点、一个具体的职场痛点、或一个生动的行业场景切入。
|
||||
- 例如:不要说“作为医生我经常看到...”,而要说“当走廊的灯光亮起,面对那些复杂的病例时,我常在想...”
|
||||
3. **职业化表达 (Professionalism)**:
|
||||
- 结合该职业特有的工作场景,将书籍内容转化为可操作的洞察。
|
||||
4. **输出限制**:
|
||||
- 必须严格按照 JSON 格式输出,不含任何 Markdown 代码块标签或解释。
|
||||
|
||||
# Context Data
|
||||
- 职业背景:{occupation}
|
||||
- 书籍名称:{bookName}
|
||||
- 作者:{author}
|
||||
- 书籍描述:{description}
|
||||
- 语气风格:{tone}
|
||||
- 目标字数:{wordCount}
|
||||
|
||||
# Output JSON Format
|
||||
{{
|
||||
"title": "此处填写读书心得标题",
|
||||
"content": "此处填写读书心得正文内容"
|
||||
}}
|
||||
`
|
||||
export const REFLECTION_SUMMARY_PROMPT = `
|
||||
# Role
|
||||
你是一位金牌图书营销编辑,擅长撰写能够瞬间抓住读者眼球的“内容提要”和“搜索关键词”。你对文字有着极高的敏感度,能够从冗长的感悟中精准钩织出书籍的核心灵魂。
|
||||
|
||||
# Goal
|
||||
基于提供的【读书心得正文】,提炼出一段极具感染力的摘要,并提取能精准覆盖内容核心的关键词。
|
||||
|
||||
# Constraints & Rules
|
||||
1. **摘要编写要求 (Summary)**:
|
||||
- **拒绝平铺直叙**:不要使用“本文介绍了...”这类陈旧开场,直接切入感悟的核心点。
|
||||
- **受众共鸣**:摘要应体现出书籍对读者的实际启发或职场/生活改变。
|
||||
- **精炼控制**:字数严格控制在 80-100 字之间,语言优美、有力。
|
||||
2. **关键词提取要求 (Keywords)**:
|
||||
- **多维覆盖**:必须包含 3-5 个词。
|
||||
- **权重分配**:1个书籍核心理念词,1-2个职业/场景词(如:幼儿教育、职业成长),1个情感/启发词。
|
||||
3. **输出限制**:
|
||||
- 必须且只能输出标准 JSON 格式,严禁任何正文外的解释说明。
|
||||
|
||||
# Context Data
|
||||
- 读书心得标题:{title}
|
||||
- 读书心得正文:{content}
|
||||
|
||||
# Output JSON Format
|
||||
{{
|
||||
"summary": "此处填写精炼且感人的摘要内容",
|
||||
"keywords": ["关键词1", "关键词2", "关键词3"]
|
||||
}}
|
||||
`
|
||||
36
src/main/services/ai/state/readingReflectionState.ts
Normal file
36
src/main/services/ai/state/readingReflectionState.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Annotation } from '@langchain/langgraph'
|
||||
import { Occupation } from '@shared/types/reflections'
|
||||
|
||||
export const ReadingReflectionState = Annotation.Root({
|
||||
// 输入任务
|
||||
bookName: Annotation<string>(),
|
||||
author: Annotation<string | undefined>(),
|
||||
description: Annotation<string>(),
|
||||
occupation: Annotation<Occupation>(),
|
||||
tone: Annotation<string | undefined>(),
|
||||
wordCount: Annotation<number>(),
|
||||
|
||||
// 标题:使用简单的覆盖逻辑
|
||||
title: Annotation<string>({
|
||||
reducer: (_oldValue, newValue) => newValue,
|
||||
default: () => ''
|
||||
}),
|
||||
|
||||
// 内容
|
||||
content: Annotation<string>({
|
||||
reducer: (_oldValue, newValue) => newValue,
|
||||
default: () => ''
|
||||
}),
|
||||
|
||||
// 摘要
|
||||
summary: Annotation<string>({
|
||||
reducer: (_oldValue, newValue) => newValue,
|
||||
default: () => ''
|
||||
}),
|
||||
|
||||
// 关键词:数组通常使用追加或替换逻辑
|
||||
keywords: Annotation<string[]>({
|
||||
reducer: (_oldValue, newValue) => newValue,
|
||||
default: () => []
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user