release: 1.5.6

This commit is contained in:
yan 2026-05-01 21:18:21 +08:00
parent 1be28a335a
commit 967f558908
9 changed files with 147 additions and 152 deletions

View file

@ -1,7 +1,7 @@
{
"id": "istart-note-ai",
"name": "IStart-Note-AI",
"version": "1.5.5",
"version": "1.5.6",
"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",

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "istart-note-ai",
"version": "1.5.5",
"version": "1.5.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "istart-note-ai",
"version": "1.5.5",
"version": "1.5.6",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",

View file

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

View file

@ -116,8 +116,10 @@ update_versions() {
# Build
# ---------------------------------------------------------------------------
build_plugin() {
info "Installing dependencies..."
(cd "$PROJECT_DIR" && npm install --silent)
if [[ ! -d "$PROJECT_DIR/node_modules" ]]; then
info "Installing dependencies..."
(cd "$PROJECT_DIR" && npm install)
fi
info "Building plugin (production)..."
(cd "$PROJECT_DIR" && npm run build)

View file

@ -65,7 +65,7 @@ export class PreviewModal extends Modal {
const previewEl = contentEl.createDiv({ attr: { style: "max-height: 60vh; overflow-y: auto; border: 1px solid var(--background-modifier-border); border-radius: 4px; padding: 12px; margin-bottom: 16px;" } });
MarkdownRenderer.render(this.app, this.previewMd, previewEl, "", this.component);
void MarkdownRenderer.render(this.app, this.previewMd, previewEl, "", this.component);
new Setting(contentEl)
.addButton((btn) =>

View file

@ -93,7 +93,8 @@ export class QuestionGraphManager {
await this.app.vault.create(indexPath, body);
}
const indexFile = this.app.vault.getAbstractFileByPath(indexPath) as TFile;
const indexFile = this.app.vault.getAbstractFileByPath(indexPath);
if (!indexFile || !(indexFile instanceof TFile)) return;
let content = await this.app.vault.read(indexFile);
// 避免重复插入

View file

@ -137,9 +137,9 @@ export class DeepSeekSettingsTab extends PluginSettingTab {
text
.setPlaceholder("your-app-secret")
.setValue(this.plugin.settings.baiduSync.appSecret)
.onChange(async (v) => {
.onChange((v) => {
this.plugin.settings.baiduSync.appSecret = v.trim();
await this.plugin.saveSettings();
void this.plugin.saveSettings();
});
text.inputEl.type = "password";
return text;

View file

@ -26,41 +26,34 @@ export default class DeepSeekPlugin extends Plugin {
async onload() {
await this.loadSettings();
// 侧边栏图标:提问
this.addRibbonIcon("brain", "DeepSeek ask", () => {
this.openQuestionModal();
});
// 侧边栏图标:百度云同步
this.addRibbonIcon("cloud", "Baidu cloud sync status", () => {
void this.activateSyncView();
});
// 注册同步状态视图
this.registerView(SYNC_VIEW_TYPE, (leaf) => new BaiduSyncView(leaf, this));
// 命令:提问(不设默认快捷键)
this.addCommand({
id: "ask-deepseek",
name: "Ask DeepSeek and generate a knowledge note",
callback: () => this.openQuestionModal(),
});
// 命令:补全当前概念页
this.addCommand({
id: "complete-current-concept",
name: "Complete current concept page",
callback: () => { void this.completeCurrentConcept(); },
});
// 命令:扫描空概念页
this.addCommand({
id: "scan-empty-concepts",
name: "Scan empty concept pages",
callback: () => { void this.scanAndBatchComplete(); },
});
// 命令:框选提问(不设默认快捷键)
this.addCommand({
id: "context-qa",
name: "Ask based on selection",
@ -75,7 +68,6 @@ export default class DeepSeekPlugin extends Plugin {
},
});
// 命令:补充当前章节
this.addCommand({
id: "append-current-section",
name: "Append content to current section",
@ -94,36 +86,32 @@ export default class DeepSeekPlugin extends Plugin {
},
});
// 命令:问题图谱索引
this.addCommand({
id: "open-question-index",
name: "Open question index",
callback: () => { void this.openQuestionIndex(); },
});
// 命令:百度云同步
this.addCommand({
id: "baidu-sync",
name: "Baidu Netdisk sync / backup",
callback: () => this.openBaiduSyncModal(),
});
// 命令:打开同步状态面板
this.addCommand({
id: "baidu-sync-view",
name: "Open Baidu cloud sync status panel",
callback: () => { void this.activateSyncView(); },
});
// 命令:百度云授权
this.addCommand({
id: "baidu-auth",
name: "Baidu Netdisk re-authorize",
callback: () => this.openBaiduAuthModal(),
});
// 设置页
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;
@ -132,30 +120,30 @@ export default class DeepSeekPlugin extends Plugin {
item
.setTitle("IStart-Note-AI: Complete this concept page")
.setIcon("brain")
.onClick(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();
.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();
})();
});
});
})
);
// 编辑器内右键菜单
this.registerEvent(
this.app.workspace.on("editor-menu", (menu, editor) => {
const selection = editor.getSelection().trim();
// 框选提问入口(有选中内容时显示)
if (selection) {
menu.addItem((item) => {
item
@ -168,7 +156,6 @@ export default class DeepSeekPlugin extends Plugin {
});
}
// 章节补充入口(光标在 ## 章节内时显示)
const cursor = editor.getCursor();
const fullContent = editor.getValue();
const appender = new SectionAppender(this.app, this.settings);
@ -186,7 +173,6 @@ export default class DeepSeekPlugin extends Plugin {
});
}
// 概念补全入口
const linkMatch = selection.match(/^\[\[(.+?)(?:\|.+?)?\]\]$/) ||
selection.match(/^(.+)$/);
const conceptName = linkMatch?.[1];
@ -196,30 +182,33 @@ export default class DeepSeekPlugin extends Plugin {
item
.setTitle(`IStart-Note-AI: Complete concept "${conceptName}"`)
.setIcon("brain")
.onClick(async () => {
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);
.onClick(() => {
void (async () => {
const manager = new ConceptPageManager(this.app, this.settings);
const conceptsPath = this.settings.conceptsPath || "Knowledge/Concepts";
const filePath = `${conceptsPath}/${conceptName}.md`;
let conceptFile = this.app.vault.getAbstractFileByPath(filePath);
if (!file || !(file instanceof TFile)) {
const writer = new VaultWriter(this.app, this.settings);
await writer.ensureConceptNote(conceptName);
file = this.app.vault.getAbstractFileByPath(filePath);
}
if (!conceptFile || !(conceptFile instanceof TFile)) {
const writer = new VaultWriter(this.app, this.settings);
await writer.ensureConceptNote(conceptName);
conceptFile = this.app.vault.getAbstractFileByPath(filePath);
}
if (!file || !(file instanceof TFile)) {
new Notice(`无法找到或创建概念页:${conceptName}`);
return;
}
if (!conceptFile || !(conceptFile instanceof TFile)) {
new Notice(`无法找到或创建概念页:${conceptName}`);
return;
}
const info = await manager.analyzeFile(file);
new DepthSelectModal(this.app, conceptName, (depth) => {
void this.runConceptCompletion(file as TFile, conceptName, depth, {
sourceQuestion: info?.sourceQuestion,
sourceAnswer: info?.sourceAnswer,
});
}).open();
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();
})();
});
});
@ -246,7 +235,6 @@ export default class DeepSeekPlugin extends Plugin {
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);
@ -263,38 +251,38 @@ export default class DeepSeekPlugin extends Plugin {
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
);
new QuestionClassifyModal(this.app, question, classification, (confirmed) => {
void (async () => {
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);
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}`);
writeNotice.hide();
new Notice(`✅ 上下文笔记已生成:${file.name}`);
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(file);
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(file);
// 自动备份
void this.triggerAutoBackup(file.path);
} catch (err) {
writeNotice.hide();
new Notice(`❌ 写入失败:${(err as Error).message}`);
console.error("[IStart-Note-AI]", err);
}
void this.triggerAutoBackup(file.path);
} catch (err) {
writeNotice.hide();
new Notice(`❌ 写入失败:${(err as Error).message}`);
console.error("[IStart-Note-AI]", err);
}
})();
}).open();
} catch (err) {
notice.hide();
@ -328,35 +316,35 @@ export default class DeepSeekPlugin extends Plugin {
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.writeQANote(question, response);
new QuestionClassifyModal(this.app, question, classification, (confirmed) => {
void (async () => {
const writeNotice = new Notice("✍️ 写入笔记...", 0);
try {
const writer = new VaultWriter(this.app, this.settings);
const file = await writer.writeQANote(question, response);
await graphManager.attachClassification(file, question, confirmed, response.concepts);
await graphManager.appendRecommendations(file, confirmed);
await graphManager.updateQuestionIndex(question, confirmed, file.path);
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}`);
writeNotice.hide();
new Notice(`✅ 笔记已生成:${file.name}`);
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(file);
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(file);
if (this.settings.autoOpenGraph) {
// commands API is not in the public type definitions
const appWithCommands = this.app as unknown as { commands: { executeCommandById: (id: string) => void } };
appWithCommands.commands.executeCommandById("graph:open");
if (this.settings.autoOpenGraph) {
const appWithCommands = this.app as unknown as { commands: { executeCommandById: (id: string) => void } };
appWithCommands.commands.executeCommandById("graph:open");
}
void this.triggerAutoBackup(file.path);
} catch (err) {
writeNotice.hide();
new Notice(`❌ 写入失败:${(err as Error).message}`);
console.error("[DeepSeek Plugin]", err);
}
// 自动备份
void this.triggerAutoBackup(file.path);
} catch (err) {
writeNotice.hide();
new Notice(`❌ 写入失败:${(err as Error).message}`);
console.error("[DeepSeek Plugin]", err);
}
})();
}).open();
} catch (err) {
notice.hide();
@ -370,7 +358,7 @@ export default class DeepSeekPlugin extends Plugin {
const indexPath = normalizePath(`${indexFolder}/问题索引.md`);
let file = this.app.vault.getAbstractFileByPath(indexPath);
if (!file || !(file instanceof TFile)) {
await this.app.vault.createFolder(indexFolder).catch(() => {});
try { await this.app.vault.createFolder(indexFolder); } catch { /* exists */ }
file = await this.app.vault.create(indexPath, "# 问题索引\n\n## 核心问题\n\n## 深化问题\n\n## 扩展问题\n");
}
if (file instanceof TFile) {
@ -416,11 +404,13 @@ export default class DeepSeekPlugin extends Plugin {
this.app,
conceptName,
previewMd,
async () => {
await manager.writeCompletion(file, result, depth);
new Notice(`✅ 概念页已补全:${conceptName}`);
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(file);
() => {
void (async () => {
await manager.writeCompletion(file, result, depth);
new Notice(`✅ 概念页已补全:${conceptName}`);
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(file);
})();
},
() => {
void this.runConceptCompletion(file, conceptName, depth, context);
@ -433,7 +423,7 @@ export default class DeepSeekPlugin extends Plugin {
}
}
private async runSectionAppend(file: TFile, sectionName: string, content: string) {
private 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
@ -441,7 +431,6 @@ export default class DeepSeekPlugin extends Plugin {
.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;
@ -467,9 +456,11 @@ export default class DeepSeekPlugin extends Plugin {
this.app,
sectionName,
result.items,
async () => {
await appender.appendToSection(file, sectionName, result.items);
new Notice(`✅ 已追加 ${result.items.length} 条内容到"${sectionName}"`);
() => {
void (async () => {
await appender.appendToSection(file, sectionName, result.items);
new Notice(`✅ 已追加 ${result.items.length} 条内容到"${sectionName}"`);
})();
},
() => {
void this.generateAndPreviewSection(file, conceptName, sectionName, existingContent, count);
@ -489,29 +480,31 @@ export default class DeepSeekPlugin extends Plugin {
const items = empties.map((e) => ({ name: e.conceptName, path: e.file.path }));
new BatchScanModal(this.app, items, async (selectedPaths, depth) => {
let done = 0;
for (const path of selectedPaths) {
const abstract = this.app.vault.getAbstractFileByPath(path);
if (!abstract || !(abstract instanceof TFile)) continue;
const info = await manager.analyzeFile(abstract);
if (!info) continue;
new BatchScanModal(this.app, items, (selectedPaths, depth) => {
void (async () => {
let done = 0;
for (const path of selectedPaths) {
const abstract = this.app.vault.getAbstractFileByPath(path);
if (!abstract || !(abstract instanceof TFile)) continue;
const info = await manager.analyzeFile(abstract);
if (!info) continue;
const n = new Notice(`⏳ 补全中 (${++done}/${selectedPaths.length})${info.conceptName}`, 0);
try {
const completer = new ConceptCompleter(this.settings);
const result = await completer.complete(info.conceptName, depth, {
sourceQuestion: info.sourceQuestion,
sourceAnswer: info.sourceAnswer,
});
await manager.writeCompletion(abstract, result, depth);
n.hide();
} catch (err) {
n.hide();
new Notice(`${info.conceptName} 补全失败:${(err as Error).message}`);
const n = new Notice(`⏳ 补全中 (${++done}/${selectedPaths.length})${info.conceptName}`, 0);
try {
const completer = new ConceptCompleter(this.settings);
const result = await completer.complete(info.conceptName, depth, {
sourceQuestion: info.sourceQuestion,
sourceAnswer: info.sourceAnswer,
});
await manager.writeCompletion(abstract, result, depth);
n.hide();
} catch (err) {
n.hide();
new Notice(`${info.conceptName} 补全失败:${(err as Error).message}`);
}
}
}
new Notice(`✅ 批量补全完成,共 ${done} 个概念页`);
new Notice(`✅ 批量补全完成,共 ${done} 个概念页`);
})();
}).open();
}
@ -542,15 +535,16 @@ export default class DeepSeekPlugin extends Plugin {
new Notice("请先在设置中启用百度云同步");
return;
}
new BaiduAuthModal(this.app, this.settings.baiduSync, async (accessToken, refreshToken, expiresAt) => {
this.settings.baiduSync.accessToken = accessToken;
this.settings.baiduSync.refreshToken = refreshToken;
this.settings.baiduSync.tokenExpiresAt = expiresAt;
await this.saveSettings();
new BaiduAuthModal(this.app, this.settings.baiduSync, (accessToken, refreshToken, expiresAt) => {
void (async () => {
this.settings.baiduSync.accessToken = accessToken;
this.settings.baiduSync.refreshToken = refreshToken;
this.settings.baiduSync.tokenExpiresAt = expiresAt;
await this.saveSettings();
})();
}).open();
}
/** 自动备份触发(生成笔记后调用) */
private async triggerAutoBackup(filePath: string) {
const cfg = this.settings.baiduSync;
if (!cfg.enabled || !cfg.autoBackup || !cfg.accessToken) return;
@ -563,7 +557,7 @@ export default class DeepSeekPlugin extends Plugin {
const folder = filePath.includes("/") ? filePath.substring(0, filePath.lastIndexOf("/")) : "";
await service.backup(folder);
} catch {
// 自动备份失败静默处理,不打扰用户
// 自动备份失败静默处理
}
}
@ -571,7 +565,6 @@ export default class DeepSeekPlugin extends Plugin {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
this.settings.baiduSync = Object.assign({}, DEFAULT_BAIDU_SYNC_CONFIG, this.settings.baiduSync);
// 启动时静默拉取远端配置(如果已授权)
if (this.settings.baiduSync.enabled && this.settings.baiduSync.accessToken) {
void this.pullConfig(true);
}
@ -581,7 +574,6 @@ export default class DeepSeekPlugin extends Plugin {
await this.saveData(this.settings);
}
/** 推送当前配置(不含凭证)到百度云 */
async pushConfig() {
const cfg = this.settings.baiduSync;
if (!cfg.enabled || !cfg.accessToken) {
@ -595,7 +587,6 @@ export default class DeepSeekPlugin extends Plugin {
new Notice(ok ? "✅ 配置已推送到百度云" : "❌ 配置推送失败");
}
/** 从百度云拉取配置并应用 */
async pullConfig(silent = false) {
const cfg = this.settings.baiduSync;
if (!cfg.enabled || !cfg.accessToken) return;

View file

@ -5,5 +5,6 @@
"1.5.2": "1.4.0",
"1.5.3": "1.4.0",
"1.5.4": "1.4.0",
"1.5.5": "1.4.0"
"1.5.5": "1.4.0",
"1.5.6": "1.4.0"
}