对章节进行补充

This commit is contained in:
yan 2026-04-25 17:12:36 +08:00
parent 45c86cf17e
commit 26e80d696b
5 changed files with 362 additions and 2 deletions

View file

@ -12,6 +12,7 @@
| 框选提问 | 选中文本 → 基于上下文提问 → 生成带来源引用的笔记 |
| 问题分类 | 自动判断问题类型(新问题 / 深化 / 扩展)并建立关联 |
| 概念补全 | 按需补全空概念页,支持轻量 / 标准两种深度 |
| 章节追加 | 对已有章节(如"示例")按需补充更多条目 |
| 批量扫描 | 扫描 Vault 中所有空概念页,批量补全 |
| 问题索引 | 自动维护问题图谱索引页 |
@ -77,6 +78,18 @@ Knowledge/
3. 在弹窗中输入针对该内容的问题
4. 生成的笔记包含来源引用,并在原笔记末尾追加反向链接
### 章节内容追加
对概念页任意章节(示例、关联概念、相关问题等)补充更多条目:
1. 将光标置于目标章节内(如 `## 示例` 下方任意位置)
2. 右键 → `IStart-Note-AI补充"示例"内容`
或命令面板:`为当前章节补充内容`
3. 选择本次新增条目数2 / 3 / 5 / 8
4. 预览生成结果,确认追加或重新生成
DeepSeek 会读取该章节现有内容作为上下文,**不重复已有条目**,风格保持一致。
### 概念页补全
**单个补全:**
@ -210,7 +223,8 @@ src/
├── ConceptCompleter.ts # 概念补全 API 调用
├── ConceptPageManager.ts # 概念页识别 / 增量写入 / 批量扫描
├── ConceptCompletionModal.ts # 深度选择 / 预览确认 / 批量扫描弹窗
└── SettingsTab.ts # 设置页面
├── SectionAppender.ts # 章节内容提取、DeepSeek 追加生成、写入
├── SectionAppendModal.ts # 数量选择弹窗 + 预览确认弹窗
```
### 扩展开发
@ -229,6 +243,12 @@ src/
## 版本历史
### v1.4.0
- 新增章节内容追加功能(对已有章节按需补充更多条目)
- 右键菜单自动识别光标所在章节
- 追加内容不重复已有条目,风格一致
- 支持预览确认 / 重新生成
### v1.3.0
- 新增框选提问Context Q&A功能
- 框选提问支持上下文传入、来源引用、反向链接

View file

@ -1,7 +1,7 @@
{
"id": "istart-note-ai",
"name": "IStart-Note-AI",
"version": "1.3.0",
"version": "1.4.0",
"minAppVersion": "1.0.0",
"description": "基于 DeepSeek AI 的知识图谱插件:框选提问、概念补全、问题图谱、自动双链",
"author": "Your Name",

105
src/SectionAppendModal.ts Normal file
View file

@ -0,0 +1,105 @@
import { App, Modal, Setting } from "obsidian";
export class SectionAppendModal extends Modal {
private count = 3;
constructor(
app: App,
private sectionName: string,
private existingCount: number,
private onConfirm: (count: number) => void
) {
super(app);
this.titleEl.setText(`补充章节:${sectionName}`);
}
onOpen() {
const { contentEl } = this;
contentEl.createEl("p", {
text: `当前已有 ${this.existingCount} 条内容,选择本次新增数量:`,
attr: { style: "color: var(--text-muted); margin-bottom: 12px;" },
});
new Setting(contentEl)
.setName("新增条目数")
.addDropdown((drop) =>
drop
.addOption("2", "2 条")
.addOption("3", "3 条(推荐)")
.addOption("5", "5 条")
.addOption("8", "8 条")
.setValue(String(this.count))
.onChange((v) => (this.count = parseInt(v)))
);
new Setting(contentEl)
.addButton((btn) =>
btn.setButtonText("生成").setCta().onClick(() => {
this.close();
this.onConfirm(this.count);
})
)
.addButton((btn) => btn.setButtonText("取消").onClick(() => this.close()));
}
onClose() {
this.contentEl.empty();
}
}
export class SectionPreviewModal extends Modal {
constructor(
app: App,
private sectionName: string,
private newItems: string[],
private onConfirm: () => void,
private onRegenerate: () => void
) {
super(app);
this.titleEl.setText(`预览新增内容:${sectionName}`);
}
onOpen() {
const { contentEl } = this;
const listEl = contentEl.createDiv({
attr: {
style: [
"border: 1px solid var(--background-modifier-border)",
"border-radius: 4px",
"padding: 12px",
"margin-bottom: 16px",
"max-height: 50vh",
"overflow-y: auto",
].join(";"),
},
});
for (const item of this.newItems) {
listEl.createEl("div", {
text: `${item}`,
attr: { style: "padding: 3px 0; font-size: 14px;" },
});
}
new Setting(contentEl)
.addButton((btn) =>
btn.setButtonText("追加写入").setCta().onClick(() => {
this.close();
this.onConfirm();
})
)
.addButton((btn) =>
btn.setButtonText("重新生成").onClick(() => {
this.close();
this.onRegenerate();
})
)
.addButton((btn) => btn.setButtonText("取消").onClick(() => this.close()));
}
onClose() {
this.contentEl.empty();
}
}

148
src/SectionAppender.ts Normal file
View file

@ -0,0 +1,148 @@
import { App, TFile } from "obsidian";
import { DeepSeekSettings } from "./types";
export interface SectionAppendResult {
items: string[]; // 新增条目列表
raw: string; // 原始追加文本
}
const APPEND_PROMPT = `你是一个个人知识图谱助手。用户希望为概念页的某个章节补充更多内容。
{{concept}}
{{section}}
{{existing}}
1.
2.
3. {{count}}
4. JSON
{
"items": ["新条目1", "新条目2"]
}`;
export class SectionAppender {
constructor(private app: App, private settings: DeepSeekSettings) {}
/** 从文件内容中提取指定 section 的现有内容 */
extractSection(content: string, sectionName: string): { existing: string; startIndex: number; endIndex: number } | null {
// 匹配 ## sectionName 到下一个 ## 或文件末尾
const regex = new RegExp(`(^##\\s+${this.escapeRegex(sectionName)}\\s*\\n)([\\s\\S]*?)(?=\\n##\\s|$)`, "m");
const match = content.match(regex);
if (!match) return null;
const startIndex = content.indexOf(match[0]);
const headerEnd = startIndex + match[1].length;
const endIndex = startIndex + match[0].length;
return {
existing: match[2].trim(),
startIndex: headerEnd,
endIndex,
};
}
/** 识别光标所在的 section 名称 */
getSectionAtCursor(content: string, cursorLine: number): string | null {
const lines = content.split("\n");
// 从光标行向上找最近的 ## 标题
for (let i = cursorLine; i >= 0; i--) {
const match = lines[i]?.match(/^##\s+(.+)/);
if (match) return match[1].trim();
}
return null;
}
/** 调用 DeepSeek 生成追加内容 */
async generate(
conceptName: string,
sectionName: string,
existingContent: string,
count = 3
): Promise<SectionAppendResult> {
if (!this.settings.apiKey) {
throw new Error("请先配置 API Key");
}
const prompt = APPEND_PROMPT
.replace("{{concept}}", conceptName)
.replace("{{section}}", sectionName)
.replace("{{existing}}", existingContent || "(暂无内容)")
.replace("{{count}}", String(count));
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: prompt }],
temperature: 0.7,
}),
});
if (!response.ok) {
const err = await response.text();
throw new Error(`API 错误: ${response.status} - ${err}`);
}
const data = await response.json();
const raw = data.choices?.[0]?.message?.content ?? "";
return this.parse(raw, sectionName);
}
/** 将新条目追加写入文件的指定 section */
async appendToSection(file: TFile, sectionName: string, newItems: string[]): Promise<void> {
const content = await this.app.vault.read(file);
const section = this.extractSection(content, sectionName);
if (!section) {
// section 不存在则在文件末尾新建
const appendText = `\n## ${sectionName}\n${this.formatItems(newItems, sectionName)}\n`;
await this.app.vault.modify(file, content.trimEnd() + appendText);
return;
}
const newText = this.formatItems(newItems, sectionName);
const updated =
content.slice(0, section.endIndex).trimEnd() +
"\n" + newText + "\n" +
content.slice(section.endIndex);
await this.app.vault.modify(file, updated);
}
private formatItems(items: string[], sectionName: string): string {
// 关联概念保持 [[链接]] 格式,其他用列表
if (sectionName === "关联概念") {
return items.map((item) => {
// 如果已经是 [[xxx]]yyy 格式则直接用,否则包装
return item.startsWith("- ") ? item : `- ${item}`;
}).join("\n");
}
return items.map((item) => `- ${item}`).join("\n");
}
private parse(content: string, sectionName: string): SectionAppendResult {
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());
const items: string[] = Array.isArray(p.items) ? p.items : [];
return { items, raw: this.formatItems(items, sectionName) };
} catch {
// 降级:把每行当作一个条目
const items = content.split("\n").map((l) => l.replace(/^[-*]\s*/, "").trim()).filter(Boolean);
return { items, raw: this.formatItems(items, sectionName) };
}
}
private escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
}

