mirror of
https://github.com/yan-istart/IStart-Note-AI-Plugin.git
synced 2026-07-22 06:51:37 +00:00
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:
parent
7b26626ccb
commit
3720bdfbfe
11 changed files with 134 additions and 28 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -2,7 +2,7 @@ name: CI
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
branches: [main, master, "release/**"]
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
|
||||
|
|
|
|||
12
README.md
12
README.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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[] = [
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
74
src/core/execution/PlanDraftStore.ts
Normal file
74
src/core/execution/PlanDraftStore.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
export { PlanBuilder } from "./PlanBuilder";
|
||||
export { PlanExecutor } from "./PlanExecutor";
|
||||
export { PlanDraftStore } from "./PlanDraftStore";
|
||||
export type {
|
||||
ExecutionPlan,
|
||||
ExecutionRecord,
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ export type PlanSource =
|
|||
| "concept"
|
||||
| "sync"
|
||||
| "beautify"
|
||||
| "scheduler"
|
||||
| "manual";
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
12
src/main.ts
12
src/main.ts
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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;" },
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue