feat: ship v0.3.0 with P2.1 editor polish and P3 AI collaboration
Add chapter reorder, search replace, inspiration capture, and AI chat with context editing, settings, offline handling, and naming checks. Fix dev black screen by keeping process.env reads in the main process only. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import { stripHtml } from './word-count'
|
||||
import type { AiContextBinding, AiContextBuildResult, ContextPreviewItem } from '../../shared/types'
|
||||
import type { BookRegistryService } from './book-registry'
|
||||
|
||||
const CHAPTER_CHAR_LIMIT = 2000
|
||||
const OTHER_CHAR_LIMIT = 500
|
||||
const TOTAL_CHAR_LIMIT = 16 * 1024
|
||||
|
||||
interface BlockCandidate {
|
||||
sortKey: number
|
||||
text: string
|
||||
preview: ContextPreviewItem
|
||||
}
|
||||
|
||||
function truncate(text: string, max: number): string {
|
||||
if (text.length <= max) return text
|
||||
return `${text.slice(0, max)}…`
|
||||
}
|
||||
|
||||
function formatBlock(kind: ContextPreviewItem['kind'], title: string, body: string): string {
|
||||
const label =
|
||||
kind === 'chapter' ? '章节' : kind === 'outline' ? '大纲' : kind === 'setting' ? '设定' : '灵感'
|
||||
return `【${label}】${title}\n${body}`
|
||||
}
|
||||
|
||||
export class AiContextBuilderService {
|
||||
build(
|
||||
binding: AiContextBinding,
|
||||
registry: BookRegistryService,
|
||||
bookId: string,
|
||||
penName: string
|
||||
): AiContextBuildResult {
|
||||
const meta = registry.getMeta(bookId)
|
||||
if (!meta) throw new Error('Book not found')
|
||||
|
||||
const candidates: BlockCandidate[] = []
|
||||
|
||||
for (const id of binding.chapterIds) {
|
||||
const chapter = registry.getChapterRepo(bookId).get(id)
|
||||
if (!chapter) continue
|
||||
const plain = stripHtml(chapter.content)
|
||||
const body = truncate(plain, CHAPTER_CHAR_LIMIT)
|
||||
candidates.push({
|
||||
sortKey: chapter.sortOrder,
|
||||
text: formatBlock('chapter', chapter.title, body),
|
||||
preview: {
|
||||
kind: 'chapter',
|
||||
id,
|
||||
title: chapter.title,
|
||||
excerpt: body,
|
||||
charCount: body.length
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for (const id of binding.outlineIds) {
|
||||
const item = registry.getOutlineRepo(bookId).get(id)
|
||||
if (!item) continue
|
||||
const body = truncate(stripHtml(item.description), OTHER_CHAR_LIMIT)
|
||||
candidates.push({
|
||||
sortKey: item.sortOrder,
|
||||
text: formatBlock('outline', item.title, body),
|
||||
preview: {
|
||||
kind: 'outline',
|
||||
id,
|
||||
title: item.title,
|
||||
excerpt: body,
|
||||
charCount: body.length
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for (const id of binding.settingIds) {
|
||||
const item = registry.getSettingRepo(bookId).get(id)
|
||||
if (!item) continue
|
||||
const body = truncate(stripHtml(item.description), OTHER_CHAR_LIMIT)
|
||||
candidates.push({
|
||||
sortKey: 0,
|
||||
text: formatBlock('setting', item.name, body),
|
||||
preview: {
|
||||
kind: 'setting',
|
||||
id,
|
||||
title: item.name,
|
||||
excerpt: body,
|
||||
charCount: body.length
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for (const id of binding.inspirationIds) {
|
||||
const item = registry.getInspirationRepo(bookId).get(id)
|
||||
if (!item) continue
|
||||
const body = truncate(stripHtml(item.content), OTHER_CHAR_LIMIT)
|
||||
candidates.push({
|
||||
sortKey: Date.parse(item.updatedAt) || 0,
|
||||
text: formatBlock('inspiration', item.title, body),
|
||||
preview: {
|
||||
kind: 'inspiration',
|
||||
id,
|
||||
title: item.title,
|
||||
excerpt: body,
|
||||
charCount: body.length
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
candidates.sort((a, b) => b.sortKey - a.sortKey)
|
||||
|
||||
const selected: BlockCandidate[] = []
|
||||
let totalChars = 0
|
||||
for (const candidate of candidates) {
|
||||
const nextTotal = totalChars + candidate.text.length + (selected.length > 0 ? 2 : 0)
|
||||
if (nextTotal > TOTAL_CHAR_LIMIT) break
|
||||
selected.push(candidate)
|
||||
totalChars = nextTotal
|
||||
}
|
||||
|
||||
const contextBlocks = selected.map((x) => x.text).join('\n\n')
|
||||
const systemPrompt = `你是笔临 AI 写作助手。作者笔名:${penName}。书籍:${meta.name}。
|
||||
以下是与本书相关的上下文(由作者勾选):
|
||||
${contextBlocks || '(无)'}
|
||||
请用简体中文回答,尊重作者的创作主权。`
|
||||
|
||||
return {
|
||||
systemPrompt,
|
||||
preview: selected.map((x) => x.preview)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const aiContextBuilder = new AiContextBuilderService()
|
||||
Reference in New Issue
Block a user