feat(2.0.0): three-module architecture — knowledge/execution/auxiliary

M1: README正式化 + badges + 三模块叙事 + 状态修正 + 版本对齐2.0.0
M2: 设置页改为侧边导航(知识/执行/辅助) + 分区布局CSS
M3: ActionDomain(knowledge/execution/auxiliary) 替代旧ActionGroup + 面板三栏
M4: Scheduler MVP — ScheduledTaskRunner + NextRunCalculator + 知识债务扫描 + 百度备份任务
    KnowledgeIndexService 新增 getAll() 公开方法
    KnowledgeDebtModal 改用公开API
This commit is contained in:
Yan 2026-05-20 16:06:28 +08:00
parent cb4bd88c65
commit 7b26626ccb
17 changed files with 1023 additions and 535 deletions

231
README.md
View file

@ -1,125 +1,127 @@
# IStart-Note-AI
> [!warning] Beta software
> IStart-Note-AI is under active development. Features marked **experimental** below may change or be removed without notice. The data model and frontmatter schema are not yet stable; back up your vault before trying new features.
<p align="center">
<strong>Turn your Obsidian vault into a knowledge-to-action system.</strong>
</p>
[简体中文 README →](./README.zh-CN.md)
<p align="center">
<a href="./README.zh-CN.md">简体中文</a> ·
<a href="#quick-start">Quick Start</a> ·
<a href="#privacy">Privacy</a> ·
<a href="#roadmap">Roadmap</a>
</p>
An Obsidian plugin that turns your notes into a structured personal knowledge system. One unified AI assistant helps you draft, expand, and organize notes, with automatic concept pages, bidirectional links, reading projects, and optional Baidu Cloud sync.
The plugin is built around an OpenAI-compatible chat completions API. The default provider is [DeepSeek](https://platform.deepseek.com); other compatible endpoints work by changing the Base URL.
<p align="center">
<img alt="Version" src="https://img.shields.io/github/v/release/yan-istart/IStart-Note-AI-Plugin?include_prereleases">
<img alt="License" src="https://img.shields.io/github/license/yan-istart/IStart-Note-AI-Plugin">
<img alt="CI" src="https://img.shields.io/github/actions/workflow/status/yan-istart/IStart-Note-AI-Plugin/ci.yml?branch=main">
<img alt="Obsidian" src="https://img.shields.io/badge/Obsidian-1.7.2%2B-7C3AED">
</p>
---
## Status overview
IStart-Note-AI is an Obsidian plugin built around three modules — **Knowledge**, **Execution**, and **Auxiliary** — that help you turn scattered notes into a searchable, interlinked, and actionable personal knowledge system.
| Feature | Status | Notes |
| --- | --- | --- |
| AI Assistant (insert / replace / append / show) | stable | Single command panel entry, content classifier + structured prompt + markdown beautifier |
| Reading projects | stable | Generate skeleton, chapter pre-reading questions, summaries, Feynman tests |
| Baidu Cloud sync (notes + plugin config) | stable | Manual and auto modes, conflict strategy, plugin/Obsidian config backup |
| Document beautification | stable | Restructures headings, adds callouts and Mermaid diagrams |
| Concept pages auto-creation | stable | Empty pages created via `[[concept]]` link scan |
| Concept page completion (`ConceptCompleter`) | experimental | Internals exist; not exposed in the unified panel yet |
| Question graph (`QuestionGraphManager`) | experimental | Frontmatter classification + Mermaid evolution graph; not yet wired into the panel |
| Vault-wide knowledge retrieval | not yet | Planned for v2 |
| Execution plan / preview / rollback | not yet | Planned for v3 |
One unified AI entry point connects all three: ask questions, generate structured notes, build execution plans, and keep everything synced — all through natural language.
> [!warning] Beta
> v2.0.0 introduces significant architectural changes. The frontmatter schema and scheduled-task model are not yet stable. Back up your vault before upgrading.
---
## Features
## Core Modules
### AI Assistant (unified entry)
### 1. Knowledge
Select text or place your cursor, then describe what you want in natural language:
Build and maintain a structured knowledge base.
- **Expand / rewrite** selected text
- **Explain** a term
- **Generate diagrams** (flowchart, sequence, state, class, ER, Gantt) and LaTeX formulas
- **Fill empty sections** based on document context
- **Continue writing** from the cursor
- **Summarize** the current document
- **Beautify** existing content with callouts, links, and visual breaks
- **Ask questions** and generate Q&A notes with automatic concept extraction.
- **Classify questions** into new / refinement / expansion and maintain a question evolution graph.
- **Create and complete concept pages** with definitions, examples, relations, and domain MOC indexes.
- **Build reading projects** — generate a book skeleton, chapter pre-reading questions, summaries, and Feynman tests.
- **Search your vault** and get answers with `[[source]]` references (metadata-based index, no embeddings).
- **Detect knowledge debt** — empty concepts, orphan questions, unfinished readings, stale drafts.
Quick tags work too: `[扩写]` `[解释]` `[画图]` `[补全]` `[续写]` `[总结]` `[公式]` `[时序图]`.
### 2. Execution
### Structured output
Turn knowledge into reviewable actions.
Model output is post-processed for knowledge-base style: short paragraphs, Obsidian callouts (`> [!summary]`, `> [!warning]`, `> [!tip]`), Mermaid diagrams where appropriate, and automatic `[[concept]]` linking against existing pages. The output style is configurable: technical, minimal, academic, product, story, dashboard.
- **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.
### Reading projects
### 3. Auxiliary
Turn any book into a structured study plan:
Keep the system usable across devices and providers.
1. Enter book title (and optionally a table of contents).
2. The plugin generates a reading roadmap, chapter relationships, and pre-reading questions.
3. Take notes per chapter, then generate chapter summaries and Feynman tests.
### Knowledge organization
- New concepts captured under `Knowledge/Concepts/_未分类/`.
- After completion, concepts are reorganized into domain subfolders.
- Domain MOC index pages are generated with Mermaid overview graphs.
- Question evolution graphs live in the question index.
### Baidu Cloud sync (optional)
- Incremental backup, bidirectional sync, or force overwrite.
- Optional backup of the plugin itself and Obsidian config (toolbar, hotkeys, appearance).
- Optional auto-backup after note generation.
> [!info] Privacy
> AI features send your selection and parts of the active note to the configured chat-completions endpoint. Sync features upload selected notes (and optionally Obsidian config) to your own Baidu Pan storage. See [PRIVACY.md](./PRIVACY.md) for the full data flow.
- **OpenAI-compatible LLM provider** — DeepSeek default; change Base URL for others.
- **Configurable output styles** — knowledge-base, technical, minimal, product, academic, story, dashboard.
- **Optional Baidu Cloud sync** — incremental backup, bidirectional sync, plugin and Obsidian config backup.
- **Diagnostics** — privacy overview, config export, index rebuild, log cleanup.
---
## Requirements
## Status
- Obsidian 1.7.2 or later.
- A DeepSeek API key (or any OpenAI-compatible endpoint).
- Baidu Cloud sync (optional): a Baidu Pan App ID and App Secret.
| Module | Feature | Status | Notes |
| --- | --- | --- | --- |
| Knowledge | AI Assistant | Stable | Insert / replace / append / show via unified entry |
| Knowledge | Reading Projects | Stable | Skeleton, chapter questions, summaries, Feynman |
| Knowledge | Concept Completion | Experimental | Exposed in command panel, preview before write |
| Knowledge | Question Graph | Experimental | Classification + index + Mermaid evolution graph |
| Knowledge | Vault QA | Experimental | Metadata-index retrieval, cited answers, no embeddings |
| Knowledge | Knowledge Debt | Experimental | Dashboard with empty/orphan/unfinished/stale stats |
| Execution | Execution Plan | Experimental | PlanBuilder + PlanExecutor, no rollback yet |
| Execution | Scheduled Tasks | Planned | Scheduler types defined, runner in progress |
| Auxiliary | Baidu Sync | Stable | Manual/auto backup and config sync |
| Auxiliary | Multi-provider LLM | Partial | OpenAI-compatible base URL supported |
---
## Quick Start
1. Install the plugin (see [Installation](#installation)).
2. Go to **Settings → IStart-Note-AI → Auxiliary → AI Service** and enter your API key.
3. Click the 🧠 ribbon icon or press the command palette → **IStart-Note-AI: AI 助手**.
4. Type a request in natural language.
---
## Installation
### From community plugins (preferred)
Submission to the community plugin store is in progress. Once available:
### From community plugins (once available)
1. Settings → Community plugins → Browse.
2. Search **IStart-Note-AI**.
3. Install → Enable.
### Manual install (recommended path during beta)
### Manual (recommended during beta)
Download the assets from a published [GitHub Release](https://github.com/yan-istart/IStart-Note-AI-Plugin/releases) — `main.js`, `manifest.json`, `styles.css` — and place them under `<your-vault>/.obsidian/plugins/istart-note-ai/`.
Download `main.js`, `manifest.json`, `styles.css` from a [GitHub Release](https://github.com/yan-istart/IStart-Note-AI-Plugin/releases) and place them in `<vault>/.obsidian/plugins/istart-note-ai/`.
> Don't copy the source repository directly: the runtime bundle is built into `dist/` and is not committed to git. Always use the release assets.
> Don't clone the source repo — the bundle lives in `dist/` and is not committed. Use release assets.
### Build from source
```bash
npm ci
npm run build
# dist/main.js, dist/manifest.json, dist/styles.css are the install artifacts
# dist/main.js, dist/manifest.json, dist/styles.css
```
---
## Configuration
Settings → IStart-Note-AI:
Settings are organized into three tabs:
| Setting | Description | Default |
| --- | --- | --- |
| API Key | DeepSeek API key (or compatible) | — |
| Base URL | Chat completions endpoint root | `https://api.deepseek.com` |
| Model | `deepseek-v4-flash` or `deepseek-v4-pro` | `deepseek-v4-flash` |
| Output style | Knowledge-base, technical, minimal, product, academic, story, dashboard | Knowledge-base |
| Q&A folder | Where Q&A notes are saved | `Knowledge/Q&A` |
| Concepts folder | Where concept pages are saved | `Knowledge/Concepts` |
| Baidu Cloud sync | Enable, App ID/Secret, remote path, auto-backup, ignore pattern | disabled |
| Tab | Key settings |
| --- | --- |
| **Knowledge** | Q&A path, Concepts path, Questions index path, Reading path, auto-classify, concept depth, vault QA source limit |
| **Execution** | Require preview, log path, scheduled tasks, safety policy |
| **Auxiliary** | API key, Base URL, model, output style, Baidu sync, UI preferences |
---
@ -127,55 +129,86 @@ Settings → IStart-Note-AI:
### Desktop
- 🧠 **Ribbon icon** opens the command panel.
- **Right-click in the editor** → `IStart-Note-AI: AI 助手`.
- **Right-click a file in the sidebar** → `IStart-Note-AI: AI 助手`.
- 🧠 **Ribbon icon** → command panel (Knowledge / Execution / Auxiliary).
- **Right-click in editor** → `IStart-Note-AI: AI 助手` or `知识库问答`.
- **Right-click file** → `IStart-Note-AI: AI 助手`.
### Mobile
- 🧠 **Ribbon icon** opens the command panel.
- Add `AI 助手` to the mobile toolbar for a one-tap entry.
### Workflow
1. Optionally select text.
2. Click 🧠 or right-click → `AI 助手`.
3. Type a request (or use a quick tag, or leave blank for auto-detect).
4. Preview the result, then choose to insert, replace, append, or show only.
- 🧠 **Ribbon icon** → command panel.
- Add commands to the mobile toolbar for one-tap access.
---
## Project layout
## Architecture
```
src/
core/ # cross-feature infrastructure
llm/ # unified LLM client + JSON extractor
ai/ # AI features (assistant, classifier, planner, ...)
features/ # UI and per-feature managers
vault/ # vault writers
settings/
actions/ # action registry / command panel
core/
llm/ Unified LLM client + JSON extractor
knowledge/ KnowledgeIndexService (metadata index)
execution/ ExecutionPlan, PlanBuilder, PlanExecutor
scheduler/ ScheduledTask types + runner (WIP)
schema.ts SCHEMA_VERSION + helpers
ai/ AI feature modules (assistant, classifier, planner, ...)
features/
assistant/ AI assistant modals
concept/ Concept completion + page manager
question/ Question classify + graph manager
reading/ Reading project manager
dashboard/ Knowledge debt modal
sync/ Baidu sync
command-panel/ Unified command panel
vault/ Vault writer (conflict-safe)
settings/ Settings tab (tabbed layout)
actions/ Action registry + definitions
main.ts
```
The `core/llm` module centralizes every chat-completions call. New AI features should depend on it instead of calling `requestUrl` directly.
---
## Privacy
AI features send your selection and partial note context to the configured chat-completions endpoint. Sync features upload to your own Baidu Pan. No telemetry. No plugin-operated servers. Full details in [PRIVACY.md](./PRIVACY.md).
---
## Roadmap
- v1.9 (in progress): unified LLM client, basic vault retrieval, AI operation preview, full open-source governance.
- v2.0: vault-wide knowledge retrieval with citations, question graph + concept maturity dashboards.
- v3.0: execution engine — turn notes into reviewable, rollback-able plans (tasks, decisions, projects).
### v2.0 — Knowledge System Foundation (current)
See [the analysis writeup](https://github.com/yan-istart/IStart-Note-AI-Plugin/issues) for the longer plan.
- Three-module product structure: Knowledge / Execution / Auxiliary.
- Vault-wide lightweight knowledge index.
- Concept completion and question graph in command panel.
- Knowledge debt dashboard.
- Basic execution plan and execution logs.
- Settings page with tabbed navigation.
- Open-source governance and privacy docs.
### v2.1 — Execution MVP
- Scheduled tasks runtime (knowledge-debt scan, auto-backup).
- Execution plan preview modal with diff.
- Safer policy: AI writes always produce plan-only by default.
- Execution history view.
### v2.2 — Trust & Control
- Rollback for recent executions.
- More granular privacy controls.
- Optional local vector index for richer vault QA.
### v3.0 — Integrations
- Tasks plugin / Periodic Notes integration.
- GitHub Issues / Linear / Todoist export.
- Multi-vault support.
---
## Contributing
PRs and issues are welcome. Read [CONTRIBUTING.md](./CONTRIBUTING.md) before opening a change. For security reports, see [SECURITY.md](./SECURITY.md).
PRs and issues welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md). Security issues: [SECURITY.md](./SECURITY.md).
## License

View file

@ -1,181 +1,182 @@
# IStart-Note-AI
> [!warning] 测试版软件
> IStart-Note-AI 仍在持续开发中。下文标记为**实验中**的功能可能在没有提前通知的情况下变更或移除。Frontmatter schema 尚未稳定,尝试新功能前请先备份你的 Vault。
<p align="center">
<strong>把 Obsidian 笔记变成"知识 → 执行"的个人系统。</strong>
</p>
[English README →](./README.md)
<p align="center">
<a href="./README.md">English</a> ·
<a href="#快速开始">快速开始</a> ·
<a href="#隐私说明">隐私说明</a> ·
<a href="#路线图">路线图</a>
</p>
把笔记升级成可被结构化、可被检索、可被执行的个人知识系统的 Obsidian 插件。统一的 AI 助手帮你起草、扩写、组织笔记,并自动维护概念页、双链、阅读项目,可选搭配百度云同步。
底层使用 OpenAI 兼容的 chat completions 接口,默认接入 [DeepSeek](https://platform.deepseek.com),更换 Base URL 可使用其他兼容服务。
<p align="center">
<img alt="Version" src="https://img.shields.io/github/v/release/yan-istart/IStart-Note-AI-Plugin?include_prereleases">
<img alt="License" src="https://img.shields.io/github/license/yan-istart/IStart-Note-AI-Plugin">
<img alt="CI" src="https://img.shields.io/github/actions/workflow/status/yan-istart/IStart-Note-AI-Plugin/ci.yml?branch=main">
<img alt="Obsidian" src="https://img.shields.io/badge/Obsidian-1.7.2%2B-7C3AED">
</p>
---
## 功能状态总览
IStart-Note-AI 是一个面向 Obsidian 的 AI 插件,围绕 **知识沉淀、执行计划、同步辅助** 三个模块,帮助你把零散笔记转化为可检索、可关联、可执行的个人知识系统。
| 功能 | 状态 | 说明 |
| --- | --- | --- |
| AI 助手(插入 / 替换 / 追加 / 仅展示) | 稳定 | 命令面板单一入口,配套内容分类 + 结构化 Prompt + Markdown 美化 |
| 阅读项目 | 稳定 | 全书骨架、章节预设问题、章节总结、费曼测试 |
| 百度云同步(笔记 + 插件配置) | 稳定 | 手动与自动模式、冲突策略、插件本身 / Obsidian 配置备份 |
| 文档美化 | 稳定 | 重新组织标题,加入 Callout 和 Mermaid |
| 概念页自动创建 | 稳定 | 从 `[[概念]]` 自动扫描并创建空概念页 |
| 概念页智能补全(`ConceptCompleter` | 实验中 | 内部实现已就绪,尚未接入命令面板 |
| 问题图谱(`QuestionGraphManager` | 实验中 | 已支持 frontmatter 分类与 Mermaid 演化图,尚未接入命令面板 |
| Vault 级知识检索 | 未实现 | 列入 v2 |
| 执行计划 / 预览 / 回滚 | 未实现 | 列入 v3 |
一个统一 AI 入口,连接三类能力:知识沉淀、执行计划、辅助同步。
> [!warning] 测试版
> v2.0.0 引入了重大架构变更。Frontmatter schema 和定时任务模型尚未稳定。升级前请备份 Vault。
---
## 主要功能
## 三大模块
### AI 助手(统一入口)
### 1. 知识 Knowledge
选中文字或停在光标处,用自然语言描述你想做的事:
构建和维护结构化知识库。
- **扩写 / 改写**选中文字
- **解释**某个术语
- **生成图表**流程图、时序图、状态图、类图、ER、甘特图和 LaTeX 公式
- **补全**当前空章节
- **续写**光标后的内容
- **总结**当前文档
- **美化**已有内容(加 Callout、双链、视觉分隔
- **提问**并生成 Q&A 笔记,自动提取概念和关系。
- **问题分类**new / refinement / expansion维护问题演化图。
- **概念页创建与补全**:定义、解释、示例、关联概念、领域 MOC 索引。
- **阅读项目**:全书骨架、章节预设问题、章节总结、费曼测试。
- **知识库问答**:基于 Vault 索引检索回答,附带 `[[来源]]` 引用。
- **知识债务看板**:空概念、孤立问题、未完成阅读、长期草稿一目了然。
也支持快捷标签:`[扩写]` `[解释]` `[画图]` `[补全]` `[续写]` `[总结]` `[公式]` `[时序图]`
### 2. 执行 Execution
### 结构化输出
把知识转化为可审查的行动。
模型输出会做一层后处理按知识库风格统一短段落、Obsidian Callout`> [!summary]`、`> [!warning]`、`> [!tip]`)、必要时的 Mermaid 图,以及对已有概念页自动加 `[[双链]]`。输出风格可切换technical / minimal / academic / product / story / dashboard。
- **生成执行计划**:预览所有 Vault 修改后再执行。
- **执行日志**:自动记录到 `Knowledge/_Executions/`
- **定时任务**MVP每日知识债务扫描、自动百度备份。
- **安全优先**AI 写入需要确认,批量操作有上限,高风险计划强制二次确认。
- 未来diff 预览、回滚、任务集成。
### 阅读项目
### 3. 辅助 Auxiliary
把一本书变成一份可执行的阅读计划:
跨设备和多服务的基础支撑。
1. 输入书名(可选粘贴目录)。
2. 插件生成阅读路线图、章节关系、读前问题。
3. 边读边记,再生成章节总结与费曼测试。
### 知识组织
- 新概念先落到 `Knowledge/Concepts/_未分类/`
- 补全完成后,按 domain 自动归类到子目录。
- 自动维护 domain MOC 索引页(含 Mermaid 概览图)。
- 问题索引下生成问题演化图。
### 百度云同步(可选)
- 增量备份 / 双向同步 / 强制覆盖。
- 可选连同插件本身及 Obsidian 配置(工具栏、快捷键、外观)一起备份。
- 可选生成笔记后自动备份。
> [!info] 隐私说明
> AI 功能会把你选中的内容以及当前笔记的部分上下文发送到所配置的 chat-completions 接口。同步功能会把笔记(可选 Obsidian 配置)上传到你自己的百度网盘。完整数据流见 [PRIVACY.md](./PRIVACY.md)。
- **OpenAI 兼容 LLM**:默认 DeepSeek切换 Base URL 可用其他服务。
- **输出风格可选**:知识库、技术、极简、产品、学术、叙事、仪表盘。
- **百度网盘同步**(可选):增量备份、双向同步、插件和 Obsidian 配置备份。
- **诊断与隐私**:查看数据流说明、导出配置、重建索引、清理日志。
---
## 环境要求
## 状态
- Obsidian 1.7.2 及以上。
- DeepSeek 或其它 OpenAI 兼容服务的 API Key。
- 百度云同步(可选):百度网盘开放平台 App ID 与 App Secret。
| 模块 | 功能 | 状态 | 说明 |
| --- | --- | --- | --- |
| 知识 | AI 助手 | 稳定 | 插入 / 替换 / 追加 / 仅展示 |
| 知识 | 阅读项目 | 稳定 | 骨架、章节问题、总结、费曼 |
| 知识 | 概念页补全 | 实验中 | 已接入命令面板,预览后写入 |
| 知识 | 问题图谱 | 实验中 | 分类 + 索引 + Mermaid 演化图 |
| 知识 | 知识库问答 | 实验中 | 元数据索引检索,无 embedding |
| 知识 | 知识债务 | 实验中 | 空概念 / 孤立问题 / 草稿统计 |
| 执行 | 执行计划 | 实验中 | PlanBuilder + Executor无回滚 |
| 执行 | 定时任务 | 规划中 | 类型定义完成runner 开发中 |
| 辅助 | 百度同步 | 稳定 | 手动/自动备份与配置同步 |
| 辅助 | 多 Provider | 部分 | 支持 OpenAI 兼容 Base URL |
---
## 快速开始
1. 安装插件(见下方安装说明)。
2. 进入**设置 → IStart-Note-AI → 辅助 → AI 服务**,输入 API Key。
3. 点击侧边栏 🧠 图标或命令面板 → **IStart-Note-AI: AI 助手**
4. 用自然语言输入指令。
---
## 安装
### 从社区插件商店(首选)
### 社区插件商店(审核后可用
正在准备提交。审核通过后:
1. 设置 → 第三方插件 → 浏览 → 搜索 **IStart-Note-AI**
2. 安装 → 启用。
1. 设置 → 第三方插件 → 浏览。
2. 搜索 **IStart-Note-AI**
3. 安装 → 启用。
### 手动安装(测试期推荐)
### 手动安装(测试版推荐方式)
从 [GitHub Release](https://github.com/yan-istart/IStart-Note-AI-Plugin/releases) 下载 `main.js`、`manifest.json`、`styles.css`,放到 `<你的 Vault>/.obsidian/plugins/istart-note-ai/` 目录下。
> 不要直接克隆源码安装:构建产物在 `dist/`,并不入库。请始终使用 release 资产。
从 [GitHub Release](https://github.com/yan-istart/IStart-Note-AI-Plugin/releases) 下载 `main.js`、`manifest.json`、`styles.css`,放到 `<Vault>/.obsidian/plugins/istart-note-ai/`
### 从源码构建
```bash
npm ci
npm run build
# dist/main.js、dist/manifest.json、dist/styles.css 即为安装文件
npm ci && npm run build
# → dist/main.js, dist/manifest.json, dist/styles.css
```
---
## 设置
## 设置
设置 → IStart-Note-AI
设置页按三个标签组织
| 设置 | 说明 | 默认 |
| --- | --- | --- |
| API Key | DeepSeek API Key或兼容服务 | — |
| Base URL | chat completions 根地址 | `https://api.deepseek.com` |
| 模型 | `deepseek-v4-flash``deepseek-v4-pro` | `deepseek-v4-flash` |
| 输出风格 | 知识库、技术、极简、产品、学术、故事、仪表盘 | 知识库 |
| 问答目录 | Q&A 笔记保存路径 | `Knowledge/Q&A` |
| 概念目录 | 概念页保存路径 | `Knowledge/Concepts` |
| 百度云同步 | 启用、App ID/Secret、远端路径、自动备份、忽略规则 | 关闭 |
| 标签 | 主要设置 |
| --- | --- |
| **知识** | Q&A 路径、概念路径、问题索引路径、知识索引状态与重建 |
| **执行** | 执行日志目录、安全策略、定时任务v2.1 |
| **辅助** | API Key、Base URL、模型、输出风格、百度同步、隐私说明 |
---
## 使用方式
## 使用
### 桌面
### 桌面
- 🧠 **侧边栏图标**:打开命令面板
- **编辑器右键**`IStart-Note-AI: AI 助手`。
- **文件列表右键**`IStart-Note-AI: AI 助手`
- 🧠 侧边栏图标 → 命令面板(知识 / 执行 / 辅助)
- 编辑器右键 → `IStart-Note-AI: AI 助手` 或 `知识库问答`。
- 文件列表右键 → `IStart-Note-AI: AI 助手`
### 移动端
- 🧠 **侧边栏图标**:打开命令面板。
- 把 `AI 助手` 添加到移动工具栏,可一键唤起。
### 工作流
1. 可选:选中文字。
2. 点击 🧠 或右键 → AI 助手。
3. 输入指令(或用快捷标签,或留空让插件自动判断)。
4. 预览结果,选择插入 / 替换 / 追加 / 仅展示。
- 🧠 侧边栏图标 → 命令面板。
- 把命令添加到移动工具栏。
---
## 目录结构
## 隐私说明
```
src/
core/ # 跨功能基础设施
llm/ # 统一 LLM 客户端 + JSON 提取
ai/ # AI 功能助手、分类器、Planner ...
features/ # 各功能 UI 与管理器
vault/ # Vault 写入层
settings/
actions/ # action 注册 / 命令面板
main.ts
```
`core/llm` 是所有 chat-completions 调用的唯一入口。新增 AI 功能应该依赖它,而不是直接调用 `requestUrl`
AI 功能会把你选中的内容和部分笔记上下文发送到所配置的 API 端点。同步功能只上传到你自己的百度网盘。无遥测、无插件方服务器。详见 [PRIVACY.md](./PRIVACY.md) / [PRIVACY.zh-CN.md](./PRIVACY.zh-CN.md)。
---
## 路线图
- v1.9(进行中):统一 LLM 客户端、基础 Vault 检索、AI 操作预览、补齐开源治理。
- v2.0:带来源引用的 Vault 级检索、问题图谱与概念成熟度看板。
- v3.0:执行引擎 —— 把笔记转换为可预览、可回滚的计划(任务、决策、项目)。
### v2.0 — 知识系统基础版(当前)
更长期规划见仓库 issue。
- 三模块产品结构:知识 / 执行 / 辅助。
- Vault 级轻量知识索引。
- 概念补全和问题图谱接入命令面板。
- 知识债务看板。
- 基础执行计划和执行日志。
- 分组设置页。
- 开源治理和隐私文档。
### v2.1 — 执行 MVP
- 定时任务运行时。
- 执行计划预览 Modal + diff。
- AI 写入默认 plan-only。
- 执行历史视图。
### v2.2 — 信任与控制
- 回滚最近执行。
- 更细粒度的隐私控制。
- 可选本地向量索引。
### v3.0 — 集成
- Tasks / Periodic Notes 集成。
- GitHub Issues / Linear / Todoist 导出。
---
## 贡献
欢迎提 Issue / PR。请先阅读 [CONTRIBUTING.md](./CONTRIBUTING.md)。安全相关问题请按 [SECURITY.md](./SECURITY.md) 流程提交。
欢迎提 Issue / PR。请先阅读 [CONTRIBUTING.md](./CONTRIBUTING.md)。安全问题见 [SECURITY.md](./SECURITY.md)
## 协议

View file

@ -1,7 +1,7 @@
{
"id": "istart-note-ai",
"name": "IStart-Note-AI",
"version": "1.9.0",
"version": "2.0.0",
"minAppVersion": "1.7.2",
"description": "Generate structured knowledge notes from questions and selected text using DeepSeek AI, with automatic concept pages, bidirectional links, and a question graph.",
"author": "Yan",

View file

@ -1,6 +1,6 @@
{
"name": "istart-note-ai",
"version": "1.9.0",
"version": "2.0.0",
"description": "IStart-Note-AI: an Obsidian plugin that turns notes into a structured personal knowledge system, powered by DeepSeek and other OpenAI-compatible LLMs.",
"main": "main.js",
"private": true,

View file

@ -1,31 +1,49 @@
import { ActionDef } from "./types";
/**
*
* 3 "AI 助手"
* 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.
*/
export const ALL_ACTIONS: ActionDef[] = [
// ── 核心入口AI 助手(覆盖 90% 场景) ────────────────────
{
id: "ai-assistant",
label: "AI 助手",
icon: "sparkles",
description: "选中文字或输入指令AI 智能执行",
group: "general",
when: { always: true },
showIn: ["panel", "editor-menu", "file-menu"],
run: (ctx) => { ctx.plugin.openAssistant(); },
},
// ══════════════════════════════════════════════════════════════
// KNOWLEDGE
// ══════════════════════════════════════════════════════════════
// ── 概念页 ────────────────────────────────────────────────
{
id: "vault-qa",
label: "知识库问答",
icon: "library",
description: "基于 Vault 检索回答,附带来源引用",
domain: "knowledge",
section: "retrieval",
when: { always: true },
showIn: ["panel", "editor-menu"],
experimental: true,
run: (ctx) => { ctx.plugin.openVaultQA(); },
},
{
id: "question-with-graph",
label: "知识提问",
icon: "help-circle",
description: "提问 → 自动分类 → 生成 Q&A → 更新问题图谱",
domain: "knowledge",
section: "question",
when: { always: true },
showIn: ["panel"],
run: (ctx) => { ctx.plugin.openQuestionWithGraph(); },
},
{
id: "complete-current-concept",
label: "补全当前概念页",
icon: "book-plus",
description: "为当前打开的空概念页生成定义、解释、示例、关联",
group: "concept",
domain: "knowledge",
section: "concept",
when: { always: true },
showIn: ["panel", "editor-menu"],
experimental: true,
run: (ctx) => { ctx.plugin.openCompleteCurrentConcept(); },
},
{
@ -33,81 +51,79 @@ export const ALL_ACTIONS: ActionDef[] = [
label: "扫描并补全空概念页",
icon: "search",
description: "扫描 Vault 中所有空概念页,批量补全",
group: "concept",
domain: "knowledge",
section: "concept",
when: { always: true },
showIn: ["panel"],
experimental: true,
run: (ctx) => { ctx.plugin.openScanEmptyConcepts(); },
},
// ── 知识提问(带图谱) ────────────────────────────────────
{
id: "question-with-graph",
label: "知识提问",
icon: "help-circle",
description: "提问 → 自动分类 → 生成 Q&A → 更新问题图谱",
group: "general",
when: { always: true },
showIn: ["panel"],
run: (ctx) => { ctx.plugin.openQuestionWithGraph(); },
},
// ── 知识库问答(带引用) ──────────────────────────────────
{
id: "vault-qa",
label: "知识库问答",
icon: "library",
description: "基于 Vault 检索回答,附带来源引用",
group: "general",
when: { always: true },
showIn: ["panel", "editor-menu"],
run: (ctx) => { ctx.plugin.openVaultQA(); },
},
// ── 阅读项目(需要专门表单) ──────────────────────────────
{
id: "new-reading-project",
label: "新建阅读项目",
icon: "book-open",
description: "输入书名,生成阅读地图",
group: "reading",
domain: "knowledge",
section: "reading",
when: { always: true },
showIn: ["panel"],
run: (ctx) => { ctx.plugin.openNewReadingProject(); },
},
// ── 知识债务看板 ──────────────────────────────────────────
{
id: "knowledge-debt",
label: "知识债务看板",
icon: "bar-chart-2",
description: "查看空概念、孤立问题、未完成阅读、长期未更新草稿",
group: "general",
description: "空概念、孤立问题、未完成阅读、长期未更新草稿",
domain: "knowledge",
section: "debt",
when: { always: true },
showIn: ["panel"],
experimental: true,
run: (ctx) => { ctx.plugin.openKnowledgeDebt(); },
},
// ── 百度云同步(独立功能) ────────────────────────────────
{
id: "baidu-sync",
label: "百度云同步",
icon: "cloud",
description: "备份 / 恢复 / 同步",
group: "sync",
when: { always: true },
showIn: ["panel"],
run: (ctx) => { ctx.plugin.openBaiduSyncModal(); },
},
// ══════════════════════════════════════════════════════════════
// EXECUTION
// ══════════════════════════════════════════════════════════════
// ── 美化当前文档 ──────────────────────────────────────────
// Placeholder: future actions like "生成执行计划", "查看执行日志", "定时任务"
// will be added here once runtime is implemented.
// ══════════════════════════════════════════════════════════════
// AUXILIARY
// ══════════════════════════════════════════════════════════════
{
id: "ai-assistant",
label: "AI 助手",
icon: "sparkles",
description: "选中文字或输入指令AI 智能执行(跨模块入口)",
domain: "auxiliary",
section: "assistant",
when: { always: true },
showIn: ["panel", "editor-menu", "file-menu"],
run: (ctx) => { ctx.plugin.openAssistant(); },
},
{
id: "beautify-note",
label: "美化当前文档",
icon: "wand",
description: "整理结构、插入 Callout、生成双链",
group: "document",
domain: "auxiliary",
section: "document",
when: { always: true },
showIn: ["panel", "editor-menu", "file-menu"],
run: (ctx) => { void ctx.plugin.beautifyCurrentNote(); },
},
{
id: "baidu-sync",
label: "百度云同步",
icon: "cloud",
description: "备份 / 恢复 / 同步",
domain: "auxiliary",
section: "sync",
when: { always: true },
showIn: ["panel"],
run: (ctx) => { ctx.plugin.openBaiduSyncModal(); },
},
];

View file

@ -1,6 +1,6 @@
import { Notice, TFile } from "obsidian";
import type DeepSeekPlugin from "../main";
import { ActionDef, ActionContext, GROUP_TITLES, GROUP_ORDER } from "./types";
import { ActionDef, ActionContext, DOMAIN_TITLES, DOMAIN_ORDER } from "./types";
import { CommandPanelModal } from "../features/command-panel/CommandPanelModal";
import type { PanelGroup, PanelAction } from "../features/command-panel/CommandPanelModal";
@ -96,17 +96,17 @@ function openPanel(plugin: DeepSeekPlugin, actions: ActionDef[]) {
}
const groups: PanelGroup[] = [];
for (const groupId of GROUP_ORDER) {
const groupActions = actions.filter(
(a) => a.group === groupId && a.showIn.includes("panel") && evaluateWhen(a.when, ctx)
for (const domainId of DOMAIN_ORDER) {
const domainActions = actions.filter(
(a) => a.domain === domainId && a.showIn.includes("panel") && evaluateWhen(a.when, ctx)
);
if (groupActions.length === 0) continue;
if (domainActions.length === 0) continue;
groups.push({
title: GROUP_TITLES[groupId],
actions: groupActions.map((a) => ({
title: DOMAIN_TITLES[domainId],
actions: domainActions.map((a) => ({
id: a.id,
icon: a.icon,
label: a.label,
label: a.label + (a.experimental ? " ⚗️" : ""),
description: a.description,
callback: () => a.run(ctx),
})),

View file

@ -7,30 +7,44 @@ export interface ActionContext {
app: App;
editor: Editor | null;
activeFile: TFile | null;
selection: string; // 选中文字trim 后)
fileContent: string; // 当前文件全文
fileType: string | undefined; // frontmatter.type
filePath: string; // 当前文件路径
sectionName: string | null; // 光标所在 section 名
// file-menu 专用:右键的目标文件(可能不是当前打开的文件)
selection: string;
fileContent: string;
fileType: string | undefined;
filePath: string;
sectionName: string | null;
targetFile: TFile | null;
}
/** 可见性条件 */
export interface ActionWhen {
always?: boolean; // 始终可见
hasSelection?: boolean; // 需要有选中文字
noSelection?: boolean; // 需要没有选中文字
fileType?: string[]; // frontmatter type 匹配其一
filePath?: string; // 文件路径包含此字符串
inSection?: boolean; // 光标在某个 ## section 内
always?: boolean;
hasSelection?: boolean;
noSelection?: boolean;
fileType?: string[];
filePath?: string;
inSection?: boolean;
}
/** 动作出现的入口 */
export type ActionEntry = "panel" | "editor-menu" | "file-menu";
/** 面板分组 */
export type ActionGroup = "general" | "selection" | "edit" | "concept" | "reading" | "sync" | "document";
/** 三大产品域 */
export type ActionDomain = "knowledge" | "execution" | "auxiliary";
/** 细分领域(用于面板二级分组、设置定位等) */
export type ActionSection =
| "question"
| "concept"
| "reading"
| "retrieval"
| "debt"
| "plan"
| "scheduler"
| "logs"
| "sync"
| "assistant"
| "document"
| "settings";
/** 动作定义 */
export interface ActionDef {
@ -38,24 +52,33 @@ export interface ActionDef {
label: string;
icon: string;
description?: string;
group: ActionGroup;
domain: ActionDomain;
section: ActionSection;
when: ActionWhen;
showIn: ActionEntry[];
/** 操作风险 */
risk?: "none" | "low" | "medium" | "high";
/** 是否实验性功能 */
experimental?: boolean;
run: (ctx: ActionContext) => void;
}
/** 分组标题映射 */
export const GROUP_TITLES: Record<ActionGroup, string> = {
general: "通用",
selection: "选中文字",
edit: "编辑",
concept: "概念页",
reading: "阅读",
sync: "同步",
document: "文档工具",
// ── 兼容层:旧 group 映射到新 domain ──────────────────────────
// 保留旧类型名以便 registry 和 panel 平滑迁移
/** @deprecated Use ActionDomain + ActionSection */
export type ActionGroup = ActionDomain;
/** 域标题(面板一级分组) */
export const DOMAIN_TITLES: Record<ActionDomain, string> = {
knowledge: "知识",
execution: "执行",
auxiliary: "辅助",
};
/** 分组排序 */
export const GROUP_ORDER: ActionGroup[] = [
"general", "selection", "edit", "concept", "reading", "sync", "document",
];
/** 域排序 */
export const DOMAIN_ORDER: ActionDomain[] = ["knowledge", "execution", "auxiliary"];
// ── 向后兼容的 GROUP 导出registry.ts 还在用) ───────────────
export const GROUP_TITLES = DOMAIN_TITLES;
export const GROUP_ORDER = DOMAIN_ORDER;

View file

@ -208,6 +208,11 @@ export class KnowledgeIndexService {
return this.entries.size;
}
/** Get all entries as an array. */
getAll(): IndexEntry[] {
return [...this.entries.values()];
}
// ── Internals ──────────────────────────────────────────────
private indexFile(file: TFile): void {

View file

@ -0,0 +1,68 @@
import { ScheduleTrigger } from "./types";
/**
* Determine whether a task is due and calculate the next run time.
*/
export class NextRunCalculator {
/** Check if the task should fire now. */
isDue(trigger: ScheduleTrigger, lastRunAt: string | undefined, now: Date): boolean {
switch (trigger.type) {
case "on-startup":
// Fire once per session — if never run this session
return !lastRunAt;
case "interval": {
if (!lastRunAt) return true;
const elapsed = now.getTime() - new Date(lastRunAt).getTime();
return elapsed >= trigger.minutes * 60_000;
}
case "daily": {
const [h, m] = trigger.time.split(":").map(Number);
if (now.getHours() < h || (now.getHours() === h && now.getMinutes() < m)) return false;
if (!lastRunAt) return true;
const lastDate = new Date(lastRunAt).toDateString();
return lastDate !== now.toDateString();
}
case "weekly": {
if (now.getDay() !== trigger.weekday) return false;
const [h, m] = trigger.time.split(":").map(Number);
if (now.getHours() < h || (now.getHours() === h && now.getMinutes() < m)) return false;
if (!lastRunAt) return true;
const elapsed = now.getTime() - new Date(lastRunAt).getTime();
return elapsed >= 6 * 24 * 60 * 60_000; // at least 6 days since last
}
}
}
/** Calculate the next scheduled run (for display). */
getNextRun(trigger: ScheduleTrigger, now: Date): Date {
switch (trigger.type) {
case "on-startup":
return now; // always "next startup"
case "interval": {
return new Date(now.getTime() + trigger.minutes * 60_000);
}
case "daily": {
const [h, m] = trigger.time.split(":").map(Number);
const next = new Date(now);
next.setHours(h, m, 0, 0);
if (next <= now) next.setDate(next.getDate() + 1);
return next;
}
case "weekly": {
const [h, m] = trigger.time.split(":").map(Number);
const next = new Date(now);
const diff = (trigger.weekday - now.getDay() + 7) % 7 || 7;
next.setDate(now.getDate() + diff);
next.setHours(h, m, 0, 0);
if (next <= now) next.setDate(next.getDate() + 7);
return next;
}
}
}
}

View file

@ -0,0 +1,150 @@
import { Notice } from "obsidian";
import type DeepSeekPlugin from "../../main";
import { ScheduledTaskConfig, ScheduledTaskResult } from "./types";
import { NextRunCalculator } from "./NextRunCalculator";
import { PlanExecutor } from "../execution";
import { PlanBuilder } from "../execution";
import { KnowledgeIndexService } from "../knowledge";
import { todayIso } from "../schema";
/**
* ScheduledTaskRunner checks due tasks every 60s while Obsidian is open.
*
* Safety:
* - "notify-only": shows a Notice and optionally writes a report.
* - "create-plan-only": generates an ExecutionPlan draft but does NOT apply.
* - "auto-execute-low-risk": applies only if plan.riskLevel === "low".
*/
export class ScheduledTaskRunner {
private running = new Set<string>();
private calc = new NextRunCalculator();
private intervalId: number | null = null;
constructor(
private plugin: DeepSeekPlugin,
private tasks: ScheduledTaskConfig[]
) {}
start(): void {
// Check every 60 seconds
this.intervalId = window.setInterval(() => void this.tick(), 60_000);
this.plugin.registerInterval(this.intervalId);
// Immediate catch-up for on-startup tasks
void this.tick();
}
stop(): void {
if (this.intervalId !== null) {
window.clearInterval(this.intervalId);
this.intervalId = null;
}
}
private async tick(): Promise<void> {
const now = new Date();
for (const task of this.tasks) {
if (!task.enabled) continue;
if (this.running.has(task.id)) continue;
if (!this.calc.isDue(task.trigger, task.lastRunAt, now)) continue;
await this.runTask(task, now);
}
}
private async runTask(task: ScheduledTaskConfig, now: Date): Promise<void> {
this.running.add(task.id);
try {
const result = await this.executeTaskKind(task);
task.lastRunAt = now.toISOString();
task.nextRunAt = this.calc.getNextRun(task.trigger, now).toISOString();
if (result.success) {
new Notice(`${task.name}${result.message}`);
}
} catch (err) {
new Notice(`${task.name} 失败:${(err as Error).message}`);
} finally {
this.running.delete(task.id);
}
}
private async executeTaskKind(task: ScheduledTaskConfig): Promise<ScheduledTaskResult> {
const base: Omit<ScheduledTaskResult, "success" | "message"> = {
taskId: task.id,
ranAt: new Date().toISOString(),
};
switch (task.kind) {
case "knowledge-debt-scan":
return this.runDebtScan(task, base);
case "baidu-backup":
return this.runBaiduBackup(task, base);
case "question-graph-rebuild":
case "reading-review":
case "stale-draft-review":
case "custom":
// Placeholder — generate notice only
return { ...base, success: true, message: "已完成扫描(详细实现待补充)" };
}
}
private async runDebtScan(
task: ScheduledTaskConfig,
base: Omit<ScheduledTaskResult, "success" | "message">
): Promise<ScheduledTaskResult> {
const index = this.plugin.knowledgeIndex;
const emptyConcepts = index.getByType("concept").filter((e) => e.status === "empty" || e.status === "pending");
const total = emptyConcepts.length;
if (task.safety === "notify-only") {
return { ...base, success: true, message: `发现 ${total} 个空概念页` };
}
// create-plan-only / auto-execute-low-risk → write report
const reportContent = this.buildDebtReport(index);
const plan = new PlanBuilder("每日知识债务扫描", "assistant")
.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} 个空概念)` };
}
// Just persist the plan draft (user confirms later)
await new PlanExecutor(this.plugin.app).execute(plan);
return { ...base, success: true, message: `报告已生成(${total} 个空概念)` };
}
private async runBaiduBackup(
task: ScheduledTaskConfig,
base: Omit<ScheduledTaskResult, "success" | "message">
): Promise<ScheduledTaskResult> {
const cfg = this.plugin.settings.baiduSync;
if (!cfg.enabled || !cfg.accessToken) {
return { ...base, success: false, message: "百度同步未启用或未授权" };
}
// Delegate to existing sync service
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 ? "配置已备份" : "备份失败" };
}
private buildDebtReport(index: KnowledgeIndexService): string {
const empty = index.getByType("concept").filter((e) => e.status === "empty" || e.status === "pending");
const lines = [
`---\ntype: report\nreport_type: knowledge-debt\ncreated_at: ${todayIso()}\n---\n`,
`# 知识债务报告 ${todayIso()}\n`,
`## 空概念页 (${empty.length})\n`,
...empty.slice(0, 30).map((e) => `- [[${e.basename}]]${e.domain ? ` [${e.domain}]` : ""}`),
empty.length > 30 ? `\n...还有 ${empty.length - 30}\n` : "",
];
return lines.join("\n");
}
}

View file

@ -0,0 +1,9 @@
export { ScheduledTaskRunner } from "./ScheduledTaskRunner";
export { NextRunCalculator } from "./NextRunCalculator";
export type {
ScheduledTaskKind,
ScheduleTrigger,
ScheduledTaskSafety,
ScheduledTaskConfig,
ScheduledTaskResult,
} from "./types";

View file

@ -0,0 +1,48 @@
/**
* Scheduled task types for IStart-Note-AI.
*
* Tasks run ONLY while Obsidian is open and the plugin is enabled.
* If a scheduled run is missed, the runner offers catch-up on next launch.
*/
export type ScheduledTaskKind =
| "knowledge-debt-scan"
| "baidu-backup"
| "question-graph-rebuild"
| "reading-review"
| "stale-draft-review"
| "custom";
export type ScheduleTrigger =
| { type: "on-startup" }
| { type: "interval"; minutes: number }
| { type: "daily"; time: string } // "22:00" (HH:mm, local)
| { type: "weekly"; weekday: number; time: string }; // weekday: 0=Sun
export type ScheduledTaskSafety =
| "notify-only"
| "create-plan-only"
| "auto-execute-low-risk";
export interface ScheduledTaskConfig {
id: string;
name: string;
enabled: boolean;
kind: ScheduledTaskKind;
trigger: ScheduleTrigger;
safety: ScheduledTaskSafety;
lastRunAt?: string; // ISO
nextRunAt?: string; // ISO
scope?: {
paths?: string[];
types?: string[];
domains?: string[];
};
}
export interface ScheduledTaskResult {
taskId: string;
ranAt: string;
success: boolean;
message: string;
}

View file

@ -144,7 +144,7 @@ export class KnowledgeDebtModal extends Modal {
});
// Reading chapters: files in "Reading" path with status != completed
const unfinishedReadings = [...this.index["entries"].values()].filter((e) => {
const unfinishedReadings = this.index.getAll().filter((e) => {
return (
e.path.includes("Reading/") &&
e.type !== "domain-index" &&
@ -153,7 +153,7 @@ export class KnowledgeDebtModal extends Modal {
);
});
const staleNotes = [...this.index["entries"].values()].filter((e) => {
const staleNotes = this.index.getAll().filter((e) => {
return (
e.status === "draft" &&
now - e.mtime > NINETY_DAYS

View file

@ -28,11 +28,14 @@ import { SCHEMA_VERSION, todayIso } from "./core/schema";
import { KnowledgeIndexService } from "./core/knowledge";
import { PlanBuilder } from "./core/execution";
import { PlanExecutor } from "./core/execution";
import { ScheduledTaskRunner, ScheduledTaskConfig } from "./core/scheduler";
export default class DeepSeekPlugin extends Plugin {
settings!: DeepSeekSettings;
/** In-memory vault knowledge index, rebuilt on load, updated incrementally. */
knowledgeIndex!: KnowledgeIndexService;
/** Scheduled task runner — only active while Obsidian is open. */
private scheduler: ScheduledTaskRunner | null = null;
async onload() {
await this.loadSettings();
@ -41,6 +44,7 @@ export default class DeepSeekPlugin extends Plugin {
this.knowledgeIndex = new KnowledgeIndexService(this.app);
this.app.workspace.onLayoutReady(() => {
this.knowledgeIndex.rebuild();
this.startScheduler();
});
// Incremental updates
this.registerEvent(
@ -676,6 +680,32 @@ ${selection ? `用户当前选中的文字:\n${selection}\n` : ""}`;
.map((f) => f.basename);
}
// ── 定时任务 ─────────────────────────────────────────────────
private startScheduler(): void {
const defaultTasks: ScheduledTaskConfig[] = [
{
id: "daily-debt-scan",
name: "每日知识债务扫描",
enabled: false, // user must opt-in via settings
kind: "knowledge-debt-scan",
trigger: { type: "daily", time: "22:00" },
safety: "notify-only",
},
{
id: "daily-baidu-backup",
name: "每日百度备份",
enabled: this.settings.baiduSync.enabled && this.settings.baiduSync.autoBackup,
kind: "baidu-backup",
trigger: { type: "daily", time: "23:00" },
safety: "auto-execute-low-risk",
},
];
this.scheduler = new ScheduledTaskRunner(this, defaultTasks);
this.scheduler.start();
}
// ── 设置 ───────────────────────────────────────────────────
async loadSettings() {

View file

@ -1,9 +1,13 @@
import { App, PluginSettingTab, Setting } from "obsidian";
import { App, Notice, PluginSettingTab, Setting } from "obsidian";
import type DeepSeekPlugin from "../main";
import type { DeepSeekSettings } from "../types";
import { BaiduAuthModal } from "../features/sync/BaiduAuthModal";
type SettingsSection = "knowledge" | "execution" | "auxiliary";
export class DeepSeekSettingsTab extends PluginSettingTab {
private activeSection: SettingsSection = "auxiliary"; // start with AI setup
constructor(app: App, private plugin: DeepSeekPlugin) {
super(app, plugin);
}
@ -11,54 +15,190 @@ export class DeepSeekSettingsTab extends PluginSettingTab {
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl).setName("DeepSeek Knowledge Graph 设置").setHeading();
containerEl.addClass("istart-settings-root");
new Setting(containerEl)
.setName("API key")
.setDesc("DeepSeek API Key在 platform.deepseek.com 获取)")
.addText((text) =>
text
.setPlaceholder("sk-...")
// ── Header ───────────────────────────────────────────────
containerEl.createEl("h2", { text: "IStart-Note-AI" });
containerEl.createEl("p", {
text: "知识沉淀 · 执行计划 · 同步辅助",
attr: { style: "color: var(--text-muted); margin-top: -8px; margin-bottom: 16px;" },
});
// ── Layout ───────────────────────────────────────────────
const layout = containerEl.createDiv({ cls: "istart-settings-layout" });
const sidebar = layout.createDiv({ cls: "istart-settings-sidebar" });
const content = layout.createDiv({ cls: "istart-settings-content" });
this.renderNav(sidebar);
this.renderSection(content);
}
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: "🔧" },
];
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.addEventListener("click", () => {
this.activeSection = sec.id;
this.display();
});
}
}
private renderSection(container: HTMLElement): void {
switch (this.activeSection) {
case "knowledge":
this.renderKnowledge(container);
break;
case "execution":
this.renderExecution(container);
break;
case "auxiliary":
this.renderAuxiliary(container);
break;
}
}
// ══════════════════════════════════════════════════════════════
// 知识
// ══════════════════════════════════════════════════════════════
private renderKnowledge(el: HTMLElement): void {
new Setting(el).setName("知识路径").setHeading();
new Setting(el)
.setName("Q&A 笔记目录")
.addText((t) =>
t.setPlaceholder("Knowledge/Q&A").setValue(this.plugin.settings.savePath).onChange(async (v) => {
this.plugin.settings.savePath = v.trim() || "Knowledge/Q&A";
await this.plugin.saveSettings();
})
);
new Setting(el)
.setName("问题索引目录")
.addText((t) =>
t.setPlaceholder("Knowledge/Questions").setValue(this.plugin.settings.questionsIndexPath).onChange(async (v) => {
this.plugin.settings.questionsIndexPath = v.trim() || "Knowledge/Questions";
await this.plugin.saveSettings();
})
);
new Setting(el)
.setName("概念页目录")
.addText((t) =>
t.setPlaceholder("Knowledge/Concepts").setValue(this.plugin.settings.conceptsPath).onChange(async (v) => {
this.plugin.settings.conceptsPath = v.trim() || "Knowledge/Concepts";
await this.plugin.saveSettings();
})
);
// ── 知识索引 ──────────────────────────────────────────────
new Setting(el).setName("知识索引").setHeading();
const indexSize = this.plugin.knowledgeIndex?.size ?? 0;
new Setting(el)
.setName("索引状态")
.setDesc(`已索引 ${indexSize} 篇笔记`)
.addButton((btn) =>
btn.setButtonText("重建索引").onClick(() => {
this.plugin.knowledgeIndex.rebuild();
new Notice(`✅ 索引已重建:${this.plugin.knowledgeIndex.size}`);
this.display();
})
);
new Setting(el)
.setName("生成笔记后自动打开图谱")
.addToggle((t) =>
t.setValue(this.plugin.settings.autoOpenGraph).onChange(async (v) => {
this.plugin.settings.autoOpenGraph = v;
await this.plugin.saveSettings();
})
);
}
// ══════════════════════════════════════════════════════════════
// 执行
// ══════════════════════════════════════════════════════════════
private renderExecution(el: HTMLElement): void {
new Setting(el).setName("执行计划").setHeading();
el.createEl("p", {
text: "执行模块当前为实验阶段。所有 AI 生成的写入操作都会先生成计划,需要你确认后再执行。回滚功能尚未实现。",
attr: { style: "color: var(--text-muted); font-size: 13px; margin-bottom: 12px;" },
});
new Setting(el)
.setName("执行日志目录")
.setDesc("每次执行计划后自动生成日志")
.addText((t) =>
t.setPlaceholder("Knowledge/_Executions").setValue("Knowledge/_Executions").setDisabled(true)
);
new Setting(el).setName("定时任务").setHeading();
el.createEl("p", {
text: "定时任务即将推出v2.1)。支持每日知识债务扫描、自动百度备份、每周问题图谱重建。",
attr: { style: "color: var(--text-muted); font-size: 13px;" },
});
}
// ══════════════════════════════════════════════════════════════
// 辅助
// ══════════════════════════════════════════════════════════════
private renderAuxiliary(el: HTMLElement): void {
// ── AI 服务 ───────────────────────────────────────────────
new Setting(el).setName("AI 服务").setHeading();
new Setting(el)
.setName("API Key")
.setDesc("DeepSeek 或其他 OpenAI 兼容服务的密钥")
.addText((t) => {
t.setPlaceholder("sk-...")
.setValue(this.plugin.settings.apiKey)
.onChange(async (value) => {
this.plugin.settings.apiKey = value.trim();
.onChange(async (v) => {
this.plugin.settings.apiKey = v.trim();
await this.plugin.saveSettings();
})
);
});
t.inputEl.type = "password";
return t;
});
new Setting(containerEl)
new Setting(el)
.setName("Base URL")
.setDesc("API 地址,默认 https://api.deepseek.com")
.addText((text) =>
text
.setPlaceholder("https://api.deepseek.com")
.setValue(this.plugin.settings.baseUrl)
.onChange(async (value) => {
this.plugin.settings.baseUrl = value.trim() || "https://api.deepseek.com";
await this.plugin.saveSettings();
})
.setDesc("Chat completions 端点根地址")
.addText((t) =>
t.setPlaceholder("https://api.deepseek.com").setValue(this.plugin.settings.baseUrl).onChange(async (v) => {
this.plugin.settings.baseUrl = v.trim() || "https://api.deepseek.com";
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
new Setting(el)
.setName("模型")
.setDesc("选择使用的 DeepSeek 模型")
.addDropdown((drop) =>
drop
.addOption("deepseek-v4-flash", "deepseek-v4-flash快速推荐")
.addDropdown((d) =>
d.addOption("deepseek-v4-flash", "deepseek-v4-flash快速")
.addOption("deepseek-v4-pro", "deepseek-v4-pro深度推理")
.setValue(this.plugin.settings.model)
.onChange(async (value: string) => {
this.plugin.settings.model = value as "deepseek-v4-flash" | "deepseek-v4-pro";
.onChange(async (v: string) => {
this.plugin.settings.model = v as "deepseek-v4-flash" | "deepseek-v4-pro";
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
new Setting(el)
.setName("输出风格")
.setDesc("AI 生成内容的默认风格")
.addDropdown((drop) =>
drop
.addOption("knowledge-base", "知识库(推荐)")
.addDropdown((d) =>
d.addOption("knowledge-base", "知识库(推荐)")
.addOption("technical", "技术文档")
.addOption("minimal", "极简")
.addOption("product", "产品设计")
@ -66,67 +206,16 @@ export class DeepSeekSettingsTab extends PluginSettingTab {
.addOption("story", "世界观/叙事")
.addOption("dashboard", "卡片化")
.setValue(this.plugin.settings.outputStyle)
.onChange(async (value: string) => {
this.plugin.settings.outputStyle = value as DeepSeekSettings["outputStyle"];
.onChange(async (v: string) => {
this.plugin.settings.outputStyle = v as DeepSeekSettings["outputStyle"];
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("笔记保存路径")
.setDesc("Q&A 笔记存储目录(相对于 Vault 根目录)")
.addText((text) =>
text
.setPlaceholder("Knowledge/Q&A")
.setValue(this.plugin.settings.savePath)
.onChange(async (value) => {
this.plugin.settings.savePath = value.trim() || "Knowledge/Q&A";
await this.plugin.saveSettings();
})
);
// ── 百度网盘同步 ──────────────────────────────────────────
new Setting(el).setName("百度网盘同步").setHeading();
new Setting(containerEl)
.setName("问题索引路径")
.setDesc("问题图谱索引页存储目录")
.addText((text) =>
text
.setPlaceholder("Knowledge/Questions")
.setValue(this.plugin.settings.questionsIndexPath)
.onChange(async (value) => {
this.plugin.settings.questionsIndexPath = value.trim() || "Knowledge/Questions";
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("概念页保存路径")
.setDesc("概念页存储目录(相对于 Vault 根目录)")
.addText((text) =>
text
.setPlaceholder("Knowledge/Concepts")
.setValue(this.plugin.settings.conceptsPath)
.onChange(async (value) => {
this.plugin.settings.conceptsPath = value.trim() || "Knowledge/Concepts";
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("自动打开 Graph view")
.setDesc("生成笔记后自动打开图谱视图")
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.autoOpenGraph)
.onChange(async (value) => {
this.plugin.settings.autoOpenGraph = value;
await this.plugin.saveSettings();
})
);
// ── 百度云同步 ──────────────────────────────────────────
new Setting(containerEl).setName("百度网盘同步").setHeading();
new Setting(containerEl)
new Setting(el)
.setName("启用百度云同步")
.addToggle((t) =>
t.setValue(this.plugin.settings.baiduSync.enabled).onChange(async (v) => {
@ -137,142 +226,100 @@ export class DeepSeekSettingsTab extends PluginSettingTab {
);
if (this.plugin.settings.baiduSync.enabled) {
new Setting(containerEl)
.setName("App ID")
.setDesc("百度开放平台应用的 App Key")
.addText((text) =>
text
.setPlaceholder("your-app-id")
.setValue(this.plugin.settings.baiduSync.appId)
.onChange(async (v) => {
this.plugin.settings.baiduSync.appId = v.trim();
await this.plugin.saveSettings();
})
);
this.renderBaiduSyncSettings(el);
}
new Setting(containerEl)
.setName("App secret")
.setDesc("百度开放平台应用的 Secret Key")
.addText((text) => {
text
.setPlaceholder("your-app-secret")
.setValue(this.plugin.settings.baiduSync.appSecret)
.onChange((v) => {
this.plugin.settings.baiduSync.appSecret = v.trim();
void this.plugin.saveSettings();
});
text.inputEl.type = "password";
return text;
// ── 隐私与诊断 ───────────────────────────────────────────
new Setting(el).setName("隐私与诊断").setHeading();
el.createEl("p", {
text: "AI 功能会把选中内容和部分上下文发送到所配置的 API 端点。同步功能会把笔记上传到你自己的百度网盘。完整说明见 PRIVACY.md。",
attr: { style: "color: var(--text-muted); font-size: 13px;" },
});
}
private renderBaiduSyncSettings(el: HTMLElement): void {
const cfg = this.plugin.settings.baiduSync;
new Setting(el)
.setName("App ID")
.addText((t) =>
t.setPlaceholder("your-app-id").setValue(cfg.appId).onChange(async (v) => {
cfg.appId = v.trim();
await this.plugin.saveSettings();
})
);
new Setting(el)
.setName("App Secret")
.addText((t) => {
t.setPlaceholder("your-app-secret").setValue(cfg.appSecret).onChange((v) => {
cfg.appSecret = v.trim();
void this.plugin.saveSettings();
});
const tokenStatus = this.plugin.settings.baiduSync.accessToken
? `已授权(过期时间:${this.plugin.settings.baiduSync.tokenExpiresAt?.slice(0, 10) ?? "未知"}`
: "未授权";
new Setting(containerEl)
.setName("授权状态")
.setDesc(tokenStatus)
.addButton((btn) =>
btn.setButtonText("重新授权").onClick(() => {
new BaiduAuthModal(
this.app,
this.plugin.settings.baiduSync,
(accessToken, refreshToken, expiresAt) => {
this.plugin.settings.baiduSync.accessToken = accessToken;
this.plugin.settings.baiduSync.refreshToken = refreshToken;
this.plugin.settings.baiduSync.tokenExpiresAt = expiresAt;
void this.plugin.saveSettings().then(() => this.display());
}
).open();
})
);
new Setting(containerEl)
.setName("远端备份路径")
.setDesc("百度网盘中的备份根目录")
.addText((text) =>
text
.setPlaceholder("/apps/istart-note-ai")
.setValue(this.plugin.settings.baiduSync.remotePath)
.onChange(async (v) => {
this.plugin.settings.baiduSync.remotePath = v.trim() || "/apps/istart-note-ai";
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("自动备份")
.setDesc("每次生成笔记后自动备份到百度云")
.addToggle((t) =>
t.setValue(this.plugin.settings.baiduSync.autoBackup).onChange(async (v) => {
this.plugin.settings.baiduSync.autoBackup = v;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("备份插件本身")
.setDesc("备份时包含插件文件main.js、配置等方便换设备恢复")
.addToggle((t) =>
t.setValue(this.plugin.settings.baiduSync.backupPlugin).onChange(async (v) => {
this.plugin.settings.baiduSync.backupPlugin = v;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("忽略规则")
.setDesc("正则表达式,匹配的文件路径将被跳过")
.addText((text) =>
text
.setPlaceholder("node_modules|.git")
.setValue(this.plugin.settings.baiduSync.ignorePattern)
.onChange(async (v) => {
this.plugin.settings.baiduSync.ignorePattern = v.trim();
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("单文件大小限制MB")
.addText((text) =>
text
.setPlaceholder("100")
.setValue(String(this.plugin.settings.baiduSync.fileSizeLimitMB))
.onChange(async (v) => {
const n = parseInt(v);
if (!isNaN(n) && n > 0) {
this.plugin.settings.baiduSync.fileSizeLimitMB = n;
await this.plugin.saveSettings();
}
})
);
// 配置同步
new Setting(containerEl).setName("配置同步").setHeading();
containerEl.createEl("p", {
text: "将路径、模型等偏好设置同步到百度云,在多台设备间共享(不含 API Key 和 Token 等凭证)。",
cls: "istart-settings-config-hint",
t.inputEl.type = "password";
return t;
});
new Setting(containerEl)
.setName("推送配置到百度云")
.setDesc("将当前设置上传,其他设备可拉取同步")
.addButton((btn) =>
btn.setButtonText("推送").onClick(async () => {
await this.plugin.pushConfig();
})
);
const tokenStatus = cfg.accessToken
? `已授权(过期:${cfg.tokenExpiresAt?.slice(0, 10) ?? "未知"}`
: "未授权";
new Setting(containerEl)
.setName("从百度云拉取配置")
.setDesc("拉取最新配置并应用(不覆盖凭证)")
.addButton((btn) =>
btn.setButtonText("拉取").onClick(async () => {
await this.plugin.pullConfig();
this.display();
})
);
}
new Setting(el)
.setName("授权状态")
.setDesc(tokenStatus)
.addButton((btn) =>
btn.setButtonText("授权").onClick(() => {
new BaiduAuthModal(this.app, cfg, (accessToken, refreshToken, expiresAt) => {
cfg.accessToken = accessToken;
cfg.refreshToken = refreshToken;
cfg.tokenExpiresAt = expiresAt;
void this.plugin.saveSettings().then(() => this.display());
}).open();
})
);
new Setting(el)
.setName("远端路径")
.addText((t) =>
t.setPlaceholder("/apps/istart-note-ai").setValue(cfg.remotePath).onChange(async (v) => {
cfg.remotePath = v.trim() || "/apps/istart-note-ai";
await this.plugin.saveSettings();
})
);
new Setting(el)
.setName("自动备份")
.setDesc("生成笔记后自动备份")
.addToggle((t) => t.setValue(cfg.autoBackup).onChange(async (v) => { cfg.autoBackup = v; await this.plugin.saveSettings(); }));
new Setting(el)
.setName("备份插件本身")
.addToggle((t) => t.setValue(cfg.backupPlugin).onChange(async (v) => { cfg.backupPlugin = v; await this.plugin.saveSettings(); }));
new Setting(el)
.setName("忽略规则")
.setDesc("正则表达式")
.addText((t) =>
t.setPlaceholder("node_modules|.git").setValue(cfg.ignorePattern).onChange(async (v) => {
cfg.ignorePattern = v.trim();
await this.plugin.saveSettings();
})
);
new Setting(el)
.setName("单文件大小限制MB")
.addText((t) =>
t.setPlaceholder("100").setValue(String(cfg.fileSizeLimitMB)).onChange(async (v) => {
const n = parseInt(v);
if (!isNaN(n) && n > 0) { cfg.fileSizeLimitMB = n; await this.plugin.saveSettings(); }
})
);
// 配置同步
new Setting(el).setName("跨设备配置同步").setDesc("不含凭证").setHeading();
new Setting(el)
.addButton((btn) => btn.setButtonText("推送配置").onClick(() => void this.plugin.pushConfig()))
.addButton((btn) => btn.setButtonText("拉取配置").onClick(async () => { await this.plugin.pullConfig(); this.display(); }));
}
}

View file

@ -1,5 +1,62 @@
/* IStart-Note-AI Plugin Styles */
/* ── Settings Page Layout ────────────────────────── */
.istart-settings-layout {
display: grid;
grid-template-columns: 160px minmax(0, 1fr);
gap: 20px;
}
.istart-settings-sidebar {
border-right: 1px solid var(--background-modifier-border);
padding-right: 12px;
}
.istart-settings-content {
min-width: 0;
}
.istart-settings-nav-item {
display: block;
width: 100%;
padding: 8px 10px;
border-radius: 6px;
color: var(--text-normal);
cursor: pointer;
margin-bottom: 4px;
font-size: 14px;
}
.istart-settings-nav-item:hover {
background: var(--background-modifier-hover);
}
.istart-settings-nav-item.is-active {
background: var(--background-modifier-hover);
color: var(--text-accent);
font-weight: 600;
}
@media (max-width: 700px) {
.istart-settings-layout {
display: block;
}
.istart-settings-sidebar {
border-right: none;
border-bottom: 1px solid var(--background-modifier-border);
margin-bottom: 12px;
padding-bottom: 8px;
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.istart-settings-nav-item {
display: inline-block;
width: auto;
}
}
/* ── Baidu Sync View ─────────────────────────────── */
.istart-sync-root {

View file

@ -23,5 +23,6 @@
"1.8.1": "1.7.2",
"1.8.2": "1.7.2",
"1.8.3": "1.7.2",
"1.9.0": "1.7.2"
"1.9.0": "1.7.2",
"2.0.0": "1.7.2"
}