View file

@ -12,6 +12,8 @@ import { QuestionGraphManager } from "./QuestionGraphManager";
import { QuestionClassifyModal } from "./QuestionClassifyModal";
import { ContextQAClient } from "./ContextQAClient";
import { ContextQAModal } from "./ContextQAModal";
import { SectionAppender } from "./SectionAppender";
import { SectionAppendModal, SectionPreviewModal } from "./SectionAppendModal";
export default class DeepSeekPlugin extends Plugin {
settings: DeepSeekSettings;
@ -62,6 +64,25 @@ export default class DeepSeekPlugin extends Plugin {
},
});
// 命令:补充当前章节
this.addCommand({
id: "append-current-section",
name: "为当前章节补充内容",
editorCallback: (editor) => {
const cursor = editor.getCursor();
const content = editor.getValue();
const appender = new SectionAppender(this.app, this.settings);
const sectionName = appender.getSectionAtCursor(content, cursor.line);
if (!sectionName) {
new Notice("请将光标置于某个章节(## 标题)内");
return;
}
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile) return;
this.runSectionAppend(activeFile, sectionName, content);
},
});
// 命令:问题图谱索引
this.addCommand({
id: "open-question-index",
@ -115,6 +136,24 @@ export default class DeepSeekPlugin extends Plugin {
});
}
// 章节补充入口(光标在 ## 章节内时显示)
const cursor = editor.getCursor();
const fullContent = editor.getValue();
const appender = new SectionAppender(this.app, this.settings);
const sectionAtCursor = appender.getSectionAtCursor(fullContent, cursor.line);
if (sectionAtCursor) {
menu.addItem((item) => {
item
.setTitle(`IStart-Note-AI补充"${sectionAtCursor}"内容`)
.setIcon("plus-circle")
.onClick(() => {
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile) return;
this.runSectionAppend(activeFile, sectionAtCursor, fullContent);
});
});
}
// 概念补全入口
const linkMatch = selection.match(/^\[\[(.+?)(?:\|.+?)?\]\]$/) ||
selection.match(/^(.+)$/);
@ -361,6 +400,54 @@ export default class DeepSeekPlugin extends Plugin {
}
}
private async runSectionAppend(file: TFile, sectionName: string, content: string) {
const appender = new SectionAppender(this.app, this.settings);
const section = appender.extractSection(content, sectionName);
const existingItems = section?.existing
.split("\n")
.filter((l) => l.trim().startsWith("-"))
.length ?? 0;
// 获取概念名(文件名或 frontmatter.name
const meta = this.app.metadataCache.getFileCache(file);
const conceptName = (meta?.frontmatter?.name as string) || file.basename;
new SectionAppendModal(this.app, sectionName, existingItems, (count) => {
this.generateAndPreviewSection(file, conceptName, sectionName, section?.existing ?? "", count);
}).open();
}
private async generateAndPreviewSection(
file: TFile,
conceptName: string,
sectionName: string,
existingContent: string,
count: number
) {
const notice = new Notice(`⏳ 生成"${sectionName}"补充内容...`, 0);
try {
const appender = new SectionAppender(this.app, this.settings);
const result = await appender.generate(conceptName, sectionName, existingContent, count);
notice.hide();
new SectionPreviewModal(
this.app,
sectionName,
result.items,
async () => {
await appender.appendToSection(file, sectionName, result.items);
new Notice(`✅ 已追加 ${result.items.length} 条内容到"${sectionName}"`);
},
() => {
this.generateAndPreviewSection(file, conceptName, sectionName, existingContent, count);
}
).open();
} catch (err) {
notice.hide();
new Notice(`❌ 生成失败:${err.message}`);
}
}
private async scanAndBatchComplete() {
const notice = new Notice("🔍 扫描空概念页中...", 0);
const manager = new ConceptPageManager(this.app, this.settings);