feat: rename to Obsidian Note Agent, fix default model display in status bar and indicator

- Plugin id/name updated to obsidian-note-agent (Bin-Home)
- Class renamed ObsidianAgentPlugin → ObsidianNoteAgentPlugin throughout
- VIEW_TYPE and default chatsFolder path updated to match new id
- Status bar and provider indicator button now use activeProfile() so
  the default model name shows instead of "-" when no override is set
- Indicator button separator changed from " / " to ":" to match status bar
- Add README.md and README-cn.md

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
This commit is contained in:
Bin-Home 2026-04-25 17:36:45 +08:00
parent 3c39fdadea
commit ff6940cbba
17 changed files with 207 additions and 32 deletions

View file

@ -11,7 +11,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- Single test: `npx vitest run tests/path/to/file.test.ts` (optionally `-t "test name"`).
- No lint script is wired up, though `eslint` + `eslint-plugin-svelte` are installed; invoke via `npx eslint <path>` if needed.
There is no install-into-vault step in scripts — `main.js`, `manifest.json`, and `styles.css` at the repo root are the plugin artifacts. Symlink or copy the repo into `<vault>/.obsidian/plugins/obsidian-agent/` for local testing.
There is no install-into-vault step in scripts — `main.js`, `manifest.json`, and `styles.css` at the repo root are the plugin artifacts. Symlink or copy the repo into `<vault>/.obsidian/plugins/obsidian-note-agent/` for local testing.
## Architecture
@ -19,7 +19,7 @@ This is an **Obsidian plugin** that adds an agentic chat pane capable of reading
### Layered structure (all under `src/`)
- **`main.ts`** — `ObsidianAgentPlugin` (extends `Plugin`). Wires everything together, owns the single active `Conversation`, exposes `sendMessage()` as an async generator that streams `LoopEvent`s to the UI. Also contains the `commitWrite` / `computeDiff` bridge that turns pending tool calls into real vault mutations after UI approval, and runs `SchedulerService` jobs.
- **`main.ts`** — `ObsidianNoteAgentPlugin` (extends `Plugin`). Wires everything together, owns the single active `Conversation`, exposes `sendMessage()` as an async generator that streams `LoopEvent`s to the UI. Also contains the `commitWrite` / `computeDiff` bridge that turns pending tool calls into real vault mutations after UI approval, and runs `SchedulerService` jobs.
- **`agent/`** — provider-agnostic orchestration.
- `agent-loop.ts` — the core tool-calling loop. For each iteration it assembles `[system, ...conversation.messages]`, passes through `trimHistory(budget)`, streams the provider response (text deltas + tool calls), then dispatches each tool call. Uses a per-iteration `AbortController` linked to both outer cancel and `turnTimeoutMs``AbortError` is translated into a user-facing timeout `LoopEvent`.
- `approval-queue.ts` — gate write tools behind UI approval.

86
README-cn.md Normal file
View file

