feat: restore Claude Code CLI backend

Add back Claude Code CLI as a backend option alongside API. Claude
Code's `-p --output-format json` is a documented interface that works
well with prompt mode. API remains the default.

Change-Id: I048936a7d59e6768952e0cb007382f75413d1ba0
This commit is contained in:
wujunchen 2026-04-26 10:08:54 +08:00
parent 9e369ae1fa
commit 84095fa0eb
4 changed files with 111 additions and 37 deletions

View file

@ -28,7 +28,16 @@
3. 将三个文件放入该文件夹
4. Obsidian → **设置 → 第三方插件 → 已安装插件** → 启用 **Parallel Reader**
### 配置 Provider
### 选择后端
插件支持两种后端:
| 后端 | 说明 |
|------|------|
| **API / Provider**(默认) | 直连 LLM API支持 20+ provider |
| **Claude Code CLI** | 通过本地 `claude -p` 调用,复用 Claude Code 登录态 |
### API / Provider 模式
在插件设置中选择一个 Provider preset填入 API Key 和模型 ID 即可。
@ -43,6 +52,20 @@
Model ID 支持 `provider/model` 写法(如 `anthropic/claude-sonnet-4-6`),匹配当前 preset 时自动剥离前缀。
### Claude Code CLI 模式
切换后端为 **Claude Code CLI**,插件通过 `claude -p --output-format json` 调用本地 Claude Code。
Obsidian 从 GUI 启动时不继承 shell `PATH`,需要在设置中填写 `claude` 的绝对路径:
```bash
# 查看你的 claude 路径
which claude
# 例如:/Users/you/.claude/local/claude
```
在插件设置的 **CLI 路径** 中填入该路径,点击 **Test** 验证。
## 命令
| 命令 | 说明 |

46
main.js

File diff suppressed because one or more lines are too long

22
main.ts
View file

@ -3,7 +3,7 @@ import { MarkdownView, Notice, Plugin, requestUrl, TFile } from 'obsidian';
import { findLineForAnchor } from './src/anchor';
import { serializeCacheFile, shouldConfirmRegenerate, touchCacheEntry } from './src/cache';
import { activeIndexAfterCardDelete, removeCardAt, updateCardAt } from './src/cards';
import { resolveCliPath } from './src/cli';
import { resolveCliPath, summarizeViaClaudeCode } from './src/cli';
import {
classifyGenerationError,
type GenerationJob,
@ -57,15 +57,19 @@ async function summarizeDocument(
) {
const { system, user } = buildPrompts(content, settings);
let cards: RawCard[];
const useStreaming = supportsStreaming(settings);
const abortController = useStreaming ? new AbortController() : null;
if (abortController) {
job.onCancel(() => abortController.abort());
}
if (useStreaming) {
cards = await summarizeViaApiStreaming(system, user, settings, onStreamProgress, abortController!.signal);
if (settings.backend === 'claude-code') {
cards = await summarizeViaClaudeCode(system, user, settings, job);
} else {
cards = await summarizeViaApi(requestUrl, system, user, settings);
const useStreaming = supportsStreaming(settings);
const abortController = useStreaming ? new AbortController() : null;
if (abortController) {
job.onCancel(() => abortController.abort());
}
if (useStreaming) {
cards = await summarizeViaApiStreaming(system, user, settings, onStreamProgress, abortController!.signal);
} else {
cards = await summarizeViaApi(requestUrl, system, user, settings);
}
}
const resolved: ResolvedCard[] = cards.map((c) => ({
title: c.title,

View file

@ -1,6 +1,7 @@
'use strict';
import { type App, Notice, type Plugin, PluginSettingTab, requestUrl, Setting } from 'obsidian';
import { resolveCliPath, runCli } from './cli';
import { testApiBackend } from './providers';
import {
API_AUTH_TYPES,
@ -17,6 +18,11 @@ import {
import type { PluginHost, PluginSettings } from './types';
async function testBackend(settings: PluginSettings) {
if (settings.backend === 'claude-code') {
const cmd = resolveCliPath('claude', settings.cliPath);
const { stdout } = await runCli(cmd, ['--version'], '', 10000);
return `claude @ ${cmd}\n${stdout.trim()}`;
}
return testApiBackend(requestUrl, settings);
}
@ -48,9 +54,46 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
});
});
containerEl.createEl('h3', { text: tr('apiProviderHeader') });
new Setting(containerEl)
.setName(tr('settingBackendName'))
.setDesc(tr('settingBackendDesc'))
.addDropdown((d) =>
d
.addOption('api', 'API / Provider')
.addOption('claude-code', 'Claude Code CLI')
.setValue(this.plugin.settings.backend)
.onChange(async (v) => {
this.plugin.settings.backend = v;
if (v === 'api' && !this.plugin.settings.apiBaseUrl) {
applyApiProviderPreset(this.plugin.settings, this.plugin.settings.apiProvider || 'anthropic');
}
await this.plugin.saveSettings();
this.display();
}),
);
{
const isCliBacked = this.plugin.settings.backend === 'claude-code';
if (isCliBacked) {
new Setting(containerEl)
.setName(tr('settingCliPathName'))
.setDesc(tr('settingCliPathDesc'))
.addText((t) =>
t
.setPlaceholder(tr('settingCliPathPlaceholder'))
.setValue(this.plugin.settings.cliPath)
.onChange(async (v) => {
this.plugin.settings.cliPath = v.trim();
this.plugin.saveSettingsDebounced();
}),
);
}
if (!isCliBacked) {
containerEl.createEl('h3', { text: tr('apiProviderHeader') });
}
if (!isCliBacked) {
const preset = getApiPreset(this.plugin.settings);
new Setting(containerEl)
.setName(tr('settingProviderPresetName'))
@ -173,13 +216,13 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName(tr('settingModelName'))
.setDesc(tr('settingModelDescApi'))
.setDesc(isCliBacked ? tr('settingModelDescCli') : tr('settingModelDescApi'))
.addText((t) =>
t
.setPlaceholder(getApiPreset(this.plugin.settings).model || 'model-id')
.setPlaceholder(isCliBacked ? DEFAULT_SETTINGS.model : getApiPreset(this.plugin.settings).model || 'model-id')
.setValue(this.plugin.settings.model)
.onChange(async (v) => {
this.plugin.settings.model = v.trim();
this.plugin.settings.model = v.trim() || (isCliBacked ? DEFAULT_SETTINGS.model : '');
this.plugin.saveSettingsDebounced();
}),
);
@ -259,7 +302,7 @@ export class ParallelReaderSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName(tr('settingTestBackendName'))
.setDesc(tr('settingTestBackendDescApi'))
.setDesc(isCliBacked ? tr('settingTestBackendDescCli') : tr('settingTestBackendDescApi'))
.addButton((b) =>
b.setButtonText('Test').onClick(async () => {
try {