mirror of
https://github.com/yan-istart/IStart-Note-AI-Plugin.git
synced 2026-07-22 06:51:37 +00:00
release: 1.8.0
This commit is contained in:
parent
68a5a92400
commit
0e8af720ab
17 changed files with 1091 additions and 374 deletions
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
|
|
@ -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" \
|
||||
|
|
|
|||
416
Readme.md
416
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
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(); },
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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<AssistantResult> {
|
||||
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("有选中内容。如果指令是改写/扩写/翻译,输出应替换选中内容(在第一行写 `<!-- mode:replace -->`)。");
|
||||
parts.push("如果是生成新内容(图表、解释等),输出应插入到选中内容下方(在第一行写 `<!-- mode:insert -->`)。");
|
||||
} else if (ctx.sectionEmpty && ctx.sectionName) {
|
||||
parts.push(`光标在空章节"${ctx.sectionName}"内。输出应填充该章节(在第一行写 \`<!-- mode:insert -->\`)。`);
|
||||
} else if (ctx.cursorLineBefore) {
|
||||
parts.push("无选中内容。输出应插入到光标位置(在第一行写 `<!-- mode:insert -->`)。");
|
||||
} else {
|
||||
parts.push("输出追加到文件末尾(在第一行写 `<!-- mode:append -->`)。");
|
||||
}
|
||||
|
||||
parts.push("如果只是回答问题/解释(不需要修改文件),在第一行写 `<!-- mode:show -->`。");
|
||||
parts.push("\n注意:mode 注释必须是输出的第一行,后面紧跟正文内容。");
|
||||
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
private async callAPI(systemPrompt: string, userPrompt: string): Promise<string> {
|
||||
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*mode:(replace|insert|append|show)\s*-->\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<string, unknown>;
|
||||
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<string, string> = {
|
||||
replace: "替换选中内容",
|
||||
insert: "插入到光标位置",
|
||||
append: "追加到文件末尾",
|
||||
show: "仅展示",
|
||||
};
|
||||
return `${modeLabels[mode]}(${contentType})`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
68
src/ai/classifier/ContentClassifier.ts
Normal file
68
src/ai/classifier/ContentClassifier.ts
Normal file
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
187
src/ai/formatter/MarkdownBeautifier.ts
Normal file
187
src/ai/formatter/MarkdownBeautifier.ts
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
/**
|
||||
* Markdown 美化器
|
||||
* 对 AI 输出(或任何 Markdown 内容)进行后处理,提升可读性。
|
||||
*/
|
||||
export class MarkdownBeautifier {
|
||||
private knownConcepts: Set<string>;
|
||||
|
||||
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(
|
||||
`(?<!\\[\\[)(?<!\\w)${this.escapeRegex(concept)}(?!\\w)(?!\\]\\])`,
|
||||
"g"
|
||||
);
|
||||
|
||||
// 只替换正文中的(跳过代码块和 mermaid)
|
||||
const lines = result.split("\n");
|
||||
let inCodeBlock = false;
|
||||
result = lines.map((line) => {
|
||||
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, "\\$&");
|
||||
}
|
||||
}
|
||||
107
src/ai/formatter/ReadableLayoutEngine.ts
Normal file
107
src/ai/formatter/ReadableLayoutEngine.ts
Normal file
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
104
src/ai/prompt/StructuredPromptBuilder.ts
Normal file
104
src/ai/prompt/StructuredPromptBuilder.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { ContentType, NoteTemplate, OutputStyle } from "../templates/types";
|
||||
import { TEMPLATES } from "../templates/index";
|
||||
import { AssistantContext } from "../AIAssistant";
|
||||
|
||||
/** 风格修饰指令 */
|
||||
const STYLE_MODIFIERS: Record<OutputStyle, string> = {
|
||||
"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");
|
||||
}
|
||||
}
|
||||
217
src/ai/templates/index.ts
Normal file
217
src/ai/templates/index.ts
Normal file
|
|
@ -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, NoteTemplate> = {
|
||||
[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,
|
||||
},
|
||||
};
|
||||
37
src/ai/templates/types.ts
Normal file
37
src/ai/templates/types.ts
Normal file
|
|
@ -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<OutputStyle, string> = {
|
||||
"minimal": "极简",
|
||||
"technical": "技术文档",
|
||||
"product": "产品设计",
|
||||
"academic": "学术",
|
||||
"knowledge-base": "知识库",
|
||||
"story": "世界观/叙事",
|
||||
"dashboard": "卡片化",
|
||||
};
|
||||
34
src/main.ts
34
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() {
|
||||
|
|
|
|||
|
|
@ -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 根目录)")
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue