2afbb83c43
Co-authored-by: Cursor <cursoragent@cursor.com>
928 lines
28 KiB
Markdown
928 lines
28 KiB
Markdown
# 笔临 P6 AI 知识上下文整合 Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 交付 v0.7.0:已审核知识库条目半自动推荐 + 用户勾选确认 + 注入对话/交互/自动/向导四种 AI 模式;修复写作流 `contextJson` 仅存标题摘要的问题。
|
||
|
||
**Architecture:** 新增 `KnowledgeRetrievalService` 对 `approved` 知识条目综合打分;扩展 `AiContextBinding.knowledgeIds` 与 `AiContextBuilderService` 合并知识块;`FlowContextPayload` 统一写作流上下文存储;`KnowledgeSuggestList` + `ContextEditorModal` 知识 Tab 供四种模式共用。测试 **禁止 Mock** AI 集成/E2E 打真实 LM Studio。
|
||
|
||
**Tech Stack:** Electron 36、electron-vite、React 18、TypeScript、node:sqlite、Zustand、i18next、Vitest、Playwright
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-07-07-bilin-p6-ai-context-design.md`
|
||
|
||
**LM Studio 默认:** `baseUrl=http://127.0.0.1:1234`,`model=gemma-4-e4b-it`
|
||
|
||
---
|
||
|
||
## File Map
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `src/shared/types.ts` | `KnowledgeSuggestOptions`、`ScoredKnowledge`、`FlowContextPayload`;扩展 `AiContextBinding` |
|
||
| `src/shared/ipc-channels.ts` | `KNOWLEDGE_SUGGEST_CONTEXT` |
|
||
| `src/main/services/knowledge-retrieval.service.ts` | 打分 + TopN 推荐 |
|
||
| `src/main/services/ai-context-builder.service.ts` | knowledge 块 + 预算裁剪 |
|
||
| `src/main/services/flow-context.util.ts` | `buildFlowContextPayload()` 共享 |
|
||
| `src/main/services/interactive-writing.service.ts` | `readContextText` → `systemPrompt` |
|
||
| `src/main/db/repositories/ai-session.repo.ts` | `parseContext` 补 `knowledgeIds` |
|
||
| `src/main/ipc/handlers/knowledge.handler.ts` | `suggestContext` |
|
||
| `src/main/ipc/handlers/interactive.handler.ts` | `FlowContextPayload` |
|
||
| `src/main/ipc/handlers/auto.handler.ts` | `readContextText` 修复 |
|
||
| `src/main/ipc/handlers/wizard.handler.ts` | 同上 + preview 知识列表 |
|
||
| `src/main/ipc/handlers/ai.handler.ts` | binding 默认 `knowledgeIds: []` |
|
||
| `src/main/services/global-settings.ts` | `knowledgeAutoSuggest` / `knowledgeSuggestTopN` 默认值 |
|
||
| `src/preload/index.ts` | `knowledge.suggestContext` |
|
||
| `src/shared/electron-api.d.ts` | 类型 |
|
||
| `src/renderer/components/ai/KnowledgeSuggestList.tsx` | 共享勾选 UI |
|
||
| `src/renderer/components/ai/ContextEditorModal.tsx` | 知识 Tab + 推荐区 |
|
||
| `src/renderer/components/ai/InteractivePanel.tsx` | context_confirm 知识区 |
|
||
| `src/renderer/components/ai/AutoPanel.tsx` | goal 后知识确认 |
|
||
| `src/renderer/components/ai/WizardPanel.tsx` | wizard_context 知识区 |
|
||
| `src/renderer/lib/ai-context-summary.ts` | 摘要含知识计数 |
|
||
| `src/renderer/stores/useInteractiveStore.ts` | binding 含 knowledgeIds |
|
||
| `public/locales/zh-CN/translation.json` | P6 文案 |
|
||
| `public/locales/en/translation.json` | P6 文案 |
|
||
| `tests/main/knowledge-retrieval.test.ts` | UT-CTX-01/04 |
|
||
| `tests/main/ai-context-builder.test.ts` | UT-CTX-02/03(扩展) |
|
||
| `tests/main/flow-context.test.ts` | FlowContextPayload 单元 |
|
||
| `tests/main/ai-context-integration.test.ts` | IT-CTX-01/02/03 |
|
||
| `e2e/ai-context.spec.ts` | E2E-CTX-* |
|
||
|
||
---
|
||
|
||
## Task 1: 类型 + IPC 通道 + 默认 binding
|
||
|
||
**Files:**
|
||
- Modify: `src/shared/types.ts`
|
||
- Modify: `src/shared/ipc-channels.ts`
|
||
- Modify: `src/main/db/repositories/ai-session.repo.ts`
|
||
|
||
- [ ] **Step 1: 扩展 types**
|
||
|
||
`src/shared/types.ts` 追加:
|
||
|
||
```typescript
|
||
export interface KnowledgeSuggestOptions {
|
||
currentChapterId?: string
|
||
goalText?: string
|
||
topN?: number
|
||
}
|
||
|
||
export interface ScoredKnowledge {
|
||
entry: KnowledgeEntry
|
||
score: number
|
||
reasons: string[]
|
||
}
|
||
|
||
export interface FlowContextPayload {
|
||
binding: AiContextBinding
|
||
systemPrompt: string
|
||
contextSummary: string
|
||
}
|
||
```
|
||
|
||
扩展 `AiContextBinding`:
|
||
|
||
```typescript
|
||
export interface AiContextBinding {
|
||
chapterIds: string[]
|
||
outlineIds: string[]
|
||
settingIds: string[]
|
||
inspirationIds: string[]
|
||
knowledgeIds: string[]
|
||
}
|
||
```
|
||
|
||
扩展 `ContextPreviewKind`:`| 'knowledge'`
|
||
|
||
扩展 `ContextPreviewItem`:`knowledgeType?: KnowledgeType`
|
||
|
||
扩展 `GlobalSettings`(可选,本 Task 仅类型,Task 2 实现默认值):
|
||
|
||
```typescript
|
||
knowledgeAutoSuggest: boolean
|
||
knowledgeSuggestTopN: number
|
||
```
|
||
|
||
- [ ] **Step 2: IPC 通道**
|
||
|
||
`src/shared/ipc-channels.ts`:
|
||
|
||
```typescript
|
||
KNOWLEDGE_SUGGEST_CONTEXT: 'knowledge:suggestContext',
|
||
```
|
||
|
||
- [ ] **Step 3: ai-session.repo 默认 knowledgeIds**
|
||
|
||
```typescript
|
||
const EMPTY_CONTEXT: AiContextBinding = {
|
||
chapterIds: [],
|
||
outlineIds: [],
|
||
settingIds: [],
|
||
inspirationIds: [],
|
||
knowledgeIds: []
|
||
}
|
||
|
||
function parseContext(json: string): AiContextBinding {
|
||
try {
|
||
const parsed = JSON.parse(json) as Partial<AiContextBinding>
|
||
return {
|
||
chapterIds: parsed.chapterIds ?? [],
|
||
outlineIds: parsed.outlineIds ?? [],
|
||
settingIds: parsed.settingIds ?? [],
|
||
inspirationIds: parsed.inspirationIds ?? [],
|
||
knowledgeIds: parsed.knowledgeIds ?? []
|
||
}
|
||
} catch {
|
||
return { ...EMPTY_CONTEXT }
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 全局修补 EMPTY_BINDING**
|
||
|
||
搜索 `inspirationIds: []` 且无 `knowledgeIds` 的 `AiContextBinding` 字面量(`ContextEditorModal.tsx`、`useInteractiveStore.ts` 等),全部补上 `knowledgeIds: []`。
|
||
|
||
- [ ] **Step 5: build 验证**
|
||
|
||
```bash
|
||
npm run build
|
||
```
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add src/shared/types.ts src/shared/ipc-channels.ts src/main/db/repositories/ai-session.repo.ts
|
||
git commit -m "feat: extend AiContextBinding with knowledgeIds for P6"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: KnowledgeRetrievalService
|
||
|
||
**Files:**
|
||
- Create: `src/main/services/knowledge-retrieval.service.ts`
|
||
- Create: `tests/main/knowledge-retrieval.test.ts`
|
||
|
||
- [ ] **Step 1: UT-CTX-01 / UT-CTX-04 失败测试**
|
||
|
||
`tests/main/knowledge-retrieval.test.ts`:
|
||
|
||
```typescript
|
||
import { describe, it, expect, afterEach } from 'vitest'
|
||
import { DatabaseSync } from 'node:sqlite'
|
||
import { migrate } from '../../src/main/db/migrate'
|
||
import { VolumeRepository } from '../../src/main/db/repositories/volume.repo'
|
||
import { ChapterRepository } from '../../src/main/db/repositories/chapter.repo'
|
||
import { KnowledgeRepository } from '../../src/main/db/repositories/knowledge.repo'
|
||
import { KnowledgeRetrievalService } from '../../src/main/services/knowledge-retrieval.service'
|
||
import type { SqliteDb } from '../../src/main/db/types'
|
||
|
||
describe('KnowledgeRetrievalService', () => {
|
||
let db: SqliteDb
|
||
afterEach(() => db?.close())
|
||
|
||
it('UT-CTX-01: ranks by importance + chapter proximity + goal match', () => {
|
||
db = new DatabaseSync(':memory:')
|
||
migrate(db)
|
||
const volId = new VolumeRepository(db).create('卷一', 0).id
|
||
const chapterRepo = new ChapterRepository(db)
|
||
const cur = chapterRepo.create(volId, '当前章', 5, '<p>x</p>')
|
||
const near = chapterRepo.create(volId, '近章', 4, '<p>x</p>')
|
||
chapterRepo.create(volId, '远章', 0, '<p>x</p>')
|
||
const repo = new KnowledgeRepository(db)
|
||
const low = repo.create({
|
||
type: 'fact',
|
||
title: '无关事实',
|
||
content: '普通内容',
|
||
importance: 2,
|
||
status: 'approved'
|
||
})
|
||
const high = repo.create({
|
||
type: 'foreshadow',
|
||
title: '测灵石异常',
|
||
content: '测灵石在入门考核中发热',
|
||
importance: 5,
|
||
status: 'approved',
|
||
foreshadowStatus: 'buried',
|
||
sourceChapterId: near.id
|
||
})
|
||
const service = new KnowledgeRetrievalService(db)
|
||
const result = service.suggest({
|
||
currentChapterId: cur.id,
|
||
goalText: '测灵石入门考核',
|
||
topN: 5
|
||
})
|
||
expect(result[0].entry.id).toBe(high.id)
|
||
expect(result[0].score).toBeGreaterThan(
|
||
result.find((r) => r.entry.id === low.id)!.score
|
||
)
|
||
expect(result[0].reasons.length).toBeGreaterThan(0)
|
||
})
|
||
|
||
it('UT-CTX-04: resolved foreshadow scores lower than buried', () => {
|
||
db = new DatabaseSync(':memory:')
|
||
migrate(db)
|
||
const volId = new VolumeRepository(db).create('卷一', 0).id
|
||
const chId = new ChapterRepository(db).create(volId, '章', 0).id
|
||
const repo = new KnowledgeRepository(db)
|
||
const buried = repo.update(
|
||
repo.create({
|
||
type: 'foreshadow',
|
||
title: '埋设伏笔',
|
||
importance: 3,
|
||
status: 'approved',
|
||
foreshadowStatus: 'buried',
|
||
sourceChapterId: chId
|
||
}).id,
|
||
{ status: 'approved' }
|
||
)
|
||
const resolved = repo.create({
|
||
type: 'foreshadow',
|
||
title: '已回收伏笔',
|
||
importance: 3,
|
||
status: 'approved',
|
||
foreshadowStatus: 'resolved',
|
||
sourceChapterId: chId
|
||
})
|
||
const service = new KnowledgeRetrievalService(db)
|
||
const scores = service.suggest({ currentChapterId: chId, topN: 10 })
|
||
const buriedScore = scores.find((s) => s.entry.id === buried.id)!.score
|
||
const resolvedScore = scores.find((s) => s.entry.id === resolved.id)!.score
|
||
expect(buriedScore).toBeGreaterThan(resolvedScore)
|
||
})
|
||
})
|
||
```
|
||
|
||
- [ ] **Step 2: 运行确认 FAIL**
|
||
|
||
```bash
|
||
npm run test -- tests/main/knowledge-retrieval.test.ts
|
||
```
|
||
|
||
- [ ] **Step 3: 实现 KnowledgeRetrievalService**
|
||
|
||
`src/main/services/knowledge-retrieval.service.ts` 核心:
|
||
|
||
```typescript
|
||
import type { KnowledgeEntry, KnowledgeSuggestOptions, ScoredKnowledge } from '../../shared/types'
|
||
import type { SqliteDb } from '../db/types'
|
||
import { ChapterRepository } from '../db/repositories/chapter.repo'
|
||
import { KnowledgeRepository } from '../db/repositories/knowledge.repo'
|
||
|
||
const STOP_WORDS = new Set(['的', '了', '在', '是', '与', '和', 'the', 'a', 'an'])
|
||
|
||
function tokenize(text: string): string[] {
|
||
return text
|
||
.toLowerCase()
|
||
.split(/[\s,,。!?、;;::]+/)
|
||
.map((t) => t.trim())
|
||
.filter((t) => t.length >= 2 && !STOP_WORDS.has(t))
|
||
}
|
||
|
||
function overlapRatio(a: string, b: string): number {
|
||
const ta = new Set(tokenize(a))
|
||
const tb = tokenize(b)
|
||
if (ta.size === 0 || tb.length === 0) return 0
|
||
let hit = 0
|
||
for (const t of tb) if (ta.has(t)) hit++
|
||
return hit / tb.length
|
||
}
|
||
|
||
function chapterProximityBonus(
|
||
entry: KnowledgeEntry,
|
||
currentSort: number | null,
|
||
chapterSort: Map<string, number>
|
||
): number {
|
||
if (currentSort == null) return 0
|
||
const ids = [entry.sourceChapterId, entry.lastMentionChapterId].filter(Boolean) as string[]
|
||
if (ids.length === 0) return 0
|
||
let best = Infinity
|
||
for (const id of ids) {
|
||
const order = chapterSort.get(id)
|
||
if (order != null) best = Math.min(best, Math.abs(currentSort - order))
|
||
}
|
||
if (best === Infinity) return 0
|
||
if (best === 0) return 30
|
||
if (best <= 3) return 20
|
||
if (best <= 10) return 10
|
||
return 0
|
||
}
|
||
|
||
export class KnowledgeRetrievalService {
|
||
constructor(private db: SqliteDb) {}
|
||
|
||
suggest(opts: KnowledgeSuggestOptions = {}): ScoredKnowledge[] {
|
||
const topN = opts.topN ?? 8
|
||
const entries = new KnowledgeRepository(this.db).list({ status: 'approved' })
|
||
if (entries.length === 0) return []
|
||
|
||
const chapters = new ChapterRepository(this.db).list()
|
||
const sortMap = new Map(chapters.map((c) => [c.id, c.sortOrder]))
|
||
const currentSort = opts.currentChapterId
|
||
? (sortMap.get(opts.currentChapterId) ?? null)
|
||
: null
|
||
const goalText = opts.goalText ?? ''
|
||
|
||
const scored: ScoredKnowledge[] = entries.map((entry) => {
|
||
let score = entry.importance * 20
|
||
const reasons: string[] = []
|
||
|
||
const prox = chapterProximityBonus(entry, currentSort, sortMap)
|
||
score += prox
|
||
if (prox > 0) reasons.push('knowledge.reason.chapterRelevant')
|
||
|
||
if (entry.type === 'foreshadow') {
|
||
const fs = entry.foreshadowStatus ?? 'buried'
|
||
if (fs === 'buried') {
|
||
score += 25
|
||
reasons.push('knowledge.reason.foreshadowBuried')
|
||
} else if (fs === 'partial') score += 15
|
||
}
|
||
|
||
if (goalText) {
|
||
const text = `${entry.title} ${entry.content}`
|
||
const ratio = overlapRatio(goalText, text)
|
||
score += Math.round(ratio * 30)
|
||
if (ratio > 0.2) reasons.push('knowledge.reason.goalMatch')
|
||
}
|
||
|
||
if (entry.importance >= 4) reasons.push('knowledge.reason.highImportance')
|
||
|
||
return { entry, score, reasons: [...new Set(reasons)] }
|
||
})
|
||
|
||
return scored.sort((a, b) => b.score - a.score).slice(0, topN)
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 运行 PASS**
|
||
|
||
```bash
|
||
npm run test -- tests/main/knowledge-retrieval.test.ts
|
||
```
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/main/services/knowledge-retrieval.service.ts tests/main/knowledge-retrieval.test.ts
|
||
git commit -m "feat: add KnowledgeRetrievalService with scoring for context suggest"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: 扩展 AiContextBuilder + flow-context 工具
|
||
|
||
**Files:**
|
||
- Modify: `src/main/services/ai-context-builder.service.ts`
|
||
- Create: `src/main/services/flow-context.util.ts`
|
||
- Modify: `tests/main/ai-context-builder.test.ts`
|
||
- Create: `tests/main/flow-context.test.ts`
|
||
|
||
- [ ] **Step 1: UT-CTX-02 / UT-CTX-03 测试**
|
||
|
||
在 `tests/main/ai-context-builder.test.ts` 追加:
|
||
|
||
```typescript
|
||
import { KnowledgeRepository } from '../../src/main/db/repositories/knowledge.repo'
|
||
|
||
it('UT-CTX-02: build includes knowledge preview items', () => {
|
||
const know = new KnowledgeRepository(db)
|
||
const entry = know.create({
|
||
type: 'foreshadow',
|
||
title: '测灵石',
|
||
content: '石头发热',
|
||
status: 'approved'
|
||
})
|
||
const result = aiContextBuilder.build(
|
||
{ chapterIds: [], outlineIds: [], settingIds: [], inspirationIds: [], knowledgeIds: [entry.id] },
|
||
registry,
|
||
bookId,
|
||
'作者'
|
||
)
|
||
expect(result.preview.some((p) => p.kind === 'knowledge')).toBe(true)
|
||
expect(result.systemPrompt).toContain('【知识·伏笔】')
|
||
expect(result.systemPrompt).toContain('测灵石')
|
||
})
|
||
|
||
it('UT-CTX-03: caps knowledge section at 4000 chars', () => {
|
||
const know = new KnowledgeRepository(db)
|
||
const ids: string[] = []
|
||
for (let i = 0; i < 20; i++) {
|
||
const e = know.create({
|
||
type: 'fact',
|
||
title: `条目${i}`,
|
||
content: '长'.repeat(400),
|
||
importance: 5,
|
||
status: 'approved'
|
||
})
|
||
ids.push(e.id)
|
||
}
|
||
const result = aiContextBuilder.build(
|
||
{ chapterIds: [], outlineIds: [], settingIds: [], inspirationIds: [], knowledgeIds: ids },
|
||
registry,
|
||
bookId,
|
||
'作者'
|
||
)
|
||
const knowledgeChars = result.preview
|
||
.filter((p) => p.kind === 'knowledge')
|
||
.reduce((s, p) => s + p.charCount, 0)
|
||
expect(knowledgeChars).toBeLessThanOrEqual(4000)
|
||
})
|
||
```
|
||
|
||
`tests/main/flow-context.test.ts`:
|
||
|
||
```typescript
|
||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||
import { mkdtempSync, rmSync } from 'fs'
|
||
import { join } from 'path'
|
||
import { tmpdir } from 'os'
|
||
import { BookRegistryService } from '../../src/main/services/book-registry'
|
||
import { buildFlowContextPayload } from '../../src/main/services/flow-context.util'
|
||
import { KnowledgeRepository } from '../../src/main/db/repositories/knowledge.repo'
|
||
|
||
describe('flow-context.util', () => {
|
||
let userDir: string
|
||
let registry: BookRegistryService
|
||
let bookId: string
|
||
|
||
beforeEach(() => {
|
||
userDir = mkdtempSync(join(tmpdir(), 'bilin-flow-ctx-'))
|
||
registry = new BookRegistryService(userDir)
|
||
bookId = registry.create({ name: '书', category: '玄幻' }).id
|
||
})
|
||
|
||
afterEach(() => rmSync(userDir, { recursive: true, force: true }))
|
||
|
||
it('buildFlowContextPayload stores systemPrompt and summary', () => {
|
||
const db = registry.getDb(bookId)
|
||
const entry = new KnowledgeRepository(db).create({
|
||
type: 'fact',
|
||
title: '测试知识',
|
||
content: '内容',
|
||
status: 'approved'
|
||
})
|
||
const payload = buildFlowContextPayload(
|
||
{
|
||
chapterIds: [],
|
||
outlineIds: [],
|
||
settingIds: [],
|
||
inspirationIds: [],
|
||
knowledgeIds: [entry.id]
|
||
},
|
||
registry,
|
||
bookId,
|
||
'作者'
|
||
)
|
||
expect(payload.systemPrompt).toContain('测试知识')
|
||
expect(payload.contextSummary).toContain('1')
|
||
db.close()
|
||
})
|
||
})
|
||
```
|
||
|
||
- [ ] **Step 2: 运行确认 FAIL**
|
||
|
||
```bash
|
||
npm run test -- tests/main/ai-context-builder.test.ts tests/main/flow-context.test.ts
|
||
```
|
||
|
||
- [ ] **Step 3: 扩展 ai-context-builder**
|
||
|
||
要点:
|
||
|
||
- `KNOWLEDGE_CHAR_LIMIT = 500`,`KNOWLEDGE_TOTAL_LIMIT = 4000`
|
||
- `knowledgeTypeLabel()` 映射五种类型
|
||
- 在 `candidates` 循环后追加 knowledge 块,带 `sortKey: entry.importance * 1000`(高 important 先保留)
|
||
- 知识区单独裁剪至 4000 后再参与总 16KB 裁剪
|
||
- 所有现有 `build()` 调用 binding 需含 `knowledgeIds`(测试已补)
|
||
|
||
`src/main/services/flow-context.util.ts`:
|
||
|
||
```typescript
|
||
import type { AiContextBinding, FlowContextPayload } from '../../shared/types'
|
||
import type { BookRegistryService } from './book-registry'
|
||
import { aiContextBuilder } from './ai-context-builder.service'
|
||
|
||
export function buildFlowContextPayload(
|
||
binding: AiContextBinding,
|
||
registry: BookRegistryService,
|
||
bookId: string,
|
||
penName: string
|
||
): FlowContextPayload {
|
||
const built = aiContextBuilder.build(binding, registry, bookId, penName)
|
||
const counts = { chapter: 0, outline: 0, setting: 0, inspiration: 0, knowledge: 0 }
|
||
for (const p of built.preview) {
|
||
if (p.kind in counts) counts[p.kind as keyof typeof counts]++
|
||
}
|
||
const contextSummary = `${counts.chapter}章·${counts.outline}纲·${counts.setting}设·${counts.inspiration}感·${counts.knowledge}知`
|
||
return { binding, systemPrompt: built.systemPrompt, contextSummary }
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 运行 PASS**
|
||
|
||
```bash
|
||
npm run test -- tests/main/ai-context-builder.test.ts tests/main/flow-context.test.ts
|
||
```
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/main/services/ai-context-builder.service.ts src/main/services/flow-context.util.ts tests/main/
|
||
git commit -m "feat: extend AiContextBuilder with knowledge blocks and flow payload helper"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: IPC suggestContext + preload
|
||
|
||
**Files:**
|
||
- Modify: `src/main/ipc/handlers/knowledge.handler.ts`
|
||
- Modify: `src/preload/index.ts`
|
||
- Modify: `src/shared/electron-api.d.ts`
|
||
- Modify: `src/main/services/global-settings.ts`
|
||
- Modify: `src/renderer/stores/useSettingsStore.ts`
|
||
|
||
- [ ] **Step 1: knowledge.handler 追加**
|
||
|
||
```typescript
|
||
ipcMain.handle(
|
||
IPC.KNOWLEDGE_SUGGEST_CONTEXT,
|
||
(
|
||
_e,
|
||
{ bookId, opts }: { bookId: string; opts?: KnowledgeSuggestOptions }
|
||
) => wrap(() => {
|
||
const settings = /* inject via handler param or read topN from settings */
|
||
new KnowledgeRetrievalService(registry.getDb(bookId)).suggest(opts)
|
||
})
|
||
)
|
||
```
|
||
|
||
`registerKnowledgeHandlers` 签名改为接收 `GlobalSettingsService`,`topN` 默认 `settings.get().knowledgeSuggestTopN ?? 8`。
|
||
|
||
- [ ] **Step 2: global-settings 默认值**
|
||
|
||
```typescript
|
||
knowledgeAutoSuggest: true,
|
||
knowledgeSuggestTopN: 8,
|
||
```
|
||
|
||
`get()` merge 补默认。
|
||
|
||
- [ ] **Step 3: preload + electron-api.d.ts**
|
||
|
||
```typescript
|
||
suggestContext: (
|
||
bookId: string,
|
||
opts?: KnowledgeSuggestOptions
|
||
): Promise<IpcResult<ScoredKnowledge[]>>
|
||
```
|
||
|
||
- [ ] **Step 4: build**
|
||
|
||
```bash
|
||
npm run build
|
||
```
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git commit -m "feat: add knowledge suggestContext IPC and global settings defaults"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: 写作流 systemPrompt 修复
|
||
|
||
**Files:**
|
||
- Modify: `src/main/services/interactive-writing.service.ts`
|
||
- Modify: `src/main/ipc/handlers/interactive.handler.ts`
|
||
- Modify: `src/main/ipc/handlers/auto.handler.ts`
|
||
- Modify: `src/main/ipc/handlers/wizard.handler.ts`
|
||
- Modify: `src/main/services/wizard-writing.service.ts`(`buildPreview` 追加知识列表)
|
||
|
||
- [ ] **Step 1: interactive confirmContext 存 FlowContextPayload**
|
||
|
||
`interactive.handler.ts` `INTERACTIVE_CONFIRM_CONTEXT`:
|
||
|
||
```typescript
|
||
const payload = buildFlowContextPayload(binding, registry, bookId, settings.get().penName)
|
||
const flow = service.confirmContext(flowId, payload)
|
||
```
|
||
|
||
`InteractiveWritingService.confirmContext` 改为:
|
||
|
||
```typescript
|
||
confirmContext(flowId: string, payload: FlowContextPayload): InteractiveFlow {
|
||
this.repo.updateStep(flowId, 'plot_suggest', {
|
||
contextJson: JSON.stringify(payload)
|
||
})
|
||
return this.repo.get(flowId)!
|
||
}
|
||
|
||
private readContextText(flowId: string): string {
|
||
try {
|
||
const parsed = JSON.parse(this.repo.getContextJson(flowId)) as FlowContextPayload
|
||
return parsed.systemPrompt || '(无上下文)'
|
||
} catch {
|
||
return '(无上下文)'
|
||
}
|
||
}
|
||
|
||
enrichFlow(flow: InteractiveFlow): InteractiveFlow {
|
||
try {
|
||
const parsed = JSON.parse(this.repo.getContextJson(flow.id)) as FlowContextPayload
|
||
return { ...flow, contextSummary: parsed.contextSummary }
|
||
} catch {
|
||
return flow
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: auto.handler / wizard.handler readContextText**
|
||
|
||
替换本地 `readContextText` 为读 `systemPrompt`:
|
||
|
||
```typescript
|
||
function readContextText(registry, bookId, flowId): string {
|
||
const row = registry.getDb(bookId)
|
||
.prepare('SELECT context_json FROM interactive_flows WHERE id = ?')
|
||
.get(flowId) as { context_json: string } | undefined
|
||
try {
|
||
const parsed = JSON.parse(row?.context_json ?? '{}') as FlowContextPayload
|
||
return parsed.systemPrompt ?? '(无上下文)'
|
||
} catch {
|
||
return '(无上下文)'
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: wizard confirmContext 同 interactive**
|
||
|
||
`wizard.handler.ts` `WIZARD_CONFIRM_CONTEXT` 使用 `buildFlowContextPayload`。
|
||
|
||
- [ ] **Step 4: buildPreview 追加知识标题**
|
||
|
||
`WizardWritingService.buildPreview` 返回前,从 `payload.binding.knowledgeIds` 查标题,append `\n将注入知识:A、B、C`。
|
||
|
||
- [ ] **Step 5: auto planScenes 前自动 confirm**
|
||
|
||
`AutoPanel` 在 `planScenes` 调用前,若 flow 无 `systemPrompt`,用 session binding + 默认推荐 knowledgeIds 调 `interactive.confirmContext` 等价逻辑(可新增 `auto:confirmContext` IPC 或复用 `buildFlowContextPayload` + `setContextSummary` 扩展为存完整 payload)。
|
||
|
||
推荐:新增 `auto.handler` `AUTO_CONFIRM_CONTEXT` 与 interactive 对称。
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git commit -m "fix: store full systemPrompt in writing flow contextJson for P6"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: KnowledgeSuggestList + ContextEditorModal
|
||
|
||
**Files:**
|
||
- Create: `src/renderer/components/ai/KnowledgeSuggestList.tsx`
|
||
- Modify: `src/renderer/components/ai/ContextEditorModal.tsx`
|
||
- Modify: `src/renderer/lib/ai-context-summary.ts`
|
||
|
||
- [ ] **Step 1: KnowledgeSuggestList**
|
||
|
||
Props:
|
||
|
||
```typescript
|
||
interface KnowledgeSuggestListProps {
|
||
items: ScoredKnowledge[]
|
||
selectedIds: string[]
|
||
onToggle: (id: string) => void
|
||
}
|
||
```
|
||
|
||
渲染 checkbox + title + `t(\`knowledge.type.${entry.type}\`)` + reason badges `t(reason)`。
|
||
|
||
- [ ] **Step 2: ContextEditorModal 扩展**
|
||
|
||
新增 props:`goalText?: string`
|
||
|
||
打开时:
|
||
|
||
```typescript
|
||
const chapterId = useBookStore.getState().selectedChapterId ?? undefined
|
||
const suggested = await ipcCall(() =>
|
||
window.electronAPI.knowledge.suggestContext(bookId, { currentChapterId: chapterId, goalText })
|
||
)
|
||
// 默认 merge suggested ids into binding.knowledgeIds
|
||
```
|
||
|
||
新增 Tab `knowledge`;顶部推荐区 + `context-refresh-suggest`;下方 approved 全列表(`knowledge.list(bookId, { status: 'approved' })`)可勾选。
|
||
|
||
- [ ] **Step 3: formatContextSummary 含 knowledge**
|
||
|
||
```typescript
|
||
const counts = { chapter: 0, outline: 0, setting: 0, inspiration: 0, knowledge: 0 }
|
||
// ...
|
||
return t('ai.contextSummary', { ...counts })
|
||
```
|
||
|
||
更新 `ai.contextSummary` i18n 为 `{{chapters}}章·{{outlines}}纲·{{settings}}设·{{inspirations}}感·{{knowledge}}知`。
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git commit -m "feat: add KnowledgeSuggestList and knowledge tab in context editor"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: 交互 / 自动 / 向导 Panel 集成
|
||
|
||
**Files:**
|
||
- Modify: `src/renderer/components/ai/InteractivePanel.tsx`
|
||
- Modify: `src/renderer/components/ai/AutoPanel.tsx`
|
||
- Modify: `src/renderer/components/ai/WizardPanel.tsx`
|
||
- Modify: `src/renderer/stores/useInteractiveStore.ts`
|
||
- Modify: `src/renderer/stores/useAutoStore.ts`(confirmContext)
|
||
- Modify: `src/renderer/components/ai/AiPanel.tsx`(传 goalText 空)
|
||
|
||
- [ ] **Step 1: InteractivePanel context_confirm**
|
||
|
||
- mount 时 `suggestContext` → state `suggested`
|
||
- 内联 `<KnowledgeSuggestList>`,`data-testid="interactive-knowledge-suggest"`
|
||
- 勾选同步到 session `context.knowledgeIds`(`ai.sessionUpdate`)
|
||
- `confirmContextAndStart` 传含 `knowledgeIds` 的 binding
|
||
|
||
- [ ] **Step 2: AutoPanel**
|
||
|
||
- `goal_setup` 区域下方折叠「知识上下文」`data-testid="auto-knowledge-suggest"`
|
||
- `goalText` = `chapterGoal` + 选中大纲 description
|
||
- `planScenes` 前调用 `auto.confirmContext`(Task 5 新增 IPC)
|
||
|
||
- [ ] **Step 3: WizardPanel wizard_context**
|
||
|
||
- 同 Interactive 内联推荐列表 `data-testid="wizard-knowledge-suggest"`
|
||
- `handleContextClose` 传 `goalText` = goal + styleNote
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git commit -m "feat: integrate knowledge suggest into interactive auto wizard panels"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 8: i18n
|
||
|
||
**Files:**
|
||
- Modify: `public/locales/zh-CN/translation.json`
|
||
- Modify: `public/locales/en/translation.json`
|
||
|
||
- [ ] **Step 1: 追加 spec §9 全部键 + `ai.contextSummary` 更新**
|
||
|
||
- [ ] **Step 2: Commit**
|
||
|
||
```bash
|
||
git commit -m "feat: add P6 AI context i18n strings"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 9: 集成测试
|
||
|
||
**Files:**
|
||
- Create: `tests/main/ai-context-integration.test.ts`
|
||
|
||
- [ ] **Step 1: IT-CTX-01**
|
||
|
||
```typescript
|
||
it('IT-CTX-01: suggestContext returns entries when approved exist', () => {
|
||
// setup approved entry → suggest → length >= 1
|
||
})
|
||
```
|
||
|
||
- [ ] **Step 2: IT-CTX-02**
|
||
|
||
通过 `aiContextBuilder.build` 含 knowledgeIds 断言 `systemPrompt` 含 `【知识·`(无需 Electron IPC)。
|
||
|
||
- [ ] **Step 3: IT-CTX-03(240s)**
|
||
|
||
```typescript
|
||
it('IT-CTX-03: interactive confirm then suggestPlots uses knowledge context', async () => {
|
||
// setup book + knowledge + interactive flow
|
||
// confirmContext with knowledgeIds
|
||
// suggestPlots returns 3 options
|
||
}, 240_000)
|
||
```
|
||
|
||
- [ ] **Step 4: 运行**
|
||
|
||
```bash
|
||
npm run test -- tests/main/ai-context-integration.test.ts
|
||
```
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git commit -m "test: add P6 AI context integration tests"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 10: E2E
|
||
|
||
**Files:**
|
||
- Create: `e2e/ai-context.spec.ts`
|
||
|
||
- [ ] **Step 1: E2E-CTX-01**
|
||
|
||
```typescript
|
||
test('E2E-CTX-01: context editor knowledge tab shows suggestions', async () => {
|
||
// create book → add approved knowledge via panel → open AI context editor → context-tab-knowledge visible
|
||
})
|
||
```
|
||
|
||
- [ ] **Step 2: E2E-CTX-02**
|
||
|
||
```typescript
|
||
test('E2E-CTX-02: interactive context_confirm shows knowledge suggest', async () => {
|
||
// open interactive → interactive-knowledge-suggest visible
|
||
})
|
||
```
|
||
|
||
- [ ] **Step 3: E2E-CTX-03(300s)**
|
||
|
||
```typescript
|
||
test('E2E-CTX-03: chat with knowledge context gets AI reply', async () => {
|
||
test.setTimeout(300_000)
|
||
// add knowledge → select in context → send chat → non-empty reply
|
||
})
|
||
```
|
||
|
||
- [ ] **Step 4: 运行**
|
||
|
||
```bash
|
||
npm run test:e2e -- e2e/ai-context.spec.ts
|
||
```
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git commit -m "test: add P6 AI context E2E specs"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 11: v0.7.0 交付
|
||
|
||
**Files:**
|
||
- Modify: `package.json`
|
||
- Modify: `README.md`
|
||
- Modify: `src/renderer/components/settings/SettingsPage.tsx`(about 版本号)
|
||
- Modify: `docs/superpowers/plans/2026-07-07-bilin-p6-ai-context.md`(勾选完成项)
|
||
|
||
- [ ] **Step 1: 版本号 `0.7.0`**
|
||
|
||
- [ ] **Step 2: README 追加**
|
||
|
||
```markdown
|
||
- 知识库 AI 上下文:半自动推荐 + 勾选注入四种写作模式(P6)
|
||
```
|
||
|
||
- [ ] **Step 3: 全量测试**
|
||
|
||
```bash
|
||
npm run build
|
||
npm run test
|
||
npm run test:e2e
|
||
```
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git commit -m "feat: ship v0.7.0 with AI knowledge context integration"
|
||
```
|
||
|
||
---
|
||
|
||
## Spec Coverage Checklist
|
||
|
||
| Spec 要求 | Task |
|
||
|-----------|------|
|
||
| KnowledgeRetrievalService 打分 | Task 2 |
|
||
| AiContextBinding.knowledgeIds | Task 1 |
|
||
| AiContextBuilder 知识块 | Task 3 |
|
||
| FlowContextPayload / systemPrompt 修复 | Task 3, 5 |
|
||
| knowledge:suggestContext IPC | Task 4 |
|
||
| ContextEditorModal 知识 Tab | Task 6 |
|
||
| 对话静默注入 | Task 5(AI_CHAT 已用 build)+ Task 6 |
|
||
| 交互 context_confirm | Task 7 |
|
||
| 自动写作知识确认 | Task 5, 7 |
|
||
| 向导 wizard_context | Task 7 |
|
||
| global-settings 默认值 | Task 4 |
|
||
| UT-CTX-01~04 | Task 2, 3 |
|
||
| IT-CTX-01~03 | Task 9 |
|
||
| E2E-CTX-01~03 | Task 10 |
|
||
| v0.7.0 DoD | Task 11 |
|