vault backup: 2026-01-30 10:38:04

This commit is contained in:
windyboy
2026-01-30 10:38:04 +08:00
parent f7aacd5bd4
commit bf57be8355
16 changed files with 465 additions and 164 deletions
+321 -42
View File
@@ -40,18 +40,30 @@ var import_obsidian = require("obsidian");
var defaultSettings = {
serviceUrl: "http://127.0.0.1:4096"
};
var MODELS_CACHE_TTL = 5 * 60 * 1e3;
// src/service.ts
var OpenCodeClient = class {
constructor(serviceUrl) {
var OpenCodeClient = class _OpenCodeClient {
constructor(serviceUrl, defaultModelId, onModelUnavailable) {
this.session = null;
this.cachedProviders = null;
this.providersCacheTime = 0;
this.cachedDefaults = null;
this.serviceUrl = serviceUrl;
this.defaultModelId = defaultModelId;
this.onModelUnavailable = onModelUnavailable;
}
async request(options) {
static {
this.DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
}
static {
this.MESSAGE_REQUEST_TIMEOUT_MS = 6e4;
}
async request(options, timeoutMs = _OpenCodeClient.DEFAULT_REQUEST_TIMEOUT_MS) {
return await Promise.race([
(0, import_obsidian.requestUrl)(options),
new Promise(
(_, reject) => setTimeout(() => reject(new Error("Request timeout after 10 seconds")), 1e4)
(_, reject) => setTimeout(() => reject(new Error(`Request timeout after ${timeoutMs / 1e3} seconds`)), timeoutMs)
)
]);
}
@@ -97,36 +109,79 @@ var OpenCodeClient = class {
if (response.status === 200) {
const data = response.json;
return {
sessionID: data.sessionID,
createTime: data.createTime,
sessionID: data.id,
createTime: data.time.created,
title: data.title
};
} else {
throw new Error(`HTTP ${response.status}: ${response.text}`);
}
if (response.status === 400) {
const details = this.formatBadRequest(response.json, response.text);
throw new Error(details);
}
throw new Error(`HTTP ${response.status}: ${response.text}`);
} catch (error) {
const err = error;
throw new Error(`Failed to create session: ${err.message}`);
}
}
formatBadRequest(payload, fallbackText) {
if (!payload || typeof payload !== "object") {
return `Bad request: ${fallbackText || "Unknown error"}`;
}
const p = payload;
const errText = typeof p.errors === "string" ? p.errors : p.errors ? JSON.stringify(p.errors) : "";
const dataText = p.data ? JSON.stringify(p.data) : "";
const msg = [errText, dataText].filter(Boolean).join(" ");
return msg ? `Bad request: ${msg}` : `Bad request: ${fallbackText || "Unknown error"}`;
}
formatNotFound(payload, fallbackText) {
if (!payload || typeof payload !== "object") {
return fallbackText || "Not found";
}
const p = payload;
const name = typeof p.name === "string" ? p.name : "";
const data = p.data ? JSON.stringify(p.data) : "";
return [name, data].filter(Boolean).join(" ") || fallbackText || "Not found";
}
async sendMessageInternal(sessionID, message) {
const url = `${this.serviceUrl}/session/${sessionID}/message`;
try {
const response = await this.request({
url,
method: "POST",
headers: {
"Content-Type": "application/json"
const response = await this.request(
{
url,
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(message)
},
body: JSON.stringify(message)
});
_OpenCodeClient.MESSAGE_REQUEST_TIMEOUT_MS
);
if (response.status === 200) {
return response.json;
} else if (response.status === 404) {
throw new Error("Session not found");
} else {
throw new Error(`HTTP ${response.status}: ${response.text}`);
const raw = (response.text ?? "").trim();
if (!raw) {
throw new Error("Server returned empty response");
}
let data;
try {
data = JSON.parse(raw);
} catch {
throw new Error(`Invalid JSON response: ${raw.slice(0, 50)}${raw.length > 50 ? "..." : ""}`);
}
if (!data || typeof data !== "object" || !Array.isArray(data.parts)) {
throw new Error("Invalid response shape (expected {info, parts[]})");
}
return data;
}
if (response.status === 400) {
const payload = response.json;
throw new Error(this.formatBadRequest(payload, response.text));
}
if (response.status === 404) {
const payload = response.json;
throw new Error(`Session not found: ${this.formatNotFound(payload, response.text)}`);
}
throw new Error(`HTTP ${response.status}: ${response.text}`);
} catch (error) {
const err = error;
throw new Error(`Failed to send message: ${err.message}`);
@@ -137,16 +192,99 @@ var OpenCodeClient = class {
this.session = await this.createSession();
}
const message = {
parts: [{ text: input, role: "user" }]
parts: [{ type: "text", text: input }]
};
const response = await this.sendMessageInternal(this.session.sessionID, message);
const assistantParts = response.parts.filter((part) => part.role === "assistant");
const assistantText = assistantParts.map((part) => part.text).join("\n");
if (this.defaultModelId) {
const parts = this.defaultModelId.split("/");
if (parts.length === 2) {
message.model = {
providerID: parts[0],
modelID: parts[1]
};
}
}
let response;
try {
response = await this.sendMessageInternal(this.session.sessionID, message);
} catch (error) {
const err = error;
if (message.model && this.isLikelyModelError(err)) {
console.log("[NoteBuddy] Selected model unavailable, falling back to server default");
if (this.onModelUnavailable) {
this.onModelUnavailable();
}
const fallbackMessage = {
parts: message.parts
};
response = await this.sendMessageInternal(this.session.sessionID, fallbackMessage);
} else {
throw error;
}
}
const assistantText = response.parts.filter((part) => part.type === "text").map((part) => part.type === "text" ? part.text : "").filter(Boolean).join("\n");
return assistantText;
}
async sendMessageToSession(sessionID, message) {
return await this.sendMessageInternal(sessionID, message);
}
isModelsCacheValid() {
if (!this.cachedProviders) {
return false;
}
const now = Date.now();
return now - this.providersCacheTime < MODELS_CACHE_TTL;
}
clearModelsCache() {
this.cachedProviders = null;
this.providersCacheTime = 0;
this.cachedDefaults = null;
}
async getCapabilities(forceRefresh = false) {
const now = Date.now();
if (!forceRefresh && this.cachedProviders && now - this.providersCacheTime < MODELS_CACHE_TTL) {
return this.cachedProviders;
}
const url = `${this.serviceUrl}/config/providers`;
try {
const response = await this.request({
url,
method: "GET"
});
if (response.status === 200) {
let data;
try {
data = response.json;
} catch {
const preview = (response.text || "").trim().slice(0, 50);
if (preview.toLowerCase().startsWith("<!")) {
throw new Error(
"Server returned HTML instead of JSON. Check the Service URL and ensure the server exposes /config/providers."
);
}
throw new Error(`Invalid JSON response: ${preview}...`);
}
const raw = data.providers || [];
this.cachedDefaults = data.default || {};
const providers = raw.map((p) => ({
id: p.id,
name: p.name,
models: Object.entries(p.models || {}).map(([id, m]) => ({ id, name: m?.name ?? id }))
}));
this.cachedProviders = providers;
this.providersCacheTime = now;
return providers;
} else {
throw new Error(`HTTP ${response.status}: ${response.text}`);
}
} catch (error) {
const err = error;
throw new Error(`Failed to get capabilities: ${err.message}`);
}
}
isLikelyModelError(err) {
const msg = (err.message || "").toLowerCase();
return msg.includes("model") && (msg.includes("not found") || msg.includes("invalid") || msg.includes("unknown"));
}
};
// src/chat-view.ts
@@ -156,7 +294,11 @@ var ChatView = class extends import_obsidian2.ItemView {
super(leaf);
this.plugin = plugin;
this.messages = [];
this.client = new OpenCodeClient(plugin.settings.serviceUrl);
this.client = new OpenCodeClient(
plugin.settings.serviceUrl,
plugin.settings.defaultModelId,
plugin.onModelUnavailable?.bind(plugin)
);
}
getViewType() {
return VIEW_TYPE_CHAT;
@@ -233,7 +375,7 @@ var ChatView = class extends import_obsidian2.ItemView {
const sendMessage = async () => {
const message = textarea.value.trim();
if (!message) return;
this.messages.push({ text: message, role: "user" });
this.messages.push({ kind: "userText", text: message });
this.renderMessages();
textarea.value = "";
try {
@@ -241,25 +383,32 @@ var ChatView = class extends import_obsidian2.ItemView {
this.plugin.sessionState = await this.client.createSession();
}
const sendData = {
parts: [{ text: message, role: "user" }],
model: {
providerID: "anthropic",
modelID: "claude-3-5-sonnet-20241022"
}
parts: [{ type: "text", text: message }]
};
if (this.plugin.settings.defaultModelId) {
const parts = this.plugin.settings.defaultModelId.split("/");
if (parts.length === 2) {
sendData.model = {
providerID: parts[0],
modelID: parts[1]
};
}
}
let response;
try {
response = await this.client.sendMessageToSession(this.plugin.sessionState.sessionID, sendData);
} catch (sendError) {
const err = sendError;
if (err.message.includes("Session not found") || err.message.includes("404")) {
if (err.message.includes("Session not found")) {
this.plugin.sessionState = await this.client.createSession();
response = await this.client.sendMessageToSession(this.plugin.sessionState.sessionID, sendData);
} else {
throw sendError;
}
}
this.messages.push(...response.parts);
for (const part of response.parts) {
this.messages.push({ kind: "assistantPart", part });
}
this.renderMessages();
this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
} catch (error) {
@@ -292,19 +441,78 @@ var ChatView = class extends import_obsidian2.ItemView {
`;
return;
}
for (const message of this.messages) {
const messageEl = this.messagesContainer.createDiv({
cls: `nb-message nb-message-${message.role}`
});
messageEl.createSpan({ text: message.text });
for (const item of this.messages) {
if (item.kind === "userText") {
const messageEl2 = this.messagesContainer.createDiv({ cls: "nb-message nb-message-user" });
messageEl2.createSpan({ text: item.text });
messageEl2.style.cssText = `
padding: 0.75rem;
background-color: var(--background-modifier-accent);
border-radius: 0.5rem;
font-size: 0.9rem;
align-self: flex-end;
max-width: 70%;
`;
continue;
}
const part = item.part;
const messageEl = this.messagesContainer.createDiv({ cls: "nb-message nb-message-assistant" });
messageEl.style.cssText = `
padding: 0.75rem;
background-color: ${message.role === "user" ? "var(--background-modifier-accent)" : "var(--background-secondary)"};
background-color: var(--background-secondary);
border-radius: 0.5rem;
font-size: 0.9rem;
align-self: ${message.role === "user" ? "flex-end" : "flex-start"};
align-self: flex-start;
max-width: 70%;
`;
switch (part.type) {
case "text":
messageEl.createSpan({ text: part.text });
break;
case "reasoning":
messageEl.createSpan({ text: part.text });
break;
case "tool":
messageEl.createEl("strong", { text: `Tool: ${part.tool}` });
messageEl.createEl("pre", { text: JSON.stringify(part.state ?? {}, null, 2) });
break;
case "patch":
messageEl.createEl("strong", { text: `Patch: ${part.hash}` });
messageEl.createEl("pre", { text: (part.files || []).join("\n") });
break;
case "file":
messageEl.createEl("strong", { text: `File: ${part.filename ?? part.url}` });
messageEl.createEl("div", { text: `mime: ${part.mime}` });
messageEl.createEl("div", { text: `url: ${part.url}` });
break;
case "agent":
messageEl.createEl("strong", { text: `Agent: ${part.name}` });
break;
case "step_start":
messageEl.createEl("strong", { text: `Step start${part.title ? `: ${part.title}` : ""}` });
break;
case "step_finish":
messageEl.createEl("strong", { text: `Step finish${part.title ? `: ${part.title}` : ""}` });
break;
case "snapshot":
messageEl.createEl("strong", { text: "Snapshot" });
messageEl.createEl("pre", { text: JSON.stringify(part, null, 2) });
break;
case "retry":
messageEl.createEl("strong", { text: "Retry" });
messageEl.createEl("pre", { text: JSON.stringify(part, null, 2) });
break;
case "compaction":
messageEl.createEl("strong", { text: "Compaction" });
messageEl.createEl("pre", { text: JSON.stringify(part, null, 2) });
break;
case "unknown":
messageEl.createEl("strong", {
text: `Unsupported part type: ${part.originalType}`
});
messageEl.createEl("pre", { text: JSON.stringify(part, null, 2) });
break;
}
}
}
};
@@ -314,12 +522,20 @@ var import_obsidian3 = require("obsidian");
var NoteBuddySettingTab = class extends import_obsidian3.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.providers = [];
this.isLoadingModels = false;
this.plugin = plugin;
}
display() {
async display(useCachedProviders) {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("h2", { text: "NoteBuddy Settings" });
if (!useCachedProviders) {
try {
await this.loadProviders();
} catch {
}
}
new import_obsidian3.Setting(containerEl).setName("Service URL").setDesc("The URL of the OpenCode service (must include protocol, hostname, and port)").addText(
(text) => text.setPlaceholder("http://127.0.0.1:4096").setValue(this.plugin.settings.serviceUrl).onChange(async (value) => {
if (!value.startsWith("http://") && !value.startsWith("https://")) {
@@ -346,7 +562,7 @@ var NoteBuddySettingTab = class extends import_obsidian3.PluginSettingTab {
);
new import_obsidian3.Setting(containerEl).setName("Test Connection").setDesc("Test the connection to the service").addButton(
(button) => button.setButtonText("Test").setCta().onClick(async () => {
const client = new OpenCodeClient(this.plugin.settings.serviceUrl);
const client = new OpenCodeClient(this.plugin.settings.serviceUrl, this.plugin.settings.defaultModelId);
const result = await client.healthCheck();
if (result.status === "connected") {
new import_obsidian3.Notice("Connection successful!");
@@ -355,6 +571,65 @@ var NoteBuddySettingTab = class extends import_obsidian3.PluginSettingTab {
}
})
);
this.displayModelSelection(containerEl);
this.displayRefreshButton(containerEl);
}
async loadProviders(client, forceRefresh) {
if (this.isLoadingModels) {
return;
}
this.isLoadingModels = true;
try {
const c = client ?? new OpenCodeClient(this.plugin.settings.serviceUrl, this.plugin.settings.defaultModelId);
this.providers = await c.getCapabilities(forceRefresh ?? false);
} catch (error) {
this.providers = [];
console.error("[NoteBuddy] Failed to load providers:", error);
throw error;
} finally {
this.isLoadingModels = false;
}
}
displayModelSelection(containerEl) {
const modelOptions = {};
modelOptions[""] = "Use server default";
for (const provider of this.providers) {
for (const model of provider.models) {
const value = `${provider.id}/${model.id}`;
modelOptions[value] = `${provider.name} - ${model.name}`;
}
}
const currentValue = this.getCurrentModelSelection();
new import_obsidian3.Setting(containerEl).setName("Default Model").setDesc("Select the default AI model to use for conversations").addDropdown(
(dropdown) => dropdown.addOptions(modelOptions).setValue(currentValue).onChange(async (value) => {
if (value) {
this.plugin.settings.defaultModelId = value;
} else {
this.plugin.settings.defaultModelId = void 0;
}
await this.plugin.saveSettings();
})
);
}
getCurrentModelSelection() {
return this.plugin.settings.defaultModelId || "";
}
displayRefreshButton(container) {
new import_obsidian3.Setting(container).setName("Refresh Models").setDesc("Reload the list of available models from the service").addButton(
(button) => button.setButtonText("Refresh").onClick(async () => {
const client = new OpenCodeClient(this.plugin.settings.serviceUrl, this.plugin.settings.defaultModelId);
client.clearModelsCache();
try {
await this.loadProviders(client, true);
new import_obsidian3.Notice("Models refreshed");
await this.display(true);
} catch (error) {
console.error("[NoteBuddy] Failed to load providers:", error);
new import_obsidian3.Notice(`Failed to refresh models: ${error.message}`);
await this.display(true);
}
})
);
}
};
@@ -407,6 +682,10 @@ var NoteBuddyPlugin = class extends import_obsidian4.Plugin {
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
// Callback for when selected model is unavailable
onModelUnavailable() {
new import_obsidian4.Notice("Selected model is unavailable. Using server default instead.");
}
async saveSettings() {
await this.saveData(this.settings);
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -5,7 +5,7 @@
"isDesktopOnly": false,
"js": "main.js",
"fundingUrl": "https://ko-fi.com/vinzent",
"version": "1.46.0",
"version": "1.46.1",
"author": "Vinzent",
"authorUrl": "https://github.com/Vinzent03"
}
+1 -1
View File
@@ -123,7 +123,7 @@
"mdBorderColor": "Black",
"mdCSS": "",
"scriptEngineSettings": {},
"previousRelease": "2.19.2",
"previousRelease": "2.20.0",
"showReleaseNotes": true,
"compareManifestToPluginVersion": true,
"showNewVersionNotification": true,
File diff suppressed because one or more lines are too long
+11 -11
View File
@@ -1,12 +1,12 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.19.2",
"minAppVersion": "1.5.7",
"description": "Sketch Your Mind. An Obsidian plugin to edit and view Excalidraw drawings. Enter the world of 4D Visual PKM.",
"author": "Zsolt Viczian",
"authorUrl": "https://excalidraw-obsidian.online",
"fundingUrl": "https://ko-fi.com/zsolt",
"helpUrl": "https://github.com/zsviczian/obsidian-excalidraw-plugin#readme",
"isDesktopOnly": false
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.20.0",
"minAppVersion": "1.5.7",
"description": "Sketch Your Mind. An Obsidian plugin to edit and view Excalidraw drawings. Enter the world of 4D Visual PKM.",
"author": "Zsolt Viczian",
"authorUrl": "https://excalidraw-obsidian.online",
"fundingUrl": "https://ko-fi.com/zsolt",
"helpUrl": "https://github.com/zsviczian/obsidian-excalidraw-plugin#readme",
"isDesktopOnly": false
}
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -65,7 +65,9 @@
"generalSettings": {},
"headingOpened": {
"核心状态": true,
"自定义状态": true
"自定义状态": true,
"Core Statuses": true,
"Custom Statuses": true
},
"debugSettings": {
"ignoreSortInstructions": false,
+17 -2
View File
@@ -1,10 +1,12 @@
{
"choices": [],
"inputPrompt": "single-line",
"persistInputPromptDrafts": true,
"useSelectionAsCaptureValue": true,
"devMode": false,
"templateFolderPath": "06_Metadata/Templates",
"announceUpdates": "all",
"version": "2.9.4",
"version": "2.10.0",
"globalVariables": {},
"onePageInputEnabled": false,
"disableOnlineFeatures": true,
@@ -12,6 +14,17 @@
"showCaptureNotification": true,
"showInputCancellationNotification": false,
"enableTemplatePropertyTypes": true,
"dateAliases": {
"t": "today",
"tm": "tomorrow",
"yd": "yesterday",
"nw": "next week",
"nm": "next month",
"ny": "next year",
"lw": "last week",
"lm": "last month",
"ly": "last year"
},
"ai": {
"defaultModel": "Ask me",
"defaultSystemPrompt": "As an AI assistant within Obsidian, your primary goal is to help users manage their ideas and knowledge more effectively. Format your responses using Markdown syntax. Please use the [[Obsidian]] link format. You can write aliases for the links by writing [[Obsidian|the alias after the pipe symbol]]. To use mathematical notation, use LaTeX syntax. LaTeX syntax for larger equations should be on separate lines, surrounded with double dollar signs ($$). You can also inline math expressions by wrapping it in $ symbols. For example, use $$w_{ij}^{\text{new}}:=w_{ij}^{\text{current}}+etacdotdelta_jcdot x_{ij}$$ on a separate line, but you can write \"($eta$ = learning rate, $delta_j$ = error term, $x_{ij}$ = input)\" inline.",
@@ -99,6 +112,8 @@
"addDefaultAIProviders": true,
"removeMacroIndirection": true,
"migrateFileOpeningSettings": true,
"setProviderModelDiscoveryMode": true
"setProviderModelDiscoveryMode": true,
"backfillFileOpeningDefaults": true,
"migrateProviderApiKeysToSecretStorage": true
}
}
+78 -73
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,8 +1,8 @@
{
"id": "quickadd",
"name": "QuickAdd",
"version": "2.9.4",
"minAppVersion": "1.6.0",
"version": "2.10.0",
"minAppVersion": "1.11.4",
"description": "Quickly add new pages or content to your vault.",
"author": "Christian B. B. Houmann",
"authorUrl": "https://bagerbach.com",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "templater-obsidian",
"name": "Templater",
"version": "2.17.0",
"version": "2.18.1",
"description": "Create and use templates",
"minAppVersion": "1.5.0",
"author": "SilentVoid",