@ -0,0 +1,86 @@
# Obsidian Note Agent
一款为 [Obsidian](https://obsidian.md) 设计的智能体 AI 助手插件,能够读取并(在您批准后)修改笔记库中的笔记,支持多种 LLM 服务提供商。
[English](README.md)
## 功能特性
- **智能对话** — 多轮对话,支持自主工具调用循环
- **笔记库工具** — 全文搜索、读取笔记、列出文件夹、跟随反链与出链、获取当前笔记和选中文本
- **编辑模式** — 创建、编辑(全量替换或统一补丁)、删除、移动笔记;所有修改以差异对比形式展示,需您审批后方可写入
- **多服务提供商** — OpenAI、Anthropic、DeepSeek、通义千问阿里云、Kimi月之暗面、智谱GLM、MiniMax、OpenRouter、Ollama本地及任意 OpenAI 或 Anthropic 兼容的自定义端点
- **三种模式**
- **问答模式** — 只读,适合问答和研究,不修改笔记库
- **编辑模式** — 完整写入权限,所有修改通过差异对比界面审批后生效
- **定时模式** — 自动后台运行(每日摘要、每周回顾),写入权限受限
- **定时任务** — 自动生成每日摘要和每周回顾,写入到可配置的文件夹
- **自动压缩** — 当对话历史接近模型上下文限制时,自动透明地进行压缩
- **分提供商配置** — 每个提供商独立保存 API Key、基础 URL 和模型名称
- **用户画像** — 可选的个人描述,注入到每次系统提示词中
- **国际化** — 支持英文和简体中文 UI自动跟随 Obsidian 的语言设置
## 安装
### 通过 BRAT推荐适合预发布版本
1. 安装社区插件 [BRAT](https://github.com/TfTHacker/obsidian42-brat)
2. 在 BRAT 设置中添加:`Bin-Home/obsidian-note-agent`
3. 在**设置 → 第三方插件**中启用该插件
### 手动安装
1. 从[最新发布页面](../../releases/latest)下载 `main.js`、`manifest.json` 和 `styles.css`
2. 创建文件夹 `<您的笔记库>/.obsidian/plugins/obsidian-note-agent/`
3. 将三个文件复制到该文件夹
4. 重启 Obsidian在**设置 → 第三方插件**中启用该插件
## 配置
1. 打开**设置 → Obsidian Note Agent**
2. 选择 LLM 服务提供商并输入 API Key
3. 可选:设置自定义基础 URL用于自托管或代理端点及模型名称
4. 选择模式:**问答模式**(只读)或**编辑模式**(可写入)
## 使用方法
- 点击左侧工具栏中的**机器人图标**,或在命令面板中运行 `Open Note Agent`
- 输入消息后按 Enter 发送Shift+Enter 换行)
- 在**编辑模式**下,智能体会以统一差异格式展示笔记修改建议,逐项审批或拒绝后方可写入磁盘
- 使用**新建对话**开始全新会话;历史对话已保存,可通过历史面板随时访问
## 支持的服务提供商
| 提供商 | 说明 |
|---|---|
| OpenAI | GPT-4o、GPT-4o-mini、o1、o3 等 |
| Anthropic | Claude 3.5 / 4 系列 |
| DeepSeek | deepseek-v3、deepseek-r1 |
| 通义千问 | 阿里云 Dashscopeqwen-plus、qwen-max 等) |
| Kimi | 月之暗面moonshot-v1 系列) |
| 智谱 | GLM-4 系列 |
| MiniMax | MiniMax-Text 系列 |
| OpenRouter | 通过 openrouter.ai 访问任意模型 |
| Ollama | 本地模型Llama、Mistral、Qwen 等) |
| 自定义 | 任意 OpenAI 兼容或 Anthropic 兼容端点 |
## 开发
```bash
npm install
npm run dev # esbuild 监听模式,保存即重新构建
npm run build # tsc 类型检查 + 生产构建
npm test # 单元测试Vitest
```
本地测试时,将仓库文件夹软链接或复制到您的笔记库:
```
<笔记库>/.obsidian/plugins/obsidian-note-agent/
```
插件产物为仓库根目录下的 `main.js`、`manifest.json` 和 `styles.css`
## 许可证
MIT

86
README.md Normal file
View file

