fix(2.0.0): address 5 blocking issues before merge

P0-1: create-plan-only no longer executes — uses new PlanDraftStore
      to persist plans under Knowledge/_ExecutionPlans/ without applying.
      PlanExecutor result is now checked for success/failure.

P0-2: Scheduler runtime disabled by default in v2.0 (commented out
      startScheduler call). Will be enabled via settings UI in v2.1.
      runBaiduBackup now checks cfg.autoBackup.
      Task renamed to '每日配置同步' to reflect actual behavior.

P1-1: Vault QA source links use [[path|title]] format for reliable
      resolution regardless of H1 vs filename mismatch.

P1-2: NextRunCalculator.getNextRun weekly bug fixed — same-day
      targets before due time now resolve to today, not next week.

P1-3: README execution section rewritten to match actual state.
      Settings page execution description updated accordingly.

P2-1: 'scheduler' added to PlanSource type.
P2-2: CI triggers on release/** branches.
P2-3: Command panel now renders AI 助手 as pinned entry above grouped actions.
This commit is contained in:
Yan 2026-05-20 16:28:55 +08:00
parent 7b26626ccb
commit 3720bdfbfe
11 changed files with 134 additions and 28 deletions

View file

@ -2,7 +2,7 @@ name: CI
on:
push:
branches: [main, master]
branches: [main, master, "release/**"]
pull_request:
branches: [main, master]

View file

@ -46,11 +46,13 @@ Build and maintain a structured knowledge base.
Turn knowledge into reviewable actions.
- **Generate execution plans** from notes — preview all vault modifications before applying.
- **Record execution logs** under `Knowledge/_Executions/` with full operation details.
- **Scheduled tasks** (MVP) — periodic knowledge-debt scans and backup jobs.
- **Safety by default** — AI-generated writes require confirmation; batch ops capped; high-risk plans force double-confirm.
- Future: diff preview, rollback, task-plugin integrations.
- **Execution plan data model** — PlanBuilder + PlanExecutor + PlanDraftStore for multi-op vault changes.
- **Execution logs** recorded under `Knowledge/_Executions/` after each plan is applied.
- **Plan drafts** stored under `Knowledge/_ExecutionPlans/` for `create-plan-only` tasks (user reviews before applying).
- **Scheduler foundation** — ScheduledTask types and runner exist; runtime is disabled by default in v2.0 (enabled in v2.1).
- **Safety by default**`create-plan-only` never auto-executes; `auto-execute-low-risk` only applies plans with `riskLevel: "low"`.
- Most AI write flows (assistant, beautify, concept completion) still use direct editor writes; migration to plan-first is in progress.
- Future: diff preview, rollback, batch-op caps, task-plugin integrations.
### 3. Auxiliary

View file

@ -3,8 +3,8 @@ import { ActionDef } from "./types";
/**
* All actions, organized by domain: Knowledge / Execution / Auxiliary.
*
* The "AI 助手" is placed in Auxiliary as a cross-cutting entry point,
* but the command panel renders it as a pinned top-level button.
* 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.
*/
export const ALL_ACTIONS: ActionDef[] = [
// ══════════════════════════════════════════════════════════════

View file

@ -95,9 +95,28 @@ function openPanel(plugin: DeepSeekPlugin, actions: ActionDef[]) {
ctx.selection = editor.getSelection().trim();
}
// Separate pinned action (AI 助手) from grouped actions
const pinnedAction = actions.find((a) => a.id === "ai-assistant");
const groupedActions = actions.filter((a) => a.id !== "ai-assistant");
const groups: PanelGroup[] = [];
// Add pinned as first "group" with a special title
if (pinnedAction && evaluateWhen(pinnedAction.when, ctx)) {
groups.push({
title: "⭐ 入口",
actions: [{
id: pinnedAction.id,
icon: pinnedAction.icon,
label: pinnedAction.label,
description: pinnedAction.description,
callback: () => pinnedAction.run(ctx),
}],
});
}
for (const domainId of DOMAIN_ORDER) {
const domainActions = actions.filter(
const domainActions = groupedActions.filter(
(a) => a.domain === domainId && a.showIn.includes("panel") && evaluateWhen(a.when, ctx)
);
if (domainActions.length === 0) continue;

View file

@ -0,0 +1,74 @@
import { App, TFile, normalizePath } from "obsidian";
import { ExecutionPlan } from "./types";
import { SCHEMA_VERSION, todayIso } from "../schema";
/**
* PlanDraftStore persists an ExecutionPlan as a "pending" draft note.
*
* The draft is NOT executed; it's stored for the user to review and manually confirm.
* This is the correct behavior for `create-plan-only` safety level.
*/
export class PlanDraftStore {
private folder = "Knowledge/_ExecutionPlans";
constructor(private app: App) {}
async persistDraft(plan: ExecutionPlan): Promise<TFile> {
const folder = normalizePath(this.folder);
await this.ensureFolder(folder);
const safeName = plan.title.replace(/[\\/:*?"<>|#[\]]/g, "-").slice(0, 40);
let path = normalizePath(`${folder}/${todayIso()}-${safeName}.md`);
let suffix = 2;
while (this.app.vault.getAbstractFileByPath(path)) {
path = normalizePath(`${folder}/${todayIso()}-${safeName}-${suffix}.md`);
suffix++;
}
const content = this.renderDraft(plan);
return await this.app.vault.create(path, content);
}
private renderDraft(plan: ExecutionPlan): string {
const ops = plan.operations.map((op, i) => {
const type = op.type;
const target = "path" in op ? (op as { path: string }).path : (op as { from: string }).from;
return `${i + 1}. \`${type}\`\`${target}\``;
}).join("\n");
return `---
type: execution-plan
schema_version: ${SCHEMA_VERSION}
status: pending
plan_id: ${plan.id}
source: ${plan.source}
risk_level: ${plan.riskLevel}
operations_count: ${plan.operations.length}
created_at: ${plan.createdAt}
---
# 📋 ${plan.title}
> [!warning]
>
##
${plan.riskLevel === "high" ? "🔴 高" : plan.riskLevel === "medium" ? "🟡 中" : "🟢 低"}
##
${ops}
##
${plan.previewMarkdown}
`;
}
private async ensureFolder(path: string): Promise<void> {
if (!this.app.vault.getAbstractFileByPath(path)) {
await this.app.vault.createFolder(path);
}
}
}

View file

@ -1,5 +1,6 @@
export { PlanBuilder } from "./PlanBuilder";
export { PlanExecutor } from "./PlanExecutor";
export { PlanDraftStore } from "./PlanDraftStore";
export type {
ExecutionPlan,
ExecutionRecord,

View file

@ -82,6 +82,7 @@ export type PlanSource =
| "concept"
| "sync"
| "beautify"
| "scheduler"
| "manual";
/**

View file

@ -57,7 +57,7 @@ export class NextRunCalculator {
case "weekly": {
const [h, m] = trigger.time.split(":").map(Number);
const next = new Date(now);
const diff = (trigger.weekday - now.getDay() + 7) % 7 || 7;
const diff = (trigger.weekday - now.getDay() + 7) % 7;
next.setDate(now.getDate() + diff);
next.setHours(h, m, 0, 0);
if (next <= now) next.setDate(next.getDate() + 7);

View file

@ -4,6 +4,7 @@ import { ScheduledTaskConfig, ScheduledTaskResult } from "./types";
import { NextRunCalculator } from "./NextRunCalculator";
import { PlanExecutor } from "../execution";
import { PlanBuilder } from "../execution";
import { PlanDraftStore } from "../execution";
import { KnowledgeIndexService } from "../knowledge";
import { todayIso } from "../schema";
@ -103,20 +104,26 @@ export class ScheduledTaskRunner {
return { ...base, success: true, message: `发现 ${total} 个空概念页` };
}
// create-plan-only / auto-execute-low-risk → write report
// Build an execution plan for creating the report
const reportContent = this.buildDebtReport(index);
const plan = new PlanBuilder("每日知识债务扫描", "assistant")
const plan = new PlanBuilder("每日知识债务扫描", "scheduler")
.createFile(`Knowledge/_Reports/${todayIso()}-知识债务.md`, reportContent)
.build();
if (task.safety === "auto-execute-low-risk" && plan.riskLevel === "low") {
await new PlanExecutor(this.plugin.app).execute(plan);
return { ...base, success: true, message: `报告已生成(${total} 个空概念)` };
const record = await new PlanExecutor(this.plugin.app).execute(plan);
return {
...base,
success: record.success,
message: record.success
? `报告已生成(${total} 个空概念)`
: `执行失败:${record.error ?? "未知错误"}`,
};
}
// Just persist the plan draft (user confirms later)
await new PlanExecutor(this.plugin.app).execute(plan);
return { ...base, success: true, message: `报告已生成(${total} 个空概念)` };
// create-plan-only: persist as draft, do NOT execute
await new PlanDraftStore(this.plugin.app).persistDraft(plan);
return { ...base, success: true, message: `已生成待确认计划${total} 个空概念)` };
}
private async runBaiduBackup(
@ -124,16 +131,16 @@ export class ScheduledTaskRunner {
base: Omit<ScheduledTaskResult, "success" | "message">
): Promise<ScheduledTaskResult> {
const cfg = this.plugin.settings.baiduSync;
if (!cfg.enabled || !cfg.accessToken) {
return { ...base, success: false, message: "百度同步未启用或未授权" };
if (!cfg.enabled || !cfg.autoBackup || !cfg.accessToken) {
return { ...base, success: false, message: "百度自动备份未启用或未授权" };
}
// Delegate to existing sync service
// Delegate to existing sync service (config sync only in v2.0)
const { BaiduSyncService } = await import("../../features/sync/BaiduSyncService");
const service = new BaiduSyncService(this.plugin.app, cfg);
const adapter = this.plugin.app.vault.adapter as unknown as { basePath?: string };
const ok = await service.pushConfig(this.plugin.settings, adapter.basePath ?? "device");
return { ...base, success: ok, message: ok ? "配置已备份" : "备份失败" };
return { ...base, success: ok, message: ok ? "配置已同步" : "配置同步失败" };
}
private buildDebtReport(index: KnowledgeIndexService): string {

View file

@ -44,7 +44,9 @@ export default class DeepSeekPlugin extends Plugin {
this.knowledgeIndex = new KnowledgeIndexService(this.app);
this.app.workspace.onLayoutReady(() => {
this.knowledgeIndex.rebuild();
this.startScheduler();
// Scheduler runtime is disabled by default in v2.0.
// Enable via settings once scheduler UI is shipped in v2.1.
// this.startScheduler();
});
// Incremental updates
this.registerEvent(
@ -530,7 +532,7 @@ export default class DeepSeekPlugin extends Plugin {
const content = await this.app.vault.cachedRead(file);
// Take first 600 chars per file to keep context manageable
const snippet = content.slice(0, 600).trim();
contextParts.push(`--- 来源:[[${entry.basename}]] (${entry.type ?? "note"}) ---\n${snippet}`);
contextParts.push(`--- 来源:[[${entry.path}|${entry.title}]] (${entry.type ?? "note"}) ---\n${snippet}`);
sourceFiles.push({ path: entry.path, title: entry.title });
}
@ -561,7 +563,7 @@ ${selection ? `用户当前选中的文字:\n${selection}\n` : ""}`;
// 6. Append source section
const sourcesSection = sourceFiles.length > 0
? `\n\n---\n\n## 依据来源\n\n${sourceFiles.map((s) => `- [[${s.title}]]`).join("\n")}\n`
? `\n\n---\n\n## 依据来源\n\n${sourceFiles.map((s) => `- [[${s.path}|${s.title}]]`).join("\n")}\n`
: "";
const finalContent = beautified + sourcesSection;
@ -693,8 +695,8 @@ ${selection ? `用户当前选中的文字:\n${selection}\n` : ""}`;
safety: "notify-only",
},
{
id: "daily-baidu-backup",
name: "每日百度备份",
id: "daily-baidu-config-sync",
name: "每日百度配置同步",
enabled: this.settings.baiduSync.enabled && this.settings.baiduSync.autoBackup,
kind: "baidu-backup",
trigger: { type: "daily", time: "23:00" },

View file

@ -133,7 +133,7 @@ export class DeepSeekSettingsTab extends PluginSettingTab {
new Setting(el).setName("执行计划").setHeading();
el.createEl("p", {
text: "执行模块当前为实验阶段。所有 AI 生成的写入操作都会先生成计划,需要你确认后再执行。回滚功能尚未实现。",
text: "执行模块当前为实验阶段。所有 AI 生成的写入操作都会先生成计划草稿,需要你确认后再执行。回滚功能尚未实现。定时任务运行时默认关闭,将在 v2.1 启用。",
attr: { style: "color: var(--text-muted); font-size: 13px; margin-bottom: 12px;" },
});
@ -146,7 +146,7 @@ export class DeepSeekSettingsTab extends PluginSettingTab {
new Setting(el).setName("定时任务").setHeading();
el.createEl("p", {
text: "定时任务即将推出v2.1)。支持每日知识债务扫描、自动百度备份、每周问题图谱重建。",
text: "定时任务运行时在 v2.0 默认关闭。基础设施已就绪(知识债务扫描、配置同步),将在 v2.1 通过设置启用。",
attr: { style: "color: var(--text-muted); font-size: 13px;" },
});
}