release: 1.8.0

This commit is contained in:
yan 2026-05-16 23:48:46 +08:00
parent 68a5a92400
commit 0e8af720ab
17 changed files with 1091 additions and 374 deletions

View file

@ -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
View file

@ -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
---

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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(); },
},
];

View file

@ -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}`;
}
}

View 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()));
}
}

View 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, "\\$&");
}
}

View 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;
}
/**
*
* H3H4+ 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");
}
}

View 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
View 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
View 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": "卡片化",
};

View file

@ -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() {

View file

@ -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 根目录)")

View file

@ -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",

View file

@ -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"
}