release: 1.7.4

This commit is contained in:
yan 2026-05-08 12:55:00 +08:00
parent cc6f12e147
commit 79aa7df7a9
8 changed files with 830 additions and 1213 deletions

View file

@ -1,7 +1,7 @@
{
"id": "istart-note-ai",
"name": "IStart-Note-AI",
"version": "1.7.3",
"version": "1.7.4",
"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.3",
"version": "1.7.4",
"description": "IStart-Note-AI: DeepSeek-powered knowledge graph plugin for Obsidian",
"main": "main.js",
"scripts": {

215
src/actions/definitions.ts Normal file
View file

@ -0,0 +1,215 @@
import { Notice } from "obsidian";
import { ActionDef } from "./types";
/**
*
*
*/
export const ALL_ACTIONS: ActionDef[] = [
// ── 通用 ──────────────────────────────────────────────────
{
id: "ask-deepseek",
label: "提问",
icon: "message-circle",
description: "向 AI 提问并生成知识笔记",
group: "general",
when: { always: true },
showIn: ["panel", "editor-menu"],
run: (ctx) => ctx.plugin.openQuestionModal(),
},
{
id: "new-reading-project",
label: "新建阅读项目",
icon: "book-open",
description: "输入书名,生成阅读地图",
group: "general",
when: { always: true },
showIn: ["panel"],
run: (ctx) => ctx.plugin.openNewReadingProject(),
},
{
id: "baidu-sync",
label: "百度云同步",
icon: "cloud",
group: "general",
when: { always: true },
showIn: ["panel"],
run: (ctx) => ctx.plugin.openBaiduSyncModal(),
},
// ── 选中文字 ──────────────────────────────────────────────
{
id: "context-qa",
label: "基于选中内容提问",
icon: "help-circle",
group: "selection",
when: { hasSelection: true },
showIn: ["panel", "editor-menu"],
run: (ctx) => {
const path = ctx.activeFile?.path ?? "";
ctx.plugin.openContextQAModal(ctx.selection, path);
},
},
{
id: "generate-diagram",
label: "生成图表 / 公式",
icon: "bar-chart-2",
group: "selection",
when: { hasSelection: true },
showIn: ["panel", "editor-menu"],
run: (ctx) => {
if (!ctx.editor) return;
ctx.plugin.openDiagramGenerator(ctx.selection, ctx.fileContent.slice(0, 800), ctx.editor);
},
},
{
id: "expand-selection",
label: "扩写选中内容",
icon: "expand",
group: "selection",
when: { hasSelection: true },
showIn: ["panel", "editor-menu"],
run: (ctx) => {
if (!ctx.editor) return;
void ctx.plugin.runExpand(ctx.selection, ctx.fileContent.slice(0, 1500), ctx.editor);
},
},
// ── 编辑 ──────────────────────────────────────────────────
{
id: "smart-complete",
label: "智能补全",
icon: "sparkles",
description: "自动判断:补全/扩写/续写",
group: "edit",
when: { always: true },
showIn: ["panel", "editor-menu", "file-menu"],
run: (ctx) => {
if (ctx.editor) {
void ctx.plugin.runSmartComplete(ctx.editor);
} else if (ctx.targetFile) {
// file-menu: 先打开文件再执行
void (async () => {
const leaf = ctx.app.workspace.getLeaf(false);
await leaf.openFile(ctx.targetFile!);
setTimeout(() => {
const ed = ctx.app.workspace.activeEditor?.editor;
if (ed) void ctx.plugin.runSmartComplete(ed);
}, 200);
})();
}
},
},
{
id: "continue-writing",
label: "续写",
icon: "pencil",
description: "从光标位置继续写",
group: "edit",
when: { noSelection: true },
showIn: ["panel", "editor-menu"],
run: (ctx) => {
if (!ctx.editor) return;
const cursor = ctx.editor.getCursor();
const before = ctx.editor.getRange({ line: 0, ch: 0 }, cursor);
void ctx.plugin.runContinue(before, ctx.editor);
},
},
{
id: "append-section",
label: "补充当前章节",
icon: "plus-circle",
group: "edit",
when: { inSection: true },
showIn: ["panel", "editor-menu"],
run: (ctx) => {
if (!ctx.editor || !ctx.activeFile || !ctx.sectionName) return;
void ctx.plugin.runSectionAppend(ctx.activeFile, ctx.sectionName, ctx.fileContent);
},
},
// ── 概念页 ────────────────────────────────────────────────
{
id: "complete-concept",
label: "补全概念页",
icon: "brain",
group: "concept",
when: { fileType: ["concept"], filePath: "Concepts/" },
showIn: ["panel", "editor-menu", "file-menu"],
run: (ctx) => { void ctx.plugin.completeCurrentConcept(); },
},
{
id: "scan-empty-concepts",
label: "扫描空概念页",
icon: "search",
group: "concept",
when: { fileType: ["concept"], filePath: "Concepts/" },
showIn: ["panel"],
run: (ctx) => { void ctx.plugin.scanAndBatchComplete(); },
},
// ── 阅读 ──────────────────────────────────────────────────
{
id: "resume-reading",
label: "补全阅读项目",
icon: "refresh-cw",
description: "补全缺失章节的预设问题",
group: "reading",
when: { fileType: ["reading-project"] },
showIn: ["panel", "editor-menu", "file-menu"],
run: (ctx) => { void ctx.plugin.resumeReadingProject(); },
},
{
id: "chapter-summary",
label: "生成章节总结",
icon: "file-text",
group: "reading",
when: { fileType: ["reading-note"] },
showIn: ["panel", "editor-menu", "file-menu"],
run: (ctx) => {
const editor = ctx.editor ?? ctx.app.workspace.activeEditor?.editor;
if (editor) void ctx.plugin.runChapterSummary(editor);
},
},
{
id: "feynman-test",
label: "费曼检验",
icon: "help-circle",
description: "检验理解程度",
group: "reading",
when: { fileType: ["reading-note"] },
showIn: ["panel", "editor-menu", "file-menu"],
run: (ctx) => {
const editor = ctx.editor ?? ctx.app.workspace.activeEditor?.editor;
if (editor) void ctx.plugin.runFeynmanTest(editor);
},
},
// ── 文档工具 ──────────────────────────────────────────────
{
id: "analyze-document",
label: "分析文档缺失",
icon: "search",
description: "AI 分析并建议补充内容",
group: "document",
when: { always: true },
showIn: ["panel", "editor-menu"],
run: (ctx) => {
if (!ctx.editor) return;
void ctx.plugin.runDocumentAnalysis(ctx.fileContent, ctx.editor);
},
},
{
id: "smart-diagram",
label: "智能生成图表",
icon: "bar-chart-2",
description: "AI 自动判断最合适的图表类型",
group: "document",
when: { hasSelection: true },
showIn: ["panel"],
run: (ctx) => {
if (!ctx.editor) return;
void ctx.plugin.runDiagramGeneration(ctx.selection, "auto", ctx.fileContent.slice(0, 800), ctx.editor);
},
},
];

195
src/actions/registry.ts Normal file
View file

@ -0,0 +1,195 @@
import { Notice, TFile } from "obsidian";
import type DeepSeekPlugin from "../main";
import { ActionDef, ActionContext, ActionEntry, ActionGroup, GROUP_TITLES, GROUP_ORDER } from "./types";
import { CommandPanelModal } from "../features/command-panel/CommandPanelModal";
import type { PanelGroup, PanelAction } from "../features/command-panel/CommandPanelModal";
import { SectionAppender } from "../ai/SectionAppender";
/**
* actions
*/
export function registerAllActions(plugin: DeepSeekPlugin, actions: ActionDef[]) {
// 1. 为每个 action 注册命令
// 需要编辑器的 action 用 editorCallback移动端工具栏可用
for (const action of actions) {
const needsEditor = action.showIn.includes("editor-menu") || action.when.hasSelection || action.when.inSection || action.when.noSelection;
if (needsEditor) {
plugin.addCommand({
id: action.id,
name: action.label,
editorCallback: (editor) => {
const ctx = buildContext(plugin, null);
ctx.editor = editor;
ctx.selection = editor.getSelection().trim();
ctx.fileContent = editor.getValue();
const cursor = editor.getCursor();
const appender = new SectionAppender(plugin.app, plugin.settings);
ctx.sectionName = appender.getSectionAtCursor(ctx.fileContent, cursor.line);
if (evaluateWhen(action.when, ctx)) {
action.run(ctx);
} else {
new Notice(`当前上下文不支持此操作`);
}
},
});
} else {
plugin.addCommand({
id: action.id,
name: action.label,
callback: () => {
const ctx = buildContext(plugin, null);
if (evaluateWhen(action.when, ctx)) {
action.run(ctx);
} else {
new Notice(`当前上下文不支持此操作`);
}
},
});
}
}
// 2. 注册 editor-menu
plugin.registerEvent(
plugin.app.workspace.on("editor-menu", (menu, editor) => {
const ctx = buildContext(plugin, null);
ctx.editor = editor;
ctx.selection = editor.getSelection().trim();
ctx.fileContent = editor.getValue();
// 重新计算 sectionName
const cursor = editor.getCursor();
const appender = new SectionAppender(plugin.app, plugin.settings);
ctx.sectionName = appender.getSectionAtCursor(ctx.fileContent, cursor.line);
const visible = actions.filter(
(a) => a.showIn.includes("editor-menu") && evaluateWhen(a.when, ctx)
);
for (const action of visible) {
menu.addItem((item) => {
item
.setTitle(`IStart-Note-AI: ${action.label}`)
.setIcon(action.icon)
.onClick(() => action.run(ctx));
});
}
})
);
// 3. 注册 file-menu
plugin.registerEvent(
plugin.app.workspace.on("file-menu", (menu, file) => {
if (!(file instanceof TFile) || file.extension !== "md") return;
const fileMeta = plugin.app.metadataCache.getFileCache(file);
const fileType = fileMeta?.frontmatter?.type as string | undefined;
const ctx = buildContext(plugin, file);
ctx.fileType = fileType;
ctx.filePath = file.path;
ctx.targetFile = file;
const visible = actions.filter(
(a) => a.showIn.includes("file-menu") && evaluateWhen(a.when, ctx)
);
for (const action of visible) {
menu.addItem((item) => {
item
.setTitle(`IStart-Note-AI: ${action.label}`)
.setIcon(action.icon)
.onClick(() => action.run(ctx));
});
}
})
);
// 4. 注册面板打开命令
plugin.addCommand({
id: "open-panel",
name: "Open command panel",
callback: () => openPanel(plugin, actions),
});
// 5. Ribbon icon 打开面板
plugin.addRibbonIcon("brain", "IStart-Note-AI", () => {
openPanel(plugin, actions);
});
}
/** 打开统一面板 */
function openPanel(plugin: DeepSeekPlugin, actions: ActionDef[]) {
const editor = plugin.app.workspace.activeEditor?.editor ?? null;
const ctx = buildContext(plugin, null);
if (editor) {
ctx.editor = editor;
ctx.selection = editor.getSelection().trim();
ctx.fileContent = editor.getValue();
const cursor = editor.getCursor();
const appender = new SectionAppender(plugin.app, plugin.settings);
ctx.sectionName = appender.getSectionAtCursor(ctx.fileContent, cursor.line);
}
// 按 group 分组,过滤可见 actions
const groups: PanelGroup[] = [];
for (const groupId of GROUP_ORDER) {
const groupActions = actions.filter(
(a) => a.group === groupId && a.showIn.includes("panel") && evaluateWhen(a.when, ctx)
);
if (groupActions.length === 0) continue;
const panelActions: PanelAction[] = groupActions.map((a) => ({
id: a.id,
icon: a.icon,
label: a.label,
description: a.description,
callback: () => a.run(ctx),
}));
groups.push({ title: GROUP_TITLES[groupId], actions: panelActions });
}
new CommandPanelModal(plugin.app, groups).open();
}
/** 构建当前上下文 */
function buildContext(plugin: DeepSeekPlugin, targetFile: TFile | null): ActionContext {
const activeFile = plugin.app.workspace.getActiveFile();
const file = targetFile ?? activeFile;
const fileMeta = file ? plugin.app.metadataCache.getFileCache(file) : null;
return {
plugin,
app: plugin.app,
editor: null,
activeFile,
selection: "",
fileContent: "",
fileType: fileMeta?.frontmatter?.type as string | undefined,
filePath: file?.path ?? "",
sectionName: null,
targetFile,
};
}
/** 评估可见性条件 */
function evaluateWhen(when: ActionDef["when"], ctx: ActionContext): boolean {
if (when.always) return true;
if (when.hasSelection && !ctx.selection) return false;
if (when.noSelection && ctx.selection) return false;
if (when.fileType) {
const match = when.fileType.some((t) => ctx.fileType === t) ||
(when.filePath && ctx.filePath.includes(when.filePath));
if (!match) return false;
} else if (when.filePath) {
if (!ctx.filePath.includes(when.filePath)) return false;
}
if (when.inSection && !ctx.sectionName) return false;
return true;
}

61
src/actions/types.ts Normal file
View file

@ -0,0 +1,61 @@
import type { App, Editor, TFile } from "obsidian";
import type DeepSeekPlugin from "../main";
/** 动作执行时的上下文 */
export interface ActionContext {
plugin: DeepSeekPlugin;
app: App;
editor: Editor | null;
activeFile: TFile | null;
selection: string; // 选中文字trim 后)
fileContent: string; // 当前文件全文
fileType: string | undefined; // frontmatter.type
filePath: string; // 当前文件路径
sectionName: string | null; // 光标所在 section 名
// file-menu 专用:右键的目标文件(可能不是当前打开的文件)
targetFile: TFile | null;
}
/** 可见性条件 */
export interface ActionWhen {
always?: boolean; // 始终可见
hasSelection?: boolean; // 需要有选中文字
noSelection?: boolean; // 需要没有选中文字
fileType?: string[]; // frontmatter type 匹配其一
filePath?: string; // 文件路径包含此字符串
inSection?: boolean; // 光标在某个 ## section 内
}
/** 动作出现的入口 */
export type ActionEntry = "panel" | "editor-menu" | "file-menu";
/** 面板分组 */
export type ActionGroup = "general" | "selection" | "edit" | "concept" | "reading" | "sync" | "document";
/** 动作定义 */
export interface ActionDef {
id: string;
label: string;
icon: string;
description?: string;
group: ActionGroup;
when: ActionWhen;
showIn: ActionEntry[];
run: (ctx: ActionContext) => void;
}
/** 分组标题映射 */
export const GROUP_TITLES: Record<ActionGroup, string> = {
general: "通用",
selection: "选中文字",
edit: "编辑",
concept: "概念页",
reading: "阅读",
sync: "同步",
document: "文档工具",
};
/** 分组排序 */
export const GROUP_ORDER: ActionGroup[] = [
"general", "selection", "edit", "concept", "reading", "sync", "document",
];

View file

@ -64,6 +64,7 @@ export class BaiduSyncView extends ItemView {
this.makeBtn(btnRow, "⬆ 强制备份", "default", () => { void this.forceBackup(); });
this.makeBtn(btnRow, "⬇ 强制更新", "default", () => { void this.forceUpdate(); });
this.makeBtn(btnRow, "⇄ 双向同步", "cta", () => { void this.runSync(); });
this.makeBtn(btnRow, "⚠️ 强制覆盖", "default", () => { void this.forceOverwrite(); });
// 上次扫描时间
if (this.lastScanTime) {
@ -251,6 +252,54 @@ export class BaiduSyncView extends ItemView {
await this.scan();
}
async forceOverwrite() {
const cfg = this.plugin.settings.baiduSync;
const service = new BaiduSyncService(this.app, cfg);
const tokenOk = await service.ensureValidToken();
if (!tokenOk) { new Notice("Token 已过期,请重新授权"); return; }
// 二次确认
const confirmed = await new Promise<boolean>((resolve) => {
const modal = new (class extends (require("obsidian") as { Modal: new (app: import("obsidian").App) => import("obsidian").Modal }).Modal {
onOpen() {
this.titleEl.setText("⚠️ 确认强制覆盖");
this.contentEl.createEl("p", {
text: "此操作将删除本地所有同步文件,然后从云端完整恢复。不可撤销!",
cls: "istart-sync-modal-warning",
});
const { Setting } = require("obsidian") as typeof import("obsidian");
new Setting(this.contentEl)
.addButton((btn: import("obsidian").ButtonComponent) => btn.setButtonText("确认覆盖").setWarning().onClick(() => { this.close(); resolve(true); }))
.addButton((btn: import("obsidian").ButtonComponent) => btn.setButtonText("取消").onClick(() => { this.close(); resolve(false); }));
}
onClose() { this.contentEl.empty(); resolve(false); }
})(this.app);
modal.open();
});
if (!confirmed) return;
const notice = new Notice("⏳ 强制覆盖:清理本地...", 0);
// 删除本地文件
const files = this.app.vault.getFiles().filter(
(f) => !f.path.split("/").some((p) => p.startsWith("."))
);
for (const f of files) {
try { await this.app.vault.delete(f); } catch { /* ignore */ }
}
// 从云端恢复
notice.setMessage("⏳ 强制覆盖:从云端恢复...");
const result = await service.restore("", true, (c, t, file) => {
notice.setMessage(`⏳ 恢复中 (${c}/${t})${file.split("/").pop()}`);
});
notice.hide();
new Notice(`✅ 强制覆盖完成:恢复 ${result.downloaded} 个文件`);
await this.scan();
}
private async uploadOne(path: string) {
const cfg = this.plugin.settings.baiduSync;
const abstract = this.app.vault.getAbstractFileByPath(path);

File diff suppressed because it is too large Load diff

View file

@ -13,5 +13,6 @@
"1.7.0": "1.4.0",
"1.7.1": "1.4.0",
"1.7.2": "1.4.0",
"1.7.3": "1.4.0"
"1.7.3": "1.4.0",
"1.7.4": "1.4.0"
}