diff --git a/Readme.md b/Readme.md index 01bca7b..4647c97 100644 --- a/Readme.md +++ b/Readme.md @@ -1,3 +1,257 @@ -快捷键 Cmd+Shift+D 或点击侧边栏脑图标打开提问框 -在设置中填入 DeepSeek API Key -提问后自动生成带双链的 Markdown 笔记,存入 Knowledge/Q&A/,同时自动创建 Knowledge/Concepts/ 下的概念页 \ No newline at end of file +# IStart-Note-AI + +基于 DeepSeek AI 的 Obsidian 知识图谱插件。将提问行为转化为结构化双链笔记,自动构建个人知识网络。 + +--- + +## 功能概览 + +| 功能 | 描述 | +|------|------| +| 普通提问 | 输入问题 → DeepSeek 回答 → 自动生成结构化笔记 | +| 框选提问 | 选中文本 → 基于上下文提问 → 生成带来源引用的笔记 | +| 问题分类 | 自动判断问题类型(新问题 / 深化 / 扩展)并建立关联 | +| 概念补全 | 按需补全空概念页,支持轻量 / 标准两种深度 | +| 批量扫描 | 扫描 Vault 中所有空概念页,批量补全 | +| 问题索引 | 自动维护问题图谱索引页 | + +--- + +## 安装 + +### 手动安装 + +1. 构建插件(见下方开发指南) +2. 将 `dist/` 目录下的文件复制到 Vault 的 `.obsidian/plugins/istart-note-ai/` +3. 在 Obsidian 设置 → 第三方插件中启用 **IStart-Note-AI** + +### 目录结构要求 + +插件会自动创建以下目录(可在设置中修改路径): + +``` +Knowledge/ +├── Q&A/ # 问答笔记 +├── Concepts/ # 概念页 +└── Questions/ # 问题索引 +``` + +--- + +## 配置 + +进入 Obsidian 设置 → IStart-Note-AI: + +| 配置项 | 说明 | 默认值 | +|--------|------|--------| +| API Key | DeepSeek API Key,在 [platform.deepseek.com](https://platform.deepseek.com) 获取 | 空 | +| Base URL | API 地址 | `https://api.deepseek.com` | +| 模型 | `deepseek-chat` 或 `deepseek-reasoner` | `deepseek-chat` | +| Q&A 保存路径 | 问答笔记存储目录 | `Knowledge/Q&A` | +| 概念页路径 | 概念页存储目录 | `Knowledge/Concepts` | +| 问题索引路径 | 问题图谱索引目录 | `Knowledge/Questions` | +| 自动打开 Graph View | 生成笔记后自动打开图谱 | 关闭 | + +--- + +## 使用方式 + +### 普通提问 + +- 快捷键:`Cmd/Ctrl + Shift + D` +- 侧边栏点击脑图标 +- 命令面板:`向 DeepSeek 提问并生成知识笔记` + +输入问题后,插件会: +1. 调用 DeepSeek 生成回答 +2. 弹出问题分类确认弹窗(可手动调整类型) +3. 生成结构化 Markdown 笔记,包含 Answer / Concepts / Relations / 推荐问题 +4. 自动创建相关概念页(空节点) +5. 更新问题索引页 + +### 框选提问 + +1. 在任意笔记中选中一段文字 +2. 右键 → **IStart-Note-AI:基于选中内容提问** + 或快捷键 `Cmd/Ctrl + Shift + Q` +3. 在弹窗中输入针对该内容的问题 +4. 生成的笔记包含来源引用,并在原笔记末尾追加反向链接 + +### 概念页补全 + +**单个补全:** +- 打开概念页 → 命令面板:`补全当前概念页` +- 编辑器内选中 `[[概念名]]` → 右键 → `IStart-Note-AI:补全概念 "xxx"` +- 文件列表右键任意 `.md` 文件 → `IStart-Note-AI:补全此概念页` + +**批量补全:** +- 命令面板:`扫描空概念页` +- 从列表中选择(最多 5 个)→ 选择补全深度 → 确认 + +补全深度: +- **轻量**:定义 + 关联概念 +- **标准**:定义 + 核心解释 + 示例 + 关联概念 + 相关问题 + +所有补全内容在写入前会弹出预览窗口,支持重新生成或取消。 + +### 问题索引 + +- 命令面板:`打开问题索引` +- 每次提问后自动更新对应索引页 + +--- + +## 笔记结构 + +### Q&A 笔记(普通提问) + +```markdown +--- +type: question +question: 五行是什么? +category: new +parent: null +related: [] +concepts: [五行, 木, 火, 土, 金, 水] +status: linked +created_at: 2026-04-25 +--- + +# 五行是什么? + +## Question +## Answer +## Concepts +## Relations +## Tags +## 推荐问题 +### 深化 +### 扩展 +``` + +### Q&A 笔记(框选提问) + +```markdown +# 为什么阴阳平衡会影响系统稳定? + +## 来源片段 +> 阴阳平衡决定系统稳定性 + +来源:[[原始笔记路径]] + +## Question +## Answer +## Concepts +## Relations +## 延伸问题 +## Tags +``` + +### 概念页 + +```markdown +--- +type: concept +name: 五行 +status: completed +completion_status: completed +created_from: Q&A +created_at: 2026-04-25 +updated_at: 2026-04-25 +--- + +# 五行 + +## 定义 +## 核心解释 +## 示例 +## 关联概念 +## 相关问题 +## 来源 +``` + +--- + +## 开发指南 + +### 环境要求 + +- Node.js >= 16 +- npm >= 8 + +### 本地开发 + +```bash +cd obsidian-deepseek-plugin +npm install +npm run dev # 监听模式,输出到 dist/main.js +``` + +### 生产构建 + +```bash +npm run build # 输出到 dist/main.js + dist/manifest.json +``` + +### 项目结构 + +``` +src/ +├── main.ts # 插件入口,注册命令 / 菜单 / 设置 +├── types.ts # 全局类型定义 +├── DeepSeekClient.ts # 普通提问 API 调用 +├── ContextQAClient.ts # 框选提问 API 调用(带上下文) +├── VaultWriter.ts # 笔记写入(Q&A / Context Q&A / 概念页) +├── QuestionModal.ts # 普通提问弹窗 +├── ContextQAModal.ts # 框选提问弹窗 +├── QuestionClassifier.ts # 问题分类(new / refinement / expansion) +├── QuestionClassifyModal.ts # 分类确认弹窗 +├── QuestionGraphManager.ts # 问题图谱:frontmatter / 索引页 / 推荐问题 +├── ConceptCompleter.ts # 概念补全 API 调用 +├── ConceptPageManager.ts # 概念页识别 / 增量写入 / 批量扫描 +├── ConceptCompletionModal.ts # 深度选择 / 预览确认 / 批量扫描弹窗 +└── SettingsTab.ts # 设置页面 +``` + +### 扩展开发 + +**新增 AI 功能**:参考 `ContextQAClient.ts` 的模式,实现 `ask()` 方法并返回结构化 JSON。 + +**新增命令**:在 `main.ts` 的 `onload()` 中调用 `this.addCommand()`。 + +**新增右键菜单项**:在 `editor-menu` 或 `file-menu` 事件监听中追加 `menu.addItem()`。 + +**修改笔记模板**:编辑 `VaultWriter.ts` 中的 `buildNoteContent()` 或 `buildContextNoteContent()`。 + +**修改 Prompt**:编辑对应 Client 文件中的 prompt 常量。 + +--- + +## 版本历史 + +### v1.3.0 +- 新增框选提问(Context Q&A)功能 +- 框选提问支持上下文传入、来源引用、反向链接 +- 插件更名为 IStart-Note-AI + +### v1.2.0 +- 新增问题图谱:自动分类(new / refinement / expansion) +- 新增问题索引页自动维护 +- 新增推荐深化 / 扩展问题 + +### v1.1.0 +- 新增概念页按需补全(轻量 / 标准) +- 新增批量扫描空概念页 +- 新增预览确认弹窗 +- 新增右键菜单支持 + +### v1.0.0 +- 基础 Q&A 提问与笔记生成 +- 自动创建概念页与双链 +- DeepSeek API 配置 + +--- + +## License + +MIT diff --git a/manifest.json b/manifest.json index 8ead7ab..38438dd 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,9 @@ { - "id": "obsidian-deepseek-knowledge", - "name": "DeepSeek Knowledge Graph", - "version": "1.0.0", + "id": "istart-note-ai", + "name": "IStart-Note-AI", + "version": "1.3.0", "minAppVersion": "1.0.0", - "description": "通过 DeepSeek AI 自动构建个人知识图谱,将问答转化为结构化双链笔记", + "description": "基于 DeepSeek AI 的知识图谱插件:框选提问、概念补全、问题图谱、自动双链", "author": "Your Name", "authorUrl": "", "isDesktopOnly": false diff --git a/package.json b/package.json index ead2198..ab94fc8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "obsidian-deepseek-knowledge", - "version": "1.0.0", - "description": "DeepSeek Knowledge Graph plugin for Obsidian", + "name": "istart-note-ai", + "version": "1.3.0", + "description": "IStart-Note-AI: DeepSeek-powered knowledge graph plugin for Obsidian", "main": "main.js", "scripts": { "dev": "node esbuild.config.mjs", diff --git a/src/ConceptPageManager.ts b/src/ConceptPageManager.ts index 9d13bdd..d90c59e 100644 --- a/src/ConceptPageManager.ts +++ b/src/ConceptPageManager.ts @@ -1,5 +1,5 @@ -import { App, TFile, parseYaml, stringifyYaml } from "obsidian"; -import { ConceptCompletionResult, CompletionDepth } from "./types"; +import { App, TFile, parseYaml, stringifyYaml, normalizePath } from "obsidian"; +import { ConceptCompletionResult, CompletionDepth, DeepSeekSettings } from "./types"; export interface ConceptPageInfo { file: TFile; @@ -11,7 +11,7 @@ export interface ConceptPageInfo { } export class ConceptPageManager { - constructor(private app: App) {} + constructor(private app: App, private settings?: DeepSeekSettings) {} /** 判断当前打开的文件是否是待补全的概念页 */ async analyzeCurrentFile(): Promise { @@ -77,6 +77,9 @@ export class ConceptPageManager { : updatedBody; await this.app.vault.modify(file, newContent); + + // 为关联概念预创建空概念页到正确路径,避免点击双链时落到根目录 + await this.ensureRelatedConceptNotes(result.related_concepts.map((c) => c.name)); } buildPreviewMarkdown(result: ConceptCompletionResult, depth: CompletionDepth): string { @@ -174,4 +177,27 @@ export class ConceptPageManager { ...(result.tags.length > 0 ? { tags: result.tags } : {}), }; } + + private async ensureRelatedConceptNotes(concepts: string[]): Promise { + const folderPath = normalizePath( + this.settings?.conceptsPath ?? "Knowledge/Concepts" + ); + + // 确保目录存在 + if (!this.app.vault.getAbstractFileByPath(folderPath)) { + await this.app.vault.createFolder(folderPath); + } + + const today = new Date().toISOString().slice(0, 10); + + for (const concept of concepts) { + const filePath = normalizePath(`${folderPath}/${concept}.md`); + if (!this.app.vault.getAbstractFileByPath(filePath)) { + await this.app.vault.create( + filePath, + `---\ntype: concept\nname: ${concept}\nstatus: empty\ncompletion_status: pending\ncreated_from: concept-completion\ncreated_at: ${today}\n---\n\n# ${concept}\n\n## 定义\n\n## 核心解释\n\n## 示例\n\n## 关联概念\n\n## 相关问题\n\n## 来源\n` + ); + } + } + } } diff --git a/src/ContextQAClient.ts b/src/ContextQAClient.ts new file mode 100644 index 0000000..f6a14f9 --- /dev/null +++ b/src/ContextQAClient.ts @@ -0,0 +1,79 @@ +import { DeepSeekSettings, ContextQAInput, ContextQAResponse, Relation } from "./types"; + +const buildPrompt = (input: ContextQAInput): string => `请基于以下上下文回答问题。 + +【上下文】 +${input.context} +${input.surroundingContext ? `\n【周围段落】\n${input.surroundingContext}` : ""} + +【问题】 +${input.question} + +要求: +1. 回答必须基于上下文,可补充必要背景知识 +2. 提取关键概念(2-5个) +3. 识别概念间关系(影响/属于/导致/依赖/对立) +4. 生成 2-3 个延伸问题 +5. 生成相关标签(2-4个) + +严格按以下 JSON 格式返回,不要有任何其他内容: +{ + "answer": "回答内容", + "concepts": ["概念A", "概念B"], + "relations": [ + { "from": "概念A", "relation": "影响", "to": "概念B" } + ], + "suggested_questions": ["延伸问题1", "延伸问题2"], + "tags": ["标签1", "标签2"] +}`; + +export class ContextQAClient { + constructor(private settings: DeepSeekSettings) {} + + async ask(input: ContextQAInput): Promise { + if (!this.settings.apiKey) { + throw new Error("请先在插件设置中配置 API Key"); + } + + const response = await fetch(`${this.settings.baseUrl}/v1/chat/completions`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.settings.apiKey}`, + }, + body: JSON.stringify({ + model: this.settings.model, + messages: [{ role: "user", content: buildPrompt(input) }], + temperature: 0.6, + }), + }); + + if (!response.ok) { + const err = await response.text(); + throw new Error(`API 错误: ${response.status} - ${err}`); + } + + const data = await response.json(); + const content = data.choices?.[0]?.message?.content ?? ""; + return this.parse(content); + } + + private parse(content: string): ContextQAResponse { + const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/) || + content.match(/(\{[\s\S]*\})/); + const jsonStr = jsonMatch ? jsonMatch[1] : content; + + try { + const p = JSON.parse(jsonStr.trim()); + return { + answer: p.answer || "", + concepts: Array.isArray(p.concepts) ? p.concepts : [], + relations: Array.isArray(p.relations) ? p.relations as Relation[] : [], + suggested_questions: Array.isArray(p.suggested_questions) ? p.suggested_questions : [], + tags: Array.isArray(p.tags) ? p.tags : [], + }; + } catch { + return { answer: content, concepts: [], relations: [], suggested_questions: [], tags: [] }; + } + } +} diff --git a/src/ContextQAModal.ts b/src/ContextQAModal.ts new file mode 100644 index 0000000..f15290c --- /dev/null +++ b/src/ContextQAModal.ts @@ -0,0 +1,74 @@ +import { App, Modal, Setting } from "obsidian"; + +export class ContextQAModal extends Modal { + private question = ""; + + constructor( + app: App, + private selectedText: string, + private onSubmit: (question: string) => void + ) { + super(app); + this.titleEl.setText("基于选中内容提问"); + } + + onOpen() { + const { contentEl } = this; + + // 显示选中内容预览 + const preview = contentEl.createDiv({ + attr: { + style: [ + "background: var(--background-secondary)", + "border-left: 3px solid var(--interactive-accent)", + "padding: 8px 12px", + "margin-bottom: 14px", + "border-radius: 4px", + "font-size: 13px", + "color: var(--text-muted)", + "max-height: 80px", + "overflow-y: auto", + "white-space: pre-wrap", + "word-break: break-word", + ].join(";"), + }, + }); + preview.setText( + this.selectedText.length > 200 + ? this.selectedText.slice(0, 200) + "…" + : this.selectedText + ); + + const textArea = contentEl.createEl("textarea", { + attr: { + placeholder: "针对上方内容,输入你的问题...", + rows: "3", + style: "width:100%; resize:vertical; padding:8px; font-size:14px;", + }, + }); + + textArea.addEventListener("input", () => (this.question = textArea.value)); + textArea.addEventListener("keydown", (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === "Enter") this.submit(); + }); + + new Setting(contentEl) + .addButton((btn) => + btn.setButtonText("提问 (Ctrl+Enter)").setCta().onClick(() => this.submit()) + ) + .addButton((btn) => btn.setButtonText("取消").onClick(() => this.close())); + + setTimeout(() => textArea.focus(), 50); + } + + private submit() { + const q = this.question.trim(); + if (!q) return; + this.close(); + this.onSubmit(q); + } + + onClose() { + this.contentEl.empty(); + } +} diff --git a/src/VaultWriter.ts b/src/VaultWriter.ts index ed734ae..3f4d18c 100644 --- a/src/VaultWriter.ts +++ b/src/VaultWriter.ts @@ -1,5 +1,5 @@ import { App, TFile, normalizePath } from "obsidian"; -import { DeepSeekResponse, DeepSeekSettings } from "./types"; +import { DeepSeekResponse, DeepSeekSettings, ContextQAInput, ContextQAResponse } from "./types"; export class VaultWriter { constructor(private app: App, private settings: DeepSeekSettings) {} @@ -11,13 +11,11 @@ export class VaultWriter { const folderPath = normalizePath(this.settings.savePath); const filePath = normalizePath(`${folderPath}/${filename}`); - // 确保目录存在 await this.ensureFolder(folderPath); const content = this.buildNoteContent(question, response); const file = await this.app.vault.create(filePath, content); - // 自动创建概念页(V2 功能) for (const concept of response.concepts) { await this.ensureConceptNote(concept); } @@ -25,15 +23,32 @@ export class VaultWriter { return file; } - private buildNoteContent(question: string, response: DeepSeekResponse): string { - const conceptLinks = response.concepts - .map((c) => `- [[${c}]]`) - .join("\n"); + async writeContextQANote(input: ContextQAInput, response: ContextQAResponse): Promise { + const date = new Date().toISOString().slice(0, 10); + const safeTitle = this.sanitizeFilename(input.question).slice(0, 50); + const filename = `${date}-ctx-${safeTitle}.md`; + const folderPath = normalizePath(this.settings.savePath); + const filePath = normalizePath(`${folderPath}/${filename}`); + await this.ensureFolder(folderPath); + + const content = this.buildContextNoteContent(input, response); + const file = await this.app.vault.create(filePath, content); + + for (const concept of response.concepts) { + await this.ensureConceptNote(concept); + } + + await this.appendBacklink(input.sourceNote, file.path, input.question); + + return file; + } + + private buildNoteContent(question: string, response: DeepSeekResponse): string { + const conceptLinks = response.concepts.map((c) => `- [[${c}]]`).join("\n"); const relationLines = response.relations .map((r) => `- [[${r.from}]] -${r.relation}-> [[${r.to}]]`) .join("\n"); - const tagLine = response.tags.map((t) => `#${t.replace(/\s+/g, "_")}`).join(" "); return `# ${question} @@ -55,6 +70,63 @@ ${tagLine || "暂无标签"} `; } + private buildContextNoteContent(input: ContextQAInput, response: ContextQAResponse): string { + const conceptLinks = response.concepts.map((c) => `- [[${c}]]`).join("\n"); + const relationLines = response.relations + .map((r) => `- [[${r.from}]] -${r.relation}-> [[${r.to}]]`) + .join("\n"); + const tagLine = response.tags.map((t) => `#${t.replace(/\s+/g, "_")}`).join(" "); + const suggestedLines = response.suggested_questions.map((q) => `- ${q}`).join("\n"); + const sourceLink = input.sourceNote ? `[[${input.sourceNote}]]` : "未知来源"; + + return `# ${input.question} + +## 来源片段 +> ${input.context.split("\n").join("\n> ")} + +来源:${sourceLink} + +## Question +${input.question} + +## Answer +${response.answer} + +## Concepts +${conceptLinks || "- 暂无"} + +## Relations +${relationLines || "- 暂无"} + +## 延伸问题 +${suggestedLines || "- 暂无"} + +## Tags +${tagLine || "暂无标签"} +`; + } + + private async appendBacklink(sourceNotePath: string, qaFilePath: string, question: string): Promise { + if (!sourceNotePath) return; + const sourceFile = this.app.vault.getAbstractFileByPath(sourceNotePath) as TFile | null; + if (!sourceFile) return; + + const content = await this.app.vault.read(sourceFile); + const backlinkSection = "## 相关问答"; + const link = `- [[${qaFilePath}|${question}]]`; + + if (content.includes(link)) return; + + if (content.includes(backlinkSection)) { + await this.app.vault.modify( + sourceFile, + content.replace(backlinkSection, `${backlinkSection}\n${link}`) + ); + } else { + await this.app.vault.modify(sourceFile, content.trimEnd() + `\n\n${backlinkSection}\n${link}\n`); + } + } + async ensureConceptNote(concept: string): Promise { const folderPath = normalizePath(this.settings.conceptsPath || "Knowledge/Concepts"); const filePath = normalizePath(`${folderPath}/${concept}.md`); diff --git a/src/main.ts b/src/main.ts index 3c8c503..c6c4d56 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,6 +10,8 @@ import { DepthSelectModal, PreviewModal, BatchScanModal } from "./ConceptComplet import { QuestionClassifier } from "./QuestionClassifier"; import { QuestionGraphManager } from "./QuestionGraphManager"; import { QuestionClassifyModal } from "./QuestionClassifyModal"; +import { ContextQAClient } from "./ContextQAClient"; +import { ContextQAModal } from "./ContextQAModal"; export default class DeepSeekPlugin extends Plugin { settings: DeepSeekSettings; @@ -44,6 +46,22 @@ export default class DeepSeekPlugin extends Plugin { callback: () => this.scanAndBatchComplete(), }); + // 命令:框选提问 + this.addCommand({ + id: "context-qa", + name: "基于选中内容提问", + hotkeys: [{ modifiers: ["Mod", "Shift"], key: "q" }], + editorCallback: (editor) => { + const selection = editor.getSelection().trim(); + if (!selection) { + new Notice("请先选中一段文字"); + return; + } + const activeFile = this.app.workspace.getActiveFile(); + this.openContextQAModal(selection, activeFile?.path ?? ""); + }, + }); + // 命令:问题图谱索引 this.addCommand({ id: "open-question-index", @@ -59,10 +77,10 @@ export default class DeepSeekPlugin extends Plugin { menu.addItem((item) => { item - .setTitle("DeepSeek:补全此概念页") + .setTitle("IStart-Note-AI:补全此概念页") .setIcon("brain") .onClick(async () => { - const manager = new ConceptPageManager(this.app); + const manager = new ConceptPageManager(this.app, this.settings); const info = await manager.analyzeFile(file); if (!info) { new Notice("该文件不是概念页"); @@ -82,8 +100,22 @@ export default class DeepSeekPlugin extends Plugin { // 编辑器内右键菜单 this.registerEvent( this.app.workspace.on("editor-menu", (menu, editor) => { - // 检查选中文字是否是 [[概念]] 格式 const selection = editor.getSelection().trim(); + + // 框选提问入口(有选中内容时显示) + if (selection) { + menu.addItem((item) => { + item + .setTitle("IStart-Note-AI:基于选中内容提问") + .setIcon("message-circle") + .onClick(() => { + const activeFile = this.app.workspace.getActiveFile(); + this.openContextQAModal(selection, activeFile?.path ?? ""); + }); + }); + } + + // 概念补全入口 const linkMatch = selection.match(/^\[\[(.+?)(?:\|.+?)?\]\]$/) || selection.match(/^(.+)$/); const conceptName = linkMatch?.[1]; @@ -91,16 +123,14 @@ export default class DeepSeekPlugin extends Plugin { menu.addItem((item) => { item - .setTitle(`DeepSeek:补全概念 "${conceptName}"`) + .setTitle(`IStart-Note-AI:补全概念 "${conceptName}"`) .setIcon("brain") .onClick(async () => { - const manager = new ConceptPageManager(this.app); - // 尝试找到对应概念页文件 + const manager = new ConceptPageManager(this.app, this.settings); const conceptsPath = this.settings.conceptsPath || "Knowledge/Concepts"; const filePath = `${conceptsPath}/${conceptName}.md`; let file = this.app.vault.getAbstractFileByPath(filePath) as TFile | null; - // 不存在则先创建 if (!file) { const writer = new VaultWriter(this.app, this.settings); await writer.ensureConceptNote(conceptName); @@ -122,10 +152,9 @@ export default class DeepSeekPlugin extends Plugin { }); }); - // 当前文件是概念页时,也提供补全入口 menu.addItem((item) => { item - .setTitle("DeepSeek:补全当前概念页") + .setTitle("IStart-Note-AI:补全当前概念页") .setIcon("brain") .onClick(() => this.completeCurrentConcept()); }); @@ -133,6 +162,73 @@ export default class DeepSeekPlugin extends Plugin { ); } + private openContextQAModal(selectedText: string, sourceNotePath: string) { + new ContextQAModal(this.app, selectedText, (question) => { + this.processContextQA(question, selectedText, sourceNotePath); + }).open(); + } + + private async processContextQA(question: string, context: string, sourceNotePath: string) { + const notice = new Notice("⏳ 基于上下文思考中...", 0); + + try { + const client = new ContextQAClient(this.settings); + const graphManager = new QuestionGraphManager(this.app, this.settings); + + // 获取周围段落作为补充上下文(取文件前 500 字) + let surroundingContext: string | undefined; + if (sourceNotePath) { + const sourceFile = this.app.vault.getAbstractFileByPath(sourceNotePath) as TFile | null; + if (sourceFile) { + const fullContent = await this.app.vault.read(sourceFile); + surroundingContext = fullContent.slice(0, 500); + } + } + + const [response, history] = await Promise.all([ + client.ask({ question, context, sourceNote: sourceNotePath, surroundingContext }), + graphManager.getQuestionHistory(), + ]); + + notice.hide(); + + // 分类 + const classifier = new QuestionClassifier(this.settings); + const classifyNotice = new Notice("🔍 分析问题关系...", 0); + const classification = await classifier.classify(question, history); + classifyNotice.hide(); + + new QuestionClassifyModal(this.app, question, classification, async (confirmed) => { + const writeNotice = new Notice("✍️ 写入笔记...", 0); + try { + const writer = new VaultWriter(this.app, this.settings); + const file = await writer.writeContextQANote( + { question, context, sourceNote: sourceNotePath, surroundingContext }, + response + ); + + await graphManager.attachClassification(file, question, confirmed, response.concepts); + await graphManager.appendRecommendations(file, confirmed); + await graphManager.updateQuestionIndex(question, confirmed, file.path); + + writeNotice.hide(); + new Notice(`✅ 上下文笔记已生成:${file.name}`); + + const leaf = this.app.workspace.getLeaf(false); + await leaf.openFile(file); + } catch (err) { + writeNotice.hide(); + new Notice(`❌ 写入失败:${err.message}`); + console.error("[IStart-Note-AI]", err); + } + }).open(); + } catch (err) { + notice.hide(); + new Notice(`❌ 错误:${err.message}`); + console.error("[IStart-Note-AI]", err); + } + } + private openQuestionModal() { new QuestionModal(this.app, (question) => { this.processQuestion(question); @@ -212,7 +308,7 @@ export default class DeepSeekPlugin extends Plugin { } private async completeCurrentConcept() { - const manager = new ConceptPageManager(this.app); + const manager = new ConceptPageManager(this.app, this.settings); const info = await manager.analyzeCurrentFile(); if (!info) { @@ -241,7 +337,7 @@ export default class DeepSeekPlugin extends Plugin { const result = await completer.complete(conceptName, depth, context); notice.hide(); - const manager = new ConceptPageManager(this.app); + const manager = new ConceptPageManager(this.app, this.settings); const previewMd = manager.buildPreviewMarkdown(result, depth); new PreviewModal( @@ -267,7 +363,7 @@ export default class DeepSeekPlugin extends Plugin { private async scanAndBatchComplete() { const notice = new Notice("🔍 扫描空概念页中...", 0); - const manager = new ConceptPageManager(this.app); + const manager = new ConceptPageManager(this.app, this.settings); const empties = await manager.scanEmptyConcepts(); notice.hide(); diff --git a/src/types.ts b/src/types.ts index 35dda94..25f5c73 100644 --- a/src/types.ts +++ b/src/types.ts @@ -52,3 +52,18 @@ export interface Relation { relation: string; to: string; } + +export interface ContextQAInput { + question: string; + context: string; // 框选内容 + sourceNote: string; // 来源文件路径 + surroundingContext?: string; // 上下文段落(可选) +} + +export interface ContextQAResponse { + answer: string; + concepts: string[]; + relations: Relation[]; + suggested_questions: string[]; + tags: string[]; +}