Fix CORS by removing outdated anthropic-beta header

The anthropic-beta: messages-2023-12-15 header triggered CORS preflight
failures. Removed it (streaming is stable API) and switched non-streaming
chat() to use Obsidian's requestUrl which bypasses CORS entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
svemagie
2026-03-04 21:01:46 +01:00
parent 415e2ebe5d
commit cbb490002c
2 changed files with 74 additions and 72 deletions
+53 -54
View File
@@ -19,14 +19,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
// src/main.ts // src/main.ts
var main_exports = {}; var main_exports = {};
__export(main_exports, { __export(main_exports, {
default: () => VaultChatPlugin default: () => MemexChatPlugin
}); });
module.exports = __toCommonJS(main_exports); module.exports = __toCommonJS(main_exports);
var import_obsidian3 = require("obsidian"); var import_obsidian4 = require("obsidian");
// src/ChatView.ts // src/ChatView.ts
var import_obsidian = require("obsidian"); var import_obsidian = require("obsidian");
var VIEW_TYPE_VAULT_CHAT = "vault-chat-view"; var VIEW_TYPE_MEMEX_CHAT = "memex-chat-view";
var ChatView = class extends import_obsidian.ItemView { var ChatView = class extends import_obsidian.ItemView {
constructor(leaf, plugin) { constructor(leaf, plugin) {
super(leaf); super(leaf);
@@ -39,10 +39,10 @@ var ChatView = class extends import_obsidian.ItemView {
this.renderComponent = new import_obsidian.Component(); this.renderComponent = new import_obsidian.Component();
} }
getViewType() { getViewType() {
return VIEW_TYPE_VAULT_CHAT; return VIEW_TYPE_MEMEX_CHAT;
} }
getDisplayText() { getDisplayText() {
return "Vault Chat"; return "Memex Chat";
} }
getIcon() { getIcon() {
return "message-circle"; return "message-circle";
@@ -67,7 +67,7 @@ var ChatView = class extends import_obsidian.ItemView {
root.empty(); root.empty();
root.addClass("vc-root"); root.addClass("vc-root");
const header = root.createDiv("vc-header"); const header = root.createDiv("vc-header");
header.createEl("span", { text: "Vault Chat", cls: "vc-header-title" }); header.createEl("span", { text: "Memex Chat", cls: "vc-header-title" });
const headerActions = header.createDiv("vc-header-actions"); const headerActions = header.createDiv("vc-header-actions");
const newThreadBtn = headerActions.createEl("button", { cls: "vc-icon-btn", title: "Neuer Thread" }); const newThreadBtn = headerActions.createEl("button", { cls: "vc-icon-btn", title: "Neuer Thread" });
newThreadBtn.innerHTML = `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none"><path d="M12 5v14M5 12h14" stroke-width="2" stroke-linecap="round"/></svg>`; newThreadBtn.innerHTML = `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none"><path d="M12 5v14M5 12h14" stroke-width="2" stroke-linecap="round"/></svg>`;
@@ -645,21 +645,24 @@ var VaultSearch = class {
}; };
// src/ClaudeClient.ts // src/ClaudeClient.ts
var import_obsidian2 = require("obsidian");
var ClaudeClient = class { var ClaudeClient = class {
constructor() { constructor() {
this.baseUrl = "https://api.anthropic.com/v1/messages"; this.baseUrl = "https://api.anthropic.com/v1/messages";
} }
headers(apiKey) {
return {
"content-type": "application/json",
"x-api-key": apiKey,
"anthropic-version": "2023-06-01"
};
}
/** Stream a chat completion, yielding text chunks */ /** Stream a chat completion, yielding text chunks */
async *streamChat(messages, options) { async *streamChat(messages, options) {
var _a, _b, _c, _d, _e, _f; var _a, _b, _c, _d, _e, _f;
const response = await fetch(this.baseUrl, { const response = await fetch(this.baseUrl, {
method: "POST", method: "POST",
headers: { headers: this.headers(options.apiKey),
"content-type": "application/json",
"x-api-key": options.apiKey,
"anthropic-version": "2023-06-01",
"anthropic-beta": "messages-2023-12-15"
},
body: JSON.stringify({ body: JSON.stringify({
model: options.model, model: options.model,
max_tokens: (_a = options.maxTokens) != null ? _a : 2048, max_tokens: (_a = options.maxTokens) != null ? _a : 2048,
@@ -712,34 +715,30 @@ var ClaudeClient = class {
} }
yield { type: "done" }; yield { type: "done" };
} }
/** Non-streaming version for simpler use cases */ /** Non-streaming version — uses Obsidian's requestUrl to bypass CORS */
async chat(messages, options) { async chat(messages, options) {
var _a, _b, _c, _d; var _a, _b, _c, _d;
const response = await fetch(this.baseUrl, { const response = await (0, import_obsidian2.requestUrl)({
url: this.baseUrl,
method: "POST", method: "POST",
headers: { headers: this.headers(options.apiKey),
"content-type": "application/json",
"x-api-key": options.apiKey,
"anthropic-version": "2023-06-01"
},
body: JSON.stringify({ body: JSON.stringify({
model: options.model, model: options.model,
max_tokens: (_a = options.maxTokens) != null ? _a : 2048, max_tokens: (_a = options.maxTokens) != null ? _a : 2048,
system: options.systemPrompt, system: options.systemPrompt,
messages messages
}) }),
throw: false
}); });
if (!response.ok) { if (response.status >= 400) {
const err = await response.text(); throw new Error(`API Error ${response.status}: ${response.text}`);
throw new Error(`API Error ${response.status}: ${err}`);
} }
const data = await response.json(); return (_d = (_c = (_b = response.json.content) == null ? void 0 : _b[0]) == null ? void 0 : _c.text) != null ? _d : "";
return (_d = (_c = (_b = data.content) == null ? void 0 : _b[0]) == null ? void 0 : _c.text) != null ? _d : "";
} }
}; };
// src/SettingsTab.ts // src/SettingsTab.ts
var import_obsidian2 = require("obsidian"); var import_obsidian3 = require("obsidian");
var DEFAULT_SETTINGS = { var DEFAULT_SETTINGS = {
apiKey: "", apiKey: "",
model: "claude-opus-4-5-20251101", model: "claude-opus-4-5-20251101",
@@ -764,7 +763,7 @@ var MODELS = [
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5 (Schnell)" }, { id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5 (Schnell)" },
{ id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5" } { id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5" }
]; ];
var VaultChatSettingsTab = class extends import_obsidian2.PluginSettingTab { var MemexChatSettingsTab = class extends import_obsidian3.PluginSettingTab {
constructor(app, plugin) { constructor(app, plugin) {
super(app, plugin); super(app, plugin);
this.plugin = plugin; this.plugin = plugin;
@@ -772,15 +771,15 @@ var VaultChatSettingsTab = class extends import_obsidian2.PluginSettingTab {
display() { display() {
const { containerEl } = this; const { containerEl } = this;
containerEl.empty(); containerEl.empty();
containerEl.createEl("h2", { text: "Vault Chat Einstellungen" }); containerEl.createEl("h2", { text: "Memex Chat Einstellungen" });
containerEl.createEl("h3", { text: "Claude API" }); containerEl.createEl("h3", { text: "Claude API" });
new import_obsidian2.Setting(containerEl).setName("API Key").setDesc("Dein Anthropic API Key (sk-ant-...)").addText( new import_obsidian3.Setting(containerEl).setName("API Key").setDesc("Dein Anthropic API Key (sk-ant-...)").addText(
(text) => text.setPlaceholder("sk-ant-api03-...").setValue(this.plugin.settings.apiKey).onChange(async (value) => { (text) => text.setPlaceholder("sk-ant-api03-...").setValue(this.plugin.settings.apiKey).onChange(async (value) => {
this.plugin.settings.apiKey = value.trim(); this.plugin.settings.apiKey = value.trim();
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
new import_obsidian2.Setting(containerEl).setName("Modell").setDesc("Welches Claude-Modell verwenden?").addDropdown((drop) => { new import_obsidian3.Setting(containerEl).setName("Modell").setDesc("Welches Claude-Modell verwenden?").addDropdown((drop) => {
for (const m of MODELS) for (const m of MODELS)
drop.addOption(m.id, m.name); drop.addOption(m.id, m.name);
drop.setValue(this.plugin.settings.model).onChange(async (value) => { drop.setValue(this.plugin.settings.model).onChange(async (value) => {
@@ -789,45 +788,45 @@ var VaultChatSettingsTab = class extends import_obsidian2.PluginSettingTab {
}); });
}); });
containerEl.createEl("h3", { text: "Kontext-Einstellungen" }); containerEl.createEl("h3", { text: "Kontext-Einstellungen" });
new import_obsidian2.Setting(containerEl).setName("Max. Kontext-Notizen").setDesc("Wie viele Notizen werden automatisch als Kontext hinzugef\xFCgt? (1\u201315)").addSlider( new import_obsidian3.Setting(containerEl).setName("Max. Kontext-Notizen").setDesc("Wie viele Notizen werden automatisch als Kontext hinzugef\xFCgt? (1\u201315)").addSlider(
(slider) => slider.setLimits(1, 15, 1).setValue(this.plugin.settings.maxContextNotes).setDynamicTooltip().onChange(async (value) => { (slider) => slider.setLimits(1, 15, 1).setValue(this.plugin.settings.maxContextNotes).setDynamicTooltip().onChange(async (value) => {
this.plugin.settings.maxContextNotes = value; this.plugin.settings.maxContextNotes = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
new import_obsidian2.Setting(containerEl).setName("Max. Zeichen pro Notiz").setDesc("Wie viele Zeichen einer Notiz in den Kontext einbezogen werden (1000\u20138000)").addSlider( new import_obsidian3.Setting(containerEl).setName("Max. Zeichen pro Notiz").setDesc("Wie viele Zeichen einer Notiz in den Kontext einbezogen werden (1000\u20138000)").addSlider(
(slider) => slider.setLimits(1e3, 8e3, 500).setValue(this.plugin.settings.maxCharsPerNote).setDynamicTooltip().onChange(async (value) => { (slider) => slider.setLimits(1e3, 8e3, 500).setValue(this.plugin.settings.maxCharsPerNote).setDynamicTooltip().onChange(async (value) => {
this.plugin.settings.maxCharsPerNote = value; this.plugin.settings.maxCharsPerNote = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
new import_obsidian2.Setting(containerEl).setName("Automatischer Kontext-Abruf").setDesc("Beim Senden automatisch relevante Notizen suchen und einbinden").addToggle( new import_obsidian3.Setting(containerEl).setName("Automatischer Kontext-Abruf").setDesc("Beim Senden automatisch relevante Notizen suchen und einbinden").addToggle(
(toggle) => toggle.setValue(this.plugin.settings.autoRetrieveContext).onChange(async (value) => { (toggle) => toggle.setValue(this.plugin.settings.autoRetrieveContext).onChange(async (value) => {
this.plugin.settings.autoRetrieveContext = value; this.plugin.settings.autoRetrieveContext = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
new import_obsidian2.Setting(containerEl).setName("Kontext-Vorschau anzeigen").setDesc("Vor dem Senden zeigen, welche Notizen als Kontext verwendet werden").addToggle( new import_obsidian3.Setting(containerEl).setName("Kontext-Vorschau anzeigen").setDesc("Vor dem Senden zeigen, welche Notizen als Kontext verwendet werden").addToggle(
(toggle) => toggle.setValue(this.plugin.settings.showContextPreview).onChange(async (value) => { (toggle) => toggle.setValue(this.plugin.settings.showContextPreview).onChange(async (value) => {
this.plugin.settings.showContextPreview = value; this.plugin.settings.showContextPreview = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
containerEl.createEl("h3", { text: "Thread-History" }); containerEl.createEl("h3", { text: "Thread-History" });
new import_obsidian2.Setting(containerEl).setName("Threads im Vault speichern").setDesc("Chat-Threads als Markdown-Notizen im Vault ablegen").addToggle( new import_obsidian3.Setting(containerEl).setName("Threads im Vault speichern").setDesc("Chat-Threads als Markdown-Notizen im Vault ablegen").addToggle(
(toggle) => toggle.setValue(this.plugin.settings.saveThreadsToVault).onChange(async (value) => { (toggle) => toggle.setValue(this.plugin.settings.saveThreadsToVault).onChange(async (value) => {
this.plugin.settings.saveThreadsToVault = value; this.plugin.settings.saveThreadsToVault = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
new import_obsidian2.Setting(containerEl).setName("Threads-Ordner").setDesc("Pfad im Vault, wo Chat-Threads gespeichert werden").addText( new import_obsidian3.Setting(containerEl).setName("Threads-Ordner").setDesc("Pfad im Vault, wo Chat-Threads gespeichert werden").addText(
(text) => text.setPlaceholder("Calendar/Chat").setValue(this.plugin.settings.threadsFolder).onChange(async (value) => { (text) => text.setPlaceholder("Calendar/Chat").setValue(this.plugin.settings.threadsFolder).onChange(async (value) => {
this.plugin.settings.threadsFolder = value; this.plugin.settings.threadsFolder = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
containerEl.createEl("h3", { text: "System Prompt" }); containerEl.createEl("h3", { text: "System Prompt" });
new import_obsidian2.Setting(containerEl).setName("System Prompt").setDesc("Instruktionen f\xFCr Claude (wie soll er sich verhalten?)").addTextArea((textarea) => { new import_obsidian3.Setting(containerEl).setName("System Prompt").setDesc("Instruktionen f\xFCr Claude (wie soll er sich verhalten?)").addTextArea((textarea) => {
textarea.setValue(this.plugin.settings.systemPrompt).onChange(async (value) => { textarea.setValue(this.plugin.settings.systemPrompt).onChange(async (value) => {
this.plugin.settings.systemPrompt = value; this.plugin.settings.systemPrompt = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
@@ -838,7 +837,7 @@ var VaultChatSettingsTab = class extends import_obsidian2.PluginSettingTab {
textarea.inputEl.style.fontSize = "12px"; textarea.inputEl.style.fontSize = "12px";
}); });
containerEl.createEl("h3", { text: "Aktionen" }); containerEl.createEl("h3", { text: "Aktionen" });
new import_obsidian2.Setting(containerEl).setName("Index neu aufbauen").setDesc("Vault-Index f\xFCr die Suche neu aufbauen (dauert je nach Vault-Gr\xF6\xDFe einige Sekunden)").addButton( new import_obsidian3.Setting(containerEl).setName("Index neu aufbauen").setDesc("Vault-Index f\xFCr die Suche neu aufbauen (dauert je nach Vault-Gr\xF6\xDFe einige Sekunden)").addButton(
(btn) => btn.setButtonText("Index neu aufbauen").setCta().onClick(async () => { (btn) => btn.setButtonText("Index neu aufbauen").setCta().onClick(async () => {
btn.setButtonText("Indiziere\u2026"); btn.setButtonText("Indiziere\u2026");
btn.setDisabled(true); btn.setDisabled(true);
@@ -854,7 +853,7 @@ var VaultChatSettingsTab = class extends import_obsidian2.PluginSettingTab {
}; };
// src/main.ts // src/main.ts
var VaultChatPlugin = class extends import_obsidian3.Plugin { var MemexChatPlugin = class extends import_obsidian4.Plugin {
async onload() { async onload() {
var _a, _b; var _a, _b;
const loaded = await this.loadData(); const loaded = await this.loadData();
@@ -865,28 +864,28 @@ var VaultChatPlugin = class extends import_obsidian3.Plugin {
this.settings = this.data.settings; this.settings = this.data.settings;
this.search = new VaultSearch(this.app); this.search = new VaultSearch(this.app);
this.claude = new ClaudeClient(); this.claude = new ClaudeClient();
this.registerView(VIEW_TYPE_VAULT_CHAT, (leaf) => new ChatView(leaf, this)); this.registerView(VIEW_TYPE_MEMEX_CHAT, (leaf) => new ChatView(leaf, this));
this.addRibbonIcon("message-circle", "Vault Chat \xF6ffnen", () => { this.addRibbonIcon("message-circle", "Memex Chat \xF6ffnen", () => {
this.activateView(); this.activateView();
}); });
this.addCommand({ this.addCommand({
id: "open-vault-chat", id: "open-memex-chat",
name: "Vault Chat \xF6ffnen", name: "Memex Chat \xF6ffnen",
callback: () => this.activateView() callback: () => this.activateView()
}); });
this.addCommand({ this.addCommand({
id: "vault-chat-rebuild-index", id: "memex-chat-rebuild-index",
name: "Vault Chat: Index neu aufbauen", name: "Memex Chat: Index neu aufbauen",
callback: () => this.rebuildIndex() callback: () => this.rebuildIndex()
}); });
this.addCommand({ this.addCommand({
id: "vault-chat-active-note", id: "memex-chat-active-note",
name: "Vault Chat: Aktive Notiz als Kontext", name: "Memex Chat: Aktive Notiz als Kontext",
callback: () => { callback: () => {
const file = this.app.workspace.getActiveFile(); const file = this.app.workspace.getActiveFile();
if (file) { if (file) {
this.activateView().then(() => { this.activateView().then(() => {
const leaf = this.app.workspace.getLeavesOfType(VIEW_TYPE_VAULT_CHAT)[0]; const leaf = this.app.workspace.getLeavesOfType(VIEW_TYPE_MEMEX_CHAT)[0];
if (leaf) { if (leaf) {
const view = leaf.view; const view = leaf.view;
view.inputEl.value = `Erkl\xE4re und verkn\xFCpfe [[${file.basename}]] mit anderen Konzepten im Vault.`; view.inputEl.value = `Erkl\xE4re und verkn\xFCpfe [[${file.basename}]] mit anderen Konzepten im Vault.`;
@@ -896,19 +895,19 @@ var VaultChatPlugin = class extends import_obsidian3.Plugin {
} }
} }
}); });
this.addSettingTab(new VaultChatSettingsTab(this.app, this)); this.addSettingTab(new MemexChatSettingsTab(this.app, this));
setTimeout(() => { setTimeout(() => {
if (!this.search.isIndexed()) { if (!this.search.isIndexed()) {
this.search.buildIndex().catch(console.error); this.search.buildIndex().catch(console.error);
} }
}, 3e3); }, 3e3);
console.log("[Vault Chat] Plugin geladen"); console.log("[Memex Chat] Plugin geladen");
} }
onunload() { onunload() {
this.app.workspace.detachLeavesOfType(VIEW_TYPE_VAULT_CHAT); this.app.workspace.detachLeavesOfType(VIEW_TYPE_MEMEX_CHAT);
} }
async activateView() { async activateView() {
const existing = this.app.workspace.getLeavesOfType(VIEW_TYPE_VAULT_CHAT); const existing = this.app.workspace.getLeavesOfType(VIEW_TYPE_MEMEX_CHAT);
if (existing.length > 0) { if (existing.length > 0) {
this.app.workspace.revealLeaf(existing[0]); this.app.workspace.revealLeaf(existing[0]);
return; return;
@@ -916,12 +915,12 @@ var VaultChatPlugin = class extends import_obsidian3.Plugin {
const leaf = this.app.workspace.getRightLeaf(false); const leaf = this.app.workspace.getRightLeaf(false);
if (!leaf) if (!leaf)
return; return;
await leaf.setViewState({ type: VIEW_TYPE_VAULT_CHAT, active: true }); await leaf.setViewState({ type: VIEW_TYPE_MEMEX_CHAT, active: true });
this.app.workspace.revealLeaf(leaf); this.app.workspace.revealLeaf(leaf);
} }
async rebuildIndex() { async rebuildIndex() {
var _a; var _a;
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_VAULT_CHAT); const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_MEMEX_CHAT);
const view = (_a = leaves[0]) == null ? void 0 : _a.view; const view = (_a = leaves[0]) == null ? void 0 : _a.view;
this.search.onProgress = (done, total) => { this.search.onProgress = (done, total) => {
if (view && done % 200 === 0) { if (view && done % 200 === 0) {
+21 -18
View File
@@ -1,3 +1,5 @@
import { requestUrl } from "obsidian";
export interface ClaudeMessage { export interface ClaudeMessage {
role: "user" | "assistant"; role: "user" | "assistant";
content: string; content: string;
@@ -20,19 +22,24 @@ export interface ClaudeStreamChunk {
export class ClaudeClient { export class ClaudeClient {
private baseUrl = "https://api.anthropic.com/v1/messages"; private baseUrl = "https://api.anthropic.com/v1/messages";
private headers(apiKey: string): Record<string, string> {
return {
"content-type": "application/json",
"x-api-key": apiKey,
"anthropic-version": "2023-06-01",
};
}
/** Stream a chat completion, yielding text chunks */ /** Stream a chat completion, yielding text chunks */
async *streamChat( async *streamChat(
messages: ClaudeMessage[], messages: ClaudeMessage[],
options: ClaudeOptions options: ClaudeOptions
): AsyncGenerator<ClaudeStreamChunk> { ): AsyncGenerator<ClaudeStreamChunk> {
// Use native fetch for streaming (requestUrl doesn't support streaming).
// The outdated anthropic-beta header is omitted — streaming is stable API.
const response = await fetch(this.baseUrl, { const response = await fetch(this.baseUrl, {
method: "POST", method: "POST",
headers: { headers: this.headers(options.apiKey),
"content-type": "application/json",
"x-api-key": options.apiKey,
"anthropic-version": "2023-06-01",
"anthropic-beta": "messages-2023-12-15",
},
body: JSON.stringify({ body: JSON.stringify({
model: options.model, model: options.model,
max_tokens: options.maxTokens ?? 2048, max_tokens: options.maxTokens ?? 2048,
@@ -90,29 +97,25 @@ export class ClaudeClient {
yield { type: "done" }; yield { type: "done" };
} }
/** Non-streaming version for simpler use cases */ /** Non-streaming version — uses Obsidian's requestUrl to bypass CORS */
async chat(messages: ClaudeMessage[], options: ClaudeOptions): Promise<string> { async chat(messages: ClaudeMessage[], options: ClaudeOptions): Promise<string> {
const response = await fetch(this.baseUrl, { const response = await requestUrl({
url: this.baseUrl,
method: "POST", method: "POST",
headers: { headers: this.headers(options.apiKey),
"content-type": "application/json",
"x-api-key": options.apiKey,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({ body: JSON.stringify({
model: options.model, model: options.model,
max_tokens: options.maxTokens ?? 2048, max_tokens: options.maxTokens ?? 2048,
system: options.systemPrompt, system: options.systemPrompt,
messages, messages,
}), }),
throw: false,
}); });
if (!response.ok) { if (response.status >= 400) {
const err = await response.text(); throw new Error(`API Error ${response.status}: ${response.text}`);
throw new Error(`API Error ${response.status}: ${err}`);
} }
const data = await response.json(); return response.json.content?.[0]?.text ?? "";
return data.content?.[0]?.text ?? "";
} }
} }