From 0e8af720ab7ff00a34059ae3f9337c05cd1d69ee Mon Sep 17 00:00:00 2001 From: yan Date: Sat, 16 May 2026 23:48:46 +0800 Subject: [PATCH] release: 1.8.0 --- .github/workflows/release.yml | 16 +- Readme.md | 416 +++++++++-------------- esbuild.config.mjs | 4 +- manifest.json | 2 +- package.json | 3 +- src/actions/definitions.ts | 12 + src/ai/AIAssistant.ts | 233 +++++++------ src/ai/classifier/ContentClassifier.ts | 68 ++++ src/ai/formatter/MarkdownBeautifier.ts | 187 ++++++++++ src/ai/formatter/ReadableLayoutEngine.ts | 107 ++++++ src/ai/prompt/StructuredPromptBuilder.ts | 104 ++++++ src/ai/templates/index.ts | 217 ++++++++++++ src/ai/templates/types.ts | 37 ++ src/main.ts | 34 +- src/settings/SettingsTab.ts | 20 ++ src/types.ts | 2 + versions.json | 3 +- 17 files changed, 1091 insertions(+), 374 deletions(-) create mode 100644 src/ai/classifier/ContentClassifier.ts create mode 100644 src/ai/formatter/MarkdownBeautifier.ts create mode 100644 src/ai/formatter/ReadableLayoutEngine.ts create mode 100644 src/ai/prompt/StructuredPromptBuilder.ts create mode 100644 src/ai/templates/index.ts create mode 100644 src/ai/templates/types.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fdae394..6dd9629 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,6 +10,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + id-token: write + attestations: write steps: - uses: actions/checkout@v4 @@ -23,16 +25,22 @@ jobs: npm install npm run build + - name: Generate artifact attestations + uses: actions/attest-build-provenance@v2 + with: + subject-path: | + dist/main.js + dist/manifest.json + dist/styles.css + - name: Create release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | tag="${GITHUB_REF#refs/tags/}" - # Upload main.js and manifest.json from dist/ - # Also upload styles.css if it exists assets="dist/main.js dist/manifest.json" - if [ -f "styles.css" ]; then - assets="$assets styles.css" + if [ -f "dist/styles.css" ]; then + assets="$assets dist/styles.css" fi gh release create "$tag" \ --title="$tag" \ diff --git a/Readme.md b/Readme.md index d6f5395..3905156 100644 --- a/Readme.md +++ b/Readme.md @@ -1,243 +1,152 @@ # IStart-Note-AI -Generate structured knowledge notes from questions and selected text using DeepSeek AI, with automatic concept pages, bidirectional links, and a question graph. +AI-powered knowledge management plugin for Obsidian. One unified AI assistant that helps you build structured notes, read books effectively, and visualize knowledge — all through natural language. + +--- + +## Core Concept + +**One input, infinite possibilities.** Instead of memorizing dozens of commands, just tell the AI what you want: + +- Select text → click 🧠 → "画个时序图" +- Cursor in empty section → click 🧠 → (leave blank, AI auto-completes) +- Reading a chapter → click 🧠 → "总结这章" + +The AI understands your context (selected text, current file, cursor position) and acts accordingly. --- ## Features -| Feature | Description | -|---------|-------------| -| Ask a question | Input a question, get a DeepSeek answer, and generate a structured note automatically. | -| Context Q&A | Select text in any note, ask a question based on it, and generate a note with source reference and backlink. | -| Question classification | Automatically classify questions as new, refinement, or expansion, and link them to related questions. | -| Concept completion | Fill in empty concept pages on demand, with light or standard depth. | -| Section append | Add more items to any existing section (e.g. Examples) without overwriting existing content. | -| Batch scan | Scan all empty concept pages in the vault and complete them in bulk. | -| Question index | Automatically maintain a question graph index page. | -| Baidu Cloud sync | Incremental backup and restore of vault files to Baidu Netdisk. | +| Feature | How to use | +|---------|-----------| +| **AI Assistant** | 🧠 button or right-click → type any instruction | +| **Reading Projects** | Command panel → "New reading project" → enter book title | +| **Baidu Cloud Sync** | Settings → enable sync → backup/restore/force-overwrite | +| **Knowledge Graph** | Automatic: concepts, relations, and Mermaid diagrams | + +### AI Assistant (unified entry) + +The AI assistant handles everything through one input: + +- **Expand** — select text, ask to expand +- **Explain** — select a term, ask to explain +- **Diagrams** — describe what you want (flowchart, sequence, state, class, ER, Gantt) +- **Formulas** — describe a math expression, get LaTeX +- **Complete** — fill empty sections based on context +- **Continue** — write more from cursor position +- **Summarize** — summarize the current document +- **Answer questions** — ask anything about the content +- **Anything else** — just describe it in natural language + +### Reading Projects + +Turn any book into a structured study plan: + +1. Enter book title (and optionally paste table of contents) +2. AI generates: + - Reading roadmap with chapter relationships (Mermaid) + - Pre-reading questions for each chapter + - Core concepts (auto-linked to concept pages) +3. As you read, record notes in chapter pages +4. Generate chapter summaries and take Feynman tests + +**Supports resume:** If generation is interrupted, re-run to complete missing chapters. + +### Knowledge Structure + +``` +Knowledge/ +├── Reading/ +│ └── Book-Title/ +│ ├── _索引.md ← Overview + progress + relationship graph +│ ├── Chapter-1.md ← Notes with pre-reading questions +│ └── ... +├── Concepts/ +│ ├── _未分类/ ← New concepts (before completion) +│ ├── 技术/ ← Auto-organized by domain after completion +│ │ ├── TCP.md +│ │ └── _索引.md ← Domain MOC with Mermaid overview +│ └── ... +└── Q&A/ + └── 2026-05-01-question.md +``` + +### Baidu Cloud Sync + +- **Incremental backup** — only uploads changed files +- **Bidirectional sync** — with conflict resolution +- **Force overwrite** — reset local to match cloud state +- **Plugin backup** — includes plugin files + Obsidian config (toolbar, hotkeys, appearance) +- **Auto backup** — triggers after note generation --- ## Requirements -- Obsidian 1.4.0 or later. -- A [DeepSeek API key](https://platform.deepseek.com). +- Obsidian 1.7.2 or later +- A [DeepSeek API key](https://platform.deepseek.com) --- ## Installation -### From the community plugin directory (recommended) +### From community plugins (recommended) -1. Open Obsidian settings → Community plugins → Browse. -2. Search for **IStart-Note-AI**. -3. Click Install, then Enable. +1. Settings → Community plugins → Browse +2. Search **IStart-Note-AI** +3. Install → Enable -### Manual installation +### Manual -1. Build the plugin (see [Development](#development)). -2. Copy the contents of `dist/` to your vault's `.obsidian/plugins/istart-note-ai/` folder. -3. Enable the plugin in Obsidian settings → Community plugins. +1. Build: `npm install && npm run build` +2. Copy `dist/` contents to `.obsidian/plugins/istart-note-ai/` +3. Enable in Settings → Community plugins --- ## Configuration -Open Obsidian settings → IStart-Note-AI. +Settings → IStart-Note-AI: | Setting | Description | Default | |---------|-------------|---------| -| API key | Your DeepSeek API key from [platform.deepseek.com](https://platform.deepseek.com). | — | -| Base URL | API endpoint. | `https://api.deepseek.com` | -| Model | `deepseek-chat` or `deepseek-reasoner`. | `deepseek-chat` | -| Q&A folder | Folder for question-and-answer notes. | `Knowledge/Q&A` | -| Concepts folder | Folder for concept pages. | `Knowledge/Concepts` | -| Questions folder | Folder for the question graph index. | `Knowledge/Questions` | -| Open graph view | Open the graph view automatically after generating a note. | Off | - -The plugin creates these folders automatically on first use. +| API Key | DeepSeek API key | — | +| Base URL | API endpoint | `https://api.deepseek.com` | +| Model | `deepseek-v4-flash` (fast) or `deepseek-v4-pro` (deep reasoning) | `deepseek-v4-flash` | +| Q&A folder | Where Q&A notes are saved | `Knowledge/Q&A` | +| Concepts folder | Where concept pages are saved | `Knowledge/Concepts` | +| Questions folder | Question graph index | `Knowledge/Questions` | --- ## Usage -### Ask a question +### Desktop -- Hotkey: `Cmd/Ctrl + Shift + D` -- Ribbon icon (brain icon in the left sidebar) -- Command palette: `Ask DeepSeek and generate a knowledge note` +- **🧠 Ribbon icon** → Opens command panel +- **Right-click in editor** → "IStart-Note-AI: AI 助手" +- **Right-click file in sidebar** → "IStart-Note-AI: AI 助手" +- **Command palette** → Search any command -After submitting a question, the plugin: +### Mobile -1. Calls DeepSeek and generates an answer. -2. Shows a classification dialog to confirm the question type (new, refinement, or expansion). -3. Creates a structured Markdown note with Answer, Concepts, Relations, and suggested follow-up questions. -4. Creates empty concept pages for all extracted concepts. -5. Updates the question index page. +- **🧠 Ribbon icon** → Opens command panel (recommended) +- Add `AI 助手` to mobile toolbar for quick access +- Select text → tap toolbar button → enter instruction -### Context Q&A +### Quick tags -1. Select any text in a note. -2. Right-click → **IStart-Note-AI: Ask based on selection**, or use `Cmd/Ctrl + Shift + Q`. -3. Enter your question in the dialog. -4. The generated note includes the source quote and a backlink is added to the original note. +The AI assistant input has quick tags for common actions: -### Section append +`[扩写]` `[解释]` `[画图]` `[补全]` `[续写]` `[总结]` `[公式]` `[时序图]` -Add more items to any section that already has content: - -1. Place the cursor inside a section (e.g. below `## Examples`). -2. Right-click → **IStart-Note-AI: Append to "Examples"**, or use the command palette: `Append content to current section`. -3. Choose how many items to generate (2, 3, 5, or 8). -4. Review the preview, then confirm to append or regenerate. - -DeepSeek reads the existing section content as context and avoids duplicating items. - -### Concept completion - -**Single page:** - -- Open a concept page → command palette: `Complete current concept page`. -- Select `[[concept name]]` in the editor → right-click → `IStart-Note-AI: Complete concept "..."`. -- Right-click any `.md` file in the file list → `IStart-Note-AI: Complete this concept page`. - -**Batch:** - -- Command palette: `Scan empty concept pages`. -- Select up to 5 pages, choose a depth, and confirm. - -Completion depth options: - -- **Light**: Definition and related concepts. -- **Standard**: Definition, explanation, examples, related concepts, and related questions. - -All generated content is shown in a preview before being written to the file. - -### Question index - -- Command palette: `Open question index`. -- The index is updated automatically after each question. - ---- - -## Baidu Netdisk sync - -### Setup - -1. Create an app at [Baidu Open Platform](https://pan.baidu.com/union/home) and get your App Key and Secret Key. -2. Open plugin settings → Baidu Netdisk sync → enable it. -3. Fill in App ID and App Secret. -4. Click "Re-authorize" to complete the OAuth flow. - -### Backup - -- Command palette: `Baidu Netdisk sync / backup`. -- Choose "Backup to Baidu Cloud", optionally specify a subfolder. -- Only files newer than the remote copy are uploaded (incremental). -- Hidden files, files matching the ignore pattern, and files exceeding the size limit are skipped. - -### Restore - -- Command palette: `Baidu Netdisk sync / backup`. -- Choose "Restore from Baidu Cloud". -- Toggle "Overwrite local files" to control whether existing local files are replaced. - -### Auto backup - -Enable "Auto backup" in settings to automatically back up the note folder after each Q&A or context Q&A note is generated. - -### Settings - -| Setting | Description | Default | -|---------|-------------|---------| -| App ID | Baidu Open Platform App Key. | — | -| App Secret | Baidu Open Platform Secret Key. | — | -| Remote path | Root folder in Baidu Netdisk. | `/apps/istart-note-ai` | -| Auto backup | Back up automatically after each note is generated. | Off | -| Ignore pattern | Regex pattern for paths to skip. | — | -| File size limit | Maximum file size to upload (MB). | `100` | - ---- - -## Note structure - -### Q&A note - -```markdown ---- -type: question -question: What are the five elements? -category: new -parent: null -related: [] -concepts: [Five elements, Wood, Fire, Earth, Metal, Water] -status: linked -created_at: 2026-04-25 ---- - -# What are the five elements? - -## Question -## Answer -## Concepts -## Relations -## Tags -## Suggested questions -### Refinement -### Expansion -``` - -### Context Q&A note - -```markdown -# Why does yin-yang balance affect system stability? - -## Source -> Yin-yang balance determines system stability. - -Source: [[path/to/original-note]] - -## Question -## Answer -## Concepts -## Relations -## Suggested questions -## Tags -``` - -### Concept page - -```markdown ---- -type: concept -name: Five elements -status: completed -completion_status: completed -created_from: Q&A -created_at: 2026-04-25 -updated_at: 2026-04-25 ---- - -# Five elements - -## Definition -## Explanation -## Examples -## Related concepts -## Related questions -## Sources -``` +Tap a tag to fill the instruction, or type your own. --- ## Development -### Requirements - -- Node.js 16 or later. -- npm 8 or later. - ### Setup ```bash @@ -248,85 +157,82 @@ npm install ### Build ```bash -npm run dev # Watch mode, outputs to dist/main.js -npm run build # Production build, outputs to dist/ +npm run build # Production → dist/ +npm run dev # Watch mode ``` -### Project structure +### Project Structure ``` src/ -├── main.ts # Plugin entry point: commands, menus, settings -├── types.ts # Shared type definitions -├── DeepSeekClient.ts # API client for standard Q&A -├── ContextQAClient.ts # API client for context-aware Q&A -├── VaultWriter.ts # Note creation and backlink management -├── QuestionModal.ts # Question input dialog -├── ContextQAModal.ts # Context Q&A input dialog -├── QuestionClassifier.ts # Question classification (new / refinement / expansion) -├── QuestionClassifyModal.ts # Classification confirmation dialog -├── QuestionGraphManager.ts # Question graph: frontmatter, index page, suggestions -├── ConceptCompleter.ts # API client for concept completion -├── ConceptPageManager.ts # Concept page analysis, incremental write, batch scan -├── ConceptCompletionModal.ts # Depth selection, preview, and batch scan dialogs -├── SectionAppender.ts # Section extraction, append generation, and write -├── SectionAppendModal.ts # Count selection and preview dialogs -├── BaiduPanClient.ts # Baidu Netdisk REST API client (OAuth, upload, download) -├── BaiduSyncService.ts # Incremental backup and restore logic -├── BaiduAuthModal.ts # OAuth authorization dialog -├── BaiduSyncModal.ts # Sync operation dialog (backup / restore) -└── SettingsTab.ts # Settings tab UI +├── main.ts # Plugin entry (minimal: onload + method implementations) +├── types.ts # Shared type definitions +├── actions/ # Action registry (defines all commands/menus) +│ ├── types.ts +│ ├── definitions.ts # All actions defined here +│ └── registry.ts # Auto-registers to commands/menus/panel +├── ai/ # AI clients (pure API calls, no UI) +│ ├── AIAssistant.ts # Unified AI assistant +│ ├── ReadingPlanner.ts # Reading project generation +│ ├── ConceptCompleter.ts # Concept page completion +│ ├── SmartCompleter.ts # Section/expand/continue +│ ├── DiagramGenerator.ts # Mermaid/LaTeX generation +│ ├── SectionAppender.ts # Section content generation +│ ├── DeepSeekClient.ts # Basic Q&A +│ ├── ContextQAClient.ts # Context-aware Q&A +│ └── QuestionClassifier.ts # Question classification +├── features/ # Feature modules (UI + logic) +│ ├── assistant/ # Unified AI assistant modal +│ ├── reading/ # Reading project management +│ ├── concept/ # Concept page management +│ ├── question/ # Question graph +│ ├── context-qa/ # Context Q&A modal +│ ├── section/ # Section append modal +│ ├── diagram/ # Diagram type/preview modals +│ ├── sync/ # Baidu cloud sync +│ ├── smart-complete/ # Document analysis modal +│ └── command-panel/ # Unified command panel +├── vault/ # Vault file operations +│ └── VaultWriter.ts +├── settings/ # Settings tab +│ └── SettingsTab.ts +└── util/ + └── md5.ts ``` -### Extending the plugin +### Adding a new feature -- To add a new AI feature, follow the pattern in `ContextQAClient.ts`: implement an `ask()` method that returns structured JSON. -- To add a command, call `this.addCommand()` in `main.ts` `onload()`. -- To add a context menu item, add `menu.addItem()` inside the `editor-menu` or `file-menu` event listener in `main.ts`. -- To change a note template, edit `buildNoteContent()` or `buildContextNoteContent()` in `VaultWriter.ts`. -- To change a prompt, edit the prompt constant in the corresponding client file. +1. Add AI client in `src/ai/` (if needed) +2. Add UI in `src/features/your-feature/` +3. Add action in `src/actions/definitions.ts` +4. Done — automatically appears in command panel, right-click menu, and command palette --- ## Changelog -### 1.5.0 +### 2.0.0 -- Added Baidu Netdisk sync: incremental backup and restore. -- OAuth authorization flow with token auto-refresh. -- Auto backup after note generation (optional). -- Ignore pattern and file size limit support. +- **Unified AI Assistant** — one input replaces all previous commands +- **Action Registry** — consistent behavior across panel, right-click, and commands +- **DeepSeek v4 models** — switched to `deepseek-v4-flash` and `deepseek-v4-pro` +- **Reading Projects** — book study with pre-reading questions, chapter summaries, Feynman tests +- **Domain organization** — concepts auto-organized into domain subdirectories +- **Mermaid diagrams** — auto-generated relationship graphs in concept pages +- **Obsidian config backup** — toolbar, hotkeys, appearance synced to cloud +- **Force overwrite** — reset local state from cloud backup +- **Code restructure** — modular architecture for easy extension -### 1.4.0 +### 1.5.x -- Added section append: add more items to any existing section without overwriting content. -- Context menu automatically detects the section at the cursor position. -- Generated items avoid duplicating existing content. - -### 1.3.0 - -- Added context Q&A: ask questions based on selected text. -- Context Q&A includes source quote, backlink to the original note. -- Renamed plugin to IStart-Note-AI. - -### 1.2.0 - -- Added question graph: automatic classification (new, refinement, expansion). -- Added question index page. -- Added suggested follow-up questions. - -### 1.1.0 - -- Added concept completion (light and standard depth). -- Added batch scan for empty concept pages. -- Added preview dialog before writing. -- Added context menu support. +- Baidu Netdisk sync with incremental backup/restore +- Section append, concept completion, context Q&A +- Question classification and graph ### 1.0.0 -- Basic Q&A note generation. -- Automatic concept page creation and bidirectional links. -- DeepSeek API configuration. +- Basic Q&A note generation with DeepSeek +- Automatic concept pages and bidirectional links --- diff --git a/esbuild.config.mjs b/esbuild.config.mjs index f72cc37..3b66d35 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -1,6 +1,6 @@ import esbuild from "esbuild"; import process from "process"; -import builtins from "builtin-modules"; +import { builtinModules } from "module"; import { copyFileSync, mkdirSync } from "fs"; const banner = `/* @@ -29,7 +29,7 @@ const context = await esbuild.context({ "@lezer/common", "@lezer/highlight", "@lezer/lr", - ...builtins, + ...builtinModules, ], format: "cjs", target: "es2018", diff --git a/manifest.json b/manifest.json index ce8bafe..91d68bc 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "istart-note-ai", "name": "IStart-Note-AI", - "version": "1.7.8", + "version": "1.8.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 24d43fc..6250073 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "istart-note-ai", - "version": "1.7.8", + "version": "1.8.0", "description": "IStart-Note-AI: DeepSeek-powered knowledge graph plugin for Obsidian", "main": "main.js", "scripts": { @@ -17,7 +17,6 @@ "@types/node": "^16.11.6", "@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/parser": "5.29.0", - "builtin-modules": "3.3.0", "esbuild": "0.17.3", "obsidian": "latest", "tslib": "2.4.0", diff --git a/src/actions/definitions.ts b/src/actions/definitions.ts index 6be0812..5f25475 100644 --- a/src/actions/definitions.ts +++ b/src/actions/definitions.ts @@ -40,4 +40,16 @@ export const ALL_ACTIONS: ActionDef[] = [ showIn: ["panel"], run: (ctx) => { ctx.plugin.openBaiduSyncModal(); }, }, + + // ── 美化当前文档 ────────────────────────────────────────── + { + id: "beautify-note", + label: "美化当前文档", + icon: "wand", + description: "整理结构、插入 Callout、生成双链", + group: "general", + when: { always: true }, + showIn: ["panel", "editor-menu", "file-menu"], + run: (ctx) => { void ctx.plugin.beautifyCurrentNote(); }, + }, ]; diff --git a/src/ai/AIAssistant.ts b/src/ai/AIAssistant.ts index 1668751..c424c75 100644 --- a/src/ai/AIAssistant.ts +++ b/src/ai/AIAssistant.ts @@ -1,76 +1,116 @@ import { requestUrl } from "obsidian"; import { DeepSeekSettings } from "../types"; +import { ContentClassifier } from "./classifier/ContentClassifier"; +import { StructuredPromptBuilder } from "./prompt/StructuredPromptBuilder"; +import { MarkdownBeautifier } from "./formatter/MarkdownBeautifier"; +import { ReadableLayoutEngine } from "./formatter/ReadableLayoutEngine"; +import { OutputStyle } from "./templates/types"; /** 用户请求的上下文 */ export interface AssistantContext { - selection: string; // 选中文字 - fileContent: string; // 当前文件全文 - fileName: string; // 文件名(不含扩展名) - fileType: string | undefined; // frontmatter.type - cursorLineBefore: string; // 光标前的最后 500 字(用于续写) - sectionName: string | null; // 光标所在 section - sectionEmpty: boolean; // 该 section 是否为空 + selection: string; + fileContent: string; + fileName: string; + fileType: string | undefined; + cursorLineBefore: string; + sectionName: string | null; + sectionEmpty: boolean; } /** AI 返回的结果 */ export interface AssistantResult { - mode: "insert" | "replace" | "append" | "show"; // 如何处理结果 - content: string; // 生成的 Markdown 内容 - explanation?: string; // 简短说明 + mode: "insert" | "replace" | "append" | "show"; + content: string; + explanation?: string; } -const SYSTEM_PROMPT = `你是 Obsidian 笔记插件的 AI 助手。用户会给你一段指令(或不给)和当前上下文,你需要智能理解意图并生成对应的 Markdown 内容。 - -## 你能做什么 - -- **扩写/改写**:基于用户选中的文字,扩展或改写 -- **续写**:在光标位置继续写 -- **补全**:填充空章节(## 标题下没有内容的区域) -- **解释**:解释选中的概念、术语、代码 -- **回答问题**:如果用户指令是个问题,直接回答 -- **画图**:生成 Mermaid 图表(流程图、时序图、状态图、类图、架构图、ER图、甘特图) -- **数学公式**:用 LaTeX 生成公式(\`$$...$$\` 包裹) -- **总结/摘要**:对文件或选中内容做总结 -- **分析文档**:找出文档缺失部分并建议补充 - -## 输出规则 - -严格返回 JSON 格式,不要其他内容: -\`\`\`json -{ - "mode": "insert | replace | append | show", - "content": "生成的 Markdown 内容", - "explanation": "一句话说明做了什么" -} -\`\`\` - -**mode 说明:** -- \`replace\`:替换用户选中的文字(扩写/改写/翻译时用) -- \`insert\`:在光标位置插入新内容(续写/补全 section/插入图表时用) -- \`append\`:追加到文件末尾(总结/分析时用) -- \`show\`:仅展示给用户看,不修改文件(回答问题/解释时用) - -**Mermaid 图表必须用代码块包裹:** -\`\`\`mermaid -... -\`\`\` - -**数学公式用 \`$$...$$\` 包裹。** - -## 智能判断(用户没给指令时) - -- 有选中文字 → 扩写 -- 光标在空 section 内 → 补全该 section -- 否则 → 续写`; - +/** + * 统一 AI 助手 + * 流程:分类 → 构建 Prompt → 调用 AI → 美化输出 + */ export class AIAssistant { - constructor(private settings: DeepSeekSettings) {} + private classifier: ContentClassifier; + private promptBuilder: StructuredPromptBuilder; + + constructor( + private settings: DeepSeekSettings, + private outputStyle: OutputStyle = "knowledge-base", + private knownConcepts: string[] = [] + ) { + this.classifier = new ContentClassifier(); + this.promptBuilder = new StructuredPromptBuilder(outputStyle); + } async run(instruction: string, ctx: AssistantContext): Promise { if (!this.settings.apiKey) throw new Error("请先配置 API Key"); - const userPrompt = this.buildUserPrompt(instruction, ctx); + // 1. 分类内容类型 + const contentType = this.classifier.classify({ + instruction, + fileName: ctx.fileName, + filePath: ctx.fileName, // 简化:用 fileName 代替完整路径 + fileType: ctx.fileType, + selection: ctx.selection, + }); + // 2. 构建结构化 prompt + const systemPrompt = this.promptBuilder.buildSystemPrompt(contentType, this.outputStyle); + const userPrompt = this.promptBuilder.buildUserPrompt(instruction, ctx); + + // 3. 追加输出模式指令 + const modeInstruction = this.buildModeInstruction(ctx); + const fullUserPrompt = userPrompt + "\n\n" + modeInstruction; + + // 4. 调用 AI + const raw = await this.callAPI(systemPrompt, fullUserPrompt); + + // 5. 解析模式 + const { mode, content } = this.parseResponse(raw, ctx); + + // 6. 美化输出 + const beautifier = new MarkdownBeautifier(this.knownConcepts); + const beautified = beautifier.beautify(content); + + // 7. 布局优化 + const layoutEngine = new ReadableLayoutEngine(); + const final = layoutEngine.optimize(beautified); + + return { + mode, + content: final, + explanation: this.generateExplanation(mode, contentType), + }; + } + + /** 美化已有文档(不调用 AI,只做格式化) */ + beautifyContent(content: string): string { + const beautifier = new MarkdownBeautifier(this.knownConcepts); + const beautified = beautifier.beautify(content); + const layoutEngine = new ReadableLayoutEngine(); + return layoutEngine.optimize(beautified); + } + + private buildModeInstruction(ctx: AssistantContext): string { + const parts: string[] = ["【输出模式判断】"]; + + if (ctx.selection) { + parts.push("有选中内容。如果指令是改写/扩写/翻译,输出应替换选中内容(在第一行写 ``)。"); + parts.push("如果是生成新内容(图表、解释等),输出应插入到选中内容下方(在第一行写 ``)。"); + } else if (ctx.sectionEmpty && ctx.sectionName) { + parts.push(`光标在空章节"${ctx.sectionName}"内。输出应填充该章节(在第一行写 \`\`)。`); + } else if (ctx.cursorLineBefore) { + parts.push("无选中内容。输出应插入到光标位置(在第一行写 ``)。"); + } else { + parts.push("输出追加到文件末尾(在第一行写 ``)。"); + } + + parts.push("如果只是回答问题/解释(不需要修改文件),在第一行写 ``。"); + parts.push("\n注意:mode 注释必须是输出的第一行,后面紧跟正文内容。"); + + return parts.join("\n"); + } + + private async callAPI(systemPrompt: string, userPrompt: string): Promise { const res = await requestUrl({ url: `${this.settings.baseUrl}/v1/chat/completions`, method: "POST", @@ -81,7 +121,7 @@ export class AIAssistant { body: JSON.stringify({ model: this.settings.model, messages: [ - { role: "system", content: SYSTEM_PROMPT }, + { role: "system", content: systemPrompt }, { role: "user", content: userPrompt }, ], temperature: 0.5, @@ -89,64 +129,41 @@ export class AIAssistant { throw: false, }); - if (res.status !== 200) { - throw new Error(`API 错误: ${res.status} - ${res.text}`); - } - - const raw = res.json.choices?.[0]?.message?.content ?? ""; - return this.parse(raw); + if (res.status !== 200) throw new Error(`API 错误: ${res.status}`); + return res.json.choices?.[0]?.message?.content ?? ""; } - private buildUserPrompt(instruction: string, ctx: AssistantContext): string { - const parts: string[] = []; + private parseResponse(raw: string, ctx: AssistantContext): { mode: AssistantResult["mode"]; content: string } { + // 提取 mode 注释 + const modeMatch = raw.match(/^\s*\n?/); + let mode: AssistantResult["mode"]; + let content: string; - // 指令 - if (instruction.trim()) { - parts.push(`【用户指令】\n${instruction.trim()}`); + if (modeMatch) { + mode = modeMatch[1] as AssistantResult["mode"]; + content = raw.slice(modeMatch[0].length).trim(); } else { - parts.push(`【用户指令】\n(未指定,请根据上下文智能判断)`); + // 没有 mode 标记,根据上下文推断 + if (ctx.selection) { + mode = "replace"; + } else if (ctx.sectionEmpty) { + mode = "insert"; + } else { + mode = "insert"; + } + content = raw.trim(); } - // 上下文 - parts.push(`\n【当前文件】${ctx.fileName}${ctx.fileType ? ` (type: ${ctx.fileType})` : ""}`); - - if (ctx.selection) { - parts.push(`\n【选中文字】\n${ctx.selection}`); - } - - if (ctx.sectionName) { - parts.push(`\n【光标所在章节】${ctx.sectionName}${ctx.sectionEmpty ? "(空)" : ""}`); - } - - if (!ctx.selection && ctx.cursorLineBefore) { - parts.push(`\n【光标前内容(用于续写参考)】\n${ctx.cursorLineBefore.slice(-500)}`); - } - - // 文件全文(截断) - if (ctx.fileContent && ctx.fileContent.length < 2000) { - parts.push(`\n【文件全文】\n${ctx.fileContent}`); - } else if (ctx.fileContent) { - parts.push(`\n【文件全文(截取)】\n${ctx.fileContent.slice(0, 2000)}\n...(省略)`); - } - - return parts.join("\n"); + return { mode, content }; } - private parse(raw: string): AssistantResult { - const jsonMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/) || raw.match(/(\{[\s\S]*\})/); - const jsonStr = jsonMatch ? jsonMatch[1] : raw; - - try { - const p = JSON.parse(jsonStr.trim()) as Record; - const mode = (p.mode as AssistantResult["mode"]) || "show"; - return { - mode: ["insert", "replace", "append", "show"].includes(mode) ? mode : "show", - content: (p.content as string) || raw, - explanation: (p.explanation as string) || undefined, - }; - } catch { - // 解析失败,当作 show 模式展示 - return { mode: "show", content: raw }; - } + private generateExplanation(mode: AssistantResult["mode"], contentType: string): string { + const modeLabels: Record = { + replace: "替换选中内容", + insert: "插入到光标位置", + append: "追加到文件末尾", + show: "仅展示", + }; + return `${modeLabels[mode]}(${contentType})`; } } diff --git a/src/ai/classifier/ContentClassifier.ts b/src/ai/classifier/ContentClassifier.ts new file mode 100644 index 0000000..5109c03 --- /dev/null +++ b/src/ai/classifier/ContentClassifier.ts @@ -0,0 +1,68 @@ +import { ContentType } from "../templates/types"; + +/** + * 内容类型分类器 + * 根据用户输入、文件路径、文件类型等信息,判断应使用哪个模板。 + */ +export class ContentClassifier { + /** + * 分类:返回最匹配的内容类型 + */ + classify(input: { + instruction: string; + fileName: string; + filePath: string; + fileType?: string; + selection?: string; + }): ContentType { + const { instruction, fileName, filePath, fileType, selection } = input; + const text = `${instruction} ${fileName} ${selection ?? ""}`.toLowerCase(); + + // 1. 基于 frontmatter type 直接判断 + if (fileType === "reading-note" || fileType === "reading-project") return ContentType.READING_NOTE; + if (fileType === "concept") return ContentType.CONCEPT; + if (fileType === "question") return ContentType.QA; + + // 2. 基于文件路径 + if (filePath.includes("Reading/")) return ContentType.READING_NOTE; + if (filePath.includes("Concepts/")) return ContentType.CONCEPT; + if (filePath.includes("Q&A/")) return ContentType.QA; + + // 3. 基于指令关键词 + if (this.matchKeywords(text, ["架构", "系统设计", "方案设计", "技术选型", "微服务", "分布式"])) { + return ContentType.ARCHITECTURE; + } + if (this.matchKeywords(text, ["产品", "需求", "用户故事", "PRD", "功能设计", "交互"])) { + return ContentType.PRODUCT_DESIGN; + } + if (this.matchKeywords(text, ["会议", "讨论", "决策", "行动项", "周会", "复盘"])) { + return ContentType.MEETING_NOTE; + } + if (this.matchKeywords(text, ["计划", "排期", "里程碑", "任务", "sprint", "迭代"])) { + return ContentType.TASK_PLAN; + } + if (this.matchKeywords(text, ["世界观", "设定", "种族", "魔法", "规则", "历史线"])) { + return ContentType.WORLD_BUILDING; + } + if (this.matchKeywords(text, ["总结", "阅读", "读书", "章节", "笔记"])) { + return ContentType.READING_NOTE; + } + if (this.matchKeywords(text, ["概念", "定义", "解释", "是什么"])) { + return ContentType.CONCEPT; + } + if (this.matchKeywords(text, ["代码", "实现", "API", "SDK", "配置", "部署", "debug", "bug"])) { + return ContentType.TECH_DOC; + } + + // 4. 默认:如果有问号,当作 QA + if (instruction.includes("?") || instruction.includes("?")) { + return ContentType.QA; + } + + return ContentType.UNKNOWN; + } + + private matchKeywords(text: string, keywords: string[]): boolean { + return keywords.some((kw) => text.includes(kw.toLowerCase())); + } +} diff --git a/src/ai/formatter/MarkdownBeautifier.ts b/src/ai/formatter/MarkdownBeautifier.ts new file mode 100644 index 0000000..8de816e --- /dev/null +++ b/src/ai/formatter/MarkdownBeautifier.ts @@ -0,0 +1,187 @@ +/** + * Markdown 美化器 + * 对 AI 输出(或任何 Markdown 内容)进行后处理,提升可读性。 + */ +export class MarkdownBeautifier { + private knownConcepts: Set; + + constructor(knownConcepts?: string[]) { + this.knownConcepts = new Set(knownConcepts ?? []); + } + + /** 执行所有美化步骤 */ + beautify(content: string): string { + let result = content; + result = this.splitLongParagraphs(result); + result = this.convertCallouts(result); + result = this.insertVisualBreaks(result); + result = this.autoLinkConcepts(result); + result = this.cleanupSpacing(result); + return result; + } + + /** + * 1. 长段落拆分 + * 超过 4 行的连续文本段落,在句号处拆分 + */ + private splitLongParagraphs(content: string): string { + const lines = content.split("\n"); + const result: string[] = []; + let paragraphLines: string[] = []; + + const flushParagraph = () => { + if (paragraphLines.length <= 4) { + result.push(...paragraphLines); + } else { + // 合并后按句号拆分 + const text = paragraphLines.join("\n"); + const sentences = text.split(/(?<=[。!?.!?])\s*/); + let chunk: string[] = []; + let lineCount = 0; + for (const sentence of sentences) { + chunk.push(sentence); + lineCount += Math.ceil(sentence.length / 40); // 估算行数 + if (lineCount >= 3) { + result.push(chunk.join("")); + result.push(""); + chunk = []; + lineCount = 0; + } + } + if (chunk.length > 0) result.push(chunk.join("")); + } + paragraphLines = []; + }; + + for (const line of lines) { + const isStructural = /^(#|>|\||-|\d+\.|```|---|\s*$)/.test(line); + if (isStructural) { + flushParagraph(); + result.push(line); + } else { + paragraphLines.push(line); + } + } + flushParagraph(); + + return result.join("\n"); + } + + /** + * 2. 自动 Callout 转换 + * 识别特定模式并转为 Obsidian Callout + */ + private convertCallouts(content: string): string { + let result = content; + + // 模式:风险:xxx / 注意:xxx / 警告:xxx + result = result.replace( + /^(风险|注意|警告|⚠️)[::]\s*(.+)$/gm, + "> [!warning] $1\n> $2" + ); + + // 模式:建议:xxx / 技巧:xxx / 提示:xxx + result = result.replace( + /^(建议|技巧|提示|💡)[::]\s*(.+)$/gm, + "> [!tip] $1\n> $2" + ); + + // 模式:总结:xxx / 结论:xxx / 摘要:xxx + result = result.replace( + /^(总结|结论|摘要|核心)[::]\s*(.+)$/gm, + "> [!summary] $1\n> $2" + ); + + // 模式:示例:xxx / 例如:xxx + result = result.replace( + /^(示例|例如|例子)[::]\s*(.+)$/gm, + "> [!example] $1\n> $2" + ); + + // 模式:决策:xxx + result = result.replace( + /^(决策|决定)[::]\s*(.+)$/gm, + "> [!abstract] $1\n> $2" + ); + + return result; + } + + /** + * 3. 插入视觉断点 + * 每 250-350 字(约 3-4 段)如果没有标题/callout/分隔线,插入分隔 + */ + private insertVisualBreaks(content: string): string { + const lines = content.split("\n"); + const result: string[] = []; + let charsSinceBreak = 0; + + for (const line of lines) { + const isBreak = /^(#{1,6}\s|>|---|\||\s*$|```mermaid)/.test(line); + + if (isBreak) { + charsSinceBreak = 0; + } else { + charsSinceBreak += line.length; + } + + result.push(line); + + // 如果累积超过 300 字且下一行不是结构性元素,插入空行 + if (charsSinceBreak > 300 && !isBreak) { + result.push(""); + charsSinceBreak = 0; + } + } + + return result.join("\n"); + } + + /** + * 4. 自动双链 + * 将已知概念转为 [[双链]] 格式 + */ + private autoLinkConcepts(content: string): string { + if (this.knownConcepts.size === 0) return content; + + let result = content; + + // 按长度降序排列,避免短词匹配到长词的子串 + const sorted = [...this.knownConcepts].sort((a, b) => b.length - a.length); + + for (const concept of sorted) { + if (concept.length < 2) continue; // 跳过太短的 + + // 不在以下位置替换:已有双链内、代码块内、标题内、Mermaid 内 + const regex = new RegExp( + `(? { + if (line.startsWith("```")) inCodeBlock = !inCodeBlock; + if (inCodeBlock) return line; + if (/^#/.test(line)) return line; // 跳过标题 + if (/^>/.test(line)) return line.replace(regex, `[[${concept}]]`); // callout 内也替换 + return line.replace(regex, `[[${concept}]]`); + }).join("\n"); + } + + return result; + } + + /** + * 5. 清理多余空行 + */ + private cleanupSpacing(content: string): string { + // 连续 3 个以上空行合并为 2 个 + return content.replace(/\n{4,}/g, "\n\n\n"); + } + + private escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + } +} diff --git a/src/ai/formatter/ReadableLayoutEngine.ts b/src/ai/formatter/ReadableLayoutEngine.ts new file mode 100644 index 0000000..e54a5dd --- /dev/null +++ b/src/ai/formatter/ReadableLayoutEngine.ts @@ -0,0 +1,107 @@ +/** + * 可读性布局引擎 + * 控制信息密度、节奏、视觉层级。 + * 在 MarkdownBeautifier 之后运行,做最终的布局优化。 + */ +export class ReadableLayoutEngine { + /** + * 优化文档布局 + */ + optimize(content: string): string { + let result = content; + result = this.enforceHeadingDepth(result); + result = this.ensureTLDR(result); + result = this.balanceDensity(result); + result = this.addSectionSpacing(result); + return result; + } + + /** + * 标题深度控制 + * 禁止超过 H3,H4+ 转为加粗或 Callout + */ + private enforceHeadingDepth(content: string): string { + const lines = content.split("\n"); + return lines.map((line) => { + // H4 → 加粗段落 + if (/^####\s+(.+)/.test(line)) { + return `**${line.replace(/^####\s+/, "")}**`; + } + // H5/H6 → 加粗 + if (/^#{5,6}\s+(.+)/.test(line)) { + return `**${line.replace(/^#{5,6}\s+/, "")}**`; + } + return line; + }).join("\n"); + } + + /** + * 确保文档顶部有 TLDR/摘要 + * 如果文档有 H1 但没有紧跟 summary callout,不强制添加(由 AI 生成) + * 这里只检查格式:如果有 > [!summary] 确保它在顶部 + */ + private ensureTLDR(content: string): string { + const lines = content.split("\n"); + + // 找到 summary callout 的位置 + const summaryIdx = lines.findIndex((l) => /^>\s*\[!summary\]/.test(l)); + if (summaryIdx <= 0) return content; // 没有或已在顶部 + + // 找到第一个 H1 的位置 + const h1Idx = lines.findIndex((l) => /^#\s+/.test(l)); + if (h1Idx < 0) return content; + + // 如果 summary 不在 H1 后面的前 3 行内,移动它 + if (summaryIdx > h1Idx + 3) { + // 提取 summary 块(可能多行) + const summaryLines: string[] = []; + let i = summaryIdx; + while (i < lines.length && (lines[i].startsWith(">") || lines[i].trim() === "")) { + if (lines[i].startsWith(">")) summaryLines.push(lines[i]); + else if (summaryLines.length > 0) break; + i++; + } + + // 从原位置删除 + lines.splice(summaryIdx, summaryLines.length); + + // 插入到 H1 后面 + lines.splice(h1Idx + 1, 0, "", ...summaryLines, ""); + } + + return lines.join("\n"); + } + + /** + * 平衡信息密度 + * 如果连续多个列表项(>8),插入分组标记 + */ + private balanceDensity(content: string): string { + const lines = content.split("\n"); + const result: string[] = []; + let consecutiveListItems = 0; + + for (const line of lines) { + if (/^[-*]\s/.test(line)) { + consecutiveListItems++; + // 每 6 个列表项后插入空行(视觉分组) + if (consecutiveListItems > 0 && consecutiveListItems % 6 === 0) { + result.push(""); + } + } else { + consecutiveListItems = 0; + } + result.push(line); + } + + return result.join("\n"); + } + + /** + * 章节间距 + * 确保每个 ## 标题前有足够空行 + */ + private addSectionSpacing(content: string): string { + return content.replace(/([^\n])\n(##\s)/g, "$1\n\n$2"); + } +} diff --git a/src/ai/prompt/StructuredPromptBuilder.ts b/src/ai/prompt/StructuredPromptBuilder.ts new file mode 100644 index 0000000..2c98369 --- /dev/null +++ b/src/ai/prompt/StructuredPromptBuilder.ts @@ -0,0 +1,104 @@ +import { ContentType, NoteTemplate, OutputStyle } from "../templates/types"; +import { TEMPLATES } from "../templates/index"; +import { AssistantContext } from "../AIAssistant"; + +/** 风格修饰指令 */ +const STYLE_MODIFIERS: Record = { + "minimal": "极简风格:只保留核心信息,去除所有冗余。段落极短,大量留白。", + "technical": "技术文档风格:精确、严谨、代码友好。使用专业术语,标注版本和兼容性。", + "product": "产品文档风格:面向用户,清晰的步骤和截图描述位。强调用户价值。", + "academic": "学术风格:严谨论证,引用来源,使用学术术语。逻辑链条完整。", + "knowledge-base": "知识库风格:模块化、可检索、强链接。每个段落独立可理解。", + "story": "叙事风格:有故事感和沉浸感,但保持结构化。适合世界观和设定文档。", + "dashboard": "卡片化风格:信息密度高,使用大量 Callout 作为信息卡片。每个知识点一个 Callout。适合快速扫描。结构示例:\n> [!abstract] 概览\n> 内容\n\n> [!info] 要点 1\n> 内容\n\n> [!tip] 要点 2\n> 内容", +}; + +/** + * 结构化 Prompt 构建器 + * 根据内容类型 + 输出风格 + 上下文,生成高质量的 system prompt + */ +export class StructuredPromptBuilder { + constructor( + private defaultStyle: OutputStyle = "knowledge-base" + ) {} + + /** + * 构建完整的 system prompt + */ + buildSystemPrompt(contentType: ContentType, style?: OutputStyle): string { + const template = TEMPLATES[contentType] || TEMPLATES[ContentType.UNKNOWN]; + const effectiveStyle = style || this.defaultStyle; + + const parts: string[] = []; + + // 1. 角色定义 + parts.push(template.systemPrompt); + + // 2. 风格修饰 + parts.push(`\n输出风格:${STYLE_MODIFIERS[effectiveStyle]}`); + + // 3. 格式规则 + parts.push("\n## 格式规则(必须遵守)\n"); + parts.push(template.formattingRules.map((r, i) => `${i + 1}. ${r}`).join("\n")); + + // 4. Callout 规则 + if (template.calloutRules.length > 0) { + parts.push("\n## Callout 使用规则\n"); + parts.push(template.calloutRules.map((r) => `- ${r}`).join("\n")); + } + + // 5. Mermaid 规则 + if (template.mermaidRules.length > 0) { + parts.push("\n## Mermaid 图表规则\n"); + parts.push(template.mermaidRules.map((r) => `- ${r}`).join("\n")); + } + + // 6. 结构模板(如果有) + if (template.markdownStructure.trim()) { + parts.push("\n## 推荐文档结构\n"); + parts.push("根据内容需要选择合适的章节(不必全部使用):"); + parts.push(template.markdownStructure); + } + + // 7. 输出格式 + parts.push(`\n## 输出要求\n`); + parts.push("直接输出 Markdown 内容,不要包裹在 JSON 或代码块中。"); + parts.push("不要输出任何解释性前缀(如「以下是...」),直接输出正文。"); + + return parts.join("\n"); + } + + /** + * 构建 user prompt(包含上下文) + */ + buildUserPrompt(instruction: string, ctx: AssistantContext): string { + const parts: string[] = []; + + if (instruction.trim()) { + parts.push(`【指令】${instruction.trim()}`); + } else { + parts.push("【指令】(未指定,请根据上下文智能判断并生成结构化内容)"); + } + + parts.push(`\n【文件】${ctx.fileName}${ctx.fileType ? ` (type: ${ctx.fileType})` : ""}`); + + if (ctx.selection) { + parts.push(`\n【选中内容】\n${ctx.selection}`); + } + + if (ctx.sectionName) { + parts.push(`\n【当前章节】${ctx.sectionName}${ctx.sectionEmpty ? "(空,需要补全)" : ""}`); + } + + if (!ctx.selection && ctx.cursorLineBefore) { + parts.push(`\n【光标前内容】\n${ctx.cursorLineBefore.slice(-500)}`); + } + + if (ctx.fileContent) { + const content = ctx.fileContent.length > 2000 ? ctx.fileContent.slice(0, 2000) + "\n...(省略)" : ctx.fileContent; + parts.push(`\n【文件全文】\n${content}`); + } + + return parts.join("\n"); + } +} diff --git a/src/ai/templates/index.ts b/src/ai/templates/index.ts new file mode 100644 index 0000000..4659ace --- /dev/null +++ b/src/ai/templates/index.ts @@ -0,0 +1,217 @@ +import { ContentType, NoteTemplate } from "./types"; + +const COMMON_FORMATTING = [ + "单段不超过 4 行", + "不允许连续长篇散文", + "优先结构化表达(列表、表格、Callout)", + "每个章节必须有视觉断点", + "重要结论使用 > [!summary] Callout", + "风险/注意事项使用 > [!warning] Callout", + "建议/技巧使用 > [!tip] Callout", + "示例使用 > [!example] Callout", + "对比内容使用表格", + "识别到的专业术语/概念用 [[双链]] 包裹", +]; + +const COMMON_MERMAID = [ + "如果内容涉及流程,自动生成 Mermaid flowchart", + "如果涉及调用关系/交互,生成 sequence diagram", + "如果涉及状态变化,生成 state diagram", + "如果涉及数据结构/实体关系,生成 erDiagram", + "如果涉及模块/类关系,生成 class diagram", + "Mermaid 节点 ID 用英文,标签用中文方括号:A[\"中文\"]", +]; + +export const TEMPLATES: Record = { + [ContentType.TECH_DOC]: { + type: ContentType.TECH_DOC, + name: "技术文档", + systemPrompt: "你正在生成技术文档风格的 Obsidian 笔记。内容应精确、结构清晰、适合工程师快速扫描。", + markdownStructure: ` +## 摘要(TLDR) +## 背景 +## 核心原理 +## 实现方案 +## 关键代码/配置 +## 注意事项 +## 相关概念 +## 参考`, + calloutRules: [ + "API 变更/Breaking Change 使用 > [!warning]", + "最佳实践使用 > [!tip]", + "代码示例使用 > [!example]", + ], + mermaidRules: COMMON_MERMAID, + formattingRules: [...COMMON_FORMATTING, "代码块标注语言", "命令行用 ```bash"], + }, + + [ContentType.ARCHITECTURE]: { + type: ContentType.ARCHITECTURE, + name: "架构设计", + systemPrompt: "你正在生成系统架构设计文档。需要清晰展示组件关系、数据流、决策依据。", + markdownStructure: ` +## 摘要 +## 问题背景 +## 设计目标 +## 架构概览(Mermaid) +## 核心组件 +## 数据流 +## 技术选型 +## 风险与权衡 +## 相关概念`, + calloutRules: [ + "架构决策使用 > [!abstract] 决策", + "权衡/Trade-off 使用 > [!warning]", + "备选方案使用 > [!tip]", + ], + mermaidRules: [...COMMON_MERMAID, "架构概览必须生成 Mermaid graph"], + formattingRules: [...COMMON_FORMATTING, "组件描述用表格:| 组件 | 职责 | 技术 |"], + }, + + [ContentType.READING_NOTE]: { + type: ContentType.READING_NOTE, + name: "阅读笔记", + systemPrompt: "你正在生成阅读笔记。帮助读者快速理解章节核心、建立知识连接。", + markdownStructure: ` +## 本章摘要 +## 核心概念 +## 关键论点 +## 与其他章节的关系 +## 我的理解 +## 行动建议`, + calloutRules: [ + "核心观点使用 > [!summary]", + "作者原话/引用使用 > [!quote]", + "个人思考使用 > [!tip] 思考", + ], + mermaidRules: ["章节间关系生成 graph", "概念关系生成 graph"], + formattingRules: [...COMMON_FORMATTING, "引用原文用 > 块引用"], + }, + + [ContentType.PRODUCT_DESIGN]: { + type: ContentType.PRODUCT_DESIGN, + name: "产品设计", + systemPrompt: "你正在生成产品设计文档。需要清晰表达用户需求、方案设计、优先级。", + markdownStructure: ` +## 摘要 +## 用户问题 +## 设计目标 +## 方案设计 +## 用户流程(Mermaid) +## 优先级 +## 风险 +## 指标`, + calloutRules: [ + "用户痛点使用 > [!warning]", + "设计原则使用 > [!abstract]", + "MVP 范围使用 > [!tip]", + ], + mermaidRules: [...COMMON_MERMAID, "用户流程必须生成 flowchart"], + formattingRules: [...COMMON_FORMATTING, "功能列表用表格:| 功能 | 优先级 | 状态 |"], + }, + + [ContentType.CONCEPT]: { + type: ContentType.CONCEPT, + name: "概念页", + systemPrompt: "你正在生成知识库概念页。内容应精确、适合长期存储和反复查阅。", + markdownStructure: ` +## 定义 +## 核心解释 +## 示例 +## 关联概念 +## 关系图(Mermaid) +## 相关问题`, + calloutRules: [ + "常见误解使用 > [!warning] 误区", + "记忆技巧使用 > [!tip]", + ], + mermaidRules: ["关联概念生成 graph LR"], + formattingRules: [...COMMON_FORMATTING], + }, + + [ContentType.QA]: { + type: ContentType.QA, + name: "问答笔记", + systemPrompt: "你正在生成结构化问答笔记。回答应清晰、有层次、便于回顾。", + markdownStructure: ` +## 摘要 +## 回答 +## 关键概念 +## 延伸问题`, + calloutRules: [ + "核心结论使用 > [!summary]", + "注意事项使用 > [!warning]", + ], + mermaidRules: COMMON_MERMAID, + formattingRules: [...COMMON_FORMATTING], + }, + + [ContentType.MEETING_NOTE]: { + type: ContentType.MEETING_NOTE, + name: "会议记录", + systemPrompt: "你正在生成结构化会议记录。突出决策、行动项、责任人。", + markdownStructure: ` +## 会议摘要 +## 讨论要点 +## 决策 +## 行动项 +## 下次议题`, + calloutRules: [ + "决策使用 > [!abstract] 决策", + "待办使用 > [!todo]", + "风险使用 > [!warning]", + ], + mermaidRules: [], + formattingRules: [...COMMON_FORMATTING, "行动项格式:- [ ] 内容 @责任人 📅日期"], + }, + + [ContentType.TASK_PLAN]: { + type: ContentType.TASK_PLAN, + name: "任务规划", + systemPrompt: "你正在生成任务规划文档。需要清晰的目标、步骤、时间线。", + markdownStructure: ` +## 目标 +## 背景 +## 步骤 +## 时间线(Mermaid Gantt) +## 风险 +## 验收标准`, + calloutRules: [ + "里程碑使用 > [!abstract]", + "阻塞风险使用 > [!warning]", + ], + mermaidRules: ["时间规划生成 gantt 图"], + formattingRules: [...COMMON_FORMATTING, "步骤用有序列表"], + }, + + [ContentType.WORLD_BUILDING]: { + type: ContentType.WORLD_BUILDING, + name: "世界观/设定", + systemPrompt: "你正在生成世界观/设定文档。内容应有沉浸感,同时保持结构化便于查阅。", + markdownStructure: ` +## 概述 +## 核心设定 +## 规则/法则 +## 关键实体 +## 关系网络(Mermaid) +## 历史/时间线 +## 开放问题`, + calloutRules: [ + "核心法则使用 > [!abstract] 法则", + "矛盾/冲突使用 > [!warning]", + "灵感使用 > [!tip]", + ], + mermaidRules: ["实体关系生成 graph", "时间线生成 timeline 或 gantt"], + formattingRules: [...COMMON_FORMATTING], + }, + + [ContentType.UNKNOWN]: { + type: ContentType.UNKNOWN, + name: "通用", + systemPrompt: "你正在生成 Obsidian 知识库内容。", + markdownStructure: "", + calloutRules: [], + mermaidRules: COMMON_MERMAID, + formattingRules: COMMON_FORMATTING, + }, +}; diff --git a/src/ai/templates/types.ts b/src/ai/templates/types.ts new file mode 100644 index 0000000..05b44f5 --- /dev/null +++ b/src/ai/templates/types.ts @@ -0,0 +1,37 @@ +/** 内容类型 */ +export enum ContentType { + TECH_DOC = "tech-doc", + PRODUCT_DESIGN = "product-design", + READING_NOTE = "reading-note", + ARCHITECTURE = "architecture", + CONCEPT = "concept", + QA = "qa", + MEETING_NOTE = "meeting-note", + TASK_PLAN = "task-plan", + WORLD_BUILDING = "world-building", + UNKNOWN = "unknown", +} + +/** 输出风格 */ +export type OutputStyle = "minimal" | "technical" | "product" | "academic" | "knowledge-base" | "story" | "dashboard"; + +/** 笔记模板定义 */ +export interface NoteTemplate { + type: ContentType; + name: string; + systemPrompt: string; + markdownStructure: string; + calloutRules: string[]; + mermaidRules: string[]; + formattingRules: string[]; +} + +export const OUTPUT_STYLE_LABELS: Record = { + "minimal": "极简", + "technical": "技术文档", + "product": "产品设计", + "academic": "学术", + "knowledge-base": "知识库", + "story": "世界观/叙事", + "dashboard": "卡片化", +}; diff --git a/src/main.ts b/src/main.ts index b9b363b..0d4ceb1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,6 +11,7 @@ import { ReadingPlanner } from "./ai/ReadingPlanner"; import { NewReadingModal } from "./features/reading/ReadingModal"; import { ReadingProjectManager } from "./features/reading/ReadingProjectManager"; import { SectionAppender } from "./ai/SectionAppender"; +import { MarkdownBeautifier } from "./ai/formatter/MarkdownBeautifier"; import { registerAllActions } from "./actions/registry"; import { ALL_ACTIONS } from "./actions/definitions"; @@ -91,7 +92,9 @@ export default class DeepSeekPlugin extends Plugin { const notice = new Notice("⏳ AI 思考中...", 0); try { - const assistant = new AIAssistant(this.settings); + const knownConcepts = this.getKnownConcepts(); + const style = this.settings.outputStyle ?? "knowledge-base"; + const assistant = new AIAssistant(this.settings, style, knownConcepts); const result = await assistant.run(instruction, ctx); notice.hide(); @@ -179,6 +182,35 @@ export default class DeepSeekPlugin extends Plugin { await workspace.revealLeaf(leaf); } + /** 美化当前文档 */ + async beautifyCurrentNote() { + const editor = this.app.workspace.activeEditor?.editor; + if (!editor) { new Notice("请先打开一个文件"); return; } + + const content = editor.getValue(); + if (!content.trim()) { new Notice("文档为空"); return; } + + const knownConcepts = this.getKnownConcepts(); + const beautifier = new MarkdownBeautifier(knownConcepts); + const beautified = beautifier.beautify(content); + + if (beautified === content) { + new Notice("✅ 文档已经很整洁,无需美化"); + return; + } + + editor.setValue(beautified); + new Notice("✅ 文档已美化"); + } + + /** 获取已知概念列表(用于自动双链) */ + private getKnownConcepts(): string[] { + const conceptsPath = normalizePath(this.settings.conceptsPath || "Knowledge/Concepts"); + return this.app.vault.getMarkdownFiles() + .filter((f) => f.path.startsWith(conceptsPath)) + .map((f) => f.basename); + } + // ── 设置 ─────────────────────────────────────────────────── async loadSettings() { diff --git a/src/settings/SettingsTab.ts b/src/settings/SettingsTab.ts index 781f62f..83bdb27 100644 --- a/src/settings/SettingsTab.ts +++ b/src/settings/SettingsTab.ts @@ -1,5 +1,6 @@ import { App, PluginSettingTab, Setting } from "obsidian"; import type DeepSeekPlugin from "../main"; +import type { DeepSeekSettings } from "../types"; import { BaiduAuthModal } from "../features/sync/BaiduAuthModal"; export class DeepSeekSettingsTab extends PluginSettingTab { @@ -52,6 +53,25 @@ export class DeepSeekSettingsTab extends PluginSettingTab { }) ); + new Setting(containerEl) + .setName("输出风格") + .setDesc("AI 生成内容的默认风格") + .addDropdown((drop) => + drop + .addOption("knowledge-base", "知识库(推荐)") + .addOption("technical", "技术文档") + .addOption("minimal", "极简") + .addOption("product", "产品设计") + .addOption("academic", "学术") + .addOption("story", "世界观/叙事") + .addOption("dashboard", "卡片化") + .setValue(this.plugin.settings.outputStyle) + .onChange(async (value: string) => { + this.plugin.settings.outputStyle = value as DeepSeekSettings["outputStyle"]; + await this.plugin.saveSettings(); + }) + ); + new Setting(containerEl) .setName("笔记保存路径") .setDesc("Q&A 笔记存储目录(相对于 Vault 根目录)") diff --git a/src/types.ts b/src/types.ts index 52b1d3c..1e45312 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,7 @@ export interface DeepSeekSettings { apiKey: string; baseUrl: string; model: "deepseek-v4-flash" | "deepseek-v4-pro"; + outputStyle: "minimal" | "technical" | "product" | "academic" | "knowledge-base" | "story" | "dashboard"; savePath: string; autoOpenGraph: boolean; conceptsPath: string; @@ -41,6 +42,7 @@ export const DEFAULT_SETTINGS: DeepSeekSettings = { apiKey: "", baseUrl: "https://api.deepseek.com", model: "deepseek-v4-flash", + outputStyle: "knowledge-base", savePath: "Knowledge/Q&A", autoOpenGraph: false, conceptsPath: "Knowledge/Concepts", diff --git a/versions.json b/versions.json index df0d90a..d131bde 100644 --- a/versions.json +++ b/versions.json @@ -18,5 +18,6 @@ "1.7.5": "1.4.0", "1.7.6": "1.4.0", "1.7.7": "1.4.0", - "1.7.8": "1.7.2" + "1.7.8": "1.7.2", + "1.8.0": "1.7.2" }