fix(plan): execution plan generates actual plan content, not internal ops

Before: 'generate plan' asked AI for JSON of vault operations (create-file,
append-section, etc.) and saved a technical PlanDraftStore note. User saw
internal file paths and operation types — not useful.

After: 'generate plan' asks AI to produce the plan itself — action items,
deadlines, priorities — as readable Markdown with checkboxes. The result
is previewed (like AI assistant), then saved as a normal plan note under
Knowledge/Plans/ with frontmatter type: plan.

The plan note looks like what you'd actually use:
  # 本周执行计划
  > 来源:[[2026-05-20 项目会议]]
  ## 高优先级
  - [ ] 完成 API 设计文档(周三前)
  - [ ] 和前端对齐接口规范
  ## 常规
  - [ ] 更新文档
  ...

PlanDraftStore/PlanExecutor remain for system-level batch operations
(artifact multi-file saves, scheduler tasks). User-facing 'plans' are
just knowledge notes with checkboxes.
This commit is contained in:
Yan 2026-05-20 22:26:48 +08:00
parent 3cf38ba59e
commit 22dfb1becf

View file

@ -789,7 +789,7 @@ ${selection ? `用户当前选中的文字:\n${selection}\n` : ""}`;
new Notice("定时任务运行时在 v2.0 默认关闭,将在 v2.1 通过设置页启用。");
}
/** 从当前笔记生成执行计划MVP简单 prompt */
/** 从当前笔记生成执行计划 */
openGeneratePlan() {
const editor = this.app.workspace.activeEditor?.editor ?? null;
const activeFile = this.app.workspace.getActiveFile();
@ -814,66 +814,92 @@ ${selection ? `用户当前选中的文字:\n${selection}\n` : ""}`;
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 systemPrompt = `你是一个执行计划生成助手。用户会给你一篇笔记和一条指令,你需要从中提取行动项,生成一份结构化的执行计划。
const userPrompt = `笔记:${sourceFile.basename}\n\n内容\n${noteContent.slice(0, 2000)}\n\n指令${instruction}`;
1. Markdown
2. - [ ]
3.
4.
5. >
6. JSON Markdown `;
const userPrompt = `来源笔记:${sourceFile.basename}\n\n笔记内容\n${noteContent.slice(0, 3000)}\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 未能从当前笔记提取出可执行计划");
if (!raw.trim()) {
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");
// Beautify with known concepts
const knownConcepts = this.getKnownConcepts();
const style = this.settings.outputStyle ?? "knowledge-base";
const assistant = new AIAssistant(this.settings, style, knownConcepts);
const beautified = assistant.beautifyContent(raw);
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);
}
}
// Build the plan note
const today = todayIso();
const title = instruction
? instruction.slice(0, 40)
: `${sourceFile.basename} 执行计划`;
const plan = builder.build();
const store = new PlanDraftStore(this.app);
const draftFile = await store.persistDraft(plan);
const planContent = `---
type: plan
schema_version: 1
source: "[[${sourceFile.path}|${sourceFile.basename}]]"
status: active
created_at: ${today}
---
new Notice(`✅ 执行计划已生成(${plan.operations.length} 项操作),请审阅后确认`);
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(draftFile);
# ${title}
${beautified}
---
##
- [[${sourceFile.path}|${sourceFile.basename}]]
- ${today}
`;
// Show preview then save
new AssistantResultModal(
this.app,
{ mode: "show", content: planContent, explanation: "执行计划预览" },
() => { void this.savePlanNote(title, planContent); },
() => { void this.runGeneratePlan(instruction, noteContent, sourceFile); }
).open();
} catch (err) {
notice.hide();
new Notice(`${(err as Error).message}`);
}
}
private async savePlanNote(title: string, content: string) {
const folder = normalizePath("Knowledge/Plans");
if (!this.app.vault.getAbstractFileByPath(folder)) {
await this.app.vault.createFolder(folder);
}
const safeName = title.replace(/[\\/:*?"<>|#[\]]/g, "-").slice(0, 50);
const today = todayIso();
let path = normalizePath(`${folder}/${today}-${safeName}.md`);
let suffix = 2;
while (this.app.vault.getAbstractFileByPath(path)) {
path = normalizePath(`${folder}/${today}-${safeName}-${suffix}.md`);
suffix++;
}
const file = await this.app.vault.create(path, content);
new Notice(`✅ 执行计划已保存`);
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(file);
}
// ── 定时任务 ─────────────────────────────────────────────────
private startScheduler(): void {