diff --git a/src/actions/definitions.ts b/src/actions/definitions.ts index 800050e..80d900b 100644 --- a/src/actions/definitions.ts +++ b/src/actions/definitions.ts @@ -5,6 +5,8 @@ import { ActionDef } from "./types"; * * The "AI 助手" is placed in Auxiliary as a cross-cutting entry point. * The command panel renders it as a pinned top-level button above the grouped actions. + * + * Icons use Lucide names (https://lucide.dev) which Obsidian supports natively. */ export const ALL_ACTIONS: ActionDef[] = [ // ══════════════════════════════════════════════════════════════ @@ -14,7 +16,7 @@ export const ALL_ACTIONS: ActionDef[] = [ { id: "vault-qa", label: "知识库问答", - icon: "library", + icon: "book-open-check", description: "基于 Vault 检索回答,附带来源引用", domain: "knowledge", section: "retrieval", @@ -26,7 +28,7 @@ export const ALL_ACTIONS: ActionDef[] = [ { id: "question-with-graph", label: "知识提问", - icon: "help-circle", + icon: "message-circle-question", description: "提问 → 自动分类 → 生成 Q&A → 更新问题图谱", domain: "knowledge", section: "question", @@ -37,7 +39,7 @@ export const ALL_ACTIONS: ActionDef[] = [ { id: "complete-current-concept", label: "补全当前概念页", - icon: "book-plus", + icon: "puzzle", description: "为当前打开的空概念页生成定义、解释、示例、关联", domain: "knowledge", section: "concept", @@ -48,8 +50,8 @@ export const ALL_ACTIONS: ActionDef[] = [ }, { id: "scan-empty-concepts", - label: "扫描并补全空概念页", - icon: "search", + label: "扫描空概念页", + icon: "scan-search", description: "扫描 Vault 中所有空概念页,批量补全", domain: "knowledge", section: "concept", @@ -61,7 +63,7 @@ export const ALL_ACTIONS: ActionDef[] = [ { id: "new-reading-project", label: "新建阅读项目", - icon: "book-open", + icon: "book-marked", description: "输入书名,生成阅读地图", domain: "knowledge", section: "reading", @@ -72,8 +74,8 @@ export const ALL_ACTIONS: ActionDef[] = [ { id: "knowledge-debt", label: "知识债务看板", - icon: "bar-chart-2", - description: "空概念、孤立问题、未完成阅读、长期未更新草稿", + icon: "activity", + description: "空概念、孤立问题、未完成阅读、长期草稿", domain: "knowledge", section: "debt", when: { always: true }, @@ -86,8 +88,52 @@ export const ALL_ACTIONS: ActionDef[] = [ // EXECUTION // ══════════════════════════════════════════════════════════════ - // Placeholder: future actions like "生成执行计划", "查看执行日志", "定时任务" - // will be added here once runtime is implemented. + { + id: "generate-plan", + label: "从当前笔记生成执行计划", + icon: "list-todo", + description: "AI 分析笔记内容,提取可执行行动项", + domain: "execution", + section: "plan", + when: { always: true }, + showIn: ["panel", "editor-menu"], + experimental: true, + run: (ctx) => { ctx.plugin.openGeneratePlan(); }, + }, + { + id: "view-pending-plans", + label: "查看待确认计划", + icon: "clipboard-list", + description: "打开 Knowledge/_ExecutionPlans 中最新的计划草稿", + domain: "execution", + section: "plan", + when: { always: true }, + showIn: ["panel"], + run: (ctx) => { ctx.plugin.openPendingPlans(); }, + }, + { + id: "view-execution-logs", + label: "查看执行日志", + icon: "scroll-text", + description: "打开 Knowledge/_Executions 中最新的执行记录", + domain: "execution", + section: "logs", + when: { always: true }, + showIn: ["panel"], + run: (ctx) => { ctx.plugin.openExecutionLogs(); }, + }, + { + id: "scheduled-tasks", + label: "定时任务", + icon: "timer", + description: "查看和管理定时任务(v2.1 启用运行时)", + domain: "execution", + section: "scheduler", + when: { always: true }, + showIn: ["panel"], + experimental: true, + run: (ctx) => { ctx.plugin.openScheduledTasks(); }, + }, // ══════════════════════════════════════════════════════════════ // AUXILIARY @@ -97,7 +143,7 @@ export const ALL_ACTIONS: ActionDef[] = [ id: "ai-assistant", label: "AI 助手", icon: "sparkles", - description: "选中文字或输入指令,AI 智能执行(跨模块入口)", + description: "选中文字或输入指令,AI 智能执行", domain: "auxiliary", section: "assistant", when: { always: true }, @@ -107,7 +153,7 @@ export const ALL_ACTIONS: ActionDef[] = [ { id: "beautify-note", label: "美化当前文档", - icon: "wand", + icon: "paintbrush", description: "整理结构、插入 Callout、生成双链", domain: "auxiliary", section: "document", @@ -118,7 +164,7 @@ export const ALL_ACTIONS: ActionDef[] = [ { id: "baidu-sync", label: "百度云同步", - icon: "cloud", + icon: "cloud-upload", description: "备份 / 恢复 / 同步", domain: "auxiliary", section: "sync", diff --git a/src/actions/registry.ts b/src/actions/registry.ts index d4dce4c..4613c27 100644 --- a/src/actions/registry.ts +++ b/src/actions/registry.ts @@ -104,7 +104,7 @@ function openPanel(plugin: DeepSeekPlugin, actions: ActionDef[]) { // Add pinned as first "group" with a special title if (pinnedAction && evaluateWhen(pinnedAction.when, ctx)) { groups.push({ - title: "⭐ 入口", + title: "入口", actions: [{ id: pinnedAction.id, icon: pinnedAction.icon, diff --git a/src/features/command-panel/CommandPanelModal.ts b/src/features/command-panel/CommandPanelModal.ts index 8db79f0..bc06710 100644 --- a/src/features/command-panel/CommandPanelModal.ts +++ b/src/features/command-panel/CommandPanelModal.ts @@ -1,4 +1,4 @@ -import { App, Modal, TFile } from "obsidian"; +import { App, Modal, setIcon, TFile } from "obsidian"; export interface PanelAction { id: string; @@ -39,9 +39,9 @@ export class CommandPanelModal extends Modal { 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 iconEl = row.createSpan({ cls: "istart-panel-action-icon" }); + setIcon(iconEl, action.icon); const textEl = row.createDiv({ cls: "istart-panel-action-text" }); textEl.createEl("span", { text: action.label, cls: "istart-panel-action-label" }); diff --git a/src/main.ts b/src/main.ts index 86b2e0d..9b10c04 100644 --- a/src/main.ts +++ b/src/main.ts @@ -682,6 +682,131 @@ ${selection ? `用户当前选中的文字:\n${selection}\n` : ""}`; .map((f) => f.basename); } + // ── 执行模块入口 ─────────────────────────────────────────────── + + /** 打开待确认计划列表 */ + openPendingPlans() { + const folder = "Knowledge/_ExecutionPlans"; + const files = this.app.vault.getMarkdownFiles().filter((f) => f.path.startsWith(folder)); + if (files.length === 0) { + new Notice("暂无待确认计划"); + return; + } + // Open the folder in file explorer, or open the most recent plan + const sorted = files.sort((a, b) => b.stat.mtime - a.stat.mtime); + const leaf = this.app.workspace.getLeaf(false); + void leaf.openFile(sorted[0]); + if (files.length > 1) { + new Notice(`共 ${files.length} 个待确认计划,已打开最新`); + } + } + + /** 打开执行日志列表 */ + openExecutionLogs() { + const folder = "Knowledge/_Executions"; + const files = this.app.vault.getMarkdownFiles().filter((f) => f.path.startsWith(folder)); + if (files.length === 0) { + new Notice("暂无执行日志"); + return; + } + const sorted = files.sort((a, b) => b.stat.mtime - a.stat.mtime); + const leaf = this.app.workspace.getLeaf(false); + void leaf.openFile(sorted[0]); + if (files.length > 1) { + new Notice(`共 ${files.length} 条执行记录,已打开最新`); + } + } + + /** 查看定时任务状态 */ + openScheduledTasks() { + new Notice("定时任务运行时在 v2.0 默认关闭,将在 v2.1 通过设置页启用。"); + } + + /** 从当前笔记生成执行计划(MVP:简单 prompt) */ + openGeneratePlan() { + const editor = this.app.workspace.activeEditor?.editor ?? null; + const activeFile = this.app.workspace.getActiveFile(); + if (!editor || !activeFile) { + new Notice("请先打开一个文件"); + return; + } + const content = editor.getValue(); + if (!content.trim()) { + new Notice("文档为空"); + return; + } + + new AssistantInputModal(this.app, `📋 从当前笔记生成执行计划`, (instruction) => { + void this.runGeneratePlan(instruction, content, activeFile); + }).open(); + } + + private async runGeneratePlan(instruction: string, noteContent: string, sourceFile: TFile) { + const notice = new Notice("⏳ AI 正在生成执行计划...", 0); + try { + const { LLMClient } = await import("./core/llm"); + const llm = new LLMClient(this.settings); + + const systemPrompt = `你是一个任务规划助手。用户会给你一篇笔记内容和一条指令,请从中提取可执行的行动项,按以下 JSON 格式返回: +{ + "title": "计划标题", + "tasks": [ + { "action": "create-file | append-section | create-link", "target": "目标文件路径或章节", "content": "内容" } + ] +} +规则: +1. 只生成具体、可在 Obsidian 中执行的操作。 +2. target 使用 Obsidian 相对路径。 +3. 如果不确定,宁可少生成也不要编造。`; + + const userPrompt = `笔记:${sourceFile.basename}\n\n内容:\n${noteContent.slice(0, 2000)}\n\n指令:${instruction}`; + + const raw = await llm.chat({ systemPrompt, userPrompt, temperature: 0.4 }); + notice.hide(); + + // Parse and create a draft plan + const { parseJsonSafe } = await import("./core/llm"); + const parsed = parseJsonSafe<{ title?: string; tasks?: { action?: string; target?: string; content?: string }[] } | null>(raw, null); + + if (!parsed || !parsed.tasks || parsed.tasks.length === 0) { + new Notice("AI 未能从当前笔记提取出可执行计划"); + return; + } + + const { PlanBuilder } = await import("./core/execution"); + const { PlanDraftStore } = await import("./core/execution"); + const builder = new PlanBuilder(parsed.title || instruction.slice(0, 40), "assistant"); + + for (const task of parsed.tasks) { + if (!task.target || !task.content) continue; + switch (task.action) { + case "create-file": + builder.createFile(task.target, task.content); + break; + case "append-section": + builder.appendSection(task.target, "行动项", task.content); + break; + case "create-link": + builder.createLink(sourceFile.path, task.target); + break; + default: + builder.appendSection(task.target || sourceFile.path, "行动项", task.content); + } + } + + const plan = builder.build(); + const store = new PlanDraftStore(this.app); + const draftFile = await store.persistDraft(plan); + + new Notice(`✅ 执行计划已生成(${plan.operations.length} 项操作),请审阅后确认`); + const leaf = this.app.workspace.getLeaf(false); + await leaf.openFile(draftFile); + } catch (err) { + notice.hide(); + new Notice(`❌ ${(err as Error).message}`); + } + } + // ── 定时任务 ───────────────────────────────────────────────── private startScheduler(): void { diff --git a/src/settings/SettingsTab.ts b/src/settings/SettingsTab.ts index 41a73a0..fb21989 100644 --- a/src/settings/SettingsTab.ts +++ b/src/settings/SettingsTab.ts @@ -34,17 +34,17 @@ export class DeepSeekSettingsTab extends PluginSettingTab { } private renderNav(container: HTMLElement): void { - const sections: { id: SettingsSection; label: string; icon: string }[] = [ - { id: "knowledge", label: "知识", icon: "📚" }, - { id: "execution", label: "执行", icon: "⚡" }, - { id: "auxiliary", label: "辅助", icon: "🔧" }, + const sections: { id: SettingsSection; label: string }[] = [ + { id: "knowledge", label: "知识" }, + { id: "execution", label: "执行" }, + { id: "auxiliary", label: "辅助" }, ]; for (const sec of sections) { const item = container.createDiv({ cls: `istart-settings-nav-item${this.activeSection === sec.id ? " is-active" : ""}`, }); - item.setText(`${sec.icon} ${sec.label}`); + item.setText(sec.label); item.addEventListener("click", () => { this.activeSection = sec.id; this.display();