release: 1.7.2

This commit is contained in:
yan 2026-05-07 22:11:41 +08:00
parent bef0ecbc87
commit 22fda29f01
13 changed files with 1991 additions and 78 deletions

BIN
.DS_Store vendored

Binary file not shown.

View file

@ -1,7 +1,7 @@
{
"id": "istart-note-ai",
"name": "IStart-Note-AI",
"version": "1.7.1",
"version": "1.7.2",
"minAppVersion": "1.4.0",
"description": "Generate structured knowledge notes from questions and selected text using DeepSeek AI, with automatic concept pages, bidirectional links, and a question graph.",
"author": "Yan",

View file

@ -1,6 +1,6 @@
{
"name": "istart-note-ai",
"version": "1.7.1",
"version": "1.7.2",
"description": "IStart-Note-AI: DeepSeek-powered knowledge graph plugin for Obsidian",
"main": "main.js",
"scripts": {

284
src/ai/ReadingPlanner.ts Normal file
View file

@ -0,0 +1,284 @@
import { requestUrl } from "obsidian";
import { DeepSeekSettings } from "../types";
export interface ReadingPlan {
bookTitle: string;
author: string;
oneLiner: string;
coreQuestions: string[];
prerequisites: string[];
chapters: ChapterSkeleton[];
chapterRelations: string[];
keyConcepts: string[];
}
/** 骨架:只有标题和重要度,不含详细问题 */
export interface ChapterSkeleton {
number: number;
title: string;
summary: string;
importance: "core" | "recommended" | "optional";
keyConcepts: string[];
}
/** 单章详情:预设问题 */
export interface ChapterDetail {
number: number;
title: string;
questions: string[];
}
export interface ChapterSummaryResult {
summary: string;
answeredQuestions: { question: string; answer: string }[];
newConcepts: string[];
connections: string[];
mermaid?: string;
}
export interface FeynmanQuestion {
question: string;
difficulty: "basic" | "intermediate" | "advanced";
hint: string;
}
// ── Prompts ──────────────────────────────────────────────────
const SKELETON_PROMPT = `你是一个阅读规划助手。用户准备阅读一本书,请生成全书骨架。
{{book_info}}
{{toc_section}}
1.
2. importance: core=, recommended=, optional=
3. chapterRelations Mermaid graph
4. keyConcepts 10-15
5. summary 1-2
JSON
{
"bookTitle": "书名",
"author": "作者",
"oneLiner": "一句话概括",
"coreQuestions": ["核心问题1", "核心问题2", "核心问题3"],
"prerequisites": ["前置知识1"],
"chapters": [
{ "number": 1, "title": "章节标题", "summary": "本章讲什么", "importance": "core", "keyConcepts": ["概念1"] }
],
"chapterRelations": ["Ch1[标题1] --> Ch2[标题2]"],
"keyConcepts": ["概念1", "概念2"]
}`;
const CHAPTER_DETAIL_PROMPT = `你是一个阅读规划助手。请为以下章节生成"读前问题"——用户带着这些问题去阅读,帮助聚焦重点。
{{book}}
{{number}} - {{title}}
{{summary}}
{{concepts}}
1. 3-5
2.
3. 1 "为什么" 1 "如何"
JSON
{
"questions": ["问题1", "问题2", "问题3", "问题4"]
}`;
const CHAPTER_SUMMARY_PROMPT = `你是一个阅读助手。用户读完了一个章节,请基于他的笔记生成章节总结。
{{book}}
{{chapter}}
{{notes}}
{{questions}}
1.
2.
3.
4.
JSON
{
"summary": "章节总结2-3段",
"answeredQuestions": [{ "question": "问题", "answer": "回答" }],
"newConcepts": ["概念1"],
"connections": ["与第X章的关联"],
"mermaid": "graph LR\\n A[概念1] -->|关系| B[概念2]"
}`;
const FEYNMAN_PROMPT = `你是一个学习检验助手。请基于章节内容提出检验性问题。
{{book}}
{{chapter}}
{{concepts}}
{{notes}}
1. 5
2.
3. 1 "如果...会怎样"
4. 1
JSON
{
"questions": [{ "question": "问题", "difficulty": "basic|intermediate|advanced", "hint": "提示" }]
}`;
export class ReadingPlanner {
constructor(private settings: DeepSeekSettings) {}
/** 第一步:生成全书骨架(轻量,一次请求) */
async planSkeleton(bookInfo: string, tableOfContents?: string): Promise<ReadingPlan> {
if (!this.settings.apiKey) throw new Error("请先配置 API Key");
const tocSection = tableOfContents
? `目录(用户提供):\n${tableOfContents}`
: "(用户未提供目录,请基于你对这本书的了解生成)";
const prompt = SKELETON_PROMPT
.replace("{{book_info}}", bookInfo)
.replace("{{toc_section}}", tocSection);
const raw = await this.call(prompt);
return this.parseSkeleton(raw);
}
/** 第二步:为单章生成预设问题 */
async generateChapterQuestions(
book: string,
chapter: ChapterSkeleton
): Promise<ChapterDetail> {
const prompt = CHAPTER_DETAIL_PROMPT
.replace("{{book}}", book)
.replace("{{number}}", String(chapter.number))
.replace("{{title}}", chapter.title)
.replace("{{summary}}", chapter.summary)
.replace("{{concepts}}", chapter.keyConcepts.join("、") || "未知");
const raw = await this.call(prompt);
const parsed = this.extractJson(raw);
try {
const p = JSON.parse(parsed) as { questions: string[] };
return {
number: chapter.number,
title: chapter.title,
questions: Array.isArray(p.questions) ? p.questions : [],
};
} catch {
return { number: chapter.number, title: chapter.title, questions: [] };
}
}
/** 生成章节总结 */
async summarizeChapter(
book: string,
chapter: string,
notes: string,
presetQuestions: string[]
): Promise<ChapterSummaryResult> {
const prompt = CHAPTER_SUMMARY_PROMPT
.replace("{{book}}", book)
.replace("{{chapter}}", chapter)
.replace("{{notes}}", notes.slice(0, 3000))
.replace("{{questions}}", presetQuestions.map((q, i) => `${i + 1}. ${q}`).join("\n"));
const raw = await this.call(prompt);
return this.parseChapterSummary(raw);
}
/** 费曼检验 */
async feynmanTest(
book: string,
chapter: string,
concepts: string[],
notes: string
): Promise<FeynmanQuestion[]> {
const prompt = FEYNMAN_PROMPT
.replace("{{book}}", book)
.replace("{{chapter}}", chapter)
.replace("{{concepts}}", concepts.join("、"))
.replace("{{notes}}", notes.slice(0, 2000));
const raw = await this.call(prompt);
return this.parseFeynman(raw);
}
private async call(prompt: string): Promise<string> {
const res = await requestUrl({
url: `${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.5,
}),
throw: false,
});
if (res.status !== 200) throw new Error(`API 错误: ${res.status}`);
return res.json.choices?.[0]?.message?.content ?? "";
}
private parseSkeleton(raw: string): ReadingPlan {
const jsonStr = this.extractJson(raw);
try {
const p = JSON.parse(jsonStr) as ReadingPlan;
return {
bookTitle: p.bookTitle || "未知书名",
author: p.author || "未知作者",
oneLiner: p.oneLiner || "",
coreQuestions: Array.isArray(p.coreQuestions) ? p.coreQuestions : [],
prerequisites: Array.isArray(p.prerequisites) ? p.prerequisites : [],
chapters: Array.isArray(p.chapters) ? p.chapters : [],
chapterRelations: Array.isArray(p.chapterRelations) ? p.chapterRelations : [],
keyConcepts: Array.isArray(p.keyConcepts) ? p.keyConcepts : [],
};
} catch {
throw new Error("AI 返回格式异常,请重试");
}
}
private parseChapterSummary(raw: string): ChapterSummaryResult {
const jsonStr = this.extractJson(raw);
try {
const p = JSON.parse(jsonStr) as ChapterSummaryResult;
return {
summary: p.summary || "",
answeredQuestions: Array.isArray(p.answeredQuestions) ? p.answeredQuestions : [],
newConcepts: Array.isArray(p.newConcepts) ? p.newConcepts : [],
connections: Array.isArray(p.connections) ? p.connections : [],
mermaid: p.mermaid || undefined,
};
} catch {
return { summary: raw, answeredQuestions: [], newConcepts: [], connections: [] };
}
}
private parseFeynman(raw: string): FeynmanQuestion[] {
const jsonStr = this.extractJson(raw);
try {
const p = JSON.parse(jsonStr) as { questions: FeynmanQuestion[] };
return Array.isArray(p.questions) ? p.questions : [];
} catch {
return [];
}
}
private extractJson(raw: string): string {
const match = raw.match(/```(?:json)?\s*([\s\S]*?)```/) || raw.match(/(\{[\s\S]*\})/);
return match ? match[1] : raw;
}
}

165
src/ai/SmartCompleter.ts Normal file
View file

@ -0,0 +1,165 @@
import { requestUrl } from "obsidian";
import { DeepSeekSettings } from "../types";
export type CompletionMode =
| "concept" // 概念页补全
| "section" // 空 section 补全
| "expand" // 选中文字扩写
| "document" // 文档缺失分析
| "continue"; // 续写
export interface SmartCompletionResult {
mode: CompletionMode;
content: string; // 生成的 Markdown 内容
explanation?: string; // 简短说明
}
const SECTION_PROMPT = `你是一个知识笔记助手。用户有一篇笔记,其中某个章节是空的,请根据文件上下文为该章节生成内容。
{{title}}
{{section}}
{{context}}
1.
2.
3.
4. "示例""相关问题" - `;
const EXPAND_PROMPT = `你是一个知识笔记助手。用户选中了一段文字,请帮助扩写/补全。
{{selection}}
{{context}}
1.
2.
3. + `;
const CONTINUE_PROMPT = `你是一个知识笔记助手。用户的光标在文件末尾或段落末尾,请续写内容。
{{before}}
1.
2.
3. 2-4
4. `;
const DOCUMENT_PROMPT = `你是一个知识笔记助手。请分析以下文档,找出可以补充的部分。
{{content}}
1.
2.
3. JSON
{
"suggestions": [
{
"section": "建议补充的章节名(已有章节名或新章节名)",
"reason": "为什么需要补充",
"content": "建议的内容"
}
]
}`;
export interface DocumentSuggestion {
section: string;
reason: string;
content: string;
}
export class SmartCompleter {
constructor(private settings: DeepSeekSettings) {}
/** 补全空 section */
async completeSection(
title: string,
sectionName: string,
fileContext: string
): Promise<SmartCompletionResult> {
const prompt = SECTION_PROMPT
.replace("{{title}}", title)
.replace("{{section}}", sectionName)
.replace("{{context}}", fileContext.slice(0, 1500));
const content = await this.call(prompt);
return { mode: "section", content, explanation: `已补全"${sectionName}"` };
}
/** 扩写选中文字 */
async expand(selection: string, context: string): Promise<SmartCompletionResult> {
const prompt = EXPAND_PROMPT
.replace("{{selection}}", selection)
.replace("{{context}}", context.slice(0, 1500));
const content = await this.call(prompt);
return { mode: "expand", content, explanation: "已扩写选中内容" };
}
/** 续写 */
async continueWriting(beforeCursor: string): Promise<SmartCompletionResult> {
const prompt = CONTINUE_PROMPT
.replace("{{before}}", beforeCursor.slice(-1500));
const content = await this.call(prompt);
return { mode: "continue", content, explanation: "已续写" };
}
/** 分析文档缺失部分 */
async analyzeDocument(content: string): Promise<DocumentSuggestion[]> {
const prompt = DOCUMENT_PROMPT
.replace("{{content}}", content.slice(0, 3000));
const raw = await this.call(prompt);
return this.parseDocumentSuggestions(raw);
}
private async call(prompt: string): Promise<string> {
if (!this.settings.apiKey) {
throw new Error("请先配置 API Key");
}
const res = await requestUrl({
url: `${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.6,
}),
throw: false,
});
if (res.status !== 200) {
throw new Error(`API 错误: ${res.status} - ${res.text}`);
}
return res.json.choices?.[0]?.message?.content ?? "";
}
private parseDocumentSuggestions(raw: string): DocumentSuggestion[] {
const jsonMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/) || raw.match(/(\{[\s\S]*\})/);
const jsonStr = jsonMatch ? jsonMatch[1] : raw;
try {
const p = JSON.parse(jsonStr.trim()) as Record<string, unknown>;
const suggestions = p.suggestions;
if (!Array.isArray(suggestions)) return [];
return suggestions as DocumentSuggestion[];
} catch {
return [];
}
}
}

View file

@ -0,0 +1,176 @@
import { App, Modal, TFile } from "obsidian";
export interface PanelAction {
id: string;
icon: string;
label: string;
description?: string;
callback: () => void;
}
export interface PanelGroup {
title: string;
actions: PanelAction[];
}
/**
*
*/
export class CommandPanelModal extends Modal {
private groups: PanelGroup[];
constructor(app: App, groups: PanelGroup[]) {
super(app);
this.groups = groups;
this.titleEl.setText("IStart-Note-AI");
}
onOpen() {
const { contentEl } = this;
contentEl.addClass("istart-command-panel");
let shortcutIndex = 1;
for (const group of this.groups) {
if (group.actions.length === 0) continue;
const groupEl = contentEl.createDiv({ cls: "istart-panel-group" });
groupEl.createEl("div", { text: group.title, cls: "istart-panel-group-title" });
for (const action of group.actions) {
const row = groupEl.createDiv({ cls: "istart-panel-action" });
const currentIndex = shortcutIndex;
row.createSpan({ text: `${action.icon}`, cls: "istart-panel-action-icon" });
const textEl = row.createDiv({ cls: "istart-panel-action-text" });
textEl.createEl("span", { text: action.label, cls: "istart-panel-action-label" });
if (action.description) {
textEl.createEl("span", { text: action.description, cls: "istart-panel-action-desc" });
}
if (shortcutIndex <= 9) {
row.createSpan({ text: `${shortcutIndex}`, cls: "istart-panel-action-key" });
}
row.addEventListener("click", () => {
this.close();
action.callback();
});
shortcutIndex++;
}
}
// 键盘快捷键支持
const handler = (e: KeyboardEvent) => {
const num = parseInt(e.key);
if (num >= 1 && num <= 9) {
const allActions = this.groups.flatMap((g) => g.actions);
const action = allActions[num - 1];
if (action) {
this.close();
action.callback();
}
}
if (e.key === "Escape") {
this.close();
}
};
document.addEventListener("keydown", handler);
this.onClose = () => {
document.removeEventListener("keydown", handler);
contentEl.empty();
};
}
}
/**
*
*/
export function buildPanelGroups(context: {
hasSelection: boolean;
selection: string;
isConceptPage: boolean;
isReadingNote: boolean;
isInSection: boolean;
sectionName: string | null;
activeFile: TFile | null;
// 回调
onAsk: () => void;
onContextQA: () => void;
onNewReading: () => void;
onSmartComplete: () => void;
onDiagram: () => void;
onExpand: () => void;
onContinue: () => void;
onCompleteConcept: () => void;
onScanConcepts: () => void;
onChapterSummary: () => void;
onFeynmanTest: () => void;
onAnalyzeDoc: () => void;
onSectionAppend: () => void;
}): PanelGroup[] {
const groups: PanelGroup[] = [];
// 通用操作
const general: PanelAction[] = [
{ id: "ask", icon: "💬", label: "提问", description: "向 AI 提问并生成知识笔记", callback: context.onAsk },
{ id: "reading", icon: "📖", label: "新建阅读项目", description: "输入书名,生成阅读地图", callback: context.onNewReading },
];
groups.push({ title: "通用", actions: general });
// 选中文字相关
if (context.hasSelection) {
const selectionActions: PanelAction[] = [
{ id: "context-qa", icon: "❓", label: "基于选中内容提问", callback: context.onContextQA },
{ id: "diagram", icon: "📊", label: "生成图表 / 公式", callback: context.onDiagram },
{ id: "expand", icon: "📝", label: "扩写选中内容", callback: context.onExpand },
];
groups.push({ title: "选中文字", actions: selectionActions });
}
// 编辑操作
const editActions: PanelAction[] = [
{ id: "smart-complete", icon: "✨", label: "智能补全", description: "自动判断:补全/扩写/续写", callback: context.onSmartComplete },
];
if (!context.hasSelection) {
editActions.push({ id: "continue", icon: "🔄", label: "续写", description: "从光标位置继续写", callback: context.onContinue });
}
if (context.isInSection && context.sectionName) {
editActions.push({ id: "section-append", icon: "📑", label: `补充"${context.sectionName}"`, callback: context.onSectionAppend });
}
groups.push({ title: "编辑", actions: editActions });
// 概念页
if (context.isConceptPage) {
groups.push({
title: "概念页",
actions: [
{ id: "complete-concept", icon: "🧩", label: "补全当前概念页", callback: context.onCompleteConcept },
{ id: "scan-concepts", icon: "🔍", label: "扫描空概念页", callback: context.onScanConcepts },
],
});
}
// 阅读笔记
if (context.isReadingNote) {
groups.push({
title: "阅读笔记",
actions: [
{ id: "chapter-summary", icon: "📋", label: "生成章节总结", callback: context.onChapterSummary },
{ id: "feynman", icon: "🎓", label: "费曼检验", callback: context.onFeynmanTest },
],
});
}
// 文档工具
groups.push({
title: "文档工具",
actions: [
{ id: "analyze", icon: "🔎", label: "分析文档缺失", description: "AI 分析并建议补充内容", callback: context.onAnalyzeDoc },
],
});
return groups;
}

View file

@ -65,15 +65,15 @@ export class ConceptPageManager {
): Promise<void> {
const content = await this.app.vault.read(file);
const { frontmatter, body } = this.splitFrontmatter(content);
const existingSections = this.getExistingSections(body);
// 清除空占位标题(只有标题没有内容的 section
// 先清除空占位标题,再判断哪些 section 已有内容
const cleanedBody = this.removeEmptySections(body);
const existingSections = this.getExistingSections(cleanedBody);
const newSections = this.buildSections(result, existingSections, depth);
const updatedFrontmatter = this.updateFrontmatter(frontmatter, result);
// 合并:保留有内容的部分 + 追加新内容
// 合并:cleanedBody 只保留顶级标题和有内容的 section新内容追加
const updatedBody = this.mergeSections(cleanedBody, newSections);
const newContent = updatedFrontmatter

View file

@ -0,0 +1,114 @@
import { App, Modal, Setting, Notice } from "obsidian";
/**
*
*/
export class NewReadingModal extends Modal {
private bookInfo = "";
private toc = "";
constructor(
app: App,
private onSubmit: (bookInfo: string, toc?: string) => void
) {
super(app);
this.titleEl.setText("新建阅读项目");
}
onOpen() {
const { contentEl } = this;
contentEl.createEl("p", {
text: "输入书名和作者AI 会生成阅读地图。如果是小众书籍,可以粘贴目录帮助 AI 理解。",
cls: "istart-diagram-hint",
});
new Setting(contentEl)
.setName("书名 / 作者")
.setDesc("如:分布式系统:概念与设计 - George Coulouris")
.addText((text) =>
text
.setPlaceholder("书名 - 作者")
.onChange((v) => { this.bookInfo = v.trim(); })
);
contentEl.createEl("p", {
text: "目录(可选,粘贴书籍目录可以让 AI 更准确):",
cls: "istart-diagram-hint",
});
const tocArea = contentEl.createEl("textarea", {
attr: {
placeholder: "第1章 概述\n第2章 系统模型\n第3章 ...\n\n留空则由 AI 基于书名推断)",
rows: "8",
},
cls: "istart-question-textarea",
});
tocArea.addEventListener("input", () => { this.toc = tocArea.value; });
new Setting(contentEl)
.addButton((btn) =>
btn.setButtonText("生成阅读地图").setCta().onClick(() => {
if (!this.bookInfo) {
new Notice("请输入书名");
return;
}
this.close();
this.onSubmit(this.bookInfo, this.toc.trim() || undefined);
})
)
.addButton((btn) => btn.setButtonText("取消").onClick(() => this.close()));
}
onClose() {
this.contentEl.empty();
}
}
/**
*
*/
export class FeynmanModal extends Modal {
constructor(
app: App,
private chapter: string,
private questions: { question: string; difficulty: string; hint: string }[]
) {
super(app);
this.titleEl.setText(`费曼检验:${chapter}`);
}
onOpen() {
const { contentEl } = this;
if (this.questions.length === 0) {
contentEl.createEl("p", { text: "暂无检验问题,请先在章节中记录笔记。" });
new Setting(contentEl).addButton((btn) => btn.setButtonText("关闭").onClick(() => this.close()));
return;
}
const difficultyLabels: Record<string, string> = {
basic: "🟢 基础",
intermediate: "🟡 进阶",
advanced: "🔴 深入",
};
for (const q of this.questions) {
const row = contentEl.createDiv({ cls: "istart-smart-suggestion-row" });
const header = row.createDiv({ cls: "istart-smart-suggestion-header" });
header.createEl("span", { text: difficultyLabels[q.difficulty] || "❓" });
header.createEl("strong", { text: q.question });
const details = row.createEl("details");
details.createEl("summary", { text: "💡 提示" });
details.createEl("p", { text: q.hint, cls: "istart-smart-suggestion-reason" });
}
new Setting(contentEl)
.addButton((btn) => btn.setButtonText("关闭").onClick(() => this.close()));
}
onClose() {
this.contentEl.empty();
}
}

View file

@ -0,0 +1,369 @@
import { App, TFile, normalizePath, Notice } from "obsidian";
import { ReadingPlan, ChapterSkeleton, ChapterDetail, ChapterSummaryResult, ReadingPlanner } from "../../ai/ReadingPlanner";
import { DeepSeekSettings } from "../../types";
const READING_ROOT = "Knowledge/Reading";
export class ReadingProjectManager {
constructor(private app: App, private settings: DeepSeekSettings) {}
/**
*
*
*/
async createProject(
plan: ReadingPlan,
onProgress?: (current: number, total: number, chapter: string) => void
): Promise<TFile> {
const projectFolder = normalizePath(`${READING_ROOT}/${this.sanitize(plan.bookTitle)}`);
await this.ensureFolder(projectFolder);
// 1. 创建索引页(骨架)
const indexFile = await this.createIndexPage(projectFolder, plan);
// 2. 创建所有章节笔记(空模板,不含问题)
for (const chapter of plan.chapters) {
await this.createChapterNote(projectFolder, plan.bookTitle, chapter);
}
// 3. 逐章生成预设问题
const planner = new ReadingPlanner(this.settings);
let generated = 0;
for (const chapter of plan.chapters) {
generated++;
onProgress?.(generated, plan.chapters.length, chapter.title);
try {
const detail = await planner.generateChapterQuestions(plan.bookTitle, chapter);
await this.writeChapterQuestions(projectFolder, chapter, detail);
} catch {
// 单章失败不中断整个流程,后续可以补全
console.error(`[Reading] Failed to generate questions for chapter ${chapter.number}`);
}
}
// 4. 更新索引页状态
await this.updateGenerationStatus(indexFile, generated, plan.chapters.length);
// 5. 预创建核心概念页
await this.ensureConceptNotes(plan.keyConcepts, plan.bookTitle);
return indexFile;
}
/**
*
*/
async resumeProject(
indexFile: TFile,
onProgress?: (current: number, total: number, chapter: string) => void
): Promise<number> {
const content = await this.app.vault.read(indexFile);
const meta = this.app.metadataCache.getFileCache(indexFile);
const fm = meta?.frontmatter;
if (fm?.type !== "reading-project") {
throw new Error("当前文件不是阅读项目索引页");
}
const bookTitle = (fm.book as string) || "";
const projectFolder = indexFile.parent?.path ?? "";
// 扫描所有章节笔记,找出缺少问题的
const chapterFiles = this.app.vault.getMarkdownFiles().filter(
(f) => f.path.startsWith(projectFolder) && f.path !== indexFile.path && f.name !== "_总结.md"
);
const incomplete: { file: TFile; number: number; title: string; summary: string; concepts: string[] }[] = [];
for (const file of chapterFiles) {
const fileMeta = this.app.metadataCache.getFileCache(file);
const fileFm = fileMeta?.frontmatter;
if (fileFm?.type !== "reading-note") continue;
if (fileFm?.questions_generated === true) continue;
// 读取文件检查是否有预设问题内容
const fileContent = await this.app.vault.read(file);
const questionsMatch = fileContent.match(/## 读前问题\n([\s\S]*?)(?=\n## )/);
const hasQuestions = questionsMatch
? questionsMatch[1].split("\n").filter((l) => l.trim().startsWith("- [")).length > 0
: false;
if (!hasQuestions) {
incomplete.push({
file,
number: (fileFm?.chapter as number) || 0,
title: (fileFm?.title as string) || file.basename,
summary: "",
concepts: [],
});
}
}
if (incomplete.length === 0) return 0;
const planner = new ReadingPlanner(this.settings);
let done = 0;
for (const ch of incomplete) {
done++;
onProgress?.(done, incomplete.length, ch.title);
try {
const skeleton: ChapterSkeleton = {
number: ch.number,
title: ch.title,
summary: ch.summary,
importance: "recommended",
keyConcepts: ch.concepts,
};
const detail = await planner.generateChapterQuestions(bookTitle, skeleton);
// 写入问题到章节文件
let fileContent = await this.app.vault.read(ch.file);
const questionsLines = detail.questions.map((q) => `- [ ] ${q}`).join("\n");
if (fileContent.includes("## 读前问题")) {
fileContent = fileContent.replace(
/## 读前问题\n[\s\S]*?(?=\n## )/,
`## 读前问题\n${questionsLines}\n`
);
}
// 更新 frontmatter
fileContent = fileContent.replace(
/questions_generated: false/,
"questions_generated: true"
);
if (!fileContent.includes("questions_generated")) {
fileContent = fileContent.replace("---\n\n", "questions_generated: true\n---\n\n");
}
await this.app.vault.modify(ch.file, fileContent);
} catch {
console.error(`[Reading] Resume failed for chapter ${ch.number}`);
}
}
// 更新索引页状态
const totalChapters = chapterFiles.filter((f) => {
const m = this.app.metadataCache.getFileCache(f);
return m?.frontmatter?.type === "reading-note";
}).length;
await this.updateGenerationStatus(indexFile, totalChapters - (incomplete.length - done), totalChapters);
return done;
}
/** 将章节总结写入章节笔记 */
async writeChapterSummary(file: TFile, result: ChapterSummaryResult): Promise<void> {
let content = await this.app.vault.read(file);
content = content.replace(/status: (unread|reading)/, "status: done");
const summarySection = `## 章节总结\n\n${result.summary}`;
if (content.includes("## 章节总结")) {
content = content.replace(/## 章节总结[\s\S]*?(?=\n## |$)/, summarySection + "\n");
} else {
content = content.trimEnd() + "\n\n" + summarySection + "\n";
}
if (result.answeredQuestions.length > 0) {
const qaLines = result.answeredQuestions
.map((qa) => `**Q: ${qa.question}**\nA: ${qa.answer}`)
.join("\n\n");
content = content.trimEnd() + `\n\n## 问题回答\n\n${qaLines}\n`;
}
if (result.mermaid) {
content = content.trimEnd() + `\n\n## 关系图\n\n\`\`\`mermaid\n${result.mermaid}\n\`\`\`\n`;
}
if (result.connections.length > 0) {
content = content.trimEnd() + `\n\n## 跨章关联\n\n${result.connections.map((c) => `- ${c}`).join("\n")}\n`;
}
await this.app.vault.modify(file, content);
}
// ── 私有方法 ───────────────────────────────────────────────
private async createIndexPage(folder: string, plan: ReadingPlan): Promise<TFile> {
const indexPath = normalizePath(`${folder}/_索引.md`);
const progressLines = plan.chapters.map((ch) => {
const icon = ch.importance === "core" ? "⭐" : ch.importance === "recommended" ? "📖" : "📄";
return `- [ ] ${icon}${ch.number}章:[[${this.sanitize(ch.title)}|${ch.title}]]`;
});
const mermaidLines = plan.chapterRelations.length > 0
? `\`\`\`mermaid\ngraph LR\n ${plan.chapterRelations.join("\n ")}\n\`\`\``
: "";
const conceptLinks = plan.keyConcepts.map((c) => `[[${c}]]`).join(" · ");
const content = `---
type: reading-project
book: "${plan.bookTitle}"
author: "${plan.author}"
started: ${new Date().toISOString().slice(0, 10)}
status: reading
generation_status: generating
chapters_generated: 0
chapters_total: ${plan.chapters.length}
---
# ${plan.bookTitle}
> ${plan.oneLiner}
**** ${plan.author}
##
${plan.coreQuestions.map((q) => `- ${q}`).join("\n")}
##
${plan.prerequisites.length > 0 ? plan.prerequisites.map((p) => `- ${p}`).join("\n") : "- 无特殊前置要求"}
##
${progressLines.join("\n")}
##
${mermaidLines}
##
${conceptLinks}
`;
const existing = this.app.vault.getAbstractFileByPath(indexPath);
if (existing instanceof TFile) {
await this.app.vault.modify(existing, content);
return existing;
}
return await this.app.vault.create(indexPath, content);
}
private async createChapterNote(folder: string, bookTitle: string, chapter: ChapterSkeleton): Promise<void> {
const fileName = this.sanitize(chapter.title);
const filePath = normalizePath(`${folder}/${fileName}.md`);
if (this.app.vault.getAbstractFileByPath(filePath)) return;
const conceptLinks = chapter.keyConcepts.map((c) => `[[${c}]]`).join(" · ");
const content = `---
type: reading-note
book: "${bookTitle}"
chapter: ${chapter.number}
title: "${chapter.title}"
status: unread
importance: ${chapter.importance}
questions_generated: false
---
# ${chapter.number}${chapter.title}
> ${chapter.summary}
##
...
##
-
##
>
##
-
##
##
${conceptLinks}
##
`;
await this.app.vault.create(filePath, content);
}
private async writeChapterQuestions(folder: string, chapter: ChapterSkeleton, detail: ChapterDetail): Promise<void> {
const fileName = this.sanitize(chapter.title);
const filePath = normalizePath(`${folder}/${fileName}.md`);
const file = this.app.vault.getAbstractFileByPath(filePath);
if (!file || !(file instanceof TFile)) return;
let content = await this.app.vault.read(file);
const questionsLines = detail.questions.map((q) => `- [ ] ${q}`).join("\n");
// 替换占位文本
content = content.replace(
/## 读前问题\n\n生成中\.\.\./,
`## 读前问题\n\n${questionsLines}`
);
// 更新 frontmatter
content = content.replace("questions_generated: false", "questions_generated: true");
await this.app.vault.modify(file, content);
}
private async updateGenerationStatus(indexFile: TFile, generated: number, total: number): Promise<void> {
let content = await this.app.vault.read(indexFile);
const status = generated >= total ? "complete" : "partial";
content = content.replace(/generation_status: \w+/, `generation_status: ${status}`);
content = content.replace(/chapters_generated: \d+/, `chapters_generated: ${generated}`);
await this.app.vault.modify(indexFile, content);
}
private async ensureConceptNotes(concepts: string[], bookTitle: string): Promise<void> {
const conceptsPath = normalizePath(this.settings.conceptsPath || "Knowledge/Concepts");
const uncategorizedPath = normalizePath(`${conceptsPath}/_未分类`);
await this.ensureFolder(uncategorizedPath);
const today = new Date().toISOString().slice(0, 10);
for (const concept of concepts) {
const existing = this.app.vault.getMarkdownFiles().find(
(f) => f.path.startsWith(conceptsPath) && f.basename === concept
);
if (existing) continue;
const filePath = normalizePath(`${uncategorizedPath}/${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: reading\nsource_book: "${bookTitle}"\ncreated_at: ${today}\n---\n\n# ${concept}\n\n## 定义\n\n## 核心解释\n\n## 示例\n\n## 关联概念\n\n## 相关问题\n\n## 来源\n`
);
}
}
}
private async ensureFolder(path: string): Promise<void> {
if (!this.app.vault.getAbstractFileByPath(path)) {
await this.app.vault.createFolder(path).catch(() => {});
}
}
private sanitize(name: string): string {
return name.replace(/[\\/:*?"<>|#[\]]/g, "-").replace(/\s+/g, " ").trim();
}
}

View file

@ -0,0 +1,123 @@
import { App, Modal, Setting, MarkdownRenderer, Component } from "obsidian";
import { DocumentSuggestion } from "../../ai/SmartCompleter";
/**
*
*/
export class DocumentAnalysisModal extends Modal {
private component: Component;
private selectedIndices = new Set<number>();
constructor(
app: App,
private suggestions: DocumentSuggestion[],
private onConfirm: (selected: DocumentSuggestion[]) => void
) {
super(app);
this.component = new Component();
this.titleEl.setText("文档分析 — 建议补充");
}
onOpen() {
const { contentEl } = this;
if (this.suggestions.length === 0) {
contentEl.createEl("p", { text: "文档结构完整,暂无补充建议。" });
new Setting(contentEl).addButton((btn) => btn.setButtonText("关闭").onClick(() => this.close()));
return;
}
contentEl.createEl("p", {
text: `发现 ${this.suggestions.length} 处可补充内容,勾选后插入:`,
cls: "istart-diagram-hint",
});
const listEl = contentEl.createDiv({ cls: "istart-smart-suggestions" });
for (let i = 0; i < this.suggestions.length; i++) {
const s = this.suggestions[i];
const row = listEl.createDiv({ cls: "istart-smart-suggestion-row" });
const header = row.createDiv({ cls: "istart-smart-suggestion-header" });
const cb = header.createEl("input", { type: "checkbox" });
header.createEl("strong", { text: `${s.section}` });
header.createEl("span", { text: `${s.reason}`, cls: "istart-smart-suggestion-reason" });
// 预览内容
const previewEl = row.createDiv({ cls: "istart-smart-suggestion-preview" });
void MarkdownRenderer.render(this.app, s.content, previewEl, "", this.component);
cb.addEventListener("change", () => {
if (cb.checked) this.selectedIndices.add(i);
else this.selectedIndices.delete(i);
});
}
new Setting(contentEl)
.addButton((btn) =>
btn.setButtonText("插入选中").setCta().onClick(() => {
const selected = [...this.selectedIndices].map((i) => this.suggestions[i]);
this.close();
this.onConfirm(selected);
})
)
.addButton((btn) =>
btn.setButtonText("全部插入").onClick(() => {
this.close();
this.onConfirm(this.suggestions);
})
)
.addButton((btn) => btn.setButtonText("取消").onClick(() => this.close()));
}
onClose() {
this.component.unload();
this.contentEl.empty();
}
}
/**
* /
*/
export class SmartPreviewModal extends Modal {
private component: Component;
constructor(
app: App,
private title: string,
private content: string,
private onConfirm: () => void,
private onRegenerate: () => void
) {
super(app);
this.component = new Component();
}
onOpen() {
const { contentEl } = this;
this.titleEl.setText(this.title);
const previewEl = contentEl.createDiv({ cls: "istart-diagram-preview" });
void MarkdownRenderer.render(this.app, this.content, previewEl, "", this.component);
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.component.unload();
this.contentEl.empty();
}
}

View file

@ -21,6 +21,12 @@ import { DEFAULT_BAIDU_SYNC_CONFIG } from "./types";
import { BaiduSyncView, SYNC_VIEW_TYPE } from "./features/sync/BaiduSyncView";
import { DiagramGenerator, DiagramType } from "./ai/DiagramGenerator";
import { DiagramTypeModal, DiagramPreviewModal } from "./features/diagram/DiagramModal";
import { SmartCompleter } from "./ai/SmartCompleter";
import { DocumentAnalysisModal, SmartPreviewModal } from "./features/smart-complete/SmartCompleteModal";
import { ReadingPlanner } from "./ai/ReadingPlanner";
import { NewReadingModal, FeynmanModal } from "./features/reading/ReadingModal";
import { ReadingProjectManager } from "./features/reading/ReadingProjectManager";
import { CommandPanelModal, buildPanelGroups } from "./features/command-panel/CommandPanelModal";
export default class DeepSeekPlugin extends Plugin {
settings: DeepSeekSettings;
@ -28,8 +34,8 @@ export default class DeepSeekPlugin extends Plugin {
async onload() {
await this.loadSettings();
this.addRibbonIcon("brain", "DeepSeek ask", () => {
this.openQuestionModal();
this.addRibbonIcon("brain", "IStart-Note-AI", () => {
this.openCommandPanel();
});
this.addRibbonIcon("cloud", "Baidu cloud sync status", () => {
@ -44,6 +50,13 @@ export default class DeepSeekPlugin extends Plugin {
callback: () => this.openQuestionModal(),
});
// 命令:打开命令面板
this.addCommand({
id: "open-panel",
name: "Open command panel",
callback: () => this.openCommandPanel(),
});
this.addCommand({
id: "complete-current-concept",
name: "Complete current concept page",
@ -142,33 +155,157 @@ export default class DeepSeekPlugin extends Plugin {
},
});
// 命令:智能补全(自动判断场景)
this.addCommand({
id: "smart-complete",
name: "Smart complete (auto-detect context)",
editorCallback: (editor) => {
void this.runSmartComplete(editor);
},
});
// 命令:扩写选中内容
this.addCommand({
id: "expand-selection",
name: "Expand selected text",
editorCallback: (editor) => {
const selection = editor.getSelection().trim();
if (!selection) {
new Notice("请先选中要扩写的文字");
return;
}
const context = editor.getValue().slice(0, 1500);
void this.runExpand(selection, context, editor);
},
});
// 命令:续写
this.addCommand({
id: "continue-writing",
name: "Continue writing from cursor",
editorCallback: (editor) => {
const cursor = editor.getCursor();
const beforeCursor = editor.getRange({ line: 0, ch: 0 }, cursor);
if (!beforeCursor.trim()) {
new Notice("光标前没有内容可续写");
return;
}
void this.runContinue(beforeCursor, editor);
},
});
// 命令:分析文档缺失
this.addCommand({
id: "analyze-document",
name: "Analyze document and suggest completions",
editorCallback: (editor) => {
const content = editor.getValue();
if (!content.trim()) {
new Notice("文档为空");
return;
}
void this.runDocumentAnalysis(content, editor);
},
});
// 命令:新建阅读项目
this.addCommand({
id: "new-reading-project",
name: "New reading project (book study)",
callback: () => this.openNewReadingProject(),
});
// 命令:生成章节总结
this.addCommand({
id: "chapter-summary",
name: "Generate chapter summary",
editorCallback: (editor) => {
void this.runChapterSummary(editor);
},
});
// 命令:费曼检验
this.addCommand({
id: "feynman-test",
name: "Feynman test (check understanding)",
editorCallback: (editor) => {
void this.runFeynmanTest(editor);
},
});
// 命令:补全阅读项目(断点续传)
this.addCommand({
id: "resume-reading-project",
name: "Resume reading project (complete missing chapters)",
callback: () => { void this.resumeReadingProject(); },
});
this.addSettingTab(new DeepSeekSettingsTab(this.app, this));
this.registerEvent(
this.app.workspace.on("file-menu", (menu, file) => {
if (!(file instanceof TFile) || file.extension !== "md") return;
menu.addItem((item) => {
item
.setTitle("IStart-Note-AI: Complete this concept page")
.setIcon("brain")
.onClick(() => {
void (async () => {
const manager = new ConceptPageManager(this.app, this.settings);
const info = await manager.analyzeFile(file);
if (!info) {
new Notice("该文件不是概念页");
return;
}
new DepthSelectModal(this.app, info.conceptName, (depth) => {
void this.runConceptCompletion(info.file, info.conceptName, depth, {
sourceQuestion: info.sourceQuestion,
sourceAnswer: info.sourceAnswer,
});
}).open();
})();
});
});
const fileMeta = this.app.metadataCache.getFileCache(file);
const fileType = fileMeta?.frontmatter?.type as string | undefined;
// 概念页:补全
if (fileType === "concept" || file.path.includes("Concepts/")) {
menu.addItem((item) => {
item
.setTitle("IStart-Note-AI: Complete this concept page")
.setIcon("brain")
.onClick(() => {
void (async () => {
const manager = new ConceptPageManager(this.app, this.settings);
const info = await manager.analyzeFile(file);
if (!info) {
new Notice("该文件不是概念页");
return;
}
new DepthSelectModal(this.app, info.conceptName, (depth) => {
void this.runConceptCompletion(info.file, info.conceptName, depth, {
sourceQuestion: info.sourceQuestion,
sourceAnswer: info.sourceAnswer,
});
}).open();
})();
});
});
}
// 阅读项目索引页:补全缺失章节
if (fileType === "reading-project") {
menu.addItem((item) => {
item
.setTitle("IStart-Note-AI: Resume reading project")
.setIcon("refresh-cw")
.onClick(() => { void this.resumeReadingProject(); });
});
}
// 阅读章节笔记:生成总结 / 费曼检验
if (fileType === "reading-note") {
menu.addItem((item) => {
item
.setTitle("IStart-Note-AI: Generate chapter summary")
.setIcon("file-text")
.onClick(() => {
const editor = this.app.workspace.activeEditor?.editor;
if (editor) void this.runChapterSummary(editor);
});
});
menu.addItem((item) => {
item
.setTitle("IStart-Note-AI: Feynman test")
.setIcon("help-circle")
.onClick(() => {
const editor = this.app.workspace.activeEditor?.editor;
if (editor) void this.runFeynmanTest(editor);
});
});
}
})
);
@ -197,6 +334,17 @@ export default class DeepSeekPlugin extends Plugin {
this.openDiagramGenerator(selection, context, editor);
});
});
// 扩写选中内容
menu.addItem((item) => {
item
.setTitle("IStart-Note-AI: Expand selection")
.setIcon("expand")
.onClick(() => {
const context = editor.getValue().slice(0, 1500);
void this.runExpand(selection, context, editor);
});
});
}
const cursor = editor.getCursor();
@ -219,70 +367,203 @@ export default class DeepSeekPlugin extends Plugin {
const linkMatch = selection.match(/^\[\[(.+?)(?:\|.+?)?\]\]$/) ||
selection.match(/^(.+)$/);
const conceptName = linkMatch?.[1];
if (!conceptName) return;
menu.addItem((item) => {
item
.setTitle(`IStart-Note-AI: Complete concept "${conceptName}"`)
.setIcon("brain")
.onClick(() => {
void (async () => {
const manager = new ConceptPageManager(this.app, this.settings);
const conceptsPath = this.settings.conceptsPath || "Knowledge/Concepts";
if (conceptName) {
menu.addItem((item) => {
item
.setTitle(`IStart-Note-AI: Complete concept "${conceptName}"`)
.setIcon("brain")
.onClick(() => {
void (async () => {
const manager = new ConceptPageManager(this.app, this.settings);
const conceptsPath = this.settings.conceptsPath || "Knowledge/Concepts";
// 在所有子目录中查找概念文件
let conceptFile: TFile | null = null;
const allFiles = this.app.vault.getMarkdownFiles();
const found = allFiles.find(
(f) => f.path.startsWith(conceptsPath) && f.basename === conceptName
);
if (found) {
conceptFile = found;
}
if (!conceptFile) {
const writer = new VaultWriter(this.app, this.settings);
await writer.ensureConceptNote(conceptName);
// 重新查找(现在在 _未分类/ 下)
const created = allFiles.find(
// 在所有子目录中查找概念文件
let conceptFile: TFile | null = null;
const allFiles = this.app.vault.getMarkdownFiles();
const found = allFiles.find(
(f) => f.path.startsWith(conceptsPath) && f.basename === conceptName
);
if (created) conceptFile = created;
else {
// 直接用路径查找
const uncatPath = `${conceptsPath}/_未分类/${conceptName}.md`;
const uncatFile = this.app.vault.getAbstractFileByPath(uncatPath);
if (uncatFile instanceof TFile) conceptFile = uncatFile;
if (found) {
conceptFile = found;
}
}
if (!conceptFile) {
new Notice(`无法找到或创建概念页:${conceptName}`);
return;
}
if (!conceptFile) {
const writer = new VaultWriter(this.app, this.settings);
await writer.ensureConceptNote(conceptName);
// 重新查找(现在在 _未分类/ 下)
const created = this.app.vault.getMarkdownFiles().find(
(f) => f.path.startsWith(conceptsPath) && f.basename === conceptName
);
if (created) conceptFile = created;
else {
// 直接用路径查找
const uncatPath = `${conceptsPath}/_未分类/${conceptName}.md`;
const uncatFile = this.app.vault.getAbstractFileByPath(uncatPath);
if (uncatFile instanceof TFile) conceptFile = uncatFile;
}
}
const targetFile = conceptFile;
const info = await manager.analyzeFile(targetFile);
new DepthSelectModal(this.app, conceptName, (depth) => {
void this.runConceptCompletion(targetFile, conceptName, depth, {
sourceQuestion: info?.sourceQuestion,
sourceAnswer: info?.sourceAnswer,
});
}).open();
})();
});
});
if (!conceptFile) {
new Notice(`无法找到或创建概念页:${conceptName}`);
return;
}
const targetFile = conceptFile;
const info = await manager.analyzeFile(targetFile);
new DepthSelectModal(this.app, conceptName, (depth) => {
void this.runConceptCompletion(targetFile, conceptName, depth, {
sourceQuestion: info?.sourceQuestion,
sourceAnswer: info?.sourceAnswer,
});
}).open();
})();
});
});
}
// 根据文件类型显示对应操作
const activeFile = this.app.workspace.getActiveFile();
const activeMeta = activeFile ? this.app.metadataCache.getFileCache(activeFile) : null;
const activeType = activeMeta?.frontmatter?.type as string | undefined;
const isConceptPage = activeType === "concept" || (activeFile?.path.includes("Concepts/") ?? false);
const isReadingNote = activeType === "reading-note";
const isReadingProject = activeType === "reading-project";
if (isConceptPage) {
menu.addItem((item) => {
item
.setTitle("IStart-Note-AI: Complete current concept page")
.setIcon("brain")
.onClick(() => { void this.completeCurrentConcept(); });
});
}
if (isReadingNote) {
menu.addItem((item) => {
item
.setTitle("IStart-Note-AI: Generate chapter summary")
.setIcon("file-text")
.onClick(() => { void this.runChapterSummary(editor); });
});
menu.addItem((item) => {
item
.setTitle("IStart-Note-AI: Feynman test")
.setIcon("help-circle")
.onClick(() => { void this.runFeynmanTest(editor); });
});
}
if (isReadingProject) {
menu.addItem((item) => {
item
.setTitle("IStart-Note-AI: Resume reading project")
.setIcon("refresh-cw")
.onClick(() => { void this.resumeReadingProject(); });
});
}
// 智能补全(始终可用)
menu.addItem((item) => {
item
.setTitle("IStart-Note-AI: Complete current concept page")
.setIcon("brain")
.onClick(() => { void this.completeCurrentConcept(); });
.setTitle("IStart-Note-AI: Smart complete")
.setIcon("sparkles")
.onClick(() => { void this.runSmartComplete(editor); });
});
// 分析文档(始终可用)
menu.addItem((item) => {
item
.setTitle("IStart-Note-AI: Analyze and suggest")
.setIcon("search")
.onClick(() => {
const content = editor.getValue();
void this.runDocumentAnalysis(content, editor);
});
});
})
);
}
private openCommandPanel() {
const editor = this.app.workspace.activeEditor?.editor;
const activeFile = this.app.workspace.getActiveFile();
const selection = editor?.getSelection().trim() ?? "";
const hasSelection = selection.length > 0;
// 判断当前文件类型
const meta = activeFile ? this.app.metadataCache.getFileCache(activeFile) : null;
const fm = meta?.frontmatter;
const isConceptPage = fm?.type === "concept" || (activeFile?.path.includes("Concepts/") ?? false);
const isReadingNote = fm?.type === "reading-note";
// 判断光标是否在 section 内
let isInSection = false;
let sectionName: string | null = null;
if (editor) {
const cursor = editor.getCursor();
const content = editor.getValue();
const appender = new SectionAppender(this.app, this.settings);
sectionName = appender.getSectionAtCursor(content, cursor.line);
isInSection = sectionName !== null;
}
const groups = buildPanelGroups({
hasSelection,
selection,
isConceptPage,
isReadingNote,
isInSection,
sectionName,
activeFile,
onAsk: () => this.openQuestionModal(),
onContextQA: () => {
if (editor && hasSelection) {
this.openContextQAModal(selection, activeFile?.path ?? "");
}
},
onNewReading: () => this.openNewReadingProject(),
onSmartComplete: () => { if (editor) void this.runSmartComplete(editor); },
onDiagram: () => {
if (editor && hasSelection) {
const context = editor.getValue().slice(0, 800);
this.openDiagramGenerator(selection, context, editor);
}
},
onExpand: () => {
if (editor && hasSelection) {
const context = editor.getValue().slice(0, 1500);
void this.runExpand(selection, context, editor);
}
},
onContinue: () => {
if (editor) {
const cursor = editor.getCursor();
const before = editor.getRange({ line: 0, ch: 0 }, cursor);
void this.runContinue(before, editor);
}
},
onCompleteConcept: () => { void this.completeCurrentConcept(); },
onScanConcepts: () => { void this.scanAndBatchComplete(); },
onChapterSummary: () => { if (editor) void this.runChapterSummary(editor); },
onFeynmanTest: () => { if (editor) void this.runFeynmanTest(editor); },
onAnalyzeDoc: () => {
if (editor) {
const content = editor.getValue();
void this.runDocumentAnalysis(content, editor);
}
},
onSectionAppend: () => {
if (editor && activeFile && sectionName) {
const content = editor.getValue();
void this.runSectionAppend(activeFile, sectionName, content);
}
},
});
new CommandPanelModal(this.app, groups).open();
}
private openContextQAModal(selectedText: string, sourceNotePath: string) {
new ContextQAModal(this.app, selectedText, (question) => {
void this.processContextQA(question, selectedText, sourceNotePath);
@ -606,6 +887,300 @@ export default class DeepSeekPlugin extends Plugin {
}).open();
}
// ── 阅读项目 ─────────────────────────────────────────────
private openNewReadingProject() {
new NewReadingModal(this.app, (bookInfo, toc) => {
void this.createReadingProject(bookInfo, toc);
}).open();
}
private async createReadingProject(bookInfo: string, toc?: string) {
const notice = new Notice("⏳ 生成全书骨架...", 0);
try {
const planner = new ReadingPlanner(this.settings);
const plan = await planner.planSkeleton(bookInfo, toc);
notice.setMessage(`✍️ 创建项目结构(${plan.chapters.length} 章)...`);
const manager = new ReadingProjectManager(this.app, this.settings);
const indexFile = await manager.createProject(plan, (current, total, chapter) => {
notice.setMessage(`⏳ 生成预设问题 (${current}/${total})${chapter}`);
});
notice.hide();
new Notice(`✅ 阅读项目已创建:${plan.bookTitle}${plan.chapters.length} 章)`);
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(indexFile);
} catch (err) {
notice.hide();
new Notice(`❌ 创建失败:${(err as Error).message}`);
console.error("[IStart-Note-AI]", err);
}
}
private async resumeReadingProject() {
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile) { new Notice("请先打开阅读项目的索引页"); return; }
const meta = this.app.metadataCache.getFileCache(activeFile);
if (meta?.frontmatter?.type !== "reading-project") {
new Notice("当前文件不是阅读项目索引页");
return;
}
const notice = new Notice("⏳ 补全缺失章节...", 0);
try {
const manager = new ReadingProjectManager(this.app, this.settings);
const count = await manager.resumeProject(activeFile, (current, total, chapter) => {
notice.setMessage(`⏳ 补全 (${current}/${total})${chapter}`);
});
notice.hide();
if (count === 0) {
new Notice("✅ 所有章节已完整,无需补全");
} else {
new Notice(`✅ 已补全 ${count} 个章节的预设问题`);
}
} catch (err) {
notice.hide();
new Notice(`❌ 补全失败:${(err as Error).message}`);
}
}
private async runChapterSummary(editor: import("obsidian").Editor) {
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile) { new Notice("请先打开一个章节笔记"); return; }
const content = editor.getValue();
const meta = this.app.metadataCache.getFileCache(activeFile);
const fm = meta?.frontmatter;
if (fm?.type !== "reading-note") {
new Notice("当前文件不是阅读章节笔记");
return;
}
const book = (fm.book as string) || "未知";
const chapter = `${fm.chapter}章:${fm.title}`;
// 提取预设问题
const questionsMatch = content.match(/## 读前问题\n([\s\S]*?)(?=\n## )/);
const questions = questionsMatch
? questionsMatch[1].split("\n").filter((l) => l.trim().startsWith("- ")).map((l) => l.replace(/^- \[.\]\s*/, "").trim())
: [];
const notice = new Notice("⏳ 生成章节总结...", 0);
try {
const planner = new ReadingPlanner(this.settings);
const result = await planner.summarizeChapter(book, chapter, content, questions);
notice.hide();
const manager = new ReadingProjectManager(this.app, this.settings);
await manager.writeChapterSummary(activeFile, result);
new Notice("✅ 章节总结已生成");
// 刷新编辑器
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(activeFile);
} catch (err) {
notice.hide();
new Notice(`❌ 生成失败:${(err as Error).message}`);
}
}
private async runFeynmanTest(editor: import("obsidian").Editor) {
const activeFile = this.app.workspace.getActiveFile();
if (!activeFile) { new Notice("请先打开一个章节笔记"); return; }
const content = editor.getValue();
const meta = this.app.metadataCache.getFileCache(activeFile);
const fm = meta?.frontmatter;
if (fm?.type !== "reading-note") {
new Notice("当前文件不是阅读章节笔记");
return;
}
const book = (fm.book as string) || "未知";
const chapter = `${fm.chapter}章:${fm.title}`;
// 提取概念
const conceptsMatch = content.match(/## 关联概念\n([\s\S]*?)(?=\n## |$)/);
const concepts = conceptsMatch
? conceptsMatch[1].match(/\[\[(.+?)\]\]/g)?.map((m) => m.replace(/\[\[|\]\]/g, "")) ?? []
: [];
const notice = new Notice("⏳ 生成检验问题...", 0);
try {
const planner = new ReadingPlanner(this.settings);
const questions = await planner.feynmanTest(book, chapter, concepts, content);
notice.hide();
new FeynmanModal(this.app, chapter, questions).open();
} catch (err) {
notice.hide();
new Notice(`❌ 生成失败:${(err as Error).message}`);
}
}
// ── 智能补全 ─────────────────────────────────────────────
/**
*
* -
* - section section
* -
*/
private async runSmartComplete(editor: import("obsidian").Editor) {
const selection = editor.getSelection().trim();
if (selection) {
// 场景 A扩写选中内容
const context = editor.getValue().slice(0, 1500);
await this.runExpand(selection, context, editor);
return;
}
// 检查光标是否在空 section 内
const cursor = editor.getCursor();
const content = editor.getValue();
const lines = content.split("\n");
// 向上找最近的 ## 标题
let sectionName: string | null = null;
let sectionStartLine = -1;
for (let i = cursor.line; i >= 0; i--) {
const match = lines[i]?.match(/^##\s+(.+)/);
if (match) {
sectionName = match[1].trim();
sectionStartLine = i;
break;
}
}
if (sectionName && sectionStartLine >= 0) {
// 检查该 section 是否为空(标题到下一个 ## 之间没有非空行)
let sectionEmpty = true;
for (let i = sectionStartLine + 1; i < lines.length; i++) {
if (/^##\s/.test(lines[i])) break;
if (lines[i].trim().length > 0) { sectionEmpty = false; break; }
}
if (sectionEmpty) {
// 场景 B补全空 section
const activeFile = this.app.workspace.getActiveFile();
const title = activeFile?.basename ?? "未知";
await this.runSectionComplete(title, sectionName, content, sectionStartLine, editor);
return;
}
}
// 场景 C续写
const beforeCursor = editor.getRange({ line: 0, ch: 0 }, cursor);
await this.runContinue(beforeCursor, editor);
}
private async runExpand(selection: string, context: string, editor: import("obsidian").Editor) {
const notice = new Notice("⏳ 扩写中...", 0);
try {
const completer = new SmartCompleter(this.settings);
const result = await completer.expand(selection, context);
notice.hide();
new SmartPreviewModal(
this.app,
"扩写预览",
result.content,
() => {
// 替换选中内容
editor.replaceSelection(result.content);
new Notice("✅ 已扩写");
},
() => { void this.runExpand(selection, context, editor); }
).open();
} catch (err) {
notice.hide();
new Notice(`❌ 扩写失败:${(err as Error).message}`);
}
}
private async runContinue(beforeCursor: string, editor: import("obsidian").Editor) {
const notice = new Notice("⏳ 续写中...", 0);
try {
const completer = new SmartCompleter(this.settings);
const result = await completer.continueWriting(beforeCursor);
notice.hide();
new SmartPreviewModal(
this.app,
"续写预览",
result.content,
() => {
const cursor = editor.getCursor();
editor.replaceRange("\n" + result.content, cursor);
new Notice("✅ 已续写");
},
() => { void this.runContinue(beforeCursor, editor); }
).open();
} catch (err) {
notice.hide();
new Notice(`❌ 续写失败:${(err as Error).message}`);
}
}
private async runSectionComplete(
title: string,
sectionName: string,
fileContent: string,
sectionStartLine: number,
editor: import("obsidian").Editor
) {
const notice = new Notice(`⏳ 补全"${sectionName}"...`, 0);
try {
const completer = new SmartCompleter(this.settings);
const result = await completer.completeSection(title, sectionName, fileContent);
notice.hide();
new SmartPreviewModal(
this.app,
`补全"${sectionName}"`,
result.content,
() => {
// 插入到 section 标题下方
const insertLine = sectionStartLine + 1;
const insertPos = { line: insertLine, ch: 0 };
editor.replaceRange(result.content + "\n\n", insertPos);
new Notice(`✅ 已补全"${sectionName}"`);
},
() => { void this.runSectionComplete(title, sectionName, fileContent, sectionStartLine, editor); }
).open();
} catch (err) {
notice.hide();
new Notice(`❌ 补全失败:${(err as Error).message}`);
}
}
private async runDocumentAnalysis(content: string, editor: import("obsidian").Editor) {
const notice = new Notice("⏳ 分析文档中...", 0);
try {
const completer = new SmartCompleter(this.settings);
const suggestions = await completer.analyzeDocument(content);
notice.hide();
new DocumentAnalysisModal(this.app, suggestions, (selected) => {
// 将选中的建议追加到文档末尾
const parts = selected.map((s) => `## ${s.section}\n${s.content}`);
const insertText = "\n\n" + parts.join("\n\n") + "\n";
const lastLine = editor.lastLine();
editor.replaceRange(insertText, { line: lastLine, ch: editor.getLine(lastLine).length });
new Notice(`✅ 已插入 ${selected.length} 处补充内容`);
}).open();
} catch (err) {
notice.hide();
new Notice(`❌ 分析失败:${(err as Error).message}`);
}
}
// ── 图表/公式生成 ─────────────────────────────────────────
private openDiagramGenerator(selection: string, context: string, editor: import("obsidian").Editor) {

View file

@ -225,3 +225,109 @@
background: var(--background-primary);
color: var(--text-normal);
}
/* ── Smart Complete ──────────────────────────────── */
.istart-smart-suggestions {
max-height: 50vh;
overflow-y: auto;
margin-bottom: 12px;
}
.istart-smart-suggestion-row {
border: 1px solid var(--background-modifier-border);
border-radius: 6px;
padding: 10px;
margin-bottom: 8px;
}
.istart-smart-suggestion-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.istart-smart-suggestion-reason {
color: var(--text-muted);
font-size: 12px;
}
.istart-smart-suggestion-preview {
font-size: 13px;
padding: 8px;
background: var(--background-secondary);
border-radius: 4px;
max-height: 150px;
overflow-y: auto;
}
/* ── Command Panel ───────────────────────────────── */
.istart-command-panel {
min-width: 360px;
}
.istart-panel-group {
margin-bottom: 12px;
}
.istart-panel-group-title {
font-size: 11px;
font-weight: 600;
color: var(--text-faint);
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 4px 8px;
margin-bottom: 2px;
}
.istart-panel-action {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
transition: background 0.1s;
}
.istart-panel-action:hover {
background: var(--background-modifier-hover);
}
.istart-panel-action-icon {
font-size: 16px;
width: 24px;
text-align: center;
flex-shrink: 0;
}
.istart-panel-action-text {
flex: 1;
display: flex;
flex-direction: column;
gap: 1px;
}
.istart-panel-action-label {
font-size: 14px;
color: var(--text-normal);
}
.istart-panel-action-desc {
font-size: 11px;
color: var(--text-muted);
}
.istart-panel-action-key {
font-size: 11px;
color: var(--text-faint);
background: var(--background-modifier-border);
border-radius: 3px;
padding: 1px 5px;
font-family: monospace;
flex-shrink: 0;
}

View file

@ -11,5 +11,6 @@
"1.6.0": "1.4.0",
"1.6.1": "1.4.0",
"1.7.0": "1.4.0",
"1.7.1": "1.4.0"
"1.7.1": "1.4.0",
"1.7.2": "1.4.0"
}