From 7b26626ccb9920397279b53dd1f2ebd20cfa9af9 Mon Sep 17 00:00:00 2001 From: Yan Date: Wed, 20 May 2026 16:06:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(2.0.0):=20three-module=20architecture=20?= =?UTF-8?q?=E2=80=94=20knowledge/execution/auxiliary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M1: README正式化 + badges + 三模块叙事 + 状态修正 + 版本对齐2.0.0 M2: 设置页改为侧边导航(知识/执行/辅助) + 分区布局CSS M3: ActionDomain(knowledge/execution/auxiliary) 替代旧ActionGroup + 面板三栏 M4: Scheduler MVP — ScheduledTaskRunner + NextRunCalculator + 知识债务扫描 + 百度备份任务 KnowledgeIndexService 新增 getAll() 公开方法 KnowledgeDebtModal 改用公开API --- README.md | 231 +++++---- README.zh-CN.md | 227 ++++----- manifest.json | 2 +- package.json | 2 +- src/actions/definitions.ts | 136 ++--- src/actions/registry.ts | 16 +- src/actions/types.ts | 79 +-- src/core/knowledge/KnowledgeIndexService.ts | 5 + src/core/scheduler/NextRunCalculator.ts | 68 +++ src/core/scheduler/ScheduledTaskRunner.ts | 150 ++++++ src/core/scheduler/index.ts | 9 + src/core/scheduler/types.ts | 48 ++ src/features/dashboard/KnowledgeDebtModal.ts | 4 +- src/main.ts | 30 ++ src/settings/SettingsTab.ts | 491 ++++++++++--------- styles.css | 57 +++ versions.json | 3 +- 17 files changed, 1023 insertions(+), 535 deletions(-) create mode 100644 src/core/scheduler/NextRunCalculator.ts create mode 100644 src/core/scheduler/ScheduledTaskRunner.ts create mode 100644 src/core/scheduler/index.ts create mode 100644 src/core/scheduler/types.ts diff --git a/README.md b/README.md index 1058e26..ad0281c 100644 --- a/README.md +++ b/README.md @@ -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. +

+ Turn your Obsidian vault into a knowledge-to-action system. +

-[简体中文 README →](./README.zh-CN.md) +

+ 简体中文 · + Quick Start · + Privacy · + Roadmap +

-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. +

+ Version + License + CI + Obsidian +

--- -## 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 `/.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 `/.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 diff --git a/README.zh-CN.md b/README.zh-CN.md index 5409b7a..d983bbd 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,181 +1,182 @@ # IStart-Note-AI -> [!warning] 测试版软件 -> IStart-Note-AI 仍在持续开发中。下文标记为**实验中**的功能可能在没有提前通知的情况下变更或移除。Frontmatter schema 尚未稳定,尝试新功能前请先备份你的 Vault。 +

+ 把 Obsidian 笔记变成"知识 → 执行"的个人系统。 +

-[English README →](./README.md) +

+ English · + 快速开始 · + 隐私说明 · + 路线图 +

-把笔记升级成可被结构化、可被检索、可被执行的个人知识系统的 Obsidian 插件。统一的 AI 助手帮你起草、扩写、组织笔记,并自动维护概念页、双链、阅读项目,可选搭配百度云同步。 - -底层使用 OpenAI 兼容的 chat completions 接口,默认接入 [DeepSeek](https://platform.deepseek.com),更换 Base URL 可使用其他兼容服务。 +

+ Version + License + CI + Obsidian +

--- -## 功能状态总览 +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`,放到 `/.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)。 ## 协议 diff --git a/manifest.json b/manifest.json index 2f6e35a..fda0216 100644 --- a/manifest.json +++ b/manifest.json @@ -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", diff --git a/package.json b/package.json index 63cd877..7f5dee3 100644 --- a/package.json +++ b/package.json @@ -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, diff --git a/src/actions/definitions.ts b/src/actions/definitions.ts index e27c4ef..2b79a23 100644 --- a/src/actions/definitions.ts +++ b/src/actions/definitions.ts @@ -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(); }, + }, ]; diff --git a/src/actions/registry.ts b/src/actions/registry.ts index 0c542c2..db6ddfd 100644 --- a/src/actions/registry.ts +++ b/src/actions/registry.ts @@ -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), })), diff --git a/src/actions/types.ts b/src/actions/types.ts index b9aabe3..8d9d29c 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -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 = { - general: "通用", - selection: "选中文字", - edit: "编辑", - concept: "概念页", - reading: "阅读", - sync: "同步", - document: "文档工具", +// ── 兼容层:旧 group 映射到新 domain ────────────────────────── +// 保留旧类型名以便 registry 和 panel 平滑迁移 + +/** @deprecated Use ActionDomain + ActionSection */ +export type ActionGroup = ActionDomain; + +/** 域标题(面板一级分组) */ +export const DOMAIN_TITLES: Record = { + 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; diff --git a/src/core/knowledge/KnowledgeIndexService.ts b/src/core/knowledge/KnowledgeIndexService.ts index 88b46be..b18f5fe 100644 --- a/src/core/knowledge/KnowledgeIndexService.ts +++ b/src/core/knowledge/KnowledgeIndexService.ts @@ -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 { diff --git a/src/core/scheduler/NextRunCalculator.ts b/src/core/scheduler/NextRunCalculator.ts new file mode 100644 index 0000000..0983b7a --- /dev/null +++ b/src/core/scheduler/NextRunCalculator.ts @@ -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; + } + } + } +} diff --git a/src/core/scheduler/ScheduledTaskRunner.ts b/src/core/scheduler/ScheduledTaskRunner.ts new file mode 100644 index 0000000..5b3341e --- /dev/null +++ b/src/core/scheduler/ScheduledTaskRunner.ts @@ -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(); + 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 { + 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 { + 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 { + const base: Omit = { + 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 + ): Promise { + 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 + ): Promise { + 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"); + } +} diff --git a/src/core/scheduler/index.ts b/src/core/scheduler/index.ts new file mode 100644 index 0000000..7aceaec --- /dev/null +++ b/src/core/scheduler/index.ts @@ -0,0 +1,9 @@ +export { ScheduledTaskRunner } from "./ScheduledTaskRunner"; +export { NextRunCalculator } from "./NextRunCalculator"; +export type { + ScheduledTaskKind, + ScheduleTrigger, + ScheduledTaskSafety, + ScheduledTaskConfig, + ScheduledTaskResult, +} from "./types"; diff --git a/src/core/scheduler/types.ts b/src/core/scheduler/types.ts new file mode 100644 index 0000000..00bceff --- /dev/null +++ b/src/core/scheduler/types.ts @@ -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; +} diff --git a/src/features/dashboard/KnowledgeDebtModal.ts b/src/features/dashboard/KnowledgeDebtModal.ts index 84d3c8b..af81315 100644 --- a/src/features/dashboard/KnowledgeDebtModal.ts +++ b/src/features/dashboard/KnowledgeDebtModal.ts @@ -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 diff --git a/src/main.ts b/src/main.ts index 0d21442..09b19df 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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() { diff --git a/src/settings/SettingsTab.ts b/src/settings/SettingsTab.ts index d98a77d..14cdde5 100644 --- a/src/settings/SettingsTab.ts +++ b/src/settings/SettingsTab.ts @@ -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(); })); } } diff --git a/styles.css b/styles.css index 7d91ad2..80f2fb2 100644 --- a/styles.css +++ b/styles.css @@ -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 { diff --git a/versions.json b/versions.json index 93696ea..d36b966 100644 --- a/versions.json +++ b/versions.json @@ -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" }