@ -0,0 +1,86 @@
# Obsidian Note Agent
An agentic AI assistant plugin for [Obsidian](https://obsidian.md) that can read and — with your approval — modify vault notes, powered by your choice of LLM provider.
[中文文档](README-cn.md)
## Features
- **Agentic chat** — multi-turn conversations with an autonomous tool-calling loop
- **Vault tools** — full-text search, read notes, list folders, follow backlinks and outgoing links, get the active note and current selection
- **Edit mode** — create, edit (full replace or unified patch), delete, and move notes; every change is shown as a diff for your review before it is committed
- **Multiple providers** — OpenAI, Anthropic, DeepSeek, Qwen (Alibaba), Kimi (Moonshot), Zhipu (GLM), MiniMax, OpenRouter, Ollama (local), and any custom OpenAI- or Anthropic-compatible endpoint
- **Three modes**
- **Ask** — read-only; for Q&A and research without touching your vault
- **Edit** — full write access; all changes require your approval via a diff UI
- **Scheduled** — automated background runs (daily summary, weekly review) with restricted write access
- **Scheduled tasks** — daily summaries and weekly reviews written automatically to configurable folders
- **Auto-compaction** — conversation history is compacted transparently when approaching the model's context limit
- **Per-provider profiles** — separate API key, base URL, and model saved per provider
- **User profile** — optional personal description injected into every system prompt
- **i18n** — English and Simplified Chinese UI, auto-detected from Obsidian's language setting
## Installation
### Via BRAT (recommended for pre-release)
1. Install the [BRAT](https://github.com/TfTHacker/obsidian42-brat) community plugin
2. In BRAT settings, add: `Bin-Home/obsidian-note-agent`
3. Enable the plugin in **Settings → Community plugins**
### Manual
1. Download `main.js`, `manifest.json`, and `styles.css` from the [latest release](../../releases/latest)
2. Create the folder `<your-vault>/.obsidian/plugins/obsidian-note-agent/`
3. Copy the three files into that folder
4. Reload Obsidian and enable the plugin in **Settings → Community plugins**
## Setup
1. Open **Settings → Obsidian Note Agent**
2. Select your LLM provider and enter your API key
3. Optionally set a custom base URL (for self-hosted or proxy endpoints) and model name
4. Choose a mode: **Ask** for read-only access, **Edit** for write access
## Usage
- Click the **bot icon** in the left ribbon, or run `Open Note Agent` from the command palette
- Type your message and press Enter (or Shift+Enter for a new line)
- In **Edit mode**, the agent proposes note changes as unified diffs — approve or reject each one before it is written to disk
- Use `New Chat` to start a fresh conversation; previous conversations are saved and accessible via the history panel
## Supported Providers
| Provider | Notes |
|---|---|
| OpenAI | GPT-4o, GPT-4o-mini, o1, o3, etc. |
| Anthropic | Claude 3.5 / 4 series |
| DeepSeek | deepseek-v3, deepseek-r1 |
| Qwen | Alibaba Cloud Dashscope (qwen-plus, qwen-max, etc.) |
| Kimi | Moonshot AI (moonshot-v1 series) |
| Zhipu | GLM-4 series |
| MiniMax | MiniMax-Text series |
| OpenRouter | Any model via openrouter.ai |
| Ollama | Local models — Llama, Mistral, Qwen, etc. |
| Custom | Any OpenAI-compatible or Anthropic-compatible endpoint |
## Development
```bash
npm install
npm run dev # esbuild watch mode — rebuilds on save
npm run build # tsc type-check + production bundle
npm test # unit tests (Vitest)
```
For local testing, symlink or copy the repo folder into your vault:
```
<vault>/.obsidian/plugins/obsidian-note-agent/
```
The plugin artifacts are `main.js`, `manifest.json`, and `styles.css` at the repo root.
## License
MIT

View file

@ -1,9 +1,10 @@
{
"id": "obsidian-agent",
"name": "Obsidian Agent",
"id": "obsidian-note-agent",
"name": "Obsidian Note Agent",
"version": "0.1.0",
"minAppVersion": "1.5.0",
"description": "Agentic assistant for personal knowledge management.",
"author": "You",
"description": "Agentic AI assistant for reading and editing vault notes.",
"author": "Bin-Home",
"authorUrl": "https://github.com/Bin-Home",
"isDesktopOnly": false
}

View file

@ -1,5 +1,5 @@
{
"name": "obsidian-agent",
"name": "obsidian-note-agent",
"version": "1.0.0",
"main": "index.js",
"directories": {

View file

@ -15,7 +15,7 @@ import { AgentChatView, VIEW_TYPE_AGENT_CHAT } from "./ui/chat-view";
import { StatusBar } from "./ui/status-bar";
import { applyUnifiedPatch } from "./utils/patch";
export default class ObsidianAgentPlugin extends Plugin {
export default class ObsidianNoteAgentPlugin extends Plugin {
settings!: Settings;
i18n!: I18n;
vault!: VaultService;

View file

@ -56,7 +56,7 @@ export const DEFAULT_SETTINGS: Settings = {
providerId: "openai",
providers: defaultProviders(),
mode: "ask",
chatsFolder: ".obsidian/plugins/obsidian-agent/chats",
chatsFolder: ".obsidian/plugins/obsidian-note-agent/chats",
locale: "auto",
userProfile: "",
historyRetentionDays: 30,

View file

@ -1,6 +1,6 @@
<script lang="ts">
import type ObsidianAgentPlugin from "../main";
export let plugin: ObsidianAgentPlugin;
import type ObsidianNoteAgentPlugin from "../main";
export let plugin: ObsidianNoteAgentPlugin;
let summary = plugin.lastTurnSummary;
plugin.onSummaryChange(s => (summary = s));

View file

@ -3,8 +3,9 @@
import MessageList from "./MessageList.svelte";
import ModeToggle from "./ModeToggle.svelte";
import ConversationList from "./ConversationList.svelte";
import type ObsidianAgentPlugin from "../main";
export let plugin: ObsidianAgentPlugin;
import type ObsidianNoteAgentPlugin from "../main";
import { activeProfile } from "../settings";
export let plugin: ObsidianNoteAgentPlugin;
let input = "";
let busy = false;
@ -92,7 +93,7 @@
setting.openTabById?.(plugin.manifest.id);
}
}
$: providerLabel = (settingsTick, `${plugin.i18n.t(`provider.${plugin.settings.providerId}`)} / ${plugin.settings.providers[plugin.settings.providerId]?.model || ""}`);
$: providerLabel = (settingsTick, `${plugin.i18n.t(`provider.${plugin.settings.providerId}`)}:${activeProfile(plugin.settings).model}`);
$: charCount = input.length;
$: showCharCount = charCount > 500;
</script>

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { onMount, createEventDispatcher } from "svelte";
import type ObsidianAgentPlugin from "../main";
export let plugin: ObsidianAgentPlugin;
import type ObsidianNoteAgentPlugin from "../main";
export let plugin: ObsidianNoteAgentPlugin;
const dispatch = createEventDispatcher<{ select: void; newChat: void }>();

View file

@ -1,8 +1,8 @@
<script lang="ts">
import { Notice } from "obsidian";
import type ObsidianAgentPlugin from "../main";
import type ObsidianNoteAgentPlugin from "../main";
export let p: any;
export let plugin: ObsidianAgentPlugin;
export let plugin: ObsidianNoteAgentPlugin;
const t = (k: string) => plugin.i18n.t(k);

View file

@ -4,12 +4,12 @@
import DiffReviewBlock from "./DiffReviewBlock.svelte";
import ChangeSummary from "./ChangeSummary.svelte";
import { markdown } from "./markdown-action";
import type ObsidianAgentPlugin from "../main";
import type ObsidianNoteAgentPlugin from "../main";
export let messages: any[];
export let streamBuf: string;
export let pending: any[];
export let plugin: ObsidianAgentPlugin;
export let plugin: ObsidianNoteAgentPlugin;
export let busy: boolean = false;
export let compacting: boolean = false;

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { onDestroy } from "svelte";
import type ObsidianAgentPlugin from "../main";
export let plugin: ObsidianAgentPlugin;
import type ObsidianNoteAgentPlugin from "../main";
export let plugin: ObsidianNoteAgentPlugin;
let mode = plugin.settings.mode;
let autoApprove = plugin.settings.autoApprove;

View file

@ -1,11 +1,11 @@
import { App, PluginSettingTab, Setting } from "obsidian";
import type ObsidianAgentPlugin from "../main";
import type ObsidianNoteAgentPlugin from "../main";
import { listProviderIds } from "../providers/registry";
import { PROVIDER_DEFAULTS, defaultProfile } from "../providers/defaults";
import type { ProviderId } from "../types";
export class AgentSettingsTab extends PluginSettingTab {
constructor(app: App, private plugin: ObsidianAgentPlugin) { super(app, plugin); }
constructor(app: App, private plugin: ObsidianNoteAgentPlugin) { super(app, plugin); }
display(): void {
const { containerEl } = this; containerEl.empty();

View file

@ -1,12 +1,12 @@
import { ItemView, WorkspaceLeaf } from "obsidian";
import ChatView from "./ChatView.svelte";
import type ObsidianAgentPlugin from "../main";
import type ObsidianNoteAgentPlugin from "../main";
export const VIEW_TYPE_AGENT_CHAT = "obsidian-agent-chat";
export const VIEW_TYPE_AGENT_CHAT = "obsidian-note-agent-chat";
export class AgentChatView extends ItemView {
private component: ChatView | null = null;
constructor(leaf: WorkspaceLeaf, private plugin: ObsidianAgentPlugin) { super(leaf); }
constructor(leaf: WorkspaceLeaf, private plugin: ObsidianNoteAgentPlugin) { super(leaf); }
getViewType() { return VIEW_TYPE_AGENT_CHAT; }
getDisplayText() { return "Agent"; }
getIcon() { return "bot"; }

View file

@ -1,9 +1,9 @@
import { MarkdownRenderer, Component } from "obsidian";
import type ObsidianAgentPlugin from "../main";
import type ObsidianNoteAgentPlugin from "../main";
export interface MarkdownParams {
text: string;
plugin: ObsidianAgentPlugin;
plugin: ObsidianNoteAgentPlugin;
}
export function markdown(node: HTMLElement, params: MarkdownParams) {

View file

@ -1,7 +1,8 @@
import type ObsidianAgentPlugin from "../main";
import type ObsidianNoteAgentPlugin from "../main";
import { activeProfile } from "../settings";
export class StatusBar {
constructor(private plugin: ObsidianAgentPlugin, private el: HTMLElement) {
constructor(private plugin: ObsidianNoteAgentPlugin, private el: HTMLElement) {
this.render("idle");
}
render(state: "idle" | "thinking" | "waiting" | "compacting") {
@ -10,6 +11,6 @@ export class StatusBar {
: state === "thinking" ? "…"
: state === "compacting" ? "⟳"
: "?";
this.el.setText(`${label} ${s.providerId}:${s.providers[s.providerId]?.model || "-"}`);
this.el.setText(`${label} ${s.providerId}:${activeProfile(s).model}`);
}
}