This commit is contained in:
yan 2026-04-25 17:05:01 +08:00
parent f933d8e975
commit 45c86cf17e
9 changed files with 649 additions and 33 deletions

260
Readme.md
View file

@ -1,3 +1,257 @@
快捷键 Cmd+Shift+D 或点击侧边栏脑图标打开提问框
在设置中填入 DeepSeek API Key
提问后自动生成带双链的 Markdown 笔记,存入 Knowledge/Q&A/,同时自动创建 Knowledge/Concepts/ 下的概念页
# 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

View file

@ -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

View file

@ -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",

View file

@ -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<ConceptPageInfo | null> {
@ -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<void> {
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`
);
}
}
}
}

79
src/ContextQAClient.ts Normal file
View file

@ -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<ContextQAResponse> {
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: [] };
}
}
}

74
src/ContextQAModal.ts Normal file
View file

@ -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();
}
}

View file

@ -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<TFile> {
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<void> {
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<void> {
const folderPath = normalizePath(this.settings.conceptsPath || "Knowledge/Concepts");
const filePath = normalizePath(`${folderPath}/${concept}.md`);

View file

@ -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();

View file

@ -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[];
}