feat(panel): execution module visible + icon professionalization

Command panel now shows all three domains with actions:
  入口: AI 助手 (pinned)
  知识: 知识库问答, 知识提问, 补全概念, 扫描空概念, 阅读项目, 知识债务
  执行: 生成执行计划, 查看待确认计划, 查看执行日志, 定时任务
  辅助: 美化文档, 百度云同步

Execution module user-facing actions:
  - 'generate-plan': AI analyzes current note, extracts action items,
    creates PlanDraft (never auto-executes)
  - 'view-pending-plans': opens most recent plan draft
  - 'view-execution-logs': opens most recent execution record
  - 'scheduled-tasks': notice placeholder until v2.1

Icons migrated from emoji to Lucide (Obsidian-native):
  sparkles, book-open-check, message-circle-question, puzzle,
  scan-search, book-marked, activity, list-todo, clipboard-list,
  scroll-text, timer, paintbrush, cloud-upload

CommandPanelModal now uses setIcon() for proper SVG rendering.
Settings nav no longer uses emoji.
This commit is contained in:
Yan 2026-05-20 16:50:26 +08:00
parent 3720bdfbfe
commit 9d2a0c08ba
5 changed files with 193 additions and 22 deletions

View file

@ -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",

View file

@ -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,

View file

@ -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" });

View file

@ -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 {

View file

@ -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();